@manybot/manybot 5.7.0 → 5.8.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 (71) hide show
  1. package/README.md +20 -3
  2. package/dist/client/banner.js +10 -0
  3. package/dist/client/banner.test.js +31 -0
  4. package/dist/client/store.js +56 -5
  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/drivers/baileys/adapter.js +58 -7
  9. package/dist/drivers/baileys/api/index.js +172 -24
  10. package/dist/drivers/baileys/index.js +62 -30
  11. package/dist/drivers/baileys/loginPrompt.js +0 -2
  12. package/dist/drivers/baileys/messageHandler.js +158 -4
  13. package/dist/drivers/baileys/messageHandler.test.js +203 -0
  14. package/dist/drivers/baileysAdapter.test.js +281 -0
  15. package/dist/drivers/jid.test.js +40 -0
  16. package/dist/drivers/types.js +5 -5
  17. package/dist/i18n/index.js +15 -2
  18. package/dist/kernel/activeDriverSend.js +21 -0
  19. package/dist/kernel/activeDriverSend.test.js +89 -0
  20. package/dist/kernel/alerts.js +3 -9
  21. package/dist/kernel/chatSession.js +65 -0
  22. package/dist/kernel/chatSession.test.js +46 -0
  23. package/dist/kernel/commandAccess.js +66 -0
  24. package/dist/kernel/commandAccess.test.js +74 -0
  25. package/dist/kernel/commandDeprecation.js +168 -0
  26. package/dist/kernel/commandDeprecation.test.js +107 -0
  27. package/dist/kernel/commandMenu.js +268 -0
  28. package/dist/kernel/commandMenu.test.js +234 -0
  29. package/dist/kernel/commandPermissions.js +125 -0
  30. package/dist/kernel/commandPermissions.test.js +159 -0
  31. package/dist/kernel/commandRegistry.js +459 -0
  32. package/dist/kernel/commandRegistry.test.js +156 -0
  33. package/dist/kernel/commandsConfig.js +517 -0
  34. package/dist/kernel/commandsConfig.test.js +236 -0
  35. package/dist/kernel/contactAutoSave.test.js +87 -0
  36. package/dist/kernel/driverManager.js +10 -6
  37. package/dist/kernel/driverManager.test.js +90 -0
  38. package/dist/kernel/integrationMode.js +88 -0
  39. package/dist/kernel/integrationMode.test.js +95 -0
  40. package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
  41. package/dist/kernel/pluginApi.test.js +583 -0
  42. package/dist/kernel/pluginGuard.js +15 -12
  43. package/dist/kernel/pluginGuard.test.js +39 -0
  44. package/dist/kernel/pluginLoader.js +96 -1
  45. package/dist/kernel/pluginLoader.test.js +80 -0
  46. package/dist/kernel/runCommand.js +245 -0
  47. package/dist/kernel/runCommand.test.js +235 -0
  48. package/dist/kernel/sendFallbackGuard.js +19 -48
  49. package/dist/kernel/sendFallbackGuard.test.js +80 -0
  50. package/dist/kernel/sendGuard.js +38 -42
  51. package/dist/kernel/sendGuard.test.js +102 -0
  52. package/dist/kernel/settingsDb.js +4 -3
  53. package/dist/kernel/statusServer.js +9 -2
  54. package/dist/kernel/statusServer.test.js +70 -0
  55. package/dist/kernel/testConfig.js +183 -0
  56. package/dist/kernel/testConfig.test.js +181 -0
  57. package/dist/kernel/updateCheck.js +33 -10
  58. package/dist/locales/en.json +64 -13
  59. package/dist/locales/es.json +64 -13
  60. package/dist/locales/pt.json +64 -13
  61. package/dist/logger/logger.js +23 -3
  62. package/dist/logger/logger.test.js +45 -0
  63. package/dist/main.js +5 -76
  64. package/dist/plugins/__manybot_integration__/index.js +167 -0
  65. package/dist/plugins/__manybot_integration__/index.test.js +184 -0
  66. package/package.json +74 -17
  67. package/dist/drivers/whatsmeow/client.js +0 -252
  68. package/dist/drivers/whatsmeow/index.js +0 -79
  69. package/dist/drivers/whatsmeow/installer.js +0 -86
  70. package/dist/drivers/whatsmeow/supervisor.js +0 -328
  71. package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
package/README.md CHANGED
@@ -1,3 +1,6 @@
1
+ > [!WARNING]
2
+ > Versions 5.6.x and 5.7.x are UNSTABLE because of management error. Please use 5.5.4 instead.
3
+
1
4
  <div align="center">
2
5
 
3
6
  ![ManyBot Logo](logo.png)
@@ -18,8 +21,6 @@ Open-source framework for message automation, extensible via community plugins.
18
21
 
19
22
  - Node.js >= 24
20
23
  - npm >= 9
21
- - Go >= 1.23 (for whatsmeow driver)
22
- - protoc (for protobuf codegen)
23
24
 
24
25
  ## Getting started
25
26
 
@@ -39,7 +40,6 @@ On first run, a configuration file is created at `~/.manybot/manybot.toml`. Edit
39
40
  git clone <repo-url>
40
41
  cd dev
41
42
  npm install
42
- go build -o whatsmeow-service/bin/whatsmeow-service ./whatsmeow-service
43
43
  npm start
44
44
  ```
45
45
 
@@ -55,6 +55,23 @@ manyplug install <plugin-name>
55
55
 
56
56
  Browse available plugins at **[manybot.org/plugins](https://manybot.org/plugins/)**.
57
57
 
58
+ ## Testing
59
+
60
+ ManyBot includes both local unit/contract tests and opt-in real WhatsApp integration tests:
61
+
62
+ ```bash
63
+ # Run unit and mock contract test suite (fast, offline)
64
+ npm test
65
+
66
+ # Run all verification gates (typecheck + lint + unit tests)
67
+ npm run check
68
+
69
+ # Run real WhatsApp integration tests (requires TEST_CHAT and live session)
70
+ TEST_CHAT="5516999999999" npm run test:integration
71
+ ```
72
+
73
+ For full details on the test architecture and API coverage classification, see [API_TEST_PLAN.md](API_TEST_PLAN.md).
74
+
58
75
  ## Contributing
59
76
 
60
77
  All kinds of contributions are welcome:
@@ -10,10 +10,20 @@ const C = {
10
10
  import { readFileSync } from "fs";
11
11
  import { fileURLToPath } from "url";
12
12
  import path from "path";
13
+ import { getLogLevel } from "#logger";
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = path.dirname(__filename);
15
16
  const v = JSON.parse(readFileSync(path.join(__dirname, "../../package.json"), "utf8")).version;
17
+ /**
18
+ * Startup ASCII banner. Optional — respects `LOG_LEVEL` (Phase 9): shown
19
+ * only at `"normal"`, silent at `"clean"`/`"minimal"` for users who prefer
20
+ * quiet logs. Caller is responsible for only invoking this once per process
21
+ * (see `drivers/baileys/index.ts`'s `bannerShown` guard) — reconnects
22
+ * shouldn't reprint it.
23
+ */
16
24
  export function printBanner() {
25
+ if (getLogLevel() !== "normal")
26
+ return;
17
27
  const banner = [
18
28
  ` _ _ `,
19
29
  ` | | | | `,
@@ -0,0 +1,31 @@
1
+ import test, { describe, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { printBanner } from "./banner.js";
4
+ import { setLogLevel } from "#logger";
5
+ describe("printBanner LOG_LEVEL gating", () => {
6
+ const originalLog = console.log;
7
+ let lines;
8
+ beforeEach(() => {
9
+ lines = [];
10
+ console.log = (...a) => { lines.push(a.join(" ")); };
11
+ });
12
+ afterEach(() => {
13
+ console.log = originalLog;
14
+ setLogLevel("normal");
15
+ });
16
+ test("prints at normal", () => {
17
+ setLogLevel("normal");
18
+ printBanner();
19
+ assert.ok(lines.length > 0);
20
+ });
21
+ test("stays silent at clean", () => {
22
+ setLogLevel("clean");
23
+ printBanner();
24
+ assert.equal(lines.length, 0);
25
+ });
26
+ test("stays silent at minimal", () => {
27
+ setLogLevel("minimal");
28
+ printBanner();
29
+ assert.equal(lines.length, 0);
30
+ });
31
+ });
@@ -42,13 +42,44 @@ export function createStore() {
42
42
  function forgetLid(lid) {
43
43
  lidMap.delete(lid);
44
44
  }
45
+ // Coerce the various shapes Baileys delivers `ephemeralExpiration` in
46
+ // (string-numbered from the wire, `null` when the timer was cleared,
47
+ // or absent entirely) into a plain finite number. Anything we can't
48
+ // parse becomes 0 — the wire convention for "no disappearing timer".
49
+ function normalizeExpiration(raw) {
50
+ if (raw === null || raw === undefined)
51
+ return 0;
52
+ const n = Number(raw);
53
+ return Number.isFinite(n) && n >= 0 ? n : 0;
54
+ }
55
+ function setChatEphemeralExpiration(jid, seconds) {
56
+ if (!jid)
57
+ return;
58
+ const existing = chatsMap.get(jid);
59
+ const next = normalizeExpiration(seconds);
60
+ if (existing) {
61
+ existing.ephemeralExpiration = next;
62
+ }
63
+ else {
64
+ chatsMap.set(jid, { id: jid, name: jid.split("@")[0], ephemeralExpiration: next });
65
+ }
66
+ }
45
67
  // Max messages kept per chat (prevents unbounded memory growth)
46
68
  const MAX_MSGS_PER_CHAT = 200;
47
69
  function upsertChat(chat) {
48
70
  if (!chat.id)
49
71
  return;
50
72
  const name = chat.name ?? chat.id.split("@")[0];
51
- chatsMap.set(chat.id, { id: chat.id, name });
73
+ const rawExp = chat.ephemeralExpiration;
74
+ const existing = chatsMap.get(chat.id);
75
+ chatsMap.set(chat.id, {
76
+ id: chat.id,
77
+ name,
78
+ // Preserve a previously-learned timer if this upsert doesn't carry
79
+ // one (chats.upsert often ships without ephemeralExpiration — the
80
+ // value arrives later via chats.update or an ephemeralMessage).
81
+ ephemeralExpiration: rawExp !== undefined ? normalizeExpiration(rawExp) : (existing?.ephemeralExpiration ?? 0),
82
+ });
52
83
  }
53
84
  function upsertContact(contact) {
54
85
  // Don't let a later, poorer sync round (e.g. a bare history-sync stub)
@@ -124,9 +155,17 @@ export function createStore() {
124
155
  if (!update.id)
125
156
  continue;
126
157
  const existing = chatsMap.get(update.id);
127
- const name = update.name;
128
- if (existing && name)
129
- existing.name = name;
158
+ if (!existing)
159
+ continue;
160
+ const u = update;
161
+ if (u.name)
162
+ existing.name = u.name;
163
+ // Baileys sends `ephemeralExpiration: null` to signal the timer
164
+ // was cleared — store 0 in that case so a later `chats.update`
165
+ // carrying a positive value can take over again.
166
+ if ("ephemeralExpiration" in u) {
167
+ existing.ephemeralExpiration = normalizeExpiration(u.ephemeralExpiration);
168
+ }
130
169
  }
131
170
  });
132
171
  ev.on("contacts.upsert", (newContacts) => {
@@ -144,8 +183,19 @@ export function createStore() {
144
183
  }
145
184
  });
146
185
  ev.on("messages.upsert", ({ messages: msgs }) => {
147
- for (const msg of msgs)
186
+ for (const msg of msgs) {
148
187
  storeMessage(msg);
188
+ // Disappearing-message timer: when a message arrives wrapped in
189
+ // an `ephemeralMessage`, the inner contextInfo carries the
190
+ // per-message timer WhatsApp negotiated for the chat. Promote
191
+ // it to the chat-level entry so subsequent sends can pick it up
192
+ // without re-scanning every message.
193
+ const m = msg.message;
194
+ const expiration = m?.ephemeralMessage?.message?.extendedTextMessage?.contextInfo?.expiration;
195
+ if (expiration !== undefined && msg.key.remoteJid) {
196
+ setChatEphemeralExpiration(msg.key.remoteJid, normalizeExpiration(expiration));
197
+ }
198
+ }
149
199
  capturePushNames(msgs);
150
200
  });
151
201
  ev.on("messages.update", (updates) => {
@@ -194,6 +244,7 @@ export function createStore() {
194
244
  resolveJid,
195
245
  learnLid,
196
246
  forgetLid,
247
+ setChatEphemeralExpiration,
197
248
  bind,
198
249
  toJSON,
199
250
  hydrate,
@@ -0,0 +1,170 @@
1
+ import assert from "node:assert/strict";
2
+ import { EventEmitter } from "node:events";
3
+ import test from "node:test";
4
+ import { createStore } from "#client/store.js";
5
+ test("store tracks disappearing-message timers from chats and message context", () => {
6
+ const store = createStore();
7
+ const ev = new EventEmitter();
8
+ store.bind(ev);
9
+ ev.emit("chats.upsert", [{ id: "chat@s.whatsapp.net", name: "Chat", ephemeralExpiration: "86400" }]);
10
+ assert.equal(store.chats.get("chat@s.whatsapp.net")?.ephemeralExpiration, 86400);
11
+ ev.emit("chats.update", [{ id: "chat@s.whatsapp.net", ephemeralExpiration: null }]);
12
+ assert.equal(store.chats.get("chat@s.whatsapp.net")?.ephemeralExpiration, 0);
13
+ ev.emit("messages.upsert", {
14
+ messages: [{
15
+ key: { id: "message", remoteJid: "chat@s.whatsapp.net" },
16
+ message: {
17
+ ephemeralMessage: {
18
+ message: { extendedTextMessage: { contextInfo: { expiration: "604800" } } },
19
+ },
20
+ },
21
+ }],
22
+ });
23
+ assert.equal(store.chats.get("chat@s.whatsapp.net")?.ephemeralExpiration, 604800);
24
+ });
25
+ test("store keeps timers in snapshots and accepts a timer learned by metadata", () => {
26
+ const store = createStore();
27
+ store.setChatEphemeralExpiration("group@g.us", 7776000);
28
+ const snapshot = store.toJSON();
29
+ assert.equal(snapshot.chats[0]?.ephemeralExpiration, 7776000);
30
+ const hydrated = createStore();
31
+ hydrated.hydrate(snapshot);
32
+ assert.equal(hydrated.chats.get("group@g.us")?.ephemeralExpiration, 7776000);
33
+ });
34
+ test("store learns, resolves, and forgets LID to PN mappings", () => {
35
+ const store = createStore();
36
+ // Invalid or identity mappings are ignored
37
+ store.learnLid("invalid_jid", "551199999999@s.whatsapp.net");
38
+ store.learnLid("123@lid", "456@lid");
39
+ assert.equal(store.resolveJid("invalid_jid"), "invalid_jid");
40
+ assert.equal(store.resolveJid("123@lid"), "123@lid");
41
+ // Valid mapping is learned and resolved
42
+ store.learnLid("123456789@lid", "551199999999@s.whatsapp.net");
43
+ assert.equal(store.resolveJid("123456789@lid"), "551199999999@s.whatsapp.net");
44
+ // Non-LID JIDs are returned untouched
45
+ assert.equal(store.resolveJid("551188888888@s.whatsapp.net"), "551188888888@s.whatsapp.net");
46
+ // Forget LID removes mapping
47
+ store.forgetLid("123456789@lid");
48
+ assert.equal(store.resolveJid("123456789@lid"), "123456789@lid");
49
+ });
50
+ test("store preserves rich contact fields during updates", () => {
51
+ const store = createStore();
52
+ const ev = new EventEmitter();
53
+ store.bind(ev);
54
+ // Initial contact with full details
55
+ ev.emit("contacts.upsert", [
56
+ { id: "user1@s.whatsapp.net", name: "Alice", notify: "Alice N", verifiedName: "Alice Verified" }
57
+ ]);
58
+ let contact = store.contacts["user1@s.whatsapp.net"];
59
+ assert.equal(contact?.name, "Alice");
60
+ assert.equal(contact?.notify, "Alice N");
61
+ assert.equal(contact?.verifiedName, "Alice Verified");
62
+ // Poorer update (e.g., from a stub) should not wipe out previous fields
63
+ ev.emit("contacts.upsert", [
64
+ { id: "user1@s.whatsapp.net" }
65
+ ]);
66
+ contact = store.contacts["user1@s.whatsapp.net"];
67
+ assert.equal(contact?.name, "Alice");
68
+ assert.equal(contact?.notify, "Alice N");
69
+ assert.equal(contact?.verifiedName, "Alice Verified");
70
+ });
71
+ test("store captures pushName from messages and respects guards", () => {
72
+ const store = createStore();
73
+ const ev = new EventEmitter();
74
+ store.bind(ev);
75
+ ev.emit("messages.upsert", {
76
+ messages: [
77
+ // DM message from other person -> should capture
78
+ {
79
+ key: { id: "m1", remoteJid: "user2@s.whatsapp.net", fromMe: false },
80
+ pushName: "Bob",
81
+ },
82
+ // Message fromMe -> should NOT capture
83
+ {
84
+ key: { id: "m2", remoteJid: "user3@s.whatsapp.net", fromMe: true },
85
+ pushName: "SelfBot",
86
+ },
87
+ // Message in group -> captures participant
88
+ {
89
+ key: { id: "m3", remoteJid: "group1@g.us", participant: "user4@s.whatsapp.net", fromMe: false },
90
+ pushName: "Charlie",
91
+ },
92
+ ],
93
+ });
94
+ assert.equal(store.contacts["user2@s.whatsapp.net"]?.notify, "Bob");
95
+ assert.equal(store.contacts["user3@s.whatsapp.net"], undefined);
96
+ assert.equal(store.contacts["user4@s.whatsapp.net"]?.notify, "Charlie");
97
+ });
98
+ test("store evicts oldest messages when exceeding MAX_MSGS_PER_CHAT (200)", () => {
99
+ const store = createStore();
100
+ const ev = new EventEmitter();
101
+ store.bind(ev);
102
+ const jid = "chat-flood@s.whatsapp.net";
103
+ const messages = [];
104
+ for (let i = 1; i <= 205; i++) {
105
+ messages.push({
106
+ key: { id: `msg-${i}`, remoteJid: jid },
107
+ message: { conversation: `text ${i}` },
108
+ });
109
+ }
110
+ ev.emit("messages.upsert", { messages });
111
+ const chatMap = store.messages.get(jid);
112
+ assert.ok(chatMap);
113
+ assert.equal(chatMap?.size, 200);
114
+ assert.equal(chatMap?.has("msg-1"), false); // evicted
115
+ assert.equal(chatMap?.has("msg-5"), false); // evicted
116
+ assert.equal(chatMap?.has("msg-6"), true); // kept
117
+ assert.equal(chatMap?.has("msg-205"), true); // kept
118
+ });
119
+ test("store accumulates poll updates on messages.update", () => {
120
+ const store = createStore();
121
+ const ev = new EventEmitter();
122
+ store.bind(ev);
123
+ const jid = "poll-chat@s.whatsapp.net";
124
+ const msgId = "poll-1";
125
+ // Initial poll message
126
+ ev.emit("messages.upsert", {
127
+ messages: [
128
+ {
129
+ key: { id: msgId, remoteJid: jid },
130
+ message: { pollCreationMessage: { name: "Question?" } },
131
+ }
132
+ ]
133
+ });
134
+ // First vote update
135
+ ev.emit("messages.update", [
136
+ {
137
+ key: { id: msgId, remoteJid: jid },
138
+ update: { pollUpdates: [{ voter: "user1" }] },
139
+ }
140
+ ]);
141
+ // Second vote update
142
+ ev.emit("messages.update", [
143
+ {
144
+ key: { id: msgId, remoteJid: jid },
145
+ update: { pollUpdates: [{ voter: "user2" }] },
146
+ }
147
+ ]);
148
+ const stored = store.messages.get(jid)?.get(msgId);
149
+ assert.ok(stored);
150
+ assert.equal(stored.pollUpdates?.length, 2);
151
+ assert.deepEqual(stored.pollUpdates, [{ voter: "user1" }, { voter: "user2" }]);
152
+ });
153
+ test("store processes messaging-history.set backfill", () => {
154
+ const store = createStore();
155
+ const ev = new EventEmitter();
156
+ store.bind(ev);
157
+ ev.emit("messaging-history.set", {
158
+ chats: [{ id: "hist-chat@s.whatsapp.net", name: "History Chat" }],
159
+ contacts: [{ id: "hist-user@s.whatsapp.net", name: "Dave" }],
160
+ messages: [
161
+ {
162
+ key: { id: "m-hist", remoteJid: "hist-chat@s.whatsapp.net", fromMe: false },
163
+ pushName: "Dave Push",
164
+ }
165
+ ],
166
+ });
167
+ assert.equal(store.chats.get("hist-chat@s.whatsapp.net")?.name, "History Chat");
168
+ assert.equal(store.contacts["hist-user@s.whatsapp.net"]?.name, "Dave");
169
+ assert.equal(store.contacts["hist-chat@s.whatsapp.net"]?.notify, "Dave Push");
170
+ });
package/dist/config.js CHANGED
@@ -25,7 +25,7 @@ import { t } from "#i18n";
25
25
  // ---------------------------------------------------------------------------
26
26
  // Paths
27
27
  // ---------------------------------------------------------------------------
28
- export const CONFIG_DIR = path.join(os.homedir(), ".manybot");
28
+ export const CONFIG_DIR = process.env.MANYBOT_CONFIG_DIR ?? path.join(os.homedir(), ".manybot");
29
29
  /** @deprecated Use TOML_CONFIG_FILE. Frozen — no new keys. */
30
30
  export const CONFIG_FILE = path.join(CONFIG_DIR, "manybot.conf");
31
31
  /** @deprecated Use TOML_PLUGIN_FILE. Frozen — no new keys. */
@@ -210,6 +210,11 @@ PHONE_NUMBER = ""
210
210
  # but reduce the risk of WhatsApp flagging the account.
211
211
  SECURITY_LEVEL = "medium"
212
212
 
213
+ # Startup log verbosity: "normal" (everything), "clean" (hides routine
214
+ # chatter, keeps success/warnings/errors), or "minimal" (warnings/errors
215
+ # only — silent on the happy path).
216
+ LOG_LEVEL = "normal"
217
+
213
218
  # ── Local alerts (critical warnings, update notices) ──────────────────────
214
219
  # Phone number (or full JID) that receives WhatsApp alerts when the bot
215
220
  # itself is up — e.g. "5511999999999" or "5511999999999@s.whatsapp.net".
@@ -248,18 +253,6 @@ LOGIN_METHOD = ""
248
253
  STATUS_ENABLED = true
249
254
  STATUS_PORT = 8080
250
255
 
251
- # ── Driver selection (advanced) ────────────────────────────────────────────
252
- # ManyBot can fall back from a primary driver to a secondary one when the
253
- # primary fails to confirm a send. Today only Baileys is wired in; leave
254
- # driver_whatsmeow_enabled = false to disable the fallback entirely (no
255
- # extra processes are spawned, no sockets are opened).
256
- driver_primary = "baileys"
257
- driver_fallback_cooldown_ms = 60000
258
- driver_verify_window_ms = [500, 1000, 1500, 2500, 5000]
259
- driver_baileys_enabled = true
260
- driver_whatsmeow_enabled = false
261
- driver_whatsmeow_grpc_address = "localhost:50051"
262
- driver_whatsmeow_binary_path = ""
263
256
  `;
264
257
  const DEFAULT_TOML_PT = `# Arquivo de configuração do ManyBot
265
258
  # Veja https://manybot.org/docs/config para saber mais
@@ -276,6 +269,11 @@ PHONE_NUMBER = ""
276
269
  # maiores), mas reduzem o risco do WhatsApp sinalizar a conta.
277
270
  SECURITY_LEVEL = "medium"
278
271
 
272
+ # Verbosidade dos logs de inicialização: "normal" (tudo), "clean" (esconde
273
+ # ruído de rotina, mantém sucesso/avisos/erros), ou "minimal" (só avisos
274
+ # e erros — silencioso quando está tudo bem).
275
+ LOG_LEVEL = "normal"
276
+
279
277
  # ── Avisos locais (alertas críticos, aviso de update) ──────────────────────
280
278
  # Número de telefone (ou JID completo) que recebe alertas via WhatsApp
281
279
  # quando o bot está de pé — ex. "5511999999999" ou
@@ -316,18 +314,6 @@ LOGIN_METHOD = ""
316
314
  STATUS_ENABLED = true
317
315
  STATUS_PORT = 8080
318
316
 
319
- # ── Seleção de driver (avançado) ───────────────────────────────────────────
320
- # ManyBot pode cair de um driver primário pra um secundário quando o
321
- # primário não confirma um envio. Hoje só Baileys está ligado; deixe
322
- # driver_whatsmeow_enabled = false pra desligar o fallback por completo
323
- # (nenhum processo extra é spawnado, nenhum socket é aberto).
324
- driver_primary = "baileys"
325
- driver_fallback_cooldown_ms = 60000
326
- driver_verify_window_ms = [500, 1000, 1500, 2500, 5000]
327
- driver_baileys_enabled = true
328
- driver_whatsmeow_enabled = false
329
- driver_whatsmeow_grpc_address = "localhost:50051"
330
- driver_whatsmeow_binary_path = ""
331
317
  `;
332
318
  const DEFAULT_TOML = detectSystemLang() === "pt" ? DEFAULT_TOML_PT : DEFAULT_TOML_EN;
333
319
  await fs.mkdir(CONFIG_DIR, { recursive: true });
@@ -368,9 +354,11 @@ const DEFAULTS = {
368
354
  CHATS: [],
369
355
  EXCLUDE_CHATS: [],
370
356
  SECURITY_LEVEL: "medium",
357
+ LOG_LEVEL: "normal",
371
358
  PLUGINS: [],
372
359
  LANGUAGE: "en",
373
360
  PHONE_NUMBER: null,
361
+ OWNER_NUMBER: null,
374
362
  LOGIN_METHOD: null,
375
363
  ADMIN_JID: "",
376
364
  SMTP_HOST: "",
@@ -390,7 +378,6 @@ const DEFAULTS = {
390
378
  fallbackCooldownMs: 60000,
391
379
  verifyWindowMs: [500, 1000, 1500, 2500, 5000],
392
380
  baileys: { enabled: true },
393
- whatsmeow: { enabled: false, grpcAddress: "localhost:50051", binaryPath: "" },
394
381
  },
395
382
  };
396
383
  function normalize(cfg) {
@@ -398,6 +385,13 @@ function normalize(cfg) {
398
385
  // can always do a simple truthiness check regardless of config source.
399
386
  if (cfg.PHONE_NUMBER === "")
400
387
  cfg.PHONE_NUMBER = null;
388
+ const rawOwner = cfg.owner_number ?? cfg.OWNER_NUMBER;
389
+ if (typeof rawOwner === "string" && rawOwner.trim().length > 0) {
390
+ cfg.OWNER_NUMBER = rawOwner.trim();
391
+ }
392
+ else {
393
+ cfg.OWNER_NUMBER = null;
394
+ }
401
395
  // Anything other than "phone"/"qr" is treated as "not configured yet",
402
396
  // so a typo or leftover garbage value falls back to the interactive
403
397
  // prompt instead of silently misbehaving.
@@ -407,6 +401,9 @@ function normalize(cfg) {
407
401
  if (!["low", "medium", "high"].includes(cfg.SECURITY_LEVEL)) {
408
402
  cfg.SECURITY_LEVEL = "medium";
409
403
  }
404
+ if (!["normal", "clean", "minimal"].includes(cfg.LOG_LEVEL)) {
405
+ cfg.LOG_LEVEL = "normal";
406
+ }
410
407
  // Legacy .conf values arrive as strings; TOML gives real types already —
411
408
  // Number()/coercion here is a no-op for the TOML path.
412
409
  cfg.SMTP_PORT = Number(cfg.SMTP_PORT) || 587;
@@ -423,8 +420,8 @@ function normalize(cfg) {
423
420
  cfg.STATUS_PORT = Number(cfg.STATUS_PORT) || 8080;
424
421
  // drivers.* — the fallback guard and DriverManager read these at every
425
422
  // send. The TOML surface is flat lowercase keys (driver_primary,
426
- // driver_baileys_enabled, driver_whatsmeow_*, …) so it matches the
427
- // rest of the file; the nested `cfg.drivers` object is rebuilt here
423
+ // driver_baileys_enabled, …) same style as the
424
+ // rest of this file. `normalize()` rebuilds the nested object below
428
425
  // from those flat keys so callers (main.ts, sendFallbackGuard.ts,
429
426
  // supervisor.ts, client.ts) keep using `CONFIG.drivers.*`.
430
427
  // Coerce strings (legacy .conf arrives as strings) and reject anything
@@ -443,15 +440,11 @@ function normalize(cfg) {
443
440
  // Allow either the new flat keys or (for backward compatibility) the
444
441
  // old nested `[drivers]` block. Flat keys take precedence.
445
442
  const nestedDrv = (cfg.drivers ?? {});
446
- const flatPrimary = cfg.driver_primary ?? nestedDrv.primary;
447
443
  const flatCooldown = cfg.driver_fallback_cooldown_ms ?? nestedDrv.fallbackCooldownMs;
448
444
  const flatVerify = cfg.driver_verify_window_ms ?? nestedDrv.verifyWindowMs;
449
445
  const flatBaileysEnabled = cfg.driver_baileys_enabled ?? nestedDrv.baileys?.enabled;
450
- const flatWhatsmeowEnabled = cfg.driver_whatsmeow_enabled ?? nestedDrv.whatsmeow?.enabled;
451
- const flatWhatsmeowGrpc = cfg.driver_whatsmeow_grpc_address ?? nestedDrv.whatsmeow?.grpcAddress;
452
- const flatWhatsmeowBinary = cfg.driver_whatsmeow_binary_path ?? nestedDrv.whatsmeow?.binaryPath;
453
446
  cfg.drivers = {
454
- primary: flatPrimary === "whatsmeow" ? "whatsmeow" : "baileys",
447
+ primary: "baileys",
455
448
  fallbackCooldownMs: Number(flatCooldown) || 60000,
456
449
  verifyWindowMs: Array.isArray(flatVerify) && flatVerify.length
457
450
  ? flatVerify.map((n) => Number(n)).filter((n) => Number.isFinite(n) && n > 0)
@@ -459,17 +452,6 @@ function normalize(cfg) {
459
452
  baileys: {
460
453
  enabled: coerceEnabled(flatBaileysEnabled, true),
461
454
  },
462
- whatsmeow: {
463
- // MANYBOT_SMOKE=1 force-enables whatsmeow regardless of the TOML
464
- // value — used by scripts/smoke-whatsmeow-supervisor.mjs so the
465
- // smoke test doesn't depend on the local manybot.toml already
466
- // having driver_whatsmeow_enabled = true. Not a documented
467
- // user-facing setting; TOML remains the source of truth outside
468
- // smoke runs.
469
- enabled: process.env.MANYBOT_SMOKE === "1" ? true : isTruthy(flatWhatsmeowEnabled),
470
- grpcAddress: typeof flatWhatsmeowGrpc === "string" ? flatWhatsmeowGrpc : "localhost:50051",
471
- binaryPath: typeof flatWhatsmeowBinary === "string" ? flatWhatsmeowBinary : "",
472
- },
473
455
  };
474
456
  // Drop the flat keys from the merged object so they don't leak as
475
457
  // loose top-level Config keys (the `Config` interface exposes only
@@ -515,9 +497,11 @@ export const CMD_PREFIX = CONFIG.CMD_PREFIX;
515
497
  export const CHATS = CONFIG.CHATS;
516
498
  export const EXCLUDE_CHATS = CONFIG.EXCLUDE_CHATS;
517
499
  export const SECURITY_LEVEL = CONFIG.SECURITY_LEVEL;
500
+ export const LOG_LEVEL = CONFIG.LOG_LEVEL;
518
501
  export const PLUGINS = CONFIG.PLUGINS;
519
502
  export const LANGUAGE = CONFIG.LANGUAGE;
520
503
  export const PHONE_NUMBER = CONFIG.PHONE_NUMBER;
504
+ export const OWNER_NUMBER = CONFIG.OWNER_NUMBER;
521
505
  export const LOGIN_METHOD = CONFIG.LOGIN_METHOD;
522
506
  export const ADMIN_JID = CONFIG.ADMIN_JID;
523
507
  export const SMTP_HOST = CONFIG.SMTP_HOST;
@@ -0,0 +1,26 @@
1
+ import test, { describe } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import path from "path";
4
+ import os from "os";
5
+ import { CONFIG_DIR, CONFIG } from "#config";
6
+ describe("config", () => {
7
+ test("CONFIG_DIR defaults to ~/.manybot or respects env override", () => {
8
+ if (process.env.MANYBOT_CONFIG_DIR) {
9
+ assert.equal(CONFIG_DIR, process.env.MANYBOT_CONFIG_DIR);
10
+ }
11
+ else {
12
+ assert.equal(CONFIG_DIR, path.join(os.homedir(), ".manybot"));
13
+ }
14
+ });
15
+ test("CONFIG has required structure and default fallback values", () => {
16
+ assert.ok(typeof CONFIG.CMD_PREFIX === "string");
17
+ assert.ok(typeof CONFIG.CLIENT_ID === "string");
18
+ assert.ok(Array.isArray(CONFIG.CHATS));
19
+ assert.ok(Array.isArray(CONFIG.EXCLUDE_CHATS));
20
+ assert.ok(["low", "medium", "high"].includes(CONFIG.SECURITY_LEVEL));
21
+ assert.ok(["normal", "clean", "minimal"].includes(CONFIG.LOG_LEVEL));
22
+ assert.ok(CONFIG.drivers);
23
+ assert.ok(CONFIG.drivers.primary === "baileys");
24
+ assert.ok(typeof CONFIG.drivers.baileys.enabled === "boolean");
25
+ });
26
+ });