@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.
- package/README.md +5 -7
- package/dist/client/banner.js +35 -0
- package/dist/client/store.js +135 -0
- package/dist/config.js +363 -0
- package/dist/core/adapter.js +12 -0
- package/dist/core/capabilities.js +16 -0
- package/dist/core/types.js +6 -0
- package/dist/download/queue.js +49 -0
- package/dist/drivers/index.js +14 -0
- package/dist/drivers/patches/index.js +9 -0
- package/dist/drivers/patches/libsignal.js +16 -0
- package/dist/drivers/patches/patch.js +1 -0
- package/dist/drivers/whatsapp/adapter.js +7 -0
- package/dist/drivers/whatsapp/api/index.js +1311 -0
- package/dist/drivers/whatsapp/index.js +164 -0
- package/dist/drivers/whatsapp/loginPrompt.js +81 -0
- package/dist/drivers/whatsapp/messageHandler.js +111 -0
- package/dist/drivers/whatsapp/sdk/baileysSock.js +124 -0
- package/dist/i18n/index.js +202 -0
- package/dist/kernel/pluginApi.js +11 -0
- package/dist/kernel/pluginGuard.js +88 -0
- package/dist/kernel/pluginLoader.js +322 -0
- package/dist/kernel/scheduler.js +110 -0
- package/dist/kernel/sendGuard.js +121 -0
- package/dist/kernel/settingsDb.js +205 -0
- package/{src → dist}/locales/en.json +18 -33
- package/dist/locales/es.json +52 -0
- package/{src → dist}/locales/pt.json +18 -34
- package/dist/logger/logger.js +16 -0
- package/dist/main.js +82 -0
- package/dist/types.js +16 -0
- package/{src → dist}/utils/file.js +3 -3
- package/package.json +35 -26
- package/src/client/banner.js +0 -57
- package/src/client/whatsappClient.js +0 -185
- package/src/config.js +0 -288
- package/src/download/queue.js +0 -55
- package/src/i18n/index.js +0 -235
- package/src/kernel/messageHandler.js +0 -88
- package/src/kernel/pluginApi.js +0 -630
- package/src/kernel/pluginGuard.js +0 -78
- package/src/kernel/pluginLoader.js +0 -177
- package/src/kernel/pluginState.js +0 -99
- package/src/kernel/scheduler.js +0 -48
- package/src/kernel/sendGuard.js +0 -166
- package/src/locales/es.json +0 -62
- package/src/logger/logger.js +0 -32
- package/src/main.js +0 -136
- package/src/utils/getChatId.js +0 -3
- package/src/utils/get_id.js +0 -177
- package/src/utils/pluginI18n.js +0 -129
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|

|
|
9
|
-

|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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,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
|
+
}
|