fix(zalo): correct json field mappings (#924)

Fixes #922 - Zalo OA bot was silently dropping all incoming messages due to JSON field mapping mismatches.

- Align zaloBotInfo/zaloFrom/zaloChat JSON tags with live API (display_name, chat_type)
- Change getUpdates to decode single-object response (API returns one update, not an array)
- Add dedicated pollClient with Timeout=0 + per-call context timeout for long-polling
- Update tests accordingly
This commit is contained in:
Uğur Tafralı authored and GitHub committed 2026-04-16 14:17:07 +07:00
1 parent 8873548102
commit 12cfcec59a
2 files changed
+26 -14

No files matched your search

+21 -9
View File
@@ -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 {
+5 -5
View File
@@ -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()