feat: add wecom notifier

This commit is contained in:
Fu Diwei 2024-12-22 11:25:08 +08:00
parent a1fec5f6ac
commit 01d30bb742
14 changed files with 235 additions and 39 deletions

View File

@ -23,7 +23,7 @@ type DeployConfig struct {
Config map[string]any `json:"config"`
}
// 以字符串形式获取配置项。
// Deprecated: 以字符串形式获取配置项。
//
// 入参:
// - key: 配置项的键。
@ -34,7 +34,7 @@ func (dc *DeployConfig) GetConfigAsString(key string) string {
return maps.GetValueAsString(dc.Config, key)
}
// 以字符串形式获取配置项。
// Deprecated: 以字符串形式获取配置项。
//
// 入参:
// - key: 配置项的键。
@ -46,7 +46,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue stri
return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
}
// 以 32 位整数形式获取配置项。
// Deprecated: 以 32 位整数形式获取配置项。
//
// 入参:
// - key: 配置项的键。
@ -57,7 +57,7 @@ func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
return maps.GetValueAsInt32(dc.Config, key)
}
// 以 32 位整数形式获取配置项。
// Deprecated: 以 32 位整数形式获取配置项。
//
// 入参:
// - key: 配置项的键。
@ -69,7 +69,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32
return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
}
// 以布尔形式获取配置项。
// Deprecated: 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
@ -80,7 +80,7 @@ func (dc *DeployConfig) GetConfigAsBool(key string) bool {
return maps.GetValueAsBool(dc.Config, key)
}
// 以布尔形式获取配置项。
// Deprecated: 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
@ -92,7 +92,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool)
return maps.GetValueOrDefaultAsBool(dc.Config, key, defaultValue)
}
// 以变量字典形式获取配置项。
// Deprecated: 以变量字典形式获取配置项。
//
// 出参:
// - 变量字典。
@ -119,7 +119,7 @@ func (dc *DeployConfig) GetConfigAsVariables() map[string]string {
return rs
}
// GetDomain returns the domain from the deploy config
// Deprecated: GetDomain returns the domain from the deploy config,
// if the domain is a wildcard domain, and wildcard is true, return the wildcard domain
func (dc *DeployConfig) GetDomain(wildcard ...bool) string {
val := dc.GetConfigAsString("domain")

View File

@ -1,13 +1,20 @@
package domain
/*
消息通知渠道常量值
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
NotifyChannelEmail = "email"
NotifyChannelWebhook = "webhook"
NotifyChannelDingtalk = "dingtalk"
NotifyChannelLark = "lark"
NotifyChannelTelegram = "telegram"
NotifyChannelServerChan = "serverchan"
NotifyChannelBark = "bark"
NotifyChannelDingtalk = "dingtalk"
NotifyChannelEmail = "email"
NotifyChannelLark = "lark"
NotifyChannelServerChan = "serverchan"
NotifyChannelTelegram = "telegram"
NotifyChannelWebhook = "webhook"
NotifyChannelWeCom = "wecom"
)
type NotifyTestPushReq struct {

View File

@ -12,6 +12,7 @@ import (
providerServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
providerTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
providerWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
providerWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
@ -64,6 +65,11 @@ func createNotifier(channel string, channelConfig map[string]any) (notifier.Noti
return providerWebhook.New(&providerWebhook.WebhookNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelWeCom:
return providerWeCom.New(&providerWeCom.WeComNotifierConfig{
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
})
}
return nil, fmt.Errorf("unsupported notifier channel: %s", channelConfig)

View File

@ -10,7 +10,7 @@ import (
)
type LarkNotifierConfig struct {
// 飞书 Webhook 地址。
// 飞书机器人 Webhook 地址。
WebhookUrl string `json:"webhookUrl"`
}

View File

@ -0,0 +1,58 @@
package serverchan
import (
"context"
"errors"
"net/http"
notifyHttp "github.com/nikoksr/notify/service/http"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type WeComNotifierConfig struct {
// 企业微信机器人 Webhook 地址。
WebhookUrl string `json:"webhookUrl"`
}
type WeComNotifier struct {
config *WeComNotifierConfig
}
var _ notifier.Notifier = (*WeComNotifier)(nil)
func New(config *WeComNotifierConfig) (*WeComNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &WeComNotifier{
config: config,
}, nil
}
func (n *WeComNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := notifyHttp.New()
srv.AddReceivers(&notifyHttp.Webhook{
URL: n.config.WebhookUrl,
Header: http.Header{},
ContentType: "application/json",
Method: http.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]any{
"msgtype": "text",
"text": map[string]string{
"content": subject + "\n\n" + message,
},
}
},
})
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@ -0,0 +1,57 @@
package serverchan_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fWebhookUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_WECOM_"
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
}
/*
Shell command to run this test:
go test -v serverchan_test.go -args \
--CERTIMATE_NOTIFIER_WECOM_WEBHOOKURL="https://example.com/your-webhook-url" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
}, "\n"))
notifier, err := provider.New(&provider.WeComNotifierConfig{
WebhookUrl: fWebhookUrl,
})
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)
})
}

View File

@ -2,7 +2,7 @@ import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useCreation, useDeepCompareEffect } from "ahooks";
import { Form } from "antd";
import { type NotifyChannelsSettingsContent } from "@/domain/settings";
import { NOTIFY_CHANNELS, type NotifyChannelsSettingsContent } from "@/domain/settings";
import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields";
import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields";
import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields";
@ -10,13 +10,14 @@ import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields";
import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields";
import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields";
import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields";
import NotifyChannelEditFormWeComFields from "./NotifyChannelEditFormWeComFields";
type NotifyChannelEditFormModelType = NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent];
export type NotifyChannelEditFormProps = {
className?: string;
style?: React.CSSProperties;
channel: keyof NotifyChannelsSettingsContent;
channel: string;
disabled?: boolean;
loading?: boolean;
model?: NotifyChannelEditFormModelType;
@ -39,20 +40,22 @@ const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyCh
NOTICE: If you add new child component, please keep ASCII order.
*/
switch (channel) {
case "bark":
case NOTIFY_CHANNELS.BARK:
return <NotifyChannelEditFormBarkFields />;
case "dingtalk":
case NOTIFY_CHANNELS.DINGTALK:
return <NotifyChannelEditFormDingTalkFields />;
case "email":
case NOTIFY_CHANNELS.EMAIL:
return <NotifyChannelEditFormEmailFields />;
case "lark":
case NOTIFY_CHANNELS.LARK:
return <NotifyChannelEditFormLarkFields />;
case "serverchan":
case NOTIFY_CHANNELS.SERVERCHAN:
return <NotifyChannelEditFormServerChanFields />;
case "telegram":
case NOTIFY_CHANNELS.TELEGRAM:
return <NotifyChannelEditFormTelegramFields />;
case "webhook":
case NOTIFY_CHANNELS.WEBHOOK:
return <NotifyChannelEditFormWebhookFields />;
case NOTIFY_CHANNELS.WECOM:
return <NotifyChannelEditFormWeComFields />;
}
}, [channel]);

View File

@ -0,0 +1,31 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormWeComFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
webhookUrl: z
.string({ message: t("settings.notification.channel.form.wecom_webhook_url.placeholder") })
.min(1, t("settings.notification.channel.form.wecom_webhook_url.placeholder"))
.url({ message: t("common.errmsg.url_invalid") }),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="webhookUrl"
label={t("settings.notification.channel.form.wecom_webhook_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.wecom_webhook_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.wecom_webhook_url.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormWeComFields;

View File

@ -39,19 +39,41 @@ export const defaultNotifyTemplate: NotifyTemplate = {
// #endregion
// #region Settings: NotifyChannels
export const NOTIFY_CHANNEL_BARK = "bark" as const;
export const NOTIFY_CHANNEL_DINGTALK = "dingtalk" as const;
export const NOTIFY_CHANNEL_EMAIL = "email" as const;
export const NOTIFY_CHANNEL_LARK = "lark" as const;
export const NOTIFY_CHANNEL_SERVERCHAN = "serverchan" as const;
export const NOTIFY_CHANNEL_TELEGRAM = "telegram" as const;
export const NOTIFY_CHANNEL_WEBHOOK = "webhook" as const;
export const NOTIFY_CHANNEL_WECOM = "wecom" as const;
export const NOTIFY_CHANNELS = Object.freeze({
BARK: NOTIFY_CHANNEL_BARK,
DINGTALK: NOTIFY_CHANNEL_DINGTALK,
EMAIL: NOTIFY_CHANNEL_EMAIL,
LARK: NOTIFY_CHANNEL_LARK,
SERVERCHAN: NOTIFY_CHANNEL_SERVERCHAN,
TELEGRAM: NOTIFY_CHANNEL_TELEGRAM,
WEBHOOK: NOTIFY_CHANNEL_WEBHOOK,
WECOM: NOTIFY_CHANNEL_WECOM,
} as const);
export type NotifyChannels = (typeof NOTIFY_CHANNELS)[keyof typeof NOTIFY_CHANNELS];
export type NotifyChannelsSettingsContent = {
/*
ASCII
NOTICE: If you add new type, please keep ASCII order.
*/
[key: string]: ({ enabled?: boolean } & Record<string, unknown>) | undefined;
bark?: BarkNotifyChannelConfig;
dingtalk?: DingTalkNotifyChannelConfig;
email?: EmailNotifyChannelConfig;
lark?: LarkNotifyChannelConfig;
serverchan?: ServerChanNotifyChannelConfig;
telegram?: TelegramNotifyChannelConfig;
webhook?: WebhookNotifyChannelConfig;
[NOTIFY_CHANNEL_BARK]?: BarkNotifyChannelConfig;
[NOTIFY_CHANNEL_DINGTALK]?: DingTalkNotifyChannelConfig;
[NOTIFY_CHANNEL_EMAIL]?: EmailNotifyChannelConfig;
[NOTIFY_CHANNEL_LARK]?: LarkNotifyChannelConfig;
[NOTIFY_CHANNEL_SERVERCHAN]?: ServerChanNotifyChannelConfig;
[NOTIFY_CHANNEL_TELEGRAM]?: TelegramNotifyChannelConfig;
[NOTIFY_CHANNEL_WEBHOOK]?: WebhookNotifyChannelConfig;
[NOTIFY_CHANNEL_WECOM]?: WeComNotifyChannelConfig;
};
export type BarkNotifyChannelConfig = {
@ -98,6 +120,11 @@ export type WebhookNotifyChannelConfig = {
enabled?: boolean;
};
export type WeComNotifyChannelConfig = {
webhookUrl: string;
enabled?: boolean;
};
export type NotifyChannel = {
type: string;
name: string;
@ -108,6 +135,7 @@ export const notifyChannelsMap: Map<NotifyChannel["type"], NotifyChannel> = new
["email", "common.notifier.email"],
["dingtalk", "common.notifier.dingtalk"],
["lark", "common.notifier.lark"],
["wecom", "common.notifier.wecom"],
["telegram", "common.notifier.telegram"],
["serverchan", "common.notifier.serverchan"],
["bark", "common.notifier.bark"],

View File

@ -90,5 +90,6 @@
"common.notifier.lark": "Lark",
"common.notifier.serverchan": "ServerChan",
"common.notifier.telegram": "Telegram",
"common.notifier.webhook": "Webhook"
"common.notifier.webhook": "Webhook",
"common.notifier.wecom": "WeCom"
}

View File

@ -67,10 +67,12 @@
"settings.notification.channel.form.telegram_chat_id.tooltip": "For more information, see <a href=\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\" target=\"_blank\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>",
"settings.notification.channel.form.webhook_url.label": "Webhook URL",
"settings.notification.channel.form.webhook_url.placeholder": "Please enter Webhook URL",
"settings.notification.channel.form.wecom_webhook_url.label": "Webhook URL",
"settings.notification.channel.form.wecom_webhook_url.placeholder": "Please enter Webhook URL",
"settings.notification.channel.form.wecom_webhook_url.tooltip": "For more information, see <a href=\"https://open.work.weixin.qq.com/help2/pc/18401#%E5%85%AD%E3%80%81%E7%BE%A4%E6%9C%BA%E5%99%A8%E4%BA%BAWebhook%E5%9C%B0%E5%9D%80\" target=\"_blank\">https://open.work.weixin.qq.com/help2/pc/18401</a>",
"settings.sslprovider.tab": "Certificate Authority",
"settings.sslprovider.form.provider.label": "ACME Provider",
"settings.sslprovider.provider.errmsg.empty": "Please select a Certificate Authority",
"settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID",
"settings.sslprovider.form.zerossl_eab_kid.placeholder": "Please enter EAB KID",
"settings.sslprovider.form.zerossl_eab_kid.tooltip": "For more information, see <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>",

View File

@ -86,9 +86,10 @@
"common.notifier.bark": "Bark",
"common.notifier.dingtalk": "钉钉",
"common.notifier.email": "电子邮件",
"common.notifier.email": "邮件",
"common.notifier.lark": "飞书",
"common.notifier.serverchan": "Server 酱",
"common.notifier.telegram": "Telegram",
"common.notifier.webhook": "Webhook"
"common.notifier.webhook": "Webhook",
"common.notifier.wecom": "企业微信"
}

View File

@ -53,8 +53,8 @@
"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.lark_webhook_url.label": "Webhook 地址",
"settings.notification.channel.form.lark_webhook_url.placeholder": "请输入 Webhook 地址",
"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": "这是什么?请参阅 <a href=\"https://www.feishu.cn/hc/zh-CN/articles/807992406756\" target=\"_blank\">https://www.feishu.cn/hc/zh-CN/articles/807992406756</a>",
"settings.notification.channel.form.serverchan_url.label": "服务器地址",
"settings.notification.channel.form.serverchan_url.placeholder": "请输入服务器地址(形如: https://sctapi.ftqq.com/*****.send",
@ -67,10 +67,12 @@
"settings.notification.channel.form.telegram_chat_id.tooltip": "这是什么?请参阅 <a href=\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\" target=\"_blank\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>",
"settings.notification.channel.form.webhook_url.label": "Webhook 回调地址",
"settings.notification.channel.form.webhook_url.placeholder": "请输入 Webhook 回调地址",
"settings.notification.channel.form.wecom_webhook_url.label": "机器人 Webhook 地址",
"settings.notification.channel.form.wecom_webhook_url.placeholder": "请输入机器人 Webhook 地址",
"settings.notification.channel.form.wecom_webhook_url.tooltip": "这是什么?请参阅 <a href=\"https://open.work.weixin.qq.com/help2/pc/18401#%E5%85%AD%E3%80%81%E7%BE%A4%E6%9C%BA%E5%99%A8%E4%BA%BAWebhook%E5%9C%B0%E5%9D%80\" target=\"_blank\">https://open.work.weixin.qq.com/help2/pc/18401</a>",
"settings.sslprovider.tab": "证书颁发机构CA",
"settings.sslprovider.form.provider.label": "ACME 提供商",
"settings.sslprovider.provider.errmsg.empty": "请选择证书分发机构",
"settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID",
"settings.sslprovider.form.zerossl_eab_kid.placeholder": "请输入 EAB KID",
"settings.sslprovider.form.zerossl_eab_kid.tooltip": "这是什么?请参阅 <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>",

View File

@ -26,7 +26,7 @@ export const useNotifyChannelStore = create<NotifyChannelState>((set, get) => {
produce(settings, (draft) => {
draft.content ??= {};
draft.content[channel] = { ...draft.content[channel], ...config };
})
}).content
);
},