chore: move '/internal/pkg' to '/pkg'

This commit is contained in:
Fu Diwei
2025-06-17 15:54:21 +08:00
parent 30840bbba5
commit 205275b52d
611 changed files with 693 additions and 693 deletions

View File

@@ -0,0 +1,76 @@
package bark
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// Bark 服务地址。
// 零值时使用官方服务器。
ServerUrl string `json:"serverUrl"`
// Bark 设备密钥。
DeviceKey string `json:"deviceKey"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
const defaultServerURL = "https://api.day.app/"
serverUrl := defaultServerURL
if n.config.ServerUrl != "" {
serverUrl = n.config.ServerUrl
}
// REF: https://bark.day.app/#/tutorial
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetBody(map[string]any{
"title": subject,
"body": message,
"device_key": n.config.DeviceKey,
})
resp, err := req.Post(serverUrl)
if err != nil {
return nil, fmt.Errorf("bark api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("bark api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,64 @@
package bark_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/bark"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fServerUrl string
fDeviceKey string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_BARK_"
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fDeviceKey, argsPrefix+"DEVICEKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./bark_test.go -args \
--CERTIMATE_NOTIFIER_BARK_SERVERURL="https://example.com/your-server-url" \
--CERTIMATE_NOTIFIER_BARK_DEVICEKEY="your-device-key"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("SERVERURL: %v", fServerUrl),
fmt.Sprintf("DEVICEKEY: %v", fDeviceKey),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
ServerUrl: fServerUrl,
DeviceKey: fDeviceKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,66 @@
package dingtalkbot
import (
"context"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/blinkbean/dingtalk"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// 钉钉机器人的 Webhook 地址。
WebhookUrl string `json:"webhookUrl"`
// 钉钉机器人的 Secret。
Secret string `json:"secret"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
webhookUrl, err := url.Parse(n.config.WebhookUrl)
if err != nil {
return nil, fmt.Errorf("dingtalk api error: invalid webhook url: %w", err)
}
var bot *dingtalk.DingTalk
if n.config.Secret == "" {
bot = dingtalk.InitDingTalk([]string{webhookUrl.Query().Get("access_token")}, "")
} else {
bot = dingtalk.InitDingTalkWithSecret(webhookUrl.Query().Get("access_token"), n.config.Secret)
}
if err := bot.SendTextMessage(subject + "\n" + message); err != nil {
return nil, fmt.Errorf("dingtalk api error: %w", err)
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,64 @@
package dingtalkbot_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/dingtalkbot"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fWebhookUrl string
fSecret string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_DINGTALKBOT_"
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
flag.StringVar(&fSecret, argsPrefix+"SECRET", "", "")
}
/*
Shell command to run this test:
go test -v ./dingtalkbot_test.go -args \
--CERTIMATE_NOTIFIER_DINGTALKBOT_WEBHOOKURL="https://example.com/your-webhook-url" \
--CERTIMATE_NOTIFIER_DINGTALKBOT_SECRET="your-secret"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
fmt.Sprintf("SECRET: %v", fSecret),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
WebhookUrl: fWebhookUrl,
Secret: fSecret,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,69 @@
package discordbot
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// Discord Bot API Token。
BotToken string `json:"botToken"`
// Discord Channel ID。
ChannelId string `json:"channelId"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
// REF: https://discord.com/developers/docs/resources/message#create-message
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Authorization", "Bot "+n.config.BotToken).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"content": subject + "\n" + message,
})
resp, err := req.Post(fmt.Sprintf("https://discord.com/api/v9/channels/%s/messages", n.config.ChannelId))
if err != nil {
return nil, fmt.Errorf("discord api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("discord api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,64 @@
package discordbot_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/discordbot"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fApiToken string
fChannelId string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_DISCORDBOT_"
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "")
}
/*
Shell command to run this test:
go test -v ./discordbot_test.go -args \
--CERTIMATE_NOTIFIER_DISCORDBOT_APITOKEN="your-bot-token" \
--CERTIMATE_NOTIFIER_DISCORDBOT_CHANNELID="your-channel-id"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("APITOKEN: %v", fApiToken),
fmt.Sprintf("CHANNELID: %v", fChannelId),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
BotToken: fApiToken,
ChannelId: fChannelId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,118 @@
package email
import (
"context"
"crypto/tls"
"errors"
"log/slog"
"net"
"net/smtp"
"strconv"
"github.com/domodwyer/mailyak/v3"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// SMTP 服务器地址。
SmtpHost string `json:"smtpHost"`
// SMTP 服务器端口。
// 零值时根据是否启用 TLS 决定。
SmtpPort int32 `json:"smtpPort"`
// 是否启用 TLS。
SmtpTls bool `json:"smtpTls"`
// 用户名。
Username string `json:"username"`
// 密码。
Password string `json:"password"`
// 发件人邮箱。
SenderAddress string `json:"senderAddress"`
// 发件人显示名称。
SenderName string `json:"senderName,omitempty"`
// 收件人邮箱。
ReceiverAddress string `json:"receiverAddress"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, 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 = net.JoinHostPort(n.config.SmtpHost, "465")
} else {
smtpAddr = net.JoinHostPort(n.config.SmtpHost, "25")
}
} else {
smtpAddr = net.JoinHostPort(n.config.SmtpHost, strconv.Itoa(int(n.config.SmtpPort)))
}
var yak *mailyak.MailYak
if n.config.SmtpTls {
yakWithTls, err := mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig())
if err != nil {
return nil, err
}
yak = yakWithTls
} else {
yak = mailyak.New(smtpAddr, smtpAuth)
}
yak.From(n.config.SenderAddress)
yak.FromName(n.config.SenderName)
yak.To(n.config.ReceiverAddress)
yak.Subject(subject)
yak.Plain().Set(message)
if err := yak.Send(); err != nil {
return nil, err
}
return &core.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,89 @@
package email_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/email"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fSmtpHost string
fSmtpPort int64
fSmtpTLS bool
fUsername string
fPassword string
fSenderAddress string
fReceiverAddress string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_EMAIL_"
flag.StringVar(&fSmtpHost, argsPrefix+"SMTPHOST", "", "")
flag.Int64Var(&fSmtpPort, argsPrefix+"SMTPPORT", 0, "")
flag.BoolVar(&fSmtpTLS, argsPrefix+"SMTPTLS", false, "")
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
flag.StringVar(&fSenderAddress, argsPrefix+"SENDERADDRESS", "", "")
flag.StringVar(&fReceiverAddress, argsPrefix+"RECEIVERADDRESS", "", "")
}
/*
Shell command to run this test:
go test -v ./email_test.go -args \
--CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
--CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
--CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
--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"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("SMTPHOST: %v", fSmtpHost),
fmt.Sprintf("SMTPPORT: %v", fSmtpPort),
fmt.Sprintf("SMTPTLS: %v", fSmtpTLS),
fmt.Sprintf("USERNAME: %v", fUsername),
fmt.Sprintf("PASSWORD: %v", fPassword),
fmt.Sprintf("SENDERADDRESS: %v", fSenderAddress),
fmt.Sprintf("RECEIVERADDRESS: %v", fReceiverAddress),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
SmtpHost: fSmtpHost,
SmtpPort: int32(fSmtpPort),
SmtpTls: fSmtpTLS,
Username: fUsername,
Password: fPassword,
SenderAddress: fSenderAddress,
ReceiverAddress: fReceiverAddress,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,76 @@
package gotify
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// Gotify 服务地址。
ServerUrl string `json:"serverUrl"`
// Gotify Token。
Token string `json:"token"`
// Gotify 消息优先级。
Priority int64 `json:"priority,omitempty"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
serverUrl := strings.TrimRight(n.config.ServerUrl, "/")
// REF: https://gotify.net/api-docs#/message/createMessage
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Authorization", "Bearer "+n.config.Token).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"title": subject,
"message": message,
"priority": n.config.Priority,
})
resp, err := req.Post(fmt.Sprintf("%s/message", serverUrl))
if err != nil {
return nil, fmt.Errorf("gotify api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("gotify api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,68 @@
package gotify_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/gotify"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fUrl string
fToken string
fPriority int64
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_GOTIFY_"
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
flag.Int64Var(&fPriority, argsPrefix+"PRIORITY", 0, "")
}
/*
Shell command to run this test:
go test -v ./gotify_test.go -args \
--CERTIMATE_NOTIFIER_GOTIFY_URL="https://example.com" \
--CERTIMATE_NOTIFIER_GOTIFY_TOKEN="your-gotify-application-token" \
--CERTIMATE_NOTIFIER_GOTIFY_PRIORITY="your-message-priority"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("URL: %v", fUrl),
fmt.Sprintf("TOKEN: %v", fToken),
fmt.Sprintf("PRIORITY: %d", fPriority),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
ServerUrl: fUrl,
Token: fToken,
Priority: fPriority,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,60 @@
package larkbot
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-lark/lark"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// 飞书机器人 Webhook 地址。
WebhookUrl string `json:"webhookUrl"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
bot := lark.NewNotificationBot(n.config.WebhookUrl)
content := lark.NewPostBuilder().
Title(subject).
TextTag(message, 1, false).
Render()
msg := lark.NewMsgBuffer(lark.MsgPost).Post(content)
resp, err := bot.PostNotificationV2(msg.Build())
if err != nil {
return nil, fmt.Errorf("lark api error: %w", err)
} else if resp.Code != 0 {
return nil, fmt.Errorf("lark api error: code='%d', message='%s'", resp.Code, resp.Msg)
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,57 @@
package larkbot_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/larkbot"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fWebhookUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_LARKBOT_"
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
}
/*
Shell command to run this test:
go test -v ./larkbot_test.go -args \
--CERTIMATE_NOTIFIER_LARKBOT_WEBHOOKURL="https://example.com/your-webhook-url"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
WebhookUrl: fWebhookUrl,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,102 @@
package mattermost
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// Mattermost 服务地址。
ServerUrl string `json:"serverUrl"`
// Mattermost 用户名。
Username string `json:"username"`
// Mattermost 密码。
Password string `json:"password"`
// Mattermost 频道 ID。
ChannelId string `json:"channelId"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
serverUrl := strings.TrimRight(n.config.ServerUrl, "/")
// REF: https://developers.mattermost.com/api-documentation/#/operations/Login
loginReq := n.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"login_id": n.config.Username,
"password": n.config.Password,
})
loginResp, err := loginReq.Post(fmt.Sprintf("%s/api/v4/users/login", serverUrl))
if err != nil {
return nil, fmt.Errorf("mattermost api error: failed to send request: %w", err)
} else if loginResp.IsError() {
return nil, fmt.Errorf("mattermost api error: unexpected status code: %d, resp: %s", loginResp.StatusCode(), loginResp.String())
} else if loginResp.Header().Get("Token") == "" {
return nil, fmt.Errorf("mattermost api error: received empty login token")
}
// REF: https://developers.mattermost.com/api-documentation/#/operations/CreatePost
postReq := n.httpClient.R().
SetContext(ctx).
SetHeader("Authorization", "Bearer "+loginResp.Header().Get("Token")).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"channel_id": n.config.ChannelId,
"props": map[string]interface{}{
"attachments": []map[string]interface{}{
{
"title": subject,
"text": message,
},
},
},
})
postResp, err := postReq.Post(fmt.Sprintf("%s/api/v4/posts", serverUrl))
if err != nil {
return nil, fmt.Errorf("mattermost api error: failed to send request: %w", err)
} else if postResp.IsError() {
return nil, fmt.Errorf("mattermost api error: unexpected status code: %d, resp: %s", postResp.StatusCode(), postResp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,74 @@
package mattermost_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/mattermost"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fServerUrl string
fChannelId string
fUsername string
fPassword string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_MATTERMOST_"
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "")
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
}
/*
Shell command to run this test:
go test -v ./mattermost_test.go -args \
--CERTIMATE_NOTIFIER_MATTERMOST_SERVERURL="https://example.com/your-server-url" \
--CERTIMATE_NOTIFIER_MATTERMOST_CHANNELID="your-chanel-id" \
--CERTIMATE_NOTIFIER_MATTERMOST_USERNAME="your-username" \
--CERTIMATE_NOTIFIER_MATTERMOST_PASSWORD="your-password"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("SERVERURL: %v", fServerUrl),
fmt.Sprintf("CHANNELID: %v", fChannelId),
fmt.Sprintf("USERNAME: %v", fUsername),
fmt.Sprintf("PASSWORD: %v", fPassword),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
ServerUrl: fServerUrl,
ChannelId: fChannelId,
Username: fUsername,
Password: fPassword,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,71 @@
package pushover
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// Pushover API Token。
Token string `json:"token"`
// 用户或分组标识。
User string `json:"user"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
// REF: https://pushover.net/api
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"title": subject,
"message": message,
"token": n.config.Token,
"user": n.config.User,
})
resp, err := req.Post("https://api.pushover.net/1/messages.json")
if err != nil {
return nil, fmt.Errorf("pushover api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("pushover api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,62 @@
package pushover_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/pushover"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fToken string
fUser string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_PUSHOVER_"
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
flag.StringVar(&fUser, argsPrefix+"USER", "", "")
}
/*
Shell command to run this test:
go test -v ./pushover_test.go -args \
--CERTIMATE_NOTIFIER_PUSHOVER_TOKEN="your-pushover-token" \
--CERTIMATE_NOTIFIER_PUSHOVER_USER="your-pushover-user" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("TOKEN: %v", fToken),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
Token: fToken,
User: fUser,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,79 @@
package pushplus
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// PushPlus Token。
Token string `json:"token"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
// REF: https://pushplus.plus/doc/guide/api.html#%E4%B8%80%E3%80%81%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF%E6%8E%A5%E5%8F%A3
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"title": subject,
"content": message,
"token": n.config.Token,
})
resp, err := req.Post("https://www.pushplus.plus/send")
if err != nil {
return nil, fmt.Errorf("pushplus api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("pushplus api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
var errorResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
}
if err := json.Unmarshal(resp.Body(), &errorResponse); err != nil {
return nil, fmt.Errorf("pushplus api error: failed to unmarshal response: %w", err)
} else if errorResponse.Code != 200 {
return nil, fmt.Errorf("pushplus api error: code='%d', message='%s'", errorResponse.Code, errorResponse.Message)
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,56 @@
package pushplus_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/pushplus"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fToken string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_PUSHPLUS_"
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
}
/*
Shell command to run this test:
go test -v ./pushplus_test.go -args \
--CERTIMATE_NOTIFIER_PUSHPLUS_TOKEN="your-pushplus-token" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("TOKEN: %v", fToken),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
Token: fToken,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,67 @@
package serverchan
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// ServerChan 服务地址。
ServerUrl string `json:"serverUrl"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
// REF: https://sct.ftqq.com/
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"text": subject,
"desp": message,
})
resp, err := req.Post(n.config.ServerUrl)
if err != nil {
return nil, fmt.Errorf("serverchan api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("serverchan api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,57 @@
package serverchan_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/serverchan"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_SERVERCHAN_"
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
}
/*
Shell command to run this test:
go test -v ./serverchan_test.go -args \
--CERTIMATE_NOTIFIER_SERVERCHAN_URL="https://example.com/your-webhook-url" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("URL: %v", fUrl),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
ServerUrl: fUrl,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,71 @@
package discordbot
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// Slack Bot API Token。
BotToken string `json:"botToken"`
// Slack Channel ID。
ChannelId string `json:"channelId"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
// REF: https://docs.slack.dev/messaging/sending-and-scheduling-messages#publishing
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Authorization", "Bearer "+n.config.BotToken).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"token": n.config.BotToken,
"channel": n.config.ChannelId,
"text": subject + "\n" + message,
})
resp, err := req.Post("https://slack.com/api/chat.postMessage")
if err != nil {
return nil, fmt.Errorf("slack api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("slack api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,64 @@
package discordbot_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/slackbot"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fApiToken string
fChannelId string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_SLACKBOT_"
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "")
}
/*
Shell command to run this test:
go test -v ./slackbot_test.go -args \
--CERTIMATE_NOTIFIER_SLACKBOT_APITOKEN="your-bot-token" \
--CERTIMATE_NOTIFIER_SLACKBOT_CHANNELID="your-channel-id"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("APITOKEN: %v", fApiToken),
fmt.Sprintf("CHANNELID: %v", fChannelId),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
BotToken: fApiToken,
ChannelId: fChannelId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,69 @@
package telegrambot
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// Telegram Bot API Token。
BotToken string `json:"botToken"`
// Telegram Chat ID。
ChatId int64 `json:"chatId"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
// REF: https://core.telegram.org/bots/api#sendmessage
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"chat_id": n.config.ChatId,
"text": subject + "\n" + message,
})
resp, err := req.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", n.config.BotToken))
if err != nil {
return nil, fmt.Errorf("telegram api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("telegram api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,64 @@
package telegrambot_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/telegrambot"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fApiToken string
fChatId int64
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_TELEGRAMBOT_"
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.Int64Var(&fChatId, argsPrefix+"CHATID", 0, "")
}
/*
Shell command to run this test:
go test -v ./telegrambot_test.go -args \
--CERTIMATE_NOTIFIER_TELEGRAMBOT_APITOKEN="your-api-token" \
--CERTIMATE_NOTIFIER_TELEGRAMBOT_CHATID=123456
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("APITOKEN: %v", fApiToken),
fmt.Sprintf("CHATID: %v", fChatId),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
BotToken: fApiToken,
ChatId: fChatId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,185 @@
package webhook
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// Webhook URL。
WebhookUrl string `json:"webhookUrl"`
// Webhook 回调数据application/json 或 application/x-www-form-urlencoded 格式)。
WebhookData string `json:"webhookData,omitempty"`
// 请求谓词。
// 零值时默认值 "POST"。
Method string `json:"method,omitempty"`
// 请求标头。
Headers map[string]string `json:"headers,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New().
SetTimeout(30 * time.Second).
SetRetryCount(3).
SetRetryWaitTime(5 * time.Second)
if config.AllowInsecureConnections {
client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
// 处理 Webhook URL
webhookUrl, err := url.Parse(n.config.WebhookUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook url: %w", err)
} else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" {
return nil, fmt.Errorf("unsupported webhook url scheme '%s'", webhookUrl.Scheme)
}
// 处理 Webhook 请求谓词
webhookMethod := strings.ToUpper(n.config.Method)
if webhookMethod == "" {
webhookMethod = http.MethodPost
} else if webhookMethod != http.MethodGet &&
webhookMethod != http.MethodPost &&
webhookMethod != http.MethodPut &&
webhookMethod != http.MethodPatch &&
webhookMethod != http.MethodDelete {
return nil, fmt.Errorf("unsupported webhook request method '%s'", webhookMethod)
}
// 处理 Webhook 请求标头
webhookHeaders := make(http.Header)
for k, v := range n.config.Headers {
webhookHeaders.Set(k, v)
}
// 处理 Webhook 请求内容类型
const CONTENT_TYPE_JSON = "application/json"
const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"
const CONTENT_TYPE_MULTIPART = "multipart/form-data"
webhookContentType := webhookHeaders.Get("Content-Type")
if webhookContentType == "" {
webhookContentType = CONTENT_TYPE_JSON
webhookHeaders.Set("Content-Type", CONTENT_TYPE_JSON)
} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {
return nil, fmt.Errorf("unsupported webhook content type '%s'", webhookContentType)
}
// 处理 Webhook 请求数据
var webhookData interface{}
if n.config.WebhookData == "" {
webhookData = map[string]string{
"subject": subject,
"message": message,
}
} else {
err = json.Unmarshal([]byte(n.config.WebhookData), &webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
}
replaceJsonValueRecursively(webhookData, "${SUBJECT}", subject)
replaceJsonValueRecursively(webhookData, "${MESSAGE}", message)
if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {
temp := make(map[string]string)
jsonb, err := json.Marshal(webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else if err := json.Unmarshal(jsonb, &temp); err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else {
webhookData = temp
}
}
}
// 生成请求
// 其中 GET 请求需转换为查询参数
req := n.httpClient.R().SetContext(ctx).SetHeaderMultiValues(webhookHeaders)
req.URL = webhookUrl.String()
req.Method = webhookMethod
if webhookMethod == http.MethodGet {
req.SetQueryParams(webhookData.(map[string]string))
} else {
switch webhookContentType {
case CONTENT_TYPE_JSON:
req.SetBody(webhookData)
case CONTENT_TYPE_FORM:
req.SetFormData(webhookData.(map[string]string))
case CONTENT_TYPE_MULTIPART:
req.SetMultipartFormData(webhookData.(map[string]string))
}
}
// 发送请求
resp, err := req.Send()
if err != nil {
return nil, fmt.Errorf("webhook error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("webhook error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
n.logger.Debug("webhook responded", slog.String("response", resp.String()))
return &core.NotifyResult{}, nil
}
func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {
switch v := data.(type) {
case map[string]any:
for k, val := range v {
v[k] = replaceJsonValueRecursively(val, oldStr, newStr)
}
case []any:
for i, val := range v {
v[i] = replaceJsonValueRecursively(val, oldStr, newStr)
}
case string:
return strings.ReplaceAll(v, oldStr, newStr)
}
return data
}

View File

@@ -0,0 +1,67 @@
package webhook_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/webhook"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fWebhookUrl string
fWebhookContentType string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_WEBHOOK_"
flag.StringVar(&fWebhookUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fWebhookContentType, argsPrefix+"CONTENTTYPE", "application/json", "")
}
/*
Shell command to run this test:
go test -v ./webhook_test.go -args \
--CERTIMATE_NOTIFIER_WEBHOOK_URL="https://example.com/your-webhook-url" \
--CERTIMATE_NOTIFIER_WEBHOOK_CONTENTTYPE="application/json"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("URL: %v", fWebhookUrl),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
WebhookUrl: fWebhookUrl,
Method: "POST",
Headers: map[string]string{
"Content-Type": fWebhookContentType,
},
AllowInsecureConnections: true,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,69 @@
package wecombot
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
)
type NotifierProviderConfig struct {
// 企业微信机器人 Webhook 地址。
WebhookUrl string `json:"webhookUrl"`
}
type NotifierProvider struct {
config *NotifierProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.Notifier = (*NotifierProvider)(nil)
func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the notifier provider is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (n *NotifierProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
n.logger = slog.New(slog.DiscardHandler)
} else {
n.logger = logger
}
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*core.NotifyResult, error) {
// REF: https://developer.work.weixin.qq.com/document/path/91770
req := n.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetBody(map[string]any{
"msgtype": "text",
"text": map[string]string{
"content": subject + "\n\n" + message,
},
})
resp, err := req.Post(n.config.WebhookUrl)
if err != nil {
return nil, fmt.Errorf("wecom api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("wecom api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &core.NotifyResult{}, nil
}

View File

@@ -0,0 +1,57 @@
package wecombot_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/pkg/core/notifier/providers/wecombot"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fWebhookUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_WECOMBOT_"
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
}
/*
Shell command to run this test:
go test -v ./wecombot_test.go -args \
--CERTIMATE_NOTIFIER_WECOMBOT_WEBHOOKURL="https://example.com/your-webhook-url" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
}, "\n"))
notifier, err := provider.NewNotifierProvider(&provider.NotifierProviderConfig{
WebhookUrl: fWebhookUrl,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}