From 00ec2ce33ef077c0e1613feb8a377579b1fa8284 Mon Sep 17 00:00:00 2001 From: catfishlty Date: Tue, 1 Apr 2025 10:53:41 +0800 Subject: [PATCH] feat(notify): add gotify --- internal/domain/notify.go | 1 + internal/notify/providers.go | 8 ++ .../core/notifier/providers/gotify/gotify.go | 106 ++++++++++++++++++ .../notifier/providers/gotify/gotify_test.go | 68 +++++++++++ .../notification/NotifyChannelEditForm.tsx | 3 + .../NotifyChannelEditFormGotifyFields.tsx | 46 ++++++++ ui/src/domain/settings.ts | 9 ++ ui/src/i18n/locales/en/nls.common.json | 1 + ui/src/i18n/locales/en/nls.settings.json | 10 ++ ui/src/i18n/locales/zh/nls.common.json | 1 + ui/src/i18n/locales/zh/nls.settings.json | 10 ++ 11 files changed, 263 insertions(+) create mode 100644 internal/pkg/core/notifier/providers/gotify/gotify.go create mode 100644 internal/pkg/core/notifier/providers/gotify/gotify_test.go create mode 100644 ui/src/components/notification/NotifyChannelEditFormGotifyFields.tsx diff --git a/internal/domain/notify.go b/internal/domain/notify.go index 3d71a3a7..070aed43 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -12,6 +12,7 @@ const ( NotifyChannelTypeBark = NotifyChannelType("bark") NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk") NotifyChannelTypeEmail = NotifyChannelType("email") + NotifyChannelTypeGotify = NotifyChannelType("gotify") NotifyChannelTypeLark = NotifyChannelType("lark") NotifyChannelTypeServerChan = NotifyChannelType("serverchan") NotifyChannelTypeTelegram = NotifyChannelType("telegram") diff --git a/internal/notify/providers.go b/internal/notify/providers.go index 66927390..d97cb842 100644 --- a/internal/notify/providers.go +++ b/internal/notify/providers.go @@ -8,6 +8,7 @@ import ( pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark" pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk" pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email" + pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify" pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark" pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan" pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram" @@ -45,6 +46,13 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a ReceiverAddress: maputil.GetString(channelConfig, "receiverAddress"), }) + case domain.NotifyChannelTypeGotify: + return pGotify.NewNotifier(&pGotify.NotifierConfig{ + Url: maputil.GetString(channelConfig, "url"), + Token: maputil.GetString(channelConfig, "token"), + Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1), + }) + case domain.NotifyChannelTypeLark: return pLark.NewNotifier(&pLark.NotifierConfig{ WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"), diff --git a/internal/pkg/core/notifier/providers/gotify/gotify.go b/internal/pkg/core/notifier/providers/gotify/gotify.go new file mode 100644 index 00000000..1ae7af76 --- /dev/null +++ b/internal/pkg/core/notifier/providers/gotify/gotify.go @@ -0,0 +1,106 @@ +package gotify + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + + "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/notifier" +) + +type NotifierConfig struct { + // Gotify 服务地址 + // 示例:https://gotify.example.com + Url string `json:"url"` + // Gotify Token + Token string `json:"token"` + // Gotify 消息优先级 + Priority int64 `json:"priority"` +} + +// Message Gotify 消息体 +type Message struct { + Title string `json:"title"` + Message string `json:"message"` + Priority int64 `json:"priority"` +} + +type NotifierProvider struct { + config *NotifierConfig + logger *slog.Logger + // 未来将移除 + httpClient *http.Client +} + +var _ notifier.Notifier = (*NotifierProvider)(nil) + +func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { + if config == nil { + panic("config is nil") + } + + return &NotifierProvider{ + config: config, + httpClient: http.DefaultClient, + }, nil +} + +func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { + if logger == nil { + n.logger = slog.Default() + } else { + n.logger = logger + } + return n +} + +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { + // Gotify 原生实现, notify 库没有实现, 等待合并 + reqBody := &Message{ + Title: subject, + Message: message, + Priority: n.config.Priority, + } + + // Make request + body, err := json.Marshal(reqBody) + if err != nil { + return nil, errors.Wrap(err, "encode message body") + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + fmt.Sprintf("%s/message", n.config.Url), + bytes.NewReader(body), + ) + if err != nil { + return nil, errors.Wrap(err, "create new request") + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", n.config.Token)) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + // Send request to gotify service + resp, err := n.httpClient.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "send request to gotify server") + } + + // Read response and verify success + result, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "read response") + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("gotify returned status code %d: %s", resp.StatusCode, string(result)) + } + return ¬ifier.NotifyResult{}, nil +} diff --git a/internal/pkg/core/notifier/providers/gotify/gotify_test.go b/internal/pkg/core/notifier/providers/gotify/gotify_test.go new file mode 100644 index 00000000..31ad64af --- /dev/null +++ b/internal/pkg/core/notifier/providers/gotify/gotify_test.go @@ -0,0 +1,68 @@ +package gotify_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify" +) + +const ( + mockSubject = "test_subject" + mockMessage = "test_message" +) + +var ( + fUrl string + fToken string + fPriority int64 +) + +func init() { + argsPrefix := "CERTIMATE_NOTIFIER_GOTIFY_" + flag.StringVar(&fUrl, argsPrefix+"URL", "", "") + flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "") + flag.Int64Var(&fPriority, argsPrefix+"PRIORITY", 0, "") +} + +/* +Shell command to run this test: + + go test -v ./gotify_test.go -args \ + --CERTIMATE_NOTIFIER_GOTIFY_URL="https://example.com" \ + --CERTIMATE_NOTIFIER_GOTIFY_TOKEN="your-gotify-application-token" \ + --CERTIMATE_NOTIFIER_GOTIFY_PRIORITY="your-message-priority" +*/ +func TestNotify(t *testing.T) { + flag.Parse() + + t.Run("Notify", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("URL: %v", fUrl), + fmt.Sprintf("TOKEN: %v", fToken), + fmt.Sprintf("PRIORITY: %d", fPriority), + }, "\n")) + + notifier, err := provider.NewNotifier(&provider.NotifierConfig{ + Url: fUrl, + Token: fToken, + Priority: fPriority, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + res, err := notifier.Notify(context.Background(), mockSubject, mockMessage) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx index d818ee4c..98352771 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -7,6 +7,7 @@ import { useAntdForm } from "@/hooks"; import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields"; +import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx"; import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields"; import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields"; import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; @@ -48,6 +49,8 @@ const NotifyChannelEditForm = forwardRef; case NOTIFY_CHANNELS.EMAIL: return ; + case NOTIFY_CHANNELS.GOTIFY: + return ; case NOTIFY_CHANNELS.LARK: return ; case NOTIFY_CHANNELS.SERVERCHAN: diff --git a/ui/src/components/notification/NotifyChannelEditFormGotifyFields.tsx b/ui/src/components/notification/NotifyChannelEditFormGotifyFields.tsx new file mode 100644 index 00000000..189f4e16 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormGotifyFields.tsx @@ -0,0 +1,46 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormGotifyFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + url: z.string({ message: t("settings.notification.channel.form.gotify_url.placeholder") }).url({ message: t("common.errmsg.url_invalid") }), + token: z.string({ message: t("settings.notification.channel.form.gotify_token.placeholder") }), + priority: z.preprocess(val => Number(val), z.number({ message: t("settings.notification.channel.form.gotify_priority.placeholder") }).gte(0, { message: t("settings.notification.channel.form.gotify_priority.error.gte0") })), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + } + > + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormGotifyFields; diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index e34105e0..df09f02d 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -40,6 +40,7 @@ export const NOTIFY_CHANNELS = Object.freeze({ BARK: "bark", DINGTALK: "dingtalk", EMAIL: "email", + GOTIFY: "gotify", LARK: "lark", SERVERCHAN: "serverchan", TELEGRAM: "telegram", @@ -58,6 +59,7 @@ export type NotifyChannelsSettingsContent = { [NOTIFY_CHANNELS.BARK]?: BarkNotifyChannelConfig; [NOTIFY_CHANNELS.DINGTALK]?: DingTalkNotifyChannelConfig; [NOTIFY_CHANNELS.EMAIL]?: EmailNotifyChannelConfig; + [NOTIFY_CHANNELS.GOTIFY]?: GotifyNotifyChannelConfig; [NOTIFY_CHANNELS.LARK]?: LarkNotifyChannelConfig; [NOTIFY_CHANNELS.SERVERCHAN]?: ServerChanNotifyChannelConfig; [NOTIFY_CHANNELS.TELEGRAM]?: TelegramNotifyChannelConfig; @@ -88,6 +90,13 @@ export type DingTalkNotifyChannelConfig = { enabled?: boolean; }; +export type GotifyNotifyChannelConfig = { + url: string; + token: string; + priority: string; + enabled?: boolean; +}; + export type LarkNotifyChannelConfig = { webhookUrl: string; enabled?: boolean; diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index c9e671ce..13097bb9 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -38,6 +38,7 @@ "common.notifier.bark": "Bark", "common.notifier.dingtalk": "DingTalk", "common.notifier.email": "Email", + "common.notifier.gotify": "Gotify", "common.notifier.lark": "Lark", "common.notifier.serverchan": "ServerChan", "common.notifier.telegram": "Telegram", diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index 74e869bd..7ecdd973 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -53,6 +53,16 @@ "settings.notification.channel.form.email_sender_address.placeholder": "Please enter sender email address", "settings.notification.channel.form.email_receiver_address.label": "Receiver email address", "settings.notification.channel.form.email_receiver_address.placeholder": "Please enter receiver email address", + "settings.notification.channel.form.gotify_url.placeholder": "Please enter Service URL", + "settings.notification.channel.form.gotify_url.label": "Service URL", + "settings.notification.channel.form.gotify_url.tooltip": "Example: https://gotify.exmaple.com, the protocol needs to be included but the trailing '/' should not be included.
For more information, see https://gotify.net/docs/pushmsg", + "settings.notification.channel.form.gotify_token.placeholder": "Please enter Application Token", + "settings.notification.channel.form.gotify_token.label": "Application Token", + "settings.notification.channel.form.gotify_token.tooltip": "For more information, see https://gotify.net/docs/pushmsg", + "settings.notification.channel.form.gotify_priority.placeholder": "Please enter message priority", + "settings.notification.channel.form.gotify_priority.label": "Message Priority", + "settings.notification.channel.form.gotify_priority.tooltip": "Message Priority, you can set it to 1 as default.
For more information, see https://gotify.net/docs/pushmsg
https://github.com/gotify/android/issues/18#issuecomment-437403888", + "settings.notification.channel.form.gotify_priority.error.gte0": "Message Priority must be greater than or equal to 0.", "settings.notification.channel.form.lark_webhook_url.label": "Webhook URL", "settings.notification.channel.form.lark_webhook_url.placeholder": "Please enter Webhook URL", "settings.notification.channel.form.lark_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 7e9b9036..172dd390 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -38,6 +38,7 @@ "common.notifier.bark": "Bark", "common.notifier.dingtalk": "钉钉", "common.notifier.email": "邮件", + "common.notifier.gotify": "Gotify", "common.notifier.lark": "飞书", "common.notifier.serverchan": "Server 酱", "common.notifier.telegram": "Telegram", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index 0c51d33e..9dd6d0af 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -53,6 +53,16 @@ "settings.notification.channel.form.email_sender_address.placeholder": "请输入发送邮箱地址", "settings.notification.channel.form.email_receiver_address.label": "接收邮箱地址", "settings.notification.channel.form.email_receiver_address.placeholder": "请输入接收邮箱地址", + "settings.notification.channel.form.gotify_url.placeholder": "请输入服务地址", + "settings.notification.channel.form.gotify_url.label": "服务地址", + "settings.notification.channel.form.gotify_url.tooltip": "示例: https://gotify.exmaple.com,需要包含协议但不要包含末尾的'/'。
请参阅 https://gotify.net/docs/pushmsg", + "settings.notification.channel.form.gotify_token.placeholder": "请输入应用Token", + "settings.notification.channel.form.gotify_token.label": "应用Token", + "settings.notification.channel.form.gotify_token.tooltip": "应用Token。
请参阅 https://gotify.net/docs/pushmsg", + "settings.notification.channel.form.gotify_priority.placeholder": "请输入消息优先级", + "settings.notification.channel.form.gotify_priority.label": "消息优先级", + "settings.notification.channel.form.gotify_priority.tooltip": "消息优先级, 可以设置为1作为默认值。
请参阅 https://gotify.net/docs/pushmsg
https://github.com/gotify/android/issues/18#issuecomment-437403888", + "settings.notification.channel.form.gotify_priority.error.gte0": "消息优先级需要大于等于0", "settings.notification.channel.form.lark_webhook_url.label": "机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.placeholder": "请输入机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756",