diff --git a/internal/channels/zalo/zalo.go b/internal/channels/zalo/zalo.go index aafa53a2..7a4c84e5 100644 --- a/internal/channels/zalo/zalo.go +++ b/internal/channels/zalo/zalo.go @@ -44,6 +44,7 @@ type Channel struct { blockReply *bool stopCh chan struct{} client *http.Client + pollClient *http.Client // pairingService, pairingDebounce are inherited from channels.BaseChannel. } @@ -74,6 +75,7 @@ func New(cfg config.ZaloConfig, msgBus *bus.MessageBus, pairingSvc store.Pairing blockReply: cfg.BlockReply, stopCh: make(chan struct{}), client: &http.Client{Timeout: 60 * time.Second}, + pollClient: &http.Client{Timeout: 0}, } ch.SetPairingService(pairingSvc) return ch, nil @@ -398,7 +400,7 @@ type zaloAPIResponse struct { type zaloBotInfo struct { ID string `json:"id"` - Name string `json:"name"` + Name string `json:"display_name"` } type zaloMessage struct { @@ -414,12 +416,12 @@ type zaloMessage struct { type zaloFrom struct { ID string `json:"id"` - Username string `json:"username"` + Username string `json:"display_name"` } type zaloChat struct { ID string `json:"id"` - Type string `json:"type"` + Type string `json:"chat_type"` } type zaloUpdate struct { @@ -428,6 +430,10 @@ type zaloUpdate struct { } func (c *Channel) callAPI(method string, body any) (json.RawMessage, error) { + return c.callAPIWith(context.Background(), c.client, method, body) +} + +func (c *Channel) callAPIWith(ctx context.Context, client *http.Client, method string, body any) (json.RawMessage, error) { url := fmt.Sprintf("%s/bot%s/%s", apiBase, c.token, method) var reqBody io.Reader @@ -439,7 +445,7 @@ func (c *Channel) callAPI(method string, body any) (json.RawMessage, error) { reqBody = bytes.NewReader(data) } - req, err := http.NewRequest("POST", url, reqBody) + req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody) if err != nil { return nil, fmt.Errorf("create request: %w", err) } @@ -447,7 +453,7 @@ func (c *Channel) callAPI(method string, body any) (json.RawMessage, error) { req.Header.Set("Content-Type", "application/json") } - resp, err := c.client.Do(req) + resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("api call %s: %w", method, err) } @@ -488,16 +494,22 @@ func (c *Channel) getUpdates(timeout int) ([]zaloUpdate, error) { "timeout": timeout, } - result, err := c.callAPI("getUpdates", params) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout+7)*time.Second) + defer cancel() + + result, err := c.callAPIWith(ctx, c.pollClient, "getUpdates", params) if err != nil { return nil, err } - var updates []zaloUpdate - if err := json.Unmarshal(result, &updates); err != nil { + var update zaloUpdate + if err := json.Unmarshal(result, &update); err != nil { return nil, fmt.Errorf("unmarshal updates: %w", err) } - return updates, nil + if update.EventName == "" { + return nil, nil + } + return []zaloUpdate{update}, nil } func (c *Channel) sendMessage(chatID, text string) error { diff --git a/internal/channels/zalo/zalo_test.go b/internal/channels/zalo/zalo_test.go index a03c1c0b..1a501104 100644 --- a/internal/channels/zalo/zalo_test.go +++ b/internal/channels/zalo/zalo_test.go @@ -115,7 +115,7 @@ func TestCallAPI_MalformedJSONReturnsError(t *testing.T) { // TestGetMe_ParsesBotInfo verifies getMe returns the zaloBotInfo struct. func TestGetMe_ParsesBotInfo(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(`{"ok":true,"result":{"id":"bot-xyz","name":"TestBot"}}`)) + _, _ = w.Write([]byte(`{"ok":true,"result":{"id":"bot-xyz","display_name":"TestBot"}}`)) })) defer srv.Close() @@ -142,10 +142,10 @@ func TestGetMe_UnmarshalError(t *testing.T) { } } -// TestGetUpdates_ParsesUpdateArray verifies getUpdates decodes the updates array. -func TestGetUpdates_ParsesUpdateArray(t *testing.T) { +// TestGetUpdates_ParsesSingleUpdate verifies getUpdates decodes the single-object API response. +func TestGetUpdates_ParsesSingleUpdate(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(`{"ok":true,"result":[{"event_name":"message.text.received","message":{"message_id":"m1","text":"hi","from":{"id":"user1"},"chat":{"id":"user1"}}}]}`)) + _, _ = w.Write([]byte(`{"ok":true,"result":{"event_name":"message.text.received","message":{"message_id":"m1","text":"hi","from":{"id":"user1"},"chat":{"id":"user1"}}}}`)) })) defer srv.Close() @@ -168,7 +168,7 @@ func TestGetUpdates_ParsesUpdateArray(t *testing.T) { // TestGetUpdates_UnmarshalError verifies a malformed result surfaces an error. func TestGetUpdates_UnmarshalError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(`{"ok":true,"result":"string-instead-of-array"}`)) + _, _ = w.Write([]byte(`{"ok":true,"result":"not-an-object"}`)) })) defer srv.Close()