From be9044ca8e419f267df77fabf42249616f42746e Mon Sep 17 00:00:00 2001 From: Luan Vu Date: Fri, 27 Mar 2026 21:50:01 +0700 Subject: [PATCH] fix(zalo): update API version, batch group fetch, fix WS decrypt (#516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump DefaultAPIVersion from 665 to 671 to match current Zalo web client - Batch getmg-v2 requests (50 groups/batch) — Zalo rejects large payloads, causing error 114 for accounts with many groups (e.g. 250+) - Fix encryptType=1 WebSocket event decryption: add gzip decompression after base64 decode (matches zca-js pako.inflate behavior) - Split contacts picker into Friends/Groups tabs for better usability Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com> --- .../channels/zalo/personal/protocol/config.go | 2 +- .../zalo/personal/protocol/contacts.go | 29 ++++++++- .../personal/protocol/listener_handlers.go | 8 ++- .../channels/zalo/zalo-contacts-picker.tsx | 60 +++++++++++-------- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/internal/channels/zalo/personal/protocol/config.go b/internal/channels/zalo/personal/protocol/config.go index 1dc9e032..c7a46d23 100644 --- a/internal/channels/zalo/personal/protocol/config.go +++ b/internal/channels/zalo/personal/protocol/config.go @@ -14,7 +14,7 @@ const ( DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0" DefaultLanguage = "vi" DefaultAPIType = 30 - DefaultAPIVersion = 665 + DefaultAPIVersion = 671 DefaultComputerName = "Web" DefaultUIDSelf = "0" DefaultEncryptVer = "v2" diff --git a/internal/channels/zalo/personal/protocol/contacts.go b/internal/channels/zalo/personal/protocol/contacts.go index 06b08c91..82cc9a51 100644 --- a/internal/channels/zalo/personal/protocol/contacts.go +++ b/internal/channels/zalo/personal/protocol/contacts.go @@ -96,8 +96,32 @@ func FetchGroups(ctx context.Context, sess *Session) ([]GroupListInfo, error) { return nil, nil } - // Step 2: Get group details from group service - return fetchGroupDetails(ctx, sess, gridVerMap) + // Step 2: Get group details in batches (Zalo rejects large payloads) + const batchSize = 50 + ids := make([]string, 0, len(gridVerMap)) + for id := range gridVerMap { + ids = append(ids, id) + } + + var allGroups []GroupListInfo + for i := 0; i < len(ids); i += batchSize { + end := i + batchSize + if end > len(ids) { + end = len(ids) + } + batch := make(map[string]string, end-i) + for _, id := range ids[i:end] { + batch[id] = gridVerMap[id] + } + groups, err := fetchGroupDetails(ctx, sess, batch) + if err != nil { + return nil, err + } + allGroups = append(allGroups, groups...) + } + + sort.Slice(allGroups, func(i, j int) bool { return allGroups[i].Name < allGroups[j].Name }) + return allGroups, nil } // fetchGroupIDs gets group ID -> version map from group_poll service. @@ -222,7 +246,6 @@ func fetchGroupDetails(ctx context.Context, sess *Session, gridVerMap map[string TotalMember: info.TotalMember, }) } - sort.Slice(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name }) return groups, nil } diff --git a/internal/channels/zalo/personal/protocol/listener_handlers.go b/internal/channels/zalo/personal/protocol/listener_handlers.go index a7d5c09e..99493580 100644 --- a/internal/channels/zalo/personal/protocol/listener_handlers.go +++ b/internal/channels/zalo/personal/protocol/listener_handlers.go @@ -163,8 +163,12 @@ func (ln *Listener) decryptEventData(data string, encType uint, cipherKey string switch encType { case 0: // plaintext result = []byte(data) - case 1: // base64 only - result, err = base64.StdEncoding.DecodeString(data) + case 1: // base64 + gzip + raw, e := base64.StdEncoding.DecodeString(data) + if e != nil { + return nil, e + } + result, err = decompressGzip(raw) case 2: // AES-GCM + gzip raw, e := ln.decryptAESGCMPayload(data, cipherKey) if e != nil { diff --git a/ui/web/src/pages/channels/zalo/zalo-contacts-picker.tsx b/ui/web/src/pages/channels/zalo/zalo-contacts-picker.tsx index 191c3678..8c9667a6 100644 --- a/ui/web/src/pages/channels/zalo/zalo-contacts-picker.tsx +++ b/ui/web/src/pages/channels/zalo/zalo-contacts-picker.tsx @@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useWsCall } from "@/hooks/use-ws-call"; interface Friend { @@ -134,37 +135,46 @@ export function ZaloContactsPicker({ instanceId, hasCredentials, value, onChange onChange={(e) => setSearch(e.target.value)} className="h-8" /> -
- {filteredFriends.length > 0 && ( - <> -

{t("zalo.friends")}

- {filteredFriends.map((f) => ( + + + + {t("zalo.friends")} ({filteredFriends.length}) + + + {t("zalo.groups")} ({filteredGroups.length}) + + + +
+ {filteredFriends.length > 0 ? filteredFriends.map((f) => ( - ))} - - )} - {filteredGroups.length > 0 && ( - <> -

{t("zalo.groups")}

- {filteredGroups.map((g) => ( + )) : ( +

+ {search ? t("zalo.noContactsMatch", { search }) : t("zalo.noContacts")} +

+ )} +
+
+ +
+ {filteredGroups.length > 0 ? filteredGroups.map((g) => ( - ))} - - )} - {filteredFriends.length === 0 && filteredGroups.length === 0 && ( -

- {search ? t("zalo.noContactsMatch", { search }) : t("zalo.noContacts")} -

- )} -
+ )) : ( +

+ {search ? t("zalo.noContactsMatch", { search }) : t("zalo.noContacts")} +

+ )} +
+ + )}