From ec0cdf8b96f52e99152d9ad17d2891e587dbe36b Mon Sep 17 00:00:00 2001 From: banto <13196831+banto6@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:55:47 +0800 Subject: [PATCH] feat(notify): add mattermost --- internal/domain/notify.go | 1 + internal/notify/providers.go | 9 ++ .../providers/mattermost/mattermost.go | 89 +++++++++++++++++++ .../providers/mattermost/mattermost_test.go | 74 +++++++++++++++ .../notification/NotifyChannelEditForm.tsx | 11 ++- .../NotifyChannelEditFormMattermostFields.tsx | 63 +++++++++++++ ui/src/domain/settings.ts | 11 +++ ui/src/i18n/locales/en/nls.common.json | 1 + ui/src/i18n/locales/en/nls.settings.json | 9 ++ ui/src/i18n/locales/zh/nls.common.json | 1 + ui/src/i18n/locales/zh/nls.settings.json | 9 ++ 11 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 internal/pkg/core/notifier/providers/mattermost/mattermost.go create mode 100644 internal/pkg/core/notifier/providers/mattermost/mattermost_test.go create mode 100644 ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx diff --git a/internal/domain/notify.go b/internal/domain/notify.go index 4bc57b85..06c8bbae 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -14,6 +14,7 @@ const ( NotifyChannelTypeEmail = NotifyChannelType("email") NotifyChannelTypeGotify = NotifyChannelType("gotify") NotifyChannelTypeLark = NotifyChannelType("lark") + NotifyChannelTypeMattermost = NotifyChannelType("mattermost") NotifyChannelTypePushPlus = NotifyChannelType("pushplus") NotifyChannelTypeServerChan = NotifyChannelType("serverchan") NotifyChannelTypeTelegram = NotifyChannelType("telegram") diff --git a/internal/notify/providers.go b/internal/notify/providers.go index 3a7cadf9..6e5a6aef 100644 --- a/internal/notify/providers.go +++ b/internal/notify/providers.go @@ -10,6 +10,7 @@ import ( 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" + pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost" pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus" pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan" pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram" @@ -59,6 +60,14 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"), }) + case domain.NotifyChannelTypeMattermost: + return pMattermost.NewNotifier(&pMattermost.NotifierConfig{ + ServerUrl: maputil.GetString(channelConfig, "serverUrl"), + ChannelId: maputil.GetString(channelConfig, "channelId"), + Username: maputil.GetString(channelConfig, "username"), + Password: maputil.GetString(channelConfig, "password"), + }) + case domain.NotifyChannelTypePushPlus: return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{ Token: maputil.GetString(channelConfig, "token"), diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost.go b/internal/pkg/core/notifier/providers/mattermost/mattermost.go new file mode 100644 index 00000000..24890794 --- /dev/null +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost.go @@ -0,0 +1,89 @@ +package mattermost + +import ( + "bytes" + "context" + "encoding/json" + "github.com/nikoksr/notify/service/mattermost" + "github.com/usual2970/certimate/internal/pkg/core/notifier" + "io" + "log/slog" + "net/http" +) + +type NotifierConfig struct { + // Mattermost 服务地址。 + ServerUrl string `json:"serverUrl"` + // 频道ID + ChannelId string `json:"channelId"` + // 用户名 + Username string `json:"username"` + // 密码 + Password string `json:"password"` +} + +type NotifierProvider struct { + config *NotifierConfig + logger *slog.Logger +} + +var _ notifier.Notifier = (*NotifierProvider)(nil) + +func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { + if config == nil { + panic("config is nil") + } + + return &NotifierProvider{ + config: config, + }, 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) { + srv := mattermost.New(n.config.ServerUrl) + + if err := srv.LoginWithCredentials(ctx, n.config.Username, n.config.Password); err != nil { + return nil, err + } + + srv.AddReceivers(n.config.ChannelId) + + // 复写消息样式 + srv.PreSend(func(req *http.Request) error { + m := map[string]interface{}{ + "channel_id": n.config.ChannelId, + "props": map[string]interface{}{ + "attachments": []map[string]interface{}{ + { + "title": subject, + "text": message, + }, + }, + }, + } + + if body, err := json.Marshal(m); err != nil { + return err + } else { + req.ContentLength = int64(len(body)) + req.Body = io.NopCloser(bytes.NewReader(body)) + } + + return nil + }) + + if err = srv.Send(ctx, subject, message); err != nil { + return nil, err + } + + return ¬ifier.NotifyResult{}, nil +} diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go b/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go new file mode 100644 index 00000000..6db6cc42 --- /dev/null +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go @@ -0,0 +1,74 @@ +package mattermost_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost" +) + +const ( + mockSubject = "test_subject" + mockMessage = "test_message" +) + +var ( + fServerUrl string + fChannelId string + fUsername string + fPassword string +) + +func init() { + argsPrefix := "CERTIMATE_NOTIFIER_MATTERMOST_" + + flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") + flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "") + flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") + flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") +} + +/* +Shell command to run this test: + + go test -v ./mattermost_test.go -args \ + --CERTIMATE_NOTIFIER_MATTERMOST_SERVERURL="https://example.com/your-server-url" \ + --CERTIMATE_NOTIFIER_MATTERMOST_CHANNELID="your-chanel-id" \ + --CERTIMATE_NOTIFIER_MATTERMOST_USERNAME="your-username" \ + --CERTIMATE_NOTIFIER_MATTERMOST_PASSWORD="your-password" +*/ +func TestNotify(t *testing.T) { + flag.Parse() + + t.Run("Notify", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("SERVERURL: %v", fServerUrl), + fmt.Sprintf("CHANNELID: %v", fChannelId), + fmt.Sprintf("USERNAME: %v", fUsername), + fmt.Sprintf("PASSWORD: %v", fPassword), + }, "\n")) + + notifier, err := provider.NewNotifier(&provider.NotifierConfig{ + ServerUrl: fServerUrl, + ChannelId: fChannelId, + Username: fUsername, + Password: fPassword, + }) + 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 aa3f4f12..fb87c5a8 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -1,8 +1,8 @@ -import { forwardRef, useImperativeHandle, useMemo } from "react"; -import { Form, type FormInstance } from "antd"; +import {forwardRef, useImperativeHandle, useMemo} from "react"; +import {Form, type FormInstance} from "antd"; -import { NOTIFY_CHANNELS, type NotifyChannelsSettingsContent } from "@/domain/settings"; -import { useAntdForm } from "@/hooks"; +import {NOTIFY_CHANNELS, type NotifyChannelsSettingsContent} from "@/domain/settings"; +import {useAntdForm} from "@/hooks"; import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; @@ -14,6 +14,7 @@ import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServer import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields"; import NotifyChannelEditFormWeComFields from "./NotifyChannelEditFormWeComFields"; +import NotifyChannelEditFormMattermostFields from "@/components/notification/NotifyChannelEditFormMattermostFields.tsx"; type NotifyChannelEditFormFieldValues = NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent]; @@ -54,6 +55,8 @@ const NotifyChannelEditForm = forwardRef; case NOTIFY_CHANNELS.LARK: return ; + case NOTIFY_CHANNELS.MATTERMOST: + return ; case NOTIFY_CHANNELS.PUSHPLUS: return ; case NOTIFY_CHANNELS.SERVERCHAN: diff --git a/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx new file mode 100644 index 00000000..de8a0b08 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx @@ -0,0 +1,63 @@ +import {useTranslation} from "react-i18next"; +import {Form, Input} from "antd"; +import {createSchemaFieldRule} from "antd-zod"; +import {z} from "zod"; + +const NotifyChannelEditFormMattermostFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + serverUrl: z + .string({ message: t("settings.notification.channel.form.mattermost_server_url.placeholder") }) + .url(t("common.errmsg.url_invalid")), + channelId: z + .string({ message: t("settings.notification.channel.form.mattermost_channel_id.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_channel_id.placeholder")), + username: z + .string({ message: t("settings.notification.channel.form.mattermost_username.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_username.placeholder")), + password: z + .string({ message: t("settings.notification.channel.form.mattermost_password.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_password.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + + + + + + + + + + + + + ); +}; + +export default NotifyChannelEditFormMattermostFields; diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index 5dc4da80..35d7f8e4 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -44,6 +44,7 @@ export const NOTIFY_CHANNELS = Object.freeze({ EMAIL: "email", GOTIFY: "gotify", LARK: "lark", + MATTERMOST: "mattermost", PUSHPLUS: "pushplus", SERVERCHAN: "serverchan", TELEGRAM: "telegram", @@ -64,6 +65,7 @@ export type NotifyChannelsSettingsContent = { [NOTIFY_CHANNELS.EMAIL]?: EmailNotifyChannelConfig; [NOTIFY_CHANNELS.GOTIFY]?: GotifyNotifyChannelConfig; [NOTIFY_CHANNELS.LARK]?: LarkNotifyChannelConfig; + [NOTIFY_CHANNELS.MATTERMOST]?: MattermostNotifyChannelConfig; [NOTIFY_CHANNELS.PUSHPLUS]?: PushPlusNotifyChannelConfig; [NOTIFY_CHANNELS.SERVERCHAN]?: ServerChanNotifyChannelConfig; [NOTIFY_CHANNELS.TELEGRAM]?: TelegramNotifyChannelConfig; @@ -106,6 +108,14 @@ export type LarkNotifyChannelConfig = { enabled?: boolean; }; +export type MattermostNotifyChannelConfig = { + serverUrl: string; + channel: string; + username: string; + password: string; + enabled?: boolean; +} + export type PushPlusNotifyChannelConfig = { token: string; enabled?: boolean; @@ -143,6 +153,7 @@ export const notifyChannelsMap: Map = new [NOTIFY_CHANNELS.DINGTALK, "common.notifier.dingtalk"], [NOTIFY_CHANNELS.GOTIFY, "common.notifier.gotify"], [NOTIFY_CHANNELS.LARK, "common.notifier.lark"], + [NOTIFY_CHANNELS.MATTERMOST, "common.notifier.mattermost"], [NOTIFY_CHANNELS.PUSHPLUS, "common.notifier.pushplus"], [NOTIFY_CHANNELS.WECOM, "common.notifier.wecom"], [NOTIFY_CHANNELS.TELEGRAM, "common.notifier.telegram"], diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index c5949d28..2587bdb3 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -41,6 +41,7 @@ "common.notifier.email": "Email", "common.notifier.gotify": "Gotify", "common.notifier.lark": "Lark", + "common.notifier.mattermost": "Mattermost", "common.notifier.pushplus": "PushPlus", "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 d436665c..8b7c15cc 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -66,6 +66,15 @@ "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", + "settings.notification.channel.form.mattermost_server_url.label": "Service URL", + "settings.notification.channel.form.mattermost_server_url.placeholder": "Please enter service URL", + "settings.notification.channel.form.mattermost_server_url.tooltip": "Example: https://exmaple.com, the protocol needs to be included but the trailing '/' should not be included.", + "settings.notification.channel.form.mattermost_channel_id.label": "Channel ID", + "settings.notification.channel.form.mattermost_channel_id.placeholder": "Please enter channel ID", + "settings.notification.channel.form.mattermost_username.label": "Username", + "settings.notification.channel.form.mattermost_username.placeholder": "Please enter username", + "settings.notification.channel.form.mattermost_password.label": "Password", + "settings.notification.channel.form.mattermost_password.placeholder": "Please enter password", "settings.notification.channel.form.pushplus_token.placeholder": "Please enter Token", "settings.notification.channel.form.pushplus_token.label": "Token", "settings.notification.channel.form.pushplus_token.tooltip": "For more information, see https://www.pushplus.plus/push1.html", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 726c5ca2..333c5f12 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -41,6 +41,7 @@ "common.notifier.email": "邮件", "common.notifier.gotify": "Gotify", "common.notifier.lark": "飞书", + "common.notifier.mattermost": "Mattermost", "common.notifier.pushplus": "PushPlus推送加", "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 c00d158a..bc0b3c7b 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -66,6 +66,15 @@ "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", + "settings.notification.channel.form.mattermost_server_url.label": "服务地址", + "settings.notification.channel.form.mattermost_server_url.placeholder": "请输入服务地址", + "settings.notification.channel.form.mattermost_server_url.tooltip": "示例: https://exmaple.com,需要包含协议但不要包含末尾的'/'", + "settings.notification.channel.form.mattermost_channel_id.label": "频道ID", + "settings.notification.channel.form.mattermost_channel_id.placeholder": "请输入频道ID", + "settings.notification.channel.form.mattermost_username.label": "用户名", + "settings.notification.channel.form.mattermost_username.placeholder": "请输入用户名", + "settings.notification.channel.form.mattermost_password.label": "密码", + "settings.notification.channel.form.mattermost_password.placeholder": "请输入密码", "settings.notification.channel.form.pushplus_token.placeholder": "请输入Token", "settings.notification.channel.form.pushplus_token.label": "Token", "settings.notification.channel.form.pushplus_token.tooltip": "请参阅 https://www.pushplus.plus/push1.html",