@manybot/manybot 5.6.0 → 5.7.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.
@@ -1,7 +1,7 @@
1
- import { mkdirSync, writeFileSync, chmodSync } from "node:fs";
1
+ import { mkdirSync, writeFileSync, chmodSync, existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import * as clack from "@clack/prompts";
4
- import { persistConfigValue } from "#config";
4
+ import { persistConfigValue, CONFIG_DIR } from "#config";
5
5
  import { t } from "#i18n";
6
6
  const SUPPORTED = [
7
7
  { os: "linux", arch: "x64", name: "whatsmeow-service-linux-x64" },
@@ -22,7 +22,7 @@ async function fetchLatestTag() {
22
22
  return data.tag_name ?? "v5.6.1";
23
23
  }
24
24
  function binaryDir() {
25
- return path.resolve(process.cwd(), "whatsmeow-service", "bin");
25
+ return path.resolve(CONFIG_DIR, "whatsmeow-service", "bin");
26
26
  }
27
27
  export async function promptWhatsmeowInstall() {
28
28
  const target = detectTarget();
@@ -30,6 +30,16 @@ export async function promptWhatsmeowInstall() {
30
30
  clack.log.warn(str(t("whatsmeow.unsupportedArch", { os: process.platform, arch: process.arch })));
31
31
  return;
32
32
  }
33
+ const outPath = path.join(binaryDir(), "whatsmeow-service");
34
+ // Binary already on disk → skip the prompt and the download, but
35
+ // make sure the config flag is set so the supervisor boots on the
36
+ // next run. The earlier "no, declined install" flow only writes the
37
+ // TOML on success, so users who later build the binary by hand also
38
+ // hit this branch on their next first-login.
39
+ if (existsSync(outPath)) {
40
+ await persistConfigValue("driver_whatsmeow_enabled", "true");
41
+ return;
42
+ }
33
43
  const choice = await clack.confirm({
34
44
  message: str(t("whatsmeow.installPrompt")),
35
45
  initialValue: false,
@@ -61,10 +71,16 @@ export async function promptWhatsmeowInstall() {
61
71
  const buffer = Buffer.from(await res.arrayBuffer());
62
72
  const dir = binaryDir();
63
73
  mkdirSync(dir, { recursive: true });
64
- const outPath = path.join(dir, "whatsmeow-service");
65
74
  writeFileSync(outPath, buffer);
66
75
  chmodSync(outPath, 0o755);
67
76
  await persistConfigValue("driver_whatsmeow_enabled", "true");
68
77
  spin.stop(str(t("whatsmeow.installed", { path: outPath })));
69
78
  clack.note(str(t("whatsmeow.restartNotice")), str(t("whatsmeow.installTitle")));
79
+ // The bot is still in its very first run (no Baileys session yet, no
80
+ // supervisor spawned) — the config has just been updated on disk but
81
+ // the in-memory `CONFIG` object and the supervisor lifecycle were
82
+ // initialized at startup with `whatsmeow.enabled = false`. Restarting
83
+ // is required for the new value to take effect. Exit cleanly so the
84
+ // user just re-runs the bot.
85
+ setTimeout(() => process.exit(0), 100);
70
86
  }
@@ -30,12 +30,13 @@
30
30
  * See the lifecycle contract this implements.
31
31
  */
32
32
  import { spawn } from "node:child_process";
33
- import { existsSync } from "node:fs";
33
+ import { existsSync, mkdirSync } from "node:fs";
34
34
  import path from "node:path";
35
35
  import { fileURLToPath } from "node:url";
36
- import { CONFIG } from "#config";
36
+ import { CONFIG, CONFIG_DIR, CLIENT_ID } from "#config";
37
37
  import { logger } from "#logger";
38
38
  import { fireAlert } from "#kernel/alerts.js";
39
+ import { getDriverManager } from "#kernel/driverManager.js";
39
40
  // ── Tunables (mirror drivers/baileys/index.ts:98-106) ──────────────────────
40
41
  const RECONNECT_BASE_MS = 1000;
41
42
  const RECONNECT_MAX_MS = 60_000;
@@ -49,7 +50,8 @@ const SHUTDOWN_GRACE_MS = 5_000;
49
50
  * first match that exists and is a regular file. Order:
50
51
  * 1. CONFIG.drivers.whatsmeow.binaryPath (explicit user choice)
51
52
  * 2. env WM_BINARY_PATH (escape hatch for exotic installs)
52
- * 3. dev layout (<cwd>/whatsmeow-service/bin/whatsmeow-service)
53
+ * 3. stable config dir (~/.manybot/whatsmeow-service/bin/whatsmeow-service)
54
+ * 4. dev layout (<cwd>/whatsmeow-service/bin/whatsmeow-service)
53
55
  * 4. npm-global layout (sibling of the node binary, /usr/local style)
54
56
  */
55
57
  function resolveBinaryPath() {
@@ -60,6 +62,8 @@ function resolveBinaryPath() {
60
62
  const fromEnv = process.env.WM_BINARY_PATH;
61
63
  if (fromEnv)
62
64
  candidates.push(path.resolve(fromEnv));
65
+ // Stable config dir: ~/.manybot/whatsmeow-service/bin/whatsmeow-service
66
+ candidates.push(path.resolve(CONFIG_DIR, "whatsmeow-service", "bin", "whatsmeow-service"));
63
67
  // Dev: `<repo>/whatsmeow-service/bin/whatsmeow-service`
64
68
  candidates.push(path.resolve(process.cwd(), "whatsmeow-service", "bin", "whatsmeow-service"));
65
69
  // Global npm install: `<prefix>/bin/../share/manybot/bin/whatsmeow-service`
@@ -148,8 +152,12 @@ export async function startWhatsmeowSupervisor() {
148
152
  "whatsmeow-service/bin/whatsmeow-service relative to cwd. Bot will run on Baileys only.");
149
153
  return null;
150
154
  }
155
+ logger.info(`[supervisor] using binary: ${binary}`);
151
156
  const grpcAddress = CONFIG.drivers.whatsmeow.grpcAddress || "localhost:50051";
152
- const sessionDir = path.resolve(process.cwd(), "whatsmeow-session.db");
157
+ const sessionDir = path.resolve(CONFIG_DIR, "sessions", CLIENT_ID, "whatsmeow", "session.db");
158
+ // Ensure the parent directory exists before the Go subprocess tries to
159
+ // create/open the SQLite file.
160
+ mkdirSync(path.dirname(sessionDir), { recursive: true });
153
161
  const state = {
154
162
  proc: null,
155
163
  pid: null,
@@ -196,6 +204,7 @@ export async function startWhatsmeowSupervisor() {
196
204
  state.halted = true;
197
205
  state.ready = false;
198
206
  state.readyDeferred?.reject(new Error(reason));
207
+ getDriverManager().markDegraded("whatsmeow", 600_000);
199
208
  fireAlert("whatsmeow_subprocess_halted", { reason });
200
209
  }
201
210
  function scheduleRestart() {
@@ -220,7 +229,17 @@ export async function startWhatsmeowSupervisor() {
220
229
  let proc;
221
230
  try {
222
231
  proc = spawn(state.binary, ["--grpc-addr", state.addr, "--session-dir", state.sessionDir], {
223
- stdio: "ignore",
232
+ stdio: ["ignore", "pipe", "pipe"],
233
+ });
234
+ // Forward Go service stdout/stderr to the bot log so we can see
235
+ // crashes, missing dependencies, port-in-use, etc. Without this
236
+ // `stdio: "ignore"` would discard everything and a crashed
237
+ // subprocess would only surface as an exit code.
238
+ proc.stdout?.on("data", (chunk) => {
239
+ process.stdout.write(`[whatsmeow-stdout] ${chunk}`);
240
+ });
241
+ proc.stderr?.on("data", (chunk) => {
242
+ process.stderr.write(`[whatsmeow-stderr] ${chunk}`);
224
243
  });
225
244
  }
226
245
  catch (e) {
@@ -81,11 +81,21 @@ export async function sendWithFallback(jid, text, opts = {}) {
81
81
  // waitForSendSlot is the same throttle the rest of the senders use
82
82
  // (fallback must respect rate-limit too).
83
83
  await waitForSendSlot(jid, { cooldown: true, jitter: true });
84
- const ref = await primary.sendText(jid, text, opts);
85
- if (await verifyDelivery(primary, jid, ref, drivers.verifyWindowMs)) {
86
- return ref;
84
+ let primaryRef = null;
85
+ let primarySendFailed = false;
86
+ try {
87
+ primaryRef = await primary.sendText(jid, text, opts);
88
+ }
89
+ catch (err) {
90
+ primarySendFailed = true;
91
+ logger.warn({ driver: primaryKey, jid, error: String(err) }, "send threw on primary");
92
+ }
93
+ if (!primarySendFailed) {
94
+ if (await verifyDelivery(primary, jid, primaryRef, drivers.verifyWindowMs)) {
95
+ return primaryRef;
96
+ }
97
+ logger.warn({ driver: primaryKey, jid, messageId: primaryRef.id }, "send not confirmed by primary");
87
98
  }
88
- logger.warn({ driver: primaryKey, jid, messageId: ref.id }, "send not confirmed by primary");
89
99
  dm.markDegraded(primaryKey, drivers.fallbackCooldownMs);
90
100
  const secondary = pickSecondary(dm, primaryKey);
91
101
  if (!secondary || !secondary.isReady()) {
@@ -94,7 +104,7 @@ export async function sendWithFallback(jid, text, opts = {}) {
94
104
  }
95
105
  try {
96
106
  const fallbackRef = await sendVia(secondary, jid, text, opts, drivers.verifyWindowMs, /*skipGuard=*/ true);
97
- logger.info({ driver: secondary.name, jid, messageId: fallbackRef.id, reason: "primary verification failed" }, "message sent via fallback");
107
+ logger.info({ driver: secondary.name, jid, messageId: fallbackRef.id, reason: primarySendFailed ? "send threw" : "primary verification failed" }, "message sent via fallback");
98
108
  return fallbackRef;
99
109
  }
100
110
  catch (err) {
@@ -71,7 +71,7 @@
71
71
  "connectGaveUp": "Couldn't connect after several attempts. Check your network and try again."
72
72
  },
73
73
  "whatsmeow": {
74
- "installPrompt": "Install whatsmeow driver for better fallback support?",
74
+ "installPrompt": "Install whatsmeow driver for fallback support? (EXPERIMENTAL — only text send & history work; other methods throw)",
75
75
  "unsupportedArch": "whatsmeow driver not available for {{os}}-{{arch}}. The bot will use Baileys only.",
76
76
  "fetchingTag": "Fetching latest whatsmeow release...",
77
77
  "fetchFailed": "Could not reach Codeberg. Skipping whatsmeow install.",
@@ -79,6 +79,7 @@
79
79
  "downloadFailed": "Download failed: {{reason}}",
80
80
  "installTitle": "whatsmeow driver",
81
81
  "installed": "whatsmeow driver installed at {{path}}",
82
- "restartNotice": "Restart the bot for the whatsmeow driver to take effect."
82
+ "restartNotice": "Restart the bot for the whatsmeow driver to take effect.",
83
+ "experimentalNotice": "whatsmeow is EXPERIMENTAL: only sendText and getHistory are implemented. sendImage, sendPoll, groupMetadata, and other methods throw."
83
84
  }
84
85
  }
@@ -71,7 +71,7 @@
71
71
  "connectGaveUp": "No se pudo conectar tras varios intentos. Revisa tu conexión e intenta de nuevo."
72
72
  },
73
73
  "whatsmeow": {
74
- "installPrompt": "¿Instalar el driver whatsmeow para mejor soporte de fallback?",
74
+ "installPrompt": "¿Instalar el driver whatsmeow para soporte de fallback? (EXPERIMENTAL — solo sendText e historial funcionan; otros métodos lanzan error)",
75
75
  "unsupportedArch": "Driver whatsmeow no disponible para {{os}}-{{arch}}. El bot usará solo Baileys.",
76
76
  "fetchingTag": "Obteniendo última versión de whatsmeow...",
77
77
  "fetchFailed": "No se pudo contactar a Codeberg. Instalación de whatsmeow omitida.",
@@ -79,6 +79,7 @@
79
79
  "downloadFailed": "Descarga fallida: {{reason}}",
80
80
  "installTitle": "Driver whatsmeow",
81
81
  "installed": "Driver whatsmeow instalado en {{path}}",
82
- "restartNotice": "Reinicia el bot para que el driver whatsmeow surta efecto."
82
+ "restartNotice": "Reinicia el bot para que el driver whatsmeow surta efecto.",
83
+ "experimentalNotice": "whatsmeow es EXPERIMENTAL: solo sendText y getHistory están implementados. sendImage, sendPoll, groupMetadata y otros métodos lanzan error."
83
84
  }
84
85
  }
@@ -71,7 +71,7 @@
71
71
  "connectGaveUp": "Não foi possível conectar após várias tentativas. Verifique sua conexão e tente de novo."
72
72
  },
73
73
  "whatsmeow": {
74
- "installPrompt": "Instalar driver whatsmeow para melhor suporte a fallback?",
74
+ "installPrompt": "Instalar driver whatsmeow para suporte a fallback? (EXPERIMENTAL — apenas sendText e histórico funcionam; outros métodos lançam erro)",
75
75
  "unsupportedArch": "Driver whatsmeow não disponível para {{os}}-{{arch}}. O bot usará apenas Baileys.",
76
76
  "fetchingTag": "Buscando última versão do whatsmeow...",
77
77
  "fetchFailed": "Não foi possível acessar o Codeberg. Instalação do whatsmeow ignorada.",
@@ -79,6 +79,7 @@
79
79
  "downloadFailed": "Download falhou: {{reason}}",
80
80
  "installTitle": "Driver whatsmeow",
81
81
  "installed": "Driver whatsmeow instalado em {{path}}",
82
- "restartNotice": "Reinicie o bot para que o driver whatsmeow entre em efeito."
82
+ "restartNotice": "Reinicie o bot para que o driver whatsmeow entre em efeito.",
83
+ "experimentalNotice": "whatsmeow é EXPERIMENTAL: apenas sendText e getHistory estão implementados. sendImage, sendPoll, groupMetadata e outros métodos lançam erro."
83
84
  }
84
85
  }
package/dist/main.js CHANGED
@@ -11,6 +11,7 @@ process.env.NODE_PATH = path.resolve(process.cwd(), "node_modules");
11
11
  Module._initPaths();
12
12
  import { baileysContract } from "#drivers/baileys/index.js";
13
13
  import { whatsmeowContract, startWhatsmeowSupervisor, wrapWithSupervisor } from "#drivers/whatsmeow/index.js";
14
+ import { promptWhatsmeowInstall } from "#drivers/whatsmeow/installer.js";
14
15
  import { cleanupPlugins } from "#kernel/pluginLoader.js";
15
16
  import { stopAll as stopScheduler } from "#kernel/scheduler.js";
16
17
  import { sendAlert } from "#kernel/alerts.js";
@@ -35,12 +36,20 @@ driverManager.register(baileysContract, { isPrimary: CONFIG.drivers.primary ===
35
36
  // running on Baileys alone, no fallback.
36
37
  let supervisor = null;
37
38
  if (CONFIG.drivers.whatsmeow.enabled) {
39
+ logger.info("[driverManager] whatsmeow enabled — spawning supervisor");
38
40
  supervisor = await startWhatsmeowSupervisor();
39
41
  if (supervisor) {
40
42
  const wrapped = wrapWithSupervisor(whatsmeowContract, supervisor);
41
43
  driverManager.register(wrapped, { isPrimary: CONFIG.drivers.primary === "whatsmeow" });
44
+ logger.info(`[driverManager] whatsmeow registered (primary=${CONFIG.drivers.primary === "whatsmeow"})`);
45
+ }
46
+ else {
47
+ logger.warn("[driverManager] whatsmeow supervisor failed to start — fallback disabled");
42
48
  }
43
49
  }
50
+ else {
51
+ logger.info("[driverManager] whatsmeow disabled by config — no fallback");
52
+ }
44
53
  const activeDriver = driverManager.active();
45
54
  const secondaryName = (activeDriver.name === "baileys" ? "whatsmeow" : "baileys");
46
55
  const secondaryDriver = driverManager.get(secondaryName);
@@ -125,6 +134,16 @@ if (process.argv.includes("--getid")) {
125
134
  process.exit(1);
126
135
  });
127
136
  }
137
+ else if (process.argv.includes("--install-whatsmeow")) {
138
+ // Re-run the whatsmeow installer outside the normal setup flow.
139
+ // Useful when the initial install failed or the binary was moved.
140
+ promptWhatsmeowInstall()
141
+ .then(() => process.exit(0))
142
+ .catch((err) => {
143
+ logger.error(`--install-whatsmeow failed: ${err.message}`);
144
+ process.exit(1);
145
+ });
146
+ }
128
147
  else {
129
148
  // Start bot
130
149
  logger.info(t("bot.initialized"));
@@ -143,10 +162,14 @@ else {
143
162
  // here is non-fatal: the primary keeps running, fallback just stays
144
163
  // unavailable (sendFallbackGuard's `isReady()` check covers that).
145
164
  if (secondaryDriver) {
165
+ logger.info(`[driverManager] connecting secondary "${secondaryName}" in background…`);
146
166
  secondaryDriver.connect()
147
167
  .then(() => logger.info(`[driverManager] secondary "${secondaryName}" connected — fallback available`))
148
168
  .catch((err) => logger.warn(`[driverManager] secondary "${secondaryName}" connect failed: ${err.message} — fallback unavailable`));
149
169
  }
170
+ else {
171
+ logger.info(`[driverManager] no secondary driver registered — running on ${activeDriver.name} only`);
172
+ }
150
173
  })
151
174
  .catch((err) => {
152
175
  shutdown(`Failed to connect driver: ${err.message}`, true);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "SyntaxError!",
6
6
  "email": "me@stxerr.dev"
7
7
  },
8
- "version": "5.6.0",
8
+ "version": "5.7.0",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {
@@ -40,7 +40,7 @@
40
40
  "@grpc/grpc-js": "^1.14.4",
41
41
  "@grpc/proto-loader": "^0.7.15",
42
42
  "@hapi/boom": "^10.0.1",
43
- "@whiskeysockets/baileys": "6.7.23",
43
+ "@whiskeysockets/baileys": "6.7.24",
44
44
  "node-cron": "^4.6.0",
45
45
  "node-webpmux": "^3.2.1",
46
46
  "nodemailer": "^9.0.3",