@manybot/manybot 5.8.0 → 5.9.1
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 +15 -7
- package/dist/client/store.js +35 -1
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +75 -8
- 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 +245 -51
- package/dist/drivers/baileys/index.js +30 -6
- package/dist/drivers/baileys/messageHandler.js +207 -21
- package/dist/drivers/baileys/messageHandler.test.js +256 -14
- package/dist/drivers/baileysAdapter.test.js +97 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +35 -1
- package/dist/i18n/index.js +5 -22
- package/dist/kernel/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- package/dist/kernel/commandAccess.test.js +2 -2
- package/dist/kernel/commandDeprecation.js +4 -2
- package/dist/kernel/commandDeprecation.test.js +8 -1
- package/dist/kernel/commandMenu.js +91 -2
- package/dist/kernel/commandMenu.test.js +131 -2
- package/dist/kernel/commandPermissions.js +69 -23
- package/dist/kernel/commandPermissions.test.js +77 -9
- package/dist/kernel/commandRegistry.js +167 -43
- package/dist/kernel/commandRegistry.test.js +4 -2
- package/dist/kernel/commandsConfig.js +470 -38
- package/dist/kernel/commandsConfig.test.js +249 -3
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/coreCommands.js +62 -0
- package/dist/kernel/pluginApi.test.js +20 -3
- package/dist/kernel/pluginGuard.js +5 -3
- package/dist/kernel/pluginLoader.js +73 -10
- package/dist/kernel/pluginLoader.test.js +111 -1
- package/dist/kernel/runCommand.js +57 -18
- package/dist/kernel/runCommand.test.js +269 -7
- package/dist/kernel/settingsDb.js +15 -2
- package/dist/kernel/testConfig.js +9 -0
- package/dist/locales/en.json +14 -1
- package/dist/locales/es.json +17 -4
- package/dist/locales/pt.json +18 -5
- package/dist/plugins/__manybot_integration__/index.js +33 -16
- package/dist/plugins/__manybot_integration__/index.test.js +42 -8
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +4 -3
|
@@ -32,6 +32,7 @@ describe("kernel/commandsConfig", () => {
|
|
|
32
32
|
assert.ok(config);
|
|
33
33
|
assert.equal(config.defaults.notifyChanges, true);
|
|
34
34
|
assert.equal(config.defaults.notifyPeriodDays, 7);
|
|
35
|
+
assert.equal(config.menu.enabled, false);
|
|
35
36
|
assert.deepEqual(config.menu.aliases, ["help", "man", "menu", "bot", "?"]);
|
|
36
37
|
assert.deepEqual(config.specs, []);
|
|
37
38
|
});
|
|
@@ -109,16 +110,36 @@ missingCmd:
|
|
|
109
110
|
cooldownSeconds: 4,
|
|
110
111
|
whitelist: { groups: ["group@g.us"], users: ["user@c.us"] },
|
|
111
112
|
blacklist: undefined,
|
|
113
|
+
dono: undefined,
|
|
114
|
+
allowedChats: undefined,
|
|
115
|
+
groupOnly: undefined,
|
|
116
|
+
dmOnly: undefined,
|
|
117
|
+
whitelistGroups: undefined,
|
|
118
|
+
blacklistUsers: undefined,
|
|
119
|
+
hiddenOutsideScope: undefined,
|
|
112
120
|
},
|
|
113
|
-
messages: {
|
|
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,
|
|
114
132
|
});
|
|
115
133
|
assert.deepEqual(config.menu, {
|
|
134
|
+
enabled: true,
|
|
116
135
|
title: { en: "Commands", pt: "Comandos" },
|
|
117
136
|
intro: "Intro",
|
|
118
137
|
footer: "Footer",
|
|
119
138
|
cmd: "menu",
|
|
120
139
|
aliases: ["help", "?"],
|
|
121
140
|
notFoundFallback: true,
|
|
141
|
+
suggestSimilar: false,
|
|
142
|
+
suggestMaxDistance: 2,
|
|
122
143
|
welcomeMessage: null,
|
|
123
144
|
welcomeWindowDays: 3,
|
|
124
145
|
pageSize: 15,
|
|
@@ -134,7 +155,8 @@ missingCmd:
|
|
|
134
155
|
cmd: "hello",
|
|
135
156
|
aliases: ["hi", "oi"],
|
|
136
157
|
plugin: "sample",
|
|
137
|
-
|
|
158
|
+
functions: ["greet"],
|
|
159
|
+
loading: null,
|
|
138
160
|
text: "Reply from file",
|
|
139
161
|
desc: { en: "Say hello", pt: "Diga oi" },
|
|
140
162
|
category: "fun",
|
|
@@ -150,8 +172,24 @@ missingCmd:
|
|
|
150
172
|
cooldownSeconds: 0,
|
|
151
173
|
whitelist: undefined,
|
|
152
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,
|
|
153
192
|
},
|
|
154
|
-
messages: { botNotAdmin: undefined, senderNotAdmin: undefined, ownerOnly: "Owners only", wrongScope: undefined, cooldown: undefined },
|
|
155
193
|
arguments: [],
|
|
156
194
|
subcommands: [],
|
|
157
195
|
});
|
|
@@ -233,4 +271,212 @@ hello:
|
|
|
233
271
|
assert.equal(config.specs.length, 1);
|
|
234
272
|
assert.equal(config.specs[0].id, "hello");
|
|
235
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
|
+
});
|
|
236
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,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
|
+
}
|
|
@@ -267,7 +267,7 @@ describe("kernel/pluginApi — buildSetupApi with Mock WaContract", () => {
|
|
|
267
267
|
assert.ok(ctx.chats);
|
|
268
268
|
assert.ok(ctx.contacts);
|
|
269
269
|
assert.ok(ctx.storage);
|
|
270
|
-
assert.equal(ctx.botId, "
|
|
270
|
+
assert.equal(ctx.botId, "99999@lid");
|
|
271
271
|
// Setup send only has .to()
|
|
272
272
|
assert.ok(ctx.send.to);
|
|
273
273
|
assert.equal(typeof ctx.send.to, "function");
|
|
@@ -371,13 +371,25 @@ describe("kernel/pluginApi — buildApi (Runtime) with Mock WaContract", () => {
|
|
|
371
371
|
await ctx.admin.kick("5516777777777@s.whatsapp.net");
|
|
372
372
|
assert.equal(calls.groupParticipantUpdates.length, 2);
|
|
373
373
|
assert.equal(calls.groupParticipantUpdates[1].action, "remove");
|
|
374
|
+
await assert.rejects(() => ctx.admin.kick("5516999999999@s.whatsapp.net"));
|
|
375
|
+
assert.equal(calls.groupParticipantUpdates.length, 2);
|
|
374
376
|
const inviteLink = await ctx.admin.getInviteLink();
|
|
375
377
|
assert.match(inviteLink, /chat\.whatsapp\.com\/mock-invite-code-123/);
|
|
376
378
|
// Contacts helper
|
|
377
379
|
const contact = await ctx.contacts.get("5516999999999@s.whatsapp.net");
|
|
378
380
|
assert.ok(contact);
|
|
379
|
-
|
|
381
|
+
// normalizeContact now returns E.164 (with leading "+") per the
|
|
382
|
+
// new contract invariant; assert accordingly.
|
|
383
|
+
assert.equal(contact?.number, "+5516999999999");
|
|
380
384
|
assert.equal(contact?.isMe, true);
|
|
385
|
+
// Looking up the bot by its own LID (ctx.botId) must resolve the
|
|
386
|
+
// same identity — number populated via the proactively-learned
|
|
387
|
+
// LID↔PN mapping, not left null for lack of a resolveLid() hit.
|
|
388
|
+
const selfByLid = await ctx.contacts.get(ctx.botId);
|
|
389
|
+
assert.ok(selfByLid);
|
|
390
|
+
assert.equal(selfByLid?.id, "99999@lid");
|
|
391
|
+
assert.equal(selfByLid?.number, "+5516999999999");
|
|
392
|
+
assert.equal(selfByLid?.isMe, true);
|
|
381
393
|
await ctx.contacts.block("5516777777777@s.whatsapp.net");
|
|
382
394
|
assert.equal(calls.blockUpdates.length, 1);
|
|
383
395
|
assert.equal(calls.blockUpdates[0].action, "block");
|
|
@@ -578,6 +590,11 @@ describe("kernel/pluginApi — buildApi (Runtime) with Mock WaContract", () => {
|
|
|
578
590
|
// contacts.get() with a raw @lid routes through contract.resolveLid()
|
|
579
591
|
// (mock: "12345@lid" -> "5516777777777@s.whatsapp.net").
|
|
580
592
|
const contact = await ctx.contacts.get("12345@lid");
|
|
581
|
-
|
|
593
|
+
// normalizeContact now keeps the ID as the LID form ("12345@lid")
|
|
594
|
+
// when known, per the invariant that user IDs are LID-or-null; plugins
|
|
595
|
+
// should treat contact.id as the canonical JID for addressing and
|
|
596
|
+
// contact.number/contact.numberRaw as the dialable string.
|
|
597
|
+
assert.equal(contact?.id, "12345@lid");
|
|
598
|
+
assert.equal(contact?.number, "+5516777777777");
|
|
582
599
|
});
|
|
583
600
|
});
|
|
@@ -38,18 +38,19 @@ function withTimeout(promise, ms, pluginName) {
|
|
|
38
38
|
}
|
|
39
39
|
export async function runPlugin(plugin, context, handler, input, options) {
|
|
40
40
|
if (plugin.status !== "active")
|
|
41
|
-
return;
|
|
41
|
+
return undefined;
|
|
42
42
|
const useTimeout = plugin.guardOptions?.timeout !== false;
|
|
43
43
|
try {
|
|
44
44
|
if (handler) {
|
|
45
45
|
const run = handler(context, input);
|
|
46
|
-
await (useTimeout ? withTimeout(run, PLUGIN_TIMEOUT_MS, plugin.name) : run);
|
|
46
|
+
return await (useTimeout ? withTimeout(run, PLUGIN_TIMEOUT_MS, plugin.name) : run);
|
|
47
47
|
}
|
|
48
48
|
else {
|
|
49
49
|
if (!plugin.run)
|
|
50
|
-
return;
|
|
50
|
+
return undefined;
|
|
51
51
|
const run = plugin.run(context);
|
|
52
52
|
await (useTimeout ? withTimeout(run, PLUGIN_TIMEOUT_MS, plugin.name) : run);
|
|
53
|
+
return undefined;
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
catch (e) {
|
|
@@ -87,5 +88,6 @@ export async function runPlugin(plugin, context, handler, input, options) {
|
|
|
87
88
|
});
|
|
88
89
|
});
|
|
89
90
|
}
|
|
91
|
+
return undefined;
|
|
90
92
|
}
|
|
91
93
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. Loading each plugin from ~/.manybot/plugins folder
|
|
7
7
|
* 3. Registering in pluginRegistry with status and public exports
|
|
8
8
|
* 4. Exposing pluginRegistry to kernel and pluginApi
|
|
9
|
-
* 5. Watching plugin files and
|
|
9
|
+
* 5. Watching plugin files, config files and commands.yaml for hot reloading
|
|
10
10
|
*/
|
|
11
11
|
import fs from "fs";
|
|
12
12
|
import path from "path";
|
|
@@ -33,6 +33,23 @@ const PLUGINS_DIR = path.join(PATHS.HOME, "plugins");
|
|
|
33
33
|
export const pluginRegistry = new Map();
|
|
34
34
|
let globalContract = null;
|
|
35
35
|
let globalStore = null;
|
|
36
|
+
/**
|
|
37
|
+
* Live WaContract + BotStore the kernel most-recently wired through
|
|
38
|
+
* `setupPlugins()`. Exposed for the integration-test harness only
|
|
39
|
+
* (see `src/plugins/__manybot_integration__/` and the
|
|
40
|
+
* `*.integration.test.ts` suite under the project root): production
|
|
41
|
+
* code paths go through `ctx.wa.contract` / `ctx.store`, not through
|
|
42
|
+
* this module-scoped singleton.
|
|
43
|
+
*
|
|
44
|
+
* Returns `null` when the bot hasn't connected yet (or after a
|
|
45
|
+
* disconnect). The integration suite waits for a non-null value
|
|
46
|
+
* before issuing any real round-trip.
|
|
47
|
+
*/
|
|
48
|
+
export function getGlobalKernelRefs() {
|
|
49
|
+
if (!globalContract || !globalStore)
|
|
50
|
+
return null;
|
|
51
|
+
return { contract: globalContract, store: globalStore };
|
|
52
|
+
}
|
|
36
53
|
const pluginWatchers = new Map();
|
|
37
54
|
// fs.watch's `recursive: true` emulates recursion on Linux by opening one
|
|
38
55
|
// inotify watch per subdirectory — a plugin shipping its own node_modules
|
|
@@ -335,22 +352,68 @@ export function unwatchPlugin(name) {
|
|
|
335
352
|
}
|
|
336
353
|
}
|
|
337
354
|
/**
|
|
338
|
-
*
|
|
355
|
+
* Reload the command registry from disk (commands.yaml + imports).
|
|
356
|
+
*
|
|
357
|
+
* Mirrors `reloadPlugin()`: callable on its own and safe to invoke from
|
|
358
|
+
* a watcher. Unlike `reloadPlugin()` there's no setup retry / disable
|
|
359
|
+
* state to track — `initCommandRegistry()` either builds a fresh
|
|
360
|
+
* `CommandRegistry` from the current file content or leaves the
|
|
361
|
+
* previous one in place via `commandsConfig.ts`'s own error handling
|
|
362
|
+
* (a malformed YAML logs an error and returns null; `initCommandRegistry`
|
|
363
|
+
* then falls back to the empty defaults).
|
|
364
|
+
*/
|
|
365
|
+
export async function reloadCommandRegistry() {
|
|
366
|
+
try {
|
|
367
|
+
await initCommandRegistry();
|
|
368
|
+
logger.info(`[watcher] Command registry reloaded from commands.yaml.`);
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
372
|
+
logger.error(`[watcher] Failed to reload command registry: ${err.message}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Watch the config directory for changes to manyplug.toml / manybot.toml
|
|
377
|
+
* (plugin list sync) or to commands.yaml and its .yaml/.yml imports
|
|
378
|
+
* (command registry reload). Both paths share the same 500ms debounce
|
|
379
|
+
* so a save that emits several inotify events only fires once.
|
|
339
380
|
*/
|
|
340
381
|
function startConfigWatcher() {
|
|
341
382
|
if (configWatcher)
|
|
342
383
|
return;
|
|
384
|
+
const isTomlChange = (filename) => filename === "manyplug.toml" || filename === "manybot.toml";
|
|
385
|
+
const isYamlChange = (filename) => {
|
|
386
|
+
if (!filename)
|
|
387
|
+
return false;
|
|
388
|
+
const lower = filename.toLowerCase();
|
|
389
|
+
return lower.endsWith(".yaml") || lower.endsWith(".yml");
|
|
390
|
+
};
|
|
343
391
|
try {
|
|
344
392
|
let configTimeout = null;
|
|
393
|
+
let yamlTimeout = null;
|
|
394
|
+
const scheduleTomlReload = (filename) => {
|
|
395
|
+
if (!isTomlChange(filename))
|
|
396
|
+
return;
|
|
397
|
+
if (configTimeout)
|
|
398
|
+
clearTimeout(configTimeout);
|
|
399
|
+
configTimeout = setTimeout(async () => {
|
|
400
|
+
logger.info(`[watcher] Config file change detected: ${filename}. Syncing plugins...`);
|
|
401
|
+
await syncPlugins();
|
|
402
|
+
}, 500);
|
|
403
|
+
};
|
|
404
|
+
const scheduleYamlReload = (filename) => {
|
|
405
|
+
if (!isYamlChange(filename))
|
|
406
|
+
return;
|
|
407
|
+
if (yamlTimeout)
|
|
408
|
+
clearTimeout(yamlTimeout);
|
|
409
|
+
yamlTimeout = setTimeout(async () => {
|
|
410
|
+
logger.info(`[watcher] commands.yaml change detected (${filename}). Reloading command registry...`);
|
|
411
|
+
await reloadCommandRegistry();
|
|
412
|
+
}, 500);
|
|
413
|
+
};
|
|
345
414
|
configWatcher = fs.watch(PATHS.HOME, (eventType, filename) => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
clearTimeout(configTimeout);
|
|
349
|
-
configTimeout = setTimeout(async () => {
|
|
350
|
-
logger.info(`[watcher] Config file change detected: ${filename}. Syncing plugins...`);
|
|
351
|
-
await syncPlugins();
|
|
352
|
-
}, 500);
|
|
353
|
-
}
|
|
415
|
+
scheduleTomlReload(filename);
|
|
416
|
+
scheduleYamlReload(filename);
|
|
354
417
|
});
|
|
355
418
|
}
|
|
356
419
|
catch (e) {
|