zalo-agent-cli 1.0.29 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,8 +35,8 @@ Built on top of [zca-js](https://github.com/AKAspanion/zca-js), the unofficial Z
35
35
  - Send text, images, files, contact cards, stickers, reactions
36
36
  - Send bank cards (55+ Vietnamese banks)
37
37
  - Generate and send VietQR transfer images via qr.sepay.vn
38
- - Friend management (list, find, add, remove, block)
39
- - Group management (create, rename, add/remove members)
38
+ - Friend management (list, find, add, remove, block, alias, recommendations)
39
+ - Group management (create, rename, members, settings, links, notes, invites)
40
40
  - Conversation management (mute, pin, archive, read/unread)
41
41
  - Export/import credentials for headless server deployment
42
42
  - Local HTTP server for QR display on VPS (via SSH tunnel)
@@ -141,59 +141,62 @@ zalo-agent whoami
141
141
 
142
142
  #### Global Flags
143
143
 
144
- | Flag | Description |
145
- |------|-------------|
146
- | `--json` | Output all results as JSON |
147
- | `-V, --version` | Show version number |
148
- | `-h, --help` | Show help |
144
+ | Flag | Description |
145
+ | --------------- | -------------------------- |
146
+ | `--json` | Output all results as JSON |
147
+ | `-V, --version` | Show version number |
148
+ | `-h, --help` | Show help |
149
149
 
150
150
  #### Auth
151
151
 
152
- | Command | Description |
153
- |---------|-------------|
152
+ | Command | Description |
153
+ | ----------------------------------------------------- | ----------------------------------------- |
154
154
  | `login [--proxy URL] [--credentials PATH] [--qr-url]` | Login via QR or from exported credentials |
155
- | `logout` | Clear current session |
156
- | `status` | Show login state |
157
- | `whoami` | Show current user profile |
155
+ | `logout` | Clear current session |
156
+ | `status` | Show login state |
157
+ | `whoami` | Show current user profile |
158
158
 
159
159
  #### Messages (`msg`)
160
160
 
161
- | Command | Description |
162
- |---------|-------------|
163
- | `msg send <threadId> <text> [-t 0\|1] [--md] [--style specs...]` | Send text message (with formatting) |
164
- | `msg send-image <threadId> <paths...> [-t 0\|1] [-m caption]` | Send images |
165
- | `msg send-file <threadId> <paths...> [-t 0\|1] [-m caption]` | Send files |
166
- | `msg send-card <threadId> <userId> [-t 0\|1] [--phone NUM]` | Send contact card |
167
- | `msg send-bank <threadId> <accountNum> -b BANK [-n name] [-t 0\|1]` | Send bank card |
168
- | `msg send-qr-transfer <threadId> <accountNum> -b BANK [-a amount] [-m content] [--template tpl]` | Send VietQR transfer image |
169
- | `msg send-link <threadId> <url> [-m caption] [-t 0\|1]` | Send link with auto-preview |
170
- | `msg send-video <threadId> <videoUrl> --thumb <thumbUrl> [-m caption] [-d ms] [-W px] [-H px]` | Send video from URL |
171
- | `msg sticker <threadId> <keyword> [-t 0\|1]` | Search and send sticker |
172
- | `msg react <msgId> <threadId> <emoji> [-t 0\|1]` | React to a message |
173
- | `msg delete <msgId> <threadId> [-t 0\|1]` | Delete a message |
174
- | `msg forward <msgId> <threadId> [-t 0\|1]` | Forward a message |
161
+ | Command | Description |
162
+ | ------------------------------------------------------------------------------------------------ | ----------------------------------- |
163
+ | `msg send <threadId> <text> [-t 0\|1] [--md] [--style specs...]` | Send text message (with formatting) |
164
+ | `msg send-image <threadId> <paths...> [-t 0\|1] [-m caption]` | Send images |
165
+ | `msg send-file <threadId> <paths...> [-t 0\|1] [-m caption]` | Send files |
166
+ | `msg send-card <threadId> <userId> [-t 0\|1] [--phone NUM]` | Send contact card |
167
+ | `msg send-bank <threadId> <accountNum> -b BANK [-n name] [-t 0\|1]` | Send bank card |
168
+ | `msg send-qr-transfer <threadId> <accountNum> -b BANK [-a amount] [-m content] [--template tpl]` | Send VietQR transfer image |
169
+ | `msg send-voice <threadId> <voiceUrl> [-t 0\|1] [--ttl ms]` | Send a voice message from URL |
170
+ | `msg send-link <threadId> <url> [-m caption] [-t 0\|1]` | Send link with auto-preview |
171
+ | `msg send-video <threadId> <videoUrl> --thumb <thumbUrl> [-m caption] [-d ms] [-W px] [-H px]` | Send video from URL |
172
+ | `msg sticker <threadId> <keyword> [-t 0\|1]` | Search and send sticker |
173
+ | `msg react <msgId> <threadId> <emoji> [-t 0\|1]` | React to a message |
174
+ | `msg delete <msgId> <threadId> [-t 0\|1]` | Delete a message |
175
+ | `msg forward <msgId> <threadId> [-t 0\|1]` | Forward a message |
175
176
 
176
177
  > `-t 0` = User (default), `-t 1` = Group
177
178
 
178
179
  **Text formatting with `--md` (markdown mode):**
180
+
179
181
  ```bash
180
182
  zalo-agent msg send <threadId> "**Bold** *Italic* __Underline__ ~~Strike~~ {red:Red} {big:BIG}" --md
181
183
  ```
182
184
 
183
- | Syntax | Style |
184
- |--------|-------|
185
- | `**text**` | Bold |
186
- | `*text*` | Italic |
187
- | `__text__` | Underline |
188
- | `~~text~~` | Strikethrough |
189
- | `{red:text}` | Red text |
190
- | `{orange:text}` | Orange text |
191
- | `{yellow:text}` | Yellow text |
192
- | `{green:text}` | Green text |
193
- | `{big:text}` | Large font |
194
- | `{small:text}` | Small font |
185
+ | Syntax | Style |
186
+ | --------------- | ------------- |
187
+ | `**text**` | Bold |
188
+ | `*text*` | Italic |
189
+ | `__text__` | Underline |
190
+ | `~~text~~` | Strikethrough |
191
+ | `{red:text}` | Red text |
192
+ | `{orange:text}` | Orange text |
193
+ | `{yellow:text}` | Yellow text |
194
+ | `{green:text}` | Green text |
195
+ | `{big:text}` | Large font |
196
+ | `{small:text}` | Small font |
195
197
 
196
198
  **Manual style with `--style` (for agents/automation):**
199
+
197
200
  ```bash
198
201
  # Format: start:len:style — style names: bold, italic, underline, strikethrough, red, orange, yellow, green, big, small
199
202
  zalo-agent msg send <threadId> "Hello World" --style 0:5:bold 6:5:italic
@@ -201,77 +204,107 @@ zalo-agent msg send <threadId> "Hello World" --style 0:5:bold 6:5:italic
201
204
 
202
205
  #### Friends (`friend`)
203
206
 
204
- | Command | Description |
205
- |---------|-------------|
206
- | `friend list` | List all friends |
207
- | `friend search <name>` | Search friends by name (get thread_id) |
208
- | `friend online` | List online friends |
209
- | `friend find <query>` | Find by phone or ID |
210
- | `friend info <userId>` | Get user profile |
211
- | `friend add <userId> [-m msg]` | Send friend request |
212
- | `friend accept <userId>` | Accept request |
213
- | `friend remove <userId>` | Remove friend |
214
- | `friend block <userId>` | Block user |
215
- | `friend unblock <userId>` | Unblock user |
216
- | `friend last-online <userId>` | Check last seen |
207
+ | Command | Description |
208
+ | ---------------------------------------- | ---------------------------------------------- |
209
+ | `friend list` | List all friends |
210
+ | `friend search <name>` | Search friends by name (get thread_id) |
211
+ | `friend online` | List online friends |
212
+ | `friend find <query>` | Find by phone or ID |
213
+ | `friend info <userId>` | Get user profile |
214
+ | `friend add <userId> [-m msg]` | Send friend request |
215
+ | `friend accept <userId>` | Accept request |
216
+ | `friend remove <userId>` | Remove friend |
217
+ | `friend block <userId>` | Block user |
218
+ | `friend unblock <userId>` | Unblock user |
219
+ | `friend last-online <userId>` | Check last seen |
220
+ | `friend find-username <username>` | Find user by Zalo username |
221
+ | `friend alias <friendId> <alias>` | Set nickname for a friend |
222
+ | `friend alias-list [-c count] [-p page]` | List all friend aliases |
223
+ | `friend alias-remove <friendId>` | Remove a friend's alias |
224
+ | `friend reject <userId>` | Reject a friend request |
225
+ | `friend undo-request <userId>` | Cancel a sent friend request |
226
+ | `friend sent-requests` | List sent friend requests |
227
+ | `friend request-status <userId>` | Check friend request status |
228
+ | `friend close` | List close friends |
229
+ | `friend recommendations` | Get friend recommendations & received requests |
230
+ | `friend find-phones <phones...>` | Find users by phone numbers |
217
231
 
218
232
  #### Groups (`group`)
219
233
 
220
- | Command | Description |
221
- |---------|-------------|
222
- | `group list` | List all groups |
223
- | `group create <name> <memberIds...>` | Create group |
224
- | `group history <groupId> [-n count]` | Get chat history (normalized JSON) |
225
- | `group info <groupId>` | Group details |
226
- | `group members <groupId>` | List members |
227
- | `group add-member <groupId> <userIds...>` | Add members |
228
- | `group remove-member <groupId> <userIds...>` | Remove members |
229
- | `group rename <groupId> <name>` | Rename |
230
- | `group upgrade-community <groupId>` | Upgrade group to Zalo Community |
231
- | `group leave <groupId>` | Leave group |
232
- | `group join <link>` | Join via invite link |
234
+ | Command | Description |
235
+ | ---------------------------------------------------- | --------------------------------------- |
236
+ | `group list` | List all groups |
237
+ | `group create <name> <memberIds...>` | Create group |
238
+ | `group history <groupId> [-n count]` | Get chat history (normalized JSON) |
239
+ | `group info <groupId>` | Group details |
240
+ | `group members <groupId>` | List members |
241
+ | `group add-member <groupId> <userIds...>` | Add members |
242
+ | `group remove-member <groupId> <userIds...>` | Remove members |
243
+ | `group rename <groupId> <name>` | Rename |
244
+ | `group upgrade-community <groupId>` | Upgrade group to Zalo Community |
245
+ | `group leave <groupId>` | Leave group |
246
+ | `group join <link>` | Join via invite link |
247
+ | `group members-info <userIds...>` | Get detailed info for members by IDs |
248
+ | `group settings <groupId> [flags]` | Update group settings (see flags below) |
249
+ | `group pending <groupId>` | List pending member requests (admin) |
250
+ | `group approve <groupId> <userIds...>` | Approve pending members (admin) |
251
+ | `group reject-member <groupId> <userIds...>` | Reject pending members (admin) |
252
+ | `group enable-link <groupId>` | Enable group invite link |
253
+ | `group disable-link <groupId>` | Disable group invite link |
254
+ | `group link-info <groupId>` | Get group invite link details |
255
+ | `group blocked <groupId> [-c count] [-p page]` | List blocked members |
256
+ | `group note-create <groupId> <title> [--pin]` | Create a note |
257
+ | `group note-edit <groupId> <noteId> <title> [--pin]` | Edit a note |
258
+ | `group invite-boxes` | List received group invitations |
259
+ | `group join-invite <groupId>` | Accept a group invitation |
260
+ | `group delete-invite <groupIds...> [--block]` | Delete invitations |
261
+ | `group invite-to <userId> <groupIds...>` | Invite user to groups |
262
+ | `group disperse <groupId>` | Disperse group (irreversible!) |
263
+
264
+ **Group settings flags:** `--block-name`, `--sign-admin`, `--msg-history`, `--join-appr`, `--lock-post`, `--lock-poll`, `--lock-msg`, `--lock-view-member` (prefix with `--no-` to disable)
233
265
 
234
266
  #### Conversations (`conv`)
235
267
 
236
- | Command | Description |
237
- |---------|-------------|
268
+ | Command | Description |
269
+ | --------------------------------------------------------- | ---------------------------------------- |
238
270
  | `conv recent [-n limit] [--friends-only] [--groups-only]` | List recent conversations with thread_id |
239
- | `conv pinned` | List pinned |
240
- | `conv archived` | List archived |
241
- | `conv mute <threadId> [-t 0\|1] [-d secs]` | Mute (-1 = forever) |
242
- | `conv unmute <threadId> [-t 0\|1]` | Unmute |
243
- | `conv read <threadId> [-t 0\|1]` | Mark as read |
244
- | `conv unread <threadId> [-t 0\|1]` | Mark as unread |
245
- | `conv delete <threadId> [-t 0\|1]` | Delete conversation |
271
+ | `conv pinned` | List pinned |
272
+ | `conv archived` | List archived |
273
+ | `conv mute <threadId> [-t 0\|1] [-d secs]` | Mute (-1 = forever) |
274
+ | `conv unmute <threadId> [-t 0\|1]` | Unmute |
275
+ | `conv read <threadId> [-t 0\|1]` | Mark as read |
276
+ | `conv unread <threadId> [-t 0\|1]` | Mark as unread |
277
+ | `conv delete <threadId> [-t 0\|1]` | Delete conversation |
246
278
 
247
279
  #### Profile (`profile`)
248
280
 
249
- | Command | Description |
250
- |---------|-------------|
251
- | `profile me` | Show your profile (name, phone, avatar, etc.) |
252
- | `profile avatar <imagePath>` | Change profile avatar |
253
- | `profile bio [text]` | View or update bio/status |
254
- | `profile update [-n name] [-d YYYY-MM-DD] [-g 0\|1]` | Update name, birthday, gender |
255
- | `profile settings` | View privacy settings |
256
- | `profile set <setting> <value>` | Update a privacy setting |
281
+ | Command | Description |
282
+ | ---------------------------------------------------- | --------------------------------------------- |
283
+ | `profile me` | Show your profile (name, phone, avatar, etc.) |
284
+ | `profile avatar <imagePath>` | Change profile avatar |
285
+ | `profile bio [text]` | View or update bio/status |
286
+ | `profile update [-n name] [-d YYYY-MM-DD] [-g 0\|1]` | Update name, birthday, gender |
287
+ | `profile settings` | View privacy settings |
288
+ | `profile set <setting> <value>` | Update a privacy setting |
257
289
 
258
290
  **Privacy settings:** `online-status`, `seen-status`, `birthday`, `receive-msg`, `accept-call`, `add-by-phone`, `add-by-qr`, `add-by-group`, `recommend`
259
291
 
260
292
  #### Polls (`poll`)
261
293
 
262
- | Command | Description |
263
- |---------|-------------|
264
- | `poll create <groupId> <question> <options...>` | Create a poll (see flags below) |
265
- | `poll info <pollId>` | View poll details and vote results |
266
- | `poll vote <pollId> <optionIds...>` | Vote on a poll (option IDs from `poll info`) |
267
- | `poll unvote <pollId>` | Remove your vote |
268
- | `poll add-option <pollId> <options...> [--vote]` | Add new options to a poll |
269
- | `poll lock <pollId>` | Close a poll (no more votes) |
270
- | `poll share <pollId>` | Share a poll |
294
+ | Command | Description |
295
+ | ------------------------------------------------ | -------------------------------------------- |
296
+ | `poll create <groupId> <question> <options...>` | Create a poll (see flags below) |
297
+ | `poll info <pollId>` | View poll details and vote results |
298
+ | `poll vote <pollId> <optionIds...>` | Vote on a poll (option IDs from `poll info`) |
299
+ | `poll unvote <pollId>` | Remove your vote |
300
+ | `poll add-option <pollId> <options...> [--vote]` | Add new options to a poll |
301
+ | `poll lock <pollId>` | Close a poll (no more votes) |
302
+ | `poll share <pollId>` | Share a poll |
271
303
 
272
304
  **Poll create flags:** `--multi` (multiple choices), `--add-options` (members can add options), `--anonymous` (hide voters), `--hide-preview` (hide results until voted), `--expire <minutes>` (auto-close)
273
305
 
274
306
  **Example:**
307
+
275
308
  ```bash
276
309
  # Create a multi-choice poll with 3 options, auto-close after 60 minutes
277
310
  zalo-agent poll create <groupId> "Chọn ngày họp" "Thứ 2" "Thứ 4" "Thứ 6" --multi --expire 60
@@ -288,18 +321,19 @@ zalo-agent poll lock <pollId>
288
321
 
289
322
  #### Reminders (`reminder`)
290
323
 
291
- | Command | Description |
292
- |---------|-------------|
293
- | `reminder create <threadId> <title> [-t 0\|1] [--time "YYYY-MM-DD HH:mm"] [--repeat mode] [--emoji]` | Create a reminder |
294
- | `reminder list <threadId> [-t 0\|1] [-n count]` | List reminders |
295
- | `reminder info <reminderId>` | View reminder details (group only) |
296
- | `reminder responses <reminderId>` | View who accepted/rejected (group only) |
297
- | `reminder edit <reminderId> <threadId> <title> [-t 0\|1] [--time] [--repeat] [--emoji]` | Edit a reminder |
298
- | `reminder remove <reminderId> <threadId> [-t 0\|1]` | Remove a reminder |
324
+ | Command | Description |
325
+ | ---------------------------------------------------------------------------------------------------- | --------------------------------------- |
326
+ | `reminder create <threadId> <title> [-t 0\|1] [--time "YYYY-MM-DD HH:mm"] [--repeat mode] [--emoji]` | Create a reminder |
327
+ | `reminder list <threadId> [-t 0\|1] [-n count]` | List reminders |
328
+ | `reminder info <reminderId>` | View reminder details (group only) |
329
+ | `reminder responses <reminderId>` | View who accepted/rejected (group only) |
330
+ | `reminder edit <reminderId> <threadId> <title> [-t 0\|1] [--time] [--repeat] [--emoji]` | Edit a reminder |
331
+ | `reminder remove <reminderId> <threadId> [-t 0\|1]` | Remove a reminder |
299
332
 
300
333
  **Repeat modes:** `none`, `daily`, `weekly`, `monthly`
301
334
 
302
335
  **Example:**
336
+
303
337
  ```bash
304
338
  # Create a daily reminder in a group at 9:00 AM tomorrow
305
339
  zalo-agent reminder create <groupId> "Standup meeting" -t 1 --time "2026-03-16 09:00" --repeat daily
@@ -319,14 +353,14 @@ zalo-agent reminder remove <reminderId> <groupId> -t 1
319
353
 
320
354
  #### Accounts (`account`)
321
355
 
322
- | Command | Description |
323
- |---------|-------------|
324
- | `account list` | List all registered accounts |
356
+ | Command | Description |
357
+ | ----------------------------------------------- | ------------------------------------- |
358
+ | `account list` | List all registered accounts |
325
359
  | `account login [-p proxy] [-n name] [--qr-url]` | Login new account with optional proxy |
326
- | `account switch <ownerId>` | Switch active account |
327
- | `account remove <ownerId>` | Remove account + credentials |
328
- | `account info` | Show active account |
329
- | `account export [ownerId] [-o path]` | Export credentials for transfer |
360
+ | `account switch <ownerId>` | Switch active account |
361
+ | `account remove <ownerId>` | Remove account + credentials |
362
+ | `account info` | Show active account |
363
+ | `account export [ownerId] [-o path]` | Export credentials for transfer |
330
364
 
331
365
  #### Listener (`listen`)
332
366
 
@@ -372,6 +406,7 @@ zalo-agent account switch 789012...
372
406
  ```
373
407
 
374
408
  **Important notes:**
409
+
375
410
  - Zalo enforces 1 account = 1 device (IMEI). Each QR login auto-generates a unique IMEI.
376
411
  - Use 1 dedicated proxy per account — sharing proxies risks both accounts being flagged.
377
412
  - Supported proxy protocols: `http://`, `https://`, `socks5://`
@@ -486,8 +521,8 @@ This is an **unofficial** project and is **not affiliated with, endorsed by, or
486
521
  - Gửi tin nhắn, hình ảnh, file, danh thiếp, sticker, reaction
487
522
  - Gửi thẻ ngân hàng (55+ ngân hàng Việt Nam)
488
523
  - Tạo và gửi ảnh QR chuyển khoản qua qr.sepay.vn
489
- - Quản lý bạn bè (danh sách, tìm kiếm, thêm, xóa, chặn)
490
- - Quản lý nhóm (tạo, đổi tên, thêm/xóa thành viên)
524
+ - Quản lý bạn bè (danh sách, tìm kiếm, thêm, xóa, chặn, biệt danh, gợi ý)
525
+ - Quản lý nhóm (tạo, đổi tên, thành viên, cài đặt, link, ghi chú, lời mời)
491
526
  - Quản lý hội thoại (tắt thông báo, ghim, lưu trữ)
492
527
  - Xuất/nhập credentials cho triển khai trên server
493
528
  - HTTP server local hiển thị QR cho VPS (qua SSH tunnel)
@@ -636,6 +671,7 @@ zalo-agent account switch <ID>
636
671
  ```
637
672
 
638
673
  **Lưu ý quan trọng:**
674
+
639
675
  - Zalo giới hạn 1 tài khoản = 1 thiết bị (IMEI). Mỗi lần quét QR tự tạo IMEI mới.
640
676
  - Dùng 1 proxy riêng cho mỗi tài khoản — dùng chung proxy có nguy cơ bị khóa cả 2.
641
677
  - Hỗ trợ: `http://`, `https://`, `socks5://`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zalo-agent-cli",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "description": "CLI tool for Zalo automation — multi-account, proxy support, bank transfers, QR payments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -199,4 +199,186 @@ export function registerFriendCommands(program) {
199
199
  error(e.message);
200
200
  }
201
201
  });
202
+
203
+ friend
204
+ .command("find-username <username>")
205
+ .description("Find a user by their Zalo username")
206
+ .action(async (username) => {
207
+ try {
208
+ const result = await getApi().findUserByUsername(username);
209
+ output(result, program.opts().json, () => {
210
+ if (!result) {
211
+ error(`No user found for username "${username}"`);
212
+ return;
213
+ }
214
+ info(`User ID: ${result.uid || "?"}`);
215
+ info(`Name: ${result.displayName || result.zaloName || "?"}`);
216
+ });
217
+ } catch (e) {
218
+ error(`Find username failed: ${e.message}`);
219
+ }
220
+ });
221
+
222
+ friend
223
+ .command("alias <friendId> <alias>")
224
+ .description("Set a nickname (alias) for a friend")
225
+ .action(async (friendId, alias) => {
226
+ try {
227
+ const result = await getApi().changeFriendAlias(alias, friendId);
228
+ output(result, program.opts().json, () => success(`Alias set to "${alias}" for ${friendId}`));
229
+ } catch (e) {
230
+ error(`Set alias failed: ${e.message}`);
231
+ }
232
+ });
233
+
234
+ friend
235
+ .command("alias-list")
236
+ .description("List all friend aliases")
237
+ .option("-c, --count <n>", "Page size", parseInt, 100)
238
+ .option("-p, --page <n>", "Page number", parseInt, 1)
239
+ .action(async (opts) => {
240
+ try {
241
+ const result = await getApi().getAliasList(opts.count, opts.page);
242
+ output(result, program.opts().json, () => {
243
+ const items = result?.items || [];
244
+ info(`${items.length} alias(es)`);
245
+ for (const item of items) {
246
+ console.log(` ${item.userId} ${item.alias}`);
247
+ }
248
+ });
249
+ } catch (e) {
250
+ error(`Get alias list failed: ${e.message}`);
251
+ }
252
+ });
253
+
254
+ friend
255
+ .command("alias-remove <friendId>")
256
+ .description("Remove a friend's alias")
257
+ .action(async (friendId) => {
258
+ try {
259
+ const result = await getApi().removeFriendAlias(friendId);
260
+ output(result, program.opts().json, () => success(`Alias removed for ${friendId}`));
261
+ } catch (e) {
262
+ error(`Remove alias failed: ${e.message}`);
263
+ }
264
+ });
265
+
266
+ friend
267
+ .command("reject <userId>")
268
+ .description("Reject a friend request")
269
+ .action(async (userId) => {
270
+ try {
271
+ const result = await getApi().rejectFriendRequest(userId);
272
+ output(result, program.opts().json, () => success(`Rejected friend request from ${userId}`));
273
+ } catch (e) {
274
+ error(`Reject friend request failed: ${e.message}`);
275
+ }
276
+ });
277
+
278
+ friend
279
+ .command("undo-request <userId>")
280
+ .description("Cancel a sent friend request")
281
+ .action(async (userId) => {
282
+ try {
283
+ const result = await getApi().undoFriendRequest(userId);
284
+ output(result, program.opts().json, () => success(`Friend request to ${userId} cancelled`));
285
+ } catch (e) {
286
+ error(`Undo friend request failed: ${e.message}`);
287
+ }
288
+ });
289
+
290
+ friend
291
+ .command("sent-requests")
292
+ .description("List all sent friend requests")
293
+ .action(async () => {
294
+ try {
295
+ const result = await getApi().getSentFriendRequest();
296
+ output(result, program.opts().json, () => {
297
+ const entries = Object.entries(result || {});
298
+ info(`${entries.length} sent request(s)`);
299
+ for (const [uid, req] of entries) {
300
+ console.log(` ${uid} ${req.displayName || req.zaloName || "?"}`);
301
+ }
302
+ });
303
+ } catch (e) {
304
+ // Code 112 = no friend requests
305
+ if (String(e.message).includes("112")) {
306
+ info("No sent friend requests");
307
+ } else {
308
+ error(`Get sent requests failed: ${e.message}`);
309
+ }
310
+ }
311
+ });
312
+
313
+ friend
314
+ .command("request-status <userId>")
315
+ .description("Check friend request status with a user")
316
+ .action(async (userId) => {
317
+ try {
318
+ const result = await getApi().getFriendRequestStatus(userId);
319
+ output(result, program.opts().json, () => {
320
+ info(`is_friend: ${result.is_friend}`);
321
+ info(`is_requested: ${result.is_requested}`);
322
+ info(`is_requesting: ${result.is_requesting}`);
323
+ });
324
+ } catch (e) {
325
+ error(`Get request status failed: ${e.message}`);
326
+ }
327
+ });
328
+
329
+ friend
330
+ .command("close")
331
+ .description("List close friends")
332
+ .action(async () => {
333
+ try {
334
+ const result = await getApi().getCloseFriends();
335
+ output(result, program.opts().json, () => {
336
+ const friends = Array.isArray(result) ? result : [];
337
+ info(`${friends.length} close friend(s)`);
338
+ for (const f of friends) {
339
+ console.log(` ${f.userId || f.uid || "?"} ${f.displayName || f.zaloName || "?"}`);
340
+ }
341
+ });
342
+ } catch (e) {
343
+ error(`Get close friends failed: ${e.message}`);
344
+ }
345
+ });
346
+
347
+ friend
348
+ .command("recommendations")
349
+ .description("Get friend recommendations and received requests")
350
+ .action(async () => {
351
+ try {
352
+ const result = await getApi().getFriendRecommendations();
353
+ output(result, program.opts().json, () => {
354
+ const items = result?.recommItems || [];
355
+ info(`${items.length} recommendation(s)`);
356
+ for (const item of items) {
357
+ const d = item.dataInfo || {};
358
+ const type = d.recommType === 2 ? "[request]" : "[suggest]";
359
+ console.log(` ${d.userId} ${d.displayName || d.zaloName || "?"} ${type}`);
360
+ }
361
+ });
362
+ } catch (e) {
363
+ error(`Get recommendations failed: ${e.message}`);
364
+ }
365
+ });
366
+
367
+ friend
368
+ .command("find-phones <phones...>")
369
+ .description("Find users by phone numbers")
370
+ .action(async (phones) => {
371
+ try {
372
+ const result = await getApi().getMultiUsersByPhones(phones);
373
+ output(result, program.opts().json, () => {
374
+ const entries = Object.entries(result || {});
375
+ info(`${entries.length} user(s) found`);
376
+ for (const [phone, user] of entries) {
377
+ console.log(` ${phone} ${user.uid || "?"} ${user.displayName || user.zaloName || "?"}`);
378
+ }
379
+ });
380
+ } catch (e) {
381
+ error(`Find by phone failed: ${e.message}`);
382
+ }
383
+ });
202
384
  }
@@ -264,4 +264,270 @@ export function registerGroupCommands(program) {
264
264
  error(e.message);
265
265
  }
266
266
  });
267
+
268
+ group
269
+ .command("members-info <userIds...>")
270
+ .description("Get detailed info for group members by user IDs")
271
+ .action(async (userIds) => {
272
+ try {
273
+ const result = await getApi().getGroupMembersInfo(userIds);
274
+ output(result, program.opts().json, () => {
275
+ const profiles = result?.profiles || {};
276
+ const entries = Object.entries(profiles);
277
+ info(`${entries.length} member(s) info`);
278
+ for (const [uid, p] of entries) {
279
+ console.log(` ${uid} ${p.displayName || p.zaloName || "?"}`);
280
+ }
281
+ });
282
+ } catch (e) {
283
+ error(`Get members info failed: ${e.message}`);
284
+ }
285
+ });
286
+
287
+ group
288
+ .command("settings <groupId>")
289
+ .description("Update group settings (flags: --block-name, --sign-admin, --join-appr, etc.)")
290
+ .option("--block-name", "Disallow members to change group name/avatar")
291
+ .option("--no-block-name", "Allow members to change group name/avatar")
292
+ .option("--sign-admin", "Highlight admin messages")
293
+ .option("--no-sign-admin", "Don't highlight admin messages")
294
+ .option("--msg-history", "Allow new members to read recent messages")
295
+ .option("--no-msg-history", "Hide message history from new members")
296
+ .option("--join-appr", "Require membership approval")
297
+ .option("--no-join-appr", "No membership approval required")
298
+ .option("--lock-post", "Disallow members to create notes/reminders")
299
+ .option("--no-lock-post", "Allow members to create notes/reminders")
300
+ .option("--lock-poll", "Disallow members to create polls")
301
+ .option("--no-lock-poll", "Allow members to create polls")
302
+ .option("--lock-msg", "Disallow members to send messages")
303
+ .option("--no-lock-msg", "Allow members to send messages")
304
+ .option("--lock-view-member", "Hide full member list (community only)")
305
+ .option("--no-lock-view-member", "Show full member list")
306
+ .action(async (groupId, opts) => {
307
+ try {
308
+ const settings = {
309
+ blockName: opts.blockName ?? false,
310
+ signAdminMsg: opts.signAdmin ?? false,
311
+ enableMsgHistory: opts.msgHistory ?? false,
312
+ joinAppr: opts.joinAppr ?? false,
313
+ lockCreatePost: opts.lockPost ?? false,
314
+ lockCreatePoll: opts.lockPoll ?? false,
315
+ lockSendMsg: opts.lockMsg ?? false,
316
+ lockViewMember: opts.lockViewMember ?? false,
317
+ };
318
+ const result = await getApi().updateGroupSettings(settings, groupId);
319
+ output(result, program.opts().json, () => success(`Group settings updated for ${groupId}`));
320
+ } catch (e) {
321
+ error(`Update settings failed: ${e.message}`);
322
+ }
323
+ });
324
+
325
+ group
326
+ .command("pending <groupId>")
327
+ .description("List pending group member requests (admin only)")
328
+ .action(async (groupId) => {
329
+ try {
330
+ const result = await getApi().getPendingGroupMembers(groupId);
331
+ output(result, program.opts().json, () => {
332
+ const users = result?.users || [];
333
+ info(`${users.length} pending member(s)`);
334
+ for (const u of users) {
335
+ console.log(` ${u.uid} ${u.dpn || "?"}`);
336
+ }
337
+ });
338
+ } catch (e) {
339
+ error(`Get pending members failed: ${e.message}`);
340
+ }
341
+ });
342
+
343
+ group
344
+ .command("approve <groupId> <userIds...>")
345
+ .description("Approve pending member requests (admin only)")
346
+ .action(async (groupId, userIds) => {
347
+ try {
348
+ const result = await getApi().reviewPendingMemberRequest(
349
+ { members: userIds, isApprove: true },
350
+ groupId,
351
+ );
352
+ output(result, program.opts().json, () => success(`Approved ${userIds.length} member(s)`));
353
+ } catch (e) {
354
+ error(`Approve members failed: ${e.message}`);
355
+ }
356
+ });
357
+
358
+ group
359
+ .command("reject-member <groupId> <userIds...>")
360
+ .description("Reject pending member requests (admin only)")
361
+ .action(async (groupId, userIds) => {
362
+ try {
363
+ const result = await getApi().reviewPendingMemberRequest(
364
+ { members: userIds, isApprove: false },
365
+ groupId,
366
+ );
367
+ output(result, program.opts().json, () => success(`Rejected ${userIds.length} member(s)`));
368
+ } catch (e) {
369
+ error(`Reject members failed: ${e.message}`);
370
+ }
371
+ });
372
+
373
+ group
374
+ .command("enable-link <groupId>")
375
+ .description("Enable and create a new group invite link")
376
+ .action(async (groupId) => {
377
+ try {
378
+ const result = await getApi().enableGroupLink(groupId);
379
+ output(result, program.opts().json, () => {
380
+ success("Group link enabled");
381
+ if (result?.link) info(`Link: ${result.link}`);
382
+ });
383
+ } catch (e) {
384
+ error(`Enable link failed: ${e.message}`);
385
+ }
386
+ });
387
+
388
+ group
389
+ .command("disable-link <groupId>")
390
+ .description("Disable group invite link")
391
+ .action(async (groupId) => {
392
+ try {
393
+ const result = await getApi().disableGroupLink(groupId);
394
+ output(result, program.opts().json, () => success("Group link disabled"));
395
+ } catch (e) {
396
+ error(`Disable link failed: ${e.message}`);
397
+ }
398
+ });
399
+
400
+ group
401
+ .command("link-info <groupId>")
402
+ .description("Get group invite link details")
403
+ .action(async (groupId) => {
404
+ try {
405
+ const result = await getApi().getGroupLinkDetail(groupId);
406
+ output(result, program.opts().json, () => {
407
+ info(`Enabled: ${result?.enabled === 1 ? "yes" : "no"}`);
408
+ if (result?.link) info(`Link: ${result.link}`);
409
+ if (result?.expiration_date) info(`Expires: ${new Date(result.expiration_date).toISOString()}`);
410
+ });
411
+ } catch (e) {
412
+ error(`Get link info failed: ${e.message}`);
413
+ }
414
+ });
415
+
416
+ group
417
+ .command("blocked <groupId>")
418
+ .description("List blocked members in a group")
419
+ .option("-c, --count <n>", "Items per page", parseInt, 50)
420
+ .option("-p, --page <n>", "Page number", parseInt, 1)
421
+ .action(async (groupId, opts) => {
422
+ try {
423
+ const result = await getApi().getGroupBlockedMember({ page: opts.page, count: opts.count }, groupId);
424
+ output(result, program.opts().json, () => {
425
+ const members = result?.blocked_members || [];
426
+ info(`${members.length} blocked member(s)`);
427
+ for (const m of members) {
428
+ console.log(` ${m.id} ${m.dName || m.zaloName || "?"}`);
429
+ }
430
+ if (result?.has_more) info("(more pages available)");
431
+ });
432
+ } catch (e) {
433
+ error(`Get blocked members failed: ${e.message}`);
434
+ }
435
+ });
436
+
437
+ group
438
+ .command("note-create <groupId> <title>")
439
+ .description("Create a note in a group")
440
+ .option("--pin", "Pin the note")
441
+ .action(async (groupId, title, opts) => {
442
+ try {
443
+ const result = await getApi().createNote({ title, pinAct: opts.pin || false }, groupId);
444
+ output(result, program.opts().json, () => success(`Note created in group ${groupId}`));
445
+ } catch (e) {
446
+ error(`Create note failed: ${e.message}`);
447
+ }
448
+ });
449
+
450
+ group
451
+ .command("note-edit <groupId> <noteId> <title>")
452
+ .description("Edit an existing note in a group")
453
+ .option("--pin", "Pin the note")
454
+ .action(async (groupId, noteId, title, opts) => {
455
+ try {
456
+ const result = await getApi().editNote({ title, topicId: noteId, pinAct: opts.pin || false }, groupId);
457
+ output(result, program.opts().json, () => success(`Note ${noteId} updated`));
458
+ } catch (e) {
459
+ error(`Edit note failed: ${e.message}`);
460
+ }
461
+ });
462
+
463
+ group
464
+ .command("invite-boxes")
465
+ .description("List pending group invitations received")
466
+ .action(async () => {
467
+ try {
468
+ const result = await getApi().getGroupInviteBoxList();
469
+ output(result, program.opts().json, () => {
470
+ const invites = result?.invitations || [];
471
+ info(`${invites.length} invitation(s) (total: ${result?.total || 0})`);
472
+ for (const inv of invites) {
473
+ const g = inv.groupInfo || {};
474
+ const inviter = inv.inviterInfo || {};
475
+ console.log(` ${g.groupId || "?"} "${g.name || "?"}" from ${inviter.dName || "?"}`);
476
+ }
477
+ });
478
+ } catch (e) {
479
+ error(`Get invite boxes failed: ${e.message}`);
480
+ }
481
+ });
482
+
483
+ group
484
+ .command("join-invite <groupId>")
485
+ .description("Accept a group invitation from invite box")
486
+ .action(async (groupId) => {
487
+ try {
488
+ const result = await getApi().joinGroupInviteBox(groupId);
489
+ output(result, program.opts().json, () => success(`Joined group ${groupId} via invitation`));
490
+ } catch (e) {
491
+ error(`Join invite failed: ${e.message}`);
492
+ }
493
+ });
494
+
495
+ group
496
+ .command("delete-invite <groupIds...>")
497
+ .description("Delete group invitations from invite box")
498
+ .option("--block", "Block future invites from these groups")
499
+ .action(async (groupIds, opts) => {
500
+ try {
501
+ const result = await getApi().deleteGroupInviteBox(groupIds, opts.block || false);
502
+ output(result, program.opts().json, () =>
503
+ success(`Deleted ${groupIds.length} invitation(s)${opts.block ? " (blocked future)" : ""}`),
504
+ );
505
+ } catch (e) {
506
+ error(`Delete invite failed: ${e.message}`);
507
+ }
508
+ });
509
+
510
+ group
511
+ .command("invite-to <userId> <groupIds...>")
512
+ .description("Invite a user to one or more groups")
513
+ .action(async (userId, groupIds) => {
514
+ try {
515
+ const result = await getApi().inviteUserToGroups(userId, groupIds);
516
+ output(result, program.opts().json, () => success(`Invited ${userId} to ${groupIds.length} group(s)`));
517
+ } catch (e) {
518
+ error(`Invite to groups failed: ${e.message}`);
519
+ }
520
+ });
521
+
522
+ group
523
+ .command("disperse <groupId>")
524
+ .description("Disperse (disband) a group permanently — WARNING: irreversible!")
525
+ .action(async (groupId) => {
526
+ try {
527
+ const result = await getApi().disperseGroup(groupId);
528
+ output(result, program.opts().json, () => success(`Group ${groupId} dispersed`));
529
+ } catch (e) {
530
+ error(`Disperse group failed: ${e.message}`);
531
+ }
532
+ });
267
533
  }
@@ -319,6 +319,20 @@ export function registerMsgCommands(program) {
319
319
  }
320
320
  });
321
321
 
322
+ msg.command("send-voice <threadId> <voiceUrl>")
323
+ .description("Send a voice message from URL")
324
+ .option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
325
+ .option("--ttl <ms>", "Time to live in milliseconds", parseInt, 0)
326
+ .action(async (threadId, voiceUrl, opts) => {
327
+ try {
328
+ info(`Sending voice: ${voiceUrl}`);
329
+ const result = await getApi().sendVoice({ voiceUrl, ttl: opts.ttl }, threadId, Number(opts.type));
330
+ output(result, program.opts().json, () => success(`Voice sent to ${threadId}`));
331
+ } catch (e) {
332
+ error(`Send voice failed: ${e.message}`);
333
+ }
334
+ });
335
+
322
336
  msg.command("send-link <threadId> <url>")
323
337
  .description("Send a link with auto-preview (title, description, thumbnail)")
324
338
  .option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")