@manybot/manybot 5.1.0 → 5.2.1

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 (51) hide show
  1. package/README.md +5 -7
  2. package/dist/client/banner.js +35 -0
  3. package/dist/client/store.js +135 -0
  4. package/dist/config.js +363 -0
  5. package/dist/core/adapter.js +12 -0
  6. package/dist/core/capabilities.js +16 -0
  7. package/dist/core/types.js +6 -0
  8. package/dist/download/queue.js +49 -0
  9. package/dist/drivers/index.js +14 -0
  10. package/dist/drivers/patches/index.js +9 -0
  11. package/dist/drivers/patches/libsignal.js +16 -0
  12. package/dist/drivers/patches/patch.js +1 -0
  13. package/dist/drivers/whatsapp/adapter.js +7 -0
  14. package/dist/drivers/whatsapp/api/index.js +1311 -0
  15. package/dist/drivers/whatsapp/index.js +164 -0
  16. package/dist/drivers/whatsapp/loginPrompt.js +81 -0
  17. package/dist/drivers/whatsapp/messageHandler.js +111 -0
  18. package/dist/drivers/whatsapp/sdk/baileysSock.js +124 -0
  19. package/dist/i18n/index.js +202 -0
  20. package/dist/kernel/pluginApi.js +11 -0
  21. package/dist/kernel/pluginGuard.js +88 -0
  22. package/dist/kernel/pluginLoader.js +322 -0
  23. package/dist/kernel/scheduler.js +110 -0
  24. package/dist/kernel/sendGuard.js +121 -0
  25. package/dist/kernel/settingsDb.js +205 -0
  26. package/{src → dist}/locales/en.json +18 -33
  27. package/dist/locales/es.json +52 -0
  28. package/{src → dist}/locales/pt.json +18 -34
  29. package/dist/logger/logger.js +16 -0
  30. package/dist/main.js +82 -0
  31. package/dist/types.js +16 -0
  32. package/{src → dist}/utils/file.js +3 -3
  33. package/package.json +35 -26
  34. package/src/client/banner.js +0 -57
  35. package/src/client/whatsappClient.js +0 -185
  36. package/src/config.js +0 -288
  37. package/src/download/queue.js +0 -55
  38. package/src/i18n/index.js +0 -235
  39. package/src/kernel/messageHandler.js +0 -88
  40. package/src/kernel/pluginApi.js +0 -630
  41. package/src/kernel/pluginGuard.js +0 -78
  42. package/src/kernel/pluginLoader.js +0 -177
  43. package/src/kernel/pluginState.js +0 -99
  44. package/src/kernel/scheduler.js +0 -48
  45. package/src/kernel/sendGuard.js +0 -166
  46. package/src/locales/es.json +0 -62
  47. package/src/logger/logger.js +0 -32
  48. package/src/main.js +0 -136
  49. package/src/utils/getChatId.js +0 -3
  50. package/src/utils/get_id.js +0 -177
  51. package/src/utils/pluginI18n.js +0 -129
package/README.md CHANGED
@@ -6,7 +6,7 @@
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)
9
- ![whatsapp-web.js](https://img.shields.io/badge/WhatsApp-25D366?logo=whatsapp&logoColor=white)
9
+ ![Baileys](https://img.shields.io/badge/WhatsApp-25D366?logo=whatsapp&logoColor=white)
10
10
 
11
11
  </div>
12
12
 
@@ -27,9 +27,9 @@ npm install -g @manybot/manyplug
27
27
  manybot
28
28
  ```
29
29
 
30
- On first run, a configuration file is created at `~/.manybot/manybot.conf`. Edit it to set up your preferences and list the plugins you want to load.
30
+ On first run, a configuration file is created at `~/.manybot/manybot.toml`. Edit it to set up your preferences.
31
31
 
32
- For detailed setup instructions, see the **[documentation](https://manybot.stxerr.dev/docs/)**.
32
+ For detailed setup instructions, see the **[documentation](https://manybot.org/docs/)**.
33
33
 
34
34
  ## Plugins
35
35
 
@@ -39,16 +39,14 @@ ManyBot's functionality comes from plugins. Install them with ManyPlug:
39
39
  manyplug install <plugin-name>
40
40
  ```
41
41
 
42
- Browse available plugins at **[manybot.stxerr.dev/plugins](https://manybot.stxerr.dev/plugins/)**.
43
-
44
- Want to build your own? The plugin API lets you add commands and features without touching the bot's core - *documentation coming soon*.
42
+ Browse available plugins at **[manybot.org/plugins](https://manybot.org/plugins/)**.
45
43
 
46
44
  ## Contributing
47
45
 
48
46
  All kinds of contributions are welcome:
49
47
 
50
48
  - **Bug reports and feature requests**: open an issue on GitHub or Codeberg
51
- - **Code**: pull requests are welcome on [GitHub](https://github.com/many-bot/manybot) or [Codeberg](https://codeberg.org/many-bot/manybot); patches by email (`devel+manybot@stxerr.dev`) are also accepted - subscribe to the mailing list [here](https://list.stxerr.dev).
49
+ - **Code**: pull requests are welcome on [GitHub](https://github.com/many-bot/manybot) or [Codeberg](https://codeberg.org/many-bot/manybot); patches by email (`devel+manybot.org`) are also accepted - subscribe to the mailing list [here](https://list.stxerr.dev).
52
50
  - **Plugins**: submit your plugin to [manyplug-repo](https://github.com/many-bot/manyplug-repo), which has instructions on how to do it
53
51
  - **Anything else**: suggestions, translations, documentation fixes - reach out by email or open an issue
54
52
 
@@ -0,0 +1,35 @@
1
+ const C = {
2
+ reset: "\x1b[0m",
3
+ bold: "\x1b[1m",
4
+ blue: "\x1b[94m",
5
+ magenta: "\x1b[95m",
6
+ cyan: "\x1b[96m",
7
+ gray: "\x1b[90m",
8
+ yellow: "\x1b[93m",
9
+ };
10
+ import { readFileSync } from "fs";
11
+ import { fileURLToPath } from "url";
12
+ import path from "path";
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const v = JSON.parse(readFileSync(path.join(__dirname, "../../package.json"), "utf8")).version;
16
+ export function printBanner() {
17
+ const banner = [
18
+ ` _ _ `,
19
+ ` | | | | `,
20
+ ` _ __ ___ __ _ _ __ _ _| |__ ___ | |_`,
21
+ `| '_ \` _ \\ / _\` | '_ \\| | | | '_ \\ / _ \\| __`,
22
+ `| | | | | | (_| | | | | |_| | |_) | (_) | |_`,
23
+ `|_| |_| |_|\\__,_|_| |_|\\__, |_.__/ \\___/ \\__`,
24
+ ` __/ | `,
25
+ ` ${v} |___/ `
26
+ ];
27
+ console.log(`${C.bold}${C.blue}`);
28
+ console.log(banner.join("\n"));
29
+ console.log(C.reset);
30
+ console.log(` made with ${C.magenta}<3${C.reset} by ${C.bold}${C.cyan}SyntaxError!${C.reset} ${C.gray}<me@stxerr.dev>${C.reset}`);
31
+ console.log();
32
+ console.log(` ${C.gray}website${C.reset} : ${C.yellow}https://manybot.org${C.reset}`);
33
+ console.log(` ${C.gray}github ${C.reset} : ${C.yellow}https://github.com/many-bot${C.reset}`);
34
+ console.log();
35
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * store.ts
3
+ *
4
+ * Minimal in-memory store for chats, contacts, and messages.
5
+ * Replaces the removed makeInMemoryStore from Baileys 6.7+.
6
+ *
7
+ * Only tracks what the ManyBot kernel actually needs:
8
+ * - chat names (for display and for building WAChat adapters)
9
+ * - contact names / notify (pushname) (for ctx.contacts)
10
+ * - messages by JID + ID (for poll vote decryption)
11
+ */
12
+ // ── Factory ───────────────────────────────────────────────────────────────────
13
+ /**
14
+ * Create a new BotStore instance.
15
+ * One store is shared across reconnects — bind it to each new socket.
16
+ */
17
+ export function createStore() {
18
+ const chatsMap = new Map();
19
+ const contacts = {};
20
+ const messages = new Map();
21
+ // @lid JID → traditional @s.whatsapp.net JID, learned from contact and
22
+ // message-key pairs (Baileys exposes both forms during the LID rollout).
23
+ const lidMap = new Map();
24
+ function learnLid(lid, pn) {
25
+ if (lid && pn && lid.endsWith("@lid") && !pn.endsWith("@lid"))
26
+ lidMap.set(lid, pn);
27
+ }
28
+ function resolveJid(jid) {
29
+ if (!jid || !jid.endsWith("@lid"))
30
+ return jid;
31
+ return lidMap.get(jid) ?? jid;
32
+ }
33
+ // Max messages kept per chat (prevents unbounded memory growth)
34
+ const MAX_MSGS_PER_CHAT = 200;
35
+ function upsertChat(chat) {
36
+ const name = chat.name ?? chat.id.split("@")[0];
37
+ chatsMap.set(chat.id, { id: chat.id, name });
38
+ }
39
+ function upsertContact(contact) {
40
+ contacts[contact.id] = {
41
+ id: contact.id,
42
+ name: contact.name,
43
+ notify: contact.notify,
44
+ verifiedName: contact.verifiedName,
45
+ };
46
+ // Baileys' Contact carries both the traditional JID (id) and the LID
47
+ // (lid) once known — learn the mapping either direction.
48
+ const lidField = contact.lid;
49
+ learnLid(lidField, contact.id);
50
+ learnLid(contact.id, lidField);
51
+ }
52
+ function storeMessage(msg) {
53
+ const jid = msg.key.remoteJid;
54
+ const id = msg.key.id;
55
+ if (!jid || !id)
56
+ return;
57
+ if (!messages.has(jid))
58
+ messages.set(jid, new Map());
59
+ const chatMsgs = messages.get(jid);
60
+ chatMsgs.set(id, msg);
61
+ // Evict oldest entries if over limit
62
+ if (chatMsgs.size > MAX_MSGS_PER_CHAT) {
63
+ const oldest = chatMsgs.keys().next().value;
64
+ if (typeof oldest === "string")
65
+ chatMsgs.delete(oldest);
66
+ }
67
+ }
68
+ function bind(ev) {
69
+ ev.on("chats.upsert", (newChats) => {
70
+ for (const chat of newChats)
71
+ upsertChat(chat);
72
+ });
73
+ ev.on("chats.update", (updates) => {
74
+ for (const update of updates) {
75
+ if (!update.id)
76
+ continue;
77
+ const existing = chatsMap.get(update.id);
78
+ const name = update.name;
79
+ if (existing && name)
80
+ existing.name = name;
81
+ }
82
+ });
83
+ ev.on("contacts.upsert", (newContacts) => {
84
+ for (const contact of newContacts)
85
+ upsertContact(contact);
86
+ });
87
+ ev.on("contacts.update", (updates) => {
88
+ for (const update of updates) {
89
+ if (!update.id)
90
+ continue;
91
+ contacts[update.id] = { ...contacts[update.id], ...update };
92
+ const lidField = update.lid;
93
+ learnLid(lidField, update.id);
94
+ learnLid(update.id, lidField);
95
+ }
96
+ });
97
+ ev.on("messages.upsert", ({ messages: msgs }) => {
98
+ for (const msg of msgs) {
99
+ storeMessage(msg);
100
+ const key = msg.key;
101
+ learnLid(key.participantAlt, key.participant);
102
+ learnLid(key.remoteJidAlt, key.remoteJid);
103
+ }
104
+ });
105
+ ev.on("messages.update", (updates) => {
106
+ for (const { key, update } of updates) {
107
+ if (!key.remoteJid || !key.id)
108
+ continue;
109
+ const existing = messages.get(key.remoteJid)?.get(key.id);
110
+ if (!existing)
111
+ continue;
112
+ // Poll votes: pollUpdates on this event are only the NEW entries,
113
+ // not the full history — must be accumulated, never overwritten,
114
+ // or getAggregateVotesInPollMessage() only ever sees the latest vote.
115
+ const incoming = update.pollUpdates;
116
+ const merged = { ...existing, ...update };
117
+ if (incoming?.length) {
118
+ const prior = existing.pollUpdates ?? [];
119
+ merged.pollUpdates = [...prior, ...incoming];
120
+ }
121
+ messages.get(key.remoteJid).set(key.id, merged);
122
+ }
123
+ });
124
+ }
125
+ return {
126
+ chats: {
127
+ get: (id) => chatsMap.get(id) ?? null,
128
+ all: () => [...chatsMap.values()],
129
+ },
130
+ contacts,
131
+ messages,
132
+ resolveJid,
133
+ bind,
134
+ };
135
+ }
package/dist/config.js ADDED
@@ -0,0 +1,363 @@
1
+ /**
2
+ * config.ts
3
+ *
4
+ * Loads config from up to four files, merged in strict precedence order:
5
+ *
6
+ * defaults < legacy .conf < TOML
7
+ *
8
+ * Legacy files (frozen — no new keys):
9
+ * ~/.manybot/manybot.conf
10
+ * ~/.manybot/manyplug.conf
11
+ *
12
+ * TOML files (all new features go here):
13
+ * ~/.manybot/manybot.toml
14
+ * ~/.manybot/manyplug.toml
15
+ *
16
+ * The final CONFIG object always has the same shape regardless of which
17
+ * files are present. Plugins must never see a structural difference.
18
+ */
19
+ import fs from "fs/promises";
20
+ import os from "os";
21
+ import path from "path";
22
+ import { parse as parseToml } from "smol-toml";
23
+ import { logger } from "#logger";
24
+ // ---------------------------------------------------------------------------
25
+ // Paths
26
+ // ---------------------------------------------------------------------------
27
+ export const CONFIG_DIR = path.join(os.homedir(), ".manybot");
28
+ /** @deprecated Use TOML_CONFIG_FILE. Frozen — no new keys. */
29
+ export const CONFIG_FILE = path.join(CONFIG_DIR, "manybot.conf");
30
+ /** @deprecated Use TOML_PLUGIN_FILE. Frozen — no new keys. */
31
+ export const PLUGIN_FILE = path.join(CONFIG_DIR, "manyplug.conf");
32
+ export const TOML_CONFIG_FILE = path.join(CONFIG_DIR, "manybot.toml");
33
+ export const TOML_PLUGIN_FILE = path.join(CONFIG_DIR, "manyplug.toml");
34
+ // ---------------------------------------------------------------------------
35
+ // Legacy .conf parser (frozen — do not extend)
36
+ // ---------------------------------------------------------------------------
37
+ function parseValue(value) {
38
+ value = value.trim();
39
+ if ((value.startsWith('"') && value.endsWith('"')) ||
40
+ (value.startsWith("'") && value.endsWith("'"))) {
41
+ value = value.slice(1, -1);
42
+ }
43
+ if (value === "true")
44
+ return true;
45
+ if (value === "false")
46
+ return false;
47
+ return value;
48
+ }
49
+ function stripInlineComment(line) {
50
+ let result = "";
51
+ let quote = null;
52
+ for (let i = 0; i < line.length; i++) {
53
+ const ch = line[i];
54
+ if ((ch === '"' || ch === "'") && line[i - 1] !== "\\") {
55
+ if (quote === ch)
56
+ quote = null;
57
+ else if (!quote)
58
+ quote = ch;
59
+ }
60
+ if (ch === "#" && !quote)
61
+ break;
62
+ result += ch;
63
+ }
64
+ return result.trim();
65
+ }
66
+ function parseConf(raw) {
67
+ const lines = raw.split(/\r?\n/);
68
+ const mergedLines = [];
69
+ let insideList = false;
70
+ let buffer = "";
71
+ for (let line of lines) {
72
+ line = stripInlineComment(line);
73
+ if (!line)
74
+ continue;
75
+ if (!insideList) {
76
+ if (/=\s*\[$/.test(line)) {
77
+ insideList = true;
78
+ buffer = line;
79
+ }
80
+ else {
81
+ mergedLines.push(line);
82
+ }
83
+ }
84
+ else {
85
+ buffer += " " + line;
86
+ if (line.includes("]")) {
87
+ mergedLines.push(buffer);
88
+ buffer = "";
89
+ insideList = false;
90
+ }
91
+ }
92
+ }
93
+ const config = {};
94
+ for (const line of mergedLines) {
95
+ const idx = line.indexOf("=");
96
+ if (idx === -1)
97
+ continue;
98
+ const key = line.slice(0, idx).trim();
99
+ let value = line.slice(idx + 1).trim();
100
+ if (value.startsWith("[") && value.endsWith("]")) {
101
+ config[key] = value
102
+ .slice(1, -1)
103
+ .split(",")
104
+ .map(v => parseValue(v))
105
+ .filter(v => v !== "");
106
+ continue;
107
+ }
108
+ config[key] = parseValue(value);
109
+ }
110
+ return config;
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // TOML migration
114
+ // ---------------------------------------------------------------------------
115
+ function escapeTomlString(s) {
116
+ return s
117
+ .replaceAll("\\", "\\\\")
118
+ .replaceAll('"', '\\"');
119
+ }
120
+ function toToml(obj) {
121
+ return Object.entries(obj)
122
+ .map(([k, v]) => {
123
+ if (Array.isArray(v))
124
+ return `${k} = [${v.map((v) => JSON.stringify(v)).join(", ")}]`;
125
+ if (v == null || v === "")
126
+ return `${k} = ""`;
127
+ return typeof v === "string"
128
+ ? `${k} = "${escapeTomlString(v)}"`
129
+ : `${k} = ${v}`;
130
+ })
131
+ .join("\n") + "\n";
132
+ }
133
+ async function migrateLegacyIfNeeded() {
134
+ if (await fileExists(TOML_CONFIG_FILE))
135
+ return;
136
+ const migrate = async (src, dest, omit = []) => {
137
+ const raw = await readFileSafe(src);
138
+ if (!raw)
139
+ return false;
140
+ const cfg = parseConf(raw);
141
+ for (const k of omit)
142
+ delete cfg[k];
143
+ await fs.writeFile(dest, toToml(cfg), "utf8");
144
+ await fs.rename(src, `${src}.bak`);
145
+ return true;
146
+ };
147
+ const migrated = await migrate(CONFIG_FILE, TOML_CONFIG_FILE, ["PLUGINS"]) ||
148
+ await migrate(PLUGIN_FILE, TOML_PLUGIN_FILE);
149
+ if (migrated)
150
+ logger.success("Config migrated to TOML");
151
+ }
152
+ // ---------------------------------------------------------------------------
153
+ // File helpers
154
+ // ---------------------------------------------------------------------------
155
+ async function fileExists(file) {
156
+ try {
157
+ await fs.stat(file);
158
+ return true;
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ async function readFileSafe(file) {
165
+ try {
166
+ return await fs.readFile(file, "utf-8");
167
+ }
168
+ catch (e) {
169
+ if (e.code !== "ENOENT")
170
+ logger.warn(`Error reading ${file}: ${e.message}`);
171
+ return null;
172
+ }
173
+ }
174
+ // ---------------------------------------------------------------------------
175
+ // Bootstrap: ensure at least one config file exists
176
+ // ---------------------------------------------------------------------------
177
+ /**
178
+ * Detects the OS locale without depending on a single env var, since LANG
179
+ * isn't reliably set on macOS GUI sessions or Windows. Only used to pick
180
+ * the language of the bootstrap config file — after that, LANGUAGE in
181
+ * manybot.toml is the single source of truth.
182
+ */
183
+ function detectSystemLang() {
184
+ try {
185
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
186
+ if (locale)
187
+ return locale.split("-")[0].toLowerCase();
188
+ }
189
+ catch {
190
+ // Intl unavailable — fall through to env vars
191
+ }
192
+ const envLocale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE;
193
+ if (envLocale)
194
+ return envLocale.split(/[_.]/)[0].toLowerCase();
195
+ return "en";
196
+ }
197
+ const DEFAULT_TOML_EN = `# ManyBot configuration file
198
+ # See https://manybot.org/docs/config to learn more
199
+
200
+ CLIENT_ID = "manybot"
201
+ CMD_PREFIX = "!"
202
+ CHATS = []
203
+ LANGUAGE = "en"
204
+ PHONE_NUMBER = ""
205
+
206
+ # How to connect the first time: "qr" (scan with WhatsApp on your phone)
207
+ # or "phone" (receive a pairing code on the number set in PHONE_NUMBER).
208
+ # Leave blank to choose interactively on first run — the choice is then
209
+ # saved here automatically.
210
+ 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
+ `;
219
+ const DEFAULT_TOML_PT = `# Arquivo de configuração do ManyBot
220
+ # Veja https://manybot.org/docs/config para saber mais
221
+
222
+ CLIENT_ID = "manybot"
223
+ CMD_PREFIX = "!"
224
+ CHATS = []
225
+ LANGUAGE = "pt"
226
+ PHONE_NUMBER = ""
227
+
228
+ # Como conectar pela primeira vez: "qr" (escaneie com o WhatsApp no celular)
229
+ # ou "phone" (recebe um código de pareamento no número definido em
230
+ # PHONE_NUMBER). Deixe em branco para escolher interativamente na primeira
231
+ # execução — a escolha é salva aqui automaticamente.
232
+ 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
+ `;
240
+ const DEFAULT_TOML = detectSystemLang() === "pt" ? DEFAULT_TOML_PT : DEFAULT_TOML_EN;
241
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
242
+ await migrateLegacyIfNeeded();
243
+ if (!await fileExists(TOML_CONFIG_FILE)) {
244
+ logger.warn(`Creating ${TOML_CONFIG_FILE}`);
245
+ await fs.writeFile(TOML_CONFIG_FILE, DEFAULT_TOML);
246
+ }
247
+ // ---------------------------------------------------------------------------
248
+ // Layer 1 — legacy .conf
249
+ // ---------------------------------------------------------------------------
250
+ const legacyLayer = {};
251
+ // ---------------------------------------------------------------------------
252
+ // Layer 2 — TOML (all new features land here)
253
+ // ---------------------------------------------------------------------------
254
+ let tomlLayer = {};
255
+ async function loadToml(file, label) {
256
+ const raw = await readFileSafe(file);
257
+ if (raw === null)
258
+ return {};
259
+ try {
260
+ return parseToml(raw);
261
+ }
262
+ catch (e) {
263
+ const err = e instanceof Error ? e : new Error(String(e));
264
+ logger.warn(`Error parsing ${label}: ${err.message}`);
265
+ return {};
266
+ }
267
+ }
268
+ tomlLayer = {
269
+ ...await loadToml(TOML_CONFIG_FILE, "manybot.toml"),
270
+ ...await loadToml(TOML_PLUGIN_FILE, "manyplug.toml"),
271
+ };
272
+ const DEFAULTS = {
273
+ CMD_PREFIX: "!",
274
+ CLIENT_ID: "manybot",
275
+ CHATS: [],
276
+ PLUGINS: [],
277
+ LANGUAGE: "en",
278
+ PHONE_NUMBER: null,
279
+ PLATFORM: "whatsapp",
280
+ TEST_CHAT: null,
281
+ LOGIN_METHOD: null,
282
+ };
283
+ function normalize(cfg) {
284
+ // Empty string and absent PHONE_NUMBER are both treated as null so plugins
285
+ // can always do a simple truthiness check regardless of config source.
286
+ if (cfg.PHONE_NUMBER === "")
287
+ cfg.PHONE_NUMBER = null;
288
+ if (cfg.TEST_CHAT === "")
289
+ cfg.TEST_CHAT = null;
290
+ // Anything other than "phone"/"qr" is treated as "not configured yet",
291
+ // so a typo or leftover garbage value falls back to the interactive
292
+ // prompt instead of silently misbehaving.
293
+ if (cfg.LOGIN_METHOD !== "phone" && cfg.LOGIN_METHOD !== "qr") {
294
+ cfg.LOGIN_METHOD = null;
295
+ }
296
+ return cfg;
297
+ }
298
+ export const CONFIG = normalize({
299
+ ...DEFAULTS,
300
+ ...legacyLayer, // legacy .conf overrides defaults
301
+ ...tomlLayer, // TOML overrides legacy .conf
302
+ });
303
+ export async function reloadConfig() {
304
+ const newTomlLayer = {
305
+ ...await loadToml(TOML_CONFIG_FILE, "manybot.toml"),
306
+ ...await loadToml(TOML_PLUGIN_FILE, "manyplug.toml"),
307
+ };
308
+ const newConfig = normalize({
309
+ ...DEFAULTS,
310
+ ...legacyLayer,
311
+ ...newTomlLayer,
312
+ });
313
+ Object.assign(CONFIG, newConfig);
314
+ // Mutate array exports to propagate changes
315
+ PLUGINS.length = 0;
316
+ PLUGINS.push(...(CONFIG.PLUGINS || []));
317
+ CHATS.length = 0;
318
+ CHATS.push(...(CONFIG.CHATS || []));
319
+ }
320
+ // ---------------------------------------------------------------------------
321
+ // Named exports — identical shape regardless of config source
322
+ // ---------------------------------------------------------------------------
323
+ export const CLIENT_ID = CONFIG.CLIENT_ID;
324
+ export const CMD_PREFIX = CONFIG.CMD_PREFIX;
325
+ export const CHATS = CONFIG.CHATS;
326
+ export const PLUGINS = CONFIG.PLUGINS;
327
+ export const LANGUAGE = CONFIG.LANGUAGE;
328
+ export const PHONE_NUMBER = CONFIG.PHONE_NUMBER;
329
+ export const PLATFORM = CONFIG.PLATFORM;
330
+ export const TEST_CHAT = CONFIG.TEST_CHAT;
331
+ export const LOGIN_METHOD = CONFIG.LOGIN_METHOD;
332
+ /**
333
+ * Writes a single value back to manybot.toml without rewriting the whole
334
+ * file (preserves comments and the rest of the content). If the key
335
+ * already exists, its line is replaced; otherwise the line is appended
336
+ * at the end. Also updates the in-memory `CONFIG`, so the change is
337
+ * reflected within the same run (needed by the interactive login flow,
338
+ * which runs before any reload).
339
+ *
340
+ * Known limitation: since CLIENT_ID, PHONE_NUMBER, etc. above are const
341
+ * primitives captured at module load time, they only reflect the new
342
+ * value after the process restarts — the same limitation already
343
+ * applied to the rest of this file. Code that needs the updated value
344
+ * within the same run (like the login flow itself) should read from
345
+ * `CONFIG.<KEY>` directly.
346
+ */
347
+ export async function persistConfigValue(key, value) {
348
+ const raw = (await readFileSafe(TOML_CONFIG_FILE)) ?? "";
349
+ const newLine = `${key} = "${escapeTomlString(value)}"`;
350
+ const lineRe = new RegExp(`^${key}\\s*=.*$`, "m");
351
+ const updated = lineRe.test(raw)
352
+ ? raw.replace(lineRe, newLine)
353
+ : `${raw.trimEnd()}\n${newLine}\n`;
354
+ await fs.writeFile(TOML_CONFIG_FILE, updated, "utf8");
355
+ CONFIG[key] = value;
356
+ }
357
+ export const PATHS = {
358
+ HOME: CONFIG_DIR,
359
+ CONFIG_FILE,
360
+ PLUGIN_FILE,
361
+ TOML_CONFIG_FILE,
362
+ TOML_PLUGIN_FILE,
363
+ };
@@ -0,0 +1,12 @@
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 {};
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * core/types.ts
3
+ *
4
+ * Common domain models shared across core, drivers, and plugin boundaries.
5
+ */
6
+ export {};
@@ -0,0 +1,49 @@
1
+ /**
2
+ * src/download/queue.ts
3
+ *
4
+ * Sequential execution queue for heavy jobs (downloads, conversions).
5
+ * Ensures only one job runs at a time — without overloading yt-dlp or ffmpeg.
6
+ *
7
+ * Plugin passes a `workFn` that does everything: download, convert, send.
8
+ * Queue only handles sequence and error handling.
9
+ *
10
+ * Usage:
11
+ * import { enqueue } from "#download";
12
+ * enqueue(async () => { ... all plugin logic ... }, onError);
13
+ */
14
+ import { logger } from "#logger";
15
+ import { t } from "#i18n";
16
+ let queue = [];
17
+ let processing = false;
18
+ /**
19
+ * Add a job to the queue and start processing if idle.
20
+ * @param workFn All plugin logic — runs exclusively until resolved.
21
+ * @param errorFn Called with the thrown error if workFn rejects.
22
+ */
23
+ export function enqueue(workFn, errorFn) {
24
+ queue.push({ workFn, errorFn });
25
+ if (!processing)
26
+ processQueue();
27
+ }
28
+ async function processQueue() {
29
+ processing = true;
30
+ while (queue.length) {
31
+ const job = queue.shift();
32
+ if (job)
33
+ await processJob(job);
34
+ }
35
+ processing = false;
36
+ }
37
+ async function processJob({ workFn, errorFn }) {
38
+ try {
39
+ await workFn();
40
+ }
41
+ catch (e) {
42
+ const err = e instanceof Error ? e : new Error(String(e));
43
+ logger.error(t("system.downloadJobFailed", { message: err.message }));
44
+ try {
45
+ await errorFn(err instanceof Error ? err : new Error(String(err)));
46
+ }
47
+ catch { }
48
+ }
49
+ }
@@ -0,0 +1,14 @@
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
+ }
@@ -0,0 +1,9 @@
1
+ import libsignal from "./libsignal.js";
2
+ const patches = [
3
+ libsignal
4
+ ];
5
+ export function applyPatches() {
6
+ for (const patch of patches) {
7
+ patch.apply();
8
+ }
9
+ }