implement ntfy notification

This commit is contained in:
yusing 2025-01-31 15:49:02 +08:00
parent 6ae391a3c9
commit c00395196f
12 changed files with 143 additions and 15 deletions

View file

@ -1,6 +1,7 @@
package notif
import (
"io"
"net/http"
"net/url"
"strings"
@ -54,3 +55,15 @@ func (base *ProviderBase) GetMethod() string {
func (base *ProviderBase) GetMIMEType() string {
return "application/json"
}
func (base *ProviderBase) SetHeaders(logMsg *LogMessage, headers http.Header) {
// no-op by default
}
func (base *ProviderBase) makeRespError(resp *http.Response) error {
body, err := io.ReadAll(resp.Body)
if err == nil {
return E.Errorf("%s status %d: %s", base.Name, resp.StatusCode, body)
}
return E.Errorf("%s status %d", base.Name, resp.StatusCode)
}

View file

@ -5,9 +5,9 @@ import "fmt"
type Color uint
const (
Red Color = 0xff0000
Green Color = 0x00ff00
Blue Color = 0x0000ff
ColorError Color = 0xff0000
ColorSuccess Color = 0x00ff00
ColorInfo Color = 0x0000ff
)
func (c Color) HexString() string {

View file

@ -38,6 +38,8 @@ func (cfg *NotificationConfig) UnmarshalMap(m map[string]any) (err E.Error) {
cfg.Provider = &Webhook{}
case ProviderGotify:
cfg.Provider = &GotifyClient{}
case ProviderNtfy:
cfg.Provider = &Ntfy{}
default:
return ErrUnknownNotifProvider.
Subject(cfg.ProviderName).

View file

@ -61,7 +61,7 @@ func (client *GotifyClient) makeRespError(resp *http.Response) error {
var errm model.Error
err := json.NewDecoder(resp.Body).Decode(&errm)
if err != nil {
return fmt.Errorf(ProviderGotify+" status %d, but failed to decode err response: %w", resp.StatusCode, err)
return fmt.Errorf("%s status %d, but failed to decode err response: %w", client.Name, resp.StatusCode, err)
}
return fmt.Errorf(ProviderGotify+" status %d %s: %s", resp.StatusCode, errm.Error, errm.ErrorDescription)
return fmt.Errorf("%s status %d %s: %s", client.Name, resp.StatusCode, errm.Error, errm.ErrorDescription)
}

89
internal/notif/ntfy.go Normal file
View file

@ -0,0 +1,89 @@
package notif
import (
"bytes"
"io"
"net/http"
"strings"
"github.com/rs/zerolog"
E "github.com/yusing/go-proxy/internal/error"
)
// See https://docs.ntfy.sh/publish
type Ntfy struct {
ProviderBase
Topic string `json:"topic"`
Style NtfyStyle `json:"style"`
}
type NtfyStyle string
const (
NtfyStyleMarkdown NtfyStyle = "markdown"
NtfyStylePlain NtfyStyle = "plain"
)
func (n *Ntfy) Validate() E.Error {
if n.URL == "" {
return E.New("url is required")
}
if n.Topic == "" {
return E.New("topic is required")
}
if n.Topic[0] == '/' {
return E.New("topic should not start with a slash")
}
switch n.Style {
case "":
n.Style = NtfyStyleMarkdown
case NtfyStyleMarkdown, NtfyStylePlain:
default:
return E.Errorf("invalid style, expecting %q or %q, got %q", NtfyStyleMarkdown, NtfyStylePlain, n.Style)
}
return nil
}
func (n *Ntfy) GetURL() string {
if n.URL[len(n.URL)-1] == '/' {
return n.URL + n.Topic
}
return n.URL + "/" + n.Topic
}
func (n *Ntfy) GetMIMEType() string {
return ""
}
func (n *Ntfy) GetToken() string {
return n.Token
}
func (n *Ntfy) MakeBody(logMsg *LogMessage) (io.Reader, error) {
switch n.Style {
case NtfyStyleMarkdown:
return strings.NewReader(formatMarkdown(logMsg.Extras)), nil
default:
return &bytes.Buffer{}, nil
}
}
func (n *Ntfy) SetHeaders(logMsg *LogMessage, headers http.Header) {
headers.Set("Title", logMsg.Title)
switch logMsg.Level {
// warning (or other unspecified) uses default priority
case zerolog.FatalLevel:
headers.Set("Priority", "urgent")
case zerolog.ErrorLevel:
headers.Set("Priority", "high")
case zerolog.InfoLevel:
headers.Set("Priority", "low")
case zerolog.DebugLevel:
headers.Set("Priority", "min")
}
if n.Style == NtfyStyleMarkdown {
headers.Set("Markdown", "yes")
}
}

View file

@ -21,6 +21,7 @@ type (
GetMIMEType() string
MakeBody(logMsg *LogMessage) (io.Reader, error)
SetHeaders(logMsg *LogMessage, headers http.Header)
makeRespError(resp *http.Response) error
}
@ -30,6 +31,7 @@ type (
const (
ProviderGotify = "gotify"
ProviderNtfy = "ntfy"
ProviderWebhook = "webhook"
)
@ -52,6 +54,7 @@ func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) err
if provider.GetToken() != "" {
req.Header.Set("Authorization", "Bearer "+provider.GetToken())
}
provider.SetHeaders(msg, req.Header)
resp, err := http.DefaultClient.Do(req)
if err != nil {

View file

@ -92,12 +92,12 @@ func (webhook *Webhook) GetMIMEType() string {
func (webhook *Webhook) makeRespError(resp *http.Response) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("webhook status %d, failed to read body: %w", resp.StatusCode, err)
return fmt.Errorf("%s status %d, failed to read body: %w", webhook.Name, resp.StatusCode, err)
}
if len(body) > 0 {
return fmt.Errorf("webhook status %d: %s", resp.StatusCode, body)
return fmt.Errorf("%s status %d: %s", webhook.Name, resp.StatusCode, body)
}
return fmt.Errorf("webhook status %d", resp.StatusCode)
return fmt.Errorf("%s status %d", webhook.Name, resp.StatusCode)
}
func (webhook *Webhook) MakeBody(logMsg *LogMessage) (io.Reader, error) {

View file

@ -217,14 +217,14 @@ func (mon *monitor) checkUpdateHealth() error {
notif.Notify(&notif.LogMessage{
Title: "✅ Service is up ✅",
Extras: extras,
Color: notif.Green,
Color: notif.ColorSuccess,
})
} else {
logger.Warn().Msg("service went down")
notif.Notify(&notif.LogMessage{
Title: "❌ Service went down ❌",
Extras: extras,
Color: notif.Red,
Color: notif.ColorError,
})
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
import { URL } from "../types";
export declare const NOTIFICATION_PROVIDERS: readonly ["webhook", "gotify"];
export declare const NOTIFICATION_PROVIDERS: readonly ["webhook", "gotify", "ntfy"];
export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number];
export type NotificationConfig = {
name: string;
@ -9,9 +9,17 @@ export interface GotifyConfig extends NotificationConfig {
provider: "gotify";
token: string;
}
export declare const NTFY_MSG_STYLES: string[];
export type NtfyStyle = (typeof NTFY_MSG_STYLES)[number];
export interface NtfyConfig extends NotificationConfig {
provider: "ntfy";
topic: string;
token?: string;
style?: NtfyStyle;
}
export declare const WEBHOOK_TEMPLATES: readonly ["", "discord"];
export declare const WEBHOOK_METHODS: readonly ["POST", "GET", "PUT"];
export declare const WEBHOOK_MIME_TYPES: readonly ["application/json", "application/x-www-form-urlencoded", "text/plain"];
export declare const WEBHOOK_MIME_TYPES: readonly ["application/json", "application/x-www-form-urlencoded", "text/plain", "text/markdown"];
export declare const WEBHOOK_COLOR_MODES: readonly ["hex", "dec"];
export type WebhookTemplate = (typeof WEBHOOK_TEMPLATES)[number];
export type WebhookMethod = (typeof WEBHOOK_METHODS)[number];

View file

@ -1,9 +1,11 @@
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify"];
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify", "ntfy"];
export const NTFY_MSG_STYLES = ["markdown", "plain"];
export const WEBHOOK_TEMPLATES = ["", "discord"];
export const WEBHOOK_METHODS = ["POST", "GET", "PUT"];
export const WEBHOOK_MIME_TYPES = [
"application/json",
"application/x-www-form-urlencoded",
"text/plain",
"text/markdown",
];
export const WEBHOOK_COLOR_MODES = ["hex", "dec"];

View file

@ -1,6 +1,6 @@
import { URL } from "../types";
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify"] as const;
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify", "ntfy"] as const;
export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number];
@ -17,12 +17,23 @@ export interface GotifyConfig extends NotificationConfig {
token: string;
}
export const NTFY_MSG_STYLES = ["markdown", "plain"];
export type NtfyStyle = (typeof NTFY_MSG_STYLES)[number];
export interface NtfyConfig extends NotificationConfig {
provider: "ntfy";
topic: string;
token?: string;
style?: NtfyStyle;
}
export const WEBHOOK_TEMPLATES = ["", "discord"] as const;
export const WEBHOOK_METHODS = ["POST", "GET", "PUT"] as const;
export const WEBHOOK_MIME_TYPES = [
"application/json",
"application/x-www-form-urlencoded",
"text/plain",
"text/markdown",
] as const;
export const WEBHOOK_COLOR_MODES = ["hex", "dec"] as const;