@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.
- package/README.md +20 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +56 -5
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/drivers/baileys/adapter.js +58 -7
- package/dist/drivers/baileys/api/index.js +172 -24
- package/dist/drivers/baileys/index.js +62 -30
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +158 -4
- package/dist/drivers/baileys/messageHandler.test.js +203 -0
- package/dist/drivers/baileysAdapter.test.js +281 -0
- package/dist/drivers/jid.test.js +40 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +15 -2
- package/dist/kernel/activeDriverSend.js +21 -0
- package/dist/kernel/activeDriverSend.test.js +89 -0
- package/dist/kernel/alerts.js +3 -9
- package/dist/kernel/chatSession.js +65 -0
- package/dist/kernel/chatSession.test.js +46 -0
- package/dist/kernel/commandAccess.js +66 -0
- package/dist/kernel/commandAccess.test.js +74 -0
- package/dist/kernel/commandDeprecation.js +168 -0
- package/dist/kernel/commandDeprecation.test.js +107 -0
- package/dist/kernel/commandMenu.js +268 -0
- package/dist/kernel/commandMenu.test.js +234 -0
- package/dist/kernel/commandPermissions.js +125 -0
- package/dist/kernel/commandPermissions.test.js +159 -0
- package/dist/kernel/commandRegistry.js +459 -0
- package/dist/kernel/commandRegistry.test.js +156 -0
- package/dist/kernel/commandsConfig.js +517 -0
- package/dist/kernel/commandsConfig.test.js +236 -0
- package/dist/kernel/contactAutoSave.test.js +87 -0
- package/dist/kernel/driverManager.js +10 -6
- package/dist/kernel/driverManager.test.js +90 -0
- package/dist/kernel/integrationMode.js +88 -0
- package/dist/kernel/integrationMode.test.js +95 -0
- package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
- package/dist/kernel/pluginApi.test.js +583 -0
- package/dist/kernel/pluginGuard.js +15 -12
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +96 -1
- package/dist/kernel/pluginLoader.test.js +80 -0
- package/dist/kernel/runCommand.js +245 -0
- package/dist/kernel/runCommand.test.js +235 -0
- package/dist/kernel/sendFallbackGuard.js +19 -48
- package/dist/kernel/sendFallbackGuard.test.js +80 -0
- package/dist/kernel/sendGuard.js +38 -42
- package/dist/kernel/sendGuard.test.js +102 -0
- package/dist/kernel/settingsDb.js +4 -3
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +183 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +64 -13
- package/dist/locales/es.json +64 -13
- package/dist/locales/pt.json +64 -13
- package/dist/logger/logger.js +23 -3
- package/dist/logger/logger.test.js +45 -0
- package/dist/main.js +5 -76
- package/dist/plugins/__manybot_integration__/index.js +167 -0
- package/dist/plugins/__manybot_integration__/index.test.js +184 -0
- package/package.json +74 -17
- package/dist/drivers/whatsmeow/client.js +0 -252
- package/dist/drivers/whatsmeow/index.js +0 -79
- package/dist/drivers/whatsmeow/installer.js +0 -86
- package/dist/drivers/whatsmeow/supervisor.js +0 -328
- 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.
|
|
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": "
|
|
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
|
-
"
|
|
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/*":
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"#
|
|
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
|
-
}
|