diff --git a/internal/domain/notify.go b/internal/domain/notify.go
index 4bc57b85..7be8b59b 100644
--- a/internal/domain/notify.go
+++ b/internal/domain/notify.go
@@ -14,6 +14,7 @@ const (
NotifyChannelTypeEmail = NotifyChannelType("email")
NotifyChannelTypeGotify = NotifyChannelType("gotify")
NotifyChannelTypeLark = NotifyChannelType("lark")
+ NotifyChannelTypePushover = NotifyChannelType("pushover")
NotifyChannelTypePushPlus = NotifyChannelType("pushplus")
NotifyChannelTypeServerChan = NotifyChannelType("serverchan")
NotifyChannelTypeTelegram = NotifyChannelType("telegram")
diff --git a/internal/notify/providers.go b/internal/notify/providers.go
index 3a7cadf9..808892b3 100644
--- a/internal/notify/providers.go
+++ b/internal/notify/providers.go
@@ -10,6 +10,7 @@ import (
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
+ pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
@@ -59,6 +60,12 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
})
+ case domain.NotifyChannelTypePushover:
+ return pPushover.NewNotifier(&pPushover.NotifierConfig{
+ Token: maputil.GetString(channelConfig, "token"),
+ User: maputil.GetString(channelConfig, "user"),
+ })
+
case domain.NotifyChannelTypePushPlus:
return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
diff --git a/internal/pkg/core/notifier/providers/pushover/pushover.go b/internal/pkg/core/notifier/providers/pushover/pushover.go
new file mode 100644
index 00000000..8f84dfd2
--- /dev/null
+++ b/internal/pkg/core/notifier/providers/pushover/pushover.go
@@ -0,0 +1,102 @@
+package pushover
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+
+ "github.com/pkg/errors"
+
+ "github.com/usual2970/certimate/internal/pkg/core/notifier"
+)
+
+type NotifierConfig struct {
+ Token string `json:"token"` // 应用 API Token
+ User string `json:"user"` // 用户/分组 Key
+}
+
+type NotifierProvider struct {
+ config *NotifierConfig
+ logger *slog.Logger
+ // 未来将移除
+ httpClient *http.Client
+}
+
+var _ notifier.Notifier = (*NotifierProvider)(nil)
+
+func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ return &NotifierProvider{
+ config: config,
+ httpClient: http.DefaultClient,
+ }, nil
+}
+
+func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
+ if logger == nil {
+ n.logger = slog.Default()
+ } else {
+ n.logger = logger
+ }
+ return n
+}
+
+// Notify 发送通知
+// 参考文档:https://pushover.net/api
+func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
+ // 请求体
+ reqBody := &struct {
+ Token string `json:"token"`
+ User string `json:"user"`
+ Title string `json:"title"`
+ Message string `json:"message"`
+ }{
+ Token: n.config.Token,
+ User: n.config.User,
+ Title: subject,
+ Message: message,
+ }
+
+ // Make request
+ body, err := json.Marshal(reqBody)
+ if err != nil {
+ return nil, errors.Wrap(err, "encode message body")
+ }
+
+ req, err := http.NewRequestWithContext(
+ ctx,
+ http.MethodPost,
+ "https://api.pushover.net/1/messages.json",
+ bytes.NewReader(body),
+ )
+ if err != nil {
+ return nil, errors.Wrap(err, "create new request")
+ }
+
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+
+ // Send request to pushover service
+ resp, err := n.httpClient.Do(req)
+ if err != nil {
+ return nil, errors.Wrapf(err, "send request to pushover server")
+ }
+ defer resp.Body.Close()
+
+ result, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, errors.Wrap(err, "read response")
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("pushover returned status code %d: %s", resp.StatusCode, string(result))
+ }
+
+ return ¬ifier.NotifyResult{}, nil
+}
diff --git a/internal/pkg/core/notifier/providers/pushover/pushover_test.go b/internal/pkg/core/notifier/providers/pushover/pushover_test.go
new file mode 100644
index 00000000..450beac1
--- /dev/null
+++ b/internal/pkg/core/notifier/providers/pushover/pushover_test.go
@@ -0,0 +1,62 @@
+package pushover_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/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.NewNotifier(&provider.NotifierConfig{
+ 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)
+ })
+}
diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx
index aa3f4f12..1aa7eb87 100644
--- a/ui/src/components/notification/NotifyChannelEditForm.tsx
+++ b/ui/src/components/notification/NotifyChannelEditForm.tsx
@@ -9,6 +9,7 @@ import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalk
import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields";
import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx";
import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields";
+import NotifyChannelEditFormPushoverFields from "./NotifyChannelEditFormPushoverFields";
import NotifyChannelEditFormPushPlusFields from "./NotifyChannelEditFormPushPlusFields";
import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields";
import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields";
@@ -54,6 +55,8 @@ const NotifyChannelEditForm = forwardRef