@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.
Files changed (81) hide show
  1. package/README.md +28 -3
  2. package/dist/client/banner.js +10 -0
  3. package/dist/client/banner.test.js +31 -0
  4. package/dist/client/store.js +91 -6
  5. package/dist/client/store.test.js +170 -0
  6. package/dist/config.js +28 -44
  7. package/dist/config.test.js +26 -0
  8. package/dist/download/queue.js +13 -4
  9. package/dist/drivers/baileys/adapter.js +133 -15
  10. package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
  11. package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
  12. package/dist/drivers/baileys/api/index.js +384 -62
  13. package/dist/drivers/baileys/index.js +92 -36
  14. package/dist/drivers/baileys/loginPrompt.js +0 -2
  15. package/dist/drivers/baileys/messageHandler.js +344 -4
  16. package/dist/drivers/baileys/messageHandler.test.js +445 -0
  17. package/dist/drivers/baileysAdapter.test.js +378 -0
  18. package/dist/drivers/jid.js +26 -0
  19. package/dist/drivers/jid.test.js +74 -0
  20. package/dist/drivers/types.js +5 -5
  21. package/dist/i18n/index.js +20 -24
  22. package/dist/kernel/activeDriverSend.js +21 -0
  23. package/dist/kernel/activeDriverSend.test.js +89 -0
  24. package/dist/kernel/alerts.js +3 -9
  25. package/dist/kernel/chatOverrides.js +46 -0
  26. package/dist/kernel/chatOverrides.test.js +59 -0
  27. package/dist/kernel/chatSession.js +65 -0
  28. package/dist/kernel/chatSession.test.js +46 -0
  29. package/dist/kernel/commandAccess.js +66 -0
  30. package/dist/kernel/commandAccess.test.js +74 -0
  31. package/dist/kernel/commandDeprecation.js +170 -0
  32. package/dist/kernel/commandDeprecation.test.js +114 -0
  33. package/dist/kernel/commandMenu.js +357 -0
  34. package/dist/kernel/commandMenu.test.js +363 -0
  35. package/dist/kernel/commandPermissions.js +171 -0
  36. package/dist/kernel/commandPermissions.test.js +227 -0
  37. package/dist/kernel/commandRegistry.js +583 -0
  38. package/dist/kernel/commandRegistry.test.js +158 -0
  39. package/dist/kernel/commandsConfig.js +949 -0
  40. package/dist/kernel/commandsConfig.test.js +482 -0
  41. package/dist/kernel/contactAutoSave.js +6 -6
  42. package/dist/kernel/contactAutoSave.test.js +87 -0
  43. package/dist/kernel/coreCommands.js +62 -0
  44. package/dist/kernel/driverManager.js +10 -6
  45. package/dist/kernel/driverManager.test.js +90 -0
  46. package/dist/kernel/integrationMode.js +88 -0
  47. package/dist/kernel/integrationMode.test.js +95 -0
  48. package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
  49. package/dist/kernel/pluginApi.test.js +600 -0
  50. package/dist/kernel/pluginGuard.js +18 -13
  51. package/dist/kernel/pluginGuard.test.js +39 -0
  52. package/dist/kernel/pluginLoader.js +169 -11
  53. package/dist/kernel/pluginLoader.test.js +190 -0
  54. package/dist/kernel/runCommand.js +284 -0
  55. package/dist/kernel/runCommand.test.js +497 -0
  56. package/dist/kernel/sendFallbackGuard.js +19 -48
  57. package/dist/kernel/sendFallbackGuard.test.js +80 -0
  58. package/dist/kernel/sendGuard.js +38 -42
  59. package/dist/kernel/sendGuard.test.js +102 -0
  60. package/dist/kernel/settingsDb.js +19 -5
  61. package/dist/kernel/statusServer.js +9 -2
  62. package/dist/kernel/statusServer.test.js +70 -0
  63. package/dist/kernel/testConfig.js +192 -0
  64. package/dist/kernel/testConfig.test.js +181 -0
  65. package/dist/kernel/updateCheck.js +33 -10
  66. package/dist/locales/en.json +77 -13
  67. package/dist/locales/es.json +77 -13
  68. package/dist/locales/pt.json +77 -13
  69. package/dist/logger/logger.js +23 -3
  70. package/dist/logger/logger.test.js +45 -0
  71. package/dist/main.js +5 -76
  72. package/dist/plugins/__manybot_integration__/index.js +184 -0
  73. package/dist/plugins/__manybot_integration__/index.test.js +218 -0
  74. package/dist/utils/phoneNumber.js +83 -0
  75. package/dist/utils/phoneNumber.test.js +53 -0
  76. package/package.json +76 -18
  77. package/dist/drivers/whatsmeow/client.js +0 -252
  78. package/dist/drivers/whatsmeow/index.js +0 -79
  79. package/dist/drivers/whatsmeow/installer.js +0 -86
  80. package/dist/drivers/whatsmeow/supervisor.js +0 -328
  81. package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
@@ -0,0 +1,284 @@
1
+ /**
2
+ * kernel/runCommand.ts
3
+ *
4
+ * ManyBot v6 — single kernel-side dispatcher for routed prefix
5
+ * commands. Centralising this gives Phase 8 a single try/catch hook
6
+ * (the "natural capture point" called out in MANYBOT-6.md) and keeps
7
+ * the per-command logic out of the message handler so each step
8
+ * (permission → required-arguments → sub-command routing → handler
9
+ * dispatch → crash alert) can be tested independently.
10
+ *
11
+ * Pipeline:
12
+ * 1. Resolve the parent entry by invocation (= cmd or alias).
13
+ * 2. Permission check (owner/scope/blacklist/whitelist/admin/botAdmin/cooldown).
14
+ * 3. Sub-command routing: if the parent has subcommands, look up the
15
+ * next token; if a match exists, dispatch to the sub instead.
16
+ * 4. Required-argument validation against the matched entry's
17
+ * declared `arguments:` block.
18
+ * 5. Handler dispatch via `runPlugin` (same timeout + 3-strikes guard
19
+ * as the message handler uses for the legacy plugin.run path).
20
+ * 6. Crash capture: any throw inside step 5 fires `fireAlert` at
21
+ * phase 8 hook level before re-raising; this is the unique
22
+ * advantage of the unified dispatcher over the legacy
23
+ * `for (plugin of pluginRegistry) await runPlugin(...)` loop.
24
+ */
25
+ import { logger } from "#logger";
26
+ import { tFor } from "#i18n";
27
+ import { CMD_PREFIX } from "#config";
28
+ import { fireAlert } from "./alerts.js";
29
+ import { runPlugin } from "./pluginGuard.js";
30
+ import { checkPermission } from "./commandPermissions.js";
31
+ import { getCommandRegistry } from "./commandRegistry.js";
32
+ import { pluginRegistry, resolvePluginCommandHandler } from "./pluginLoader.js";
33
+ import { STOP_CHAIN } from "./commandsConfig.js";
34
+ import { resolveCoreCommandHandler } from "./coreCommands.js";
35
+ import { getChatLocale, getChatPrefix } from "./chatOverrides.js";
36
+ function flatten(target) {
37
+ if (target.kind === "sub") {
38
+ return {
39
+ kind: "sub",
40
+ entry: target.parent,
41
+ name: `${target.parent.cmd} ${target.sub.cmd}`,
42
+ permissions: target.sub.permissions,
43
+ arguments: target.sub.arguments ?? [],
44
+ args: target.args,
45
+ };
46
+ }
47
+ return {
48
+ kind: "parent",
49
+ entry: target.entry,
50
+ name: target.entry.cmd,
51
+ permissions: target.entry.permissions,
52
+ arguments: target.entry.arguments ?? [],
53
+ args: target.args,
54
+ };
55
+ }
56
+ /**
57
+ * Pure resolution — finds the parent + sub (if any) without invoking.
58
+ * `rawArgs` is the body fragment with the leading `<cmd> ` stripped.
59
+ */
60
+ export function resolveDispatch(command, rawArgs) {
61
+ const registry = getCommandRegistry();
62
+ if (!registry)
63
+ return { target: { kind: "none" } };
64
+ const parentId = registry.byInvocation.get(command);
65
+ if (!parentId)
66
+ return { target: { kind: "none" } };
67
+ const entry = registry.byId.get(parentId);
68
+ if (!entry)
69
+ return { target: { kind: "none" } };
70
+ const trimmed = rawArgs.trim();
71
+ if (Object.keys(entry.subcommands).length === 0 || !trimmed) {
72
+ return { target: { kind: "parent", entry, args: trimmed ? trimmed.split(/\s+/) : [] } };
73
+ }
74
+ const token = trimmed.split(/\s+/)[0].toLowerCase();
75
+ const sub = entry.subcommands[token];
76
+ if (!sub) {
77
+ const remaining = trimmed.split(/\s+/).slice(1);
78
+ return {
79
+ target: { kind: "parent", entry, args: remaining },
80
+ unmatchedSubToken: token,
81
+ };
82
+ }
83
+ const rest = trimmed.split(/\s+/).slice(1);
84
+ return { target: { kind: "sub", parent: entry, sub, args: rest } };
85
+ }
86
+ /**
87
+ * Imperative run: takes the dispatcher-ready shapes (plugin, ctx,
88
+ * resolved target) and runs them through the unified pipeline.
89
+ *
90
+ * Returns a small result so the caller (the message handler) can
91
+ * decide whether to send any extra reply, log the action, or fall
92
+ * through to the legacy run loop.
93
+ */
94
+ export async function runCommand(opts) {
95
+ const { resolution, pluginName, ctx, reply, chatId } = opts;
96
+ const { target } = resolution;
97
+ // Resolved once per dispatch: this chat's `!config` overrides (or the
98
+ // global defaults when none were set) for every kernel-authored reply
99
+ // below (unknown-sub hint, missing-argument usage, ...).
100
+ const lang = chatId ? getChatLocale(chatId) : undefined;
101
+ const prefix = chatId ? getChatPrefix(chatId) : CMD_PREFIX;
102
+ if (target.kind === "none") {
103
+ return { status: "no_dispatch", sentReply: null, suggestedReply: null };
104
+ }
105
+ const flat = flatten(target);
106
+ // Sub-tokens that don't match any declared sub go through the parent
107
+ // handler — same convention as a CLI tool with an unknown subcommand.
108
+ if (resolution.unmatchedSubToken && target.kind === "parent") {
109
+ const validSubs = Object.keys(target.entry.subcommands);
110
+ const help = tFor(lang, "commandRun.unknownSubcommand", {
111
+ sub: resolution.unmatchedSubToken,
112
+ cmd: target.entry.cmd,
113
+ valid: validSubs.join(", ") || "(none)",
114
+ });
115
+ await reply.text(help);
116
+ return { status: "unknown_sub", sentReply: help, suggestedReply: help };
117
+ }
118
+ // Permission check uses the resolved (parent OR sub) permissions.
119
+ const permEntry = target.kind === "sub" ? subAsEntry(target.parent, target.sub) : target.entry;
120
+ const perm = await checkPermission(permEntry, {
121
+ isGroup: ctx.chat.isGroup,
122
+ chatId: ctx.chat.id,
123
+ sender: { lid: ctx.msg.sender, pn: ctx.msg.senderPn },
124
+ isSenderAdmin: () => ctx.chat.isSenderAdmin(),
125
+ isBotAdmin: () => ctx.chat.isBotAdmin(),
126
+ });
127
+ if (!perm.allowed) {
128
+ if (perm.message) {
129
+ await reply.text(perm.message);
130
+ }
131
+ return { status: "permission_denied", sentReply: perm.message ?? null, suggestedReply: perm.message ?? null };
132
+ }
133
+ // Required-argument check — purely advisory, the plugin still gets
134
+ // called with whatever args arrived. Surface the kernel-side message
135
+ // + auto-generated usage before invoking.
136
+ const requiredCount = flat.arguments.filter(a => a.required).length;
137
+ if (requiredCount > flat.args.length) {
138
+ const usage = renderUsage(target, prefix);
139
+ const msg = `${tFor(lang, "commandRun.missingRequiredArg")}\n\n${usage}`;
140
+ await reply.text(msg);
141
+ return { status: "argument_missing", sentReply: msg, suggestedReply: msg };
142
+ }
143
+ // Wrap the dispatch in a try/catch and fire alerts on any throw.
144
+ // This is the Phase 8 hook.
145
+ try {
146
+ if (!pluginName) {
147
+ // text-only command handled below (caller will handle the fixed-text path).
148
+ return { status: "no_dispatch", sentReply: null, suggestedReply: null };
149
+ }
150
+ // Function chain dispatch — v6 reference yaml allows a command to
151
+ // declare `functions: [a, b, c]` and have them run top-to-bottom.
152
+ // Each function receives the same `(ctx, { args, subcommand })`
153
+ // shape; a function may short-circuit the rest of the chain by
154
+ // returning the sentinel `STOP_CHAIN` (exported from
155
+ // `commandsConfig.ts`). Anything else (void/undefined/regular value)
156
+ // lets the chain continue. Empty chain → "no_dispatch" (parent is
157
+ // metadata-only, e.g. a sub-container with no override).
158
+ const fnNames = target.kind === "sub" ? target.sub.functions : target.entry.functions;
159
+ if (fnNames.length === 0) {
160
+ return { status: "no_dispatch", sentReply: null, suggestedReply: null };
161
+ }
162
+ const subId = target.kind === "sub" ? target.sub.cmd : undefined;
163
+ const input = { args: flat.args, subcommand: subId };
164
+ if (pluginName === "core") {
165
+ for (const fnName of fnNames) {
166
+ const handler = resolveCoreCommandHandler(fnName);
167
+ if (!handler) {
168
+ logger.warn(`[runCommand] Core namespace does not expose handler "${fnName}"`);
169
+ continue;
170
+ }
171
+ const result = await handler(ctx, input);
172
+ if (result === STOP_CHAIN)
173
+ break;
174
+ }
175
+ return { status: "executed", sentReply: null, suggestedReply: null };
176
+ }
177
+ const plugin = lookupPlugin(pluginName);
178
+ if (!plugin) {
179
+ const msg = `Plugin "${pluginName}" is not active`;
180
+ logger.warn(`[runCommand] ${msg}`);
181
+ return { status: "no_dispatch", sentReply: null, suggestedReply: msg };
182
+ }
183
+ for (const fnName of fnNames) {
184
+ const handler = resolvePluginCommandHandler(plugin.commands?.[fnName]);
185
+ if (!handler) {
186
+ const msg = `Plugin "${pluginName}" does not expose handler for !${flat.name}.${fnName}`;
187
+ logger.warn(`[runCommand] ${msg}`);
188
+ continue;
189
+ }
190
+ const result = await runPlugin(plugin, ctx, handler, input, { rethrow: true });
191
+ if (result === STOP_CHAIN)
192
+ break;
193
+ }
194
+ return { status: "executed", sentReply: null, suggestedReply: null };
195
+ }
196
+ catch (e) {
197
+ const err = e instanceof Error ? e : new Error(String(e));
198
+ fireAlert("plugin_crash", {
199
+ plugin: pluginName,
200
+ command: flat.name,
201
+ kind: err.message?.startsWith("timed out") ? "timeout" : "exception",
202
+ message: err.message,
203
+ });
204
+ // Re-raise so pluginGuard can keep its 3-strike bookkeeping.
205
+ throw err;
206
+ }
207
+ }
208
+ /**
209
+ * Treat a sub-command as if it were a CommandEntry for permission
210
+ * checks. We materialise a virtual entry sharing the sub's resolved
211
+ * permissions, since `checkPermission` works against `CommandEntry`.
212
+ */
213
+ function subAsEntry(parent, sub) {
214
+ return {
215
+ ...parent,
216
+ id: sub.id,
217
+ cmd: sub.cmd,
218
+ permissions: sub.permissions,
219
+ subcommands: {},
220
+ arguments: sub.arguments,
221
+ group: null,
222
+ manual: sub.manual,
223
+ categoryHiddenInScope: null,
224
+ };
225
+ }
226
+ /**
227
+ * Look up the real, live `PluginEntry` for a plugin command dispatch.
228
+ * `pluginRegistry` (from `pluginLoader.ts`) is the same module-level
229
+ * map `commandRegistry.ts` reads at build time — its `commands` field
230
+ * carries the plugin's raw `commands` export as-is (bare handler
231
+ * function or `{ handler, ... }` object); `resolvePluginCommandHandler`
232
+ * normalizes either shape into a callable handler.
233
+ *
234
+ * Returning the *real* entry (not a throwaway `{ name }` stand-in)
235
+ * matters: `runPlugin` (`pluginGuard.ts`) gates on `plugin.status ===
236
+ * "active"` and persists `errorCount`/3-strikes bookkeeping onto the
237
+ * object it's given — a fresh literal each call would silently no-op
238
+ * every dispatch (fails the `status` check) and never accumulate
239
+ * crash counts.
240
+ *
241
+ * NOTE: this is deliberately NOT `ctx.plugins.require(pluginName)` —
242
+ * that facet returns `PluginEntry.exports` (the plugin's pure `api`
243
+ * object, Phase 3's `export const api = {...}`, which never receives
244
+ * `ctx`). Command/sub-command handlers are a different facet
245
+ * (`PluginEntry.commands`) and always receive `ctx`.
246
+ */
247
+ function lookupPlugin(pluginName) {
248
+ const plugin = pluginRegistry.get(pluginName);
249
+ if (!plugin || plugin.status !== "active" || !plugin.commands)
250
+ return null;
251
+ return plugin;
252
+ }
253
+ /**
254
+ * Render an auto-generated usage line for a command/sub. Format:
255
+ * !<cmd>[ <sub>] [--<arg1> ...]
256
+ * Built from the declared `arguments:` block.
257
+ */
258
+ export function renderUsage(target, prefix = CMD_PREFIX) {
259
+ if (target.kind === "none")
260
+ return "";
261
+ const flat = flatten(target);
262
+ const head = target.kind === "sub"
263
+ ? `${prefix}${target.parent.cmd} ${target.sub.cmd}`
264
+ : `${prefix}${flat.entry.cmd}`;
265
+ const headClean = head.replace(/\s+$/, "");
266
+ if (flat.arguments.length === 0)
267
+ return headClean;
268
+ const parts = [];
269
+ for (const arg of flat.arguments) {
270
+ const tok = arg.type === "boolean" ? `--${arg.name}[=true|false]` :
271
+ arg.type === "choice" ? `--${arg.name}=<${arg.choices?.join("|") ?? "..."}>` :
272
+ arg.type === "mention" ? `@<user>` :
273
+ arg.type === "url" ? `<url>` :
274
+ arg.type === "media_direct" ? `<media>` :
275
+ arg.type === "media_reply" ? `<reply-media>` :
276
+ arg.type === "number" ? `<n>` :
277
+ arg.type === "duration" ? `<duration>` :
278
+ arg.type === "quoted_text" ? `"<text>"` :
279
+ arg.type === "reply" ? `<reply>` :
280
+ `<${arg.name}>`;
281
+ parts.push(arg.required ? tok : `[${tok}]`);
282
+ }
283
+ return `${headClean} ${parts.join(" ")}`;
284
+ }