@manybot/manybot 5.2.4 → 5.2.6

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![ManyBot Logo](logo.png)
4
4
 
5
- ![Node.js 20+](https://img.shields.io/badge/Node.js-20+-339933?logo=node.js&logoColor=white)
5
+ ![Node.js 24+](https://img.shields.io/badge/Node.js-20+-339933?logo=node.js&logoColor=white)
6
6
  ![npm 9+](https://img.shields.io/badge/npm-9+-CB3837?logo=npm&logoColor=white)
7
7
  ![GPL v3](https://img.shields.io/badge/License-GPL--v3-blue.svg)
8
8
  ![Linux](https://img.shields.io/badge/Linux%20%7C%20Windows-lightgrey)
@@ -16,7 +16,7 @@ Open-source framework for message automation, extensible via community plugins.
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Node.js >= 20
19
+ - Node.js >= 24.17.0
20
20
  - npm
21
21
 
22
22
  ## Getting started
package/dist/config.js CHANGED
@@ -21,6 +21,7 @@ import os from "os";
21
21
  import path from "path";
22
22
  import { parse as parseToml } from "smol-toml";
23
23
  import { logger } from "#logger";
24
+ import { t } from "#i18n";
24
25
  // ---------------------------------------------------------------------------
25
26
  // Paths
26
27
  // ---------------------------------------------------------------------------
@@ -208,13 +209,6 @@ PHONE_NUMBER = ""
208
209
  # Leave blank to choose interactively on first run — the choice is then
209
210
  # saved here automatically.
210
211
  LOGIN_METHOD = ""
211
-
212
- # JID of a single chat where the bot is allowed to respond to messages
213
- # sent by yourself (fromMe) — useful for testing commands without
214
- # affecting other conversations. In every other chat, fromMe messages
215
- # are always ignored.
216
- # Example: TEST_CHAT = "5511999999999@c.us"
217
- TEST_CHAT = ""
218
212
  `;
219
213
  const DEFAULT_TOML_PT = `# Arquivo de configuração do ManyBot
220
214
  # Veja https://manybot.org/docs/config para saber mais
@@ -230,12 +224,6 @@ PHONE_NUMBER = ""
230
224
  # PHONE_NUMBER). Deixe em branco para escolher interativamente na primeira
231
225
  # execução — a escolha é salva aqui automaticamente.
232
226
  LOGIN_METHOD = ""
233
-
234
- # JID de um único chat onde o bot pode responder a mensagens enviadas por
235
- # você mesmo (fromMe) — útil para testar comandos sem afetar outras
236
- # conversas. Em qualquer outro chat, mensagens fromMe são sempre ignoradas.
237
- # Exemplo: TEST_CHAT = "5511999999999@c.us"
238
- TEST_CHAT = ""
239
227
  `;
240
228
  const DEFAULT_TOML = detectSystemLang() === "pt" ? DEFAULT_TOML_PT : DEFAULT_TOML_EN;
241
229
  await fs.mkdir(CONFIG_DIR, { recursive: true });
@@ -261,8 +249,9 @@ async function loadToml(file, label) {
261
249
  }
262
250
  catch (e) {
263
251
  const err = e instanceof Error ? e : new Error(String(e));
264
- logger.warn(`Error parsing ${label}: ${err.message}`);
265
- return {};
252
+ logger.error(t("errors.invalid_config", { file: label, error: err.message }));
253
+ logger.error(t("errors.fix_config", { file: label }));
254
+ process.exit(1);
266
255
  }
267
256
  }
268
257
  tomlLayer = {
@@ -277,7 +266,6 @@ const DEFAULTS = {
277
266
  LANGUAGE: "en",
278
267
  PHONE_NUMBER: null,
279
268
  PLATFORM: "whatsapp",
280
- TEST_CHAT: null,
281
269
  LOGIN_METHOD: null,
282
270
  };
283
271
  function normalize(cfg) {
@@ -285,8 +273,6 @@ function normalize(cfg) {
285
273
  // can always do a simple truthiness check regardless of config source.
286
274
  if (cfg.PHONE_NUMBER === "")
287
275
  cfg.PHONE_NUMBER = null;
288
- if (cfg.TEST_CHAT === "")
289
- cfg.TEST_CHAT = null;
290
276
  // Anything other than "phone"/"qr" is treated as "not configured yet",
291
277
  // so a typo or leftover garbage value falls back to the interactive
292
278
  // prompt instead of silently misbehaving.
@@ -327,7 +313,6 @@ export const PLUGINS = CONFIG.PLUGINS;
327
313
  export const LANGUAGE = CONFIG.LANGUAGE;
328
314
  export const PHONE_NUMBER = CONFIG.PHONE_NUMBER;
329
315
  export const PLATFORM = CONFIG.PLATFORM;
330
- export const TEST_CHAT = CONFIG.TEST_CHAT;
331
316
  export const LOGIN_METHOD = CONFIG.LOGIN_METHOD;
332
317
  /**
333
318
  * Writes a single value back to manybot.toml without rewriting the whole
@@ -64,15 +64,19 @@ async function startBot() {
64
64
  });
65
65
  // Incoming messages
66
66
  sock.ev.on("messages.upsert", async ({ messages, type }) => {
67
- if (type !== "notify" || state !== "READY")
67
+ if (state !== "READY")
68
+ return;
69
+ if (type !== "notify" && type !== "append")
68
70
  return;
69
71
  for (const msg of messages) {
70
- // Skip empty messages (e.g. presence updates)
71
- const body = getBodyQuick(msg);
72
- if (!body && !msgHasMediaQuick(msg))
72
+ const m = msg;
73
+ if (type === "append" && !m.key.fromMe)
74
+ continue;
75
+ const body = getBodyQuick(m);
76
+ if (!body && !msgHasMediaQuick(m))
73
77
  continue;
74
78
  try {
75
- await handleMessage(msg, sock, store);
79
+ await handleMessage(m, sock, store);
76
80
  }
77
81
  catch (e) {
78
82
  const err = e instanceof Error ? e : new Error(String(e));
@@ -123,9 +127,9 @@ export const whatsappDriver = {
123
127
  * it's your own message or someone else's — prints the normalized
124
128
  * JID and the chat name (if any), and exits.
125
129
  *
126
- * Intentionally does not go through the CHATS/fromMe/dedup filters of
130
+ * Intentionally does not go through the CHATS/dedup filters of
127
131
  * handleMessage: the only goal here is to discover the JID so you can
128
- * configure CHATS/TEST_CHAT afterward, so there's no reason to filter
132
+ * configure CHATS afterward, so there's no reason to filter
129
133
  * anything out yet.
130
134
  */
131
135
  async getId() {
@@ -151,7 +155,7 @@ export const whatsappDriver = {
151
155
  logger.success(`[getid] JID: ${jid}`);
152
156
  if (name)
153
157
  logger.info(`[getid] Chat/sender: ${name}`);
154
- logger.info("[getid] Paste this value into CHATS or TEST_CHAT in manybot.toml.");
158
+ logger.info("[getid] Paste this value into CHATS in manybot.toml.");
155
159
  resolve();
156
160
  });
157
161
  });
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Each plugin decides whether to act or ignore.
13
13
  */
14
- import { CHATS, TEST_CHAT } from "#config";
14
+ import { CHATS } from "#config";
15
15
  import { buildApi, buildChatFromMsg } from "./api/index.js";
16
16
  import { pluginRegistry } from "#kernel/pluginLoader.js";
17
17
  import { runPlugin } from "#kernel/pluginGuard.js";
@@ -58,10 +58,6 @@ export async function handleMessage(msg, sock, store) {
58
58
  if (CHATS.length > 0 && !CHATS.includes(jid)) {
59
59
  return;
60
60
  }
61
- const isTestChat = !!TEST_CHAT && jid === normalizeJid(TEST_CHAT);
62
- if (msg.key.fromMe && !isTestChat) {
63
- return;
64
- }
65
61
  if (alreadyProcessed(msg.key.id)) {
66
62
  return;
67
63
  }
@@ -42,14 +42,41 @@ function loadLocale(lang) {
42
42
  }
43
43
  }
44
44
  /**
45
- * Gets configured language or default
45
+ * Detects the OS locale without depending on a single env var, since LANG
46
+ * isn't reliably set on macOS GUI sessions or Windows. Used as a fallback
47
+ * when CONFIG.LANGUAGE isn't available yet (e.g. circular import during
48
+ * config bootstrap) or isn't set.
49
+ */
50
+ function detectSystemLang() {
51
+ try {
52
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
53
+ if (locale)
54
+ return locale.split("-")[0].toLowerCase();
55
+ }
56
+ catch {
57
+ // Intl unavailable — fall through to env vars
58
+ }
59
+ const envLocale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE;
60
+ if (envLocale)
61
+ return envLocale.split(/[_.]/)[0].toLowerCase();
62
+ return DEFAULT_LANG;
63
+ }
64
+ /**
65
+ * Gets configured language or falls back to system locale, then English.
46
66
  * @returns {string}
47
67
  */
48
68
  function getConfiguredLang() {
49
- const lang = CONFIG.LANGUAGE?.trim().toLowerCase();
50
- if (!lang)
51
- return DEFAULT_LANG;
52
- // Check if file exists
69
+ let lang;
70
+ try {
71
+ lang = CONFIG.LANGUAGE?.trim().toLowerCase();
72
+ }
73
+ catch {
74
+ // CONFIG not initialized yet (e.g. this module was pulled in via a
75
+ // circular import while #config is still bootstrapping)
76
+ }
77
+ if (!lang) {
78
+ lang = detectSystemLang();
79
+ }
53
80
  const filePath = path.join(LOCALES_DIR, `${lang}.json`);
54
81
  if (!fs.existsSync(filePath)) {
55
82
  console.warn(`[i18n] Language "${lang}" not found, falling back to "${DEFAULT_LANG}"`);
@@ -58,9 +85,16 @@ function getConfiguredLang() {
58
85
  return lang;
59
86
  }
60
87
  // Load languages
61
- const currentLang = getConfiguredLang();
62
- const currentTranslations = loadLocale(currentLang) || {};
63
- const fallbackTranslations = loadLocale(DEFAULT_LANG) || {};
88
+ let currentLang = null;
89
+ let currentTranslations = {};
90
+ let fallbackTranslations = {};
91
+ function ensureLoaded() {
92
+ if (currentLang !== null)
93
+ return;
94
+ currentLang = getConfiguredLang();
95
+ currentTranslations = loadLocale(currentLang) || {};
96
+ fallbackTranslations = loadLocale(DEFAULT_LANG) || {};
97
+ }
64
98
  /**
65
99
  * Gets a nested value from an object using dot path
66
100
  * @param {object} obj
@@ -96,6 +130,7 @@ function interpolate(str, context = {}) {
96
130
  * @returns {string}
97
131
  */
98
132
  export function t(key, context = {}) {
133
+ ensureLoaded();
99
134
  // Try current language first
100
135
  let value = getNestedValue(currentTranslations, key);
101
136
  // Fallback to English if not found
@@ -135,6 +170,7 @@ export function t(key, context = {}) {
135
170
  export function createPluginT(pluginMetaUrl) {
136
171
  const pluginDir = path.dirname(fileURLToPath(pluginMetaUrl));
137
172
  const pluginLocaleDir = path.join(pluginDir, "locale");
173
+ ensureLoaded();
138
174
  // Get bot's configured language
139
175
  const targetLang = currentLang;
140
176
  // Load plugin translations
@@ -184,19 +220,16 @@ export function createPluginT(pluginMetaUrl) {
184
220
  */
185
221
  export function reloadTranslations() {
186
222
  translations.clear();
187
- const lang = getConfiguredLang();
188
- const newTranslations = loadLocale(lang) || {};
189
- const newFallback = loadLocale(DEFAULT_LANG) || {};
190
- // Update references
191
- Object.assign(currentTranslations, newTranslations);
192
- Object.assign(fallbackTranslations, newFallback);
193
- console.log(`[i18n] Translations reloaded for language: ${lang}`);
223
+ currentLang = null;
224
+ ensureLoaded();
225
+ console.log(`[i18n] Translations reloaded for language: ${currentLang}`);
194
226
  }
195
227
  /**
196
228
  * Returns current language
197
229
  * @returns {string}
198
230
  */
199
231
  export function getCurrentLang() {
232
+ ensureLoaded();
200
233
  return currentLang;
201
234
  }
202
235
  export default { t, createPluginT, reloadTranslations, getCurrentLang };
@@ -20,7 +20,7 @@
20
20
  * }
21
21
  */
22
22
  import cron from "node-cron";
23
- import Database from "better-sqlite3";
23
+ import { DatabaseSync } from "node:sqlite";
24
24
  import path from "path";
25
25
  import { mkdirSync } from "fs";
26
26
  import { logger } from "#logger";
@@ -30,8 +30,8 @@ import { CONFIG_DIR } from "#config";
30
30
  const tasks = new Map();
31
31
  // ── Persistence (metadata only — fn can't be serialized) ────────────────────
32
32
  mkdirSync(CONFIG_DIR, { recursive: true });
33
- const db = new Database(path.join(CONFIG_DIR, "scheduler.db"));
34
- db.pragma("journal_mode = WAL");
33
+ const db = new DatabaseSync(path.join(CONFIG_DIR, "scheduler.db"));
34
+ db.exec("PRAGMA journal_mode = WAL");
35
35
  db.exec(`
36
36
  CREATE TABLE IF NOT EXISTS scheduled_tasks (
37
37
  plugin_name TEXT NOT NULL,
@@ -10,16 +10,16 @@
10
10
  *
11
11
  * Plugins never touch the DB directly — only through buildSettingsApi().
12
12
  */
13
- import Database from "better-sqlite3";
13
+ import { DatabaseSync } from "node:sqlite";
14
14
  import path from "path";
15
15
  import { mkdirSync } from "fs";
16
16
  import { CONFIG_DIR } from "#config";
17
17
  // ── DB init ───────────────────────────────────────────────────────────────────
18
18
  const DB_PATH = path.join(CONFIG_DIR, "settings.db");
19
19
  mkdirSync(path.dirname(DB_PATH), { recursive: true });
20
- const db = new Database(DB_PATH);
21
- db.pragma("journal_mode = WAL");
22
- db.pragma("foreign_keys = ON");
20
+ const db = new DatabaseSync(DB_PATH);
21
+ db.exec("PRAGMA journal_mode = WAL");
22
+ db.exec("PRAGMA foreign_keys = ON");
23
23
  db.exec(`
24
24
  CREATE TABLE IF NOT EXISTS plugin_settings (
25
25
  plugin_name TEXT NOT NULL,
@@ -34,7 +34,9 @@
34
34
  "sessionExpired": "Session expired or logged out — removing local session and restarting..."
35
35
  },
36
36
  "errors": {
37
- "stack": "Stack"
37
+ "stack": "Stack",
38
+ "invalid_config": "Invalid config file {{file}}: {{error}}",
39
+ "fix_config": "Fix the syntax in {{file}} before starting the bot."
38
40
  },
39
41
  "onboarding": {
40
42
  "intro": "ManyBot — first login",
@@ -34,7 +34,9 @@
34
34
  "sessionExpired": "Sesión expirada o cerrada — eliminando sesión local y reiniciando..."
35
35
  },
36
36
  "errors": {
37
- "stack": "Stack"
37
+ "stack": "Stack",
38
+ "invalid_config": "Archivo de configuración inválido {{file}}: {{error}}",
39
+ "fix_config": "Corrige la sintaxis en {{file}} antes de iniciar el bot."
38
40
  },
39
41
  "onboarding": {
40
42
  "intro": "ManyBot — primer inicio de sesión",
@@ -34,7 +34,9 @@
34
34
  "sessionExpired": "Sessão expirada ou desconectada — removendo sessão local e reiniciando..."
35
35
  },
36
36
  "errors": {
37
- "stack": "Stack"
37
+ "stack": "Stack",
38
+ "invalid_config": "Arquivo de configuração inválido {{file}}: {{error}}",
39
+ "fix_config": "Corrija a sintaxe em {{file}} antes de iniciar o bot."
38
40
  },
39
41
  "onboarding": {
40
42
  "intro": "ManyBot — primeiro login",
package/dist/main.js CHANGED
@@ -55,7 +55,7 @@ process.on("SIGINT", () => shutdown("SIGINT"));
55
55
  // ── --getid mode (provisional) ───────────────────────────────────────────
56
56
  // Usage: npm run start -- --getid
57
57
  // Connects, waits for the next message to arrive from any chat, and prints
58
- // the JID to the console, to paste into CHATS/TEST_CHAT in manybot.toml.
58
+ // the JID to the console, to paste into CHATS in manybot.toml.
59
59
  // Does not enter the normal bot flow (plugins are not loaded).
60
60
  if (process.argv.includes("--getid")) {
61
61
  if (!activeDriver.getId) {
package/package.json CHANGED
@@ -5,11 +5,11 @@
5
5
  "name": "SyntaxError!",
6
6
  "email": "me@stxerr.dev"
7
7
  },
8
- "version": "5.2.4",
8
+ "version": "5.2.6",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {
12
- "node": ">=20"
12
+ "node": ">=24.17.0"
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",
@@ -30,16 +30,14 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "devDependencies": {
33
- "@types/better-sqlite3": "^7.6.12",
34
33
  "@types/node": "^22.0.0",
35
34
  "tsx": "^4.19.2",
36
35
  "typescript": "^5.7.3"
37
36
  },
38
37
  "dependencies": {
39
- "@clack/prompts": "^0.10.0",
38
+ "@clack/prompts": "^0.10.1",
40
39
  "@hapi/boom": "^10.0.1",
41
40
  "@whiskeysockets/baileys": "^6.7.23",
42
- "better-sqlite3": "^12.11.1",
43
41
  "node-cron": "^4.6.0",
44
42
  "node-webpmux": "^3.2.1",
45
43
  "pino": "^10.3.1",