@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
@@ -11,12 +11,7 @@
11
11
  * globalSock in pluginLoader.ts. Only main.ts is expected to call
12
12
  * register(); everywhere else reads through active() / get() / isDegraded.
13
13
  *
14
- * Shutdown order in shutdown() is reverse-registration, so a driver
15
- * that was added later (e.g. whatsmeow) is disconnected before the
16
- * primary one (typically Baileys). Re-registering the same name
17
- * overwrites the previous instance — the old driver is NOT disconnected
18
- * automatically, callers must disconnect it first if they want it torn
19
- * down.
14
+ * Shutdown order in shutdown() is reverse-registration.
20
15
  *
21
16
  * See the interface and cooldown semantics.
22
17
  */
@@ -69,6 +64,15 @@ class DriverManager {
69
64
  markDegraded(name, durationMs) {
70
65
  this.degradedUntil.set(name, Date.now() + durationMs);
71
66
  }
67
+ /**
68
+ * Drop the degradation entry for `name` so the next `isDegraded()`
69
+ * check returns false. Used after a successful send to clear the
70
+ * cooldown that the most recent failed send had set, without waiting
71
+ * for the timer to expire.
72
+ */
73
+ clearDegraded(name) {
74
+ this.degradedUntil.delete(name);
75
+ }
72
76
  /**
73
77
  * Promote a different driver to active. Used by tests / hot-swap;
74
78
  * the production sendFallbackGuard never calls this — fallback uses
@@ -0,0 +1,90 @@
1
+ import test, { describe, beforeEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { getDriverManager, _resetDriverManagerForTests } from "#kernel/driverManager.js";
4
+ function createFakeDriver(name, ready = true) {
5
+ const state = { disconnected: false };
6
+ const mockRef = (id) => ({ id, chatId: "123@c.us", timestamp: Date.now() });
7
+ return {
8
+ name,
9
+ isReady: () => ready,
10
+ disconnect: async () => {
11
+ state.disconnected = true;
12
+ },
13
+ get disconnected() {
14
+ return state.disconnected;
15
+ },
16
+ connect: async () => { },
17
+ me: () => ({ id: "123@c.us" }),
18
+ sendText: async () => mockRef("msg1"),
19
+ sendImage: async () => mockRef("msg2"),
20
+ sendVideo: async () => mockRef("msg3"),
21
+ sendAudio: async () => mockRef("msg4"),
22
+ sendSticker: async () => mockRef("msg5"),
23
+ sendDocument: async () => mockRef("msg6"),
24
+ sendLocation: async () => mockRef("msg7"),
25
+ sendContact: async () => mockRef("msg8"),
26
+ sendReaction: async () => { },
27
+ sendPoll: async () => mockRef("msg9"),
28
+ react: async () => { },
29
+ deleteMessage: async () => { },
30
+ editMessage: async () => { },
31
+ sendPresenceUpdate: async () => { },
32
+ readMessages: async () => { },
33
+ onWhatsApp: async () => null,
34
+ getBusinessProfile: async () => null,
35
+ profilePictureUrl: async () => null,
36
+ fetchStatus: async () => null,
37
+ updateBlockStatus: async () => { },
38
+ addOrEditContact: async () => { },
39
+ removeContact: async () => { },
40
+ groupMetadata: async () => ({ subject: "Test Group", participants: [] }),
41
+ groupParticipantsUpdate: async () => [],
42
+ groupUpdateSubject: async () => { },
43
+ groupUpdateDescription: async () => { },
44
+ groupInviteCode: async () => "",
45
+ groupRevokeInvite: async () => "",
46
+ updateProfilePicture: async () => { },
47
+ updateProfileName: async () => { },
48
+ updateProfileStatus: async () => { },
49
+ downloadMedia: async () => null,
50
+ getContact: async () => null,
51
+ getProfilePictureUrl: async () => null,
52
+ on: () => () => { },
53
+ };
54
+ }
55
+ describe("kernel/driverManager", () => {
56
+ beforeEach(() => {
57
+ _resetDriverManagerForTests();
58
+ });
59
+ test("registers driver and returns active instance", () => {
60
+ const dm = getDriverManager();
61
+ const baileys = createFakeDriver("baileys");
62
+ dm.register(baileys, { isPrimary: true });
63
+ assert.equal(dm.activeName_(), "baileys");
64
+ assert.equal(dm.active(), baileys);
65
+ assert.equal(dm.get("baileys"), baileys);
66
+ assert.equal(dm.isReady("baileys"), true);
67
+ });
68
+ test("throws if active() called with no drivers registered", () => {
69
+ const dm = getDriverManager();
70
+ assert.throws(() => dm.active(), /no active driver registered/);
71
+ });
72
+ test("tracks degradation with expiration", async (t) => {
73
+ t.mock.timers.enable({ apis: ["Date"] });
74
+ const dm = getDriverManager();
75
+ const baileys = createFakeDriver("baileys");
76
+ dm.register(baileys, { isPrimary: true });
77
+ assert.equal(dm.isDegraded("baileys"), false);
78
+ dm.markDegraded("baileys", 1000);
79
+ assert.equal(dm.isDegraded("baileys"), true);
80
+ t.mock.timers.tick(1001);
81
+ assert.equal(dm.isDegraded("baileys"), false);
82
+ });
83
+ test("shutdown disconnects all drivers", async () => {
84
+ const dm = getDriverManager();
85
+ const baileys = createFakeDriver("baileys");
86
+ dm.register(baileys, { isPrimary: true });
87
+ await dm.shutdown();
88
+ assert.equal(baileys.disconnected, true);
89
+ });
90
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * kernel/integrationMode.ts
3
+ *
4
+ * Toggles the "integration" mode that gates the WhatsApp integration
5
+ * test suite. In integration mode the bot loads a private plugin
6
+ * (`__manybot_integration__`) whose job is to drive real-WhatsApp
7
+ * operations from the test harness, and refuse to act in any chat
8
+ * other than the configured `TEST_CHAT`.
9
+ *
10
+ * Activation requires BOTH signals (mirrors `testConfig`):
11
+ * - `MANYBOT_RUN_WHATSAPP_TESTS=1` (explicit opt-in)
12
+ * - a configured `TEST_CHAT` (env or manybot.toml)
13
+ *
14
+ * Production code never imports this module in its hot path. The
15
+ * integration plugin loader is the only consumer; the production bot
16
+ * does not load the integration plugin and is not affected by the
17
+ * opt-in flag.
18
+ *
19
+ * The reserved plugin name uses a `__manybot_*__` double-underscore
20
+ * pattern so it cannot collide with a real plugin (the loader's
21
+ * normal `~/.manybot/plugins/<name>/` path does not create a
22
+ * directory starting with `__` on user machines, and the test setup
23
+ * installs it into a separate, non-user-controlled location).
24
+ */
25
+ import path from "path";
26
+ import { getTestConfig, requireTestConfig } from "#kernel/testConfig.js";
27
+ import { logger } from "#logger";
28
+ /**
29
+ * Reserved plugin name. The leading and trailing `__` are
30
+ * deliberate: this name is documented as reserved by the runtime,
31
+ * so no real plugin directory should ever be created with it.
32
+ * Plugins loaded from `~/.manybot/plugins/` with names that match
33
+ * this pattern are NOT auto-loaded by `loadPlugins()`; the
34
+ * integration harness has to ask for them explicitly.
35
+ */
36
+ export const INTEGRATION_PLUGIN_NAME = "__manybot_integration__";
37
+ /**
38
+ * Absolute path to the integration plugin's source directory inside
39
+ * the project. The plugin ships with the repo (not with the user's
40
+ * `~/.manybot/plugins/`) and is loaded directly from `src/`.
41
+ *
42
+ * Resolved relative to this module so the path survives the build
43
+ * (the plugin source is part of the published `src/` tree today;
44
+ * if/when the integration plugin gets moved into `dist/`, only
45
+ * this single constant needs to change).
46
+ */
47
+ const INTEGRATION_PLUGIN_DIR = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "plugins", INTEGRATION_PLUGIN_NAME);
48
+ /** True only when the operator has set the explicit opt-in flag. */
49
+ export function isIntegrationOptIn() {
50
+ return process.env.MANYBOT_RUN_WHATSAPP_TESTS === "1";
51
+ }
52
+ export async function getIntegrationModeStatus() {
53
+ const cfg = await getTestConfig();
54
+ if (!isIntegrationOptIn()) {
55
+ return {
56
+ ready: false,
57
+ reason: `MANYBOT_RUN_WHATSAPP_TESTS=1 is required to enable integration mode`,
58
+ chat: cfg.chat,
59
+ source: cfg.source,
60
+ };
61
+ }
62
+ if (cfg.chat === null) {
63
+ return {
64
+ ready: false,
65
+ reason: cfg.skipReason,
66
+ chat: null,
67
+ source: null,
68
+ };
69
+ }
70
+ return { ready: true, reason: null, chat: cfg.chat, source: cfg.source };
71
+ }
72
+ /**
73
+ * Hard version: throws if integration mode is not fully ready. Use
74
+ * this at the entry point of every test that touches the real bot.
75
+ */
76
+ export async function requireIntegrationMode() {
77
+ if (!isIntegrationOptIn()) {
78
+ throw new Error(`[integrationMode] cannot run: MANYBOT_RUN_WHATSAPP_TESTS=1 is required. ` +
79
+ `Set it explicitly to opt in to the real-WhatsApp test suite.`);
80
+ }
81
+ const cfg = await requireTestConfig();
82
+ logger.info(`[integrationMode] enabled — chat=${cfg.chat} source=${cfg.source}`);
83
+ return { chat: cfg.chat, source: cfg.source };
84
+ }
85
+ /** Absolute path of the integration plugin's source directory. */
86
+ export function getIntegrationPluginDir() {
87
+ return INTEGRATION_PLUGIN_DIR;
88
+ }
@@ -0,0 +1,95 @@
1
+ import test, { describe, before, after, beforeEach } from "node:test";
2
+ import assert from "node:assert/strict";
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-integration-mode-"));
7
+ process.env.MANYBOT_CONFIG_DIR = configDir;
8
+ const { getIntegrationModeStatus, requireIntegrationMode, isIntegrationOptIn, INTEGRATION_PLUGIN_NAME, getIntegrationPluginDir, } = await import("#kernel/integrationMode.js");
9
+ const { _resetTestConfigForTests, TEST_CHAT_ENV, RUN_WHATSAPP_TESTS_ENV, } = await import("#kernel/testConfig.js");
10
+ const tomlPath = path.join(configDir, "manybot.toml");
11
+ function clearEnv() {
12
+ delete process.env[TEST_CHAT_ENV];
13
+ delete process.env[RUN_WHATSAPP_TESTS_ENV];
14
+ delete process.env.MANYBOT_RUN_WHATSAPP_TESTS;
15
+ }
16
+ before(async () => {
17
+ await fs.writeFile(tomlPath, "", "utf8");
18
+ });
19
+ beforeEach(async () => {
20
+ clearEnv();
21
+ await fs.writeFile(tomlPath, "", "utf8");
22
+ _resetTestConfigForTests();
23
+ });
24
+ after(async () => {
25
+ clearEnv();
26
+ await fs.rm(configDir, { recursive: true, force: true });
27
+ });
28
+ describe("kernel/integrationMode — isIntegrationOptIn", () => {
29
+ test("false when env is unset", () => {
30
+ assert.equal(isIntegrationOptIn(), false);
31
+ });
32
+ test("false when env is anything other than '1'", () => {
33
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "true";
34
+ assert.equal(isIntegrationOptIn(), false);
35
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "0";
36
+ assert.equal(isIntegrationOptIn(), false);
37
+ });
38
+ test("true only when env equals '1'", () => {
39
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
40
+ assert.equal(isIntegrationOptIn(), true);
41
+ });
42
+ });
43
+ describe("kernel/integrationMode — getIntegrationModeStatus", () => {
44
+ test("not ready when opt-in is missing (even with TEST_CHAT set)", async () => {
45
+ process.env[TEST_CHAT_ENV] = "5516000000001";
46
+ const status = await getIntegrationModeStatus();
47
+ assert.equal(status.ready, false);
48
+ assert.match(status.reason, /MANYBOT_RUN_WHATSAPP_TESTS=1/);
49
+ assert.equal(status.chat, "5516000000001@s.whatsapp.net");
50
+ });
51
+ test("not ready when TEST_CHAT is missing (even with opt-in)", async () => {
52
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
53
+ const status = await getIntegrationModeStatus();
54
+ assert.equal(status.ready, false);
55
+ assert.match(status.reason, /TEST_CHAT is not set/);
56
+ assert.equal(status.chat, null);
57
+ });
58
+ test("ready only when both opt-in and TEST_CHAT are configured", async () => {
59
+ process.env[TEST_CHAT_ENV] = "5516000000002";
60
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
61
+ const status = await getIntegrationModeStatus();
62
+ assert.equal(status.ready, true);
63
+ assert.equal(status.reason, null);
64
+ assert.equal(status.chat, "5516000000002@s.whatsapp.net");
65
+ assert.equal(status.source, "env");
66
+ });
67
+ });
68
+ describe("kernel/integrationMode — requireIntegrationMode", () => {
69
+ test("returns chat and source when ready", async () => {
70
+ process.env[TEST_CHAT_ENV] = "5516000000003";
71
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
72
+ const res = await requireIntegrationMode();
73
+ assert.equal(res.chat, "5516000000003@s.whatsapp.net");
74
+ assert.equal(res.source, "env");
75
+ });
76
+ test("throws when opt-in is missing", async () => {
77
+ process.env[TEST_CHAT_ENV] = "5516000000004";
78
+ await assert.rejects(requireIntegrationMode(), /MANYBOT_RUN_WHATSAPP_TESTS=1/);
79
+ });
80
+ test("throws when TEST_CHAT is missing", async () => {
81
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
82
+ await assert.rejects(requireIntegrationMode(), /TEST_CHAT is not set/);
83
+ });
84
+ });
85
+ describe("kernel/integrationMode — constants", () => {
86
+ test("integration plugin name is reserved (double-underscore)", () => {
87
+ assert.equal(INTEGRATION_PLUGIN_NAME, "__manybot_integration__");
88
+ assert.ok(INTEGRATION_PLUGIN_NAME.startsWith("__"));
89
+ assert.ok(INTEGRATION_PLUGIN_NAME.endsWith("__"));
90
+ });
91
+ test("integration plugin dir resolves under src/plugins/", () => {
92
+ const dir = getIntegrationPluginDir();
93
+ assert.ok(dir.endsWith(path.join("src", "plugins", "__manybot_integration__")));
94
+ });
95
+ });
@@ -0,0 +1,67 @@
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-integration-loader-"));
7
+ process.env.MANYBOT_CONFIG_DIR = configDir;
8
+ const { loadIntegrationPlugin, pluginRegistry, cleanupPlugins, } = await import("#kernel/pluginLoader.js");
9
+ const { INTEGRATION_PLUGIN_NAME } = await import("#kernel/integrationMode.js");
10
+ const ORIGINAL_OPT_IN = process.env.MANYBOT_RUN_WHATSAPP_TESTS;
11
+ beforeEach(async () => {
12
+ await cleanupPlugins();
13
+ pluginRegistry.clear();
14
+ // Force-clear and re-apply the opt-in between tests; each test sets
15
+ // it explicitly so we know what the contract looks like.
16
+ delete process.env.MANYBOT_RUN_WHATSAPP_TESTS;
17
+ });
18
+ after(async () => {
19
+ await cleanupPlugins();
20
+ pluginRegistry.clear();
21
+ if (ORIGINAL_OPT_IN === undefined)
22
+ delete process.env.MANYBOT_RUN_WHATSAPP_TESTS;
23
+ else
24
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = ORIGINAL_OPT_IN;
25
+ await fs.rm(configDir, { recursive: true, force: true });
26
+ });
27
+ describe("kernel/pluginLoader — loadIntegrationPlugin", () => {
28
+ test("refuses to load without the opt-in flag", async () => {
29
+ // opt-in explicitly NOT set
30
+ await assert.rejects(loadIntegrationPlugin(), /MANYBOT_RUN_WHATSAPP_TESTS=1/);
31
+ assert.equal(pluginRegistry.has(INTEGRATION_PLUGIN_NAME), false);
32
+ });
33
+ test("registers the integration plugin when opt-in is set", async () => {
34
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
35
+ const entry = await loadIntegrationPlugin();
36
+ assert.equal(entry.name, INTEGRATION_PLUGIN_NAME);
37
+ assert.equal(entry.status, "active");
38
+ assert.equal(typeof entry.run, "function");
39
+ assert.equal(entry.commands, null, "integration plugin must not register user commands");
40
+ assert.ok(pluginRegistry.has(INTEGRATION_PLUGIN_NAME));
41
+ });
42
+ test("is idempotent — second call returns the same entry without re-importing", async () => {
43
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
44
+ const first = await loadIntegrationPlugin();
45
+ const second = await loadIntegrationPlugin();
46
+ assert.equal(first, second);
47
+ // Single registration, no duplicate.
48
+ assert.equal(pluginRegistry.size, 1);
49
+ });
50
+ test("exports a public API object via plugin.exports", async () => {
51
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
52
+ const entry = await loadIntegrationPlugin();
53
+ const api = entry.exports;
54
+ assert.ok(api, "integration plugin must expose a public API");
55
+ assert.equal(typeof api.isTestChat, "function");
56
+ assert.equal(typeof api.waitForMarker, "function");
57
+ assert.equal(typeof api.recentBodies, "function");
58
+ // testChat is populated in setup(), not on registration.
59
+ assert.equal(api.testChat, "");
60
+ });
61
+ test("loads the plugin from the source path shipped with the repo", async () => {
62
+ process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
63
+ const entry = await loadIntegrationPlugin();
64
+ // The plugin must be the real one (default export present).
65
+ assert.equal(typeof entry.run, "function");
66
+ });
67
+ });