Răsfoiți Sursa

feat(notification): add support for Ntfy (#1332)

* feat(notification): add support for Ntfy

* Update internal/notification/ntfy.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update internal/notification/ntfy.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Jacky <jacky-943572677@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
憶夣 5 luni în urmă
părinte
comite
3335e60a2e

+ 4 - 0
app/src/language/ar/app.po

@@ -2792,6 +2792,10 @@ msgstr "الانتقال إلى عارض السجلات الخام"
 msgid "Gotify"
 msgstr "غوتيفاي"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "نتفي"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/de_DE/app.po

@@ -2836,6 +2836,10 @@ msgstr "Zum Rohprotokoll-Viewer gehen"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/en/app.po

@@ -2713,6 +2713,10 @@ msgstr ""
 msgid "Gotify"
 msgstr ""
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr ""
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/es/app.po

@@ -2841,6 +2841,10 @@ msgstr "Ir al Visor de Registros en Bruto"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/fr_FR/app.po

@@ -2846,6 +2846,10 @@ msgstr "Aller au visualiseur de logs bruts"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/ja_JP/app.po

@@ -2798,6 +2798,10 @@ msgstr "生ログビューアへ移動"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/ko_KR/app.po

@@ -2775,6 +2775,10 @@ msgstr "원시 로그 뷰어로 이동"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/pt_PT/app.po

@@ -2821,6 +2821,10 @@ msgstr "Ir para o Visualizador de Logs Brutos"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/ru_RU/app.po

@@ -2820,6 +2820,10 @@ msgstr "Перейти к просмотру сырых логов"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/tr_TR/app.po

@@ -2819,6 +2819,10 @@ msgstr "Ham Log Görüntüleyiciye Git"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/uk_UA/app.po

@@ -2891,6 +2891,10 @@ msgstr "Перейти до перегляду сирих логів"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/vi_VN/app.po

@@ -2791,6 +2791,10 @@ msgstr "Đi đến Trình xem Nhật ký Thô"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/zh_CN/app.po

@@ -2755,6 +2755,10 @@ msgstr "转到原始日志查看器"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 4 - 0
app/src/language/zh_TW/app.po

@@ -2760,6 +2760,10 @@ msgstr "轉到原始日誌查看器"
 msgid "Gotify"
 msgstr "Gotify"
 
+#: src/views/preference/components/ExternalNotify/ntfy.ts:5
+msgid "Ntfy"
+msgstr "Ntfy"
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:502
 msgid ""
 "gRPC health check requires server to implement gRPC Health Check service "

+ 2 - 0
app/src/views/preference/components/ExternalNotify/index.ts

@@ -4,6 +4,7 @@ import DingTalkConfig from './dingtalk'
 import GotifyConfig from './gotify'
 import LarkConfig from './lark'
 import LarkCustomConfig from './lark_custom'
+import NtfyConfig from './ntfy'
 import TelegramConfig from './telegram'
 import WeComConfig from './wecom'
 
@@ -13,6 +14,7 @@ const configMap = {
   gotify: GotifyConfig,
   lark: LarkConfig,
   lark_custom: LarkCustomConfig,
+  ntfy: NtfyConfig,
   telegram: TelegramConfig,
   wecom: WeComConfig,
 }

+ 46 - 0
app/src/views/preference/components/ExternalNotify/ntfy.ts

@@ -0,0 +1,46 @@
+// This file is auto-generated by notification generator. DO NOT EDIT.
+import type { ExternalNotifyConfig } from './types'
+
+const NtfyConfig: ExternalNotifyConfig = {
+  name: () => $gettext('Ntfy'),
+  config: [
+    {
+      key: 'server_url',
+      label: 'Server URL',
+    },
+    {
+      key: 'topic',
+      label: 'Topic',
+    },
+    {
+      key: 'priority',
+      label: 'Priority(int, one of: 1, 2, 3, 4, 5)',
+    },
+    {
+      key: 'tags',
+      label: 'Tags(string array)',
+    },
+    {
+      key: 'click',
+      label: 'Click URL',
+    },
+    {
+      key: 'actions',
+      label: 'Actions(JSON array)',
+    },
+    {
+      key: 'username',
+      label: 'Username',
+    },
+    {
+      key: 'password',
+      label: 'Password',
+    },
+    {
+      key: 'token',
+      label: 'Token',
+    },
+  ],
+}
+
+export default NtfyConfig

+ 129 - 0
internal/notification/ntfy.go

@@ -0,0 +1,129 @@
+package notification
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/uozi-tech/cosy/map2struct"
+	"net/http"
+	"strconv"
+)
+
+const (
+	DEFAULT_NTFY_PRIORITY = 3
+	DEFAULT_NTFY_ICON     = "https://nginxui.com/assets/logo.svg"
+)
+
+// @external_notifier(Ntfy)
+type Ntfy struct {
+	ServerURL string `json:"server_url" title:"Server URL"`
+	Topic     string `json:"topic" title:"Topic"`
+	Priority  string `json:"priority" title:"Priority"`
+	Tags      string `json:"tags" title:"Tags"`
+	Click     string `json:"click" title:"Click URL"`
+	Actions   string `json:"actions" title:"Actions"`
+	Username  string `json:"username" title:"Username"`
+	Password  string `json:"password" title:"Password"`
+	Token     string `json:"token" title:"Token"`
+}
+
+type NtfyMessage struct {
+	Topic    string        `json:"topic,omitempty"`
+	Message  string        `json:"message,omitempty"`
+	Title    string        `json:"title,omitempty"`
+	Priority int           `json:"priority,omitempty"`
+	Tags     []string      `json:"tags,omitempty"`
+	Click    string        `json:"click,omitempty"`
+	Actions  []interface{} `json:"actions,omitempty"`
+	Icon     string        `json:"icon,omitempty"`
+}
+
+func init() {
+	RegisterExternalNotifier("ntfy", func(ctx context.Context, n *model.ExternalNotify, msg *ExternalMessage) error {
+		ntfyConfig := &Ntfy{}
+		err := map2struct.WeakDecode(n.Config, ntfyConfig)
+		if err != nil {
+			return err
+		}
+		if ntfyConfig.ServerURL == "" || ntfyConfig.Topic == "" {
+			return ErrInvalidNotifierConfig
+		}
+
+		// Convert priority string to int
+		priority := DEFAULT_NTFY_PRIORITY
+		if ntfyConfig.Priority != "" {
+			p, err := strconv.Atoi(ntfyConfig.Priority)
+			if err != nil {
+				return fmt.Errorf("invalid priority: %w", err)
+			}
+			if p < 1 || p > 5 {
+				return fmt.Errorf("invalid priority: must be between 1 and 5")
+			}
+			priority = p
+		}
+
+		// Prepare the message
+		ntfyMsg := NtfyMessage{
+			Topic:    ntfyConfig.Topic,
+			Message:  msg.GetContent(n.Language),
+			Title:    msg.GetTitle(n.Language),
+			Priority: priority,
+			Icon:     DEFAULT_NTFY_ICON,
+			Click:    ntfyConfig.Click,
+		}
+
+		// Add tags if provided
+		if ntfyConfig.Tags != "" {
+			var tags []string
+			if err := json.Unmarshal([]byte(ntfyConfig.Tags), &tags); err != nil {
+				return fmt.Errorf("invalid tags: %w", err)
+			}
+			ntfyMsg.Tags = tags
+		}
+
+		// Add actions if provided
+		if ntfyConfig.Actions != "" {
+			var actions []interface{}
+			if err := json.Unmarshal([]byte(ntfyConfig.Actions), &actions); err != nil {
+				return fmt.Errorf("invalid actions: %w", err)
+			}
+			ntfyMsg.Actions = actions
+		}
+
+		// Create HTTP request
+		jsonData, err := json.Marshal(ntfyMsg)
+		if err != nil {
+			return fmt.Errorf("failed to marshal ntfy message: %w", err)
+		}
+		req, err := http.NewRequestWithContext(ctx, "POST", ntfyConfig.ServerURL, bytes.NewBuffer(jsonData))
+		if err != nil {
+			return fmt.Errorf("failed to create HTTP request: %w", err)
+		}
+
+		// Set headers
+		req.Header.Set("Content-Type", "application/json")
+		req.Header.Set("User-Agent", "Nginx-UI")
+		if ntfyConfig.Token != "" {
+			req.Header.Set("Authorization", "Bearer "+ntfyConfig.Token)
+		} else if ntfyConfig.Username != "" && ntfyConfig.Password != "" {
+			req.SetBasicAuth(ntfyConfig.Username, ntfyConfig.Password)
+		}
+
+		// Send request
+		client := &http.Client{}
+		resp, err := client.Do(req)
+		if err != nil {
+			return fmt.Errorf("failed to send ntfy request: %w", err)
+		}
+		defer resp.Body.Close()
+
+		// Check response status
+		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+			return fmt.Errorf("ntfy request failed with status: %d", resp.StatusCode)
+		}
+
+		return nil
+	})
+}