@ardennguyen/zalo-agent-cli 2.5.0 → 2.5.3
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 +41 -33
- package/node_modules/zca-js/dist/apis.js +0 -8
- package/node_modules/zca-js/dist/cjs/apis.cjs +0 -8
- package/package.json +3 -3
- package/patches/zca-js+2.2.0.patch +4 -236
- package/src/commands/bank.js +126 -0
- package/src/commands/catalog.js +14 -0
- package/src/commands/conv.js +19 -0
- package/src/commands/friend.js +66 -21
- package/src/commands/group.js +38 -2
- package/src/commands/listen.js +51 -2
- package/src/commands/mcp.js +232 -2
- package/src/commands/msg.js +60 -0
- package/src/commands/profile.js +39 -0
- package/src/commands/sync.js +1 -68
- package/src/core/bank-accounts.js +52 -0
- package/src/core/daemon-channel.js +191 -2
- package/src/core/friend-requests.js +142 -0
- package/src/core/friend-search.js +41 -0
- package/src/core/group-admin.js +116 -0
- package/src/core/group-invite.js +53 -0
- package/src/core/sync.js +4 -235
- package/src/index.js +2 -0
- package/src/mcp/mcp-config.js +1 -0
- package/src/mcp/mcp-tools.js +373 -13
- package/src/utils/cli-args.js +84 -0
- package/src/utils/secrets.js +69 -0
- package/node_modules/zca-js/dist/apis/deleteSnapshotMobileMsg.js +0 -18
- package/node_modules/zca-js/dist/apis/getCrossDB.js +0 -18
- package/node_modules/zca-js/dist/apis/pullMobileMsg.js +0 -22
- package/node_modules/zca-js/dist/apis/requestSyncMessage.js +0 -18
- package/node_modules/zca-js/dist/cjs/apis/deleteSnapshotMobileMsg.cjs +0 -22
- package/node_modules/zca-js/dist/cjs/apis/getCrossDB.cjs +0 -22
- package/node_modules/zca-js/dist/cjs/apis/pullMobileMsg.cjs +0 -26
- package/node_modules/zca-js/dist/cjs/apis/requestSyncMessage.cjs +0 -22
package/src/mcp/mcp-tools.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP tool registrations for Zalo message access, sending, user lookup, and group productivity.
|
|
3
|
-
* Registers
|
|
3
|
+
* Registers 25 tools: zalo_get_messages, zalo_get_history, zalo_send_message, zalo_list_threads,
|
|
4
4
|
* zalo_search_threads, zalo_mark_read, zalo_view_media, zalo_react, zalo_undo, zalo_get_group_members,
|
|
5
5
|
* zalo_list_conversations, zalo_coverage, zalo_sync, zalo_sync_status, zalo_search_messages,
|
|
6
6
|
* zalo_lookup_user, zalo_forward_message, zalo_delete_message, zalo_reminder, zalo_note, zalo_poll,
|
|
7
|
-
* zalo_wait_for_message, zalo_label.
|
|
7
|
+
* zalo_wait_for_message, zalo_label, zalo_friend, zalo_group.
|
|
8
8
|
*
|
|
9
9
|
* A tool that mirrors a CLI command calls the same code the command does (src/core/cached-message.js,
|
|
10
10
|
* forward.js, pin.js, group-members.js, recent-conversations.js, src/utils/my-documents.js, urgency.js),
|
|
@@ -47,6 +47,23 @@ import { expandMentions, ALL_MENTION_UID } from "../utils/mentions.js";
|
|
|
47
47
|
import { resolveSelfThread } from "../utils/my-documents.js";
|
|
48
48
|
import { buildQuote, resolveQuoteSender } from "../utils/quote.js";
|
|
49
49
|
import { urgencyLevel } from "../utils/urgency.js";
|
|
50
|
+
import {
|
|
51
|
+
describeFriendAddError,
|
|
52
|
+
extractErrorCode,
|
|
53
|
+
friendStatusFrom,
|
|
54
|
+
friendsFrom,
|
|
55
|
+
receivedRequestsFrom,
|
|
56
|
+
sentRequestsFrom,
|
|
57
|
+
} from "../core/friend-requests.js";
|
|
58
|
+
import {
|
|
59
|
+
CONFIRMED_ACTIONS,
|
|
60
|
+
NOT_A_MEMBER_CODES,
|
|
61
|
+
REMOVE_NOT_IN_GROUP,
|
|
62
|
+
createdGroupFrom,
|
|
63
|
+
groupConfirmRequired,
|
|
64
|
+
groupInfoFrom,
|
|
65
|
+
memberChangeFrom,
|
|
66
|
+
} from "../core/group-admin.js";
|
|
50
67
|
import { resolveCurrentUserNames, withCurrentSenderNames } from "../core/current-user-names.js";
|
|
51
68
|
|
|
52
69
|
/** Repeat mode codes matching zca-js ReminderRepeatMode enum and src/commands/reminder.js. */
|
|
@@ -460,8 +477,8 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
460
477
|
{
|
|
461
478
|
title: "Send Zalo Message",
|
|
462
479
|
description:
|
|
463
|
-
"Send a text message,
|
|
464
|
-
"(DM or group). threadType: 0=DM(User), 1=Group. " +
|
|
480
|
+
"Send a text message, file/image attachment (`filePath` or `files`), link preview card (`link`), or " +
|
|
481
|
+
"contact card (`cardUserId`) to a Zalo thread (DM or group). threadType: 0=DM(User), 1=Group. " +
|
|
465
482
|
"To @-mention someone in a group, put `@[uid]` in the text where the tag belongs — " +
|
|
466
483
|
"`@[123456]` becomes `@Their Name` and notifies them, and `@[-1]` is @All. " +
|
|
467
484
|
"Use the `senderId` from zalo_get_messages or zalo_get_history as the uid; the display name " +
|
|
@@ -481,8 +498,22 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
481
498
|
.min(1)
|
|
482
499
|
.optional()
|
|
483
500
|
.describe(
|
|
484
|
-
"Message text to send (required unless `
|
|
485
|
-
"`@[uid]` tokens expand to @-mentions; `@[-1]` is @All.",
|
|
501
|
+
"Message text to send (required unless `filePath`, `files`, `link`, or `cardUserId` is set; " +
|
|
502
|
+
"optional caption when `filePath`, `files`, or `link` is set). `@[uid]` tokens expand to @-mentions; `@[-1]` is @All.",
|
|
503
|
+
),
|
|
504
|
+
filePath: z
|
|
505
|
+
.string()
|
|
506
|
+
.min(1)
|
|
507
|
+
.optional()
|
|
508
|
+
.describe(
|
|
509
|
+
"Optional local file or image path to send as an attachment (uses `text` as optional caption)",
|
|
510
|
+
),
|
|
511
|
+
files: z
|
|
512
|
+
.array(z.string().min(1))
|
|
513
|
+
.min(1)
|
|
514
|
+
.optional()
|
|
515
|
+
.describe(
|
|
516
|
+
"Optional list of local file or image paths to send as attachments in one call (uses `text` as optional caption)",
|
|
486
517
|
),
|
|
487
518
|
link: z
|
|
488
519
|
.string()
|
|
@@ -520,17 +551,27 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
520
551
|
),
|
|
521
552
|
})
|
|
522
553
|
.refine(
|
|
523
|
-
(val) =>
|
|
524
|
-
|
|
554
|
+
(val) =>
|
|
555
|
+
Boolean(
|
|
556
|
+
val.text ||
|
|
557
|
+
val.link ||
|
|
558
|
+
val.cardUserId ||
|
|
559
|
+
val.filePath ||
|
|
560
|
+
(Array.isArray(val.files) && val.files.length > 0),
|
|
561
|
+
),
|
|
562
|
+
"Provide at least one of text, filePath, files, link, or cardUserId",
|
|
525
563
|
)
|
|
526
|
-
.refine(
|
|
527
|
-
(val
|
|
528
|
-
|
|
529
|
-
|
|
564
|
+
.refine((val) => {
|
|
565
|
+
const hasFile = Boolean(val.filePath || (Array.isArray(val.files) && val.files.length > 0));
|
|
566
|
+
const modes = [Boolean(val.link), Boolean(val.cardUserId), hasFile].filter(Boolean).length;
|
|
567
|
+
return modes <= 1;
|
|
568
|
+
}, "Cannot combine link, cardUserId, and file attachments in a single call"),
|
|
530
569
|
},
|
|
531
570
|
async ({
|
|
532
571
|
threadId: threadIdIn,
|
|
533
572
|
text,
|
|
573
|
+
filePath,
|
|
574
|
+
files,
|
|
534
575
|
link,
|
|
535
576
|
cardUserId,
|
|
536
577
|
threadType: threadTypeIn,
|
|
@@ -577,8 +618,44 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
577
618
|
});
|
|
578
619
|
}
|
|
579
620
|
|
|
621
|
+
const rawFiles = [...(filePath ? [filePath] : []), ...(Array.isArray(files) ? files : [])];
|
|
622
|
+
if (rawFiles.length > 0) {
|
|
623
|
+
const absPaths = rawFiles.map((p) => resolve(String(p).trim()));
|
|
624
|
+
const missing = absPaths.filter((p) => !existsSync(p));
|
|
625
|
+
if (missing.length > 0) {
|
|
626
|
+
return err(`File not found: ${missing.join(", ")}`);
|
|
627
|
+
}
|
|
628
|
+
const result = await api.sendMessage(
|
|
629
|
+
{ msg: text || "", attachments: absPaths },
|
|
630
|
+
threadId,
|
|
631
|
+
Number(threadType),
|
|
632
|
+
);
|
|
633
|
+
const rawAtt = result?.attachment;
|
|
634
|
+
const attRows = rawAtt ? (Array.isArray(rawAtt) ? rawAtt : [rawAtt]) : [];
|
|
635
|
+
const sent = attRows
|
|
636
|
+
.filter((r) => (r?.msgId ?? null) !== null)
|
|
637
|
+
.map((r) => ({
|
|
638
|
+
msgId: String(r.msgId),
|
|
639
|
+
cliMsgId: (r.cliMsgId ?? null) === null ? null : String(r.cliMsgId),
|
|
640
|
+
}));
|
|
641
|
+
const messageId =
|
|
642
|
+
result?.message?.msgId ?? result?.msgId ?? (sent.length > 0 ? sent[0].msgId : null);
|
|
643
|
+
const cliMsgId =
|
|
644
|
+
result?.message?.cliMsgId ?? result?.cliMsgId ?? (sent.length > 0 ? sent[0].cliMsgId : null);
|
|
645
|
+
return ok({
|
|
646
|
+
success: true,
|
|
647
|
+
kind: "attachment",
|
|
648
|
+
files: absPaths,
|
|
649
|
+
sent,
|
|
650
|
+
messageId,
|
|
651
|
+
...(cliMsgId !== null && { cliMsgId: String(cliMsgId) }),
|
|
652
|
+
threadType,
|
|
653
|
+
...(thread.notice && { threadId, notice: thread.notice }),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
580
657
|
if (!text) {
|
|
581
|
-
return err("Provide text, link, or cardUserId.");
|
|
658
|
+
return err("Provide text, filePath, files, link, or cardUserId.");
|
|
582
659
|
}
|
|
583
660
|
|
|
584
661
|
const warnings = [];
|
|
@@ -2489,4 +2566,287 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
2489
2566
|
}
|
|
2490
2567
|
},
|
|
2491
2568
|
);
|
|
2569
|
+
|
|
2570
|
+
// --- zalo_friend ---
|
|
2571
|
+
server.registerTool(
|
|
2572
|
+
"zalo_friend",
|
|
2573
|
+
{
|
|
2574
|
+
title: "Manage Zalo Friends and Friend Requests",
|
|
2575
|
+
description:
|
|
2576
|
+
"Check, list and answer friend requests, and list friends. Actions: " +
|
|
2577
|
+
"'status' (the relationship with userId: is_friend, and is_requested / is_requesting, Zalo's two flags " +
|
|
2578
|
+
"for a request pending between you and them; to tell which direction a request goes, use the two lists), " +
|
|
2579
|
+
"'sent_requests' (requests you sent that are still pending), " +
|
|
2580
|
+
"'received_requests' (requests others sent you, newest first, with their message), " +
|
|
2581
|
+
"'send' (friend request to userId, with an optional message), " +
|
|
2582
|
+
"'accept' / 'reject' (the request from userId), " +
|
|
2583
|
+
"'undo' (cancel the request you sent to userId), " +
|
|
2584
|
+
"'list' (your friends, most recently active first, capped by limit). " +
|
|
2585
|
+
"userId is the numeric Zalo id (zalo_lookup_user finds one from a phone number). " +
|
|
2586
|
+
"Zalo restricts accounts that send many friend requests, so send sparingly.",
|
|
2587
|
+
inputSchema: z.object({
|
|
2588
|
+
action: z
|
|
2589
|
+
.enum(["status", "sent_requests", "received_requests", "send", "accept", "reject", "undo", "list"])
|
|
2590
|
+
.describe("Friend operation to perform"),
|
|
2591
|
+
userId: z
|
|
2592
|
+
.string()
|
|
2593
|
+
.min(1)
|
|
2594
|
+
.optional()
|
|
2595
|
+
.describe("Numeric Zalo user id (required for status, send, accept, reject and undo)"),
|
|
2596
|
+
message: z
|
|
2597
|
+
.string()
|
|
2598
|
+
.optional()
|
|
2599
|
+
.describe("Message sent with the friend request (only for 'send'; default: none)"),
|
|
2600
|
+
limit: z
|
|
2601
|
+
.number()
|
|
2602
|
+
.int()
|
|
2603
|
+
.min(1)
|
|
2604
|
+
.max(1000)
|
|
2605
|
+
.optional()
|
|
2606
|
+
.describe(
|
|
2607
|
+
"Most friends to return for 'list' (default 200; the reply says how many there are in all)",
|
|
2608
|
+
),
|
|
2609
|
+
}),
|
|
2610
|
+
},
|
|
2611
|
+
async ({ action, userId, message, limit }) => {
|
|
2612
|
+
const needsUser = ["status", "send", "accept", "reject", "undo"].includes(action);
|
|
2613
|
+
const target = userId === undefined ? "" : String(userId).trim();
|
|
2614
|
+
if (needsUser) {
|
|
2615
|
+
if (!target) return err(`userId is required for action='${action}'.`);
|
|
2616
|
+
if (!/^\d{1,30}$/.test(target)) {
|
|
2617
|
+
return err(
|
|
2618
|
+
`userId must be a numeric Zalo id (got "${target}"). zalo_lookup_user finds one from a phone number.`,
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
try {
|
|
2623
|
+
switch (action) {
|
|
2624
|
+
case "status":
|
|
2625
|
+
return ok({ action, ...friendStatusFrom(await api.getFriendRequestStatus(target), target) });
|
|
2626
|
+
case "sent_requests": {
|
|
2627
|
+
let res = null;
|
|
2628
|
+
try {
|
|
2629
|
+
res = await api.getSentFriendRequest();
|
|
2630
|
+
} catch (e) {
|
|
2631
|
+
// Zalo answers code 112 when there are none, which is an empty list, not a failure.
|
|
2632
|
+
if (!/\b112\b/.test(String(e?.message))) throw e;
|
|
2633
|
+
}
|
|
2634
|
+
const requests = sentRequestsFrom(res);
|
|
2635
|
+
return ok({ action, count: requests.length, requests });
|
|
2636
|
+
}
|
|
2637
|
+
case "received_requests": {
|
|
2638
|
+
const requests = receivedRequestsFrom(await api.getFriendRecommendations());
|
|
2639
|
+
return ok({ action, count: requests.length, requests });
|
|
2640
|
+
}
|
|
2641
|
+
case "send":
|
|
2642
|
+
await api.sendFriendRequest(message ?? "", target);
|
|
2643
|
+
return ok({ action, success: true, userId: target });
|
|
2644
|
+
case "accept":
|
|
2645
|
+
await api.acceptFriendRequest(target);
|
|
2646
|
+
return ok({ action, success: true, userId: target });
|
|
2647
|
+
case "reject":
|
|
2648
|
+
await api.rejectFriendRequest(target);
|
|
2649
|
+
return ok({ action, success: true, userId: target });
|
|
2650
|
+
case "undo":
|
|
2651
|
+
await api.undoFriendRequest(target);
|
|
2652
|
+
return ok({ action, success: true, userId: target });
|
|
2653
|
+
case "list": {
|
|
2654
|
+
const { total, friends } = friendsFrom(await api.getAllFriends(), limit ?? 200);
|
|
2655
|
+
return ok({ action, total, count: friends.length, truncated: friends.length < total, friends });
|
|
2656
|
+
}
|
|
2657
|
+
default:
|
|
2658
|
+
return err(`Unsupported friend action: ${action}`);
|
|
2659
|
+
}
|
|
2660
|
+
} catch (e) {
|
|
2661
|
+
console.error("[mcp-tools] zalo_friend error:", e.message);
|
|
2662
|
+
// 'send' keeps the CLI's wording for Zalo's refusal codes (already friends, blocked, ...).
|
|
2663
|
+
const known =
|
|
2664
|
+
action === "send" ? describeFriendAddError(e.code ?? extractErrorCode(e.message), target) : null;
|
|
2665
|
+
return err(known || e.message);
|
|
2666
|
+
}
|
|
2667
|
+
},
|
|
2668
|
+
);
|
|
2669
|
+
|
|
2670
|
+
// --- zalo_group ---
|
|
2671
|
+
server.registerTool(
|
|
2672
|
+
"zalo_group",
|
|
2673
|
+
{
|
|
2674
|
+
title: "Manage Zalo Groups",
|
|
2675
|
+
description:
|
|
2676
|
+
"Leave, inspect, create and change the membership of a group (zalo_get_group_members lists who is " +
|
|
2677
|
+
"in one). Actions: " +
|
|
2678
|
+
"'info' (the group's name, description, creator, admins, member count and limit, creation time and " +
|
|
2679
|
+
"settings), " +
|
|
2680
|
+
"'create' (a new group named `name` with the members in userIds), " +
|
|
2681
|
+
"'add_members' (add userIds to the group), " +
|
|
2682
|
+
"'remove_member' (remove the one member userId from the group), " +
|
|
2683
|
+
"'leave' (leave the group; `silent` hides the usual notice from ordinary members; the owner still sees it). " +
|
|
2684
|
+
"groupId is the group's thread id (zalo_search_threads or zalo_list_conversations finds it); " +
|
|
2685
|
+
"user ids are numeric Zalo ids (zalo_lookup_user finds one from a phone number). " +
|
|
2686
|
+
"'leave' and 'remove_member' cannot be undone from here, so they need confirm: true (a plain " +
|
|
2687
|
+
"parameter, no prompt; a deployment can turn the check off with groupConfirm: false in " +
|
|
2688
|
+
"mcp-config.json or ZALO_MCP_GROUP_CONFIRM=0). Zalo can refuse some members " +
|
|
2689
|
+
"of a create or add_members: the reply lists the members it did and did not take.",
|
|
2690
|
+
inputSchema: z.object({
|
|
2691
|
+
action: z
|
|
2692
|
+
.enum(["leave", "info", "create", "add_members", "remove_member"])
|
|
2693
|
+
.describe("Group operation to perform"),
|
|
2694
|
+
groupId: z
|
|
2695
|
+
.string()
|
|
2696
|
+
.min(1)
|
|
2697
|
+
.optional()
|
|
2698
|
+
.describe("The group's id (required for leave, info, add_members and remove_member)"),
|
|
2699
|
+
name: z.string().optional().describe("The new group's name (required for 'create')"),
|
|
2700
|
+
userIds: z
|
|
2701
|
+
.array(z.string().min(1))
|
|
2702
|
+
.min(1)
|
|
2703
|
+
.max(100)
|
|
2704
|
+
.optional()
|
|
2705
|
+
.describe("Numeric Zalo user ids (required for 'create' and 'add_members')"),
|
|
2706
|
+
userId: z.string().min(1).optional().describe("Numeric Zalo user id (required for 'remove_member')"),
|
|
2707
|
+
silent: z
|
|
2708
|
+
.boolean()
|
|
2709
|
+
.optional()
|
|
2710
|
+
.describe(
|
|
2711
|
+
"Only for 'leave': hide the usual notice from ordinary members; the owner still sees it (default false)",
|
|
2712
|
+
),
|
|
2713
|
+
confirm: z
|
|
2714
|
+
.boolean()
|
|
2715
|
+
.optional()
|
|
2716
|
+
.describe(
|
|
2717
|
+
"Must be true for 'leave' and 'remove_member', which cannot be undone from here " +
|
|
2718
|
+
"(ignored when the deployment has switched the check off)",
|
|
2719
|
+
),
|
|
2720
|
+
}),
|
|
2721
|
+
},
|
|
2722
|
+
async ({ action, groupId, name, userIds, userId, silent, confirm }) => {
|
|
2723
|
+
const gid = groupId === undefined ? "" : String(groupId).trim();
|
|
2724
|
+
if (["leave", "info", "add_members", "remove_member"].includes(action)) {
|
|
2725
|
+
if (!gid) return err(`groupId is required for action='${action}'.`);
|
|
2726
|
+
if (!/^\d{1,30}$/.test(gid)) {
|
|
2727
|
+
return err(
|
|
2728
|
+
`groupId must be a numeric Zalo group id (got "${gid}"). zalo_search_threads finds one by name.`,
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
const badId = (id) => !/^\d{1,30}$/.test(id);
|
|
2733
|
+
let members = [];
|
|
2734
|
+
if (action === "create" || action === "add_members") {
|
|
2735
|
+
members = [...new Set((userIds ?? []).map((u) => String(u).trim()))];
|
|
2736
|
+
if (members.length === 0) return err(`userIds is required for action='${action}'.`);
|
|
2737
|
+
const bad = members.find(badId);
|
|
2738
|
+
if (bad !== undefined) {
|
|
2739
|
+
return err(
|
|
2740
|
+
`userIds must be numeric Zalo ids (got "${bad}"). zalo_lookup_user finds one from a phone number.`,
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
const target = userId === undefined ? "" : String(userId).trim();
|
|
2745
|
+
if (action === "remove_member") {
|
|
2746
|
+
if (!target) return err("userId is required for action='remove_member'.");
|
|
2747
|
+
if (badId(target)) {
|
|
2748
|
+
return err(
|
|
2749
|
+
`userId must be a numeric Zalo id (got "${target}"). zalo_get_group_members lists a group's members.`,
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
const groupName = name === undefined ? "" : String(name).trim();
|
|
2754
|
+
if (action === "create" && !groupName) return err("name is required for action='create'.");
|
|
2755
|
+
|
|
2756
|
+
// After validation, before Zalo: a refused call changes nothing and says exactly how to retry.
|
|
2757
|
+
if (CONFIRMED_ACTIONS.includes(action) && confirm !== true && groupConfirmRequired({ config })) {
|
|
2758
|
+
const what = action === "leave" ? `leave group ${gid}` : `remove ${target} from group ${gid}`;
|
|
2759
|
+
return err(
|
|
2760
|
+
`Not done: this would ${what}, which cannot be undone from here. Repeat the call with ` +
|
|
2761
|
+
"confirm: true to proceed. (An automatic or headless deployment can turn this check off " +
|
|
2762
|
+
"with groupConfirm: false in mcp-config.json, or ZALO_MCP_GROUP_CONFIRM=0 in the environment.)",
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
try {
|
|
2767
|
+
switch (action) {
|
|
2768
|
+
case "info": {
|
|
2769
|
+
const info = groupInfoFrom(await api.getGroupInfo([gid]), gid);
|
|
2770
|
+
if (!info) {
|
|
2771
|
+
return err(
|
|
2772
|
+
`Zalo's answer does not include group ${gid}: this account is not in it, or it is not ` +
|
|
2773
|
+
"a group id (a 1-1 conversation's id is the other person's uid).",
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
return ok({ action, ...info });
|
|
2777
|
+
}
|
|
2778
|
+
case "create": {
|
|
2779
|
+
const made = createdGroupFrom(await api.createGroup({ name: groupName, members }), members);
|
|
2780
|
+
if (!made.groupId)
|
|
2781
|
+
return err("Zalo did not return a group id; the group may not have been created.");
|
|
2782
|
+
return ok({
|
|
2783
|
+
action,
|
|
2784
|
+
groupId: made.groupId,
|
|
2785
|
+
name: groupName,
|
|
2786
|
+
added: made.added,
|
|
2787
|
+
failed: made.failed,
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
case "add_members": {
|
|
2791
|
+
const change = memberChangeFrom(await api.addUserToGroup(members, gid), members);
|
|
2792
|
+
if (change.done.length === 0) {
|
|
2793
|
+
const why = change.errorData
|
|
2794
|
+
? ` (Zalo's error codes: ${JSON.stringify(change.errorData)})`
|
|
2795
|
+
: "";
|
|
2796
|
+
return err(
|
|
2797
|
+
`Zalo did not add any of the ${members.length} member(s) to group ${gid}${why}.`,
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
return ok({
|
|
2801
|
+
action,
|
|
2802
|
+
groupId: gid,
|
|
2803
|
+
added: change.done,
|
|
2804
|
+
failed: change.failed,
|
|
2805
|
+
...(change.errorData && { errorData: change.errorData }),
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
case "remove_member": {
|
|
2809
|
+
const change = memberChangeFrom(await api.removeUserFromGroup([target], gid), [target]);
|
|
2810
|
+
if (change.failed.length > 0) {
|
|
2811
|
+
return err(
|
|
2812
|
+
`Zalo did not remove ${target} from group ${gid}: not a member, or this account is not ` +
|
|
2813
|
+
"allowed to remove members (a group's owner or an admin can).",
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
return ok({ action, success: true, groupId: gid, userId: target });
|
|
2817
|
+
}
|
|
2818
|
+
case "leave": {
|
|
2819
|
+
const quiet = silent === true;
|
|
2820
|
+
const res = await api.leaveGroup(gid, quiet);
|
|
2821
|
+
const memberError = Array.isArray(res?.memberError) ? res.memberError : [];
|
|
2822
|
+
return ok({
|
|
2823
|
+
action,
|
|
2824
|
+
success: true,
|
|
2825
|
+
groupId: gid,
|
|
2826
|
+
silent: quiet,
|
|
2827
|
+
...(memberError.length > 0 && { memberError }),
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
default:
|
|
2831
|
+
return err(`Unsupported group action: ${action}`);
|
|
2832
|
+
}
|
|
2833
|
+
} catch (e) {
|
|
2834
|
+
console.error("[mcp-tools] zalo_group error:", e.message);
|
|
2835
|
+
if (action === "leave" && NOT_A_MEMBER_CODES.includes(Number(e.code ?? extractErrorCode(e.message)))) {
|
|
2836
|
+
return err(
|
|
2837
|
+
`This account is not in group ${gid} (already left, or never joined), so there is nothing to leave.`,
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
if (
|
|
2841
|
+
action === "remove_member" &&
|
|
2842
|
+
Number(e.code ?? extractErrorCode(e.message)) === REMOVE_NOT_IN_GROUP
|
|
2843
|
+
) {
|
|
2844
|
+
return err(
|
|
2845
|
+
`Zalo says ${target} is not in group ${gid} ("${e.message}", code ${REMOVE_NOT_IN_GROUP}).`,
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
return err(e.message);
|
|
2849
|
+
}
|
|
2850
|
+
},
|
|
2851
|
+
);
|
|
2492
2852
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small argument parsers for the commands that wrap a zca-js call taking an
|
|
3
|
+
* enum or a boolean: typing indicator, report, active status, language.
|
|
4
|
+
*
|
|
5
|
+
* Each throws an Error whose message says what was accepted, so the command's
|
|
6
|
+
* catch block can print it as the usage error. None touches Zalo.
|
|
7
|
+
*/
|
|
8
|
+
import { ReportReason, UpdateLangAvailableLanguages } from "zca-js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `--type <n>`: 0 = user, 1 = group.
|
|
12
|
+
*
|
|
13
|
+
* @param {string|number} value
|
|
14
|
+
* @returns {0|1}
|
|
15
|
+
* @throws {Error} for anything else
|
|
16
|
+
*/
|
|
17
|
+
export function parseThreadTypeArg(value) {
|
|
18
|
+
const s = String(value).trim();
|
|
19
|
+
if (s === "0") return 0;
|
|
20
|
+
if (s === "1") return 1;
|
|
21
|
+
throw new Error(`--type must be 0 (user) or 1 (group), got "${value}"`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `on` / `off` (also true/false, 1/0, yes/no, enable/disable).
|
|
26
|
+
*
|
|
27
|
+
* @param {string} value
|
|
28
|
+
* @param {string} [what] - the setting's name, for the error message
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
* @throws {Error} for anything else
|
|
31
|
+
*/
|
|
32
|
+
export function parseOnOff(value, what = "value") {
|
|
33
|
+
const s = String(value).trim().toLowerCase();
|
|
34
|
+
if (["on", "true", "1", "yes", "enable", "enabled"].includes(s)) return true;
|
|
35
|
+
if (["off", "false", "0", "no", "disable", "disabled"].includes(s)) return false;
|
|
36
|
+
throw new Error(`${what} must be "on" or "off", got "${value}"`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `vi` / `en`, as zca-js's `updateLang` takes them (it sends `VI` / `EN`).
|
|
41
|
+
*
|
|
42
|
+
* @param {string} value
|
|
43
|
+
* @returns {string} an UpdateLangAvailableLanguages value
|
|
44
|
+
* @throws {Error} for any other language
|
|
45
|
+
*/
|
|
46
|
+
export function parseLanguage(value) {
|
|
47
|
+
const key = String(value).trim().toUpperCase();
|
|
48
|
+
if (key in UpdateLangAvailableLanguages) return UpdateLangAvailableLanguages[key];
|
|
49
|
+
throw new Error(`language must be one of: ${Object.keys(UpdateLangAvailableLanguages).join(", ").toLowerCase()}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The reasons `msg report` accepts, by name. */
|
|
53
|
+
export const REPORT_REASONS = Object.freeze({
|
|
54
|
+
sensitive: ReportReason.Sensitive,
|
|
55
|
+
annoy: ReportReason.Annoy,
|
|
56
|
+
fraud: ReportReason.Fraud,
|
|
57
|
+
other: ReportReason.Other,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The options object zca-js's `sendReport` takes.
|
|
62
|
+
*
|
|
63
|
+
* `other` is the only reason that carries text, and zca-js sends it only for
|
|
64
|
+
* that reason, so a text given with another reason would be dropped without a
|
|
65
|
+
* word: refuse it instead of pretending it was reported.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} reason - sensitive | annoy | fraud | other
|
|
68
|
+
* @param {string} [content] - required with `other`, refused otherwise
|
|
69
|
+
* @returns {{reason: number, content?: string}}
|
|
70
|
+
* @throws {Error} for an unknown reason, `other` without text, or text with any other reason
|
|
71
|
+
*/
|
|
72
|
+
export function parseReportReason(reason, content) {
|
|
73
|
+
const key = String(reason).trim().toLowerCase();
|
|
74
|
+
if (!(key in REPORT_REASONS)) {
|
|
75
|
+
throw new Error(`--reason must be one of: ${Object.keys(REPORT_REASONS).join(", ")}, got "${reason}"`);
|
|
76
|
+
}
|
|
77
|
+
const text = content === undefined ? "" : String(content).trim();
|
|
78
|
+
if (key === "other") {
|
|
79
|
+
if (!text) throw new Error('--reason other needs the details: add --content "<what happened>"');
|
|
80
|
+
return { reason: REPORT_REASONS.other, content: text };
|
|
81
|
+
}
|
|
82
|
+
if (text) throw new Error(`--content is only sent with --reason other; "${key}" carries no text`);
|
|
83
|
+
return { reason: REPORT_REASONS[key] };
|
|
84
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets that arrive as command-line flags: masking them for logs, and
|
|
3
|
+
* resolving the webhook signing secret from its three sources.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Flags whose value is a secret and must never be written to a log. */
|
|
7
|
+
export const SECRET_FLAGS = Object.freeze(["--auth", "--client-secret", "--webhook-secret"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A copy of an argument list with every secret flag's value replaced by `***`.
|
|
11
|
+
*
|
|
12
|
+
* Handles both `--auth value` and `--auth=value`. The launcher for `-d` writes
|
|
13
|
+
* its argument list into `daemon.log` ("starting daemon: ..."), and a bearer
|
|
14
|
+
* token or signing secret has no business in a log that gets shipped or
|
|
15
|
+
* attached to an issue.
|
|
16
|
+
*
|
|
17
|
+
* @param {string[]} args
|
|
18
|
+
* @returns {string[]} a new array; the input is not modified
|
|
19
|
+
*/
|
|
20
|
+
export function maskSecretArgs(args) {
|
|
21
|
+
const out = [];
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
const a = String(args[i]);
|
|
24
|
+
const eq = a.indexOf("=");
|
|
25
|
+
if (eq > 0 && SECRET_FLAGS.includes(a.slice(0, eq))) {
|
|
26
|
+
out.push(`${a.slice(0, eq)}=***`);
|
|
27
|
+
} else if (SECRET_FLAGS.includes(a) && i + 1 < args.length) {
|
|
28
|
+
out.push(a, "***");
|
|
29
|
+
i++;
|
|
30
|
+
} else {
|
|
31
|
+
out.push(a);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The webhook signing secret: the `--webhook-secret` flag, else the
|
|
39
|
+
* `ZALO_WEBHOOK_SECRET[_<ownId>]` environment variable, else the MCP config's
|
|
40
|
+
* `webhookSecret`. Null when none is set, which means unsigned webhooks.
|
|
41
|
+
*
|
|
42
|
+
* A flag that is present but empty throws instead of falling through: an
|
|
43
|
+
* unset shell variable (`--webhook-secret "$SECRET"`) is how an empty value
|
|
44
|
+
* arrives, and falling back to "no secret" would silently send unsigned
|
|
45
|
+
* webhooks to a receiver that believes it is verifying signatures.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} sources
|
|
48
|
+
* @param {string|undefined} [sources.flag] - the parsed `--webhook-secret` value
|
|
49
|
+
* @param {string|null|undefined} [sources.env] - the environment variable's value
|
|
50
|
+
* @param {string|null|undefined} [sources.config] - `webhookSecret` from mcp-config.json
|
|
51
|
+
* @returns {string|null}
|
|
52
|
+
* @throws {Error} when the flag is given empty
|
|
53
|
+
*/
|
|
54
|
+
export function resolveWebhookSecret({ flag, env, config } = {}) {
|
|
55
|
+
if (flag !== undefined) {
|
|
56
|
+
const s = String(flag).trim();
|
|
57
|
+
if (!s) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"--webhook-secret was given an empty secret. Refusing to start: webhooks would go out unsigned " +
|
|
60
|
+
"while the receiver expects signatures.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return s;
|
|
64
|
+
}
|
|
65
|
+
for (const v of [env, config]) {
|
|
66
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { ZaloApiError } from "../Errors/ZaloApiError.js";
|
|
2
|
-
import { apiFactory } from "../utils.js";
|
|
3
|
-
|
|
4
|
-
export const deleteSnapshotMobileMsgFactory = apiFactory()((api, ctx, utils) => {
|
|
5
|
-
const serviceURL = utils.makeURL(`${api.zpwServiceMap.file[0]}/api/message/delete_snapshot_mobile_msg`);
|
|
6
|
-
return async function deleteSnapshotMobileMsg(publicKey) {
|
|
7
|
-
const params = {
|
|
8
|
-
public_key: publicKey,
|
|
9
|
-
imei: ctx.imei
|
|
10
|
-
};
|
|
11
|
-
const encryptedParams = utils.encodeAES(JSON.stringify(params));
|
|
12
|
-
if (!encryptedParams) throw new ZaloApiError("Failed to encrypt message");
|
|
13
|
-
const response = await utils.request(`${serviceURL}¶ms=${encodeURIComponent(encryptedParams)}`, {
|
|
14
|
-
method: "GET"
|
|
15
|
-
});
|
|
16
|
-
return utils.resolve(response);
|
|
17
|
-
};
|
|
18
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { ZaloApiError } from "../Errors/ZaloApiError.js";
|
|
2
|
-
import { apiFactory } from "../utils.js";
|
|
3
|
-
|
|
4
|
-
export const getCrossDBFactory = apiFactory()((api, _, utils) => {
|
|
5
|
-
const serviceURL = utils.makeURL(`${api.zpwServiceMap.file[0]}/api/message/get_crossdb`);
|
|
6
|
-
return async function getCrossDB(syncSession) {
|
|
7
|
-
const params = {
|
|
8
|
-
pc_name: "Web",
|
|
9
|
-
sync_session: syncSession
|
|
10
|
-
};
|
|
11
|
-
const encryptedParams = utils.encodeAES(JSON.stringify(params));
|
|
12
|
-
if (!encryptedParams) throw new ZaloApiError("Failed to encrypt message");
|
|
13
|
-
const response = await utils.request(`${serviceURL}¶ms=${encodeURIComponent(encryptedParams)}`, {
|
|
14
|
-
method: "GET"
|
|
15
|
-
});
|
|
16
|
-
return utils.resolve(response);
|
|
17
|
-
};
|
|
18
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { ZaloApiError } from "../Errors/ZaloApiError.js";
|
|
2
|
-
import { apiFactory } from "../utils.js";
|
|
3
|
-
|
|
4
|
-
export const pullMobileMsgFactory = apiFactory()((api, _, utils) => {
|
|
5
|
-
const serviceURL = utils.makeURL(`${api.zpwServiceMap.file[0]}/api/message/pull_mobile_msg`);
|
|
6
|
-
return async function pullMobileMsg(publicKey, fromSeqId = 0, isRetry = 0, tempKey = "") {
|
|
7
|
-
const params = {
|
|
8
|
-
pc_name: "Web",
|
|
9
|
-
public_key: publicKey,
|
|
10
|
-
from_seq_id: fromSeqId,
|
|
11
|
-
is_retry: isRetry,
|
|
12
|
-
min_seq_id: 0,
|
|
13
|
-
temp_key: tempKey
|
|
14
|
-
};
|
|
15
|
-
const encryptedParams = utils.encodeAES(JSON.stringify(params));
|
|
16
|
-
if (!encryptedParams) throw new ZaloApiError("Failed to encrypt message");
|
|
17
|
-
const response = await utils.request(`${serviceURL}¶ms=${encodeURIComponent(encryptedParams)}`, {
|
|
18
|
-
method: "GET"
|
|
19
|
-
});
|
|
20
|
-
return utils.resolve(response);
|
|
21
|
-
};
|
|
22
|
-
});
|