@manybot/manybot 5.5.4 → 5.6.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 (40) hide show
  1. package/README.md +15 -1
  2. package/dist/client/cache.js +1 -1
  3. package/dist/client/store.js +9 -0
  4. package/dist/config.js +230 -12
  5. package/dist/drivers/baileys/adapter.js +556 -0
  6. package/dist/drivers/{whatsapp → baileys}/api/index.js +533 -386
  7. package/dist/drivers/baileys/index.js +560 -0
  8. package/dist/drivers/{whatsapp → baileys}/loginPrompt.js +2 -0
  9. package/dist/drivers/{whatsapp → baileys}/messageHandler.js +46 -16
  10. package/dist/drivers/{whatsapp → baileys}/sdk/baileysSock.js +0 -29
  11. package/dist/drivers/jid.js +31 -0
  12. package/dist/drivers/types.js +14 -0
  13. package/dist/drivers/whatsmeow/client.js +203 -0
  14. package/dist/drivers/whatsmeow/index.js +79 -0
  15. package/dist/drivers/whatsmeow/installer.js +70 -0
  16. package/dist/drivers/whatsmeow/supervisor.js +309 -0
  17. package/dist/drivers/whatsmeow/whatsmeow.proto +64 -0
  18. package/dist/i18n/index.js +8 -12
  19. package/dist/kernel/alerts.js +190 -0
  20. package/dist/kernel/contactAutoSave.js +200 -0
  21. package/dist/kernel/driverManager.js +117 -0
  22. package/dist/kernel/pluginApi.js +25 -7
  23. package/dist/kernel/pluginLoader.js +12 -12
  24. package/dist/kernel/sendFallbackGuard.js +173 -0
  25. package/dist/kernel/sendGuard.js +143 -33
  26. package/dist/kernel/statusServer.js +39 -0
  27. package/dist/kernel/updateCheck.js +88 -0
  28. package/dist/kernel/waContract.js +16 -0
  29. package/dist/locales/en.json +15 -1
  30. package/dist/locales/es.json +15 -1
  31. package/dist/locales/pt.json +15 -1
  32. package/dist/main.js +77 -5
  33. package/dist/types.js +18 -11
  34. package/package.json +6 -8
  35. package/dist/core/adapter.js +0 -12
  36. package/dist/core/capabilities.js +0 -16
  37. package/dist/core/types.js +0 -6
  38. package/dist/drivers/index.js +0 -14
  39. package/dist/drivers/whatsapp/adapter.js +0 -7
  40. package/dist/drivers/whatsapp/index.js +0 -382
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "SyntaxError!",
6
6
  "email": "me@stxerr.dev"
7
7
  },
8
- "version": "5.5.4",
8
+ "version": "5.6.0",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {
@@ -25,28 +25,31 @@
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 });\"",
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",
29
29
  "start": "node dist/main.js",
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.0.0",
34
+ "@types/nodemailer": "^8.0.1",
34
35
  "tsx": "^4.19.2",
35
36
  "typescript": "^5.7.3"
36
37
  },
37
38
  "dependencies": {
38
39
  "@clack/prompts": "^0.10.1",
40
+ "@grpc/grpc-js": "^1.14.4",
41
+ "@grpc/proto-loader": "^0.7.15",
39
42
  "@hapi/boom": "^10.0.1",
40
43
  "@whiskeysockets/baileys": "6.7.23",
41
44
  "node-cron": "^4.6.0",
42
45
  "node-webpmux": "^3.2.1",
46
+ "nodemailer": "^9.0.3",
43
47
  "pino": "^10.3.1",
44
48
  "qrcode-terminal": "^0.12.0",
45
49
  "smol-toml": "^1.7.0"
46
50
  },
47
51
  "imports": {
48
52
  "#drivers/*": "./dist/drivers/*",
49
- "#core/*": "./dist/core/*",
50
53
  "#client/*": "./dist/client/*",
51
54
  "#kernel/*": "./dist/kernel/*",
52
55
  "#manyapi": "./dist/kernel/pluginApi.js",
@@ -59,10 +62,5 @@
59
62
  "#config": "./dist/config.js",
60
63
  "#main": "./dist/main.js",
61
64
  "#types": "./dist/types.js"
62
- },
63
- "allowScripts": {
64
- "@whiskeysockets/baileys@6.7.23": true,
65
- "esbuild@0.28.1": true,
66
- "protobufjs@7.6.4": true
67
65
  }
68
66
  }
@@ -1,12 +0,0 @@
1
- /**
2
- * core/adapter.ts
3
- *
4
- * Contract every messaging driver (whatsapp, discord, telegram, ...)
5
- * must implement. The kernel talks only to this interface, never to a
6
- * driver's native client.
7
- *
8
- * Required methods must work on every driver. Optional methods are gated
9
- * by `capabilities.has(...)` before being called - a driver that doesn't
10
- * support a capability simply omits the method.
11
- */
12
- export {};
@@ -1,16 +0,0 @@
1
- /**
2
- * core/capabilities.ts
3
- *
4
- * Optional features a driver may or may not support. The kernel checks
5
- * `adapter.capabilities.has(...)` before calling an optional method,
6
- * instead of assuming every platform behaves like WhatsApp.
7
- */
8
- export class CapabilitySet {
9
- set;
10
- constructor(capabilities) {
11
- this.set = new Set(capabilities);
12
- }
13
- has(capability) {
14
- return this.set.has(capability);
15
- }
16
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * core/types.ts
3
- *
4
- * Common domain models shared across core, drivers, and plugin boundaries.
5
- */
6
- export {};
@@ -1,14 +0,0 @@
1
- import { whatsappDriver } from "./whatsapp/index.js";
2
- import { PLATFORM } from "#config";
3
- import { applyPatches } from "./patches/index.js";
4
- applyPatches();
5
- const DRIVERS = {
6
- whatsapp: whatsappDriver,
7
- };
8
- export function initializeSelectedDriver() {
9
- const driver = DRIVERS[PLATFORM];
10
- if (!driver) {
11
- throw new Error(`Unsupported platform/driver: ${PLATFORM}`);
12
- }
13
- return driver;
14
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * drivers/whatsapp/adapter.ts (DEPRECATED)
3
- *
4
- * This file exists for backward compatibility.
5
- * All logic has been moved to ./index.ts
6
- */
7
- export { whatsappDriver } from "./index.js";
@@ -1,382 +0,0 @@
1
- /**
2
- * drivers/whatsapp/index.ts
3
- *
4
- * Main WhatsApp driver entry point.
5
- * Implements the BotDriver interface for WhatsApp (Baileys).
6
- *
7
- * Responsibilities:
8
- * - Socket lifecycle (connect/disconnect)
9
- * - Message routing to kernel
10
- * - Plugin context building (via buildApi)
11
- * - Event handling and plugin execution
12
- */
13
- import { createSocket, normalizeJid, sessionDir, AUTH_DIR, store as sharedStore } from "./sdk/baileysSock.js";
14
- import { handleMessage } from "./messageHandler.js";
15
- import { loadPlugins, setupPlugins } from "#kernel/pluginLoader.js";
16
- import { logger } from "#logger";
17
- import { PLUGINS, CLIENT_ID } from "#config";
18
- import { t } from "#i18n";
19
- import { printBanner } from "#client/banner.js";
20
- import { loadChatCache, saveChatCache, isCacheFresh } from "#client/cache.js";
21
- import { DisconnectReason, normalizeMessageContent } from "@whiskeysockets/baileys";
22
- import fs from "fs/promises";
23
- import * as clack from "@clack/prompts";
24
- import { copyToClipboard } from "#utils/clipboard.js";
25
- let state = "BOOT";
26
- let shuttingDown = false;
27
- let currentSock = null;
28
- let currentStore = null;
29
- let reconnectTimer = null;
30
- let connecting = false;
31
- let reconnectAttempts = 0;
32
- let cacheHydrated = false;
33
- let cacheSaveTimer = null;
34
- // ── Per-chat message queue ──────────────────────────────────────────────────
35
- // Messages from the same chat are processed one at a time (in order), but
36
- // different chats run concurrently — a slow plugin in one chat (e.g. sticker
37
- // generation) no longer blocks replies in every other chat.
38
- const chatQueues = new Map();
39
- function enqueueForChat(jid, task) {
40
- const prev = chatQueues.get(jid) ?? Promise.resolve();
41
- const settled = prev.catch(() => { }).then(task).catch((e) => {
42
- const err = e instanceof Error ? e : new Error(String(e));
43
- logger.error(`${err.message}\n${err.stack}`);
44
- });
45
- chatQueues.set(jid, settled);
46
- settled.finally(() => {
47
- if (chatQueues.get(jid) === settled)
48
- chatQueues.delete(jid);
49
- });
50
- }
51
- // Messages older than this (WhatsApp's own delivery delay — e.g. backlog
52
- // dumped after the bot reconnects) are skipped. Checked at arrival time,
53
- // so time spent waiting in chatQueues never counts against a message.
54
- const MAX_MESSAGE_AGE_SECONDS = 60;
55
- function isMessageStale(msg) {
56
- const msgTimestamp = Number(msg.messageTimestamp);
57
- if (!msgTimestamp)
58
- return false;
59
- const nowInSeconds = Math.floor(Date.now() / 1000);
60
- const age = nowInSeconds - msgTimestamp;
61
- if (age > MAX_MESSAGE_AGE_SECONDS) {
62
- logger.debug(`[whatsapp] Skipping stale message (age: ${age}s, id: ${msg.key.id})`);
63
- return true;
64
- }
65
- return false;
66
- }
67
- const RECONNECT_BASE_MS = 1000;
68
- const RECONNECT_MAX_MS = 60000;
69
- const CACHE_SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5min
70
- /**
71
- * Loads the on-disk cache and merges it into `store` (union, never
72
- * overwrite — see client/cache.ts). Runs once per process: the shared
73
- * store singleton already accumulates across reconnects, so re-hydrating
74
- * later would just redo a no-op merge.
75
- */
76
- async function hydrateFromCache(store) {
77
- if (cacheHydrated)
78
- return;
79
- cacheHydrated = true;
80
- const snapshot = await loadChatCache();
81
- if (!snapshot)
82
- return;
83
- store.hydrate(snapshot);
84
- const fresh = await isCacheFresh();
85
- const count = snapshot.chats.length;
86
- const key = fresh ? "system.cacheLoaded" : "system.cacheLoadedStale";
87
- logger.info(`[cache] ${t(key, { count })}`);
88
- }
89
- function startCacheAutosave(store) {
90
- if (cacheSaveTimer)
91
- return;
92
- cacheSaveTimer = setInterval(() => { saveChatCache(store); }, CACHE_SAVE_INTERVAL_MS);
93
- }
94
- function stopCacheAutosave() {
95
- if (!cacheSaveTimer)
96
- return;
97
- clearInterval(cacheSaveTimer);
98
- cacheSaveTimer = null;
99
- }
100
- function nextBackoffMs() {
101
- const delay = RECONNECT_BASE_MS * 2 ** reconnectAttempts;
102
- reconnectAttempts++;
103
- return Math.min(delay, RECONNECT_MAX_MS);
104
- }
105
- function teardownSock(sock) {
106
- if (!sock)
107
- return;
108
- try {
109
- sock.ev.removeAllListeners();
110
- }
111
- catch { }
112
- try {
113
- sock.end(undefined);
114
- }
115
- catch { }
116
- }
117
- function scheduleReconnect(delayMs) {
118
- if (shuttingDown)
119
- return;
120
- if (reconnectTimer)
121
- clearTimeout(reconnectTimer);
122
- reconnectTimer = setTimeout(() => {
123
- reconnectTimer = null;
124
- startBot();
125
- }, delayMs);
126
- }
127
- async function startBot() {
128
- if (connecting)
129
- return;
130
- connecting = true;
131
- await hydrateFromCache(sharedStore);
132
- const previousSock = currentSock;
133
- const { sock, store } = await createSocket();
134
- teardownSock(previousSock);
135
- currentSock = sock;
136
- currentStore = store;
137
- connecting = false;
138
- let pluginsReady = false;
139
- // ── Normal bot mode ─────────────────────────────────────────────────────────
140
- sock.ev.on("connection.update", async (update) => {
141
- const { connection, lastDisconnect } = update;
142
- if (connection === "open") {
143
- state = "READY_INIT";
144
- reconnectAttempts = 0;
145
- logger.success(t("system.connected"));
146
- logger.info(t("system.clientId", { id: CLIENT_ID }));
147
- printBanner();
148
- if (!pluginsReady) {
149
- pluginsReady = true;
150
- await loadPlugins(PLUGINS);
151
- await setupPlugins(sock, store);
152
- }
153
- startCacheAutosave(store);
154
- // buffer anti-replay / sync ghost messages
155
- setTimeout(() => { state = "READY"; }, 2000);
156
- }
157
- if (connection === "close") {
158
- const code = lastDisconnect?.error?.output?.statusCode;
159
- const loggedOut = code === DisconnectReason.loggedOut;
160
- state = "BOOT";
161
- logger.warn(t("system.disconnected", { reason: String(code) }));
162
- if (loggedOut) {
163
- logger.warn(t("system.sessionExpired"));
164
- try {
165
- await fs.rm(AUTH_DIR, { recursive: true, force: true });
166
- }
167
- catch (e) {
168
- logger.error(`[whatsapp] Failed to remove session dir: ${e.message}`);
169
- }
170
- scheduleReconnect(1000);
171
- }
172
- else if (!shuttingDown) {
173
- const delay = nextBackoffMs();
174
- logger.info(t("system.reconnecting", { secs: Math.round(delay / 1000) }));
175
- scheduleReconnect(delay);
176
- }
177
- }
178
- });
179
- // Incoming messages
180
- sock.ev.on("messages.upsert", async ({ messages, type }) => {
181
- if (state !== "READY")
182
- return;
183
- if (type !== "notify" && type !== "append")
184
- return;
185
- for (const msg of messages) {
186
- const m = msg;
187
- if (type === "append" && !m.key.fromMe)
188
- continue;
189
- const body = getBodyQuick(m);
190
- if (!body && !msgHasMediaQuick(m))
191
- continue;
192
- if (isMessageStale(m))
193
- continue;
194
- const jid = normalizeJid(m.key.remoteJid ?? "");
195
- enqueueForChat(jid, () => handleMessage(m, sock, store));
196
- }
197
- });
198
- }
199
- /** Quick body extraction to avoid importing helpers here. */
200
- function getBodyQuick(msg) {
201
- const m = normalizeMessageContent(msg.message);
202
- if (!m)
203
- return "";
204
- return (m.conversation ??
205
- m.extendedTextMessage?.text ??
206
- m.imageMessage?.caption ??
207
- m.videoMessage?.caption ??
208
- "");
209
- }
210
- function msgHasMediaQuick(msg) {
211
- const m = normalizeMessageContent(msg.message);
212
- return !!(m?.imageMessage || m?.videoMessage || m?.audioMessage || m?.documentMessage || m?.stickerMessage);
213
- }
214
- export const whatsappDriver = {
215
- async connect() {
216
- shuttingDown = false;
217
- await startBot();
218
- },
219
- async disconnect() {
220
- shuttingDown = true;
221
- reconnectAttempts = 0;
222
- if (reconnectTimer) {
223
- clearTimeout(reconnectTimer);
224
- reconnectTimer = null;
225
- }
226
- stopCacheAutosave();
227
- if (currentStore)
228
- await saveChatCache(currentStore);
229
- teardownSock(currentSock);
230
- currentSock = null;
231
- currentStore = null;
232
- },
233
- /**
234
- * Diagnostic mode: connects on its own session (separate from the
235
- * running bot's, so it doesn't compete for the same WhatsApp Web
236
- * slot), waits for the initial chat sync, then shows an interactive
237
- * list — arrow keys to navigate, Enter to pick. The selected chat's
238
- * id is normalized, resolved from `@lid` to the real phone-based JID
239
- * when known, copied to the clipboard, and printed.
240
- */
241
- async getId() {
242
- logger.info(`[getid] ${t("getid.connecting")}`);
243
- await hydrateFromCache(sharedStore);
244
- const CONNECT_TIMEOUT_MS = 25000;
245
- const MAX_ATTEMPTS = 3;
246
- const MAX_ROUNDS = 2;
247
- const getidAuthDir = `${CLIENT_ID}-getid`;
248
- let sock = null;
249
- let store = null;
250
- for (let round = 1; round <= MAX_ROUNDS && !sock; round++) {
251
- for (let attempt = 1; attempt <= MAX_ATTEMPTS && !sock; attempt++) {
252
- const created = await createSocket(getidAuthDir);
253
- // Not a spinner here on purpose: if this session isn't paired yet,
254
- // Baileys prints the QR/pairing code through the normal logger
255
- // right after this — a spinner redrawing the line would bury it,
256
- // so the person never gets a chance to approve it on their phone
257
- // and the connection hangs forever waiting for "open".
258
- const opened = await new Promise((resolve) => {
259
- let settled = false;
260
- const timer = setTimeout(() => {
261
- if (!settled) {
262
- settled = true;
263
- resolve(false);
264
- }
265
- }, CONNECT_TIMEOUT_MS);
266
- created.sock.ev.on("connection.update", (update) => {
267
- if (settled)
268
- return;
269
- if (update.connection === "open") {
270
- settled = true;
271
- clearTimeout(timer);
272
- resolve(true);
273
- }
274
- else if (update.connection === "close") {
275
- const code = update.lastDisconnect?.error?.output?.statusCode;
276
- settled = true;
277
- clearTimeout(timer);
278
- logger.warn(`[getid] ${t("getid.connectFailed", { reason: String(code ?? "?") })}`);
279
- resolve(false);
280
- }
281
- });
282
- });
283
- if (opened) {
284
- sock = created.sock;
285
- store = created.store;
286
- break;
287
- }
288
- try {
289
- created.sock.ev?.removeAllListeners();
290
- created.sock.end(undefined);
291
- }
292
- catch { }
293
- if (attempt < MAX_ATTEMPTS) {
294
- logger.info(`[getid] ${t("getid.retrying", { attempt: attempt + 1, max: MAX_ATTEMPTS })}`);
295
- await new Promise((r) => setTimeout(r, 2000));
296
- }
297
- }
298
- // Exhausted every attempt this round without ever reaching "open" —
299
- // likely a corrupted/stuck -getid session (not the normal first-pairing
300
- // 515, which already succeeds within a round). Wipe it and re-pair
301
- // from scratch instead of forcing the person to rerun the command.
302
- if (!sock && round < MAX_ROUNDS) {
303
- logger.warn(`[getid] ${t("getid.sessionWiped", { round: round + 1, max: MAX_ROUNDS })}`);
304
- try {
305
- await fs.rm(sessionDir(getidAuthDir), { recursive: true, force: true });
306
- }
307
- catch (e) {
308
- logger.error(`[getid] Failed to remove session dir: ${e.message}`);
309
- }
310
- await new Promise((r) => setTimeout(r, 1000));
311
- }
312
- }
313
- if (!sock || !store) {
314
- logger.error(`[getid] ${t("getid.connectGaveUp")}`);
315
- return;
316
- }
317
- const s = clack.spinner();
318
- s.start(t("getid.syncingChats"));
319
- // History sync arrives as several independent streams (chats,
320
- // contacts, push-names...), each firing its own `isLatest: true` when
321
- // THAT stream ends — not when everything is done. Stopping on the
322
- // first one cuts the others short (fewer chats, missing names).
323
- // Instead, wait for a quiet period with no sync activity at all.
324
- let lastActivityAt = null;
325
- const markActivity = () => { lastActivityAt = Date.now(); };
326
- sock.ev.on("messaging-history.set", markActivity);
327
- sock.ev.on("chats.upsert", markActivity);
328
- sock.ev.on("contacts.upsert", markActivity);
329
- sock.ev.on("contacts.update", markActivity);
330
- const QUIET_MS = 2000;
331
- const deadline = Date.now() + 20000;
332
- while (Date.now() < deadline) {
333
- if (lastActivityAt !== null && Date.now() - lastActivityAt >= QUIET_MS)
334
- break;
335
- await new Promise((r) => setTimeout(r, 200));
336
- }
337
- const chats = store.chats.all();
338
- s.stop(t("getid.chatsFound", { count: chats.length }));
339
- await saveChatCache(store);
340
- try {
341
- sock.ev?.removeAllListeners();
342
- sock.end(undefined);
343
- }
344
- catch { }
345
- if (chats.length === 0) {
346
- logger.warn(`[getid] ${t("getid.noChatsSynced")}`);
347
- return;
348
- }
349
- const options = chats
350
- .map((chat) => {
351
- const isGroup = chat.id.endsWith("@g.us");
352
- const resolved = normalizeJid(store.resolveJid(chat.id));
353
- const contact = store.contacts[chat.id] ?? store.contacts[resolved];
354
- const hasChatName = chat.name && chat.name !== chat.id.split("@")[0];
355
- const displayName = hasChatName ? chat.name : contact?.name ?? contact?.notify ?? resolved;
356
- return {
357
- value: resolved,
358
- label: `${isGroup ? "👥" : "👤"} ${displayName}`,
359
- hint: resolved,
360
- };
361
- })
362
- .sort((a, b) => a.label.localeCompare(b.label));
363
- const picked = await clack.multiselect({
364
- message: t("getid.pickPrompt"),
365
- options,
366
- required: true,
367
- });
368
- if (clack.isCancel(picked)) {
369
- clack.cancel(t("getid.cancelled"));
370
- return;
371
- }
372
- const ids = picked;
373
- const joined = ids.join("\n");
374
- const copied = await copyToClipboard(joined);
375
- if (copied) {
376
- logger.success(`[getid] ${t("getid.copied", { count: ids.length, ids: joined })}`);
377
- }
378
- else {
379
- logger.warn(`[getid] ${t("getid.copyFailed", { count: ids.length, ids: joined })}`);
380
- }
381
- },
382
- };