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.
This commit is contained in:
+64
-26
@@ -5,8 +5,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,14 +118,27 @@ func (e *SafewAuthError) Error() string {
|
|||||||
|
|
||||||
var safewLongPollClient = &http.Client{Timeout: 45 * time.Second}
|
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) {
|
func (s *SafeWSender) PollGroupChats(token string, offset int64, timeout int) ([]SafewChat, int64, error) {
|
||||||
token = strings.TrimSpace(token)
|
token = strings.TrimSpace(token)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return nil, offset, fmt.Errorf("safew: token is required")
|
return nil, offset, fmt.Errorf("safew: token is required")
|
||||||
}
|
}
|
||||||
|
s.ensureWebhookCleared(token)
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"timeout": timeout,
|
"timeout": timeout,
|
||||||
"limit": 100,
|
"limit": 100,
|
||||||
|
"allowed_updates": safewAllowedUpdates,
|
||||||
}
|
}
|
||||||
if offset > 0 {
|
if offset > 0 {
|
||||||
payload["offset"] = offset
|
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)
|
return nil, offset, fmt.Errorf("safew: %s", api.Description)
|
||||||
}
|
}
|
||||||
chats, maxID, err := GroupsFromUpdates(respBody)
|
chats, maxID, n, err := GroupsFromUpdates(respBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, offset, err
|
return nil, offset, err
|
||||||
}
|
}
|
||||||
@@ -163,9 +178,27 @@ func (s *SafeWSender) PollGroupChats(token string, offset int64, timeout int) ([
|
|||||||
if maxID > 0 {
|
if maxID > 0 {
|
||||||
next = maxID + 1
|
next = maxID + 1
|
||||||
}
|
}
|
||||||
|
slog.Info("safew getUpdates", "timeout", timeout, "updates", n, "groups", len(chats), "offset", next)
|
||||||
return chats, next, nil
|
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) {
|
func parseSafewChatID(raw json.RawMessage) (string, error) {
|
||||||
raw = bytes.TrimSpace(raw)
|
raw = bytes.TrimSpace(raw)
|
||||||
if len(raw) == 0 || string(raw) == "null" {
|
if len(raw) == 0 || string(raw) == "null" {
|
||||||
@@ -220,7 +253,7 @@ type SafewChat struct {
|
|||||||
Username *string `json:"username"`
|
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 := json.NewDecoder(bytes.NewReader(body))
|
||||||
dec.UseNumber()
|
dec.UseNumber()
|
||||||
var wrap struct {
|
var wrap struct {
|
||||||
@@ -228,14 +261,14 @@ func GroupsFromUpdates(body []byte) ([]SafewChat, int64, error) {
|
|||||||
Result []json.RawMessage `json:"result"`
|
Result []json.RawMessage `json:"result"`
|
||||||
}
|
}
|
||||||
if err := dec.Decode(&wrap); err != nil {
|
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{}
|
seen := map[string]SafewChat{}
|
||||||
var maxID int64
|
var maxID int64
|
||||||
for _, item := range wrap.Result {
|
for _, item := range wrap.Result {
|
||||||
id, chat, err := parseUpdateItem(item)
|
id, chat, err := parseUpdateItem(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
if id > maxID {
|
if id > maxID {
|
||||||
maxID = id
|
maxID = id
|
||||||
@@ -252,7 +285,7 @@ func GroupsFromUpdates(body []byte) ([]SafewChat, int64, error) {
|
|||||||
for _, c := range seen {
|
for _, c := range seen {
|
||||||
out = append(out, c)
|
out = append(out, c)
|
||||||
}
|
}
|
||||||
return out, maxID, nil
|
return out, maxID, len(wrap.Result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUpdateItem(item json.RawMessage) (int64, *SafewChat, error) {
|
func parseUpdateItem(item json.RawMessage) (int64, *SafewChat, error) {
|
||||||
@@ -269,7 +302,10 @@ func parseUpdateItem(item json.RawMessage) (int64, *SafewChat, error) {
|
|||||||
updateID, _ = n.Int64()
|
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]
|
raw, ok := u[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -289,26 +325,28 @@ func chatFromNested(raw json.RawMessage) *SafewChat {
|
|||||||
if err := dec.Decode(&obj); err != nil {
|
if err := dec.Decode(&obj); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
chatRaw, ok := obj["chat"]
|
if chatRaw, ok := obj["chat"]; ok {
|
||||||
if !ok {
|
dec = json.NewDecoder(bytes.NewReader(chatRaw))
|
||||||
return nil
|
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))
|
if msgRaw, ok := obj["message"]; ok {
|
||||||
dec.UseNumber()
|
return chatFromNested(msgRaw)
|
||||||
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
|
||||||
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 {
|
func FilterSafewChats(chats []SafewChat, q string) []SafewChat {
|
||||||
|
|||||||
@@ -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"}}}
|
{"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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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) {
|
func TestFilterSafewChats(t *testing.T) {
|
||||||
chats := []SafewChat{
|
chats := []SafewChat{
|
||||||
{ID: "10000778141", Type: "group", Title: "测试AI"},
|
{ID: "10000778141", Type: "group", Title: "测试AI"},
|
||||||
@@ -69,6 +89,11 @@ func TestFilterSafewChats(t *testing.T) {
|
|||||||
|
|
||||||
func TestPollGroupChatsSuccess(t *testing.T) {
|
func TestPollGroupChatsSuccess(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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" {
|
if r.URL.Path != "/bottok/getUpdates" {
|
||||||
t.Errorf("path=%s", r.URL.Path)
|
t.Errorf("path=%s", r.URL.Path)
|
||||||
}
|
}
|
||||||
@@ -81,6 +106,10 @@ func TestPollGroupChatsSuccess(t *testing.T) {
|
|||||||
if body["offset"] != float64(5) {
|
if body["offset"] != float64(5) {
|
||||||
t.Errorf("offset=%v", body["offset"])
|
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.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write([]byte(`{"ok":true,"result":[{"update_id":10,"message":{"chat":{"id":10000778141,"type":"group","title":"测试AI"}}}]}`))
|
_, _ = w.Write([]byte(`{"ok":true,"result":[{"update_id":10,"message":{"chat":{"id":10000778141,"type":"group","title":"测试AI"}}}]}`))
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user