zalo-agent-cli 1.0.30 → 1.1.0
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 +122 -676
- package/package.json +6 -3
- package/src/commands/auto-reply.js +83 -0
- package/src/commands/catalog.js +143 -0
- package/src/commands/conv.js +86 -0
- package/src/commands/label.js +39 -0
- package/src/commands/msg.js +47 -3
- 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/profile.js +64 -0
- package/src/commands/quick-msg.js +55 -0
- package/src/core/oa-client.js +391 -0
- package/src/index.js +15 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zalo-agent-cli",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "CLI tool for Zalo automation — multi-account, proxy
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "CLI tool for Zalo automation — multi-account, proxy, bank transfers, QR payments, Official Account API v3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zalo-agent": "src/index.js"
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
"vietnam",
|
|
29
29
|
"proxy",
|
|
30
30
|
"vietqr",
|
|
31
|
-
"zca-js"
|
|
31
|
+
"zca-js",
|
|
32
|
+
"official-account",
|
|
33
|
+
"zalo-oa",
|
|
34
|
+
"webhook"
|
|
32
35
|
],
|
|
33
36
|
"repository": {
|
|
34
37
|
"type": "git",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-reply commands — list, create, update, delete auto-reply messages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerAutoReplyCommands(program) {
|
|
9
|
+
const ar = program.command("auto-reply").description("Manage auto-reply messages");
|
|
10
|
+
|
|
11
|
+
ar.command("list")
|
|
12
|
+
.description("List all auto-reply rules")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await getApi().getAutoReplyList();
|
|
16
|
+
output(result, program.opts().json);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
error(`Get auto-reply list failed: ${e.message}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
ar.command("create <content>")
|
|
23
|
+
.description("Create an auto-reply rule")
|
|
24
|
+
.option("--enable", "Enable the rule (default: true)", true)
|
|
25
|
+
.option("--no-enable", "Create disabled")
|
|
26
|
+
.option("--start <ms>", "Start time (epoch ms)", parseInt, 0)
|
|
27
|
+
.option("--end <ms>", "End time (epoch ms)", parseInt, 0)
|
|
28
|
+
.option("--scope <n>", "Scope: 0=all, 1=friends, 2=strangers", parseInt, 0)
|
|
29
|
+
.option("--uids <ids...>", "Specific user IDs to auto-reply to")
|
|
30
|
+
.action(async (content, opts) => {
|
|
31
|
+
try {
|
|
32
|
+
const payload = {
|
|
33
|
+
content,
|
|
34
|
+
isEnable: opts.enable,
|
|
35
|
+
startTime: opts.start,
|
|
36
|
+
endTime: opts.end,
|
|
37
|
+
scope: opts.scope,
|
|
38
|
+
};
|
|
39
|
+
if (opts.uids) payload.uids = opts.uids;
|
|
40
|
+
const result = await getApi().createAutoReply(payload);
|
|
41
|
+
output(result, program.opts().json, () => success("Auto-reply created"));
|
|
42
|
+
} catch (e) {
|
|
43
|
+
error(`Create auto-reply failed: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
ar.command("update <id> <content>")
|
|
48
|
+
.description("Update an auto-reply rule")
|
|
49
|
+
.option("--enable", "Enable the rule")
|
|
50
|
+
.option("--no-enable", "Disable the rule")
|
|
51
|
+
.option("--start <ms>", "Start time (epoch ms)", parseInt, 0)
|
|
52
|
+
.option("--end <ms>", "End time (epoch ms)", parseInt, 0)
|
|
53
|
+
.option("--scope <n>", "Scope: 0=all, 1=friends, 2=strangers", parseInt, 0)
|
|
54
|
+
.option("--uids <ids...>", "Specific user IDs")
|
|
55
|
+
.action(async (id, content, opts) => {
|
|
56
|
+
try {
|
|
57
|
+
const payload = {
|
|
58
|
+
id: Number(id),
|
|
59
|
+
content,
|
|
60
|
+
isEnable: opts.enable ?? true,
|
|
61
|
+
startTime: opts.start,
|
|
62
|
+
endTime: opts.end,
|
|
63
|
+
scope: opts.scope,
|
|
64
|
+
};
|
|
65
|
+
if (opts.uids) payload.uids = opts.uids;
|
|
66
|
+
const result = await getApi().updateAutoReply(payload);
|
|
67
|
+
output(result, program.opts().json, () => success(`Auto-reply ${id} updated`));
|
|
68
|
+
} catch (e) {
|
|
69
|
+
error(`Update auto-reply failed: ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ar.command("delete <id>")
|
|
74
|
+
.description("Delete an auto-reply rule")
|
|
75
|
+
.action(async (id) => {
|
|
76
|
+
try {
|
|
77
|
+
const result = await getApi().deleteAutoReply(Number(id));
|
|
78
|
+
output(result, program.opts().json, () => success(`Auto-reply ${id} deleted`));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
error(`Delete auto-reply failed: ${e.message}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog commands — manage Zalo Shop catalogs and products.
|
|
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
|
+
export function registerCatalogCommands(program) {
|
|
10
|
+
const catalog = program.command("catalog").description("Manage Zalo Shop catalogs and products");
|
|
11
|
+
|
|
12
|
+
catalog
|
|
13
|
+
.command("list")
|
|
14
|
+
.description("List all catalogs")
|
|
15
|
+
.option("-l, --limit <n>", "Items per page", parseInt, 20)
|
|
16
|
+
.option("-p, --page <n>", "Page number", parseInt, 0)
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
try {
|
|
19
|
+
const result = await getApi().getCatalogList({ limit: opts.limit, page: opts.page });
|
|
20
|
+
output(result, program.opts().json);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
error(`Get catalogs failed: ${e.message}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
catalog
|
|
27
|
+
.command("create <name>")
|
|
28
|
+
.description("Create a new catalog")
|
|
29
|
+
.action(async (name) => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await getApi().createCatalog(name);
|
|
32
|
+
output(result, program.opts().json, () => success(`Catalog "${name}" created`));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
error(`Create catalog failed: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
catalog
|
|
39
|
+
.command("rename <catalogId> <name>")
|
|
40
|
+
.description("Rename a catalog")
|
|
41
|
+
.action(async (catalogId, name) => {
|
|
42
|
+
try {
|
|
43
|
+
const result = await getApi().updateCatalog({ catalogId, catalogName: name });
|
|
44
|
+
output(result, program.opts().json, () => success(`Catalog renamed to "${name}"`));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
error(`Rename catalog failed: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
catalog
|
|
51
|
+
.command("delete <catalogId>")
|
|
52
|
+
.description("Delete a catalog")
|
|
53
|
+
.action(async (catalogId) => {
|
|
54
|
+
try {
|
|
55
|
+
const result = await getApi().deleteCatalog(catalogId);
|
|
56
|
+
output(result, program.opts().json, () => success(`Catalog ${catalogId} deleted`));
|
|
57
|
+
} catch (e) {
|
|
58
|
+
error(`Delete catalog failed: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
catalog
|
|
63
|
+
.command("products <catalogId>")
|
|
64
|
+
.description("List products in a catalog")
|
|
65
|
+
.option("-l, --limit <n>", "Items per page", parseInt, 100)
|
|
66
|
+
.option("-p, --page <n>", "Page number", parseInt, 0)
|
|
67
|
+
.action(async (catalogId, opts) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await getApi().getProductCatalogList({
|
|
70
|
+
catalogId,
|
|
71
|
+
limit: opts.limit,
|
|
72
|
+
page: opts.page,
|
|
73
|
+
});
|
|
74
|
+
output(result, program.opts().json);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
error(`Get products failed: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
catalog
|
|
81
|
+
.command("add-product <catalogId> <name> <price> <description>")
|
|
82
|
+
.description("Add a product to a catalog")
|
|
83
|
+
.option("--photos <urls...>", "Product photo URLs (up to 5)")
|
|
84
|
+
.action(async (catalogId, name, price, description, opts) => {
|
|
85
|
+
try {
|
|
86
|
+
const payload = { catalogId, productName: name, price, description };
|
|
87
|
+
if (opts.photos) payload.product_photos = opts.photos;
|
|
88
|
+
const result = await getApi().createProductCatalog(payload);
|
|
89
|
+
output(result, program.opts().json, () => success(`Product "${name}" added to catalog`));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
error(`Add product failed: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
catalog
|
|
96
|
+
.command("update-product <catalogId> <productId> <name> <price> <description>")
|
|
97
|
+
.description("Update a product in a catalog")
|
|
98
|
+
.option("--photos <urls...>", "Product photo URLs (up to 5)")
|
|
99
|
+
.action(async (catalogId, productId, name, price, description, opts) => {
|
|
100
|
+
try {
|
|
101
|
+
const payload = {
|
|
102
|
+
catalogId,
|
|
103
|
+
productId,
|
|
104
|
+
productName: name,
|
|
105
|
+
price,
|
|
106
|
+
description,
|
|
107
|
+
createTime: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
if (opts.photos) payload.product_photos = opts.photos;
|
|
110
|
+
const result = await getApi().updateProductCatalog(payload);
|
|
111
|
+
output(result, program.opts().json, () => success(`Product ${productId} updated`));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
error(`Update product failed: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
catalog
|
|
118
|
+
.command("delete-product <catalogId> <productIds...>")
|
|
119
|
+
.description("Delete product(s) from a catalog")
|
|
120
|
+
.action(async (catalogId, productIds) => {
|
|
121
|
+
try {
|
|
122
|
+
const result = await getApi().deleteProductCatalog({ catalogId, productIds });
|
|
123
|
+
output(result, program.opts().json, () => success(`Deleted ${productIds.length} product(s)`));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
error(`Delete product failed: ${e.message}`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
catalog
|
|
130
|
+
.command("upload-photo <filePath>")
|
|
131
|
+
.description("Upload a product photo (returns URL for use with add-product --photos)")
|
|
132
|
+
.action(async (filePath) => {
|
|
133
|
+
try {
|
|
134
|
+
const absPath = resolve(filePath);
|
|
135
|
+
const result = await getApi().uploadProductPhoto({ file: absPath });
|
|
136
|
+
output(result, program.opts().json, () => {
|
|
137
|
+
info("Photo uploaded. Use the URL with: catalog add-product --photos <url>");
|
|
138
|
+
});
|
|
139
|
+
} catch (e) {
|
|
140
|
+
error(`Upload photo failed: ${e.message}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
package/src/commands/conv.js
CHANGED
|
@@ -163,6 +163,92 @@ export function registerConvCommands(program) {
|
|
|
163
163
|
}
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
conv.command("hidden")
|
|
167
|
+
.description("List hidden conversations")
|
|
168
|
+
.action(async () => {
|
|
169
|
+
try {
|
|
170
|
+
const result = await getApi().getHiddenConversations();
|
|
171
|
+
output(result, program.opts().json);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
error(`Get hidden conversations failed: ${e.message}`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
conv.command("hide <threadIds...>")
|
|
178
|
+
.description("Hide conversation(s)")
|
|
179
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
180
|
+
.action(async (threadIds, opts) => {
|
|
181
|
+
try {
|
|
182
|
+
const result = await getApi().setHiddenConversations(true, threadIds, Number(opts.type));
|
|
183
|
+
output(result, program.opts().json, () => success(`Hidden ${threadIds.length} conversation(s)`));
|
|
184
|
+
} catch (e) {
|
|
185
|
+
error(`Hide failed: ${e.message}`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
conv.command("unhide <threadIds...>")
|
|
190
|
+
.description("Unhide conversation(s)")
|
|
191
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
192
|
+
.action(async (threadIds, opts) => {
|
|
193
|
+
try {
|
|
194
|
+
const result = await getApi().setHiddenConversations(false, threadIds, Number(opts.type));
|
|
195
|
+
output(result, program.opts().json, () => success(`Unhidden ${threadIds.length} conversation(s)`));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
error(`Unhide failed: ${e.message}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
conv.command("hidden-pin <pin>")
|
|
202
|
+
.description("Set or update PIN for hidden conversations (4 digits)")
|
|
203
|
+
.action(async (pin) => {
|
|
204
|
+
try {
|
|
205
|
+
const result = await getApi().updateHiddenConversPin(pin);
|
|
206
|
+
output(result, program.opts().json, () => success("Hidden conversation PIN updated"));
|
|
207
|
+
} catch (e) {
|
|
208
|
+
error(`Update PIN failed: ${e.message}`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
conv.command("hidden-pin-reset")
|
|
213
|
+
.description("Reset hidden conversations PIN")
|
|
214
|
+
.action(async () => {
|
|
215
|
+
try {
|
|
216
|
+
const result = await getApi().resetHiddenConversPin();
|
|
217
|
+
output(result, program.opts().json, () => success("Hidden conversation PIN reset"));
|
|
218
|
+
} catch (e) {
|
|
219
|
+
error(`Reset PIN failed: ${e.message}`);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
conv.command("auto-delete-status")
|
|
224
|
+
.description("View auto-delete chat settings")
|
|
225
|
+
.action(async () => {
|
|
226
|
+
try {
|
|
227
|
+
const result = await getApi().getAutoDeleteChat();
|
|
228
|
+
output(result, program.opts().json);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
error(`Get auto-delete status failed: ${e.message}`);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
conv.command("auto-delete <threadId> <ttl>")
|
|
235
|
+
.description("Set auto-delete for a conversation (off, 1d, 7d, 14d)")
|
|
236
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
237
|
+
.action(async (threadId, ttl, opts) => {
|
|
238
|
+
try {
|
|
239
|
+
const ttlMap = { off: 0, "1d": 86400000, "7d": 604800000, "14d": 1209600000 };
|
|
240
|
+
const ttlValue = ttlMap[ttl];
|
|
241
|
+
if (ttlValue === undefined) {
|
|
242
|
+
error(`Invalid TTL "${ttl}". Valid: off, 1d, 7d, 14d`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const result = await getApi().updateAutoDeleteChat(ttlValue, threadId, Number(opts.type));
|
|
246
|
+
output(result, program.opts().json, () => success(`Auto-delete set to ${ttl} for ${threadId}`));
|
|
247
|
+
} catch (e) {
|
|
248
|
+
error(`Set auto-delete failed: ${e.message}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
166
252
|
conv.command("delete <threadId>")
|
|
167
253
|
.description("Delete conversation history")
|
|
168
254
|
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label commands — list and update conversation/contact labels.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerLabelCommands(program) {
|
|
9
|
+
const label = program.command("label").description("Manage conversation labels");
|
|
10
|
+
|
|
11
|
+
label
|
|
12
|
+
.command("list")
|
|
13
|
+
.description("List all labels")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
try {
|
|
16
|
+
const result = await getApi().getLabels();
|
|
17
|
+
output(result, program.opts().json);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
error(`Get labels failed: ${e.message}`);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
label
|
|
24
|
+
.command("update <json>")
|
|
25
|
+
.description('Update labels (JSON payload: {"labelData":[...],"version":N})')
|
|
26
|
+
.action(async (json) => {
|
|
27
|
+
try {
|
|
28
|
+
const payload = JSON.parse(json);
|
|
29
|
+
const result = await getApi().updateLabels(payload);
|
|
30
|
+
output(result, program.opts().json, () => success("Labels updated"));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (e instanceof SyntaxError) {
|
|
33
|
+
error(`Invalid JSON: ${e.message}`);
|
|
34
|
+
} else {
|
|
35
|
+
error(`Update labels failed: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
package/src/commands/msg.js
CHANGED
|
@@ -307,12 +307,18 @@ export function registerMsgCommands(program) {
|
|
|
307
307
|
try {
|
|
308
308
|
const api = getApi();
|
|
309
309
|
const search = await api.searchSticker(keyword);
|
|
310
|
-
const
|
|
311
|
-
if (!
|
|
310
|
+
const first = search?.[0];
|
|
311
|
+
if (!first) {
|
|
312
312
|
error("No sticker found");
|
|
313
313
|
return;
|
|
314
314
|
}
|
|
315
|
-
|
|
315
|
+
// sendSticker expects {id, cateId, type} object
|
|
316
|
+
const stickerObj = {
|
|
317
|
+
id: first.sticker_id || first.stickerId || first.id,
|
|
318
|
+
cateId: first.cate_id || first.cateId,
|
|
319
|
+
type: first.type || 7,
|
|
320
|
+
};
|
|
321
|
+
const result = await api.sendSticker(stickerObj, threadId, Number(opts.type));
|
|
316
322
|
output(result, program.opts().json, () => success("Sticker sent"));
|
|
317
323
|
} catch (e) {
|
|
318
324
|
error(e.message);
|
|
@@ -376,6 +382,44 @@ export function registerMsgCommands(program) {
|
|
|
376
382
|
}
|
|
377
383
|
});
|
|
378
384
|
|
|
385
|
+
msg.command("sticker-list <keyword>")
|
|
386
|
+
.description("Search stickers by keyword (returns sticker IDs)")
|
|
387
|
+
.action(async (keyword) => {
|
|
388
|
+
try {
|
|
389
|
+
const result = await getApi().getStickers(keyword);
|
|
390
|
+
output(result, program.opts().json, () => {
|
|
391
|
+
const ids = Array.isArray(result) ? result : [];
|
|
392
|
+
info(`${ids.length} sticker(s) found for "${keyword}"`);
|
|
393
|
+
for (const id of ids) console.log(` ${id}`);
|
|
394
|
+
});
|
|
395
|
+
} catch (e) {
|
|
396
|
+
error(`Sticker search failed: ${e.message}`);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
msg.command("sticker-detail <stickerIds...>")
|
|
401
|
+
.description("Get sticker details by IDs")
|
|
402
|
+
.action(async (stickerIds) => {
|
|
403
|
+
try {
|
|
404
|
+
const ids = stickerIds.map(Number);
|
|
405
|
+
const result = await getApi().getStickersDetail(ids);
|
|
406
|
+
output(result, program.opts().json);
|
|
407
|
+
} catch (e) {
|
|
408
|
+
error(`Sticker detail failed: ${e.message}`);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
msg.command("sticker-category <categoryId>")
|
|
413
|
+
.description("Get sticker category details")
|
|
414
|
+
.action(async (categoryId) => {
|
|
415
|
+
try {
|
|
416
|
+
const result = await getApi().getStickerCategoryDetail(Number(categoryId));
|
|
417
|
+
output(result, program.opts().json);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
error(`Sticker category failed: ${e.message}`);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
379
423
|
msg.command("react <msgId> <threadId> <reaction>")
|
|
380
424
|
.description(
|
|
381
425
|
"React to a message. Reaction codes: :> (haha), /-heart (heart), /-strong (like), :o (wow), :-(( (cry), :-h (angry)",
|