@manybot/manybot 5.7.0 → 5.9.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 +28 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +91 -6
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +133 -15
- package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
- package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
- package/dist/drivers/baileys/api/index.js +384 -62
- package/dist/drivers/baileys/index.js +92 -36
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +344 -4
- package/dist/drivers/baileys/messageHandler.test.js +445 -0
- package/dist/drivers/baileysAdapter.test.js +378 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +74 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +20 -24
- package/dist/kernel/activeDriverSend.js +21 -0
- package/dist/kernel/activeDriverSend.test.js +89 -0
- package/dist/kernel/alerts.js +3 -9
- package/dist/kernel/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- package/dist/kernel/chatSession.js +65 -0
- package/dist/kernel/chatSession.test.js +46 -0
- package/dist/kernel/commandAccess.js +66 -0
- package/dist/kernel/commandAccess.test.js +74 -0
- package/dist/kernel/commandDeprecation.js +170 -0
- package/dist/kernel/commandDeprecation.test.js +114 -0
- package/dist/kernel/commandMenu.js +357 -0
- package/dist/kernel/commandMenu.test.js +363 -0
- package/dist/kernel/commandPermissions.js +171 -0
- package/dist/kernel/commandPermissions.test.js +227 -0
- package/dist/kernel/commandRegistry.js +583 -0
- package/dist/kernel/commandRegistry.test.js +158 -0
- package/dist/kernel/commandsConfig.js +949 -0
- package/dist/kernel/commandsConfig.test.js +482 -0
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/contactAutoSave.test.js +87 -0
- package/dist/kernel/coreCommands.js +62 -0
- package/dist/kernel/driverManager.js +10 -6
- package/dist/kernel/driverManager.test.js +90 -0
- package/dist/kernel/integrationMode.js +88 -0
- package/dist/kernel/integrationMode.test.js +95 -0
- package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
- package/dist/kernel/pluginApi.test.js +600 -0
- package/dist/kernel/pluginGuard.js +18 -13
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +169 -11
- package/dist/kernel/pluginLoader.test.js +190 -0
- package/dist/kernel/runCommand.js +284 -0
- package/dist/kernel/runCommand.test.js +497 -0
- package/dist/kernel/sendFallbackGuard.js +19 -48
- package/dist/kernel/sendFallbackGuard.test.js +80 -0
- package/dist/kernel/sendGuard.js +38 -42
- package/dist/kernel/sendGuard.test.js +102 -0
- package/dist/kernel/settingsDb.js +19 -5
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +192 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +77 -13
- package/dist/locales/es.json +77 -13
- package/dist/locales/pt.json +77 -13
- package/dist/logger/logger.js +23 -3
- package/dist/logger/logger.test.js +45 -0
- package/dist/main.js +5 -76
- package/dist/plugins/__manybot_integration__/index.js +184 -0
- package/dist/plugins/__manybot_integration__/index.test.js +218 -0
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +76 -18
- package/dist/drivers/whatsmeow/client.js +0 -252
- package/dist/drivers/whatsmeow/index.js +0 -79
- package/dist/drivers/whatsmeow/installer.js +0 -86
- package/dist/drivers/whatsmeow/supervisor.js +0 -328
- package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, beforeEach, describe, test } from "node:test";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "manybot-commands-config-"));
|
|
7
|
+
process.env.MANYBOT_CONFIG_DIR = configDir;
|
|
8
|
+
const { loadCommandsConfig, parseLocalizedString, resolveFileRef } = await import("#kernel/commandsConfig.js");
|
|
9
|
+
const commandsFile = path.join(configDir, "commands.yaml");
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await fs.rm(commandsFile, { force: true });
|
|
12
|
+
});
|
|
13
|
+
after(async () => {
|
|
14
|
+
await fs.rm(configDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
describe("kernel/commandsConfig", () => {
|
|
17
|
+
test("parses localized strings and preserves only usable values", () => {
|
|
18
|
+
assert.equal(parseLocalizedString(" hello "), "hello");
|
|
19
|
+
assert.equal(parseLocalizedString(" "), null);
|
|
20
|
+
assert.deepEqual(parseLocalizedString({ en: " Hello ", pt: " Olá ", invalid: 3 }), {
|
|
21
|
+
en: "Hello",
|
|
22
|
+
pt: "Olá",
|
|
23
|
+
});
|
|
24
|
+
assert.equal(parseLocalizedString([]), null);
|
|
25
|
+
});
|
|
26
|
+
test("returns null when commands.yaml does not exist", async () => {
|
|
27
|
+
assert.equal(await loadCommandsConfig(), null);
|
|
28
|
+
});
|
|
29
|
+
test("uses defaults for an empty YAML document", async () => {
|
|
30
|
+
await fs.writeFile(commandsFile, "null\n", "utf8");
|
|
31
|
+
const config = await loadCommandsConfig();
|
|
32
|
+
assert.ok(config);
|
|
33
|
+
assert.equal(config.defaults.notifyChanges, true);
|
|
34
|
+
assert.equal(config.defaults.notifyPeriodDays, 7);
|
|
35
|
+
assert.equal(config.menu.enabled, false);
|
|
36
|
+
assert.deepEqual(config.menu.aliases, ["help", "man", "menu", "bot", "?"]);
|
|
37
|
+
assert.deepEqual(config.specs, []);
|
|
38
|
+
});
|
|
39
|
+
test("rejects malformed and non-object YAML roots", async () => {
|
|
40
|
+
await fs.writeFile(commandsFile, "commands: [unterminated", "utf8");
|
|
41
|
+
assert.equal(await loadCommandsConfig(), null);
|
|
42
|
+
await fs.writeFile(commandsFile, "- not\n- a mapping\n", "utf8");
|
|
43
|
+
assert.equal(await loadCommandsConfig(), null);
|
|
44
|
+
});
|
|
45
|
+
test("loads defaults, menu, categories, manuals, file references and command permissions", async () => {
|
|
46
|
+
await fs.writeFile(path.join(configDir, "reply.txt"), "Reply from file", "utf8");
|
|
47
|
+
await fs.writeFile(path.join(configDir, "manual-pt.txt"), "Manual em português", "utf8");
|
|
48
|
+
await fs.writeFile(commandsFile, `
|
|
49
|
+
defaults:
|
|
50
|
+
notifyChanges: false
|
|
51
|
+
notifyPeriodDays: 12.8
|
|
52
|
+
notifyMessage: " Command changed "
|
|
53
|
+
permissions:
|
|
54
|
+
admin: true
|
|
55
|
+
scope: GROUP
|
|
56
|
+
cooldownSeconds: 4
|
|
57
|
+
whitelist:
|
|
58
|
+
groups: [ " group@g.us ", 1 ]
|
|
59
|
+
users: [ " user@c.us " ]
|
|
60
|
+
messages:
|
|
61
|
+
cooldown: " Wait "
|
|
62
|
+
menu:
|
|
63
|
+
title: { en: " Commands ", pt: " Comandos " }
|
|
64
|
+
intro: " Intro "
|
|
65
|
+
footer: " Footer "
|
|
66
|
+
aliases: [ help, " ? ", 1 ]
|
|
67
|
+
notFoundFallback: true
|
|
68
|
+
categories:
|
|
69
|
+
fun:
|
|
70
|
+
label: { en: " Fun " }
|
|
71
|
+
order: 2
|
|
72
|
+
uncategorized: {}
|
|
73
|
+
manuals:
|
|
74
|
+
greeting: "file: manual-pt.txt"
|
|
75
|
+
hello:
|
|
76
|
+
cmd: " hello "
|
|
77
|
+
aliases: [ hi, " oi ", 3 ]
|
|
78
|
+
plugin: " sample "
|
|
79
|
+
function: " greet "
|
|
80
|
+
text: "file: reply.txt"
|
|
81
|
+
desc: { en: " Say hello ", pt: " Diga oi " }
|
|
82
|
+
category: " fun "
|
|
83
|
+
manual: { pt: "file: manual-pt.txt", en: " Plain manual " }
|
|
84
|
+
deprecatedMessage: " Old command "
|
|
85
|
+
notifyChanges: false
|
|
86
|
+
permissions:
|
|
87
|
+
botAdmin: true
|
|
88
|
+
owner: false
|
|
89
|
+
scope: dm
|
|
90
|
+
cooldownSeconds: 0
|
|
91
|
+
blacklist:
|
|
92
|
+
users: [ blocked@c.us ]
|
|
93
|
+
messages:
|
|
94
|
+
ownerOnly: " Owners only "
|
|
95
|
+
invalid: "not a command"
|
|
96
|
+
missingCmd:
|
|
97
|
+
aliases: [ no ]
|
|
98
|
+
`, "utf8");
|
|
99
|
+
const config = await loadCommandsConfig();
|
|
100
|
+
assert.ok(config);
|
|
101
|
+
assert.deepEqual(config.defaults, {
|
|
102
|
+
notifyChanges: false,
|
|
103
|
+
notifyPeriodDays: 12,
|
|
104
|
+
notifyMessage: "Command changed",
|
|
105
|
+
permissions: {
|
|
106
|
+
admin: true,
|
|
107
|
+
botAdmin: undefined,
|
|
108
|
+
owner: undefined,
|
|
109
|
+
scope: "group",
|
|
110
|
+
cooldownSeconds: 4,
|
|
111
|
+
whitelist: { groups: ["group@g.us"], users: ["user@c.us"] },
|
|
112
|
+
blacklist: undefined,
|
|
113
|
+
dono: undefined,
|
|
114
|
+
allowedChats: undefined,
|
|
115
|
+
groupOnly: undefined,
|
|
116
|
+
dmOnly: undefined,
|
|
117
|
+
whitelistGroups: undefined,
|
|
118
|
+
blacklistUsers: undefined,
|
|
119
|
+
hiddenOutsideScope: undefined,
|
|
120
|
+
},
|
|
121
|
+
messages: {
|
|
122
|
+
botNotAdmin: undefined,
|
|
123
|
+
senderNotAdmin: undefined,
|
|
124
|
+
ownerOnly: undefined,
|
|
125
|
+
donoOnly: undefined,
|
|
126
|
+
wrongScope: undefined,
|
|
127
|
+
cooldown: "Wait",
|
|
128
|
+
blacklist: undefined,
|
|
129
|
+
allowedChats: undefined,
|
|
130
|
+
},
|
|
131
|
+
loading: null,
|
|
132
|
+
});
|
|
133
|
+
assert.deepEqual(config.menu, {
|
|
134
|
+
enabled: true,
|
|
135
|
+
title: { en: "Commands", pt: "Comandos" },
|
|
136
|
+
intro: "Intro",
|
|
137
|
+
footer: "Footer",
|
|
138
|
+
cmd: "menu",
|
|
139
|
+
aliases: ["help", "?"],
|
|
140
|
+
notFoundFallback: true,
|
|
141
|
+
suggestSimilar: false,
|
|
142
|
+
suggestMaxDistance: 2,
|
|
143
|
+
welcomeMessage: null,
|
|
144
|
+
welcomeWindowDays: 3,
|
|
145
|
+
pageSize: 15,
|
|
146
|
+
});
|
|
147
|
+
assert.deepEqual(config.categories, {
|
|
148
|
+
fun: { label: { en: "Fun" }, order: 2, scope: null, hiddenInScope: null },
|
|
149
|
+
uncategorized: { label: "uncategorized", order: 999, scope: null, hiddenInScope: null },
|
|
150
|
+
});
|
|
151
|
+
assert.deepEqual(config.manuals, { greeting: "Manual em português" });
|
|
152
|
+
assert.equal(config.specs.length, 1);
|
|
153
|
+
assert.deepEqual(config.specs[0], {
|
|
154
|
+
id: "hello",
|
|
155
|
+
cmd: "hello",
|
|
156
|
+
aliases: ["hi", "oi"],
|
|
157
|
+
plugin: "sample",
|
|
158
|
+
functions: ["greet"],
|
|
159
|
+
loading: null,
|
|
160
|
+
text: "Reply from file",
|
|
161
|
+
desc: { en: "Say hello", pt: "Diga oi" },
|
|
162
|
+
category: "fun",
|
|
163
|
+
group: null,
|
|
164
|
+
manual: { pt: "Manual em português", en: "Plain manual" },
|
|
165
|
+
deprecatedMessage: "Old command",
|
|
166
|
+
notifyChanges: false,
|
|
167
|
+
permissions: {
|
|
168
|
+
admin: undefined,
|
|
169
|
+
botAdmin: true,
|
|
170
|
+
owner: false,
|
|
171
|
+
scope: "dm",
|
|
172
|
+
cooldownSeconds: 0,
|
|
173
|
+
whitelist: undefined,
|
|
174
|
+
blacklist: { groups: undefined, users: ["blocked@c.us"] },
|
|
175
|
+
dono: undefined,
|
|
176
|
+
allowedChats: undefined,
|
|
177
|
+
groupOnly: undefined,
|
|
178
|
+
dmOnly: undefined,
|
|
179
|
+
whitelistGroups: undefined,
|
|
180
|
+
blacklistUsers: undefined,
|
|
181
|
+
hiddenOutsideScope: undefined,
|
|
182
|
+
},
|
|
183
|
+
messages: {
|
|
184
|
+
botNotAdmin: undefined,
|
|
185
|
+
senderNotAdmin: undefined,
|
|
186
|
+
ownerOnly: "Owners only",
|
|
187
|
+
donoOnly: undefined,
|
|
188
|
+
wrongScope: undefined,
|
|
189
|
+
cooldown: undefined,
|
|
190
|
+
blacklist: undefined,
|
|
191
|
+
allowedChats: undefined,
|
|
192
|
+
},
|
|
193
|
+
arguments: [],
|
|
194
|
+
subcommands: [],
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
test("keeps an unreadable file reference as the original text", async () => {
|
|
198
|
+
assert.equal(await resolveFileRef("file: missing.txt"), "file: missing.txt");
|
|
199
|
+
assert.deepEqual(await resolveFileRef({ en: "file: missing.txt", pt: "Texto" }), {
|
|
200
|
+
en: "file: missing.txt",
|
|
201
|
+
pt: "Texto",
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
test("import: merges top-level sections from auxiliary files", async () => {
|
|
205
|
+
await fs.writeFile(path.join(configDir, "menu.yaml"), `
|
|
206
|
+
menu:
|
|
207
|
+
title: "Imported Menu"
|
|
208
|
+
aliases: ["ajuda"]
|
|
209
|
+
`, "utf8");
|
|
210
|
+
await fs.writeFile(path.join(configDir, "manual.yaml"), `
|
|
211
|
+
manuals:
|
|
212
|
+
hello: "Imported manual"
|
|
213
|
+
`, "utf8");
|
|
214
|
+
await fs.writeFile(commandsFile, `
|
|
215
|
+
import:
|
|
216
|
+
- menu.yaml
|
|
217
|
+
- manual.yaml
|
|
218
|
+
hello:
|
|
219
|
+
cmd: hello
|
|
220
|
+
plugin: sample
|
|
221
|
+
function: greet
|
|
222
|
+
`, "utf8");
|
|
223
|
+
const config = await loadCommandsConfig();
|
|
224
|
+
assert.ok(config);
|
|
225
|
+
assert.equal(config.menu.title, "Imported Menu");
|
|
226
|
+
assert.deepEqual(config.menu.aliases, ["ajuda"]);
|
|
227
|
+
assert.deepEqual(config.manuals, { hello: "Imported manual" });
|
|
228
|
+
assert.equal(config.specs.length, 1);
|
|
229
|
+
assert.equal(config.specs[0].id, "hello");
|
|
230
|
+
});
|
|
231
|
+
test("import: accepts a single path (not wrapped in a list)", async () => {
|
|
232
|
+
await fs.writeFile(path.join(configDir, "menu.yaml"), `
|
|
233
|
+
menu:
|
|
234
|
+
title: "Solo Import"
|
|
235
|
+
`, "utf8");
|
|
236
|
+
await fs.writeFile(commandsFile, `
|
|
237
|
+
import: menu.yaml
|
|
238
|
+
`, "utf8");
|
|
239
|
+
const config = await loadCommandsConfig();
|
|
240
|
+
assert.ok(config);
|
|
241
|
+
assert.equal(config.menu.title, "Solo Import");
|
|
242
|
+
});
|
|
243
|
+
test("import: a key already owned by the main file or an earlier import is kept, not overwritten", async () => {
|
|
244
|
+
await fs.writeFile(path.join(configDir, "menu.yaml"), `
|
|
245
|
+
menu:
|
|
246
|
+
title: "Should be ignored"
|
|
247
|
+
`, "utf8");
|
|
248
|
+
await fs.writeFile(commandsFile, `
|
|
249
|
+
import:
|
|
250
|
+
- menu.yaml
|
|
251
|
+
menu:
|
|
252
|
+
title: "Main file wins"
|
|
253
|
+
`, "utf8");
|
|
254
|
+
const config = await loadCommandsConfig();
|
|
255
|
+
assert.ok(config);
|
|
256
|
+
assert.equal(config.menu.title, "Main file wins");
|
|
257
|
+
});
|
|
258
|
+
test("import: a missing or malformed import file is skipped without failing the whole load", async () => {
|
|
259
|
+
await fs.writeFile(path.join(configDir, "broken.yaml"), "not: [a, valid\n", "utf8");
|
|
260
|
+
await fs.writeFile(commandsFile, `
|
|
261
|
+
import:
|
|
262
|
+
- does-not-exist.yaml
|
|
263
|
+
- broken.yaml
|
|
264
|
+
hello:
|
|
265
|
+
cmd: hello
|
|
266
|
+
plugin: sample
|
|
267
|
+
function: greet
|
|
268
|
+
`, "utf8");
|
|
269
|
+
const config = await loadCommandsConfig();
|
|
270
|
+
assert.ok(config);
|
|
271
|
+
assert.equal(config.specs.length, 1);
|
|
272
|
+
assert.equal(config.specs[0].id, "hello");
|
|
273
|
+
});
|
|
274
|
+
// ── loading: snake_case flat-form props (reference yaml uses these) ──────
|
|
275
|
+
describe("loading: snake_case flat-form props", () => {
|
|
276
|
+
test("loading_presets accepts on_success/on_error (reaction) and interval_ms (spinner) — reference yaml's exact shape", async () => {
|
|
277
|
+
await fs.writeFile(commandsFile, `
|
|
278
|
+
loading_presets:
|
|
279
|
+
padrao:
|
|
280
|
+
type: reaction
|
|
281
|
+
icon: "⏳"
|
|
282
|
+
on_success: "✅"
|
|
283
|
+
on_error: "❌"
|
|
284
|
+
spinner_classico:
|
|
285
|
+
type: spinner
|
|
286
|
+
frames: ["⠋", "⠙"]
|
|
287
|
+
interval_ms: 1000
|
|
288
|
+
on_success: "✅ Pronto!"
|
|
289
|
+
on_error: "Erro: {erro}"
|
|
290
|
+
loading: padrao
|
|
291
|
+
`, "utf8");
|
|
292
|
+
const config = await loadCommandsConfig();
|
|
293
|
+
assert.ok(config);
|
|
294
|
+
assert.deepEqual(config.loadingPresets.padrao, {
|
|
295
|
+
type: "reaction",
|
|
296
|
+
icon: "⏳",
|
|
297
|
+
onSuccess: "✅",
|
|
298
|
+
onError: "❌",
|
|
299
|
+
});
|
|
300
|
+
assert.deepEqual(config.loadingPresets.spinner_classico, {
|
|
301
|
+
type: "spinner",
|
|
302
|
+
frames: ["⠋", "⠙"],
|
|
303
|
+
intervalMs: 1000,
|
|
304
|
+
onSuccess: "✅ Pronto!",
|
|
305
|
+
onError: "Erro: {erro}",
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
test("camelCase and snake_case forms are equivalent, not additive (last one parsed wins, neither is required)", async () => {
|
|
309
|
+
await fs.writeFile(commandsFile, `
|
|
310
|
+
loading_presets:
|
|
311
|
+
onlyCamel:
|
|
312
|
+
type: reaction
|
|
313
|
+
onSuccess: "camel"
|
|
314
|
+
onlySnake:
|
|
315
|
+
type: reaction
|
|
316
|
+
on_success: "snake"
|
|
317
|
+
`, "utf8");
|
|
318
|
+
const config = await loadCommandsConfig();
|
|
319
|
+
assert.ok(config);
|
|
320
|
+
assert.equal(config.loadingPresets.onlyCamel.onSuccess, "camel");
|
|
321
|
+
assert.equal(config.loadingPresets.onlySnake.onSuccess, "snake");
|
|
322
|
+
});
|
|
323
|
+
test("an actually-unknown property for the declared type is still fatal (malformed config)", async () => {
|
|
324
|
+
await fs.writeFile(commandsFile, `
|
|
325
|
+
loading_presets:
|
|
326
|
+
bad:
|
|
327
|
+
type: reaction
|
|
328
|
+
frames: ["not", "valid", "for", "reaction"]
|
|
329
|
+
`, "utf8");
|
|
330
|
+
const config = await loadCommandsConfig();
|
|
331
|
+
assert.ok(config);
|
|
332
|
+
assert.equal(config.loadingPresets.bad, undefined, "malformed preset is dropped, not silently accepted");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
// ── top-level `loading:` global default overlays onto defaults.loading ──
|
|
336
|
+
test("top-level loading: <preset-name> overlays onto defaults.loading, same as notify_*/permission_messages", async () => {
|
|
337
|
+
await fs.writeFile(commandsFile, `
|
|
338
|
+
loading_presets:
|
|
339
|
+
padrao:
|
|
340
|
+
type: reaction
|
|
341
|
+
icon: "⏳"
|
|
342
|
+
loading: padrao
|
|
343
|
+
`, "utf8");
|
|
344
|
+
const config = await loadCommandsConfig();
|
|
345
|
+
assert.ok(config);
|
|
346
|
+
assert.deepEqual(config.defaults.loading, { type: "reaction", icon: "⏳" });
|
|
347
|
+
});
|
|
348
|
+
test("top-level loading: accepts an inline spec, not just a preset name", async () => {
|
|
349
|
+
await fs.writeFile(commandsFile, `
|
|
350
|
+
loading:
|
|
351
|
+
type: typing
|
|
352
|
+
`, "utf8");
|
|
353
|
+
const config = await loadCommandsConfig();
|
|
354
|
+
assert.ok(config);
|
|
355
|
+
assert.deepEqual(config.defaults.loading, { type: "typing" });
|
|
356
|
+
});
|
|
357
|
+
test("top-level loading: wins over defaults.loading when both are present (overlay semantics)", async () => {
|
|
358
|
+
await fs.writeFile(commandsFile, `
|
|
359
|
+
defaults:
|
|
360
|
+
loading:
|
|
361
|
+
type: typing
|
|
362
|
+
loading:
|
|
363
|
+
type: none
|
|
364
|
+
`, "utf8");
|
|
365
|
+
const config = await loadCommandsConfig();
|
|
366
|
+
assert.ok(config);
|
|
367
|
+
assert.deepEqual(config.defaults.loading, { type: "none" });
|
|
368
|
+
});
|
|
369
|
+
// ── plugin: registry-key normalization ───────────────────────────────
|
|
370
|
+
// The caller passes the active pluginRegistry's keys via
|
|
371
|
+
// loadCommandsConfig({ validPluginKeys }). parseEntry uses them to
|
|
372
|
+
// resolve shorthand `plugin: <name>` entries to the canonical
|
|
373
|
+
// `owner/repo` key (and to leave fully-qualified entries alone).
|
|
374
|
+
describe("plugin: registry-key normalization", () => {
|
|
375
|
+
test("keeps an exact owner/repo key verbatim", async () => {
|
|
376
|
+
await fs.writeFile(commandsFile, `
|
|
377
|
+
hello:
|
|
378
|
+
cmd: hello
|
|
379
|
+
plugin: synt-xerror/welcome
|
|
380
|
+
functions: [greet]
|
|
381
|
+
`, "utf8");
|
|
382
|
+
const config = await loadCommandsConfig(new Set(["synt-xerror/welcome", "de/welcome-test"]));
|
|
383
|
+
assert.ok(config);
|
|
384
|
+
assert.equal(config.specs.length, 1);
|
|
385
|
+
assert.equal(config.specs[0].plugin, "synt-xerror/welcome");
|
|
386
|
+
assert.deepEqual(config.specs[0].functions, ["greet"]);
|
|
387
|
+
});
|
|
388
|
+
test("resolves a bare name to the unique matching owner/repo key", async () => {
|
|
389
|
+
await fs.writeFile(commandsFile, `
|
|
390
|
+
hello:
|
|
391
|
+
cmd: hello
|
|
392
|
+
plugin: welcome
|
|
393
|
+
functions: [greet]
|
|
394
|
+
`, "utf8");
|
|
395
|
+
const config = await loadCommandsConfig(new Set(["synt-xerror/welcome", "de/welcome-test"]));
|
|
396
|
+
assert.ok(config);
|
|
397
|
+
assert.equal(config.specs[0].plugin, "synt-xerror/welcome");
|
|
398
|
+
});
|
|
399
|
+
test("splits the inline owner/repo.fn form before normalization", async () => {
|
|
400
|
+
await fs.writeFile(commandsFile, `
|
|
401
|
+
hello:
|
|
402
|
+
cmd: hello
|
|
403
|
+
plugin: synt-xerror/welcome.ping
|
|
404
|
+
`, "utf8");
|
|
405
|
+
const config = await loadCommandsConfig(new Set(["synt-xerror/welcome", "de/welcome-test"]));
|
|
406
|
+
assert.ok(config);
|
|
407
|
+
assert.equal(config.specs[0].plugin, "synt-xerror/welcome");
|
|
408
|
+
assert.deepEqual(config.specs[0].functions, ["ping"]);
|
|
409
|
+
});
|
|
410
|
+
test("splits the inline bare-name.fn form before normalization", async () => {
|
|
411
|
+
await fs.writeFile(commandsFile, `
|
|
412
|
+
hello:
|
|
413
|
+
cmd: hello
|
|
414
|
+
plugin: welcome.ping
|
|
415
|
+
`, "utf8");
|
|
416
|
+
const config = await loadCommandsConfig(new Set(["synt-xerror/welcome"]));
|
|
417
|
+
assert.ok(config);
|
|
418
|
+
assert.equal(config.specs[0].plugin, "synt-xerror/welcome");
|
|
419
|
+
assert.deepEqual(config.specs[0].functions, ["ping"]);
|
|
420
|
+
});
|
|
421
|
+
test("splits core.fn items in functions lists", async () => {
|
|
422
|
+
await fs.writeFile(commandsFile, `
|
|
423
|
+
hello:
|
|
424
|
+
cmd: hello
|
|
425
|
+
functions: [core.greet, core.reply]
|
|
426
|
+
`, "utf8");
|
|
427
|
+
const config = await loadCommandsConfig(new Set(["core"]));
|
|
428
|
+
assert.ok(config);
|
|
429
|
+
assert.equal(config.specs[0].plugin, "core");
|
|
430
|
+
assert.deepEqual(config.specs[0].functions, ["greet", "reply"]);
|
|
431
|
+
});
|
|
432
|
+
test("splits canonical owner/plugin.fn items in subcommands", async () => {
|
|
433
|
+
await fs.writeFile(commandsFile, `
|
|
434
|
+
figurinha:
|
|
435
|
+
cmd: f
|
|
436
|
+
subcommands:
|
|
437
|
+
- cmd: criar
|
|
438
|
+
functions: [synt-xerror/figurinha.validarMidia, synt-xerror/figurinha.criarFigurinha]
|
|
439
|
+
`, "utf8");
|
|
440
|
+
const config = await loadCommandsConfig(new Set(["synt-xerror/figurinha"]));
|
|
441
|
+
assert.ok(config);
|
|
442
|
+
assert.equal(config.specs[0].plugin, "synt-xerror/figurinha");
|
|
443
|
+
assert.deepEqual(config.specs[0].subcommands[0].functions, [
|
|
444
|
+
"validarMidia",
|
|
445
|
+
"criarFigurinha",
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
test("keeps an unknown bare name verbatim (caller surfaces the miss)", async () => {
|
|
449
|
+
await fs.writeFile(commandsFile, `
|
|
450
|
+
hello:
|
|
451
|
+
cmd: hello
|
|
452
|
+
plugin: unknown-plugin
|
|
453
|
+
functions: [greet]
|
|
454
|
+
`, "utf8");
|
|
455
|
+
const config = await loadCommandsConfig(new Set(["synt-xerror/welcome", "de/welcome-test"]));
|
|
456
|
+
assert.ok(config);
|
|
457
|
+
assert.equal(config.specs[0].plugin, "unknown-plugin");
|
|
458
|
+
});
|
|
459
|
+
test("keeps a name verbatim when more than one owner matches (ambiguity surfaces at dispatch)", async () => {
|
|
460
|
+
await fs.writeFile(commandsFile, `
|
|
461
|
+
hello:
|
|
462
|
+
cmd: hello
|
|
463
|
+
plugin: shared
|
|
464
|
+
functions: [greet]
|
|
465
|
+
`, "utf8");
|
|
466
|
+
const config = await loadCommandsConfig(new Set(["alice/shared", "bob/shared"]));
|
|
467
|
+
assert.ok(config);
|
|
468
|
+
assert.equal(config.specs[0].plugin, "shared");
|
|
469
|
+
});
|
|
470
|
+
test("without validPluginKeys the value is used verbatim (legacy path)", async () => {
|
|
471
|
+
await fs.writeFile(commandsFile, `
|
|
472
|
+
hello:
|
|
473
|
+
cmd: hello
|
|
474
|
+
plugin: welcome
|
|
475
|
+
functions: [greet]
|
|
476
|
+
`, "utf8");
|
|
477
|
+
const config = await loadCommandsConfig();
|
|
478
|
+
assert.ok(config);
|
|
479
|
+
assert.equal(config.specs[0].plugin, "welcome");
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
@@ -75,10 +75,10 @@ function randomInt(min, max) {
|
|
|
75
75
|
* failure would be mistaken for a real save and never retried.
|
|
76
76
|
*/
|
|
77
77
|
async function addContact(contract, jid, name) {
|
|
78
|
-
// `jid` here is
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
78
|
+
// `jid` here is LID-canonical (see getMsgSender()) — Baileys needs the
|
|
79
|
+
// real wire JID or it silently no-ops instead of throwing, which is
|
|
80
|
+
// exactly how this went unnoticed before. toWireJid() passes @lid
|
|
81
|
+
// through unchanged, so no extra handling is needed here.
|
|
82
82
|
const wireJid = toWireJid(jid);
|
|
83
83
|
try {
|
|
84
84
|
await contract.addOrEditContact(wireJid, {
|
|
@@ -101,7 +101,7 @@ async function addContact(contract, jid, name) {
|
|
|
101
101
|
*
|
|
102
102
|
* @param {WaContract} contract
|
|
103
103
|
* @param {BotMessage} msg — driver-neutral incoming message envelope
|
|
104
|
-
* @param {string}
|
|
104
|
+
* @param {string|null} senderJid — normalized sender JID (LID-canonical; never a group JID), or null when no LID is known yet for this contact — those messages are skipped, since there's no stable id to track progress against
|
|
105
105
|
* @param {boolean} isGroup — whether this message came from a group chat
|
|
106
106
|
* @param {boolean} triggeredBot — true if this message invoked the bot
|
|
107
107
|
* (command prefix). Ignored outside groups.
|
|
@@ -109,7 +109,7 @@ async function addContact(contract, jid, name) {
|
|
|
109
109
|
export async function trackIncomingForContactSave(contract, msg, senderJid, isGroup, triggeredBot) {
|
|
110
110
|
try {
|
|
111
111
|
const pushName = msg.pushName?.trim();
|
|
112
|
-
if (!pushName || senderJid.endsWith("@g.us"))
|
|
112
|
+
if (!pushName || !senderJid || senderJid.endsWith("@g.us"))
|
|
113
113
|
return;
|
|
114
114
|
let s = getSenderState(senderJid);
|
|
115
115
|
// Completing a refresh takes priority over new-save logic, but in
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import test, { describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { trackIncomingForContactSave } from "#kernel/contactAutoSave.js";
|
|
4
|
+
function createMockContract() {
|
|
5
|
+
const saved = [];
|
|
6
|
+
return {
|
|
7
|
+
savedContacts: saved,
|
|
8
|
+
addOrEditContact: async (jid, info) => {
|
|
9
|
+
saved.push({ jid, name: info.fullName || "" });
|
|
10
|
+
},
|
|
11
|
+
name: "baileys",
|
|
12
|
+
isReady: () => true,
|
|
13
|
+
connect: async () => { },
|
|
14
|
+
disconnect: async () => { },
|
|
15
|
+
me: () => ({ id: "123@c.us" }),
|
|
16
|
+
sendText: async () => ({ id: "msg", chatId: "123@c.us", timestamp: Date.now() }),
|
|
17
|
+
sendImage: async () => ({ id: "img", chatId: "123@c.us", timestamp: Date.now() }),
|
|
18
|
+
sendVideo: async () => ({ id: "vid", chatId: "123@c.us", timestamp: Date.now() }),
|
|
19
|
+
sendAudio: async () => ({ id: "aud", chatId: "123@c.us", timestamp: Date.now() }),
|
|
20
|
+
sendDocument: async () => ({ id: "doc", chatId: "123@c.us", timestamp: Date.now() }),
|
|
21
|
+
sendSticker: async () => ({ id: "stk", chatId: "123@c.us", timestamp: Date.now() }),
|
|
22
|
+
sendLocation: async () => ({ id: "loc", chatId: "123@c.us", timestamp: Date.now() }),
|
|
23
|
+
sendContact: async () => ({ id: "cnt", chatId: "123@c.us", timestamp: Date.now() }),
|
|
24
|
+
sendReaction: async () => { },
|
|
25
|
+
sendPoll: async () => ({ id: "pll", chatId: "123@c.us", timestamp: Date.now() }),
|
|
26
|
+
react: async () => { },
|
|
27
|
+
deleteMessage: async () => { },
|
|
28
|
+
editMessage: async () => { },
|
|
29
|
+
sendPresenceUpdate: async () => { },
|
|
30
|
+
readMessages: async () => { },
|
|
31
|
+
onWhatsApp: async () => null,
|
|
32
|
+
getBusinessProfile: async () => null,
|
|
33
|
+
profilePictureUrl: async () => null,
|
|
34
|
+
fetchStatus: async () => null,
|
|
35
|
+
updateBlockStatus: async () => { },
|
|
36
|
+
removeContact: async () => { },
|
|
37
|
+
groupMetadata: async () => ({ subject: "Test Group", participants: [] }),
|
|
38
|
+
groupParticipantsUpdate: async () => [],
|
|
39
|
+
groupUpdateSubject: async () => { },
|
|
40
|
+
groupUpdateDescription: async () => { },
|
|
41
|
+
groupInviteCode: async () => "",
|
|
42
|
+
groupRevokeInvite: async () => "",
|
|
43
|
+
updateProfilePicture: async () => { },
|
|
44
|
+
updateProfileName: async () => { },
|
|
45
|
+
updateProfileStatus: async () => { },
|
|
46
|
+
downloadMedia: async () => null,
|
|
47
|
+
on: () => () => { },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
describe("kernel/contactAutoSave", () => {
|
|
51
|
+
test("accumulates DM messages and triggers addOrEditContact when target reached", async () => {
|
|
52
|
+
const contract = createMockContract();
|
|
53
|
+
const jid = "5511999991111@c.us";
|
|
54
|
+
const msg = {
|
|
55
|
+
id: "1",
|
|
56
|
+
chatId: jid,
|
|
57
|
+
fromMe: false,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
text: "hello",
|
|
60
|
+
pushName: "Alice",
|
|
61
|
+
raw: {},
|
|
62
|
+
};
|
|
63
|
+
// Send up to 6 DM messages (max target range is 3-6)
|
|
64
|
+
for (let i = 0; i < 6; i++) {
|
|
65
|
+
await trackIncomingForContactSave(contract, msg, jid, false, false);
|
|
66
|
+
}
|
|
67
|
+
assert.ok(contract.savedContacts.length > 0);
|
|
68
|
+
assert.equal(contract.savedContacts[0].name, "Alice");
|
|
69
|
+
});
|
|
70
|
+
test("ignores silent group messages (triggeredBot = false)", async () => {
|
|
71
|
+
const contract = createMockContract();
|
|
72
|
+
const jid = "5511999992222@c.us";
|
|
73
|
+
const msg = {
|
|
74
|
+
id: "2",
|
|
75
|
+
chatId: "group1@g.us",
|
|
76
|
+
fromMe: false,
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
text: "just chatting",
|
|
79
|
+
pushName: "Bob",
|
|
80
|
+
raw: {},
|
|
81
|
+
};
|
|
82
|
+
for (let i = 0; i < 10; i++) {
|
|
83
|
+
await trackIncomingForContactSave(contract, msg, jid, true, false);
|
|
84
|
+
}
|
|
85
|
+
assert.equal(contract.savedContacts.length, 0);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coreCommands.ts
|
|
3
|
+
*
|
|
4
|
+
* Built-in command handlers live in the kernel namespace, independently of
|
|
5
|
+
* the external plugin registry and manyplug configuration.
|
|
6
|
+
*/
|
|
7
|
+
// Mirrors src/locales/*.json — the only languages with a translation file.
|
|
8
|
+
const AVAILABLE_LOCALES = ["pt", "en", "es"];
|
|
9
|
+
const handlers = {
|
|
10
|
+
ping: async (ctx) => {
|
|
11
|
+
await ctx.send.text("pong");
|
|
12
|
+
},
|
|
13
|
+
status: async (ctx) => {
|
|
14
|
+
await ctx.send.text("ManyBot está online.");
|
|
15
|
+
},
|
|
16
|
+
// `!configurar` / `!config` / `!cfg` (no subcommand) — shows the
|
|
17
|
+
// current per-chat overrides. Persisted via ctx.settings, which
|
|
18
|
+
// (per pluginApi.ts) is already scoped to plugin "core" + the
|
|
19
|
+
// current chat — same storage pattern already used for
|
|
20
|
+
// last_welcome_seen in commandMenu.ts.
|
|
21
|
+
setChatConfig: async (ctx) => {
|
|
22
|
+
const pctx = ctx;
|
|
23
|
+
const prefix = pctx.settings.get("chat_prefix", null);
|
|
24
|
+
const locale = pctx.settings.get("chat_locale", null);
|
|
25
|
+
await pctx.send.text("⚙️ Configuração deste chat:\n" +
|
|
26
|
+
`• Prefixo: ${prefix ?? "(padrão)"}\n` +
|
|
27
|
+
`• Idioma: ${locale ?? "(padrão)"}\n\n` +
|
|
28
|
+
"Use !config prefixo <novo> ou !config idioma <pt|en|es> para alterar.");
|
|
29
|
+
},
|
|
30
|
+
// `!config prefixo <novo>`
|
|
31
|
+
setChatPrefix: async (ctx, input) => {
|
|
32
|
+
const pctx = ctx;
|
|
33
|
+
const value = input?.args?.[0];
|
|
34
|
+
if (!value || value.length > 5) {
|
|
35
|
+
await pctx.send.text("Uso: !config prefixo <novo prefixo> (até 5 caracteres)");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
pctx.settings.set("chat_prefix", value);
|
|
39
|
+
await pctx.send.text(`✅ Prefixo salvo como "${value}" para este chat.\n` +
|
|
40
|
+
`A partir de agora, use "${value}" em vez do prefixo padrão neste chat.`);
|
|
41
|
+
},
|
|
42
|
+
// `!config idioma <pt|en|es>`
|
|
43
|
+
setChatLocale: async (ctx, input) => {
|
|
44
|
+
const pctx = ctx;
|
|
45
|
+
const value = input?.args?.[0]?.toLowerCase();
|
|
46
|
+
if (!value || !AVAILABLE_LOCALES.includes(value)) {
|
|
47
|
+
await pctx.send.text(`Uso: !config idioma <${AVAILABLE_LOCALES.join("|")}>`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
pctx.settings.set("chat_locale", value);
|
|
51
|
+
await pctx.send.text(`✅ Idioma salvo como "${value}" para este chat.\n` +
|
|
52
|
+
"As mensagens do sistema de comandos (menu, permissões, avisos de uso) passam a usar esse idioma neste chat.");
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
export function resolveCoreCommandHandler(name) {
|
|
56
|
+
return handlers[name] ?? (async () => {
|
|
57
|
+
throw new Error(`Core handler "${name}" is not registered`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function registerCoreCommand(name, handler) {
|
|
61
|
+
handlers[name] = handler;
|
|
62
|
+
}
|