@manybot/manybot 5.7.0 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +28 -3
  2. package/dist/client/banner.js +10 -0
  3. package/dist/client/banner.test.js +31 -0
  4. package/dist/client/store.js +91 -6
  5. package/dist/client/store.test.js +170 -0
  6. package/dist/config.js +28 -44
  7. package/dist/config.test.js +26 -0
  8. package/dist/download/queue.js +13 -4
  9. package/dist/drivers/baileys/adapter.js +133 -15
  10. package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
  11. package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
  12. package/dist/drivers/baileys/api/index.js +384 -62
  13. package/dist/drivers/baileys/index.js +92 -36
  14. package/dist/drivers/baileys/loginPrompt.js +0 -2
  15. package/dist/drivers/baileys/messageHandler.js +344 -4
  16. package/dist/drivers/baileys/messageHandler.test.js +445 -0
  17. package/dist/drivers/baileysAdapter.test.js +378 -0
  18. package/dist/drivers/jid.js +26 -0
  19. package/dist/drivers/jid.test.js +74 -0
  20. package/dist/drivers/types.js +5 -5
  21. package/dist/i18n/index.js +20 -24
  22. package/dist/kernel/activeDriverSend.js +21 -0
  23. package/dist/kernel/activeDriverSend.test.js +89 -0
  24. package/dist/kernel/alerts.js +3 -9
  25. package/dist/kernel/chatOverrides.js +46 -0
  26. package/dist/kernel/chatOverrides.test.js +59 -0
  27. package/dist/kernel/chatSession.js +65 -0
  28. package/dist/kernel/chatSession.test.js +46 -0
  29. package/dist/kernel/commandAccess.js +66 -0
  30. package/dist/kernel/commandAccess.test.js +74 -0
  31. package/dist/kernel/commandDeprecation.js +170 -0
  32. package/dist/kernel/commandDeprecation.test.js +114 -0
  33. package/dist/kernel/commandMenu.js +357 -0
  34. package/dist/kernel/commandMenu.test.js +363 -0
  35. package/dist/kernel/commandPermissions.js +171 -0
  36. package/dist/kernel/commandPermissions.test.js +227 -0
  37. package/dist/kernel/commandRegistry.js +583 -0
  38. package/dist/kernel/commandRegistry.test.js +158 -0
  39. package/dist/kernel/commandsConfig.js +949 -0
  40. package/dist/kernel/commandsConfig.test.js +482 -0
  41. package/dist/kernel/contactAutoSave.js +6 -6
  42. package/dist/kernel/contactAutoSave.test.js +87 -0
  43. package/dist/kernel/coreCommands.js +62 -0
  44. package/dist/kernel/driverManager.js +10 -6
  45. package/dist/kernel/driverManager.test.js +90 -0
  46. package/dist/kernel/integrationMode.js +88 -0
  47. package/dist/kernel/integrationMode.test.js +95 -0
  48. package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
  49. package/dist/kernel/pluginApi.test.js +600 -0
  50. package/dist/kernel/pluginGuard.js +18 -13
  51. package/dist/kernel/pluginGuard.test.js +39 -0
  52. package/dist/kernel/pluginLoader.js +169 -11
  53. package/dist/kernel/pluginLoader.test.js +190 -0
  54. package/dist/kernel/runCommand.js +284 -0
  55. package/dist/kernel/runCommand.test.js +497 -0
  56. package/dist/kernel/sendFallbackGuard.js +19 -48
  57. package/dist/kernel/sendFallbackGuard.test.js +80 -0
  58. package/dist/kernel/sendGuard.js +38 -42
  59. package/dist/kernel/sendGuard.test.js +102 -0
  60. package/dist/kernel/settingsDb.js +19 -5
  61. package/dist/kernel/statusServer.js +9 -2
  62. package/dist/kernel/statusServer.test.js +70 -0
  63. package/dist/kernel/testConfig.js +192 -0
  64. package/dist/kernel/testConfig.test.js +181 -0
  65. package/dist/kernel/updateCheck.js +33 -10
  66. package/dist/locales/en.json +77 -13
  67. package/dist/locales/es.json +77 -13
  68. package/dist/locales/pt.json +77 -13
  69. package/dist/logger/logger.js +23 -3
  70. package/dist/logger/logger.test.js +45 -0
  71. package/dist/main.js +5 -76
  72. package/dist/plugins/__manybot_integration__/index.js +184 -0
  73. package/dist/plugins/__manybot_integration__/index.test.js +218 -0
  74. package/dist/utils/phoneNumber.js +83 -0
  75. package/dist/utils/phoneNumber.test.js +53 -0
  76. package/package.json +76 -18
  77. package/dist/drivers/whatsmeow/client.js +0 -252
  78. package/dist/drivers/whatsmeow/index.js +0 -79
  79. package/dist/drivers/whatsmeow/installer.js +0 -86
  80. package/dist/drivers/whatsmeow/supervisor.js +0 -328
  81. package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
@@ -0,0 +1,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.7.0",
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": "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 && 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": "6.7.24",
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/*": "./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"
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
- }