From a07963e1500eec2ed25b432bcf96951224b96a6e Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 15 Aug 2026 01:56:12 +0800 Subject: [PATCH] feat: enhance Safew chat handling and testing - Updated `GroupsFromUpdates` function to return the number of updates processed, improving the polling mechanism. - Added a new test `TestGroupsFromUpdatesCallbackAndJoinRequest` to validate handling of callback queries and join requests. - Introduced `safewAllowedUpdates` to specify allowed update types in the polling request, enhancing chat management. - Implemented `ensureWebhookCleared` method to manage webhook state before polling, improving reliability. --- internal/adapter/safew.go | 90 ++++++++++++++++++++-------- internal/adapter/safew_chats_test.go | 31 +++++++++- 2 files changed, 94 insertions(+), 27 deletions(-) diff --git a/internal/adapter/safew.go b/internal/adapter/safew.go index ac6390f..f5c41a8 100644 --- a/internal/adapter/safew.go +++ b/internal/adapter/safew.go @@ -5,8 +5,10 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "strings" + "sync" "time" ) @@ -116,14 +118,27 @@ func (e *SafewAuthError) Error() string { var safewLongPollClient = &http.Client{Timeout: 45 * time.Second} +var safewAllowedUpdates = []string{ + "message", + "edited_message", + "my_chat_member", + "chat_member", + "chat_join_request", + "callback_query", +} + +var safewWebhookCleared sync.Map + 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") } + s.ensureWebhookCleared(token) payload := map[string]any{ - "timeout": timeout, - "limit": 100, + "timeout": timeout, + "limit": 100, + "allowed_updates": safewAllowedUpdates, } if offset > 0 { payload["offset"] = offset @@ -155,7 +170,7 @@ func (s *SafeWSender) PollGroupChats(token string, offset int64, timeout int) ([ } return nil, offset, fmt.Errorf("safew: %s", api.Description) } - chats, maxID, err := GroupsFromUpdates(respBody) + chats, maxID, n, err := GroupsFromUpdates(respBody) if err != nil { return nil, offset, err } @@ -163,9 +178,27 @@ func (s *SafeWSender) PollGroupChats(token string, offset int64, timeout int) ([ if maxID > 0 { next = maxID + 1 } + slog.Info("safew getUpdates", "timeout", timeout, "updates", n, "groups", len(chats), "offset", next) return chats, next, nil } +func (s *SafeWSender) ensureWebhookCleared(token string) { + if _, loaded := safewWebhookCleared.LoadOrStore(token, true); loaded { + return + } + reqBody, _ := json.Marshal(map[string]any{"drop_pending_updates": false}) + resp, err := http.Post(s.methodURL(token, "deleteWebhook"), "application/json", bytes.NewReader(reqBody)) + if err != nil { + slog.Warn("safew deleteWebhook", "error", err) + return + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 400 { + slog.Warn("safew deleteWebhook", "status", resp.StatusCode, "error", safewErrorDescription(body)) + } +} + func parseSafewChatID(raw json.RawMessage) (string, error) { raw = bytes.TrimSpace(raw) if len(raw) == 0 || string(raw) == "null" { @@ -220,7 +253,7 @@ type SafewChat struct { Username *string `json:"username"` } -func GroupsFromUpdates(body []byte) ([]SafewChat, int64, error) { +func GroupsFromUpdates(body []byte) ([]SafewChat, int64, int, error) { dec := json.NewDecoder(bytes.NewReader(body)) dec.UseNumber() var wrap struct { @@ -228,14 +261,14 @@ func GroupsFromUpdates(body []byte) ([]SafewChat, int64, error) { Result []json.RawMessage `json:"result"` } if err := dec.Decode(&wrap); err != nil { - return nil, 0, fmt.Errorf("safew updates decode: %w", err) + return nil, 0, 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 + return nil, 0, 0, err } if id > maxID { maxID = id @@ -252,7 +285,7 @@ func GroupsFromUpdates(body []byte) ([]SafewChat, int64, error) { for _, c := range seen { out = append(out, c) } - return out, maxID, nil + return out, maxID, len(wrap.Result), nil } func parseUpdateItem(item json.RawMessage) (int64, *SafewChat, error) { @@ -269,7 +302,10 @@ func parseUpdateItem(item json.RawMessage) (int64, *SafewChat, error) { updateID, _ = n.Int64() } } - for _, key := range []string{"message", "edited_message", "channel_post", "edited_channel_post", "my_chat_member", "chat_member"} { + for _, key := range []string{ + "message", "edited_message", "channel_post", "edited_channel_post", + "my_chat_member", "chat_member", "chat_join_request", "callback_query", + } { raw, ok := u[key] if !ok { continue @@ -289,26 +325,28 @@ func chatFromNested(raw json.RawMessage) *SafewChat { if err := dec.Decode(&obj); err != nil { return nil } - chatRaw, ok := obj["chat"] - if !ok { - return nil + if chatRaw, ok := obj["chat"]; ok { + 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} } - 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 msgRaw, ok := obj["message"]; ok { + return chatFromNested(msgRaw) } - 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} + return nil } func FilterSafewChats(chats []SafewChat, q string) []SafewChat { diff --git a/internal/adapter/safew_chats_test.go b/internal/adapter/safew_chats_test.go index c11ab05..9b7d57a 100644 --- a/internal/adapter/safew_chats_test.go +++ b/internal/adapter/safew_chats_test.go @@ -18,7 +18,7 @@ func TestGroupsFromUpdatesKeepsGroupsDropsPrivate(t *testing.T) { {"update_id": 100000003, "my_chat_member": {"chat": {"id": 22, "type": "supergroup", "title": "SG", "username": "sg_name"}}} ] }`) - chats, maxID, err := GroupsFromUpdates(body) + chats, maxID, _, err := GroupsFromUpdates(body) if err != nil { t.Fatal(err) } @@ -48,6 +48,26 @@ func TestGroupsFromUpdatesKeepsGroupsDropsPrivate(t *testing.T) { } } +func TestGroupsFromUpdatesCallbackAndJoinRequest(t *testing.T) { + body := []byte(`{ + "ok": true, + "result": [ + {"update_id": 1, "callback_query": {"message": {"chat": {"id": 10000778141, "type": "group", "title": "测试AI"}}}}, + {"update_id": 2, "chat_join_request": {"chat": {"id": 33, "type": "group", "title": "join-me"}}} + ] + }`) + chats, maxID, _, err := GroupsFromUpdates(body) + if err != nil { + t.Fatal(err) + } + if maxID != 2 { + t.Fatalf("maxID=%d", maxID) + } + if len(chats) != 2 { + t.Fatalf("len=%d %#v", len(chats), chats) + } +} + func TestFilterSafewChats(t *testing.T) { chats := []SafewChat{ {ID: "10000778141", Type: "group", Title: "测试AI"}, @@ -69,6 +89,11 @@ func TestFilterSafewChats(t *testing.T) { func TestPollGroupChatsSuccess(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/bottok/deleteWebhook" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"ok":true,"result":true}`)) + return + } if r.URL.Path != "/bottok/getUpdates" { t.Errorf("path=%s", r.URL.Path) } @@ -81,6 +106,10 @@ func TestPollGroupChatsSuccess(t *testing.T) { if body["offset"] != float64(5) { t.Errorf("offset=%v", body["offset"]) } + got, _ := body["allowed_updates"].([]any) + if len(got) == 0 { + t.Errorf("missing allowed_updates: %s", raw) + } w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"ok":true,"result":[{"update_id":10,"message":{"chat":{"id":10000778141,"type":"group","title":"测试AI"}}}]}`)) }))