@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.
- 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 +58 -7
- package/dist/drivers/baileys/api/index.js +172 -24
- 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 +74 -17
- 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
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
import { createBaileysAdapter } from "#drivers/baileys/adapter.js";
|
|
6
|
+
test("Baileys sends messages with the chat's disappearing-message timer", async () => {
|
|
7
|
+
const store = createStore();
|
|
8
|
+
const jid = "chat@s.whatsapp.net";
|
|
9
|
+
store.setChatEphemeralExpiration(jid, 86400);
|
|
10
|
+
store.messages.set(jid, new Map([["quoted", {
|
|
11
|
+
key: { id: "quoted", remoteJid: jid, fromMe: false },
|
|
12
|
+
message: { conversation: "original" },
|
|
13
|
+
}]]));
|
|
14
|
+
const calls = [];
|
|
15
|
+
const sock = {
|
|
16
|
+
ev: new EventEmitter(),
|
|
17
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
18
|
+
sendMessage: async (...args) => {
|
|
19
|
+
calls.push(args);
|
|
20
|
+
return { key: { id: `sent-${calls.length}`, remoteJid: args[0] } };
|
|
21
|
+
},
|
|
22
|
+
groupMetadata: async () => ({ subject: "Group", participants: [], ephemeralDuration: 604800 }),
|
|
23
|
+
};
|
|
24
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
25
|
+
const quoted = { id: "quoted", remoteJid: jid, fromMe: false };
|
|
26
|
+
await contract.sendText(jid, "text", { quoted });
|
|
27
|
+
await contract.sendImage(jid, Buffer.from("image"));
|
|
28
|
+
await contract.sendVideo(jid, Buffer.from("video"));
|
|
29
|
+
await contract.sendAudio(jid, Buffer.from("audio"));
|
|
30
|
+
await contract.sendSticker(jid, Buffer.from("sticker"));
|
|
31
|
+
await contract.sendDocument(jid, Buffer.from("document"), "file.txt", "text/plain");
|
|
32
|
+
await contract.sendPoll(jid, { name: "Poll", values: ["one"] });
|
|
33
|
+
for (const call of calls) {
|
|
34
|
+
assert.equal(call[2]?.ephemeralExpiration, 86400);
|
|
35
|
+
}
|
|
36
|
+
assert.ok((calls[0]?.[2]).quoted, "quoted and ephemeral options are merged");
|
|
37
|
+
await contract.react(jid, quoted, "👍");
|
|
38
|
+
assert.equal(calls.at(-1)?.length, 2, "reactions do not receive ephemeral send options");
|
|
39
|
+
await contract.groupMetadata("group@g.us");
|
|
40
|
+
await contract.sendText("group@g.us", "group text");
|
|
41
|
+
assert.equal((calls.at(-1)?.[2]).ephemeralExpiration, 604800);
|
|
42
|
+
});
|
|
43
|
+
test("Baileys adapter getBusinessProfile handles success and failure", async () => {
|
|
44
|
+
const store = createStore();
|
|
45
|
+
const jid = "12345678@s.whatsapp.net";
|
|
46
|
+
const sock = {
|
|
47
|
+
ev: new EventEmitter(),
|
|
48
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
49
|
+
getBusinessProfile: async () => ({ name: "Test Corp", id: "12345678" }),
|
|
50
|
+
};
|
|
51
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
52
|
+
const result = await contract.getBusinessProfile(jid);
|
|
53
|
+
assert.deepStrictEqual(result, { name: "Test Corp", id: "12345678" });
|
|
54
|
+
const sockFail = {
|
|
55
|
+
ev: new EventEmitter(),
|
|
56
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
57
|
+
getBusinessProfile: async () => { throw new Error("API error"); },
|
|
58
|
+
};
|
|
59
|
+
const { contract: contractFail } = createBaileysAdapter({ sock: sockFail, store });
|
|
60
|
+
const resultFail = await contractFail.getBusinessProfile(jid);
|
|
61
|
+
assert.strictEqual(resultFail, null);
|
|
62
|
+
});
|
|
63
|
+
test("Baileys adapter profilePictureUrl retrieves URL and handles error", async () => {
|
|
64
|
+
const store = createStore();
|
|
65
|
+
const jid = "12345678@s.whatsapp.net";
|
|
66
|
+
const sock = {
|
|
67
|
+
ev: new EventEmitter(),
|
|
68
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
69
|
+
profilePictureUrl: async () => "https://example.com/profile.jpg",
|
|
70
|
+
};
|
|
71
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
72
|
+
const result = await contract.profilePictureUrl(jid);
|
|
73
|
+
assert.strictEqual(result, "https://example.com/profile.jpg");
|
|
74
|
+
const sockFail = {
|
|
75
|
+
ev: new EventEmitter(),
|
|
76
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
77
|
+
profilePictureUrl: async () => { throw new Error("API error"); },
|
|
78
|
+
};
|
|
79
|
+
const { contract: contractFail } = createBaileysAdapter({ sock: sockFail, store });
|
|
80
|
+
const resultFail = await contractFail.profilePictureUrl(jid);
|
|
81
|
+
assert.strictEqual(resultFail, null);
|
|
82
|
+
});
|
|
83
|
+
test("Baileys adapter fetchStatus retrieves status and handles error", async () => {
|
|
84
|
+
const store = createStore();
|
|
85
|
+
const jid = "12345678@s.whatsapp.net";
|
|
86
|
+
const sock = {
|
|
87
|
+
ev: new EventEmitter(),
|
|
88
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
89
|
+
fetchStatus: async () => [{ id: jid, status: { status: "available" } }],
|
|
90
|
+
};
|
|
91
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
92
|
+
const result = await contract.fetchStatus(jid);
|
|
93
|
+
assert.strictEqual(result, "available");
|
|
94
|
+
const sockNoArray = {
|
|
95
|
+
ev: new EventEmitter(),
|
|
96
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
97
|
+
fetchStatus: async () => ({ status: "away" }),
|
|
98
|
+
};
|
|
99
|
+
const { contract: contractNoArray } = createBaileysAdapter({ sock: sockNoArray, store });
|
|
100
|
+
const resultNoArray = await contractNoArray.fetchStatus(jid);
|
|
101
|
+
assert.strictEqual(resultNoArray, "away");
|
|
102
|
+
const sockFail = {
|
|
103
|
+
ev: new EventEmitter(),
|
|
104
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
105
|
+
fetchStatus: async () => { throw new Error("API error"); },
|
|
106
|
+
};
|
|
107
|
+
const { contract: contractFail } = createBaileysAdapter({ sock: sockFail, store });
|
|
108
|
+
const resultFail = await contractFail.fetchStatus(jid);
|
|
109
|
+
assert.strictEqual(resultFail, null);
|
|
110
|
+
});
|
|
111
|
+
test("Baileys adapter updateBlockStatus calls the underlying method", async () => {
|
|
112
|
+
const store = createStore();
|
|
113
|
+
const jid = "12345678@s.whatsapp.net";
|
|
114
|
+
const sock = {
|
|
115
|
+
ev: new EventEmitter(),
|
|
116
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
117
|
+
updateBlockStatus: async () => { },
|
|
118
|
+
};
|
|
119
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
120
|
+
await contract.updateBlockStatus(jid, "block");
|
|
121
|
+
assert.ok(true, "updateBlockStatus did not throw");
|
|
122
|
+
await contract.updateBlockStatus(jid, "unblock");
|
|
123
|
+
assert.ok(true, "updateBlockStatus remove did not throw");
|
|
124
|
+
});
|
|
125
|
+
test("Baileys adapter addOrEditContact calls the underlying method", async () => {
|
|
126
|
+
const store = createStore();
|
|
127
|
+
const jid = "12345678@s.whatsapp.net";
|
|
128
|
+
const info = { fullName: "Test User", firstName: "Test", saveOnPrimaryAddressbook: true };
|
|
129
|
+
const sock = {
|
|
130
|
+
ev: new EventEmitter(),
|
|
131
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
132
|
+
addOrEditContact: async () => { },
|
|
133
|
+
};
|
|
134
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
135
|
+
await contract.addOrEditContact(jid, info);
|
|
136
|
+
assert.ok(true, "addOrEditContact did not throw");
|
|
137
|
+
await contract.addOrEditContact(jid, { fullName: "Another User" });
|
|
138
|
+
assert.ok(true, "addOrEditContact second call did not throw");
|
|
139
|
+
});
|
|
140
|
+
test("Baileys adapter removeContact calls the underlying method", async () => {
|
|
141
|
+
const store = createStore();
|
|
142
|
+
const jid = "12345678@s.whatsapp.net";
|
|
143
|
+
const sock = {
|
|
144
|
+
ev: new EventEmitter(),
|
|
145
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
146
|
+
removeContact: async () => { },
|
|
147
|
+
};
|
|
148
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
149
|
+
await contract.removeContact(jid);
|
|
150
|
+
assert.ok(true, "removeContact did not throw");
|
|
151
|
+
});
|
|
152
|
+
test("Baileys adapter groupParticipantsUpdate calls the underlying method", async () => {
|
|
153
|
+
const store = createStore();
|
|
154
|
+
const jid = "group@g.us";
|
|
155
|
+
const users = ["user1@s.whatsapp.net", "user2@s.whatsapp.net"];
|
|
156
|
+
const action = "add";
|
|
157
|
+
const sock = {
|
|
158
|
+
ev: new EventEmitter(),
|
|
159
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
160
|
+
groupParticipantsUpdate: async () => [{ status: "success" }],
|
|
161
|
+
};
|
|
162
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
163
|
+
const result = await contract.groupParticipantsUpdate(jid, users, action);
|
|
164
|
+
assert.deepStrictEqual(result, [{ status: "success" }]);
|
|
165
|
+
await contract.groupParticipantsUpdate(jid, ["user3@s.whatsapp.net"], "remove");
|
|
166
|
+
assert.ok(true, "groupParticipantsUpdate remove did not throw");
|
|
167
|
+
});
|
|
168
|
+
test("Baileys adapter groupUpdateSubject and groupUpdateDescription call underlying methods", async () => {
|
|
169
|
+
const store = createStore();
|
|
170
|
+
const jid = "group@g.us";
|
|
171
|
+
const sock = {
|
|
172
|
+
ev: new EventEmitter(),
|
|
173
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
174
|
+
groupUpdateSubject: async () => { },
|
|
175
|
+
groupUpdateDescription: async () => { },
|
|
176
|
+
};
|
|
177
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
178
|
+
await contract.groupUpdateSubject(jid, "New Subject");
|
|
179
|
+
assert.ok(true, "groupUpdateSubject did not throw");
|
|
180
|
+
await contract.groupUpdateDescription(jid, "New Description");
|
|
181
|
+
assert.ok(true, "groupUpdateDescription did not throw");
|
|
182
|
+
});
|
|
183
|
+
test("Baileys adapter groupInviteCode and groupRevokeInvite call underlying methods", async () => {
|
|
184
|
+
const store = createStore();
|
|
185
|
+
const jid = "group@g.us";
|
|
186
|
+
const sock = {
|
|
187
|
+
ev: new EventEmitter(),
|
|
188
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
189
|
+
groupInviteCode: async () => "https://chat.whatsapp.com/AAAA",
|
|
190
|
+
groupRevokeInvite: async () => { },
|
|
191
|
+
};
|
|
192
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
193
|
+
const code = await contract.groupInviteCode(jid);
|
|
194
|
+
assert.strictEqual(code, "https://chat.whatsapp.com/AAAA");
|
|
195
|
+
await contract.groupRevokeInvite(jid);
|
|
196
|
+
assert.ok(true, "groupRevokeInvite did not throw");
|
|
197
|
+
});
|
|
198
|
+
test("Baileys adapter updateProfilePicture, updateProfileName, updateProfileStatus call underlying methods", async () => {
|
|
199
|
+
const store = createStore();
|
|
200
|
+
const buffer = Buffer.from("fake-image-data");
|
|
201
|
+
const sock = {
|
|
202
|
+
ev: new EventEmitter(),
|
|
203
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
204
|
+
updateProfilePicture: async () => { },
|
|
205
|
+
updateProfileName: async () => { },
|
|
206
|
+
updateProfileStatus: async () => { },
|
|
207
|
+
};
|
|
208
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
209
|
+
await contract.updateProfilePicture("12345678@s.whatsapp.net", buffer);
|
|
210
|
+
assert.ok(true, "updateProfilePicture did not throw");
|
|
211
|
+
await contract.updateProfileName("New Name");
|
|
212
|
+
assert.ok(true, "updateProfileName did not throw");
|
|
213
|
+
await contract.updateProfileStatus("Available");
|
|
214
|
+
assert.ok(true, "updateProfileStatus did not throw");
|
|
215
|
+
});
|
|
216
|
+
test("Baileys adapter me returns user info", async () => {
|
|
217
|
+
const store = createStore();
|
|
218
|
+
const sock = {
|
|
219
|
+
ev: new EventEmitter(),
|
|
220
|
+
user: { id: "bot@s.whatsapp.net", lid: "12345678@lid" },
|
|
221
|
+
};
|
|
222
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
223
|
+
const me = await contract.me();
|
|
224
|
+
assert.strictEqual(me.id, "bot@s.whatsapp.net");
|
|
225
|
+
assert.strictEqual(me.lid, "12345678@lid");
|
|
226
|
+
});
|
|
227
|
+
test("Baileys adapter getHistory returns messages from store", async () => {
|
|
228
|
+
const store = createStore();
|
|
229
|
+
const jid = "12345678@s.whatsapp.net";
|
|
230
|
+
store.messages.set(jid, new Map([
|
|
231
|
+
["msg1", { key: { id: "msg1", remoteJid: jid, fromMe: false }, message: { conversation: "hello" } }],
|
|
232
|
+
["msg2", { key: { id: "msg2", remoteJid: jid, fromMe: true }, message: { conversation: "world" } }],
|
|
233
|
+
]));
|
|
234
|
+
const sock = {
|
|
235
|
+
ev: new EventEmitter(),
|
|
236
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
237
|
+
};
|
|
238
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
239
|
+
const history = await contract.getHistory?.(jid, { limit: 2 });
|
|
240
|
+
assert.strictEqual(history?.length, 2);
|
|
241
|
+
assert.ok(history?.some((m) => m.body === "hello"));
|
|
242
|
+
assert.ok(history?.some((m) => m.body === "world"));
|
|
243
|
+
});
|
|
244
|
+
test("Baileys adapter downloadMedia returns null when no raw message", async () => {
|
|
245
|
+
const store = createStore();
|
|
246
|
+
const sock = {
|
|
247
|
+
ev: new EventEmitter(),
|
|
248
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
249
|
+
};
|
|
250
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
251
|
+
const result = await contract.downloadMedia({ chatId: "123", id: "msg1" }, {});
|
|
252
|
+
assert.strictEqual(result, null);
|
|
253
|
+
});
|
|
254
|
+
test("Baileys adapter decryptPollVote returns null when no vote data", async () => {
|
|
255
|
+
const store = createStore();
|
|
256
|
+
const sock = {
|
|
257
|
+
ev: new EventEmitter(),
|
|
258
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
259
|
+
};
|
|
260
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
261
|
+
const result = await contract.decryptPollVote?.({
|
|
262
|
+
voteKey: { remoteJid: "123", id: "msg1" },
|
|
263
|
+
pollKey: { remoteJid: "123", id: "msg1" },
|
|
264
|
+
pollEncKey: Buffer.alloc(0),
|
|
265
|
+
});
|
|
266
|
+
assert.strictEqual(result, null);
|
|
267
|
+
});
|
|
268
|
+
test("Baileys adapter aggregatePollVotes returns empty array when no poll data", async () => {
|
|
269
|
+
const store = createStore();
|
|
270
|
+
const sock = {
|
|
271
|
+
ev: new EventEmitter(),
|
|
272
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
273
|
+
};
|
|
274
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
275
|
+
const result = contract.aggregatePollVotes?.({
|
|
276
|
+
pollKey: { remoteJid: "123", id: "msg1" },
|
|
277
|
+
selfJid: undefined,
|
|
278
|
+
votes: [],
|
|
279
|
+
});
|
|
280
|
+
assert.deepStrictEqual(result, []);
|
|
281
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import test, { describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { normalizeJid, denormalizeJid, toWireJid } from "#drivers/jid.js";
|
|
4
|
+
describe("drivers/jid", () => {
|
|
5
|
+
describe("normalizeJid", () => {
|
|
6
|
+
test("handles empty string or nullish input", () => {
|
|
7
|
+
assert.equal(normalizeJid(""), "");
|
|
8
|
+
});
|
|
9
|
+
test("converts @s.whatsapp.net to @c.us", () => {
|
|
10
|
+
assert.equal(normalizeJid("5511999999999@s.whatsapp.net"), "5511999999999@c.us");
|
|
11
|
+
});
|
|
12
|
+
test("removes device suffix e.g. :12@", () => {
|
|
13
|
+
assert.equal(normalizeJid("5511999999999:12@s.whatsapp.net"), "5511999999999@c.us");
|
|
14
|
+
});
|
|
15
|
+
test("leaves group @g.us untouched", () => {
|
|
16
|
+
assert.equal(normalizeJid("12036301234567890@g.us"), "12036301234567890@g.us");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe("denormalizeJid", () => {
|
|
20
|
+
test("converts @c.us to @s.whatsapp.net", () => {
|
|
21
|
+
assert.equal(denormalizeJid("5511999999999@c.us"), "5511999999999@s.whatsapp.net");
|
|
22
|
+
});
|
|
23
|
+
test("leaves already wire or non-c.us JIDs untouched", () => {
|
|
24
|
+
assert.equal(denormalizeJid("12036301234567890@g.us"), "12036301234567890@g.us");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe("toWireJid", () => {
|
|
28
|
+
test("returns wire format for @s.whatsapp.net, @lid, @g.us untouched", () => {
|
|
29
|
+
assert.equal(toWireJid("5511999999999@s.whatsapp.net"), "5511999999999@s.whatsapp.net");
|
|
30
|
+
assert.equal(toWireJid("12345@lid"), "12345@lid");
|
|
31
|
+
assert.equal(toWireJid("12036301234567890@g.us"), "12036301234567890@g.us");
|
|
32
|
+
});
|
|
33
|
+
test("converts @c.us JID to @s.whatsapp.net", () => {
|
|
34
|
+
assert.equal(toWireJid("5511999999999@c.us"), "5511999999999@s.whatsapp.net");
|
|
35
|
+
});
|
|
36
|
+
test("converts plain phone number / digits to @s.whatsapp.net", () => {
|
|
37
|
+
assert.equal(toWireJid("+55 (11) 99999-9999"), "5511999999999@s.whatsapp.net");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
package/dist/drivers/types.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* src/drivers/types.ts
|
|
3
3
|
*
|
|
4
4
|
* Driver-neutral envelope types shared by every WhatsApp driver
|
|
5
|
-
* implementation (Baileys today
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* implementation (Baileys today). The full driver surface (send, react,
|
|
6
|
+
* presence, contacts, groups, profile, media) is declared as
|
|
7
|
+
* `WaContract` in `#kernel/waContract.js` — every driver implements it.
|
|
8
|
+
* This module holds only the message shapes that flow across the driver
|
|
9
|
+
* boundary.
|
|
10
10
|
*
|
|
11
11
|
* Plugins depend on these types through the `WaContract` re-exports,
|
|
12
12
|
* never on a specific driver's Baileys/grpc types.
|
package/dist/i18n/index.js
CHANGED
|
@@ -125,11 +125,24 @@ function interpolate(str, context = {}) {
|
|
|
125
125
|
}
|
|
126
126
|
export function t(key, context = {}) {
|
|
127
127
|
ensureLoaded();
|
|
128
|
+
return translate(currentTranslations, fallbackTranslations, key, context);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Translates a key for an explicit language. This is useful for rendered
|
|
132
|
+
* content that accepts a language override, such as the command menu.
|
|
133
|
+
*/
|
|
134
|
+
export function tFor(lang, key, context = {}) {
|
|
135
|
+
ensureLoaded();
|
|
136
|
+
const targetLang = lang?.trim().toLowerCase() || currentLang || DEFAULT_LANG;
|
|
137
|
+
const targetTranslations = loadLocale(targetLang) || fallbackTranslations;
|
|
138
|
+
return translate(targetTranslations, fallbackTranslations, key, context);
|
|
139
|
+
}
|
|
140
|
+
function translate(targetTranslations, englishTranslations, key, context) {
|
|
128
141
|
// Try current language first
|
|
129
|
-
let value = getNestedValue(
|
|
142
|
+
let value = getNestedValue(targetTranslations, key);
|
|
130
143
|
// Fallback to English if not found
|
|
131
144
|
if (value === undefined) {
|
|
132
|
-
value = getNestedValue(
|
|
145
|
+
value = getNestedValue(englishTranslations, key);
|
|
133
146
|
}
|
|
134
147
|
// If still not found, return the key
|
|
135
148
|
if (value === undefined) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* activeDriverSend.ts
|
|
3
|
+
*
|
|
4
|
+
* Driver-neutral send helper. Ensures rate-limiting and throttling
|
|
5
|
+
* are applied to all outbound text sends from plugins, then sends
|
|
6
|
+
* through whichever single driver is currently active in the
|
|
7
|
+
* DriverManager. There is no fallback between drivers — driver
|
|
8
|
+
* selection is mutually exclusive and decided once at boot (see
|
|
9
|
+
* main.ts / driverManager.ts).
|
|
10
|
+
*/
|
|
11
|
+
import { waitForSendSlot } from "./sendGuard.js";
|
|
12
|
+
import { getDriverManager } from "./driverManager.js";
|
|
13
|
+
/**
|
|
14
|
+
* Send text using the active driver, respecting the rate-limiting send guard.
|
|
15
|
+
*/
|
|
16
|
+
export async function sendActiveDriverText(jid, text, opts = {}) {
|
|
17
|
+
const dm = getDriverManager();
|
|
18
|
+
const driver = dm.active();
|
|
19
|
+
await waitForSendSlot(jid, { cooldown: true, jitter: true });
|
|
20
|
+
return await driver.sendText(jid, text, opts);
|
|
21
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import test, { describe, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { sendActiveDriverText } from "#kernel/activeDriverSend.js";
|
|
4
|
+
import { getDriverManager, _resetDriverManagerForTests } from "#kernel/driverManager.js";
|
|
5
|
+
function createMockDriver(name, ready = true, failsSend = false) {
|
|
6
|
+
const mockRef = (id) => ({ id, chatId: "123@c.us", timestamp: Date.now() });
|
|
7
|
+
return {
|
|
8
|
+
name,
|
|
9
|
+
isReady: () => ready,
|
|
10
|
+
sendText: async (jid, text) => {
|
|
11
|
+
if (failsSend)
|
|
12
|
+
throw new Error(`${name} sendText failed`);
|
|
13
|
+
return mockRef(`msg_${name}`);
|
|
14
|
+
},
|
|
15
|
+
connect: async () => { },
|
|
16
|
+
disconnect: async () => { },
|
|
17
|
+
me: () => ({ id: "123@c.us" }),
|
|
18
|
+
sendImage: async () => mockRef("image"),
|
|
19
|
+
sendVideo: async () => mockRef("image"),
|
|
20
|
+
sendAudio: async () => mockRef("image"),
|
|
21
|
+
sendDocument: async () => mockRef("image"),
|
|
22
|
+
sendSticker: async () => mockRef("image"),
|
|
23
|
+
sendLocation: async () => mockRef("loc"),
|
|
24
|
+
sendContact: async () => mockRef("contact"),
|
|
25
|
+
sendReaction: async () => { },
|
|
26
|
+
sendPoll: async () => mockRef("poll"),
|
|
27
|
+
react: async () => { },
|
|
28
|
+
deleteMessage: async () => { },
|
|
29
|
+
editMessage: async () => { },
|
|
30
|
+
sendPresenceUpdate: async () => { },
|
|
31
|
+
readMessages: async () => { },
|
|
32
|
+
onWhatsApp: async () => null,
|
|
33
|
+
getBusinessProfile: async () => null,
|
|
34
|
+
profilePictureUrl: async () => null,
|
|
35
|
+
fetchStatus: async () => null,
|
|
36
|
+
updateBlockStatus: async () => { },
|
|
37
|
+
addOrEditContact: async () => { },
|
|
38
|
+
removeContact: async () => { },
|
|
39
|
+
groupMetadata: async () => ({ subject: "Test Group", participants: [] }),
|
|
40
|
+
groupParticipantsUpdate: async () => [],
|
|
41
|
+
groupUpdateSubject: async () => { },
|
|
42
|
+
groupUpdateDescription: async () => { },
|
|
43
|
+
groupInviteCode: async () => "",
|
|
44
|
+
groupRevokeInvite: async () => "",
|
|
45
|
+
updateProfilePicture: async () => { },
|
|
46
|
+
updateProfileName: async () => { },
|
|
47
|
+
updateProfileStatus: async () => { },
|
|
48
|
+
downloadMedia: async () => null,
|
|
49
|
+
on: () => () => { },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
describe("kernel/activeDriverSend", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
_resetDriverManagerForTests();
|
|
55
|
+
});
|
|
56
|
+
test("delivers text via the active driver when healthy", async () => {
|
|
57
|
+
const dm = getDriverManager();
|
|
58
|
+
const driver = createMockDriver("baileys");
|
|
59
|
+
dm.register(driver, { isPrimary: true });
|
|
60
|
+
const ref = await sendActiveDriverText("5511999999999@c.us", "hello");
|
|
61
|
+
assert.equal(ref.id, "msg_baileys");
|
|
62
|
+
});
|
|
63
|
+
test("propagates error when the active driver fails to send", async () => {
|
|
64
|
+
const dm = getDriverManager();
|
|
65
|
+
const failingDriver = createMockDriver("baileys", true, true);
|
|
66
|
+
dm.register(failingDriver, { isPrimary: true });
|
|
67
|
+
await assert.rejects(async () => sendActiveDriverText("5511999999999@c.us", "failing send"), /baileys sendText failed/);
|
|
68
|
+
});
|
|
69
|
+
test("passes quoted and mentions options through to the driver", async () => {
|
|
70
|
+
const dm = getDriverManager();
|
|
71
|
+
let receivedOpts = null;
|
|
72
|
+
const driver = {
|
|
73
|
+
...createMockDriver("baileys"),
|
|
74
|
+
sendText: async (_jid, _text, opts) => {
|
|
75
|
+
receivedOpts = opts;
|
|
76
|
+
return { id: "msg1", chatId: _jid, timestamp: Date.now() };
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
dm.register(driver, { isPrimary: true });
|
|
80
|
+
const quotedRef = { id: "orig-msg", remoteJid: "123@s.whatsapp.net", fromMe: false };
|
|
81
|
+
const ref = await sendActiveDriverText("5511999999999@c.us", "reply", { quoted: quotedRef, mentions: ["@user1"] });
|
|
82
|
+
assert.equal(ref.id, "msg1");
|
|
83
|
+
assert.deepStrictEqual(receivedOpts, { quoted: quotedRef, mentions: ["@user1"] });
|
|
84
|
+
});
|
|
85
|
+
test("throws when no driver is registered", async () => {
|
|
86
|
+
const dm = getDriverManager();
|
|
87
|
+
await assert.rejects(async () => sendActiveDriverText("5511999999999@c.us", "no driver"), /no active driver/i);
|
|
88
|
+
});
|
|
89
|
+
});
|
package/dist/kernel/alerts.js
CHANGED
|
@@ -29,6 +29,7 @@ import { spawn } from "child_process";
|
|
|
29
29
|
import nodemailer from "nodemailer";
|
|
30
30
|
import { CONFIG_DIR, ADMIN_JID, SMTP_HOST, SMTP_PORT, SMTP_SEC, SMTP_USER, SMTP_PASS, SMTP_FROM, SMTP_TO, SMTP_INSECURE, } from "#config";
|
|
31
31
|
import { logger } from "#logger";
|
|
32
|
+
import { t } from "#i18n";
|
|
32
33
|
const ALERTS_LOG_FILE = path.join(CONFIG_DIR, "alerts.log");
|
|
33
34
|
let sockProvider = null;
|
|
34
35
|
/**
|
|
@@ -160,25 +161,18 @@ export function fireAlert(kind, details = {}) {
|
|
|
160
161
|
if (kind === "send_failed_no_fallback") {
|
|
161
162
|
event = {
|
|
162
163
|
level: "critical",
|
|
163
|
-
title: "
|
|
164
|
+
title: t("alerts.noFallbackTitle"),
|
|
164
165
|
message: `jid=${details.jid} primary=${details.primary}`,
|
|
165
166
|
};
|
|
166
167
|
}
|
|
167
168
|
else if (kind === "send_failed_both_drivers") {
|
|
168
169
|
event = {
|
|
169
170
|
level: "critical",
|
|
170
|
-
title: "
|
|
171
|
+
title: t("alerts.bothDriversFailedTitle"),
|
|
171
172
|
message: `jid=${details.jid} ${details.primary}->${details.secondary}` +
|
|
172
173
|
(details.error ? ` error=${String(details.error)}` : ""),
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
|
-
else if (kind === "whatsmeow_subprocess_halted") {
|
|
176
|
-
event = {
|
|
177
|
-
level: "critical",
|
|
178
|
-
title: "manybot: subprocesso whatsmeow halted",
|
|
179
|
-
message: `fallback indisponível: ${details.reason ?? "unknown"} — bot segue só com Baileys`,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
176
|
else {
|
|
183
177
|
event = {
|
|
184
178
|
level: "warning",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chatSession.ts
|
|
3
|
+
*
|
|
4
|
+
* Phase 7 of MANYBOT-6.md — exclusive chat session, kernel primitive.
|
|
5
|
+
*
|
|
6
|
+
* Prevents two plugins from running an interactive flow (a game, the
|
|
7
|
+
* figurinha timeout session, a music-download prompt, etc.) in the same
|
|
8
|
+
* chat at the same time. The kernel only owns the lock itself — WHO holds
|
|
9
|
+
* it and for how long. Everything about the session's own state (timeout,
|
|
10
|
+
* collected media, turn tracking, ...) stays entirely inside the owning
|
|
11
|
+
* plugin; `commands.yaml` only ever registers commands, never internal
|
|
12
|
+
* flow state.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately NOT persisted (no settingsDb / SQLite): a session lock only
|
|
15
|
+
* makes sense for the lifetime of the running process — restarting the
|
|
16
|
+
* bot should never leave a chat stuck "locked" by a plugin that no longer
|
|
17
|
+
* remembers it opened one.
|
|
18
|
+
*
|
|
19
|
+
* many-ai's passive continuation window (its own multi-turn follow-up
|
|
20
|
+
* mechanism) is a separate category by design and never touches this
|
|
21
|
+
* lock — see the Phase 7 note in MANYBOT-6.md. This module does not
|
|
22
|
+
* special-case many-ai; it simply never gets called by it.
|
|
23
|
+
*/
|
|
24
|
+
const sessions = new Map();
|
|
25
|
+
/**
|
|
26
|
+
* Attempts to open an exclusive session for `pluginName` in `chatId`.
|
|
27
|
+
* Returns `true` if the session is now held by `pluginName` — either it
|
|
28
|
+
* was free, or `pluginName` already held it (idempotent re-acquire, e.g.
|
|
29
|
+
* a plugin calling acquire() again on a later message of its own flow).
|
|
30
|
+
* Returns `false` if another plugin already holds the session.
|
|
31
|
+
*/
|
|
32
|
+
export function acquireSession(chatId, pluginName) {
|
|
33
|
+
const current = sessions.get(chatId);
|
|
34
|
+
if (current && current.pluginName !== pluginName) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
sessions.set(chatId, { pluginName, acquiredAt: current?.acquiredAt ?? Date.now() });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Releases the session in `chatId`, but only if it is currently held by
|
|
42
|
+
* `pluginName` — a plugin can never release a lock it doesn't own. No-op
|
|
43
|
+
* (returns `false`) if the chat has no session, or it's held by someone
|
|
44
|
+
* else.
|
|
45
|
+
*/
|
|
46
|
+
export function releaseSession(chatId, pluginName) {
|
|
47
|
+
const current = sessions.get(chatId);
|
|
48
|
+
if (!current || current.pluginName !== pluginName) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
sessions.delete(chatId);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/** Whether `chatId` currently has an open exclusive session (by anyone). */
|
|
55
|
+
export function isSessionLocked(chatId) {
|
|
56
|
+
return sessions.has(chatId);
|
|
57
|
+
}
|
|
58
|
+
/** Which plugin currently holds the session in `chatId`, if any. */
|
|
59
|
+
export function getSessionHolder(chatId) {
|
|
60
|
+
return sessions.get(chatId)?.pluginName ?? null;
|
|
61
|
+
}
|
|
62
|
+
/** Test-only: wipe all session state between tests. */
|
|
63
|
+
export function __resetSessionsForTests() {
|
|
64
|
+
sessions.clear();
|
|
65
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test, { describe, beforeEach } from "node:test";
|
|
3
|
+
import { acquireSession, releaseSession, isSessionLocked, getSessionHolder, __resetSessionsForTests, } from "#kernel/chatSession.js";
|
|
4
|
+
describe("kernel/chatSession — Phase 7 exclusive chat session", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
__resetSessionsForTests();
|
|
7
|
+
});
|
|
8
|
+
test("acquire on a free chat succeeds and locks it", () => {
|
|
9
|
+
assert.equal(acquireSession("chat1", "gamePlugin"), true);
|
|
10
|
+
assert.equal(isSessionLocked("chat1"), true);
|
|
11
|
+
assert.equal(getSessionHolder("chat1"), "gamePlugin");
|
|
12
|
+
});
|
|
13
|
+
test("a different plugin cannot acquire an already-held session", () => {
|
|
14
|
+
assert.equal(acquireSession("chat1", "gamePlugin"), true);
|
|
15
|
+
assert.equal(acquireSession("chat1", "figurinhaPlugin"), false);
|
|
16
|
+
assert.equal(getSessionHolder("chat1"), "gamePlugin", "holder unchanged");
|
|
17
|
+
});
|
|
18
|
+
test("the same plugin re-acquiring its own session is idempotent", () => {
|
|
19
|
+
assert.equal(acquireSession("chat1", "gamePlugin"), true);
|
|
20
|
+
assert.equal(acquireSession("chat1", "gamePlugin"), true);
|
|
21
|
+
assert.equal(getSessionHolder("chat1"), "gamePlugin");
|
|
22
|
+
});
|
|
23
|
+
test("release only works for the plugin that holds the session", () => {
|
|
24
|
+
acquireSession("chat1", "gamePlugin");
|
|
25
|
+
assert.equal(releaseSession("chat1", "figurinhaPlugin"), false, "wrong plugin cannot release");
|
|
26
|
+
assert.equal(isSessionLocked("chat1"), true, "still locked");
|
|
27
|
+
assert.equal(releaseSession("chat1", "gamePlugin"), true);
|
|
28
|
+
assert.equal(isSessionLocked("chat1"), false);
|
|
29
|
+
assert.equal(getSessionHolder("chat1"), null);
|
|
30
|
+
});
|
|
31
|
+
test("releasing a chat with no session is a no-op", () => {
|
|
32
|
+
assert.equal(releaseSession("neverLocked", "anyPlugin"), false);
|
|
33
|
+
});
|
|
34
|
+
test("sessions are independent per chat", () => {
|
|
35
|
+
assert.equal(acquireSession("chatA", "gamePlugin"), true);
|
|
36
|
+
assert.equal(acquireSession("chatB", "figurinhaPlugin"), true);
|
|
37
|
+
assert.equal(getSessionHolder("chatA"), "gamePlugin");
|
|
38
|
+
assert.equal(getSessionHolder("chatB"), "figurinhaPlugin");
|
|
39
|
+
});
|
|
40
|
+
test("a freed session can be acquired by a different plugin afterward", () => {
|
|
41
|
+
acquireSession("chat1", "gamePlugin");
|
|
42
|
+
releaseSession("chat1", "gamePlugin");
|
|
43
|
+
assert.equal(acquireSession("chat1", "figurinhaPlugin"), true);
|
|
44
|
+
assert.equal(getSessionHolder("chat1"), "figurinhaPlugin");
|
|
45
|
+
});
|
|
46
|
+
});
|