fix(zalo): update API version, batch group fetch, fix WS decrypt (#516)

- 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>
This commit is contained in:
Luan VuandLuvu182 authored and GitHub committed 2026-03-27 21:50:01 +07:00
1 parent e297fb63b8
commit be9044ca8e
4 files changed
+68 -31

No files matched your search

@@ -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"
@@ -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
}
@@ -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 {
@@ -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"
/>
<div className="max-h-48 overflow-y-auto rounded border p-2 space-y-1">
{filteredFriends.length > 0 && (
<>
<p className="text-xs font-medium text-muted-foreground">{t("zalo.friends")}</p>
{filteredFriends.map((f) => (
<Tabs defaultValue="friends">
<TabsList className="w-full">
<TabsTrigger value="friends" className="flex-1 text-xs">
{t("zalo.friends")} ({filteredFriends.length})
</TabsTrigger>
<TabsTrigger value="groups" className="flex-1 text-xs">
{t("zalo.groups")} ({filteredGroups.length})
</TabsTrigger>
</TabsList>
<TabsContent value="friends" className="mt-2">
<div className="max-h-56 overflow-y-auto rounded border p-2 space-y-1">
{filteredFriends.length > 0 ? filteredFriends.map((f) => (
<label key={f.userId} className="flex items-center gap-2 py-0.5 text-sm cursor-pointer hover:bg-muted/50 rounded px-1">
<input type="checkbox" checked={value.includes(f.userId)} onChange={() => toggle(f.userId)} />
<span>{f.displayName}</span>
<span className="text-xs text-muted-foreground ml-auto">{f.userId}</span>
<span className="truncate">{f.displayName}</span>
<span className="text-xs text-muted-foreground ml-auto shrink-0">{f.userId}</span>
</label>
))}
</>
)}
{filteredGroups.length > 0 && (
<>
<p className="text-xs font-medium text-muted-foreground mt-2">{t("zalo.groups")}</p>
{filteredGroups.map((g) => (
)) : (
<p className="text-sm text-muted-foreground py-2 text-center">
{search ? t("zalo.noContactsMatch", { search }) : t("zalo.noContacts")}
</p>
)}
</div>
</TabsContent>
<TabsContent value="groups" className="mt-2">
<div className="max-h-56 overflow-y-auto rounded border p-2 space-y-1">
{filteredGroups.length > 0 ? filteredGroups.map((g) => (
<label key={g.groupId} className="flex items-center gap-2 py-0.5 text-sm cursor-pointer hover:bg-muted/50 rounded px-1">
<input type="checkbox" checked={value.includes(g.groupId)} onChange={() => toggle(g.groupId)} />
<span>{g.name}</span>
<span className="text-xs text-muted-foreground ml-auto">{t("zalo.membersCount", { count: g.totalMember })}</span>
<span className="truncate">{g.name}</span>
<span className="text-xs text-muted-foreground ml-auto shrink-0">{t("zalo.membersCount", { count: g.totalMember })}</span>
</label>
))}
</>
)}
{filteredFriends.length === 0 && filteredGroups.length === 0 && (
<p className="text-sm text-muted-foreground py-2 text-center">
{search ? t("zalo.noContactsMatch", { search }) : t("zalo.noContacts")}
</p>
)}
</div>
)) : (
<p className="text-sm text-muted-foreground py-2 text-center">
{search ? t("zalo.noContactsMatch", { search }) : t("zalo.noContacts")}
</p>
)}
</div>
</TabsContent>
</Tabs>
</>
)}