@manybot/manybot 5.6.1 → 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.
- package/README.md +20 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +56 -5
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/drivers/baileys/adapter.js +207 -8
- package/dist/drivers/baileys/api/index.js +300 -130
- package/dist/drivers/baileys/index.js +62 -30
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +158 -4
- package/dist/drivers/baileys/messageHandler.test.js +203 -0
- package/dist/drivers/baileysAdapter.test.js +281 -0
- package/dist/drivers/jid.test.js +40 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +15 -2
- package/dist/kernel/activeDriverSend.js +21 -0
- package/dist/kernel/activeDriverSend.test.js +89 -0
- package/dist/kernel/alerts.js +3 -9
- package/dist/kernel/chatSession.js +65 -0
- package/dist/kernel/chatSession.test.js +46 -0
- package/dist/kernel/commandAccess.js +66 -0
- package/dist/kernel/commandAccess.test.js +74 -0
- package/dist/kernel/commandDeprecation.js +168 -0
- package/dist/kernel/commandDeprecation.test.js +107 -0
- package/dist/kernel/commandMenu.js +268 -0
- package/dist/kernel/commandMenu.test.js +234 -0
- package/dist/kernel/commandPermissions.js +125 -0
- package/dist/kernel/commandPermissions.test.js +159 -0
- package/dist/kernel/commandRegistry.js +459 -0
- package/dist/kernel/commandRegistry.test.js +156 -0
- package/dist/kernel/commandsConfig.js +517 -0
- package/dist/kernel/commandsConfig.test.js +236 -0
- package/dist/kernel/contactAutoSave.test.js +87 -0
- package/dist/kernel/driverManager.js +10 -6
- package/dist/kernel/driverManager.test.js +90 -0
- package/dist/kernel/integrationMode.js +88 -0
- package/dist/kernel/integrationMode.test.js +95 -0
- package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
- package/dist/kernel/pluginApi.test.js +583 -0
- package/dist/kernel/pluginGuard.js +15 -12
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +96 -1
- package/dist/kernel/pluginLoader.test.js +80 -0
- package/dist/kernel/runCommand.js +245 -0
- package/dist/kernel/runCommand.test.js +235 -0
- package/dist/kernel/sendFallbackGuard.js +19 -48
- package/dist/kernel/sendFallbackGuard.test.js +80 -0
- package/dist/kernel/sendGuard.js +38 -42
- package/dist/kernel/sendGuard.test.js +102 -0
- package/dist/kernel/settingsDb.js +4 -3
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +183 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +64 -13
- package/dist/locales/es.json +64 -13
- package/dist/locales/pt.json +64 -13
- package/dist/logger/logger.js +23 -3
- package/dist/logger/logger.test.js +45 -0
- package/dist/main.js +5 -76
- package/dist/plugins/__manybot_integration__/index.js +167 -0
- package/dist/plugins/__manybot_integration__/index.test.js +184 -0
- package/package.json +75 -18
- package/dist/drivers/whatsmeow/client.js +0 -252
- package/dist/drivers/whatsmeow/index.js +0 -79
- package/dist/drivers/whatsmeow/installer.js +0 -86
- package/dist/drivers/whatsmeow/supervisor.js +0 -328
- 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
|

|
|
@@ -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:
|
package/dist/client/banner.js
CHANGED
|
@@ -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
|
+
});
|
package/dist/client/store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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,
|
|
427
|
-
// rest of
|
|
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:
|
|
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
|
+
});
|