发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
This commit is contained in:
+134
-18
@@ -24,12 +24,12 @@ const notifyTextLimit = 3800
|
||||
// telegramAPIBase 是 Telegram Bot API 官方根地址。
|
||||
const telegramAPIBase = "https://api.telegram.org"
|
||||
|
||||
// Notifier 通过 Telegram Bot API 推送通知;未启用或未配置时静默跳过。
|
||||
// 云外 HTTP 调用,不属于 internal/oci 的职责范围。
|
||||
// Notifier 向 Telegram 与全部启用的通知渠道(webhook/ntfy/bark/smtp)推送;
|
||||
// 未启用或未配置的渠道静默跳过。云外 HTTP 调用,不属于 internal/oci 的职责范围。
|
||||
type Notifier struct {
|
||||
settings *SettingService
|
||||
client *http.Client
|
||||
base string // API 根地址,测试时注入 httptest 服务
|
||||
base string // Telegram API 根地址,测试时注入 httptest 服务
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
@@ -42,8 +42,28 @@ func NewNotifier(settings *SettingService) *Notifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Send 读取配置并发送一条消息;未启用或未配置时直接返回 nil。
|
||||
// Send 向全部启用渠道发送一条纯文本消息;无渠道可用时返回 nil。
|
||||
func (n *Notifier) Send(ctx context.Context, text string) error {
|
||||
return n.dispatch(ctx, notifyMsg{Title: "oci-portal 通知", Text: text})
|
||||
}
|
||||
|
||||
// dispatch 把消息投递到 Telegram 与全部启用渠道;渠道间互不影响,错误合并返回供日志。
|
||||
func (n *Notifier) dispatch(ctx context.Context, msg notifyMsg) error {
|
||||
msg.Text = truncateText(msg.Text, notifyTextLimit)
|
||||
var errs []error
|
||||
if err := n.sendTelegram(ctx, msg); err != nil {
|
||||
errs = append(errs, fmt.Errorf("telegram: %w", err))
|
||||
}
|
||||
for _, chType := range NotifyChannelTypes {
|
||||
if err := n.sendChannel(ctx, chType, msg); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", chType, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// sendTelegram 走既有 Telegram 路径;未启用或未配置时静默跳过。
|
||||
func (n *Notifier) sendTelegram(ctx context.Context, msg notifyMsg) error {
|
||||
cfg, err := n.settings.TelegramConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -51,7 +71,38 @@ func (n *Notifier) Send(ctx context.Context, text string) error {
|
||||
if !cfg.Enabled || cfg.BotToken == "" || cfg.ChatID == "" {
|
||||
return nil
|
||||
}
|
||||
return n.post(ctx, cfg, text)
|
||||
return n.postHTML(ctx, cfg, mdToTelegramHTML(msg.Text))
|
||||
}
|
||||
|
||||
// sendChannel 向单个新增渠道投递;未启用或关键字段缺失时静默跳过。
|
||||
func (n *Notifier) sendChannel(ctx context.Context, chType string, msg notifyMsg) error {
|
||||
switch chType {
|
||||
case ChannelWebhook:
|
||||
cfg, err := n.settings.webhookChannel(ctx)
|
||||
if err != nil || !cfg.Enabled || cfg.URL == "" {
|
||||
return err
|
||||
}
|
||||
return sendWebhook(ctx, n.client, cfg, msg)
|
||||
case ChannelNtfy:
|
||||
cfg, err := n.settings.ntfyChannel(ctx)
|
||||
if err != nil || !cfg.Enabled || cfg.Topic == "" {
|
||||
return err
|
||||
}
|
||||
return sendNtfy(ctx, n.client, cfg, msg)
|
||||
case ChannelBark:
|
||||
cfg, err := n.settings.barkChannel(ctx)
|
||||
if err != nil || !cfg.Enabled || cfg.DeviceKey == "" {
|
||||
return err
|
||||
}
|
||||
return sendBark(ctx, n.client, cfg, msg)
|
||||
case ChannelSMTP:
|
||||
cfg, err := n.settings.smtpChannel(ctx)
|
||||
if err != nil || !cfg.Enabled || cfg.Host == "" {
|
||||
return err
|
||||
}
|
||||
return sendSMTP(ctx, cfg, msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendAsync 异步发送并自带超时,失败只记日志,绝不影响调用链路。
|
||||
@@ -62,13 +113,13 @@ func (n *Notifier) SendAsync(text string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), notifySendTimeout)
|
||||
defer cancel()
|
||||
if err := n.Send(ctx, text); err != nil {
|
||||
log.Printf("telegram notify: %v", err)
|
||||
log.Printf("notify: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SendTemplateAsync 按 kind 的生效模板(自定义或默认)渲染变量后异步发送;
|
||||
// 模板支持轻量 Markdown,按 Telegram HTML 语义投递。
|
||||
// 模板支持轻量 Markdown,Telegram 按 HTML 语义投递,其余渠道按纯文本投递。
|
||||
func (n *Notifier) SendTemplateAsync(kind string, vars map[string]string) {
|
||||
n.wg.Add(1)
|
||||
go func() {
|
||||
@@ -76,7 +127,7 @@ func (n *Notifier) SendTemplateAsync(kind string, vars map[string]string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), notifySendTimeout)
|
||||
defer cancel()
|
||||
if err := n.sendTemplate(ctx, kind, "", vars); err != nil {
|
||||
log.Printf("telegram notify %s: %v", kind, err)
|
||||
log.Printf("notify %s: %v", kind, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -88,12 +139,12 @@ func (n *Notifier) SendTemplateTest(ctx context.Context, kind, tplOverride strin
|
||||
if !ok {
|
||||
return fmt.Errorf("未知通知类型 %q", kind)
|
||||
}
|
||||
cfg, err := n.settings.TelegramConfig(ctx)
|
||||
enabled, err := n.anyChannelEnabled(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cfg.Enabled || cfg.BotToken == "" || cfg.ChatID == "" {
|
||||
return fmt.Errorf("Telegram 通知未启用或未配置,请先在「通知方式」保存配置")
|
||||
if !enabled {
|
||||
return fmt.Errorf("没有已启用的通知渠道,请先在「通知方式」配置并启用")
|
||||
}
|
||||
vars := map[string]string{}
|
||||
for k, v := range def.Sample {
|
||||
@@ -102,15 +153,29 @@ func (n *Notifier) SendTemplateTest(ctx context.Context, kind, tplOverride strin
|
||||
return n.sendTemplate(ctx, kind, tplOverride, vars)
|
||||
}
|
||||
|
||||
// sendTemplate 渲染并发送:取生效模板 → 变量替换 → 截断 → Markdown 转 Telegram HTML。
|
||||
func (n *Notifier) sendTemplate(ctx context.Context, kind, tplOverride string, vars map[string]string) error {
|
||||
cfg, err := n.settings.TelegramConfig(ctx)
|
||||
// anyChannelEnabled 报告是否存在至少一个已启用且配置齐全的渠道(含 Telegram)。
|
||||
func (n *Notifier) anyChannelEnabled(ctx context.Context) (bool, error) {
|
||||
tg, err := n.settings.TelegramConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
if !cfg.Enabled || cfg.BotToken == "" || cfg.ChatID == "" {
|
||||
return nil
|
||||
if tg.Enabled && tg.BotToken != "" && tg.ChatID != "" {
|
||||
return true, nil
|
||||
}
|
||||
views, err := n.settings.NotifyChannels(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, v := range views {
|
||||
if v.Enabled {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// sendTemplate 渲染并分发:取生效模板 → 变量替换 → 投递到全部启用渠道。
|
||||
func (n *Notifier) sendTemplate(ctx context.Context, kind, tplOverride string, vars map[string]string) error {
|
||||
tpl := tplOverride
|
||||
if strings.TrimSpace(tpl) == "" {
|
||||
custom, err := n.settings.NotifyTemplate(ctx, kind)
|
||||
@@ -120,7 +185,58 @@ func (n *Notifier) sendTemplate(ctx context.Context, kind, tplOverride string, v
|
||||
tpl = notifyTplText(custom, kind)
|
||||
}
|
||||
text := renderNotifyTemplate(tpl, vars)
|
||||
return n.postHTML(ctx, cfg, mdToTelegramHTML(truncateText(text, notifyTextLimit)))
|
||||
return n.dispatch(ctx, notifyMsg{Title: notifyTplLabel(kind), Text: text})
|
||||
}
|
||||
|
||||
// SendChannelTest 向指定渠道同步发送测试消息;不看启用开关,便于保存后即时验证。
|
||||
func (n *Notifier) SendChannelTest(ctx context.Context, chType string) error {
|
||||
msg := notifyMsg{Title: "oci-portal 测试消息", Text: "✅ 通知渠道配置成功:" + chType}
|
||||
switch chType {
|
||||
case ChannelWebhook:
|
||||
cfg, err := n.settings.webhookChannel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.URL == "" {
|
||||
return fmt.Errorf("请先保存 Webhook URL")
|
||||
}
|
||||
return sendWebhook(ctx, n.client, cfg, msg)
|
||||
case ChannelNtfy:
|
||||
cfg, err := n.settings.ntfyChannel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Topic == "" {
|
||||
return fmt.Errorf("请先保存 ntfy topic")
|
||||
}
|
||||
return sendNtfy(ctx, n.client, cfg, msg)
|
||||
}
|
||||
return n.sendChannelTestRest(ctx, chType, msg)
|
||||
}
|
||||
|
||||
// sendChannelTestRest 承接 SendChannelTest 的 bark/smtp 分支(拆分控制函数长度)。
|
||||
func (n *Notifier) sendChannelTestRest(ctx context.Context, chType string, msg notifyMsg) error {
|
||||
switch chType {
|
||||
case ChannelBark:
|
||||
cfg, err := n.settings.barkChannel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.DeviceKey == "" {
|
||||
return fmt.Errorf("请先保存 Bark device key")
|
||||
}
|
||||
return sendBark(ctx, n.client, cfg, msg)
|
||||
case ChannelSMTP:
|
||||
cfg, err := n.settings.smtpChannel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Host == "" || cfg.From == "" || cfg.To == "" {
|
||||
return fmt.Errorf("请先保存 SMTP 主机、发件人与收件人")
|
||||
}
|
||||
return sendSMTP(ctx, cfg, msg)
|
||||
}
|
||||
return ErrUnknownNotifyChannel
|
||||
}
|
||||
|
||||
// Wait 等待全部在途异步通知发完,供退出前收尾与测试同步。
|
||||
|
||||
Reference in New Issue
Block a user