@manybot/manybot 5.7.0 → 5.8.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 (71) hide show
  1. package/README.md +20 -3
  2. package/dist/client/banner.js +10 -0
  3. package/dist/client/banner.test.js +31 -0
  4. package/dist/client/store.js +56 -5
  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/drivers/baileys/adapter.js +58 -7
  9. package/dist/drivers/baileys/api/index.js +172 -24
  10. package/dist/drivers/baileys/index.js +62 -30
  11. package/dist/drivers/baileys/loginPrompt.js +0 -2
  12. package/dist/drivers/baileys/messageHandler.js +158 -4
  13. package/dist/drivers/baileys/messageHandler.test.js +203 -0
  14. package/dist/drivers/baileysAdapter.test.js +281 -0
  15. package/dist/drivers/jid.test.js +40 -0
  16. package/dist/drivers/types.js +5 -5
  17. package/dist/i18n/index.js +15 -2
  18. package/dist/kernel/activeDriverSend.js +21 -0
  19. package/dist/kernel/activeDriverSend.test.js +89 -0
  20. package/dist/kernel/alerts.js +3 -9
  21. package/dist/kernel/chatSession.js +65 -0
  22. package/dist/kernel/chatSession.test.js +46 -0
  23. package/dist/kernel/commandAccess.js +66 -0
  24. package/dist/kernel/commandAccess.test.js +74 -0
  25. package/dist/kernel/commandDeprecation.js +168 -0
  26. package/dist/kernel/commandDeprecation.test.js +107 -0
  27. package/dist/kernel/commandMenu.js +268 -0
  28. package/dist/kernel/commandMenu.test.js +234 -0
  29. package/dist/kernel/commandPermissions.js +125 -0
  30. package/dist/kernel/commandPermissions.test.js +159 -0
  31. package/dist/kernel/commandRegistry.js +459 -0
  32. package/dist/kernel/commandRegistry.test.js +156 -0
  33. package/dist/kernel/commandsConfig.js +517 -0
  34. package/dist/kernel/commandsConfig.test.js +236 -0
  35. package/dist/kernel/contactAutoSave.test.js +87 -0
  36. package/dist/kernel/driverManager.js +10 -6
  37. package/dist/kernel/driverManager.test.js +90 -0
  38. package/dist/kernel/integrationMode.js +88 -0
  39. package/dist/kernel/integrationMode.test.js +95 -0
  40. package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
  41. package/dist/kernel/pluginApi.test.js +583 -0
  42. package/dist/kernel/pluginGuard.js +15 -12
  43. package/dist/kernel/pluginGuard.test.js +39 -0
  44. package/dist/kernel/pluginLoader.js +96 -1
  45. package/dist/kernel/pluginLoader.test.js +80 -0
  46. package/dist/kernel/runCommand.js +245 -0
  47. package/dist/kernel/runCommand.test.js +235 -0
  48. package/dist/kernel/sendFallbackGuard.js +19 -48
  49. package/dist/kernel/sendFallbackGuard.test.js +80 -0
  50. package/dist/kernel/sendGuard.js +38 -42
  51. package/dist/kernel/sendGuard.test.js +102 -0
  52. package/dist/kernel/settingsDb.js +4 -3
  53. package/dist/kernel/statusServer.js +9 -2
  54. package/dist/kernel/statusServer.test.js +70 -0
  55. package/dist/kernel/testConfig.js +183 -0
  56. package/dist/kernel/testConfig.test.js +181 -0
  57. package/dist/kernel/updateCheck.js +33 -10
  58. package/dist/locales/en.json +64 -13
  59. package/dist/locales/es.json +64 -13
  60. package/dist/locales/pt.json +64 -13
  61. package/dist/logger/logger.js +23 -3
  62. package/dist/logger/logger.test.js +45 -0
  63. package/dist/main.js +5 -76
  64. package/dist/plugins/__manybot_integration__/index.js +167 -0
  65. package/dist/plugins/__manybot_integration__/index.test.js +184 -0
  66. package/package.json +74 -17
  67. package/dist/drivers/whatsmeow/client.js +0 -252
  68. package/dist/drivers/whatsmeow/index.js +0 -79
  69. package/dist/drivers/whatsmeow/installer.js +0 -86
  70. package/dist/drivers/whatsmeow/supervisor.js +0 -328
  71. package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
@@ -0,0 +1,184 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, beforeEach, describe, test } from "node:test";
3
+ const integrationPluginUrl = new URL("../../plugins/__manybot_integration__/index.ts", import.meta.url).href;
4
+ describe("plugins/__manybot_integration__", () => {
5
+ let mod;
6
+ beforeEach(async () => {
7
+ // Use a cache-busting query so each test gets a fresh module
8
+ // (and therefore a fresh ring buffer + EventEmitter).
9
+ mod = await import(`${integrationPluginUrl}?t=${Date.now()}-${Math.random()}`);
10
+ });
11
+ after(async () => {
12
+ // Best-effort cleanup; nothing in the plugin subscribes to
13
+ // process-level resources, but we re-clear the configured
14
+ // chat so a subsequent suite (e.g. loadIntegrationPlugin.test.ts)
15
+ // starts blank.
16
+ process.env.MANYBOT_TEST_CHAT = "";
17
+ });
18
+ test("exports default, setup, and api", () => {
19
+ assert.equal(typeof mod.default, "function");
20
+ assert.equal(typeof mod.setup, "function");
21
+ assert.ok(mod.api);
22
+ });
23
+ test("api.isTestChat is false before setup() is called", () => {
24
+ assert.equal(mod.api.isTestChat("5516999999999@s.whatsapp.net"), false);
25
+ assert.equal(mod.api.isTestChat(null), false);
26
+ });
27
+ test("api.recentBodies() starts empty", () => {
28
+ assert.deepEqual(mod.api.recentBodies(), []);
29
+ });
30
+ test("setup() throws when MANYBOT_TEST_CHAT is unset", async () => {
31
+ delete process.env.MANYBOT_TEST_CHAT;
32
+ const { ctx } = makeSetupCtx();
33
+ await assert.rejects(mod.setup(ctx), /MANYBOT_TEST_CHAT/);
34
+ });
35
+ test("setup() captures the test chat and isTestChat accepts it", async () => {
36
+ process.env.MANYBOT_TEST_CHAT = "5516999999999@s.whatsapp.net";
37
+ const { ctx } = makeSetupCtx();
38
+ await mod.setup(ctx);
39
+ assert.equal(mod.api.testChat, "5516999999999@s.whatsapp.net");
40
+ assert.equal(mod.api.isTestChat("5516999999999@s.whatsapp.net"), true);
41
+ assert.equal(mod.api.isTestChat("5516000000000@s.whatsapp.net"), false);
42
+ });
43
+ test("default() refuses to act on a non-test chat", async () => {
44
+ process.env.MANYBOT_TEST_CHAT = "5516999999999@s.whatsapp.net";
45
+ const { ctx: setupCtx } = makeSetupCtx();
46
+ await mod.setup(setupCtx);
47
+ const { ctx, replyCalls } = makeMessageCtx("120363012345678@g.us");
48
+ // Should not throw and should not produce a reply.
49
+ await mod.default(ctx);
50
+ assert.equal(replyCalls.length, 0);
51
+ });
52
+ test("default() does not act even in the test chat (plugin is passive)", async () => {
53
+ process.env.MANYBOT_TEST_CHAT = "5516999999999@s.whatsapp.net";
54
+ const { ctx: setupCtx } = makeSetupCtx();
55
+ await mod.setup(setupCtx);
56
+ const { ctx, replyCalls } = makeMessageCtx("5516999999999@s.whatsapp.net");
57
+ await mod.default(ctx);
58
+ assert.equal(replyCalls.length, 0);
59
+ });
60
+ test("default() before setup() is a no-op, not a crash", async () => {
61
+ delete process.env.MANYBOT_TEST_CHAT;
62
+ const { ctx, replyCalls } = makeMessageCtx("5516999999999@s.whatsapp.net");
63
+ await mod.default(ctx); // must not throw
64
+ assert.equal(replyCalls.length, 0);
65
+ });
66
+ test("waitForMarker resolves when a matching messages.upsert is delivered via setup()", async () => {
67
+ process.env.MANYBOT_TEST_CHAT = "5516999999999@s.whatsapp.net";
68
+ const { ctx, handlers } = makeSetupCtx();
69
+ await mod.setup(ctx);
70
+ // Fire the wait first, then deliver the event.
71
+ const pending = mod.api.waitForMarker("PING:", 1000);
72
+ const handlerList = handlers.get("messages.upsert");
73
+ assert.ok(handlerList && handlerList.length > 0, "setup() must register a messages.upsert handler");
74
+ handlerList[0]({
75
+ messages: [{ chatId: "5516999999999@s.whatsapp.net", body: "PING:hello", from: "5516000000000@s.whatsapp.net" }],
76
+ });
77
+ const id = await pending;
78
+ assert.match(id, /^recent:\d+$/);
79
+ assert.ok(mod.api.recentBodies().some((b) => b === "PING:hello"));
80
+ });
81
+ test("waitForMarker ignores messages from a different chat", async () => {
82
+ process.env.MANYBOT_TEST_CHAT = "5516999999999@s.whatsapp.net";
83
+ const { ctx, handlers } = makeSetupCtx();
84
+ await mod.setup(ctx);
85
+ const pending = mod.api.waitForMarker("PING:", 200);
86
+ const handlerList = handlers.get("messages.upsert");
87
+ assert.ok(handlerList);
88
+ // Send from a different chat — must not resolve the wait.
89
+ handlerList[0]({
90
+ messages: [{ chatId: "120363012345678@g.us", body: "PING:should-not-match", from: "120363012345678@g.us" }],
91
+ });
92
+ await assert.rejects(pending, /timed out/);
93
+ });
94
+ test("waitForMarker resolves immediately when a matching message is already in the buffer", async () => {
95
+ process.env.MANYBOT_TEST_CHAT = "5516999999999@s.whatsapp.net";
96
+ const { ctx, handlers } = makeSetupCtx();
97
+ await mod.setup(ctx);
98
+ const handlerList = handlers.get("messages.upsert");
99
+ assert.ok(handlerList);
100
+ handlerList[0]({
101
+ messages: [{ chatId: "5516999999999@s.whatsapp.net", body: "PING:already-here", from: "5516000000000@s.whatsapp.net" }],
102
+ });
103
+ const id = await mod.api.waitForMarker("PING:", 200);
104
+ assert.match(id, /^recent:\d+$/);
105
+ });
106
+ test("waitForMarker does not match a body that does not start with the marker", async () => {
107
+ process.env.MANYBOT_TEST_CHAT = "5516999999999@s.whatsapp.net";
108
+ const { ctx, handlers } = makeSetupCtx();
109
+ await mod.setup(ctx);
110
+ const handlerList = handlers.get("messages.upsert");
111
+ assert.ok(handlerList);
112
+ handlerList[0]({
113
+ messages: [{ chatId: "5516999999999@s.whatsapp.net", body: "PONG:no-match", from: "5516000000000@s.whatsapp.net" }],
114
+ });
115
+ await assert.rejects(mod.api.waitForMarker("PING:", 150), /timed out/);
116
+ });
117
+ test("setup() respects TEST_CHAT env var when MANYBOT_TEST_CHAT is unset", async () => {
118
+ delete process.env.MANYBOT_TEST_CHAT;
119
+ process.env.TEST_CHAT = "5516999998888@s.whatsapp.net";
120
+ const { ctx } = makeSetupCtx();
121
+ await mod.setup(ctx);
122
+ assert.equal(mod.api.testChat, "5516999998888@s.whatsapp.net");
123
+ assert.equal(mod.api.isTestChat("5516999998888@s.whatsapp.net"), true);
124
+ });
125
+ test("setup() throws when given an invalid test chat format", async () => {
126
+ process.env.MANYBOT_TEST_CHAT = "not_a_valid_jid";
127
+ const { ctx } = makeSetupCtx();
128
+ await assert.rejects(mod.setup(ctx), /invalid test chat provided/);
129
+ });
130
+ test("ring buffer caps at 50 messages and evicts oldest", async () => {
131
+ process.env.MANYBOT_TEST_CHAT = "5516999999999@s.whatsapp.net";
132
+ const { ctx, handlers } = makeSetupCtx();
133
+ await mod.setup(ctx);
134
+ const handlerList = handlers.get("messages.upsert");
135
+ assert.ok(handlerList);
136
+ // Push 55 messages
137
+ const messages = Array.from({ length: 55 }, (_, i) => ({
138
+ chatId: "5516999999999@s.whatsapp.net",
139
+ body: `MSG_${i}`,
140
+ from: "5516000000000@s.whatsapp.net",
141
+ }));
142
+ handlerList[0]({ messages });
143
+ const recent = mod.api.recentBodies();
144
+ assert.equal(recent.length, 50);
145
+ // Oldest 5 (MSG_0 to MSG_4) should have been evicted
146
+ assert.equal(recent[0], "MSG_5");
147
+ assert.equal(recent.at(-1), "MSG_54");
148
+ });
149
+ });
150
+ function makeSetupCtx() {
151
+ const handlers = new Map();
152
+ const mock = {
153
+ handlers,
154
+ events: {
155
+ on(event, handler) {
156
+ const list = handlers.get(event) ?? [];
157
+ list.push(handler);
158
+ handlers.set(event, list);
159
+ return () => {
160
+ const arr = handlers.get(event) ?? [];
161
+ const idx = arr.indexOf(handler);
162
+ if (idx >= 0)
163
+ arr.splice(idx, 1);
164
+ };
165
+ },
166
+ },
167
+ };
168
+ // The plugin only touches `events` in setup(); the rest of the
169
+ // SetupContext surface is unused, so we type-cast through `unknown`
170
+ // rather than build a full fake.
171
+ return { ctx: mock, handlers };
172
+ }
173
+ function makeMessageCtx(chatId) {
174
+ const mock = {
175
+ chat: {
176
+ id: chatId,
177
+ name: "test",
178
+ isGroup: chatId.endsWith("@g.us"),
179
+ },
180
+ msg: { id: "msg-1", body: "hello" },
181
+ replyCalls: [],
182
+ };
183
+ return { ctx: mock, replyCalls: mock.replyCalls };
184
+ }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "SyntaxError!",
6
6
  "email": "me@stxerr.dev"
7
7
  },
8
- "version": "5.7.0",
8
+ "version": "5.8.0",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {
@@ -25,15 +25,26 @@
25
25
  "LICENSE"
26
26
  ],
27
27
  "scripts": {
28
- "build": "npm i && tsc && node -e \"fs.mkdirSync('dist/locales', { recursive: true }); fs.cpSync('src/locales', 'dist/locales', { recursive: true }); fs.mkdirSync('dist/drivers/whatsmeow', { recursive: true }); fs.cpSync('src/drivers/whatsmeow/whatsmeow.proto', 'dist/drivers/whatsmeow/whatsmeow.proto');\" && node scripts/build-whatsmeow.mjs",
28
+ "build": "tsc && node -e \"fs.mkdirSync('dist/locales', { recursive: true }); fs.cpSync('src/locales', 'dist/locales', { recursive: true });\" && npm run build:types",
29
+ "build:types": "tsc --noEmit -p packages/types/tsconfig.json",
29
30
  "start": "node dist/main.js",
30
- "typecheck": "tsc --noEmit"
31
+ "lint": "eslint .",
32
+ "test": "NODE_ENV=test bash -O globstar -c 'tsx --conditions development --test --experimental-test-coverage src/**/*.test.ts'",
33
+ "test:integration": "NODE_ENV=test MANYBOT_RUN_WHATSAPP_TESTS=1 bash -O globstar -c 'tsx --conditions development --test src/**/*.integration.test.ts'",
34
+ "test:integration:local": "NODE_ENV=test MANYBOT_RUN_WHATSAPP_TESTS=1 bash -O globstar -c 'tsx --conditions development --import ./src/main.ts --test src/**/*.integration.test.ts'",
35
+ "typecheck": "tsc --noEmit",
36
+ "check": "npm run typecheck && npm run lint && npm run test"
31
37
  },
32
38
  "devDependencies": {
39
+ "@eslint/js": "^10.0.1",
40
+ "@types/js-yaml": "^4.0.9",
33
41
  "@types/node": "^22.0.0",
34
42
  "@types/nodemailer": "^8.0.1",
43
+ "eslint": "^10.8.1",
44
+ "eslint-plugin-import-x": "^4.17.1",
35
45
  "tsx": "^4.19.2",
36
- "typescript": "^5.7.3"
46
+ "typescript": "^5.7.3",
47
+ "typescript-eslint": "^8.67.0"
37
48
  },
38
49
  "dependencies": {
39
50
  "@clack/prompts": "^0.10.1",
@@ -41,6 +52,7 @@
41
52
  "@grpc/proto-loader": "^0.7.15",
42
53
  "@hapi/boom": "^10.0.1",
43
54
  "@whiskeysockets/baileys": "6.7.24",
55
+ "js-yaml": "^5.2.3",
44
56
  "node-cron": "^4.6.0",
45
57
  "node-webpmux": "^3.2.1",
46
58
  "nodemailer": "^9.0.3",
@@ -49,18 +61,63 @@
49
61
  "smol-toml": "^1.7.0"
50
62
  },
51
63
  "imports": {
52
- "#drivers/*": "./dist/drivers/*",
53
- "#client/*": "./dist/client/*",
54
- "#kernel/*": "./dist/kernel/*",
55
- "#manyapi": "./dist/kernel/pluginApi.js",
56
- "#settingsdb": "./dist/kernel/settingsDb.js",
57
- "#sendguard": "./dist/kernel/sendGuard.js",
58
- "#logger": "./dist/logger/logger.js",
59
- "#utils/*": "./dist/utils/*",
60
- "#i18n": "./dist/i18n/index.js",
61
- "#download": "./dist/download/queue.js",
62
- "#config": "./dist/config.js",
63
- "#main": "./dist/main.js",
64
- "#types": "./dist/types.js"
64
+ "#drivers/*": {
65
+ "development": "./src/drivers/*",
66
+ "default": "./dist/drivers/*"
67
+ },
68
+ "#client/*": {
69
+ "development": "./src/client/*",
70
+ "default": "./dist/client/*"
71
+ },
72
+ "#kernel/*": {
73
+ "development": "./src/kernel/*",
74
+ "default": "./dist/kernel/*"
75
+ },
76
+ "#manyapi": {
77
+ "development": "./src/kernel/pluginApi.ts",
78
+ "default": "./dist/kernel/pluginApi.js"
79
+ },
80
+ "#settingsdb": {
81
+ "development": "./src/kernel/settingsDb.ts",
82
+ "default": "./dist/kernel/settingsDb.js"
83
+ },
84
+ "#sendguard": {
85
+ "development": "./src/kernel/sendGuard.ts",
86
+ "default": "./dist/kernel/sendGuard.js"
87
+ },
88
+ "#logger": {
89
+ "development": "./src/logger/logger.ts",
90
+ "default": "./dist/logger/logger.js"
91
+ },
92
+ "#utils/*": {
93
+ "development": "./src/utils/*",
94
+ "default": "./dist/utils/*"
95
+ },
96
+ "#i18n": {
97
+ "development": "./src/i18n/index.ts",
98
+ "default": "./dist/i18n/index.js"
99
+ },
100
+ "#download": {
101
+ "development": "./src/download/queue.ts",
102
+ "default": "./dist/download/queue.js"
103
+ },
104
+ "#config": {
105
+ "development": "./src/config.ts",
106
+ "default": "./dist/config.js"
107
+ },
108
+ "#main": {
109
+ "development": "./src/main.ts",
110
+ "default": "./dist/main.js"
111
+ },
112
+ "#types": {
113
+ "development": "./src/types.ts",
114
+ "default": "./dist/types.js"
115
+ }
116
+ },
117
+ "allowScripts": {
118
+ "@whiskeysockets/baileys@6.7.24": true,
119
+ "protobufjs@7.6.5": true,
120
+ "esbuild@0.28.2": true,
121
+ "unrs-resolver@1.12.2": true
65
122
  }
66
123
  }
@@ -1,252 +0,0 @@
1
- import { logger } from "#logger";
2
- import { CONFIG } from "#config";
3
- import { t } from "#i18n";
4
- import * as grpc from "@grpc/grpc-js";
5
- import * as protoLoader from "@grpc/proto-loader";
6
- import path from "path";
7
- import { fileURLToPath } from "node:url";
8
- import qrcode from "qrcode-terminal";
9
- /**
10
- * Whatsmeow gRPC client implementing the WaContract interface.
11
- *
12
- * Phase 1 scope: only the send path (sendText) and the
13
- * verification primitives (getHistory) are fully wired. Every other
14
- * WaContract method throws "not implemented" — the kernel loads plugins
15
- * only after `connection.update === "open"`, and a plugin that calls e.g.
16
- * `groupMetadata` on a whatsmeow-primary bot will surface a clear error
17
- * to the caller, not a silent no-op.
18
- *
19
- * Connects to the address defined in config (default localhost:50051).
20
- */
21
- class WhatsmeowClient {
22
- name = "whatsmeow";
23
- client; // grpc client stub
24
- ready = false;
25
- handlers = new Map();
26
- /**
27
- * Resolve the .proto path relative to this compiled module so it works
28
- * in ESM (where __dirname doesn't exist), under `node dist/main.js`
29
- * (proto is copied to dist/drivers/whatsmeow/whatsmeow.proto by the
30
- * build), and under a global npm install (the proto ships alongside
31
- * the JS in the package's `dist/` per `files` in package.json). In
32
- * dev (`tsx src/main.ts`) the proto already lives next to this source
33
- * file, so the same relative path resolves correctly there too.
34
- */
35
- resolveProtoPath() {
36
- const here = path.dirname(fileURLToPath(import.meta.url));
37
- return path.resolve(here, "whatsmeow.proto");
38
- }
39
- loadProto() {
40
- const protoPath = this.resolveProtoPath();
41
- const packageDef = protoLoader.loadSync(protoPath, {
42
- keepCase: true,
43
- longs: String,
44
- enums: String,
45
- defaults: true,
46
- oneofs: true,
47
- });
48
- const grpcObj = grpc.loadPackageDefinition(packageDef);
49
- return grpcObj.whatsmeow.WhatsmeowService;
50
- }
51
- async connect() {
52
- const address = CONFIG.drivers.whatsmeow.grpcAddress ?? "localhost:50051";
53
- const Service = this.loadProto();
54
- this.client = new Service(address, grpc.credentials.createInsecure());
55
- // 1. Health check — confirm the gRPC server is up
56
- await new Promise((resolve, reject) => {
57
- this.client.HealthCheck({}, (err, resp) => {
58
- if (err)
59
- return reject(err);
60
- this.ready = !!resp?.ready;
61
- if (this.ready)
62
- resolve();
63
- else
64
- reject(new Error("Whatsmeow service not ready"));
65
- });
66
- });
67
- logger.info("[whatsmeow] gRPC service ready");
68
- // 2. Call Connect RPC — initiates WhatsApp auth (QR or reuse existing session)
69
- const connectResp = await new Promise((resolve, reject) => {
70
- this.client.Connect({}, (err, resp) => {
71
- if (err)
72
- return reject(err);
73
- resolve(resp);
74
- });
75
- });
76
- const needsAuth = !connectResp.ok;
77
- if (needsAuth && connectResp.qrCode) {
78
- logger.info(t("system.qrScan"));
79
- qrcode.generate(connectResp.qrCode, { small: true });
80
- }
81
- // 3. Set up auth deferred BEFORE SubscribeEvents to avoid race
82
- let authDeferred = null;
83
- let authDone = false;
84
- const authPromise = needsAuth
85
- ? new Promise((resolve, reject) => {
86
- authDeferred = { resolve, reject };
87
- setTimeout(() => {
88
- if (!authDone) {
89
- authDone = true;
90
- reject(new Error("Whatsmeow auth timeout (2 min)"));
91
- }
92
- }, 120_000);
93
- })
94
- : Promise.resolve();
95
- // 4. Open the server-streaming event subscription
96
- const stream = this.client.SubscribeEvents({});
97
- stream.on("data", (raw) => {
98
- // Resolve auth promise when connection opens (QR scanned / session reused)
99
- if (!authDone && authDeferred && raw.connState?.state === "open") {
100
- authDone = true;
101
- authDeferred.resolve();
102
- authDeferred = null;
103
- }
104
- try {
105
- if (raw.connState) {
106
- const state = raw.connState.state ?? "connecting";
107
- this.dispatch("connection.update", {
108
- connection: state === "open" ? "open" : state === "close" ? "close" : "connecting",
109
- });
110
- }
111
- else if (raw.message) {
112
- this.dispatch("messages.upsert", {
113
- messages: [raw.message],
114
- type: "notify",
115
- });
116
- }
117
- }
118
- catch (e) {
119
- logger.debug(`[whatsmeow] event dispatch failed: ${e.message}`);
120
- }
121
- });
122
- stream.on("error", (err) => {
123
- if (!authDone) {
124
- authDone = true;
125
- authDeferred?.reject(err);
126
- authDeferred = null;
127
- }
128
- logger.warn(`[whatsmeow] event stream error: ${err.message}`);
129
- this.ready = false;
130
- });
131
- stream.on("end", () => {
132
- if (!authDone) {
133
- authDone = true;
134
- authDeferred?.reject(new Error("Event stream ended before auth completed"));
135
- authDeferred = null;
136
- }
137
- logger.warn(`[whatsmeow] event stream ended`);
138
- this.ready = false;
139
- });
140
- // 5. If not authenticated, wait for connState === "open" from the event stream
141
- await authPromise;
142
- if (needsAuth) {
143
- logger.info("[whatsmeow] authenticated");
144
- }
145
- this.ready = true;
146
- }
147
- async disconnect() {
148
- if (this.client) {
149
- try {
150
- await new Promise((resolve) => {
151
- this.client.Disconnect({}, () => resolve());
152
- });
153
- }
154
- catch { }
155
- this.client.close();
156
- }
157
- this.ready = false;
158
- }
159
- isReady() {
160
- return this.ready;
161
- }
162
- // ── Event fan-out ─────────────────────────────────────────────────────────
163
- on(event, handler) {
164
- let set = this.handlers.get(event);
165
- if (!set) {
166
- set = new Set();
167
- this.handlers.set(event, set);
168
- }
169
- set.add(handler);
170
- return () => set.delete(handler);
171
- }
172
- dispatch(event, payload) {
173
- const set = this.handlers.get(event);
174
- if (!set)
175
- return;
176
- for (const h of set) {
177
- try {
178
- h(payload);
179
- }
180
- catch (e) {
181
- logger.debug(`[whatsmeow] handler for "${event}" threw: ${e.message}`);
182
- }
183
- }
184
- }
185
- // ── Send ───────────────────────────────────────────────────────────────────
186
- // Only sendText is in phase-1 scope. Media fallback is
187
- // documented in the interface but explicitly deferred.
188
- async sendText(jid, text, opts) {
189
- const req = {
190
- jid,
191
- text,
192
- quotedId: opts?.quoted?.id ?? "",
193
- mentions: opts?.mentions ?? [],
194
- };
195
- return new Promise((resolve, reject) => {
196
- this.client.SendText(req, (err, resp) => {
197
- if (err)
198
- return reject(err);
199
- resolve({ id: resp.id, chatId: resp.chatId, timestamp: Number(resp.timestamp) });
200
- });
201
- });
202
- }
203
- // ── Verification primitive ─────────────────────────────────────────────────
204
- async getHistory(jid, opts) {
205
- const req = { jid, limit: opts?.limit ?? 5 };
206
- return new Promise((resolve, reject) => {
207
- this.client.GetHistory(req, (err, resp) => {
208
- if (err)
209
- return reject(err);
210
- resolve(resp.messages ?? []);
211
- });
212
- });
213
- }
214
- // ── All other WaContract methods: stubbed for now ─────────────────────────
215
- // These throw a clear error so a plugin calling them on a whatsmeow-
216
- // primary bot fails loudly instead of silently no-op'ing. Coverage will
217
- // grow in later phases as the whatsmeow .proto grows.
218
- unimplemented(method) {
219
- throw new Error(`[whatsmeow] ${method} not implemented in whatsmeow driver yet`);
220
- }
221
- async resolveLid(_lid) { return null; }
222
- async sendImage(_jid, _buffer, _opts) { this.unimplemented("sendImage"); }
223
- async sendVideo(_jid, _buffer, _opts) { this.unimplemented("sendVideo"); }
224
- async sendAudio(_jid, _buffer, _opts) { this.unimplemented("sendAudio"); }
225
- async sendSticker(_jid, _buffer, _opts) { this.unimplemented("sendSticker"); }
226
- async sendDocument(_jid, _buffer, _filename, _mimetype, _opts) { this.unimplemented("sendDocument"); }
227
- async sendPoll(_jid, _opts) { this.unimplemented("sendPoll"); }
228
- async react(_jid, _target, _emoji) { this.unimplemented("react"); }
229
- async deleteMessage(_jid, _target, _forEveryone) { this.unimplemented("deleteMessage"); }
230
- async editMessage(_jid, _target, _text) { this.unimplemented("editMessage"); }
231
- async sendPresenceUpdate(_state, _jid) { this.unimplemented("sendPresenceUpdate"); }
232
- async readMessages(_keys) { this.unimplemented("readMessages"); }
233
- async onWhatsApp(_jid) { this.unimplemented("onWhatsApp"); }
234
- async getBusinessProfile(_jid) { this.unimplemented("getBusinessProfile"); }
235
- async profilePictureUrl(_jid) { this.unimplemented("profilePictureUrl"); }
236
- async fetchStatus(_jid) { this.unimplemented("fetchStatus"); }
237
- async updateBlockStatus(_jid, _action) { this.unimplemented("updateBlockStatus"); }
238
- async addOrEditContact(_jid, _info) { this.unimplemented("addOrEditContact"); }
239
- async removeContact(_jid) { this.unimplemented("removeContact"); }
240
- async groupMetadata(_jid) { this.unimplemented("groupMetadata"); }
241
- async groupParticipantsUpdate(_jid, _users, _action) { this.unimplemented("groupParticipantsUpdate"); }
242
- async groupUpdateSubject(_jid, _subject) { this.unimplemented("groupUpdateSubject"); }
243
- async groupUpdateDescription(_jid, _description) { this.unimplemented("groupUpdateDescription"); }
244
- async groupInviteCode(_jid) { this.unimplemented("groupInviteCode"); }
245
- async groupRevokeInvite(_jid) { this.unimplemented("groupRevokeInvite"); }
246
- async updateProfilePicture(_jid, _buffer) { this.unimplemented("updateProfilePicture"); }
247
- async updateProfileName(_name) { this.unimplemented("updateProfileName"); }
248
- async updateProfileStatus(_status) { this.unimplemented("updateProfileStatus"); }
249
- me() { this.unimplemented("me"); }
250
- async downloadMedia(_msg, _opts) { this.unimplemented("downloadMedia"); }
251
- }
252
- export const whatsmeowContract = new WhatsmeowClient();
@@ -1,79 +0,0 @@
1
- /**
2
- * drivers/whatsmeow/index.ts
3
- *
4
- * Public surface of the whatsmeow driver:
5
- * - whatsmeowContract : the raw contract (test-only / fallback path)
6
- * - wrapWithSupervisor(...) : returns a contract whose lifecycle
7
- * methods (connect / disconnect / isReady)
8
- * are gated on the supervisor state
9
- *
10
- * The supervisor is the lifecycle authority for the subprocess; this
11
- * proxy exists so the DriverManager sees one contract whose `isReady()`
12
- * never lies — true only when both the gRPC client is up AND the
13
- * subprocess has answered HealthCheck{ready:true}.
14
- */
15
- import { whatsmeowContract } from "./client.js";
16
- export { whatsmeowContract };
17
- export { startWhatsmeowSupervisor } from "./supervisor.js";
18
- /**
19
- * Wraps a raw whatsmeow contract so its lifecycle methods delegate to
20
- * the supervisor. Send/event methods still pass through unchanged —
21
- * the contract already knows how to talk gRPC; the supervisor only
22
- * owns "is it safe to use right now?".
23
- */
24
- export function wrapWithSupervisor(contract, supervisor) {
25
- return {
26
- name: contract.name,
27
- async connect() {
28
- await supervisor.whenReady();
29
- await contract.connect();
30
- },
31
- async disconnect() {
32
- // Try to stop the subprocess too — disconnecting the contract
33
- // alone would leave the Go process running until the bot shuts
34
- // down. Idempotent; safe to call multiple times.
35
- await Promise.allSettled([
36
- contract.disconnect(),
37
- supervisor.shutdown(),
38
- ]);
39
- },
40
- isReady: () => supervisor.isReady() && contract.isReady(),
41
- on: (...args) => contract.on(...args),
42
- resolveLid: contract.resolveLid
43
- ? (lid) => contract.resolveLid(lid)
44
- : undefined,
45
- sendText: (...args) => contract.sendText(...args),
46
- sendImage: (...args) => contract.sendImage(...args),
47
- sendVideo: (...args) => contract.sendVideo(...args),
48
- sendAudio: (...args) => contract.sendAudio(...args),
49
- sendSticker: (...args) => contract.sendSticker(...args),
50
- sendDocument: (...args) => contract.sendDocument(...args),
51
- sendPoll: (...args) => contract.sendPoll(...args),
52
- react: (...args) => contract.react(...args),
53
- deleteMessage: (...args) => contract.deleteMessage(...args),
54
- editMessage: (...args) => contract.editMessage(...args),
55
- sendPresenceUpdate: (...args) => contract.sendPresenceUpdate(...args),
56
- readMessages: (...args) => contract.readMessages(...args),
57
- onWhatsApp: (...args) => contract.onWhatsApp(...args),
58
- getBusinessProfile: (...args) => contract.getBusinessProfile(...args),
59
- profilePictureUrl: (...args) => contract.profilePictureUrl(...args),
60
- fetchStatus: (...args) => contract.fetchStatus(...args),
61
- updateBlockStatus: (...args) => contract.updateBlockStatus(...args),
62
- addOrEditContact: (...args) => contract.addOrEditContact(...args),
63
- removeContact: (...args) => contract.removeContact(...args),
64
- groupMetadata: (...args) => contract.groupMetadata(...args),
65
- groupParticipantsUpdate: (...args) => contract.groupParticipantsUpdate(...args),
66
- groupUpdateSubject: (...args) => contract.groupUpdateSubject(...args),
67
- groupUpdateDescription: (...args) => contract.groupUpdateDescription(...args),
68
- groupInviteCode: (...args) => contract.groupInviteCode(...args),
69
- groupRevokeInvite: (...args) => contract.groupRevokeInvite(...args),
70
- updateProfilePicture: (...args) => contract.updateProfilePicture(...args),
71
- updateProfileName: (...args) => contract.updateProfileName(...args),
72
- updateProfileStatus: (...args) => contract.updateProfileStatus(...args),
73
- me: () => contract.me(),
74
- downloadMedia: (...args) => contract.downloadMedia(...args),
75
- getHistory: contract.getHistory
76
- ? (...args) => contract.getHistory(...args)
77
- : undefined,
78
- };
79
- }