diff --git a/internal/domain/notify.go b/internal/domain/notify.go index 3d71a3a7..247c691f 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -13,6 +13,7 @@ const ( NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk") NotifyChannelTypeEmail = NotifyChannelType("email") NotifyChannelTypeLark = NotifyChannelType("lark") + NotifyChannelTypePushPlus = NotifyChannelType("pushplus") NotifyChannelTypeServerChan = NotifyChannelType("serverchan") NotifyChannelTypeTelegram = NotifyChannelType("telegram") NotifyChannelTypeWebhook = NotifyChannelType("webhook") diff --git a/internal/notify/providers.go b/internal/notify/providers.go index 66927390..a5f93a91 100644 --- a/internal/notify/providers.go +++ b/internal/notify/providers.go @@ -9,6 +9,7 @@ import ( pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk" pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email" pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark" + 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" pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook" @@ -50,6 +51,11 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"), }) + case domain.NotifyChannelTypePushPlus: + return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{ + Token: maputil.GetString(channelConfig, "token"), + }) + case domain.NotifyChannelTypeServerChan: return pServerChan.NewNotifier(&pServerChan.NotifierConfig{ Url: maputil.GetString(channelConfig, "url"), diff --git a/internal/pkg/core/notifier/notifier.go b/internal/pkg/core/notifier/notifier.go index 97485215..876b5d48 100644 --- a/internal/pkg/core/notifier/notifier.go +++ b/internal/pkg/core/notifier/notifier.go @@ -1,4 +1,4 @@ -package notifier +package notifier import ( "context" diff --git a/internal/pkg/core/notifier/providers/pushplus/pushplus.go b/internal/pkg/core/notifier/providers/pushplus/pushplus.go new file mode 100644 index 00000000..4edac14e --- /dev/null +++ b/internal/pkg/core/notifier/providers/pushplus/pushplus.go @@ -0,0 +1,113 @@ +package pushplus + +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 { + // PushPlus Token + Token string `json:"token"` +} + +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 +} + +// Notify 发送通知 +// 参考文档:https://pushplus.plus/doc/guide/api.html +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { + // 请求体 + reqBody := &struct { + Token string `json:"token"` + Title string `json:"title"` + Content string `json:"content"` + }{ + Token: n.config.Token, + Title: subject, + Content: message, + } + + // 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, + "https://www.pushplus.plus/send", + bytes.NewReader(body), + ) + if err != nil { + return nil, errors.Wrap(err, "create new request") + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + // Send request to pushplus service + resp, err := n.httpClient.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "send request to pushplus server") + } + defer resp.Body.Close() + + 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("pushplus returned status code %d: %s", resp.StatusCode, string(result)) + } + + // 解析响应 + var errorResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + } + if err := json.Unmarshal(result, &errorResponse); err != nil { + return nil, errors.Wrap(err, "decode response") + } + + if errorResponse.Code != 200 { + return nil, fmt.Errorf("pushplus returned error: %s", errorResponse.Msg) + } + + return ¬ifier.NotifyResult{}, nil +} diff --git a/internal/pkg/core/notifier/providers/pushplus/pushplus_test.go b/internal/pkg/core/notifier/providers/pushplus/pushplus_test.go new file mode 100644 index 00000000..f504c168 --- /dev/null +++ b/internal/pkg/core/notifier/providers/pushplus/pushplus_test.go @@ -0,0 +1,56 @@ +package pushplus_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus" +) + +const ( + mockSubject = "test_subject" + mockMessage = "test_message" +) + +var fToken string + +func init() { + argsPrefix := "CERTIMATE_NOTIFIER_PUSHPLUS_" + flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./pushplus_test.go -args \ + --CERTIMATE_NOTIFIER_PUSHPLUS_TOKEN="your-pushplus-token" \ +*/ +func TestNotify(t *testing.T) { + flag.Parse() + + t.Run("Notify", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("TOKEN: %v", fToken), + }, "\n")) + + notifier, err := provider.NewNotifier(&provider.NotifierConfig{ + Token: fToken, + }) + 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..dbeddaaf 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -8,6 +8,7 @@ import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields"; import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields"; +import NotifyChannelEditFormPushPlusFields from "./NotifyChannelEditFormPushPlusFields"; import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields"; import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields"; @@ -50,6 +51,8 @@ const NotifyChannelEditForm = forwardRef; case NOTIFY_CHANNELS.LARK: return ; + case NOTIFY_CHANNELS.PUSHPLUS: + return ; case NOTIFY_CHANNELS.SERVERCHAN: return ; case NOTIFY_CHANNELS.TELEGRAM: diff --git a/ui/src/components/notification/NotifyChannelEditFormPushPlusFields.tsx b/ui/src/components/notification/NotifyChannelEditFormPushPlusFields.tsx new file mode 100644 index 00000000..36f6e21a --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormPushPlusFields.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormPushPlusFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + token: z.string({ message: t("settings.notification.channel.form.pushplus_token.placeholder") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + ); +}; + +export default NotifyChannelEditFormPushPlusFields; diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index e34105e0..16a0ae69 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -41,6 +41,7 @@ export const NOTIFY_CHANNELS = Object.freeze({ DINGTALK: "dingtalk", EMAIL: "email", LARK: "lark", + PUSHPLUS: "pushplus", SERVERCHAN: "serverchan", TELEGRAM: "telegram", WEBHOOK: "webhook", @@ -59,6 +60,7 @@ export type NotifyChannelsSettingsContent = { [NOTIFY_CHANNELS.DINGTALK]?: DingTalkNotifyChannelConfig; [NOTIFY_CHANNELS.EMAIL]?: EmailNotifyChannelConfig; [NOTIFY_CHANNELS.LARK]?: LarkNotifyChannelConfig; + [NOTIFY_CHANNELS.PUSHPLUS]?: PushPlusNotifyChannelConfig; [NOTIFY_CHANNELS.SERVERCHAN]?: ServerChanNotifyChannelConfig; [NOTIFY_CHANNELS.TELEGRAM]?: TelegramNotifyChannelConfig; [NOTIFY_CHANNELS.WEBHOOK]?: WebhookNotifyChannelConfig; @@ -93,6 +95,11 @@ export type LarkNotifyChannelConfig = { enabled?: boolean; }; +export type PushPlusNotifyChannelConfig = { + token: string; + enabled?: boolean; +}; + export type ServerChanNotifyChannelConfig = { url: string; enabled?: boolean; @@ -124,6 +131,7 @@ export const notifyChannelsMap: Map = new [NOTIFY_CHANNELS.EMAIL, "common.notifier.email"], [NOTIFY_CHANNELS.DINGTALK, "common.notifier.dingtalk"], [NOTIFY_CHANNELS.LARK, "common.notifier.lark"], + [NOTIFY_CHANNELS.PUSHPLUS, "common.notifier.pushplus"], [NOTIFY_CHANNELS.WECOM, "common.notifier.wecom"], [NOTIFY_CHANNELS.TELEGRAM, "common.notifier.telegram"], [NOTIFY_CHANNELS.SERVERCHAN, "common.notifier.serverchan"], diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index c9e671ce..42911b88 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -39,6 +39,7 @@ "common.notifier.dingtalk": "DingTalk", "common.notifier.email": "Email", "common.notifier.lark": "Lark", + "common.notifier.pushplus": "PushPlus", "common.notifier.serverchan": "ServerChan", "common.notifier.telegram": "Telegram", "common.notifier.webhook": "Webhook", diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index 74e869bd..642ad59a 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -56,6 +56,9 @@ "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.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", "settings.notification.channel.form.serverchan_url.label": "Server URL", "settings.notification.channel.form.serverchan_url.placeholder": "Please enter ServerChan server URL (e.g. https://sctapi.ftqq.com/*****.send)", "settings.notification.channel.form.serverchan_url.tooltip": "For more information, see https://sct.ftqq.com/forward", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 7e9b9036..76b5dfd6 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -39,6 +39,7 @@ "common.notifier.dingtalk": "钉钉", "common.notifier.email": "邮件", "common.notifier.lark": "飞书", + "common.notifier.pushplus": "PushPlus推送加", "common.notifier.serverchan": "Server 酱", "common.notifier.telegram": "Telegram", "common.notifier.webhook": "Webhook", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index 0c51d33e..0d593c80 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -56,6 +56,9 @@ "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.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", "settings.notification.channel.form.serverchan_url.label": "服务器地址", "settings.notification.channel.form.serverchan_url.placeholder": "请输入服务器地址(形如: https://sctapi.ftqq.com/*****.send)", "settings.notification.channel.form.serverchan_url.tooltip": "这是什么?请参阅 https://sct.ftqq.com/forward",