@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.
- package/README.md +28 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +91 -6
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +133 -15
- package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
- package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
- package/dist/drivers/baileys/api/index.js +384 -62
- package/dist/drivers/baileys/index.js +92 -36
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +344 -4
- package/dist/drivers/baileys/messageHandler.test.js +445 -0
- package/dist/drivers/baileysAdapter.test.js +378 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +74 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +20 -24
- 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/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- 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 +170 -0
- package/dist/kernel/commandDeprecation.test.js +114 -0
- package/dist/kernel/commandMenu.js +357 -0
- package/dist/kernel/commandMenu.test.js +363 -0
- package/dist/kernel/commandPermissions.js +171 -0
- package/dist/kernel/commandPermissions.test.js +227 -0
- package/dist/kernel/commandRegistry.js +583 -0
- package/dist/kernel/commandRegistry.test.js +158 -0
- package/dist/kernel/commandsConfig.js +949 -0
- package/dist/kernel/commandsConfig.test.js +482 -0
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/contactAutoSave.test.js +87 -0
- package/dist/kernel/coreCommands.js +62 -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 +600 -0
- package/dist/kernel/pluginGuard.js +18 -13
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +169 -11
- package/dist/kernel/pluginLoader.test.js +190 -0
- package/dist/kernel/runCommand.js +284 -0
- package/dist/kernel/runCommand.test.js +497 -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 +19 -5
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +192 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +77 -13
- package/dist/locales/es.json +77 -13
- package/dist/locales/pt.json +77 -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 +184 -0
- package/dist/plugins/__manybot_integration__/index.test.js +218 -0
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +76 -18
- 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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/phoneNumber.ts
|
|
3
|
+
*
|
|
4
|
+
* Phone-number normalization built on `libphonenumber-js`. Centralized so
|
|
5
|
+
* the Baileys adapter, the `whatsmeow` driver, and any future call site
|
|
6
|
+
* produce a single, consistent shape for the `IContact` returned to
|
|
7
|
+
* plugins:
|
|
8
|
+
*
|
|
9
|
+
* {
|
|
10
|
+
* number: "+12025550100", // E.164 with leading "+"
|
|
11
|
+
* numberRaw: "12025550100", // digits only, no "+"
|
|
12
|
+
* numberPretty: "+1 202 555-0100", // formatInternational()
|
|
13
|
+
* country: "US", // ISO 3166-1 alpha-2
|
|
14
|
+
* countryCallingCode: "1", // ITU calling code
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* When the input is not a parseable phone number (bot JIDs, group JIDs,
|
|
18
|
+
* random strings, malformed numbers) every field is `null`. The helper
|
|
19
|
+
* never throws and never calls the network — it's a pure synchronous
|
|
20
|
+
* transform over the digits.
|
|
21
|
+
*
|
|
22
|
+
* `libphonenumber-js` has many format variants; we pick `formatInternational()`
|
|
23
|
+
* for `numberPretty` because it's the form most users expect to see in
|
|
24
|
+
* a chat reply ("+63 938 346-4136") and it preserves the country-code
|
|
25
|
+
* prefix unambiguously.
|
|
26
|
+
*/
|
|
27
|
+
import parsePhoneNumber from "libphonenumber-js";
|
|
28
|
+
/** All-null {@link ParsedPhoneNumber} — the obvious return value when
|
|
29
|
+
* the input isn't a phone number at all. Exported so test code (and
|
|
30
|
+
* any consumer that wants to deep-compare) has a stable reference. */
|
|
31
|
+
export const NULL_PHONE = Object.freeze({
|
|
32
|
+
number: null,
|
|
33
|
+
numberRaw: null,
|
|
34
|
+
numberPretty: null,
|
|
35
|
+
country: null,
|
|
36
|
+
countryCallingCode: null,
|
|
37
|
+
});
|
|
38
|
+
function isParseable(p) {
|
|
39
|
+
// libphonenumber-js's `parsePhoneNumberFromString` returns a PhoneNumber
|
|
40
|
+
// even for garbage input — but `country` is only set when the parser
|
|
41
|
+
// could pin a country. Use that as the "this is a real phone" signal:
|
|
42
|
+
// a country-less parse has nothing useful to say about country/CC.
|
|
43
|
+
return Boolean(p && p.country);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a phone number into the canonical ManyBot shape. Accepts either
|
|
47
|
+
* a phone-based JID (`"5511999999999@s.whatsapp.net"`), a `@c.us` legacy
|
|
48
|
+
* form (`"5511999999999@c.us"`), or a plain digit string. Returns the
|
|
49
|
+
* all-null `NULL_PHONE` when the input is empty, a non-phone JID (group
|
|
50
|
+
* `@g.us`, bot meta-AI, status broadcast), or simply not parseable.
|
|
51
|
+
*/
|
|
52
|
+
export function parsePhone(value) {
|
|
53
|
+
if (!value)
|
|
54
|
+
return NULL_PHONE;
|
|
55
|
+
// Strip JID wrapper if present. Anything ending in a non-`@s.whatsapp.net` /
|
|
56
|
+
// non-`@c.us` server (groups, status, newsletters, bot meta) isn't a phone
|
|
57
|
+
// number at all — short-circuit to NULL_PHONE before calling the parser.
|
|
58
|
+
const isJid = value.includes("@");
|
|
59
|
+
if (isJid) {
|
|
60
|
+
const server = value.slice(value.indexOf("@") + 1);
|
|
61
|
+
if (server !== "s.whatsapp.net" && server !== "c.us")
|
|
62
|
+
return NULL_PHONE;
|
|
63
|
+
}
|
|
64
|
+
const digits = value.replace(/\D/g, "");
|
|
65
|
+
if (!digits)
|
|
66
|
+
return NULL_PHONE;
|
|
67
|
+
// Try the raw digit form first (covers the "5511999999999@s.whatsapp.net"
|
|
68
|
+
// case where the parser knows no default country); the "+"-prefixed form
|
|
69
|
+
// second (covers "5511999999999" when a default country is supplied by the
|
|
70
|
+
// caller). Without a country hint libphonenumber-js can't tell a Brazilian
|
|
71
|
+
// 55-prefix from a Portuguese 55-prefix and returns no country — that's
|
|
72
|
+
// why the wrapper JID case is the common path.
|
|
73
|
+
const parsed = parsePhoneNumber(value) ?? parsePhoneNumber("+" + digits);
|
|
74
|
+
if (!isParseable(parsed))
|
|
75
|
+
return NULL_PHONE;
|
|
76
|
+
return {
|
|
77
|
+
number: parsed.number,
|
|
78
|
+
numberRaw: digits,
|
|
79
|
+
numberPretty: parsed.formatInternational(),
|
|
80
|
+
country: parsed.country ?? null,
|
|
81
|
+
countryCallingCode: parsed.countryCallingCode ?? null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { parsePhone, NULL_PHONE } from "#utils/phoneNumber.js";
|
|
4
|
+
test("parsePhone — US number (NANPA +1)", () => {
|
|
5
|
+
// Use the 555-0100 reserved-for-fiction range so the example never
|
|
6
|
+
// collides with a real subscriber anywhere on Earth.
|
|
7
|
+
const r = parsePhone("+12025550100");
|
|
8
|
+
assert.equal(r.number, "+12025550100");
|
|
9
|
+
assert.equal(r.numberRaw, "12025550100");
|
|
10
|
+
assert.equal(r.country, "US");
|
|
11
|
+
assert.equal(r.countryCallingCode, "1");
|
|
12
|
+
assert.match(r.numberPretty, /^\+1 /);
|
|
13
|
+
});
|
|
14
|
+
test("parsePhone — BR number from @s.whatsapp.net JID", () => {
|
|
15
|
+
const r = parsePhone("5516999999999@s.whatsapp.net");
|
|
16
|
+
assert.equal(r.number, "+5516999999999");
|
|
17
|
+
assert.equal(r.numberRaw, "5516999999999");
|
|
18
|
+
assert.equal(r.country, "BR");
|
|
19
|
+
assert.equal(r.countryCallingCode, "55");
|
|
20
|
+
assert.match(r.numberPretty, /^\+55 /);
|
|
21
|
+
});
|
|
22
|
+
test("parsePhone — US number with leading +", () => {
|
|
23
|
+
const r = parsePhone("+14155552671");
|
|
24
|
+
assert.equal(r.country, "US");
|
|
25
|
+
assert.equal(r.countryCallingCode, "1");
|
|
26
|
+
assert.equal(r.numberRaw, "14155552671");
|
|
27
|
+
});
|
|
28
|
+
test("parsePhone — group JID is not a phone number", () => {
|
|
29
|
+
// @g.us must NOT be parsed as a phone number, even though its user-part
|
|
30
|
+
// is digit-only. The wrapper short-circuits to NULL_PHONE.
|
|
31
|
+
assert.deepEqual(parsePhone("120363402117932687@g.us"), NULL_PHONE);
|
|
32
|
+
});
|
|
33
|
+
test("parsePhone — status/broadcast/newsletter JIDs are not phones", () => {
|
|
34
|
+
assert.deepEqual(parsePhone("status@broadcast"), NULL_PHONE);
|
|
35
|
+
assert.deepEqual(parsePhone("0@c.us"), NULL_PHONE);
|
|
36
|
+
});
|
|
37
|
+
test("parsePhone — empty / null / undefined all return NULL_PHONE", () => {
|
|
38
|
+
assert.deepEqual(parsePhone(""), NULL_PHONE);
|
|
39
|
+
assert.deepEqual(parsePhone(null), NULL_PHONE);
|
|
40
|
+
assert.deepEqual(parsePhone(undefined), NULL_PHONE);
|
|
41
|
+
});
|
|
42
|
+
test("parsePhone — unparseable digit string returns NULL_PHONE", () => {
|
|
43
|
+
// 7 digits with no country hint → libphonenumber-js returns no country
|
|
44
|
+
// for the raw form and no useful result for the +N form either.
|
|
45
|
+
assert.deepEqual(parsePhone("1234567"), NULL_PHONE);
|
|
46
|
+
});
|
|
47
|
+
test("parsePhone — accepts the legacy @c.us form (framework internal JID)", () => {
|
|
48
|
+
// Some code paths still hand us the framework's normalized "@c.us" form
|
|
49
|
+
// instead of the raw "@s.whatsapp.net" one. Both should parse identically.
|
|
50
|
+
const wire = parsePhone("5516999999999@s.whatsapp.net");
|
|
51
|
+
const internal = parsePhone("5516999999999@c.us");
|
|
52
|
+
assert.deepEqual(internal, wire);
|
|
53
|
+
});
|
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.9.0",
|
|
9
9
|
"license": "GPL-3.0-only",
|
|
10
10
|
"private": false,
|
|
11
11
|
"engines": {
|
|
@@ -25,22 +25,35 @@
|
|
|
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 && npx tsx scripts/check-types-drift.ts"
|
|
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",
|
|
40
51
|
"@grpc/grpc-js": "^1.14.4",
|
|
41
52
|
"@grpc/proto-loader": "^0.7.15",
|
|
42
53
|
"@hapi/boom": "^10.0.1",
|
|
43
|
-
"@whiskeysockets/baileys": "
|
|
54
|
+
"@whiskeysockets/baileys": "^7.0.0-rc14",
|
|
55
|
+
"js-yaml": "^5.2.3",
|
|
56
|
+
"libphonenumber-js": "^1.13.11",
|
|
44
57
|
"node-cron": "^4.6.0",
|
|
45
58
|
"node-webpmux": "^3.2.1",
|
|
46
59
|
"nodemailer": "^9.0.3",
|
|
@@ -49,18 +62,63 @@
|
|
|
49
62
|
"smol-toml": "^1.7.0"
|
|
50
63
|
},
|
|
51
64
|
"imports": {
|
|
52
|
-
"#drivers/*":
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"#
|
|
65
|
+
"#drivers/*": {
|
|
66
|
+
"development": "./src/drivers/*",
|
|
67
|
+
"default": "./dist/drivers/*"
|
|
68
|
+
},
|
|
69
|
+
"#client/*": {
|
|
70
|
+
"development": "./src/client/*",
|
|
71
|
+
"default": "./dist/client/*"
|
|
72
|
+
},
|
|
73
|
+
"#kernel/*": {
|
|
74
|
+
"development": "./src/kernel/*",
|
|
75
|
+
"default": "./dist/kernel/*"
|
|
76
|
+
},
|
|
77
|
+
"#manyapi": {
|
|
78
|
+
"development": "./src/kernel/pluginApi.ts",
|
|
79
|
+
"default": "./dist/kernel/pluginApi.js"
|
|
80
|
+
},
|
|
81
|
+
"#settingsdb": {
|
|
82
|
+
"development": "./src/kernel/settingsDb.ts",
|
|
83
|
+
"default": "./dist/kernel/settingsDb.js"
|
|
84
|
+
},
|
|
85
|
+
"#sendguard": {
|
|
86
|
+
"development": "./src/kernel/sendGuard.ts",
|
|
87
|
+
"default": "./dist/kernel/sendGuard.js"
|
|
88
|
+
},
|
|
89
|
+
"#logger": {
|
|
90
|
+
"development": "./src/logger/logger.ts",
|
|
91
|
+
"default": "./dist/logger/logger.js"
|
|
92
|
+
},
|
|
93
|
+
"#utils/*": {
|
|
94
|
+
"development": "./src/utils/*",
|
|
95
|
+
"default": "./dist/utils/*"
|
|
96
|
+
},
|
|
97
|
+
"#i18n": {
|
|
98
|
+
"development": "./src/i18n/index.ts",
|
|
99
|
+
"default": "./dist/i18n/index.js"
|
|
100
|
+
},
|
|
101
|
+
"#download": {
|
|
102
|
+
"development": "./src/download/queue.ts",
|
|
103
|
+
"default": "./dist/download/queue.js"
|
|
104
|
+
},
|
|
105
|
+
"#config": {
|
|
106
|
+
"development": "./src/config.ts",
|
|
107
|
+
"default": "./dist/config.js"
|
|
108
|
+
},
|
|
109
|
+
"#main": {
|
|
110
|
+
"development": "./src/main.ts",
|
|
111
|
+
"default": "./dist/main.js"
|
|
112
|
+
},
|
|
113
|
+
"#types": {
|
|
114
|
+
"development": "./src/types.ts",
|
|
115
|
+
"default": "./dist/types.js"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"allowScripts": {
|
|
119
|
+
"@whiskeysockets/baileys@6.7.24": true,
|
|
120
|
+
"protobufjs@7.6.5": true,
|
|
121
|
+
"esbuild@0.28.2": true,
|
|
122
|
+
"unrs-resolver@1.12.2": true
|
|
65
123
|
}
|
|
66
124
|
}
|
|
@@ -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
|
-
}
|