package adapter import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) 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"` ErrorMsg string `json:"error_msg"` } 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 { return s.methodURL(token, "sendMessage") } func (s *SafeWSender) methodURL(token, method string) string { base := s.apiBase if base == "" { base = safewAPIBase } return strings.TrimRight(base, "/") + "/bot" + token + "/" + method } type SafewAuthError struct { Description string } func (e *SafewAuthError) Error() string { if e.Description == "" { return "safew unauthorized" } return e.Description } var safewLongPollClient = &http.Client{Timeout: 45 * time.Second} func (s *SafeWSender) PollGroupChats(token string, offset int64, timeout int) ([]SafewChat, int64, error) { token = strings.TrimSpace(token) if token == "" { return nil, offset, fmt.Errorf("safew: token is required") } payload := map[string]any{ "timeout": timeout, "limit": 100, } if offset > 0 { payload["offset"] = offset } reqBody, _ := json.Marshal(payload) resp, err := safewLongPollClient.Post(s.methodURL(token, "getUpdates"), "application/json", bytes.NewReader(reqBody)) if err != nil { return nil, offset, fmt.Errorf("safew getUpdates: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, offset, fmt.Errorf("safew getUpdates read: %w", err) } if resp.StatusCode == http.StatusUnauthorized { return nil, offset, &SafewAuthError{Description: safewErrorDescription(respBody)} } if resp.StatusCode >= 400 { desc := safewErrorDescription(respBody) if desc != "" { return nil, offset, fmt.Errorf("safew getUpdates status %d: %s", resp.StatusCode, desc) } return nil, offset, fmt.Errorf("safew getUpdates status %d", resp.StatusCode) } var api safewAPIResponse if err := json.Unmarshal(respBody, &api); err == nil && !api.OK { if resp.StatusCode == 401 || strings.Contains(strings.ToLower(api.Description), "token") { return nil, offset, &SafewAuthError{Description: api.Description} } return nil, offset, fmt.Errorf("safew: %s", api.Description) } chats, maxID, err := GroupsFromUpdates(respBody) if err != nil { return nil, offset, err } next := offset if maxID > 0 { next = maxID + 1 } return chats, next, nil } 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)) } switch { case api.Description != "" && api.ErrorMsg != "": return api.Description + ": " + api.ErrorMsg case api.ErrorMsg != "": return api.ErrorMsg default: 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() } type SafewChat struct { ID string `json:"id"` Type string `json:"type"` Title string `json:"title"` Username *string `json:"username"` } func GroupsFromUpdates(body []byte) ([]SafewChat, int64, error) { dec := json.NewDecoder(bytes.NewReader(body)) dec.UseNumber() var wrap struct { OK bool `json:"ok"` Result []json.RawMessage `json:"result"` } if err := dec.Decode(&wrap); err != nil { return nil, 0, fmt.Errorf("safew updates decode: %w", err) } seen := map[string]SafewChat{} var maxID int64 for _, item := range wrap.Result { id, chat, err := parseUpdateItem(item) if err != nil { return nil, 0, err } if id > maxID { maxID = id } if chat == nil { continue } if chat.Type != "group" && chat.Type != "supergroup" { continue } seen[chat.ID] = *chat } out := make([]SafewChat, 0, len(seen)) for _, c := range seen { out = append(out, c) } return out, maxID, nil } func parseUpdateItem(item json.RawMessage) (int64, *SafewChat, error) { dec := json.NewDecoder(bytes.NewReader(item)) dec.UseNumber() var u map[string]json.RawMessage if err := dec.Decode(&u); err != nil { return 0, nil, err } var updateID int64 if raw, ok := u["update_id"]; ok { var n json.Number if err := json.Unmarshal(raw, &n); err == nil { updateID, _ = n.Int64() } } for _, key := range []string{"message", "edited_message", "channel_post", "edited_channel_post", "my_chat_member", "chat_member"} { raw, ok := u[key] if !ok { continue } chat := chatFromNested(raw) if chat != nil { return updateID, chat, nil } } return updateID, nil, nil } func chatFromNested(raw json.RawMessage) *SafewChat { dec := json.NewDecoder(bytes.NewReader(raw)) dec.UseNumber() var obj map[string]json.RawMessage if err := dec.Decode(&obj); err != nil { return nil } chatRaw, ok := obj["chat"] if !ok { return nil } dec = json.NewDecoder(bytes.NewReader(chatRaw)) dec.UseNumber() var c struct { ID json.Number `json:"id"` Type string `json:"type"` Title string `json:"title"` Username *string `json:"username"` } if err := dec.Decode(&c); err != nil { return nil } id := strings.TrimSpace(c.ID.String()) if id == "" { return nil } return &SafewChat{ID: id, Type: c.Type, Title: c.Title, Username: c.Username} } func FilterSafewChats(chats []SafewChat, q string) []SafewChat { q = strings.TrimSpace(strings.ToLower(q)) if q == "" { return chats } var out []SafewChat for _, c := range chats { uname := "" if c.Username != nil { uname = *c.Username } if strings.Contains(strings.ToLower(c.Title), q) || strings.Contains(strings.ToLower(uname), q) || strings.Contains(strings.ToLower(c.ID), q) { out = append(out, c) } } return out }