feat: 新增 SafeW 出站通知渠道
通过 Bot API sendMessage 投递渲染后的通知,MarkdownV2 自动转义标题与正文。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const safewAPIBase = "https://api.safew.bot"
|
||||
|
||||
type SafeWSender struct {
|
||||
apiBase string
|
||||
}
|
||||
|
||||
type safewConfig struct {
|
||||
Token string `json:"token"`
|
||||
ChatID json.RawMessage `json:"chat_id"`
|
||||
}
|
||||
|
||||
type safewMessage struct {
|
||||
ChatID string `json:"chat_id"`
|
||||
Text string `json:"text"`
|
||||
ParseMode string `json:"parse_mode"`
|
||||
}
|
||||
|
||||
type safewAPIResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (s *SafeWSender) Type() string { return "safew" }
|
||||
|
||||
func (s *SafeWSender) Send(title, content string, config json.RawMessage) error {
|
||||
var cfg safewConfig
|
||||
if err := json.Unmarshal(config, &cfg); err != nil {
|
||||
return fmt.Errorf("parse safew config: %w", err)
|
||||
}
|
||||
token := strings.TrimSpace(cfg.Token)
|
||||
if token == "" {
|
||||
return fmt.Errorf("safew: token is required")
|
||||
}
|
||||
chatID, err := parseSafewChatID(cfg.ChatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := safewMessage{
|
||||
ChatID: chatID,
|
||||
Text: "*" + escapeMarkdownV2(title) + "*\n" + escapeMarkdownV2(content),
|
||||
ParseMode: "MarkdownV2",
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safew marshal: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(s.endpoint(token), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("safew send: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safew read: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
desc := safewErrorDescription(respBody)
|
||||
if desc != "" {
|
||||
return fmt.Errorf("safew returned status %d: %s", resp.StatusCode, desc)
|
||||
}
|
||||
return fmt.Errorf("safew returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var api safewAPIResponse
|
||||
if err := json.Unmarshal(respBody, &api); err != nil {
|
||||
return fmt.Errorf("safew decode: %w", err)
|
||||
}
|
||||
if !api.OK {
|
||||
desc := api.Description
|
||||
if desc == "" {
|
||||
desc = "ok=false"
|
||||
}
|
||||
return fmt.Errorf("safew: %s", desc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SafeWSender) endpoint(token string) string {
|
||||
base := s.apiBase
|
||||
if base == "" {
|
||||
base = safewAPIBase
|
||||
}
|
||||
return strings.TrimRight(base, "/") + "/bot" + token + "/sendMessage"
|
||||
}
|
||||
|
||||
func parseSafewChatID(raw json.RawMessage) (string, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return "", fmt.Errorf("safew: chat_id is required")
|
||||
}
|
||||
if raw[0] == '"' {
|
||||
var id string
|
||||
if err := json.Unmarshal(raw, &id); err != nil {
|
||||
return "", fmt.Errorf("safew: invalid chat_id: %w", err)
|
||||
}
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("safew: chat_id is required")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
func safewErrorDescription(body []byte) string {
|
||||
var api safewAPIResponse
|
||||
if err := json.Unmarshal(body, &api); err != nil {
|
||||
return strings.TrimSpace(string(body))
|
||||
}
|
||||
return api.Description
|
||||
}
|
||||
|
||||
func escapeMarkdownV2(s string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!', '\\':
|
||||
b.WriteByte('\\')
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user