Merge pull request #321 from fudiwei/feat/notifier

feat: notifiers
This commit is contained in:
usual2970 2024-11-11 18:16:30 +08:00 committed by GitHub
commit c36db3545f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1735 additions and 1011 deletions

2
go.mod
View File

@ -113,7 +113,7 @@ require (
github.com/cloudflare/cloudflare-go v0.104.0 // indirect github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/disintegration/imaging v1.6.2 // indirect github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect github.com/domodwyer/mailyak/v3 v3.6.2
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.17.0 // indirect github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/gabriel-vasile/mimetype v1.4.6 // indirect

View File

@ -3,6 +3,8 @@ package domain
import ( import (
"encoding/json" "encoding/json"
"strings" "strings"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
) )
type ApplyConfig struct { type ApplyConfig struct {
@ -29,7 +31,7 @@ type DeployConfig struct {
// 出参: // 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。 // - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
func (dc *DeployConfig) GetConfigAsString(key string) string { func (dc *DeployConfig) GetConfigAsString(key string) string {
return dc.GetConfigOrDefaultAsString(key, "") return maps.GetValueAsString(dc.Config, key)
} }
// 以字符串形式获取配置项。 // 以字符串形式获取配置项。
@ -41,17 +43,7 @@ func (dc *DeployConfig) GetConfigAsString(key string) string {
// 出参: // 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。 // - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string { func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
if dc.Config == nil { return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(string); ok {
return result
}
}
return defaultValue
} }
// 以 32 位整数形式获取配置项。 // 以 32 位整数形式获取配置项。
@ -62,7 +54,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue stri
// 出参: // 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。 // - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。
func (dc *DeployConfig) GetConfigAsInt32(key string) int32 { func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
return dc.GetConfigOrDefaultAsInt32(key, 0) return maps.GetValueAsInt32(dc.Config, key)
} }
// 以 32 位整数形式获取配置项。 // 以 32 位整数形式获取配置项。
@ -74,17 +66,7 @@ func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
// 出参: // 出参:
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。 // - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 { func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
if dc.Config == nil { return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(int32); ok {
return result
}
}
return defaultValue
} }
// 以布尔形式获取配置项。 // 以布尔形式获取配置项。
@ -95,7 +77,7 @@ func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32
// 出参: // 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。 // - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
func (dc *DeployConfig) GetConfigAsBool(key string) bool { func (dc *DeployConfig) GetConfigAsBool(key string) bool {
return dc.GetConfigOrDefaultAsBool(key, false) return maps.GetValueAsBool(dc.Config, key)
} }
// 以布尔形式获取配置项。 // 以布尔形式获取配置项。
@ -107,17 +89,7 @@ func (dc *DeployConfig) GetConfigAsBool(key string) bool {
// 出参: // 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。 // - 配置项的值。如果配置项不存在或者类型不是布尔,则返回默认值。
func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool { func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) bool {
if dc.Config == nil { return maps.GetValueOrDefaultAsBool(dc.Config, key, defaultValue)
return defaultValue
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(bool); ok {
return result
}
}
return defaultValue
} }
// 以变量字典形式获取配置项。 // 以变量字典形式获取配置项。

View File

@ -1,12 +1,12 @@
package domain package domain
const ( const (
NotifyChannelDingtalk = "dingtalk" NotifyChannelEmail = "email"
NotifyChannelWebhook = "webhook" NotifyChannelWebhook = "webhook"
NotifyChannelTelegram = "telegram" NotifyChannelDingtalk = "dingtalk"
NotifyChannelLark = "lark" NotifyChannelLark = "lark"
NotifyChannelTelegram = "telegram"
NotifyChannelServerChan = "serverchan" NotifyChannelServerChan = "serverchan"
NotifyChannelMail = "mail"
NotifyChannelBark = "bark" NotifyChannelBark = "bark"
) )

View File

@ -24,7 +24,7 @@ func (s *Setting) GetChannelContent(channel string) (map[string]any, error) {
v, ok := (*conf)[channel] v, ok := (*conf)[channel]
if !ok { if !ok {
return nil, fmt.Errorf("channel %s not found", channel) return nil, fmt.Errorf("channel \"%s\" not found", channel)
} }
return v, nil return v, nil

View File

@ -12,19 +12,13 @@ import (
"github.com/usual2970/certimate/internal/utils/xtime" "github.com/usual2970/certimate/internal/utils/xtime"
) )
type msg struct {
subject string
message string
}
const ( const (
defaultExpireSubject = "您有{COUNT}张证书即将过期" defaultExpireSubject = "您有 {COUNT} 张证书即将过期"
defaultExpireMsg = "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!" defaultExpireMessage = "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!"
) )
func PushExpireMsg() { func PushExpireMsg() {
// 查询即将过期的证书 // 查询即将过期的证书
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0, records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)}) dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
if err != nil { if err != nil {
@ -34,12 +28,12 @@ func PushExpireMsg() {
// 组装消息 // 组装消息
msg := buildMsg(records) msg := buildMsg(records)
if msg == nil { if msg == nil {
return return
} }
if err := Send(msg.subject, msg.message); err != nil { // 发送通知
if err := SendToAllChannels(msg.Subject, msg.Message); err != nil {
app.GetApp().Logger().Error("send expire msg", "error", err) app.GetApp().Logger().Error("send expire msg", "error", err)
} }
} }
@ -53,22 +47,27 @@ type notifyTemplate struct {
Content string `json:"content"` Content string `json:"content"`
} }
func buildMsg(records []*models.Record) *msg { type notifyMessage struct {
Subject string
Message string
}
func buildMsg(records []*models.Record) *notifyMessage {
if len(records) == 0 { if len(records) == 0 {
return nil return nil
} }
// 查询模板信息 // 查询模板信息
templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'") templateRecord, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='templates'")
title := defaultExpireSubject subject := defaultExpireSubject
content := defaultExpireMsg message := defaultExpireMessage
if err == nil { if err == nil {
var templates *notifyTemplates var templates *notifyTemplates
templateRecord.UnmarshalJSONField("content", templates) templateRecord.UnmarshalJSONField("content", templates)
if templates != nil && len(templates.NotifyTemplates) > 0 { if templates != nil && len(templates.NotifyTemplates) > 0 {
title = templates.NotifyTemplates[0].Title subject = templates.NotifyTemplates[0].Title
content = templates.NotifyTemplates[0].Content message = templates.NotifyTemplates[0].Content
} }
} }
@ -81,17 +80,17 @@ func buildMsg(records []*models.Record) *msg {
} }
countStr := strconv.Itoa(count) countStr := strconv.Itoa(count)
domainStr := strings.Join(domains, ",") domainStr := strings.Join(domains, ";")
title = strings.ReplaceAll(title, "{COUNT}", countStr) subject = strings.ReplaceAll(subject, "{COUNT}", countStr)
title = strings.ReplaceAll(title, "{DOMAINS}", domainStr) subject = strings.ReplaceAll(subject, "{DOMAINS}", domainStr)
content = strings.ReplaceAll(content, "{COUNT}", countStr) message = strings.ReplaceAll(message, "{COUNT}", countStr)
content = strings.ReplaceAll(content, "{DOMAINS}", domainStr) message = strings.ReplaceAll(message, "{DOMAINS}", domainStr)
// 返回消息 // 返回消息
return &msg{ return &notifyMessage{
subject: title, Subject: subject,
message: content, Message: message,
} }
} }

View File

@ -0,0 +1,66 @@
package notify
import (
"errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
notifierBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
notifierDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
notifierLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
notifierServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
notifierTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
notifierWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
)
func createNotifier(channel string, channelConfig map[string]any) (notifier.Notifier, error) {
switch channel {
case domain.NotifyChannelEmail:
return notifierEmail.New(&notifierEmail.EmailNotifierConfig{
SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"),
SmtpPort: maps.GetValueAsInt32(channelConfig, "smtpPort"),
SmtpTLS: maps.GetValueOrDefaultAsBool(channelConfig, "smtpTLS", true),
Username: maps.GetValueOrDefaultAsString(channelConfig, "username", maps.GetValueAsString(channelConfig, "senderAddress")),
Password: maps.GetValueAsString(channelConfig, "password"),
SenderAddress: maps.GetValueAsString(channelConfig, "senderAddress"),
ReceiverAddress: maps.GetValueAsString(channelConfig, "receiverAddress"),
})
case domain.NotifyChannelWebhook:
return notifierWebhook.New(&notifierWebhook.WebhookNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelDingtalk:
return notifierDingTalk.New(&notifierDingTalk.DingTalkNotifierConfig{
AccessToken: maps.GetValueAsString(channelConfig, "accessToken"),
Secret: maps.GetValueAsString(channelConfig, "secret"),
})
case domain.NotifyChannelLark:
return notifierLark.New(&notifierLark.LarkNotifierConfig{
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
})
case domain.NotifyChannelTelegram:
return notifierTelegram.New(&notifierTelegram.TelegramNotifierConfig{
ApiToken: maps.GetValueAsString(channelConfig, "apiToken"),
ChatId: maps.GetValueAsInt64(channelConfig, "chatId"),
})
case domain.NotifyChannelServerChan:
return notifierServerChan.New(&notifierServerChan.ServerChanNotifierConfig{
Url: maps.GetValueAsString(channelConfig, "url"),
})
case domain.NotifyChannelBark:
return notifierBark.New(&notifierBark.BarkNotifierConfig{
DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"),
ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"),
})
}
return nil, errors.New("unsupported notifier channel")
}

View File

@ -1,56 +0,0 @@
package notify
import (
"context"
"fmt"
"net/mail"
"strconv"
"github.com/pocketbase/pocketbase/tools/mailer"
)
const defaultSmtpHostPort = "25"
type Mail struct {
username string
to string
client *mailer.SmtpClient
}
func NewMail(senderAddress, receiverAddresses, smtpHostAddr, smtpHostPort, password string) (*Mail, error) {
if smtpHostPort == "" {
smtpHostPort = defaultSmtpHostPort
}
port, err := strconv.Atoi(smtpHostPort)
if err != nil {
return nil, fmt.Errorf("invalid smtp port: %w", err)
}
client := mailer.SmtpClient{
Host: smtpHostAddr,
Port: port,
Username: senderAddress,
Password: password,
Tls: true,
}
return &Mail{
username: senderAddress,
client: &client,
to: receiverAddresses,
}, nil
}
func (m *Mail) Send(ctx context.Context, subject, content string) error {
message := &mailer.Message{
From: mail.Address{
Address: m.username,
},
To: []mail.Address{{Address: m.to}},
Subject: subject,
Text: content,
}
return m.client.Send(message)
}

View File

@ -3,24 +3,16 @@ package notify
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
stdhttp "net/http" "golang.org/x/sync/errgroup"
"github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/core/notifier"
"github.com/usual2970/certimate/internal/pkg/utils/maps"
"github.com/usual2970/certimate/internal/utils/app" "github.com/usual2970/certimate/internal/utils/app"
notifyPackage "github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/bark"
"github.com/nikoksr/notify/service/dingding"
"github.com/nikoksr/notify/service/http"
"github.com/nikoksr/notify/service/lark"
"github.com/nikoksr/notify/service/telegram"
) )
func Send(title, content string) error { func SendToAllChannels(subject, message string) error {
// 获取所有的推送渠道 notifiers, err := getEnabledNotifiers()
notifiers, err := getNotifiers()
if err != nil { if err != nil {
return err return err
} }
@ -28,184 +20,56 @@ func Send(title, content string) error {
return nil return nil
} }
n := notifyPackage.New() var eg errgroup.Group
// 添加推送渠道 for _, n := range notifiers {
n.UseServices(notifiers...) if n == nil {
continue
}
// 发送消息 eg.Go(func() error {
return n.Send(context.Background(), title, content) _, err := n.Notify(context.Background(), subject, message)
return err
})
}
err = eg.Wait()
return err
} }
type sendTestParam struct { func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
Title string `json:"title"` notifier, err := createNotifier(channel, channelConfig)
Content string `json:"content"`
Channel string `json:"channel"`
Conf map[string]any `json:"conf"`
}
func SendTest(param *sendTestParam) error {
notifier, err := getNotifier(param.Channel, param.Conf)
if err != nil { if err != nil {
return err return err
} }
n := notifyPackage.New() _, err = notifier.Notify(context.Background(), subject, message)
return err
// 添加推送渠道
n.UseServices(notifier)
// 发送消息
return n.Send(context.Background(), param.Title, param.Content)
} }
func getNotifiers() ([]notifyPackage.Notifier, error) { func getEnabledNotifiers() ([]notifier.Notifier, error) {
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'") settings, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='notifyChannels'")
if err != nil { if err != nil {
return nil, fmt.Errorf("find notifyChannels error: %w", err) return nil, fmt.Errorf("find notifyChannels error: %w", err)
} }
notifiers := make([]notifyPackage.Notifier, 0)
rs := make(map[string]map[string]any) rs := make(map[string]map[string]any)
if err := settings.UnmarshalJSONField("content", &rs); err != nil {
if err := resp.UnmarshalJSONField("content", &rs); err != nil {
return nil, fmt.Errorf("unmarshal notifyChannels error: %w", err) return nil, fmt.Errorf("unmarshal notifyChannels error: %w", err)
} }
notifiers := make([]notifier.Notifier, 0)
for k, v := range rs { for k, v := range rs {
if !maps.GetValueAsBool(v, "enabled") {
if !getBool(v, "enabled") {
continue continue
} }
notifier, err := getNotifier(k, v) notifier, err := createNotifier(k, v)
if err != nil { if err != nil {
continue continue
} }
notifiers = append(notifiers, notifier) notifiers = append(notifiers, notifier)
} }
return notifiers, nil return notifiers, nil
} }
func getNotifier(channel string, conf map[string]any) (notifyPackage.Notifier, error) {
switch channel {
case domain.NotifyChannelTelegram:
temp := getTelegramNotifier(conf)
if temp == nil {
return nil, fmt.Errorf("telegram notifier config error")
}
return temp, nil
case domain.NotifyChannelDingtalk:
return getDingTalkNotifier(conf), nil
case domain.NotifyChannelLark:
return getLarkNotifier(conf), nil
case domain.NotifyChannelWebhook:
return getWebhookNotifier(conf), nil
case domain.NotifyChannelServerChan:
return getServerChanNotifier(conf), nil
case domain.NotifyChannelMail:
return getMailNotifier(conf)
case domain.NotifyChannelBark:
return getBarkNotifier(conf), nil
}
return nil, fmt.Errorf("notifier not found")
}
func getWebhookNotifier(conf map[string]any) notifyPackage.Notifier {
rs := http.New()
rs.AddReceiversURLs(getString(conf, "url"))
return rs
}
func getTelegramNotifier(conf map[string]any) notifyPackage.Notifier {
rs, err := telegram.New(getString(conf, "apiToken"))
if err != nil {
return nil
}
chatId := getString(conf, "chatId")
id, err := strconv.ParseInt(chatId, 10, 64)
if err != nil {
return nil
}
rs.AddReceivers(id)
return rs
}
func getServerChanNotifier(conf map[string]any) notifyPackage.Notifier {
rs := http.New()
rs.AddReceivers(&http.Webhook{
URL: getString(conf, "url"),
Header: stdhttp.Header{},
ContentType: "application/json",
Method: stdhttp.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]string{
"text": subject,
"desp": message,
}
},
})
return rs
}
func getBarkNotifier(conf map[string]any) notifyPackage.Notifier {
deviceKey := getString(conf, "deviceKey")
serverURL := getString(conf, "serverUrl")
if serverURL == "" {
return bark.New(deviceKey)
}
return bark.NewWithServers(deviceKey, serverURL)
}
func getDingTalkNotifier(conf map[string]any) notifyPackage.Notifier {
return dingding.New(&dingding.Config{
Token: getString(conf, "accessToken"),
Secret: getString(conf, "secret"),
})
}
func getLarkNotifier(conf map[string]any) notifyPackage.Notifier {
return lark.NewWebhookService(getString(conf, "webhookUrl"))
}
func getMailNotifier(conf map[string]any) (notifyPackage.Notifier, error) {
rs, err := NewMail(getString(conf, "senderAddress"),
getString(conf, "receiverAddresses"),
getString(conf, "smtpHostAddr"),
getString(conf, "smtpHostPort"),
getString(conf, "password"),
)
if err != nil {
return nil, err
}
return rs, nil
}
func getString(conf map[string]any, key string) string {
if _, ok := conf[key]; !ok {
return ""
}
return conf[key].(string)
}
func getBool(conf map[string]any, key string) bool {
if _, ok := conf[key]; !ok {
return false
}
return conf[key].(bool)
}

View File

@ -29,18 +29,13 @@ func NewNotifyService(settingRepo SettingRepository) *NotifyService {
func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error { func (n *NotifyService) Test(ctx context.Context, req *domain.NotifyTestPushReq) error {
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels") setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
if err != nil { if err != nil {
return fmt.Errorf("get notify channels setting failed: %w", err) return fmt.Errorf("failed to get notify channels settings: %w", err)
} }
conf, err := setting.GetChannelContent(req.Channel) channelConfig, err := setting.GetChannelContent(req.Channel)
if err != nil { if err != nil {
return fmt.Errorf("get notify channel %s config failed: %w", req.Channel, err) return fmt.Errorf("failed to get notify channel \"%s\" config: %w", req.Channel, err)
} }
return SendTest(&sendTestParam{ return SendToChannel(notifyTestTitle, notifyTestBody, req.Channel, channelConfig)
Title: notifyTestTitle,
Content: notifyTestBody,
Channel: req.Channel,
Conf: conf,
})
} }

View File

@ -0,0 +1,23 @@
package notifier
import "context"
// 表示定义消息通知器的抽象类型接口。
type Notifier interface {
// 发送通知。
//
// 入参:
// - ctx上下文。
// - subject通知主题。
// - message通知内容。
//
// 出参:
// - res发送结果。
// - err: 错误。
Notify(ctx context.Context, subject string, message string) (res *NotifyResult, err error)
}
// 表示通知发送结果的数据结构。
type NotifyResult struct {
NotificationData map[string]any `json:"notificationData,omitempty"`
}

View File

@ -0,0 +1,48 @@
package bark
import (
"context"
"errors"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/bark"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type BarkNotifierConfig struct {
ServerUrl string `json:"serverUrl"`
DeviceKey string `json:"deviceKey"`
}
type BarkNotifier struct {
config *BarkNotifierConfig
}
var _ notifier.Notifier = (*BarkNotifier)(nil)
func New(config *BarkNotifierConfig) (*BarkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &BarkNotifier{
config: config,
}, nil
}
func (n *BarkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
var srv notify.Notifier
if n.config.ServerUrl == "" {
srv = bark.New(n.config.DeviceKey)
} else {
srv = bark.NewWithServers(n.config.DeviceKey, n.config.ServerUrl)
}
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@ -0,0 +1,45 @@
package dingtalk
import (
"context"
"errors"
"github.com/nikoksr/notify/service/dingding"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type DingTalkNotifierConfig struct {
AccessToken string `json:"accessToken"`
Secret string `json:"secret"`
}
type DingTalkNotifier struct {
config *DingTalkNotifierConfig
}
var _ notifier.Notifier = (*DingTalkNotifier)(nil)
func New(config *DingTalkNotifierConfig) (*DingTalkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &DingTalkNotifier{
config: config,
}, nil
}
func (n *DingTalkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := dingding.New(&dingding.Config{
Token: n.config.AccessToken,
Secret: n.config.Secret,
})
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@ -0,0 +1,95 @@
package email
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/smtp"
"github.com/domodwyer/mailyak/v3"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type EmailNotifierConfig struct {
SmtpHost string `json:"smtpHost"`
SmtpPort int32 `json:"smtpPort"`
SmtpTLS bool `json:"smtpTLS"`
Username string `json:"username"`
Password string `json:"password"`
SenderAddress string `json:"senderAddress"`
ReceiverAddress string `json:"receiverAddress"`
}
type EmailNotifier struct {
config *EmailNotifierConfig
}
var _ notifier.Notifier = (*EmailNotifier)(nil)
func New(config *EmailNotifierConfig) (*EmailNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &EmailNotifier{
config: config,
}, nil
}
func (n *EmailNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
var smtpAuth smtp.Auth
if n.config.Username != "" || n.config.Password != "" {
smtpAuth = smtp.PlainAuth("", n.config.Username, n.config.Password, n.config.SmtpHost)
}
var smtpAddr string
if n.config.SmtpPort == 0 {
if n.config.SmtpTLS {
smtpAddr = fmt.Sprintf("%s:465", n.config.SmtpHost)
} else {
smtpAddr = fmt.Sprintf("%s:25", n.config.SmtpHost)
}
} else {
smtpAddr = fmt.Sprintf("%s:%d", n.config.SmtpHost, n.config.SmtpPort)
}
var yak *mailyak.MailYak
if n.config.SmtpTLS {
yak, err = mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig())
if err != nil {
return nil, err
}
} else {
yak = mailyak.New(smtpAddr, smtpAuth)
}
yak.From(n.config.SenderAddress)
yak.To(n.config.ReceiverAddress)
yak.Subject(subject)
yak.Plain().Set(message)
err = yak.Send()
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}
func newTlsConfig() *tls.Config {
var suiteIds []uint16
for _, suite := range tls.CipherSuites() {
suiteIds = append(suiteIds, suite.ID)
}
for _, suite := range tls.InsecureCipherSuites() {
suiteIds = append(suiteIds, suite.ID)
}
// 为兼容国内部分低版本 TLS 的 SMTP 服务商
return &tls.Config{
MinVersion: tls.VersionTLS10,
CipherSuites: suiteIds,
}
}

View File

@ -0,0 +1,51 @@
package email_test
import (
"os"
"strconv"
"testing"
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
)
/*
Shell command to run this test:
CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
CERTIMATE_NOTIFIER_EMAIL_USERNAME="your-username" \
CERTIMATE_NOTIFIER_EMAIL_PASSWORD="your-password" \
CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS="sender@example.com" \
CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS="receiver@example.com" \
go test -v -run TestNotify email_test.go
*/
func TestNotify(t *testing.T) {
smtpPort, err := strconv.ParseInt(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPPORT"), 10, 32)
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
smtpTLS, err := strconv.ParseBool(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPTLS"))
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
res, err := notifierEmail.New(&notifierEmail.EmailNotifierConfig{
SmtpHost: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPHOST"),
SmtpPort: int32(smtpPort),
SmtpTLS: smtpTLS,
Username: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_USERNAME"),
Password: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_PASSWORD"),
SenderAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS"),
ReceiverAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS"),
})
if err != nil {
t.Errorf("invalid envvar: %+v", err)
panic(err)
}
t.Logf("notify result: %v", res)
}

View File

@ -0,0 +1,41 @@
package lark
import (
"context"
"errors"
"github.com/nikoksr/notify/service/lark"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type LarkNotifierConfig struct {
WebhookUrl string `json:"webhookUrl"`
}
type LarkNotifier struct {
config *LarkNotifierConfig
}
var _ notifier.Notifier = (*LarkNotifier)(nil)
func New(config *LarkNotifierConfig) (*LarkNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &LarkNotifier{
config: config,
}, nil
}
func (n *LarkNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := lark.NewWebhookService(n.config.WebhookUrl)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@ -0,0 +1,55 @@
package serverchan
import (
"context"
"errors"
"net/http"
notifyHttp "github.com/nikoksr/notify/service/http"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type ServerChanNotifierConfig struct {
Url string `json:"url"`
}
type ServerChanNotifier struct {
config *ServerChanNotifierConfig
}
var _ notifier.Notifier = (*ServerChanNotifier)(nil)
func New(config *ServerChanNotifierConfig) (*ServerChanNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &ServerChanNotifier{
config: config,
}, nil
}
func (n *ServerChanNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := notifyHttp.New()
srv.AddReceivers(&notifyHttp.Webhook{
URL: n.config.Url,
Header: http.Header{},
ContentType: "application/json",
Method: http.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]string{
"text": subject,
"desp": message,
}
},
})
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@ -0,0 +1,47 @@
package telegram
import (
"context"
"errors"
"github.com/nikoksr/notify/service/telegram"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type TelegramNotifierConfig struct {
ApiToken string `json:"apiToken"`
ChatId int64 `json:"chatId"`
}
type TelegramNotifier struct {
config *TelegramNotifierConfig
}
var _ notifier.Notifier = (*TelegramNotifier)(nil)
func New(config *TelegramNotifierConfig) (*TelegramNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &TelegramNotifier{
config: config,
}, nil
}
func (n *TelegramNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv, err := telegram.New(n.config.ApiToken)
if err != nil {
return nil, err
}
srv.AddReceivers(n.config.ChatId)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@ -0,0 +1,43 @@
package webhook
import (
"context"
"errors"
"github.com/nikoksr/notify/service/http"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type WebhookNotifierConfig struct {
Url string `json:"url"`
}
type WebhookNotifier struct {
config *WebhookNotifierConfig
}
var _ notifier.Notifier = (*WebhookNotifier)(nil)
func New(config *WebhookNotifierConfig) (*WebhookNotifier, error) {
if config == nil {
return nil, errors.New("config is nil")
}
return &WebhookNotifier{
config: config,
}, nil
}
func (n *WebhookNotifier) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := http.New()
srv.AddReceiversURLs(n.config.Url)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@ -2,6 +2,7 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -26,7 +27,13 @@ type AliyunCASUploader struct {
sdkClient *aliyunCas.Client sdkClient *aliyunCas.Client
} }
var _ uploader.Uploader = (*AliyunCASUploader)(nil)
func New(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) { func New(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient( client, err := createSdkClient(
config.AccessKeyId, config.AccessKeyId,
config.AccessKeySecret, config.AccessKeySecret,

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -28,7 +29,13 @@ type AliyunSLBUploader struct {
sdkClient *aliyunSlb.Client sdkClient *aliyunSlb.Client
} }
var _ uploader.Uploader = (*AliyunSLBUploader)(nil)
func New(config *AliyunSLBUploaderConfig) (*AliyunSLBUploader, error) { func New(config *AliyunSLBUploaderConfig) (*AliyunSLBUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient( client, err := createSdkClient(
config.AccessKeyId, config.AccessKeyId,
config.AccessKeySecret, config.AccessKeySecret,

View File

@ -2,6 +2,7 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
@ -21,7 +22,13 @@ type DogeCloudUploader struct {
sdkClient *doge.Client sdkClient *doge.Client
} }
var _ uploader.Uploader = (*DogeCloudUploader)(nil)
func New(config *DogeCloudUploaderConfig) (*DogeCloudUploader, error) { func New(config *DogeCloudUploaderConfig) (*DogeCloudUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient( client, err := createSdkClient(
config.AccessKey, config.AccessKey,
config.SecretKey, config.SecretKey,

View File

@ -32,7 +32,13 @@ type HuaweiCloudELBUploader struct {
sdkClient *hcElb.ElbClient sdkClient *hcElb.ElbClient
} }
var _ uploader.Uploader = (*HuaweiCloudELBUploader)(nil)
func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) { func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient( client, err := createSdkClient(
config.AccessKeyId, config.AccessKeyId,
config.SecretAccessKey, config.SecretAccessKey,

View File

@ -2,6 +2,7 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
@ -27,7 +28,13 @@ type HuaweiCloudSCMUploader struct {
sdkClient *hcScm.ScmClient sdkClient *hcScm.ScmClient
} }
var _ uploader.Uploader = (*HuaweiCloudSCMUploader)(nil)
func New(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) { func New(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient( client, err := createSdkClient(
config.AccessKeyId, config.AccessKeyId,
config.SecretAccessKey, config.SecretAccessKey,

View File

@ -2,6 +2,7 @@
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
@ -23,7 +24,13 @@ type QiniuSSLCertUploader struct {
sdkClient *qiniuEx.Client sdkClient *qiniuEx.Client
} }
var _ uploader.Uploader = (*QiniuSSLCertUploader)(nil)
func New(config *QiniuSSLCertUploaderConfig) (*QiniuSSLCertUploader, error) { func New(config *QiniuSSLCertUploaderConfig) (*QiniuSSLCertUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient( client, err := createSdkClient(
config.AccessKey, config.AccessKey,
config.SecretKey, config.SecretKey,

View File

@ -2,6 +2,7 @@
import ( import (
"context" "context"
"errors"
xerrors "github.com/pkg/errors" xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
@ -21,7 +22,13 @@ type TencentCloudSSLUploader struct {
sdkClient *tcSsl.Client sdkClient *tcSsl.Client
} }
var _ uploader.Uploader = (*TencentCloudSSLUploader)(nil)
func New(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) { func New(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient( client, err := createSdkClient(
config.SecretId, config.SecretId,
config.SecretKey, config.SecretKey,

View File

@ -2,7 +2,7 @@
import "context" import "context"
// 表示定义证书上传的抽象类型接口。 // 表示定义证书上传的抽象类型接口。
// 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。 // 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。
// 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。 // 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。
type Uploader interface { type Uploader interface {

View File

@ -0,0 +1,164 @@
package maps
import "strconv"
// 以字符串形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回空字符串。
func GetValueAsString(dict map[string]any, key string) string {
return GetValueOrDefaultAsString(dict, key, "")
}
// 以字符串形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回默认值。
func GetValueOrDefaultAsString(dict map[string]any, key string, defaultValue string) string {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(string); ok {
return result
}
}
return defaultValue
}
// 以 32 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回 0。
func GetValueAsInt32(dict map[string]any, key string) int32 {
return GetValueOrDefaultAsInt32(dict, key, 0)
}
// 以 32 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回默认值。
func GetValueOrDefaultAsInt32(dict map[string]any, key string, defaultValue int32) int32 {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(int32); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 32); err == nil {
return int32(result)
}
}
}
return defaultValue
}
// 以 64 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回 0。
func GetValueAsInt64(dict map[string]any, key string) int64 {
return GetValueOrDefaultAsInt64(dict, key, 0)
}
// 以 64 位整数形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回默认值。
func GetValueOrDefaultAsInt64(dict map[string]any, key string, defaultValue int64) int64 {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(int64); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 64); err == nil {
return result
}
}
}
return defaultValue
}
// 以布尔形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是布尔,则返回 false。
func GetValueAsBool(dict map[string]any, key string) bool {
return GetValueOrDefaultAsBool(dict, key, false)
}
// 以布尔形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
// - defaultValue: 默认值。
//
// 出参:
// - 字典中键对应的值。如果指定键不存在或者值的类型不是布尔,则返回默认值。
func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool) bool {
if dict == nil {
return defaultValue
}
if value, ok := dict[key]; ok {
if result, ok := value.(bool); ok {
return result
}
// 兼容字符串类型的值
if str, ok := value.(string); ok {
if result, err := strconv.ParseBool(str); err == nil {
return result
}
}
}
return defaultValue
}

View File

@ -123,22 +123,29 @@ const Bark = () => {
} }
}; };
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => { const handlePushTestClick = async () => {
if (testing) return;
try { try {
setTesting(true);
await notifyTest("bark"); await notifyTest("bark");
toast({ toast({
title: t("settings.notification.config.push.test.message.success.message"), title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.config.push.test.message.success.message"), description: t("settings.notification.push_test_message.succeeded.message"),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: t("settings.notification.config.push.test.message.failed.message"), title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`, description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} finally {
setTesting(false);
} }
}; };
@ -177,9 +184,11 @@ const Bark = () => {
}; };
return ( return (
<div className="flex flex-col space-y-4">
<div> <div>
<Label>{t("settings.notification.bark.server_url.label")}</Label>
<Input <Input
placeholder={t("settings.notification.bark.serverUrl.placeholder")} placeholder={t("settings.notification.bark.server_url.placeholder")}
value={bark.data.serverUrl} value={bark.data.serverUrl}
onChange={(e) => { onChange={(e) => {
const newData = { const newData = {
@ -194,9 +203,12 @@ const Bark = () => {
setBark(newData); setBark(newData);
}} }}
/> />
</div>
<div>
<Label>{t("settings.notification.bark.device_key.label")}</Label>
<Input <Input
placeholder={t("settings.notification.bark.deviceKey.placeholder")} placeholder={t("settings.notification.bark.device_key.placeholder")}
value={bark.data.deviceKey} value={bark.data.deviceKey}
onChange={(e) => { onChange={(e) => {
const newData = { const newData = {
@ -211,13 +223,15 @@ const Bark = () => {
setBark(newData); setBark(newData);
}} }}
/> />
</div>
<div className="flex items-center space-x-1 mt-2"> <div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} /> <Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label> <Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex items-center space-x-1">
<Show when={changed}> <Show when={changed}>
<Button <Button
onClick={() => { onClick={() => {
@ -231,15 +245,17 @@ const Bark = () => {
<Show when={!changed && bark.id != ""}> <Show when={!changed && bark.id != ""}>
<Button <Button
variant="secondary" variant="secondary"
loading={testing}
onClick={() => { onClick={() => {
handlePushTestClick(); handlePushTestClick();
}} }}
> >
{t("settings.notification.config.push.test.message")} {t("settings.notification.push_test_message")}
</Button> </Button>
</Show> </Show>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -120,23 +120,30 @@ const DingTalk = () => {
description: `${t("settings.notification.config.failed.message")}: ${msg}`, description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} finally {
setTesting(false);
} }
}; };
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => { const handlePushTestClick = async () => {
if (testing) return;
try { try {
setTesting(true);
await notifyTest("dingtalk"); await notifyTest("dingtalk");
toast({ toast({
title: t("settings.notification.config.push.test.message.success.message"), title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.config.push.test.message.success.message"), description: t("settings.notification.push_test_message.succeeded.message"),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: t("settings.notification.config.push.test.message.failed.message"), title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`, description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} }
@ -177,9 +184,11 @@ const DingTalk = () => {
}; };
return ( return (
<div className="flex flex-col space-y-4">
<div> <div>
<Label>{t("settings.notification.dingtalk.access_token.label")}</Label>
<Input <Input
placeholder="AccessToken" placeholder={t("settings.notification.dingtalk.access_token.placeholder")}
value={dingtalk.data.accessToken} value={dingtalk.data.accessToken}
onChange={(e) => { onChange={(e) => {
const newData = { const newData = {
@ -193,9 +202,12 @@ const DingTalk = () => {
setDingtalk(newData); setDingtalk(newData);
}} }}
/> />
</div>
<div>
<Label>{t("settings.notification.dingtalk.secret.label")}</Label>
<Input <Input
placeholder={t("settings.notification.dingtalk.secret.placeholder")} placeholder={t("settings.notification.dingtalk.secret.placeholder")}
className="mt-2"
value={dingtalk.data.secret} value={dingtalk.data.secret}
onChange={(e) => { onChange={(e) => {
const newData = { const newData = {
@ -209,12 +221,15 @@ const DingTalk = () => {
setDingtalk(newData); setDingtalk(newData);
}} }}
/> />
<div className="flex items-center space-x-1 mt-2"> </div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} /> <Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label> <Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex items-center space-x-1">
<Show when={changed}> <Show when={changed}>
<Button <Button
onClick={() => { onClick={() => {
@ -228,15 +243,17 @@ const DingTalk = () => {
<Show when={!changed && dingtalk.id != ""}> <Show when={!changed && dingtalk.id != ""}>
<Button <Button
variant="secondary" variant="secondary"
loading={testing}
onClick={() => { onClick={() => {
handlePushTestClick(); handlePushTestClick();
}} }}
> >
{t("settings.notification.config.push.test.message")} {t("settings.notification.push_test_message")}
</Button> </Button>
</Show> </Show>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -0,0 +1,384 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMessage } from "@/lib/error";
import { NotifyChannelEmail, NotifyChannels } from "@/domain/settings";
import { useNotifyContext } from "@/providers/notify";
import { update } from "@/repository/settings";
import Show from "@/components/Show";
import { notifyTest } from "@/api/notify";
type EmailSetting = {
id: string;
name: string;
data: NotifyChannelEmail;
};
const Mail = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [mail, setMail] = useState<EmailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
smtpHost: "",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
},
});
const [originMail, setOriginMail] = useState<EmailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
smtpHost: "",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailMail();
setOriginMail({
id: config.id ?? "",
name: "email",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailMail();
setMail({
id: config.id ?? "",
name: "email",
data,
});
}, [config]);
const { toast } = useToast();
const getDetailMail = () => {
const df: NotifyChannelEmail = {
smtpHost: "smtp.example.com",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.email) {
return df;
}
return chanels.email as NotifyChannelEmail;
};
const checkChanged = (data: NotifyChannelEmail) => {
if (
data.smtpHost !== originMail.data.smtpHost ||
data.smtpPort !== originMail.data.smtpPort ||
data.smtpTLS !== originMail.data.smtpTLS ||
data.username !== originMail.data.username ||
data.password !== originMail.data.password ||
data.senderAddress !== originMail.data.senderAddress ||
data.receiverAddress !== originMail.data.receiverAddress
) {
setChanged(true);
} else {
setChanged(false);
}
};
const handleSaveClick = async () => {
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
email: {
...mail.data,
},
},
});
setChannels(resp);
toast({
title: t("common.save.succeeded.message"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("common.save.failed.message"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("email");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const handleSwitchChange = async () => {
const newData = {
...mail,
data: {
...mail.data,
enabled: !mail.data.enabled,
},
};
setMail(newData);
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
email: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("common.save.failed.message"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div className="flex space-x-4">
<div className="w-2/5">
<Label>{t("settings.notification.email.smtp_host.label")}</Label>
<Input
placeholder={t("settings.notification.email.smtp_host.placeholder")}
value={mail.data.smtpHost}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpHost: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-2/5">
<Label>{t("settings.notification.email.smtp_port.label")}</Label>
<Input
type="number"
placeholder={t("settings.notification.email.smtp_port.placeholder")}
value={mail.data.smtpPort}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpPort: +e.target.value || 0,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-1/5">
<Label>{t("settings.notification.email.smtp_tls.label")}</Label>
<Switch
className="block mt-2"
checked={mail.data.smtpTLS}
onCheckedChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpPort: e && mail.data.smtpPort === 25 ? 465 : !e && mail.data.smtpPort === 465 ? 25 : mail.data.smtpPort,
smtpTLS: e,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
</div>
<div className="flex space-x-4">
<div className="w-1/2">
<Label>{t("settings.notification.email.username.label")}</Label>
<Input
placeholder={t("settings.notification.email.username.placeholder")}
value={mail.data.username}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
username: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-1/2">
<Label>{t("settings.notification.email.password.label")}</Label>
<Input
placeholder={t("settings.notification.email.password.placeholder")}
value={mail.data.password}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
password: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
</div>
<div>
<Label>{t("settings.notification.email.sender_address.label")}</Label>
<Input
placeholder={t("settings.notification.email.sender_address.placeholder")}
value={mail.data.senderAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
senderAddress: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.email.receiver_address.label")}</Label>
<Input
placeholder={t("settings.notification.email.receiver_address.placeholder")}
value={mail.data.receiverAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
receiverAddress: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && mail.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Mail;

View File

@ -116,23 +116,30 @@ const Lark = () => {
description: `${t("settings.notification.config.failed.message")}: ${msg}`, description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} finally {
setTesting(false);
} }
}; };
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => { const handlePushTestClick = async () => {
if (testing) return;
try { try {
setTesting(true);
await notifyTest("lark"); await notifyTest("lark");
toast({ toast({
title: t("settings.notification.config.push.test.message.success.message"), title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.config.push.test.message.success.message"), description: t("settings.notification.push_test_message.succeeded.message"),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: t("settings.notification.config.push.test.message.failed.message"), title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`, description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} }
@ -173,9 +180,11 @@ const Lark = () => {
}; };
return ( return (
<div className="flex flex-col space-y-4">
<div> <div>
<Label>{t("settings.notification.lark.webhook_url.label")}</Label>
<Input <Input
placeholder="Webhook Url" placeholder={t("settings.notification.lark.webhook_url.placeholder")}
value={lark.data.webhookUrl} value={lark.data.webhookUrl}
onChange={(e) => { onChange={(e) => {
const newData = { const newData = {
@ -190,12 +199,15 @@ const Lark = () => {
setLark(newData); setLark(newData);
}} }}
/> />
<div className="flex items-center space-x-1 mt-2"> </div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} /> <Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label> <Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex items-center space-x-1">
<Show when={changed}> <Show when={changed}>
<Button <Button
onClick={() => { onClick={() => {
@ -209,15 +221,17 @@ const Lark = () => {
<Show when={!changed && lark.id != ""}> <Show when={!changed && lark.id != ""}>
<Button <Button
variant="secondary" variant="secondary"
loading={testing}
onClick={() => { onClick={() => {
handlePushTestClick(); handlePushTestClick();
}} }}
> >
{t("settings.notification.config.push.test.message")} {t("settings.notification.push_test_message")}
</Button> </Button>
</Show> </Show>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -1,319 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMessage } from "@/lib/error";
import { NotifyChannelMail, NotifyChannels } from "@/domain/settings";
import { useNotifyContext } from "@/providers/notify";
import { update } from "@/repository/settings";
import Show from "@/components/Show";
import { notifyTest } from "@/api/notify";
type MailSetting = {
id: string;
name: string;
data: NotifyChannelMail;
};
const Mail = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [mail, setmail] = useState<MailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
senderAddress: "",
receiverAddresses: "",
smtpHostAddr: "",
smtpHostPort: "25",
username: "",
password: "",
enabled: false,
},
});
const [originMail, setoriginMail] = useState<MailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
senderAddress: "",
receiverAddresses: "",
smtpHostAddr: "",
smtpHostPort: "25",
username: "",
password: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailMail();
setoriginMail({
id: config.id ?? "",
name: "mail",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailMail();
setmail({
id: config.id ?? "",
name: "mail",
data,
});
}, [config]);
const { toast } = useToast();
const getDetailMail = () => {
const df: NotifyChannelMail = {
senderAddress: "",
receiverAddresses: "",
smtpHostAddr: "",
smtpHostPort: "25",
username: "",
password: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.mail) {
return df;
}
return chanels.mail as NotifyChannelMail;
};
const checkChanged = (data: NotifyChannelMail) => {
if (data.senderAddress !== originMail.data.senderAddress || data.receiverAddresses !== originMail.data.receiverAddresses || data.smtpHostAddr !== originMail.data.smtpHostAddr || data.smtpHostPort !== originMail.data.smtpHostPort || data.username !== originMail.data.username || data.password !== originMail.data.password) {
setChanged(true);
} else {
setChanged(false);
}
};
const handleSaveClick = async () => {
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
mail: {
...mail.data,
},
},
});
setChannels(resp);
toast({
title: t("common.save.succeeded.message"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("common.save.failed.message"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const handlePushTestClick = async () => {
try {
await notifyTest("mail");
toast({
title: t("settings.notification.config.push.test.message.success.message"),
description: t("settings.notification.config.push.test.message.success.message"),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("settings.notification.config.push.test.message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const handleSwitchChange = async () => {
const newData = {
...mail,
data: {
...mail.data,
enabled: !mail.data.enabled,
},
};
setmail(newData);
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
mail: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMessage(e);
toast({
title: t("common.save.failed.message"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div>
<Input
placeholder={t("settings.notification.mail.sender_address.placeholder")}
value={mail.data.senderAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
senderAddress: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.receiver_address.placeholder")}
className="mt-2"
value={mail.data.receiverAddresses}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
receiverAddresses: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.smtp_host.placeholder")}
className="mt-2"
value={mail.data.smtpHostAddr}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpHostAddr: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.smtp_port.placeholder")}
className="mt-2"
value={mail.data.smtpHostPort}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpHostPort: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.username.placeholder")}
className="mt-2"
value={mail.data.username}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
username: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<Input
placeholder={t("settings.notification.mail.password.placeholder")}
className="mt-2"
value={mail.data.password}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
password: e.target.value,
},
};
checkChanged(newData.data);
setmail(newData);
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex justify-end mt-2">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</Show>
<Show when={!changed && mail.id != ""}>
<Button
variant="secondary"
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.config.push.test.message")}
</Button>
</Show>
</div>
</div>
);
};
export default Mail;

View File

@ -97,7 +97,7 @@ const ServerChan = () => {
if (!isValidURL(serverchan.data.url)) { if (!isValidURL(serverchan.data.url)) {
toast({ toast({
title: t("common.save.failed.message"), title: t("common.save.failed.message"),
description: t("settings.notification.url.errmsg.invalid"), description: t("common.errmsg.url_invalid"),
variant: "destructive", variant: "destructive",
}); });
return; return;
@ -130,22 +130,29 @@ const ServerChan = () => {
} }
}; };
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => { const handlePushTestClick = async () => {
if (testing) return;
try { try {
setTesting(true);
await notifyTest("serverchan"); await notifyTest("serverchan");
toast({ toast({
title: t("settings.notification.config.push.test.message.success.message"), title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.config.push.test.message.success.message"), description: t("settings.notification.push_test_message.succeeded.message"),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: t("settings.notification.config.push.test.message.failed.message"), title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`, description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} finally {
setTesting(false);
} }
}; };
@ -184,7 +191,9 @@ const ServerChan = () => {
}; };
return ( return (
<div className="flex flex-col space-y-4">
<div> <div>
<Label>{t("settings.notification.serverchan.url.label")}</Label>
<Input <Input
placeholder={t("settings.notification.serverchan.url.placeholder")} placeholder={t("settings.notification.serverchan.url.placeholder")}
value={serverchan.data.url} value={serverchan.data.url}
@ -201,13 +210,15 @@ const ServerChan = () => {
setServerChan(newData); setServerChan(newData);
}} }}
/> />
</div>
<div className="flex items-center space-x-1 mt-2"> <div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} /> <Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label> <Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex items-center space-x-1">
<Show when={changed}> <Show when={changed}>
<Button <Button
onClick={() => { onClick={() => {
@ -221,15 +232,17 @@ const ServerChan = () => {
<Show when={!changed && serverchan.id != ""}> <Show when={!changed && serverchan.id != ""}>
<Button <Button
variant="secondary" variant="secondary"
loading={testing}
onClick={() => { onClick={() => {
handlePushTestClick(); handlePushTestClick();
}} }}
> >
{t("settings.notification.config.push.test.message")} {t("settings.notification.push_test_message")}
</Button> </Button>
</Show> </Show>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -123,22 +123,29 @@ const Telegram = () => {
} }
}; };
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => { const handlePushTestClick = async () => {
if (testing) return;
try { try {
setTesting(true);
await notifyTest("telegram"); await notifyTest("telegram");
toast({ toast({
title: t("settings.notification.config.push.test.message.success.message"), title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.config.push.test.message.success.message"), description: t("settings.notification.push_test_message.succeeded.message"),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: t("settings.notification.config.push.test.message.failed.message"), title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`, description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} finally {
setTesting(false);
} }
}; };
@ -177,9 +184,11 @@ const Telegram = () => {
}; };
return ( return (
<div className="flex flex-col space-y-4">
<div> <div>
<Label>{t("settings.notification.telegram.api_token.label")}</Label>
<Input <Input
placeholder="ApiToken" placeholder={t("settings.notification.telegram.api_token.placeholder")}
value={telegram.data.apiToken} value={telegram.data.apiToken}
onChange={(e) => { onChange={(e) => {
const newData = { const newData = {
@ -194,9 +203,12 @@ const Telegram = () => {
setTelegram(newData); setTelegram(newData);
}} }}
/> />
</div>
<div>
<Label>{t("settings.notification.telegram.chat_id.label")}</Label>
<Input <Input
placeholder="ChatId" placeholder={t("settings.notification.telegram.chat_id.placeholder")}
value={telegram.data.chatId} value={telegram.data.chatId}
onChange={(e) => { onChange={(e) => {
const newData = { const newData = {
@ -211,13 +223,15 @@ const Telegram = () => {
setTelegram(newData); setTelegram(newData);
}} }}
/> />
</div>
<div className="flex items-center space-x-1 mt-2"> <div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} /> <Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label> <Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex items-center space-x-1">
<Show when={changed}> <Show when={changed}>
<Button <Button
onClick={() => { onClick={() => {
@ -231,15 +245,17 @@ const Telegram = () => {
<Show when={!changed && telegram.id != ""}> <Show when={!changed && telegram.id != ""}>
<Button <Button
variant="secondary" variant="secondary"
loading={testing}
onClick={() => { onClick={() => {
handlePushTestClick(); handlePushTestClick();
}} }}
> >
{t("settings.notification.config.push.test.message")} {t("settings.notification.push_test_message")}
</Button> </Button>
</Show> </Show>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -97,7 +97,7 @@ const Webhook = () => {
if (!isValidURL(webhook.data.url)) { if (!isValidURL(webhook.data.url)) {
toast({ toast({
title: t("common.save.failed.message"), title: t("common.save.failed.message"),
description: t("settings.notification.url.errmsg.invalid"), description: t("common.errmsg.url_invalid"),
variant: "destructive", variant: "destructive",
}); });
return; return;
@ -130,22 +130,29 @@ const Webhook = () => {
} }
}; };
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => { const handlePushTestClick = async () => {
if (testing) return;
try { try {
setTesting(true);
await notifyTest("webhook"); await notifyTest("webhook");
toast({ toast({
title: t("settings.notification.config.push.test.message.success.message"), title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.config.push.test.message.success.message"), description: t("settings.notification.push_test_message.succeeded.message"),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: t("settings.notification.config.push.test.message.failed.message"), title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`, description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} finally {
setTesting(false);
} }
}; };
@ -184,9 +191,11 @@ const Webhook = () => {
}; };
return ( return (
<div className="flex flex-col space-y-4">
<div> <div>
<Label>{t("settings.notification.webhook.url.label")}</Label>
<Input <Input
placeholder="Url" placeholder={t("settings.notification.webhook.url.placeholder")}
value={webhook.data.url} value={webhook.data.url}
onChange={(e) => { onChange={(e) => {
const newData = { const newData = {
@ -201,13 +210,15 @@ const Webhook = () => {
setWebhook(newData); setWebhook(newData);
}} }}
/> />
</div>
<div className="flex items-center space-x-1 mt-2"> <div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} /> <Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label> <Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex items-center space-x-1">
<Show when={changed}> <Show when={changed}>
<Button <Button
onClick={() => { onClick={() => {
@ -221,15 +232,17 @@ const Webhook = () => {
<Show when={!changed && webhook.id != ""}> <Show when={!changed && webhook.id != ""}>
<Button <Button
variant="secondary" variant="secondary"
loading={testing}
onClick={() => { onClick={() => {
handlePushTestClick(); handlePushTestClick();
}} }}
> >
{t("settings.notification.config.push.test.message")} {t("settings.notification.push_test_message")}
</Button> </Button>
</Show> </Show>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -1,6 +1,7 @@
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -32,11 +33,38 @@ const buttonVariants = cva(
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean;
loading?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => { const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, loading, asChild = false, children, ...props }, ref) => {
const Comp = asChild ? Slot : "button"; if (asChild) {
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />; return (
<Slot ref={ref} {...props}>
<>
{React.Children.map(children as React.ReactElement, (child: React.ReactElement) => {
return React.cloneElement(child, {
className: cn(buttonVariants({ variant, size }), className),
children: (
<>
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
{child.props.children}
</>
),
});
})}
</>
</Slot>
);
}
return (
<button className={cn(buttonVariants({ variant, size, className }))} disabled={loading} ref={ref} {...props}>
<>
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
{children}
</>
</button>
);
}); });
Button.displayName = "Button"; Button.displayName = "Button";

View File

@ -18,24 +18,40 @@ export type NotifyTemplate = {
}; };
export type NotifyChannels = { export type NotifyChannels = {
email?: NotifyChannelEmail;
webhook?: NotifyChannel;
dingtalk?: NotifyChannel; dingtalk?: NotifyChannel;
lark?: NotifyChannel; lark?: NotifyChannel;
telegram?: NotifyChannel; telegram?: NotifyChannel;
webhook?: NotifyChannel;
serverchan?: NotifyChannel; serverchan?: NotifyChannel;
mail?: NotifyChannelMail;
bark?: NotifyChannelBark; bark?: NotifyChannelBark;
}; };
export type NotifyChannel = export type NotifyChannel =
| NotifyChannelEmail
| NotifyChannelWebhook
| NotifyChannelDingTalk | NotifyChannelDingTalk
| NotifyChannelLark | NotifyChannelLark
| NotifyChannelTelegram | NotifyChannelTelegram
| NotifyChannelWebhook
| NotifyChannelServerChan | NotifyChannelServerChan
| NotifyChannelMail
| NotifyChannelBark; | NotifyChannelBark;
export type NotifyChannelEmail = {
smtpHost: string;
smtpPort: number;
smtpTLS: boolean;
username: string;
password: string;
senderAddress: string;
receiverAddress: string;
enabled: boolean;
};
export type NotifyChannelWebhook = {
url: string;
enabled: boolean;
};
export type NotifyChannelDingTalk = { export type NotifyChannelDingTalk = {
accessToken: string; accessToken: string;
secret: string; secret: string;
@ -53,26 +69,11 @@ export type NotifyChannelTelegram = {
enabled: boolean; enabled: boolean;
}; };
export type NotifyChannelWebhook = {
url: string;
enabled: boolean;
};
export type NotifyChannelServerChan = { export type NotifyChannelServerChan = {
url: string; url: string;
enabled: boolean; enabled: boolean;
}; };
export type NotifyChannelMail = {
senderAddress: string;
receiverAddresses: string;
smtpHostAddr: string;
smtpHostPort: string;
username: string;
password: string;
enabled: boolean;
};
export type NotifyChannelBark = { export type NotifyChannelBark = {
deviceKey: string; deviceKey: string;
serverUrl: string; serverUrl: string;

View File

@ -83,12 +83,12 @@
"common.provider.local": "Local Deployment", "common.provider.local": "Local Deployment",
"common.provider.ssh": "SSH Deployment", "common.provider.ssh": "SSH Deployment",
"common.provider.webhook": "Webhook", "common.provider.webhook": "Webhook",
"common.provider.serverchan": "ServerChan",
"common.provider.kubernetes": "Kubernetes", "common.provider.kubernetes": "Kubernetes",
"common.provider.kubernetes.secret": "Kubernetes - Secret", "common.provider.kubernetes.secret": "Kubernetes - Secret",
"common.provider.email": "Email",
"common.provider.dingtalk": "DingTalk", "common.provider.dingtalk": "DingTalk",
"common.provider.telegram": "Telegram",
"common.provider.lark": "Lark", "common.provider.lark": "Lark",
"common.provider.mail": "Mail", "common.provider.telegram": "Telegram",
"common.provider.serverchan": "ServerChan",
"common.provider.bark": "Bark" "common.provider.bark": "Bark"
} }

View File

@ -30,20 +30,40 @@
"settings.notification.config.enable": "Enable", "settings.notification.config.enable": "Enable",
"settings.notification.config.saved.message": "Configuration saved successfully", "settings.notification.config.saved.message": "Configuration saved successfully",
"settings.notification.config.failed.message": "Configuration save failed", "settings.notification.config.failed.message": "Configuration save failed",
"settings.notification.config.push.test.message": "Send test notification", "settings.notification.push_test_message": "Send test notification",
"settings.notification.config.push.test.message.failed.message": "Send test notification failed", "settings.notification.push_test_message.succeeded.message": "Send test notification successfully",
"settings.notification.config.push.test.message.success.message": "Send test notification successfully", "settings.notification.push_test_message.failed.message": "Send test notification failed",
"settings.notification.dingtalk.secret.placeholder": "Signature for signed addition", "settings.notification.email.smtp_host.label": "SMTP Host",
"settings.notification.url.errmsg.invalid": "Invalid Url format", "settings.notification.email.smtp_host.placeholder": "Please enter SMTP host",
"settings.notification.serverchan.url.placeholder": "Url, e.g. https://sctapi.ftqq.com/****************.send", "settings.notification.email.smtp_port.label": "SMTP Port",
"settings.notification.mail.sender_address.placeholder": "Sender email address", "settings.notification.email.smtp_port.placeholder": "Please enter SMTP port",
"settings.notification.mail.receiver_address.placeholder": "Receiver email address", "settings.notification.email.smtp_tls.label": "Use TLS/SSL",
"settings.notification.mail.smtp_host.placeholder": "SMTP server address", "settings.notification.email.username.label": "Username",
"settings.notification.mail.smtp_port.placeholder": "SMTP server port, if not set, default is 25", "settings.notification.email.username.placeholder": "please enter username",
"settings.notification.mail.username.placeholder": "username", "settings.notification.email.password.label": "Password",
"settings.notification.mail.password.placeholder": "password", "settings.notification.email.password.placeholder": "please enter password",
"settings.notification.bark.serverUrl.placeholder": "Server URL, e.g. https://your-bark-server.com, leave it blank to use the bark default server", "settings.notification.email.sender_address.label": "Sender Email Address",
"settings.notification.bark.deviceKey.placeholder": "Device Keye.g. XXXXXXXXXXXXXXXXXXXX", "settings.notification.email.sender_address.placeholder": "Please enter sender email address",
"settings.notification.email.receiver_address.label": "Receiver Email Address",
"settings.notification.email.receiver_address.placeholder": "Please enter receiver email address",
"settings.notification.webhook.url.label": "Webhook URL",
"settings.notification.webhook.url.placeholder": "Please enter Webhook URL",
"settings.notification.dingtalk.access_token.label": "AccessToken",
"settings.notification.dingtalk.access_token.placeholder": "Please enter access token",
"settings.notification.dingtalk.secret.label": "Secret",
"settings.notification.dingtalk.secret.placeholder": "Please enter secret",
"settings.notification.lark.webhook_url.label": "Webhook URL",
"settings.notification.lark.webhook_url.placeholder": "Please enter Webhook URL",
"settings.notification.telegram.api_token.label": "API Token",
"settings.notification.telegram.api_token.placeholder": "Please enter API token",
"settings.notification.telegram.chat_id.label": "Chat ID",
"settings.notification.telegram.chat_id.placeholder": "Please enter Telegram chat ID",
"settings.notification.serverchan.url.label": "Server URL",
"settings.notification.serverchan.url.placeholder": "Please enter server URL (e.g. https://sctapi.ftqq.com/*****.send)",
"settings.notification.bark.server_url.label": "Server URL",
"settings.notification.bark.server_url.placeholder": "Please enter server URL (e.g. https://your-bark-server.com. Leave it blank to use the bark default server)",
"settings.notification.bark.device_key.label": "Device Key",
"settings.notification.bark.device_key.placeholder": "Please enter device key",
"settings.ca.tab": "Certificate Authority", "settings.ca.tab": "Certificate Authority",
"settings.ca.provider.errmsg.empty": "Please select a Certificate Authority", "settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",

View File

@ -83,12 +83,12 @@
"common.provider.local": "本地部署", "common.provider.local": "本地部署",
"common.provider.ssh": "SSH 部署", "common.provider.ssh": "SSH 部署",
"common.provider.webhook": "Webhook", "common.provider.webhook": "Webhook",
"common.provider.serverchan": "Server酱",
"common.provider.kubernetes": "Kubernetes", "common.provider.kubernetes": "Kubernetes",
"common.provider.kubernetes.secret": "Kubernetes - Secret", "common.provider.kubernetes.secret": "Kubernetes - Secret",
"common.provider.email": "电子邮件",
"common.provider.dingtalk": "钉钉", "common.provider.dingtalk": "钉钉",
"common.provider.telegram": "Telegram",
"common.provider.lark": "飞书", "common.provider.lark": "飞书",
"common.provider.mail": "电子邮件", "common.provider.telegram": "Telegram",
"common.provider.serverchan": "Server酱",
"common.provider.bark": "Bark" "common.provider.bark": "Bark"
} }

View File

@ -30,20 +30,40 @@
"settings.notification.config.enable": "是否启用", "settings.notification.config.enable": "是否启用",
"settings.notification.config.saved.message": "配置保存成功", "settings.notification.config.saved.message": "配置保存成功",
"settings.notification.config.failed.message": "配置保存失败", "settings.notification.config.failed.message": "配置保存失败",
"settings.notification.config.push.test.message": "推送测试消息", "settings.notification.push_test_message": "推送测试消息",
"settings.notification.config.push.test.message.failed.message": "推送测试消息失败", "settings.notification.push_test_message.failed.message": "推送测试消息失败",
"settings.notification.config.push.test.message.success.message": "推送测试消息成功", "settings.notification.push_test_message.succeeded.message": "推送测试消息成功",
"settings.notification.dingtalk.secret.placeholder": "加签的签名", "settings.notification.email.smtp_host.label": "SMTP 服务器地址",
"settings.notification.url.errmsg.invalid": "URL 格式不正确", "settings.notification.email.smtp_host.placeholder": "请输入 SMTP 服务器地址",
"settings.notification.serverchan.url.placeholder": "Url, 形如: https://sctapi.ftqq.com/****************.send", "settings.notification.email.smtp_port.label": "SMTP 服务器端口",
"settings.notification.mail.sender_address.placeholder": "发送邮箱地址", "settings.notification.email.smtp_port.placeholder": "请输入 SMTP 服务器端口",
"settings.notification.mail.receiver_address.placeholder": "接收邮箱地址", "settings.notification.email.smtp_tls.label": "TLS/SSL 连接",
"settings.notification.mail.smtp_host.placeholder": "SMTP服务器地址", "settings.notification.email.username.label": "用户名",
"settings.notification.mail.smtp_port.placeholder": "SMTP服务器端口, 如果未设置, 默认为25", "settings.notification.email.username.placeholder": "请输入用户名",
"settings.notification.mail.username.placeholder": "用于登录到邮件服务器的用户名", "settings.notification.email.password.label": "密码",
"settings.notification.mail.password.placeholder": "用于登录到邮件服务器的密码", "settings.notification.email.password.placeholder": "请输入密码",
"settings.notification.bark.serverUrl.placeholder": "服务器URL形如: https://your-bark-server.com 留空则使用 Bark 默认服务器", "settings.notification.email.sender_address.label": "发送邮箱地址",
"settings.notification.bark.deviceKey.placeholder": "设备密钥,形如: XXXXXXXXXXXXXXXXXXXX", "settings.notification.email.sender_address.placeholder": "请输入发送邮箱地址",
"settings.notification.email.receiver_address.label": "接收邮箱地址",
"settings.notification.email.receiver_address.placeholder": "请输入接收邮箱地址",
"settings.notification.webhook.url.label": "Webhook 回调地址",
"settings.notification.webhook.url.placeholder": "请输入 Webhook 回调地址",
"settings.notification.dingtalk.access_token.label": "AccessToken",
"settings.notification.dingtalk.access_token.placeholder": "请输入 AccessToken",
"settings.notification.dingtalk.secret.label": "签名密钥",
"settings.notification.dingtalk.secret.placeholder": "请输入签名密钥",
"settings.notification.lark.webhook_url.label": "Webhook URL",
"settings.notification.lark.webhook_url.placeholder": "请输入 Webhook URL",
"settings.notification.telegram.api_token.label": "API Token",
"settings.notification.telegram.api_token.placeholder": "请输入 API token",
"settings.notification.telegram.chat_id.label": "会话 ID",
"settings.notification.telegram.chat_id.placeholder": "请输入 Telegram 会话 ID",
"settings.notification.serverchan.url.label": "服务器 URL",
"settings.notification.serverchan.url.placeholder": "请输入服务器 URL形如: https://sctapi.ftqq.com/*****.send",
"settings.notification.bark.server_url.label": "服务器 URL",
"settings.notification.bark.server_url.placeholder": "请输入服务器 URL形如: https://your-bark-server.com留空则使用 Bark 默认服务器)",
"settings.notification.bark.device_key.label": "设备密钥",
"settings.notification.bark.device_key.placeholder": "请输入设备密钥",
"settings.ca.tab": "证书颁发机构CA", "settings.ca.tab": "证书颁发机构CA",
"settings.ca.provider.errmsg.empty": "请选择证书分发机构", "settings.ca.provider.errmsg.empty": "请选择证书分发机构",

View File

@ -7,7 +7,7 @@ import NotifyTemplate from "@/components/notify/NotifyTemplate";
import Telegram from "@/components/notify/Telegram"; import Telegram from "@/components/notify/Telegram";
import Webhook from "@/components/notify/Webhook"; import Webhook from "@/components/notify/Webhook";
import ServerChan from "@/components/notify/ServerChan"; import ServerChan from "@/components/notify/ServerChan";
import Mail from "@/components/notify/Mail"; import Email from "@/components/notify/Email";
import Bark from "@/components/notify/Bark"; import Bark from "@/components/notify/Bark";
import { NotifyProvider } from "@/providers/notify"; import { NotifyProvider } from "@/providers/notify";
@ -27,51 +27,52 @@ const Notify = () => {
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</div> </div>
<div className="border rounded-md p-5 mt-7 shadow-lg"> <div className="border rounded-md p-5 mt-7 shadow-lg">
<Accordion type={"single"} className="dark:text-stone-200"> <Accordion type={"single"} className="dark:text-stone-200">
<AccordionItem value="item-2" className="dark:border-stone-200"> <AccordionItem value="item-email" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger> <AccordionTrigger>{t("common.provider.email")}</AccordionTrigger>
<AccordionContent> <AccordionContent>
<DingTalk /> <Email />
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem value="item-3" className="dark:border-stone-200"> <AccordionItem value="item-webhook" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
<AccordionContent>
<Lark />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
<AccordionContent>
<Telegram />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-5" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.webhook")}</AccordionTrigger> <AccordionTrigger>{t("common.provider.webhook")}</AccordionTrigger>
<AccordionContent> <AccordionContent>
<Webhook /> <Webhook />
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem value="item-6" className="dark:border-stone-200"> <AccordionItem value="item-dingtalk" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger>
<AccordionContent>
<DingTalk />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-lark" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
<AccordionContent>
<Lark />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-telegram" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
<AccordionContent>
<Telegram />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-serverchan" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.serverchan")}</AccordionTrigger> <AccordionTrigger>{t("common.provider.serverchan")}</AccordionTrigger>
<AccordionContent> <AccordionContent>
<ServerChan /> <ServerChan />
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem value="item-7" className="dark:border-stone-200"> <AccordionItem value="item-bark" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.mail")}</AccordionTrigger>
<AccordionContent>
<Mail />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-8" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.bark")}</AccordionTrigger> <AccordionTrigger>{t("common.provider.bark")}</AccordionTrigger>
<AccordionContent> <AccordionContent>
<Bark /> <Bark />