chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听) - 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP) - 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点) - 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SendWebhook(ctx context.Context, client *http.Client, cfg RuntimeConfig, content AlertContent) error {
|
||||
switch cfg.Webhook.Provider {
|
||||
case WebhookBark:
|
||||
return sendBark(ctx, client, cfg, content.PushTitle, content.barkMessage())
|
||||
case WebhookNtfy:
|
||||
return sendNtfy(ctx, client, cfg, content.PushTitle, content.PlainBody)
|
||||
case WebhookGotify:
|
||||
return sendGotify(ctx, client, cfg, content.PushTitle, content.PlainBody)
|
||||
case WebhookCustom:
|
||||
return sendCustomWebhook(ctx, client, cfg, content)
|
||||
default:
|
||||
return fmt.Errorf("不支持的 Webhook 预设")
|
||||
}
|
||||
}
|
||||
|
||||
func webhookSecret(cfg RuntimeConfig) string {
|
||||
if strings.TrimSpace(cfg.WebhookSecret) != "" {
|
||||
return strings.TrimSpace(cfg.WebhookSecret)
|
||||
}
|
||||
return strings.TrimSpace(cfg.Webhook.Key)
|
||||
}
|
||||
|
||||
func sendBark(ctx context.Context, client *http.Client, cfg RuntimeConfig, title, message string) error {
|
||||
key := webhookSecret(cfg)
|
||||
if key == "" {
|
||||
return fmt.Errorf("Bark Device Key 未配置")
|
||||
}
|
||||
server := normalizeServerURL(cfg.Webhook.Server)
|
||||
if server == "" {
|
||||
server = DefaultBarkServer
|
||||
}
|
||||
endpoint := fmt.Sprintf("%s/%s/%s/%s", server, url.PathEscape(key), url.PathEscape(title), url.PathEscape(message))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return doRequest(client, req)
|
||||
}
|
||||
|
||||
func sendNtfy(ctx context.Context, client *http.Client, cfg RuntimeConfig, title, message string) error {
|
||||
topic := strings.TrimSpace(cfg.Webhook.Topic)
|
||||
if topic == "" {
|
||||
return fmt.Errorf("ntfy Topic 未配置")
|
||||
}
|
||||
server := normalizeServerURL(cfg.Webhook.Server)
|
||||
if server == "" {
|
||||
server = "https://ntfy.sh"
|
||||
}
|
||||
endpoint := joinURL(server, topic)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(message))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Title", title)
|
||||
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
if token := webhookSecret(cfg); token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
return doRequest(client, req)
|
||||
}
|
||||
|
||||
func sendGotify(ctx context.Context, client *http.Client, cfg RuntimeConfig, title, message string) error {
|
||||
token := webhookSecret(cfg)
|
||||
if token == "" {
|
||||
return fmt.Errorf("Gotify App Token 未配置")
|
||||
}
|
||||
server := normalizeServerURL(cfg.Webhook.Server)
|
||||
if server == "" {
|
||||
return fmt.Errorf("Gotify 服务地址未配置")
|
||||
}
|
||||
endpoint := joinURL(server, "message") + "?token=" + url.QueryEscape(token)
|
||||
payload := map[string]any{
|
||||
"title": title,
|
||||
"message": message,
|
||||
"priority": 5,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return doRequest(client, req)
|
||||
}
|
||||
|
||||
func sendCustomWebhook(ctx context.Context, client *http.Client, cfg RuntimeConfig, content AlertContent) error {
|
||||
target := strings.TrimSpace(cfg.Webhook.URL)
|
||||
if target == "" {
|
||||
return fmt.Errorf("Webhook URL 未配置")
|
||||
}
|
||||
body, _ := json.Marshal(content.webhookPayload())
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return doRequest(client, req)
|
||||
}
|
||||
|
||||
func doRequest(client *http.Client, req *http.Request) error {
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 10 * time.Second}
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
if len(body) > 0 {
|
||||
return fmt.Errorf("请求失败 (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return fmt.Errorf("请求失败 (%d)", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeServerURL(server string) string {
|
||||
server = strings.TrimSpace(server)
|
||||
server = strings.TrimRight(server, "/")
|
||||
if server == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.Contains(server, "://") {
|
||||
server = "https://" + server
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
func joinURL(base string, parts ...string) string {
|
||||
base = strings.TrimRight(normalizeServerURL(base), "/")
|
||||
for _, part := range parts {
|
||||
part = strings.Trim(part, "/")
|
||||
if part != "" {
|
||||
base += "/" + part
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
Reference in New Issue
Block a user