diff --git a/internal/domain/notify.go b/internal/domain/notify.go
index 3d71a3a7..070aed43 100644
--- a/internal/domain/notify.go
+++ b/internal/domain/notify.go
@@ -12,6 +12,7 @@ const (
NotifyChannelTypeBark = NotifyChannelType("bark")
NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk")
NotifyChannelTypeEmail = NotifyChannelType("email")
+ NotifyChannelTypeGotify = NotifyChannelType("gotify")
NotifyChannelTypeLark = NotifyChannelType("lark")
NotifyChannelTypeServerChan = NotifyChannelType("serverchan")
NotifyChannelTypeTelegram = NotifyChannelType("telegram")
diff --git a/internal/notify/providers.go b/internal/notify/providers.go
index 66927390..d97cb842 100644
--- a/internal/notify/providers.go
+++ b/internal/notify/providers.go
@@ -8,6 +8,7 @@ import (
pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
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"
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
@@ -45,6 +46,13 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a
ReceiverAddress: maputil.GetString(channelConfig, "receiverAddress"),
})
+ case domain.NotifyChannelTypeGotify:
+ return pGotify.NewNotifier(&pGotify.NotifierConfig{
+ Url: maputil.GetString(channelConfig, "url"),
+ Token: maputil.GetString(channelConfig, "token"),
+ Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1),
+ })
+
case domain.NotifyChannelTypeLark:
return pLark.NewNotifier(&pLark.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
diff --git a/internal/pkg/core/notifier/providers/gotify/gotify.go b/internal/pkg/core/notifier/providers/gotify/gotify.go
new file mode 100644
index 00000000..1ae7af76
--- /dev/null
+++ b/internal/pkg/core/notifier/providers/gotify/gotify.go
@@ -0,0 +1,106 @@
+package gotify
+
+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 {
+ // Gotify 服务地址
+ // 示例:https://gotify.example.com
+ Url string `json:"url"`
+ // Gotify Token
+ Token string `json:"token"`
+ // Gotify 消息优先级
+ Priority int64 `json:"priority"`
+}
+
+// Message Gotify 消息体
+type Message struct {
+ Title string `json:"title"`
+ Message string `json:"message"`
+ Priority int64 `json:"priority"`
+}
+
+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
+}
+
+func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
+ // Gotify 原生实现, notify 库没有实现, 等待合并
+ reqBody := &Message{
+ Title: subject,
+ Message: message,
+ Priority: n.config.Priority,
+ }
+
+ // 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,
+ fmt.Sprintf("%s/message", n.config.Url),
+ bytes.NewReader(body),
+ )
+ if err != nil {
+ return nil, errors.Wrap(err, "create new request")
+ }
+
+ req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", n.config.Token))
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+
+ // Send request to gotify service
+ resp, err := n.httpClient.Do(req)
+ if err != nil {
+ return nil, errors.Wrapf(err, "send request to gotify server")
+ }
+
+ // Read response and verify success
+ 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("gotify returned status code %d: %s", resp.StatusCode, string(result))
+ }
+ return ¬ifier.NotifyResult{}, nil
+}
diff --git a/internal/pkg/core/notifier/providers/gotify/gotify_test.go b/internal/pkg/core/notifier/providers/gotify/gotify_test.go
new file mode 100644
index 00000000..31ad64af
--- /dev/null
+++ b/internal/pkg/core/notifier/providers/gotify/gotify_test.go
@@ -0,0 +1,68 @@
+package gotify_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/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.NewNotifier(&provider.NotifierConfig{
+ Url: 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)
+ })
+}
diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx
index d818ee4c..98352771 100644
--- a/ui/src/components/notification/NotifyChannelEditForm.tsx
+++ b/ui/src/components/notification/NotifyChannelEditForm.tsx
@@ -7,6 +7,7 @@ import { useAntdForm } from "@/hooks";
import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields";
import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields";
import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields";
+import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx";
import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields";
import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields";
import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields";
@@ -48,6 +49,8 @@ const NotifyChannelEditForm = forwardRef