@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
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
* 3. Pass context to all active plugins
|
|
11
11
|
*
|
|
12
12
|
* Each plugin decides whether to act or ignore.
|
|
13
|
+
*
|
|
14
|
+
* v6 loading indicators: a matched command may declare a `loading:` spec
|
|
15
|
+
* (or reference a top-level `loading_presets:` preset) which controls
|
|
16
|
+
* the user-visible "processando..." signal — `reaction`, `typing`,
|
|
17
|
+
* `recording_audio`, `spinner`, or `none`. The spec is resolved at
|
|
18
|
+
* registry build time (defaults → category → command → sub), so by the
|
|
19
|
+
* time the dispatch path reads it we only have to apply it.
|
|
13
20
|
*/
|
|
14
|
-
import { CHATS, EXCLUDE_CHATS
|
|
21
|
+
import { CHATS, EXCLUDE_CHATS } from "#config";
|
|
22
|
+
import { getChatPrefix, getChatLocale } from "#kernel/chatOverrides.js";
|
|
15
23
|
import { buildApi, buildChatFromMsg, buildMessageContext } from "./api/index.js";
|
|
16
24
|
import { pluginRegistry } from "#kernel/pluginLoader.js";
|
|
17
25
|
import { getCommandByInvocation, getCommandRegistry } from "#kernel/commandRegistry.js";
|
|
@@ -36,6 +44,122 @@ function extractCommand(body, prefix) {
|
|
|
36
44
|
const first = body.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
37
45
|
return first.startsWith(prefix) ? first.slice(prefix.length) : "";
|
|
38
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Drive the per-command loading indicator described by the registry.
|
|
49
|
+
*
|
|
50
|
+
* - `reaction` : drop an emoji reaction on the source message,
|
|
51
|
+
* update with onSuccess/onError on completion.
|
|
52
|
+
* - `typing` : native WhatsApp "typing..." presence, refreshed
|
|
53
|
+
* on a 4s interval; cleared on completion.
|
|
54
|
+
* - `recording_audio` : native WhatsApp "recording audio..." presence,
|
|
55
|
+
* same interval/clear semantics as typing.
|
|
56
|
+
* - `spinner` : self-sent message showing a frame sequence,
|
|
57
|
+
* edited every `intervalMs` (default 1500); last
|
|
58
|
+
* frame stays on completion.
|
|
59
|
+
* - `none` : no-op — caller doesn't see a difference.
|
|
60
|
+
*
|
|
61
|
+
* All best-effort: any driver-level failure is logged at `warn` level
|
|
62
|
+
* and swallowed so a broken indicator never breaks a command.
|
|
63
|
+
*/
|
|
64
|
+
function startLoadingIndicator(spec, contract, rawJid, msgKey) {
|
|
65
|
+
const noop = async () => { };
|
|
66
|
+
if (!spec)
|
|
67
|
+
return { stop: noop };
|
|
68
|
+
if (spec.type === "reaction") {
|
|
69
|
+
const icon = spec.icon ?? "⏳";
|
|
70
|
+
if (msgKey) {
|
|
71
|
+
contract.react(rawJid, msgKey, icon).catch((e) => {
|
|
72
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
73
|
+
logger.warn(`[messageHandler] loading.reaction send failed: ${err.message}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
stop: async (outcome) => {
|
|
78
|
+
if (!msgKey)
|
|
79
|
+
return;
|
|
80
|
+
const next = outcome === "success" ? spec.onSuccess : spec.onError;
|
|
81
|
+
try {
|
|
82
|
+
await contract.react(rawJid, msgKey, next ?? "");
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
86
|
+
logger.warn(`[messageHandler] loading.reaction clear failed: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (spec.type === "typing" || spec.type === "recording_audio") {
|
|
92
|
+
const presence = spec.type === "typing" ? "composing" : "recording";
|
|
93
|
+
const interval = setInterval(() => {
|
|
94
|
+
contract.sendPresenceUpdate(presence, rawJid).catch(() => { });
|
|
95
|
+
}, 4000);
|
|
96
|
+
contract.sendPresenceUpdate(presence, rawJid).catch(() => { });
|
|
97
|
+
return {
|
|
98
|
+
stop: async () => {
|
|
99
|
+
clearInterval(interval);
|
|
100
|
+
contract.sendPresenceUpdate("paused", rawJid).catch(() => { });
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (spec.type === "spinner") {
|
|
105
|
+
const frames = spec.frames && spec.frames.length > 0 ? spec.frames : ["⏳"];
|
|
106
|
+
const intervalMs = Math.max(1000, spec.intervalMs ?? 1500);
|
|
107
|
+
let frameIdx = 0;
|
|
108
|
+
let sentMsgId = null;
|
|
109
|
+
const cycle = async () => {
|
|
110
|
+
try {
|
|
111
|
+
if (sentMsgId === null) {
|
|
112
|
+
const sent = await contract.sendText(rawJid, frames[0]);
|
|
113
|
+
sentMsgId = sent.id;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
117
|
+
await contract.editMessage(rawJid, { id: sentMsgId, remoteJid: rawJid, fromMe: true }, frames[frameIdx]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Spinner is best-effort — leave the previous frame in place
|
|
122
|
+
// when an edit fails rather than aborting the whole indicator.
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
cycle().catch(() => { });
|
|
126
|
+
const interval = setInterval(() => { cycle().catch(() => { }); }, intervalMs);
|
|
127
|
+
return {
|
|
128
|
+
stop: async (outcome) => {
|
|
129
|
+
clearInterval(interval);
|
|
130
|
+
if (sentMsgId !== null) {
|
|
131
|
+
const finalText = outcome === "success" ? spec.onSuccess : spec.onError;
|
|
132
|
+
try {
|
|
133
|
+
if (finalText !== undefined) {
|
|
134
|
+
await contract.editMessage(rawJid, { id: sentMsgId, remoteJid: rawJid, fromMe: true }, finalText);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
await contract.deleteMessage(rawJid, { id: sentMsgId, remoteJid: rawJid, fromMe: true }, true);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
142
|
+
logger.warn(`[messageHandler] loading.spinner finalize failed: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// `none` or any future variant — nothing to do.
|
|
149
|
+
return { stop: noop };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the loading spec for the matched command/sub. Subcommands win
|
|
153
|
+
* (their own chain inheritance ran at registry build time); falls back
|
|
154
|
+
* to the parent's resolved spec.
|
|
155
|
+
*/
|
|
156
|
+
function resolveLoadingForDispatch(entry, resolution) {
|
|
157
|
+
if (resolution.target.kind === "sub")
|
|
158
|
+
return resolution.target.sub.loading ?? entry.loading;
|
|
159
|
+
if (resolution.target.kind === "parent")
|
|
160
|
+
return entry.loading;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
39
163
|
// ── Dedup of already-processed messages ────────────────────────────────────
|
|
40
164
|
// WhatsApp resends messages without a delivery/read confirmation (the
|
|
41
165
|
// protocol's own retry, usually up to 3 times) when the socket reconnects.
|
|
@@ -107,18 +231,39 @@ export async function handleMessage(msg, contract, store) {
|
|
|
107
231
|
// Caps how many chats get answered at the same time — see SECURITY_LEVEL.
|
|
108
232
|
const releaseChatSlot = await acquireChatSlot(jid);
|
|
109
233
|
try {
|
|
110
|
-
await runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid);
|
|
234
|
+
await runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid, rawKey);
|
|
111
235
|
}
|
|
112
236
|
finally {
|
|
113
237
|
releaseChatSlot();
|
|
114
238
|
}
|
|
115
239
|
}
|
|
116
|
-
async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid) {
|
|
117
|
-
const
|
|
240
|
+
async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid, rawKey) {
|
|
241
|
+
const chatPrefix = getChatPrefix(msg.chatId);
|
|
242
|
+
const chatLocale = getChatLocale(msg.chatId);
|
|
243
|
+
const command = extractCommand(msgCtx.body, chatPrefix);
|
|
118
244
|
const registry = getCommandRegistry();
|
|
119
245
|
// 0. Welcome message (first message within the configured window)
|
|
120
|
-
|
|
121
|
-
|
|
246
|
+
//
|
|
247
|
+
// Two gates before we even consider firing the welcome:
|
|
248
|
+
//
|
|
249
|
+
// - `!msg.fromMe`: skip when the bot itself "sent" the message
|
|
250
|
+
// that triggered this dispatch. Baileys' history-sync replays
|
|
251
|
+
// the bot's own outgoing messages as `messages.upsert` with
|
|
252
|
+
// `fromMe=true` on every reconnect — without this gate, the
|
|
253
|
+
// bot would greet itself in its own DM/group on every restart
|
|
254
|
+
// the moment history-sync completes. The welcome is for
|
|
255
|
+
// *incoming* messages only.
|
|
256
|
+
//
|
|
257
|
+
// - `!chat.isGroup`: skip when the message arrived in a group.
|
|
258
|
+
// A new member joining a group shouldn't get a per-member
|
|
259
|
+
// welcome reply inside the group's conversation — it's noise
|
|
260
|
+
// and reads weird in front of everyone else. The welcome is
|
|
261
|
+
// only meaningful in a 1:1 (DM) chat where the message
|
|
262
|
+
// originates from the user being greeted, and where the reply
|
|
263
|
+
// goes back to the same chat (`msgCtx.reply` already targets
|
|
264
|
+
// the source chat).
|
|
265
|
+
if (registry && !msg.fromMe && !chat.isGroup) {
|
|
266
|
+
const welcomeMsg = checkAndTriggerWelcomeMessage(msgCtx.sender ?? msg.chatId, registry, { body: msgCtx.body, timestamp: msg.timestamp }, chatLocale, chatPrefix);
|
|
122
267
|
if (welcomeMsg) {
|
|
123
268
|
try {
|
|
124
269
|
await msgCtx.reply.text(welcomeMsg);
|
|
@@ -131,9 +276,9 @@ async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid)
|
|
|
131
276
|
}
|
|
132
277
|
// 1. Menu aliases match (overview / category / manual / notFound)
|
|
133
278
|
if (command && registry && registry.menuAliases.has(command)) {
|
|
134
|
-
const rawArgs = msgCtx.body.trim().slice(
|
|
279
|
+
const rawArgs = msgCtx.body.trim().slice(chatPrefix.length + command.length).trim();
|
|
135
280
|
const scope = chat.isGroup ? "group" : "dm";
|
|
136
|
-
const menuResponse = handleMenuCommand(command, rawArgs, registry,
|
|
281
|
+
const menuResponse = handleMenuCommand(command, rawArgs, registry, chatLocale, scope);
|
|
137
282
|
try {
|
|
138
283
|
await msgCtx.reply.text(menuResponse);
|
|
139
284
|
}
|
|
@@ -150,7 +295,7 @@ async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid)
|
|
|
150
295
|
// active in production. Kept separate from the `matched` top-level
|
|
151
296
|
// permission pre-check further down (unchanged, still gates the whole
|
|
152
297
|
// per-message plugin loop exactly as before `runCommand` existed).
|
|
153
|
-
const rawArgsForDispatch = command ? msgCtx.body.trim().slice(
|
|
298
|
+
const rawArgsForDispatch = command ? msgCtx.body.trim().slice(chatPrefix.length + command.length).trim() : "";
|
|
154
299
|
const resolution = command ? resolveDispatch(command, rawArgsForDispatch) : { target: { kind: "none" } };
|
|
155
300
|
if (matched) {
|
|
156
301
|
const permApi = buildApi({
|
|
@@ -165,7 +310,7 @@ async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid)
|
|
|
165
310
|
const permResult = await checkPermission(matched, {
|
|
166
311
|
isGroup: permApi.chat.isGroup,
|
|
167
312
|
chatId: permApi.chat.id,
|
|
168
|
-
|
|
313
|
+
sender: { lid: msgCtx.sender, pn: msgCtx.senderPn },
|
|
169
314
|
isSenderAdmin: () => permApi.chat.isSenderAdmin(),
|
|
170
315
|
isBotAdmin: () => permApi.chat.isBotAdmin(),
|
|
171
316
|
});
|
|
@@ -186,7 +331,7 @@ async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid)
|
|
|
186
331
|
// invoke any plugin (legacy or migrated) for this message.
|
|
187
332
|
if (matched && matched.source === "text" && matched.text !== null) {
|
|
188
333
|
try {
|
|
189
|
-
const textContent = resolveLocalizedString(matched.text) ?? "";
|
|
334
|
+
const textContent = resolveLocalizedString(matched.text, chatLocale) ?? "";
|
|
190
335
|
await msgCtx.reply.text(textContent);
|
|
191
336
|
}
|
|
192
337
|
catch (e) {
|
|
@@ -216,6 +361,37 @@ async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid)
|
|
|
216
361
|
}
|
|
217
362
|
}
|
|
218
363
|
const matchedPlugin = matched && matched.source === "plugin" ? matched : null;
|
|
364
|
+
// "core" (ping/status/config/...) is a synthetic pseudo-plugin: it only
|
|
365
|
+
// exists in a *local* `allPlugins` map built inside commandRegistry.ts
|
|
366
|
+
// for registry-building purposes — it is never inserted into the real
|
|
367
|
+
// `pluginRegistry` singleton that the loop below iterates. Left as-is,
|
|
368
|
+
// no "core"-sourced command could ever be dispatched (runCommand() would
|
|
369
|
+
// never be called for it, plugin count or not), so it's handled here
|
|
370
|
+
// explicitly, once, before the real-plugin loop.
|
|
371
|
+
if (matchedPlugin && matchedPlugin.pluginName === "core") {
|
|
372
|
+
const coreCtx = buildApi({
|
|
373
|
+
msg,
|
|
374
|
+
chat,
|
|
375
|
+
contract,
|
|
376
|
+
store,
|
|
377
|
+
pluginRegistry,
|
|
378
|
+
pluginName: "core",
|
|
379
|
+
guardOptions: {},
|
|
380
|
+
});
|
|
381
|
+
const coreLoading = startLoadingIndicator(resolveLoadingForDispatch(matchedPlugin, resolution), contract, rawJid, rawKey);
|
|
382
|
+
let coreOutcome = "success";
|
|
383
|
+
try {
|
|
384
|
+
await runCommand({ pluginName: "core", ctx: coreCtx, resolution, reply: msgCtx.reply, chatId: msg.chatId });
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
coreOutcome = "error";
|
|
388
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
389
|
+
logger.warn(`[messageHandler] runCommand crashed for core: ${err.message}`);
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
await coreLoading.stop(coreOutcome);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
219
395
|
for (const plugin of pluginRegistry.values()) {
|
|
220
396
|
const ctx = buildApi({
|
|
221
397
|
msg,
|
|
@@ -226,7 +402,7 @@ async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid)
|
|
|
226
402
|
pluginName: plugin.name,
|
|
227
403
|
guardOptions: plugin.guardOptions,
|
|
228
404
|
});
|
|
229
|
-
const useTyping = plugin.guardOptions?.typing !== false;
|
|
405
|
+
const useTyping = !matchedPlugin && plugin.guardOptions?.typing !== false;
|
|
230
406
|
let typingInterval;
|
|
231
407
|
if (useTyping) {
|
|
232
408
|
// Refresh presence every 4s so WhatsApp doesn't auto-clear it
|
|
@@ -244,16 +420,26 @@ async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid)
|
|
|
244
420
|
// this same plugin that did NOT match the registry) keep their legacy
|
|
245
421
|
// run(ctx).
|
|
246
422
|
if (matchedPlugin && matchedPlugin.pluginName === plugin.name && matchedPlugin.handler) {
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
423
|
+
// Loading indicator: the resolved spec is read once per dispatch
|
|
424
|
+
// and drives start/stop around the runCommand call.
|
|
425
|
+
const loading = startLoadingIndicator(resolveLoadingForDispatch(matchedPlugin, resolution), contract, rawJid, rawKey);
|
|
426
|
+
let outcome = "success";
|
|
251
427
|
try {
|
|
252
|
-
|
|
428
|
+
// runCommand() opts into rethrow so its Phase-8 fireAlert("plugin_crash")
|
|
429
|
+
// catch actually runs (runPlugin() swallows by default — see pluginGuard.ts).
|
|
430
|
+
// Swallow here too, at the boundary: this loop must never crash the bot,
|
|
431
|
+
// same guarantee the legacy runPlugin(plugin, ctx) branch below already has.
|
|
432
|
+
try {
|
|
433
|
+
await runCommand({ pluginName: plugin.name, ctx, resolution, reply: msgCtx.reply, chatId: msg.chatId });
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
outcome = "error";
|
|
437
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
438
|
+
logger.warn(`[messageHandler] runCommand crashed for plugin "${plugin.name}": ${err.message}`);
|
|
439
|
+
}
|
|
253
440
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
logger.warn(`[messageHandler] runCommand crashed for plugin "${plugin.name}": ${err.message}`);
|
|
441
|
+
finally {
|
|
442
|
+
await loading.stop(outcome);
|
|
257
443
|
}
|
|
258
444
|
}
|
|
259
445
|
else {
|
|
@@ -273,7 +459,7 @@ async function runPluginsForMessage(msg, chat, msgCtx, contract, store, rawJid)
|
|
|
273
459
|
// alongside this fallback.
|
|
274
460
|
if (!matched && command && registry?.menu.notFoundFallback) {
|
|
275
461
|
try {
|
|
276
|
-
await msgCtx.reply.text(renderNotFound(command, registry));
|
|
462
|
+
await msgCtx.reply.text(renderNotFound(command, registry, chatLocale));
|
|
277
463
|
}
|
|
278
464
|
catch (e) {
|
|
279
465
|
const err = e instanceof Error ? e : new Error(String(e));
|