@ardennguyen/zalo-agent-cli 1.0.7
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/DISCLAIMER.md +27 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/package.json +64 -0
- package/src/cli.test.js +98 -0
- package/src/commands/account.js +178 -0
- package/src/commands/auto-reply.js +83 -0
- package/src/commands/catalog.js +143 -0
- package/src/commands/conv.js +264 -0
- package/src/commands/friend.js +384 -0
- package/src/commands/group.js +606 -0
- package/src/commands/label.js +39 -0
- package/src/commands/listen.js +293 -0
- package/src/commands/login.js +165 -0
- package/src/commands/mcp.js +219 -0
- package/src/commands/msg.js +608 -0
- package/src/commands/oa-init.js +503 -0
- package/src/commands/oa-listen.js +215 -0
- package/src/commands/oa.js +657 -0
- package/src/commands/poll.js +159 -0
- package/src/commands/profile.js +274 -0
- package/src/commands/quick-msg.js +55 -0
- package/src/commands/reminder.js +195 -0
- package/src/core/accounts.js +93 -0
- package/src/core/credentials.js +63 -0
- package/src/core/oa-client.js +407 -0
- package/src/core/zalo-client.js +201 -0
- package/src/index.js +95 -0
- package/src/mcp/mcp-config.js +81 -0
- package/src/mcp/mcp-http-transport.js +84 -0
- package/src/mcp/mcp-server.js +40 -0
- package/src/mcp/mcp-tools.js +353 -0
- package/src/mcp/media-downloader.js +191 -0
- package/src/mcp/media-downloader.test.js +45 -0
- package/src/mcp/message-buffer.js +127 -0
- package/src/mcp/message-buffer.test.js +308 -0
- package/src/mcp/notifier.js +141 -0
- package/src/mcp/notifier.test.js +269 -0
- package/src/mcp/thread-filter.js +79 -0
- package/src/mcp/thread-filter.test.js +206 -0
- package/src/mcp/thread-name-cache.js +162 -0
- package/src/mcp/thread-name-cache.test.js +139 -0
- package/src/utils/bank-helpers.js +233 -0
- package/src/utils/bank-helpers.test.js +66 -0
- package/src/utils/extract-message-text.js +21 -0
- package/src/utils/output.js +21 -0
- package/src/utils/proxy-helpers.js +14 -0
- package/src/utils/proxy-helpers.test.js +36 -0
- package/src/utils/qr-display.js +91 -0
- package/src/utils/qr-display.test.js +97 -0
- package/src/utils/qr-http-server.js +190 -0
- package/src/utils/update-check.js +56 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll commands — create, view, vote, add options, lock, and share polls in groups.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerPollCommands(program) {
|
|
9
|
+
const poll = program.command("poll").description("Create and manage polls in groups");
|
|
10
|
+
|
|
11
|
+
poll.command("create <groupId> <question> <options...>")
|
|
12
|
+
.description("Create a poll in a group (options separated by spaces, quote multi-word options)")
|
|
13
|
+
.option("--multi", "Allow multiple choices")
|
|
14
|
+
.option("--add-options", "Allow members to add new options")
|
|
15
|
+
.option("--anonymous", "Hide voter identities")
|
|
16
|
+
.option("--hide-preview", "Hide results until voted")
|
|
17
|
+
.option("--expire <minutes>", "Auto-close after N minutes", parseInt)
|
|
18
|
+
.action(async (groupId, question, options, opts) => {
|
|
19
|
+
try {
|
|
20
|
+
if (options.length < 2) {
|
|
21
|
+
error("A poll requires at least 2 options.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const expiredTime = opts.expire ? opts.expire * 60 * 1000 : 0;
|
|
25
|
+
const result = await getApi().createPoll(
|
|
26
|
+
{
|
|
27
|
+
question,
|
|
28
|
+
options,
|
|
29
|
+
allowMultiChoices: !!opts.multi,
|
|
30
|
+
allowAddNewOption: !!opts.addOptions,
|
|
31
|
+
isAnonymous: !!opts.anonymous,
|
|
32
|
+
hideVotePreview: !!opts.hidePreview,
|
|
33
|
+
expiredTime,
|
|
34
|
+
},
|
|
35
|
+
groupId,
|
|
36
|
+
);
|
|
37
|
+
output(result, program.opts().json, () => {
|
|
38
|
+
success(`Poll created: "${question}"`);
|
|
39
|
+
info(`Poll ID: ${result.poll_id}`);
|
|
40
|
+
if (result.options) {
|
|
41
|
+
result.options.forEach((o) => console.log(` [${o.option_id}] ${o.content}`));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
} catch (e) {
|
|
45
|
+
error(`Create poll failed: ${e.message}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
poll.command("info <pollId>")
|
|
50
|
+
.description("View poll details and results")
|
|
51
|
+
.action(async (pollId) => {
|
|
52
|
+
try {
|
|
53
|
+
const result = await getApi().getPollDetail(Number(pollId));
|
|
54
|
+
output(result, program.opts().json, () => {
|
|
55
|
+
info(`Question: ${result.question}`);
|
|
56
|
+
info(`Poll ID: ${result.poll_id}`);
|
|
57
|
+
info(`Status: ${result.closed ? "Closed" : "Open"}`);
|
|
58
|
+
info(`Total votes: ${result.num_vote}`);
|
|
59
|
+
info(`Multi-choice: ${result.allow_multi_choices ? "Yes" : "No"}`);
|
|
60
|
+
info(`Anonymous: ${result.is_anonymous ? "Yes" : "No"}`);
|
|
61
|
+
if (result.expired_time > 0) {
|
|
62
|
+
const expDate = new Date(result.expired_time);
|
|
63
|
+
info(`Expires: ${expDate.toLocaleString()}`);
|
|
64
|
+
}
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(" ID VOTES OPTION");
|
|
67
|
+
console.log(" " + "-".repeat(50));
|
|
68
|
+
for (const o of result.options || []) {
|
|
69
|
+
const voted = o.voted ? " ✓" : "";
|
|
70
|
+
console.log(
|
|
71
|
+
` ${String(o.option_id).padEnd(6)} ${String(o.votes).padEnd(6)} ${o.content}${voted}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
} catch (e) {
|
|
76
|
+
error(`Get poll failed: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
poll.command("vote <pollId> <optionIds...>")
|
|
81
|
+
.description("Vote on a poll (use option IDs from 'poll info')")
|
|
82
|
+
.action(async (pollId, optionIds) => {
|
|
83
|
+
try {
|
|
84
|
+
const ids = optionIds.map(Number);
|
|
85
|
+
const result = await getApi().votePoll(Number(pollId), ids);
|
|
86
|
+
output(result, program.opts().json, () => {
|
|
87
|
+
success(`Voted on poll ${pollId}`);
|
|
88
|
+
if (result.options) {
|
|
89
|
+
for (const o of result.options) {
|
|
90
|
+
const voted = o.voted ? " ✓" : "";
|
|
91
|
+
console.log(` [${o.option_id}] ${o.content}: ${o.votes} vote(s)${voted}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
} catch (e) {
|
|
96
|
+
error(`Vote failed: ${e.message}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
poll.command("unvote <pollId>")
|
|
101
|
+
.description("Remove your vote from a poll")
|
|
102
|
+
.action(async (pollId) => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await getApi().votePoll(Number(pollId), []);
|
|
105
|
+
output(result, program.opts().json, () => success(`Removed vote from poll ${pollId}`));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
error(`Unvote failed: ${e.message}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
poll.command("add-option <pollId> <options...>")
|
|
112
|
+
.description("Add new options to an existing poll")
|
|
113
|
+
.option("--vote", "Also vote for the new options")
|
|
114
|
+
.action(async (pollId, options, opts) => {
|
|
115
|
+
try {
|
|
116
|
+
const newOptions = options.map((content) => ({
|
|
117
|
+
content,
|
|
118
|
+
voted: !!opts.vote,
|
|
119
|
+
}));
|
|
120
|
+
const result = await getApi().addPollOptions({
|
|
121
|
+
pollId: Number(pollId),
|
|
122
|
+
options: newOptions,
|
|
123
|
+
votedOptionIds: [],
|
|
124
|
+
});
|
|
125
|
+
output(result, program.opts().json, () => {
|
|
126
|
+
success(`Added ${options.length} option(s) to poll ${pollId}`);
|
|
127
|
+
if (result.options) {
|
|
128
|
+
for (const o of result.options) {
|
|
129
|
+
console.log(` [${o.option_id}] ${o.content}: ${o.votes} vote(s)`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
} catch (e) {
|
|
134
|
+
error(`Add option failed: ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
poll.command("lock <pollId>")
|
|
139
|
+
.description("Lock/close a poll (no more votes)")
|
|
140
|
+
.action(async (pollId) => {
|
|
141
|
+
try {
|
|
142
|
+
const result = await getApi().lockPoll(Number(pollId));
|
|
143
|
+
output(result, program.opts().json, () => success(`Poll ${pollId} locked`));
|
|
144
|
+
} catch (e) {
|
|
145
|
+
error(`Lock poll failed: ${e.message}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
poll.command("share <pollId>")
|
|
150
|
+
.description("Share a poll")
|
|
151
|
+
.action(async (pollId) => {
|
|
152
|
+
try {
|
|
153
|
+
const result = await getApi().sharePoll(Number(pollId));
|
|
154
|
+
output(result, program.opts().json, () => success(`Poll ${pollId} shared`));
|
|
155
|
+
} catch (e) {
|
|
156
|
+
error(`Share poll failed: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile commands — view/update profile, avatar, bio, and privacy settings.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import { getApi } from "../core/zalo-client.js";
|
|
7
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
8
|
+
|
|
9
|
+
/** Valid privacy setting keys and their human-readable labels + value descriptions. */
|
|
10
|
+
const SETTING_MAP = {
|
|
11
|
+
"online-status": {
|
|
12
|
+
key: "show_online_status",
|
|
13
|
+
label: "Show online status",
|
|
14
|
+
values: { 0: "hidden", 1: "visible" },
|
|
15
|
+
},
|
|
16
|
+
"seen-status": {
|
|
17
|
+
key: "display_seen_status",
|
|
18
|
+
label: "Display seen status",
|
|
19
|
+
values: { 0: "hidden", 1: "visible" },
|
|
20
|
+
},
|
|
21
|
+
birthday: {
|
|
22
|
+
key: "view_birthday",
|
|
23
|
+
label: "Birthday visibility",
|
|
24
|
+
values: { 0: "hidden", 1: "full (day/month/year)", 2: "partial (day/month)" },
|
|
25
|
+
},
|
|
26
|
+
"receive-msg": {
|
|
27
|
+
key: "receive_message",
|
|
28
|
+
label: "Receive messages from",
|
|
29
|
+
values: { 1: "everyone", 2: "friends only" },
|
|
30
|
+
},
|
|
31
|
+
"accept-call": {
|
|
32
|
+
key: "accept_stranger_call",
|
|
33
|
+
label: "Accept calls from",
|
|
34
|
+
values: { 2: "friends only", 3: "everyone", 4: "friends + contacted" },
|
|
35
|
+
},
|
|
36
|
+
"add-by-phone": {
|
|
37
|
+
key: "add_friend_via_phone",
|
|
38
|
+
label: "Find by phone number",
|
|
39
|
+
values: { 0: "disabled", 1: "enabled" },
|
|
40
|
+
},
|
|
41
|
+
"add-by-qr": {
|
|
42
|
+
key: "add_friend_via_qr",
|
|
43
|
+
label: "Find by QR code",
|
|
44
|
+
values: { 0: "disabled", 1: "enabled" },
|
|
45
|
+
},
|
|
46
|
+
"add-by-group": {
|
|
47
|
+
key: "add_friend_via_group",
|
|
48
|
+
label: "Find via group",
|
|
49
|
+
values: { 0: "disabled", 1: "enabled" },
|
|
50
|
+
},
|
|
51
|
+
recommend: {
|
|
52
|
+
key: "display_on_recommend_friend",
|
|
53
|
+
label: "Show in friend recommendations",
|
|
54
|
+
values: { 0: "disabled", 1: "enabled" },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function registerProfileCommands(program) {
|
|
59
|
+
const profile = program.command("profile").description("View and update your Zalo profile and settings");
|
|
60
|
+
|
|
61
|
+
profile
|
|
62
|
+
.command("me")
|
|
63
|
+
.description("Show current account profile")
|
|
64
|
+
.action(async () => {
|
|
65
|
+
try {
|
|
66
|
+
const result = await getApi().fetchAccountInfo();
|
|
67
|
+
const p = result?.profile || {};
|
|
68
|
+
output(result, program.opts().json, () => {
|
|
69
|
+
info(`Name: ${p.displayName || p.zaloName || "?"}`);
|
|
70
|
+
info(`User ID: ${p.userId || "?"}`);
|
|
71
|
+
info(`Phone: ${p.phoneNumber || "?"}`);
|
|
72
|
+
info(`Gender: ${p.gender === 0 ? "Male" : p.gender === 1 ? "Female" : "?"}`);
|
|
73
|
+
info(`DOB: ${p.sdob || "?"}`);
|
|
74
|
+
info(`Status: ${p.status || "(none)"}`);
|
|
75
|
+
info(`Avatar: ${p.avatar || "?"}`);
|
|
76
|
+
});
|
|
77
|
+
} catch (e) {
|
|
78
|
+
error(e.message);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
profile
|
|
83
|
+
.command("avatar <imagePath>")
|
|
84
|
+
.description("Change your profile avatar")
|
|
85
|
+
.action(async (imagePath) => {
|
|
86
|
+
try {
|
|
87
|
+
const absPath = resolve(imagePath);
|
|
88
|
+
const result = await getApi().changeAccountAvatar(absPath);
|
|
89
|
+
output(result, program.opts().json, () => success("Avatar updated"));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
error(`Avatar update failed: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
profile
|
|
96
|
+
.command("bio [text]")
|
|
97
|
+
.description("View or update your profile bio/status")
|
|
98
|
+
.action(async (text) => {
|
|
99
|
+
try {
|
|
100
|
+
if (text === undefined) {
|
|
101
|
+
const result = await getApi().fetchAccountInfo();
|
|
102
|
+
const bio = result?.profile?.status || "(empty)";
|
|
103
|
+
output({ bio }, program.opts().json, () => info(`Bio: ${bio}`));
|
|
104
|
+
} else {
|
|
105
|
+
await getApi().updateProfileBio(text);
|
|
106
|
+
// Verify by reading back (Zalo may cache, so check)
|
|
107
|
+
const verify = await getApi().fetchAccountInfo();
|
|
108
|
+
const newBio = verify?.profile?.status || "";
|
|
109
|
+
if (newBio === text) {
|
|
110
|
+
output({ bio: newBio }, program.opts().json, () => success(`Bio updated to: "${text}"`));
|
|
111
|
+
} else {
|
|
112
|
+
output({ bio: newBio, requested: text }, program.opts().json, () => {
|
|
113
|
+
success(`Bio update request sent: "${text}"`);
|
|
114
|
+
info(
|
|
115
|
+
"Note: Zalo may take time to reflect changes, or bio may not be supported for your account type.",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
error(`Bio update failed: ${e.message}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
profile
|
|
126
|
+
.command("update")
|
|
127
|
+
.description("Update profile name, birthday, or gender")
|
|
128
|
+
.option("-n, --name <name>", "Display name")
|
|
129
|
+
.option("-d, --dob <YYYY-MM-DD>", "Date of birth")
|
|
130
|
+
.option("-g, --gender <0|1>", "Gender: 0=Male, 1=Female")
|
|
131
|
+
.action(async (opts) => {
|
|
132
|
+
try {
|
|
133
|
+
if (!opts.name && !opts.dob && !opts.gender) {
|
|
134
|
+
error("Provide at least one: --name, --dob, or --gender");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Fetch current profile to fill missing fields (API requires all 3)
|
|
138
|
+
const current = await getApi().fetchAccountInfo();
|
|
139
|
+
const p = current?.profile || {};
|
|
140
|
+
// Convert sdob (DD/MM/YYYY) to YYYY-MM-DD format required by API
|
|
141
|
+
let currentDob = "2000-01-01";
|
|
142
|
+
if (p.sdob) {
|
|
143
|
+
const parts = p.sdob.split("/");
|
|
144
|
+
if (parts.length === 3) currentDob = `${parts[2]}-${parts[1]}-${parts[0]}`;
|
|
145
|
+
}
|
|
146
|
+
const payload = {
|
|
147
|
+
profile: {
|
|
148
|
+
name: opts.name || p.displayName || p.zaloName,
|
|
149
|
+
dob: opts.dob || currentDob,
|
|
150
|
+
gender: opts.gender !== undefined ? Number(opts.gender) : (p.gender ?? 0),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const result = await getApi().updateProfile(payload);
|
|
154
|
+
output(result, program.opts().json, () => success("Profile updated"));
|
|
155
|
+
} catch (e) {
|
|
156
|
+
error(`Profile update failed: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
profile
|
|
161
|
+
.command("settings")
|
|
162
|
+
.description("View current privacy settings")
|
|
163
|
+
.action(async () => {
|
|
164
|
+
try {
|
|
165
|
+
const result = await getApi().getSettings();
|
|
166
|
+
output(result, program.opts().json, () => {
|
|
167
|
+
info("Privacy settings:");
|
|
168
|
+
console.log();
|
|
169
|
+
for (const [slug, meta] of Object.entries(SETTING_MAP)) {
|
|
170
|
+
const raw = result[meta.key];
|
|
171
|
+
// Normalize booleans to 0/1 (API returns bool for some settings)
|
|
172
|
+
const val = raw === true ? 1 : raw === false ? 0 : raw;
|
|
173
|
+
const label = meta.values[val] || String(val);
|
|
174
|
+
console.log(` ${meta.label.padEnd(30)} ${label} (${slug}=${val})`);
|
|
175
|
+
}
|
|
176
|
+
console.log();
|
|
177
|
+
info("Update: zalo-agent profile set <setting> <value>");
|
|
178
|
+
info("Example: zalo-agent profile set online-status 0");
|
|
179
|
+
});
|
|
180
|
+
} catch (e) {
|
|
181
|
+
error(e.message);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
profile
|
|
186
|
+
.command("avatars")
|
|
187
|
+
.description("List your avatar gallery")
|
|
188
|
+
.option("-c, --count <n>", "Page size", parseInt, 50)
|
|
189
|
+
.option("-p, --page <n>", "Page number", parseInt, 1)
|
|
190
|
+
.action(async (opts) => {
|
|
191
|
+
try {
|
|
192
|
+
const result = await getApi().getAvatarList(opts.count, opts.page);
|
|
193
|
+
output(result, program.opts().json);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
error(`Get avatars failed: ${e.message}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
profile
|
|
200
|
+
.command("full-avatar <friendId>")
|
|
201
|
+
.description("Get full-size avatar URL for a user")
|
|
202
|
+
.action(async (friendId) => {
|
|
203
|
+
try {
|
|
204
|
+
const result = await getApi().getFullAvatar(friendId);
|
|
205
|
+
output(result, program.opts().json, () => {
|
|
206
|
+
info(`Avatar: ${result?.avatar || "?"}`);
|
|
207
|
+
});
|
|
208
|
+
} catch (e) {
|
|
209
|
+
error(`Get full avatar failed: ${e.message}`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
profile
|
|
214
|
+
.command("avatar-url <friendIds...>")
|
|
215
|
+
.description("Get avatar URLs for one or more users")
|
|
216
|
+
.action(async (friendIds) => {
|
|
217
|
+
try {
|
|
218
|
+
const result = await getApi().getAvatarUrlProfile(friendIds);
|
|
219
|
+
output(result, program.opts().json);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
error(`Get avatar URLs failed: ${e.message}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
profile
|
|
226
|
+
.command("delete-avatar <photoIds...>")
|
|
227
|
+
.description("Delete avatar(s) from your gallery")
|
|
228
|
+
.action(async (photoIds) => {
|
|
229
|
+
try {
|
|
230
|
+
const result = await getApi().deleteAvatar(photoIds);
|
|
231
|
+
output(result, program.opts().json, () => success(`Deleted ${photoIds.length} avatar(s)`));
|
|
232
|
+
} catch (e) {
|
|
233
|
+
error(`Delete avatar failed: ${e.message}`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
profile
|
|
238
|
+
.command("reuse-avatar <photoId>")
|
|
239
|
+
.description("Reuse a previous avatar from your gallery")
|
|
240
|
+
.action(async (photoId) => {
|
|
241
|
+
try {
|
|
242
|
+
const result = await getApi().reuseAvatar(photoId);
|
|
243
|
+
output(result, program.opts().json, () => success("Avatar reused"));
|
|
244
|
+
} catch (e) {
|
|
245
|
+
error(`Reuse avatar failed: ${e.message}`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
profile
|
|
250
|
+
.command("set <setting> <value>")
|
|
251
|
+
.description("Update a privacy setting (use 'profile settings' to see options)")
|
|
252
|
+
.action(async (setting, value) => {
|
|
253
|
+
try {
|
|
254
|
+
const meta = SETTING_MAP[setting];
|
|
255
|
+
if (!meta) {
|
|
256
|
+
error(`Unknown setting: "${setting}". Valid: ${Object.keys(SETTING_MAP).join(", ")}`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const numVal = Number(value);
|
|
260
|
+
if (!(numVal in meta.values)) {
|
|
261
|
+
error(
|
|
262
|
+
`Invalid value for ${setting}. Valid: ${Object.entries(meta.values)
|
|
263
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
264
|
+
.join(", ")}`,
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const result = await getApi().updateSettings(meta.key, numVal);
|
|
269
|
+
output(result, program.opts().json, () => success(`${meta.label}: ${meta.values[numVal]} (${numVal})`));
|
|
270
|
+
} catch (e) {
|
|
271
|
+
error(`Setting update failed: ${e.message}`);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick message commands — list, add, update, remove saved quick messages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerQuickMsgCommands(program) {
|
|
9
|
+
const qm = program.command("quick-msg").description("Manage quick/saved messages");
|
|
10
|
+
|
|
11
|
+
qm.command("list")
|
|
12
|
+
.description("List all quick messages")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await getApi().getQuickMessageList();
|
|
16
|
+
output(result, program.opts().json);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
error(`Get quick messages failed: ${e.message}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
qm.command("add <keyword> <title>")
|
|
23
|
+
.description("Add a quick message")
|
|
24
|
+
.action(async (keyword, title) => {
|
|
25
|
+
try {
|
|
26
|
+
const result = await getApi().addQuickMessage({ keyword, title });
|
|
27
|
+
output(result, program.opts().json, () => success(`Quick message added: "${keyword}" → "${title}"`));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
error(`Add quick message failed: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
qm.command("update <itemId> <keyword> <title>")
|
|
34
|
+
.description("Update a quick message")
|
|
35
|
+
.action(async (itemId, keyword, title) => {
|
|
36
|
+
try {
|
|
37
|
+
const result = await getApi().updateQuickMessage({ keyword, title }, Number(itemId));
|
|
38
|
+
output(result, program.opts().json, () => success(`Quick message ${itemId} updated`));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
error(`Update quick message failed: ${e.message}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
qm.command("remove <itemIds...>")
|
|
45
|
+
.description("Remove quick message(s)")
|
|
46
|
+
.action(async (itemIds) => {
|
|
47
|
+
try {
|
|
48
|
+
const ids = itemIds.map(Number);
|
|
49
|
+
const result = await getApi().removeQuickMessage(ids);
|
|
50
|
+
output(result, program.opts().json, () => success(`Removed ${ids.length} quick message(s)`));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
error(`Remove quick message failed: ${e.message}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reminder commands — create, list, info, edit, remove reminders in users/groups.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
/** Repeat mode labels matching zca-js ReminderRepeatMode enum. */
|
|
9
|
+
const REPEAT_MODES = { none: 0, daily: 1, weekly: 2, monthly: 3 };
|
|
10
|
+
const REPEAT_LABELS = { 0: "None", 1: "Daily", 2: "Weekly", 3: "Monthly" };
|
|
11
|
+
|
|
12
|
+
/** Parse a datetime string into Unix timestamp (ms). Accepts ISO or "YYYY-MM-DD HH:mm". */
|
|
13
|
+
function parseTime(str) {
|
|
14
|
+
// Try "YYYY-MM-DD HH:mm" format
|
|
15
|
+
const match = str.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})$/);
|
|
16
|
+
if (match) {
|
|
17
|
+
const [, y, mo, d, h, mi] = match;
|
|
18
|
+
return new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi)).getTime();
|
|
19
|
+
}
|
|
20
|
+
// Fallback to Date.parse (ISO, etc.)
|
|
21
|
+
const ts = Date.parse(str);
|
|
22
|
+
if (isNaN(ts)) return null;
|
|
23
|
+
return ts;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerReminderCommands(program) {
|
|
27
|
+
const reminder = program.command("reminder").description("Create and manage reminders in users/groups");
|
|
28
|
+
|
|
29
|
+
reminder
|
|
30
|
+
.command("create <threadId> <title>")
|
|
31
|
+
.description("Create a reminder")
|
|
32
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
33
|
+
.option("--time <datetime>", 'Reminder time: "YYYY-MM-DD HH:mm" (default: now)')
|
|
34
|
+
.option("--emoji <emoji>", "Emoji icon", "⏰")
|
|
35
|
+
.option("--repeat <mode>", "Repeat: none, daily, weekly, monthly", "none")
|
|
36
|
+
.action(async (threadId, title, opts) => {
|
|
37
|
+
try {
|
|
38
|
+
const repeatMode = REPEAT_MODES[opts.repeat];
|
|
39
|
+
if (repeatMode === undefined) {
|
|
40
|
+
error(`Invalid repeat mode: "${opts.repeat}". Valid: none, daily, weekly, monthly`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let startTime = Date.now();
|
|
44
|
+
if (opts.time) {
|
|
45
|
+
startTime = parseTime(opts.time);
|
|
46
|
+
if (!startTime) {
|
|
47
|
+
error(`Invalid time format: "${opts.time}". Use "YYYY-MM-DD HH:mm" or ISO format.`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const result = await getApi().createReminder(
|
|
52
|
+
{ title, emoji: opts.emoji, startTime, repeat: repeatMode },
|
|
53
|
+
threadId,
|
|
54
|
+
Number(opts.type),
|
|
55
|
+
);
|
|
56
|
+
output(result, program.opts().json, () => {
|
|
57
|
+
success(`Reminder created: "${title}"`);
|
|
58
|
+
const id = result.reminderId || result.id || "?";
|
|
59
|
+
info(`Reminder ID: ${id}`);
|
|
60
|
+
info(`Time: ${new Date(startTime).toLocaleString()}`);
|
|
61
|
+
if (repeatMode > 0) info(`Repeat: ${REPEAT_LABELS[repeatMode]}`);
|
|
62
|
+
});
|
|
63
|
+
} catch (e) {
|
|
64
|
+
error(`Create reminder failed: ${e.message}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
reminder
|
|
69
|
+
.command("list <threadId>")
|
|
70
|
+
.description("List reminders in a thread")
|
|
71
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
72
|
+
.option("-n, --count <n>", "Max results", "20")
|
|
73
|
+
.action(async (threadId, opts) => {
|
|
74
|
+
try {
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = await getApi().getListReminder({ count: Number(opts.count) }, threadId, Number(opts.type));
|
|
78
|
+
} catch {
|
|
79
|
+
result = null;
|
|
80
|
+
}
|
|
81
|
+
const items = Array.isArray(result) ? result : [];
|
|
82
|
+
output(items, program.opts().json, () => {
|
|
83
|
+
if (items.length === 0) {
|
|
84
|
+
info("No reminders found.");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
info(`${items.length} reminder(s):`);
|
|
88
|
+
console.log();
|
|
89
|
+
for (const r of items) {
|
|
90
|
+
const id = r.reminderId || r.id || "?";
|
|
91
|
+
const title = r.params?.title || "?";
|
|
92
|
+
const time = new Date(r.startTime).toLocaleString();
|
|
93
|
+
const repeat = REPEAT_LABELS[r.repeat] || "None";
|
|
94
|
+
const emoji = r.emoji || "";
|
|
95
|
+
console.log(` ${emoji} [${id}] ${title}`);
|
|
96
|
+
console.log(` Time: ${time} | Repeat: ${repeat}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
error(`List reminders failed: ${e.message}`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
reminder
|
|
105
|
+
.command("info <reminderId>")
|
|
106
|
+
.description("View reminder details (group reminders only)")
|
|
107
|
+
.action(async (reminderId) => {
|
|
108
|
+
try {
|
|
109
|
+
const result = await getApi().getReminder(reminderId);
|
|
110
|
+
output(result, program.opts().json, () => {
|
|
111
|
+
const title = result.params?.title || "?";
|
|
112
|
+
const id = result.id || reminderId;
|
|
113
|
+
info(`Title: ${result.emoji || ""} ${title}`);
|
|
114
|
+
info(`Reminder ID: ${id}`);
|
|
115
|
+
info(`Created: ${new Date(result.createTime).toLocaleString()}`);
|
|
116
|
+
info(`Time: ${new Date(result.startTime).toLocaleString()}`);
|
|
117
|
+
info(`Repeat: ${REPEAT_LABELS[result.repeat] || "None"}`);
|
|
118
|
+
info(`Creator: ${result.creatorId}`);
|
|
119
|
+
if (result.responseMem) {
|
|
120
|
+
info(
|
|
121
|
+
`Responses: ${result.responseMem.acceptMember} accepted, ${result.responseMem.rejectMember} rejected`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
} catch (e) {
|
|
126
|
+
error(`Get reminder failed: ${e.message}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
reminder
|
|
131
|
+
.command("responses <reminderId>")
|
|
132
|
+
.description("View who accepted/rejected a reminder (group only)")
|
|
133
|
+
.action(async (reminderId) => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await getApi().getReminderResponses(reminderId);
|
|
136
|
+
output(result, program.opts().json, () => {
|
|
137
|
+
const accepted = result.acceptMember || [];
|
|
138
|
+
const rejected = result.rejectMember || [];
|
|
139
|
+
info(`Accepted (${accepted.length}):`);
|
|
140
|
+
accepted.forEach((uid) => console.log(` ✓ ${uid}`));
|
|
141
|
+
info(`Rejected (${rejected.length}):`);
|
|
142
|
+
rejected.forEach((uid) => console.log(` ✗ ${uid}`));
|
|
143
|
+
});
|
|
144
|
+
} catch (e) {
|
|
145
|
+
error(`Get responses failed: ${e.message}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
reminder
|
|
150
|
+
.command("edit <reminderId> <threadId> <title>")
|
|
151
|
+
.description("Edit a reminder")
|
|
152
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
153
|
+
.option("--time <datetime>", 'New time: "YYYY-MM-DD HH:mm"')
|
|
154
|
+
.option("--emoji <emoji>", "New emoji icon")
|
|
155
|
+
.option("--repeat <mode>", "Repeat: none, daily, weekly, monthly")
|
|
156
|
+
.action(async (reminderId, threadId, title, opts) => {
|
|
157
|
+
try {
|
|
158
|
+
const editOpts = { title, topicId: reminderId };
|
|
159
|
+
if (opts.emoji) editOpts.emoji = opts.emoji;
|
|
160
|
+
if (opts.time) {
|
|
161
|
+
const ts = parseTime(opts.time);
|
|
162
|
+
if (!ts) {
|
|
163
|
+
error(`Invalid time format: "${opts.time}". Use "YYYY-MM-DD HH:mm".`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
editOpts.startTime = ts;
|
|
167
|
+
}
|
|
168
|
+
if (opts.repeat) {
|
|
169
|
+
const mode = REPEAT_MODES[opts.repeat];
|
|
170
|
+
if (mode === undefined) {
|
|
171
|
+
error(`Invalid repeat mode: "${opts.repeat}". Valid: none, daily, weekly, monthly`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
editOpts.repeat = mode;
|
|
175
|
+
}
|
|
176
|
+
const result = await getApi().editReminder(editOpts, threadId, Number(opts.type));
|
|
177
|
+
output(result, program.opts().json, () => success(`Reminder ${reminderId} updated`));
|
|
178
|
+
} catch (e) {
|
|
179
|
+
error(`Edit reminder failed: ${e.message}`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
reminder
|
|
184
|
+
.command("remove <reminderId> <threadId>")
|
|
185
|
+
.description("Remove a reminder")
|
|
186
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
187
|
+
.action(async (reminderId, threadId, opts) => {
|
|
188
|
+
try {
|
|
189
|
+
const result = await getApi().removeReminder(reminderId, threadId, Number(opts.type));
|
|
190
|
+
output(result, program.opts().json, () => success(`Reminder ${reminderId} removed`));
|
|
191
|
+
} catch (e) {
|
|
192
|
+
error(`Remove reminder failed: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|