mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
120 lines
3.1 KiB
Go
120 lines
3.1 KiB
Go
// Package service — Webhook 通知 Provider。
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// WebhookProvider 通过自定义 HTTP Webhook 发送通知。
|
|
// 支持自定义 HTTP 方法和请求头。
|
|
type WebhookProvider struct{}
|
|
|
|
// Send 发送 Webhook 通知。
|
|
func (p *WebhookProvider) Send(ctx context.Context, cfg map[string]string, event NotifyEvent) error {
|
|
webhookURL := cfg["url"]
|
|
if webhookURL == "" {
|
|
return fmt.Errorf("webhook: url is required")
|
|
}
|
|
|
|
method := cfg["method"]
|
|
if method == "" {
|
|
method = "POST"
|
|
}
|
|
method = strings.ToUpper(method)
|
|
|
|
// 构建请求体
|
|
bodyTemplate := cfg["body_template"]
|
|
var bodyStr string
|
|
if bodyTemplate != "" {
|
|
bodyStr = renderTemplate(bodyTemplate, event)
|
|
} else {
|
|
// 默认 JSON 格式
|
|
bodyStr = fmt.Sprintf(`{"type":"%s","title":"%s","message":"%s","data":{}}`,
|
|
event.Type, event.Title, event.Message)
|
|
if len(event.Data) > 0 {
|
|
var dataParts []string
|
|
for k, v := range event.Data {
|
|
dataParts = append(dataParts, fmt.Sprintf(`"%s":%v`, k, v))
|
|
}
|
|
bodyStr = fmt.Sprintf(`{"type":"%s","title":"%s","message":"%s","data":{%s}}`,
|
|
event.Type, event.Title, event.Message, strings.Join(dataParts, ","))
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, webhookURL, strings.NewReader(bodyStr))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// 自定义请求头
|
|
headersJSON := cfg["headers_json"]
|
|
if headersJSON != "" {
|
|
headers := parseHeadersJSON(headersJSON)
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
}
|
|
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("webhook error %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateConfig 验证 Webhook 配置。
|
|
func (p *WebhookProvider) ValidateConfig(cfg map[string]string) error {
|
|
if cfg["url"] == "" {
|
|
return fmt.Errorf("webhook: url is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// renderTemplate 简单模板渲染,支持 {{title}}, {{message}}, {{type}} 占位符。
|
|
func renderTemplate(template string, event NotifyEvent) string {
|
|
result := template
|
|
result = strings.ReplaceAll(result, "{{title}}", event.Title)
|
|
result = strings.ReplaceAll(result, "{{message}}", event.Message)
|
|
result = strings.ReplaceAll(result, "{{type}}", event.Type)
|
|
return result
|
|
}
|
|
|
|
// parseHeadersJSON 简单解析 headers JSON(格式: {"key":"value",...})。
|
|
func parseHeadersJSON(jsonStr string) map[string]string {
|
|
result := make(map[string]string)
|
|
jsonStr = strings.TrimSpace(jsonStr)
|
|
if jsonStr == "" || (jsonStr[0] != '{' && jsonStr[len(jsonStr)-1] != '}') {
|
|
return result
|
|
}
|
|
|
|
// 简单 key:value 解析
|
|
inner := jsonStr[1 : len(jsonStr)-1]
|
|
parts := strings.Split(inner, ",")
|
|
for _, part := range parts {
|
|
kv := strings.SplitN(part, ":", 2)
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
key := strings.Trim(strings.TrimSpace(kv[0]), `"`)
|
|
value := strings.Trim(strings.TrimSpace(kv[1]), `"`)
|
|
if key != "" {
|
|
result[key] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|