@manybot/manybot 5.7.0 → 5.9.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 +28 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +91 -6
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +133 -15
- package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
- package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
- package/dist/drivers/baileys/api/index.js +384 -62
- package/dist/drivers/baileys/index.js +92 -36
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +344 -4
- package/dist/drivers/baileys/messageHandler.test.js +445 -0
- package/dist/drivers/baileysAdapter.test.js +378 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +74 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +20 -24
- 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/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- 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 +170 -0
- package/dist/kernel/commandDeprecation.test.js +114 -0
- package/dist/kernel/commandMenu.js +357 -0
- package/dist/kernel/commandMenu.test.js +363 -0
- package/dist/kernel/commandPermissions.js +171 -0
- package/dist/kernel/commandPermissions.test.js +227 -0
- package/dist/kernel/commandRegistry.js +583 -0
- package/dist/kernel/commandRegistry.test.js +158 -0
- package/dist/kernel/commandsConfig.js +949 -0
- package/dist/kernel/commandsConfig.test.js +482 -0
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/contactAutoSave.test.js +87 -0
- package/dist/kernel/coreCommands.js +62 -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 +600 -0
- package/dist/kernel/pluginGuard.js +18 -13
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +169 -11
- package/dist/kernel/pluginLoader.test.js +190 -0
- package/dist/kernel/runCommand.js +284 -0
- package/dist/kernel/runCommand.test.js +497 -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 +19 -5
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +192 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +77 -13
- package/dist/locales/es.json +77 -13
- package/dist/locales/pt.json +77 -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 +184 -0
- package/dist/plugins/__manybot_integration__/index.test.js +218 -0
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +76 -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
|
@@ -0,0 +1,378 @@
|
|
|
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("toBotMessage resolves LID/PN by suffix, not by field position (addressingMode 'lid')", async () => {
|
|
44
|
+
// Modern default WhatsApp addressing: `participant` is ALREADY the LID,
|
|
45
|
+
// `participantAlt` carries the PN companion — the reverse of the legacy
|
|
46
|
+
// "pn" mode. See https://baileys.wiki/concepts/jids.
|
|
47
|
+
const store = createStore();
|
|
48
|
+
const sock = { ev: new EventEmitter(), user: { id: "bot@s.whatsapp.net" } };
|
|
49
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
50
|
+
const received = [];
|
|
51
|
+
contract.on("messages.upsert", (payload) => received.push(payload));
|
|
52
|
+
sock.ev.emit("messages.upsert", {
|
|
53
|
+
type: "notify",
|
|
54
|
+
messages: [{
|
|
55
|
+
key: {
|
|
56
|
+
remoteJid: "120363999999999999@g.us",
|
|
57
|
+
fromMe: false,
|
|
58
|
+
id: "MSG1",
|
|
59
|
+
participant: "98765@lid", // already LID
|
|
60
|
+
participantAlt: "5511999999999@s.whatsapp.net", // PN companion
|
|
61
|
+
addressingMode: "lid",
|
|
62
|
+
},
|
|
63
|
+
messageTimestamp: 1700000000,
|
|
64
|
+
pushName: "Alice",
|
|
65
|
+
message: { conversation: "oi" },
|
|
66
|
+
}],
|
|
67
|
+
});
|
|
68
|
+
const batch = received[0];
|
|
69
|
+
const msg = batch.messages[0];
|
|
70
|
+
assert.equal(msg.fromLid, "98765@lid", "fromLid must be the value that's actually @lid, regardless of which field it came from");
|
|
71
|
+
assert.equal(msg.fromPn, "5511999999999@s.whatsapp.net", "fromPn must be the value that's actually NOT @lid");
|
|
72
|
+
assert.equal(msg.participantAlt, "98765@lid", "participantAlt (consumed directly by getMsgSender) must also be suffix-verified");
|
|
73
|
+
});
|
|
74
|
+
test("toBotMessage resolves LID/PN by suffix, not by field position (legacy addressingMode 'pn')", async () => {
|
|
75
|
+
const store = createStore();
|
|
76
|
+
const sock = { ev: new EventEmitter(), user: { id: "bot@s.whatsapp.net" } };
|
|
77
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
78
|
+
const received = [];
|
|
79
|
+
contract.on("messages.upsert", (payload) => received.push(payload));
|
|
80
|
+
sock.ev.emit("messages.upsert", {
|
|
81
|
+
type: "notify",
|
|
82
|
+
messages: [{
|
|
83
|
+
key: {
|
|
84
|
+
remoteJid: "120363999999999999@g.us",
|
|
85
|
+
fromMe: false,
|
|
86
|
+
id: "MSG2",
|
|
87
|
+
participant: "5511999999999@s.whatsapp.net", // PN
|
|
88
|
+
participantAlt: "98765@lid", // LID companion
|
|
89
|
+
addressingMode: "pn",
|
|
90
|
+
},
|
|
91
|
+
messageTimestamp: 1700000000,
|
|
92
|
+
pushName: "Bob",
|
|
93
|
+
message: { conversation: "oi" },
|
|
94
|
+
}],
|
|
95
|
+
});
|
|
96
|
+
const batch = received[0];
|
|
97
|
+
const msg = batch.messages[0];
|
|
98
|
+
assert.equal(msg.fromLid, "98765@lid");
|
|
99
|
+
assert.equal(msg.fromPn, "5511999999999@s.whatsapp.net");
|
|
100
|
+
});
|
|
101
|
+
test("Baileys adapter getBusinessProfile handles success and failure", async () => {
|
|
102
|
+
const store = createStore();
|
|
103
|
+
const jid = "12345678@s.whatsapp.net";
|
|
104
|
+
const sock = {
|
|
105
|
+
ev: new EventEmitter(),
|
|
106
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
107
|
+
getBusinessProfile: async () => ({ name: "Test Corp", id: "12345678" }),
|
|
108
|
+
};
|
|
109
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
110
|
+
const result = await contract.getBusinessProfile(jid);
|
|
111
|
+
assert.deepStrictEqual(result, { name: "Test Corp", id: "12345678" });
|
|
112
|
+
const sockFail = {
|
|
113
|
+
ev: new EventEmitter(),
|
|
114
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
115
|
+
getBusinessProfile: async () => { throw new Error("API error"); },
|
|
116
|
+
};
|
|
117
|
+
const { contract: contractFail } = createBaileysAdapter({ sock: sockFail, store });
|
|
118
|
+
const resultFail = await contractFail.getBusinessProfile(jid);
|
|
119
|
+
assert.strictEqual(resultFail, null);
|
|
120
|
+
});
|
|
121
|
+
test("Baileys adapter profilePictureUrl retrieves URL and handles error", async () => {
|
|
122
|
+
const store = createStore();
|
|
123
|
+
const jid = "12345678@s.whatsapp.net";
|
|
124
|
+
const sock = {
|
|
125
|
+
ev: new EventEmitter(),
|
|
126
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
127
|
+
profilePictureUrl: async () => "https://example.com/profile.jpg",
|
|
128
|
+
};
|
|
129
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
130
|
+
const result = await contract.profilePictureUrl(jid);
|
|
131
|
+
assert.strictEqual(result, "https://example.com/profile.jpg");
|
|
132
|
+
const sockFail = {
|
|
133
|
+
ev: new EventEmitter(),
|
|
134
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
135
|
+
profilePictureUrl: async () => { throw new Error("API error"); },
|
|
136
|
+
};
|
|
137
|
+
const { contract: contractFail } = createBaileysAdapter({ sock: sockFail, store });
|
|
138
|
+
const resultFail = await contractFail.profilePictureUrl(jid);
|
|
139
|
+
assert.strictEqual(resultFail, null);
|
|
140
|
+
});
|
|
141
|
+
test("Baileys adapter fetchStatus retrieves status and handles error", async () => {
|
|
142
|
+
const store = createStore();
|
|
143
|
+
const jid = "12345678@s.whatsapp.net";
|
|
144
|
+
const sock = {
|
|
145
|
+
ev: new EventEmitter(),
|
|
146
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
147
|
+
fetchStatus: async () => [{ id: jid, status: { status: "available" } }],
|
|
148
|
+
};
|
|
149
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
150
|
+
const result = await contract.fetchStatus(jid);
|
|
151
|
+
assert.strictEqual(result, "available");
|
|
152
|
+
const sockNoArray = {
|
|
153
|
+
ev: new EventEmitter(),
|
|
154
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
155
|
+
fetchStatus: async () => ({ status: "away" }),
|
|
156
|
+
};
|
|
157
|
+
const { contract: contractNoArray } = createBaileysAdapter({ sock: sockNoArray, store });
|
|
158
|
+
const resultNoArray = await contractNoArray.fetchStatus(jid);
|
|
159
|
+
assert.strictEqual(resultNoArray, "away");
|
|
160
|
+
const sockFail = {
|
|
161
|
+
ev: new EventEmitter(),
|
|
162
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
163
|
+
fetchStatus: async () => { throw new Error("API error"); },
|
|
164
|
+
};
|
|
165
|
+
const { contract: contractFail } = createBaileysAdapter({ sock: sockFail, store });
|
|
166
|
+
const resultFail = await contractFail.fetchStatus(jid);
|
|
167
|
+
assert.strictEqual(resultFail, null);
|
|
168
|
+
});
|
|
169
|
+
test("Baileys adapter updateBlockStatus calls the underlying method", async () => {
|
|
170
|
+
const store = createStore();
|
|
171
|
+
const jid = "12345678@s.whatsapp.net";
|
|
172
|
+
const sock = {
|
|
173
|
+
ev: new EventEmitter(),
|
|
174
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
175
|
+
updateBlockStatus: async () => { },
|
|
176
|
+
};
|
|
177
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
178
|
+
await contract.updateBlockStatus(jid, "block");
|
|
179
|
+
assert.ok(true, "updateBlockStatus did not throw");
|
|
180
|
+
await contract.updateBlockStatus(jid, "unblock");
|
|
181
|
+
assert.ok(true, "updateBlockStatus remove did not throw");
|
|
182
|
+
});
|
|
183
|
+
test("Baileys adapter addOrEditContact calls the underlying method", async () => {
|
|
184
|
+
const store = createStore();
|
|
185
|
+
const jid = "12345678@s.whatsapp.net";
|
|
186
|
+
const info = { fullName: "Test User", firstName: "Test", saveOnPrimaryAddressbook: true };
|
|
187
|
+
const sock = {
|
|
188
|
+
ev: new EventEmitter(),
|
|
189
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
190
|
+
addOrEditContact: async () => { },
|
|
191
|
+
};
|
|
192
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
193
|
+
await contract.addOrEditContact(jid, info);
|
|
194
|
+
assert.ok(true, "addOrEditContact did not throw");
|
|
195
|
+
await contract.addOrEditContact(jid, { fullName: "Another User" });
|
|
196
|
+
assert.ok(true, "addOrEditContact second call did not throw");
|
|
197
|
+
});
|
|
198
|
+
test("Baileys adapter removeContact calls the underlying method", async () => {
|
|
199
|
+
const store = createStore();
|
|
200
|
+
const jid = "12345678@s.whatsapp.net";
|
|
201
|
+
const sock = {
|
|
202
|
+
ev: new EventEmitter(),
|
|
203
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
204
|
+
removeContact: async () => { },
|
|
205
|
+
};
|
|
206
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
207
|
+
await contract.removeContact(jid);
|
|
208
|
+
assert.ok(true, "removeContact did not throw");
|
|
209
|
+
});
|
|
210
|
+
test("Baileys adapter groupParticipantsUpdate calls the underlying method", async () => {
|
|
211
|
+
const store = createStore();
|
|
212
|
+
const jid = "group@g.us";
|
|
213
|
+
const users = ["user1@s.whatsapp.net", "user2@s.whatsapp.net"];
|
|
214
|
+
const action = "add";
|
|
215
|
+
const sock = {
|
|
216
|
+
ev: new EventEmitter(),
|
|
217
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
218
|
+
groupParticipantsUpdate: async () => [{ status: "success" }],
|
|
219
|
+
};
|
|
220
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
221
|
+
const result = await contract.groupParticipantsUpdate(jid, users, action);
|
|
222
|
+
assert.deepStrictEqual(result, [{ status: "success" }]);
|
|
223
|
+
await contract.groupParticipantsUpdate(jid, ["user3@s.whatsapp.net"], "remove");
|
|
224
|
+
assert.ok(true, "groupParticipantsUpdate remove did not throw");
|
|
225
|
+
});
|
|
226
|
+
test("Baileys adapter groupUpdateSubject and groupUpdateDescription call underlying methods", async () => {
|
|
227
|
+
const store = createStore();
|
|
228
|
+
const jid = "group@g.us";
|
|
229
|
+
const sock = {
|
|
230
|
+
ev: new EventEmitter(),
|
|
231
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
232
|
+
groupUpdateSubject: async () => { },
|
|
233
|
+
groupUpdateDescription: async () => { },
|
|
234
|
+
};
|
|
235
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
236
|
+
await contract.groupUpdateSubject(jid, "New Subject");
|
|
237
|
+
assert.ok(true, "groupUpdateSubject did not throw");
|
|
238
|
+
await contract.groupUpdateDescription(jid, "New Description");
|
|
239
|
+
assert.ok(true, "groupUpdateDescription did not throw");
|
|
240
|
+
});
|
|
241
|
+
test("Baileys adapter groupInviteCode and groupRevokeInvite call underlying methods", async () => {
|
|
242
|
+
const store = createStore();
|
|
243
|
+
const jid = "group@g.us";
|
|
244
|
+
const sock = {
|
|
245
|
+
ev: new EventEmitter(),
|
|
246
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
247
|
+
groupInviteCode: async () => "https://chat.whatsapp.com/AAAA",
|
|
248
|
+
groupRevokeInvite: async () => { },
|
|
249
|
+
};
|
|
250
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
251
|
+
const code = await contract.groupInviteCode(jid);
|
|
252
|
+
assert.strictEqual(code, "https://chat.whatsapp.com/AAAA");
|
|
253
|
+
await contract.groupRevokeInvite(jid);
|
|
254
|
+
assert.ok(true, "groupRevokeInvite did not throw");
|
|
255
|
+
});
|
|
256
|
+
test("Baileys adapter updateProfilePicture, updateProfileName, updateProfileStatus call underlying methods", async () => {
|
|
257
|
+
const store = createStore();
|
|
258
|
+
const buffer = Buffer.from("fake-image-data");
|
|
259
|
+
const sock = {
|
|
260
|
+
ev: new EventEmitter(),
|
|
261
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
262
|
+
updateProfilePicture: async () => { },
|
|
263
|
+
updateProfileName: async () => { },
|
|
264
|
+
updateProfileStatus: async () => { },
|
|
265
|
+
};
|
|
266
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
267
|
+
await contract.updateProfilePicture("12345678@s.whatsapp.net", buffer);
|
|
268
|
+
assert.ok(true, "updateProfilePicture did not throw");
|
|
269
|
+
await contract.updateProfileName("New Name");
|
|
270
|
+
assert.ok(true, "updateProfileName did not throw");
|
|
271
|
+
await contract.updateProfileStatus("Available");
|
|
272
|
+
assert.ok(true, "updateProfileStatus did not throw");
|
|
273
|
+
});
|
|
274
|
+
test("Baileys adapter me returns user info", async () => {
|
|
275
|
+
const store = createStore();
|
|
276
|
+
const sock = {
|
|
277
|
+
ev: new EventEmitter(),
|
|
278
|
+
user: { id: "bot@s.whatsapp.net", lid: "12345678@lid" },
|
|
279
|
+
};
|
|
280
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
281
|
+
const me = await contract.me();
|
|
282
|
+
assert.strictEqual(me.id, "bot@s.whatsapp.net");
|
|
283
|
+
assert.strictEqual(me.lid, "12345678@lid");
|
|
284
|
+
});
|
|
285
|
+
test("Baileys adapter getHistory returns messages from store", async () => {
|
|
286
|
+
const store = createStore();
|
|
287
|
+
const jid = "12345678@s.whatsapp.net";
|
|
288
|
+
store.messages.set(jid, new Map([
|
|
289
|
+
["msg1", { key: { id: "msg1", remoteJid: jid, fromMe: false }, message: { conversation: "hello" } }],
|
|
290
|
+
["msg2", { key: { id: "msg2", remoteJid: jid, fromMe: true }, message: { conversation: "world" } }],
|
|
291
|
+
]));
|
|
292
|
+
const sock = {
|
|
293
|
+
ev: new EventEmitter(),
|
|
294
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
295
|
+
};
|
|
296
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
297
|
+
const history = await contract.getHistory?.(jid, { limit: 2 });
|
|
298
|
+
assert.strictEqual(history?.length, 2);
|
|
299
|
+
assert.ok(history?.some((m) => m.body === "hello"));
|
|
300
|
+
assert.ok(history?.some((m) => m.body === "world"));
|
|
301
|
+
});
|
|
302
|
+
test("Baileys adapter downloadMedia returns null when no raw message", async () => {
|
|
303
|
+
const store = createStore();
|
|
304
|
+
const sock = {
|
|
305
|
+
ev: new EventEmitter(),
|
|
306
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
307
|
+
};
|
|
308
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
309
|
+
const result = await contract.downloadMedia({ chatId: "123", id: "msg1" }, {});
|
|
310
|
+
assert.strictEqual(result, null);
|
|
311
|
+
});
|
|
312
|
+
test("Baileys adapter decryptPollVote returns null when no vote data", async () => {
|
|
313
|
+
const store = createStore();
|
|
314
|
+
const sock = {
|
|
315
|
+
ev: new EventEmitter(),
|
|
316
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
317
|
+
};
|
|
318
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
319
|
+
const result = await contract.decryptPollVote?.({
|
|
320
|
+
voteKey: { remoteJid: "123", id: "msg1" },
|
|
321
|
+
pollKey: { remoteJid: "123", id: "msg1" },
|
|
322
|
+
pollEncKey: Buffer.alloc(0),
|
|
323
|
+
});
|
|
324
|
+
assert.strictEqual(result, null);
|
|
325
|
+
});
|
|
326
|
+
test("Baileys adapter aggregatePollVotes returns empty array when no poll data", async () => {
|
|
327
|
+
const store = createStore();
|
|
328
|
+
const sock = {
|
|
329
|
+
ev: new EventEmitter(),
|
|
330
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
331
|
+
};
|
|
332
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
333
|
+
const result = contract.aggregatePollVotes?.({
|
|
334
|
+
pollKey: { remoteJid: "123", id: "msg1" },
|
|
335
|
+
selfJid: undefined,
|
|
336
|
+
votes: [],
|
|
337
|
+
});
|
|
338
|
+
assert.deepStrictEqual(result, []);
|
|
339
|
+
});
|
|
340
|
+
test("Baileys adapter passes through the real group-participants.update shape (author/action, string[] participants) and feeds LID↔PN cache", async () => {
|
|
341
|
+
const store = createStore();
|
|
342
|
+
const sock = {
|
|
343
|
+
ev: new EventEmitter(),
|
|
344
|
+
user: { id: "bot@s.whatsapp.net" },
|
|
345
|
+
};
|
|
346
|
+
const { contract } = createBaileysAdapter({ sock, store });
|
|
347
|
+
const received = [];
|
|
348
|
+
contract.on("group-participants.update", (payload) => received.push(payload));
|
|
349
|
+
// Baileys v7 ships `participants` as GroupParticipant[] (Contact & { admin?… })
|
|
350
|
+
// — each entry carries `id` (LID form, the addressing mode the group uses),
|
|
351
|
+
// `lid?` (explicit LID alias), and `phoneNumber?` (PN form). The adapter
|
|
352
|
+
// projects to a flat JID list for the kernel, but also feeds the LID↔PN
|
|
353
|
+
// cache from the richer data while it has it. See BaileysEventMap.
|
|
354
|
+
sock.ev.emit("group-participants.update", {
|
|
355
|
+
id: "120363402117932687@g.us",
|
|
356
|
+
author: "99999@lid",
|
|
357
|
+
authorPn: "5516999999999@s.whatsapp.net",
|
|
358
|
+
participants: [
|
|
359
|
+
{ id: "69119495901215@lid", phoneNumber: "5516111222333@s.whatsapp.net" },
|
|
360
|
+
{ id: "69119495901216@lid", phoneNumber: "5516111222444@s.whatsapp.net" },
|
|
361
|
+
],
|
|
362
|
+
action: "add",
|
|
363
|
+
});
|
|
364
|
+
assert.deepStrictEqual(received, [{
|
|
365
|
+
id: "120363402117932687@g.us",
|
|
366
|
+
author: "99999@lid",
|
|
367
|
+
participants: [
|
|
368
|
+
"69119495901215@lid",
|
|
369
|
+
"69119495901216@lid",
|
|
370
|
+
],
|
|
371
|
+
action: "add",
|
|
372
|
+
}]);
|
|
373
|
+
// Passive LID↔PN cache filled from the richer payload — both directions
|
|
374
|
+
// (resolveJid returns the PN, resolvePn returns the LID).
|
|
375
|
+
assert.equal(store.resolveJid("69119495901215@lid"), "5516111222333@s.whatsapp.net");
|
|
376
|
+
assert.equal(store.resolvePn("5516111222444@s.whatsapp.net"), "69119495901216@lid");
|
|
377
|
+
assert.equal(store.resolveJid("99999@lid"), "5516999999999@s.whatsapp.net");
|
|
378
|
+
});
|
package/dist/drivers/jid.js
CHANGED
|
@@ -29,3 +29,29 @@ export function toWireJid(id) {
|
|
|
29
29
|
const digits = trimmed.replace(/\D/g, "");
|
|
30
30
|
return `${digits}@s.whatsapp.net`;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Split a Baileys "primary/alt" JID pair (e.g. `key.participant` +
|
|
34
|
+
* `key.participantAlt`, or `key.remoteJid` + `key.remoteJidAlt`) into its
|
|
35
|
+
* LID and PN forms.
|
|
36
|
+
*
|
|
37
|
+
* Baileys only labels the primary field as the PN and the alt field as the
|
|
38
|
+
* LID under the legacy `addressingMode: "pn"`. Under the modern default
|
|
39
|
+
* `addressingMode: "lid"` the roles are reversed — the primary field IS
|
|
40
|
+
* already the LID, and the alt field carries the PN instead (see
|
|
41
|
+
* https://baileys.wiki/concepts/jids: "Group participant fields are
|
|
42
|
+
* typically LIDs; participantAlt carries the matching PN, and vice versa").
|
|
43
|
+
*
|
|
44
|
+
* Branching on `addressingMode` itself isn't reliable either — it's been
|
|
45
|
+
* observed flip-flopping for the same conversation across rc builds (see
|
|
46
|
+
* https://github.com/WhiskeySockets/Baileys/issues/1827). The one thing
|
|
47
|
+
* that's actually trustworthy is the JID suffix itself: whichever of the
|
|
48
|
+
* two values ends in "@lid" IS the LID, regardless of which field it came
|
|
49
|
+
* from or what addressingMode claims.
|
|
50
|
+
*/
|
|
51
|
+
export function splitLidPn(primary, alt) {
|
|
52
|
+
const candidates = [primary, alt].filter((v) => !!v);
|
|
53
|
+
return {
|
|
54
|
+
lid: candidates.find(v => v.endsWith("@lid")),
|
|
55
|
+
pn: candidates.find(v => !v.endsWith("@lid")),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import test, { describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { normalizeJid, denormalizeJid, toWireJid, splitLidPn } 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
|
+
describe("splitLidPn", () => {
|
|
41
|
+
test("addressingMode 'pn' (legacy): primary is PN, alt is LID", () => {
|
|
42
|
+
const { lid, pn } = splitLidPn("5511999999999@s.whatsapp.net", "12345@lid");
|
|
43
|
+
assert.equal(lid, "12345@lid");
|
|
44
|
+
assert.equal(pn, "5511999999999@s.whatsapp.net");
|
|
45
|
+
});
|
|
46
|
+
test("addressingMode 'lid' (modern default): primary is LID, alt is PN — roles reversed", () => {
|
|
47
|
+
const { lid, pn } = splitLidPn("12345@lid", "5511999999999@s.whatsapp.net");
|
|
48
|
+
assert.equal(lid, "12345@lid");
|
|
49
|
+
assert.equal(pn, "5511999999999@s.whatsapp.net");
|
|
50
|
+
});
|
|
51
|
+
test("only primary present, and it's a LID", () => {
|
|
52
|
+
const { lid, pn } = splitLidPn("12345@lid", undefined);
|
|
53
|
+
assert.equal(lid, "12345@lid");
|
|
54
|
+
assert.equal(pn, undefined);
|
|
55
|
+
});
|
|
56
|
+
test("only primary present, and it's a PN", () => {
|
|
57
|
+
const { lid, pn } = splitLidPn("5511999999999@s.whatsapp.net", undefined);
|
|
58
|
+
assert.equal(lid, undefined);
|
|
59
|
+
assert.equal(pn, "5511999999999@s.whatsapp.net");
|
|
60
|
+
});
|
|
61
|
+
test("neither present", () => {
|
|
62
|
+
const { lid, pn } = splitLidPn(undefined, null);
|
|
63
|
+
assert.equal(lid, undefined);
|
|
64
|
+
assert.equal(pn, undefined);
|
|
65
|
+
});
|
|
66
|
+
test("group JID (@g.us) as primary with no alt is treated as pn-shaped, never as lid", () => {
|
|
67
|
+
// Not a real person's PN, but callers only ever consume `.lid` from
|
|
68
|
+
// this pairing for the group case — `.pn` is discarded there.
|
|
69
|
+
const { lid, pn } = splitLidPn("120363999999999999@g.us", undefined);
|
|
70
|
+
assert.equal(lid, undefined);
|
|
71
|
+
assert.equal(pn, "120363999999999999@g.us");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
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
|
@@ -42,27 +42,10 @@ function loadLocale(lang) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
function detectSystemLang() {
|
|
51
|
-
try {
|
|
52
|
-
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
53
|
-
if (locale)
|
|
54
|
-
return locale.split("-")[0].toLowerCase();
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
// Intl unavailable — fall through to env vars
|
|
58
|
-
}
|
|
59
|
-
const envLocale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE;
|
|
60
|
-
if (envLocale)
|
|
61
|
-
return envLocale.split(/[_.]/)[0].toLowerCase();
|
|
62
|
-
return DEFAULT_LANG;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Gets configured language or falls back to system locale, then English.
|
|
45
|
+
* Gets configured language or falls back to English. `CONFIG.LANGUAGE` is
|
|
46
|
+
* the single source of truth (set from manybot.toml, defaulted to "en") —
|
|
47
|
+
* this never guesses from the OS locale, so bot output language doesn't
|
|
48
|
+
* depend on the host machine/CI environment it happens to run on.
|
|
66
49
|
* @returns {string}
|
|
67
50
|
*/
|
|
68
51
|
function getConfiguredLang() {
|
|
@@ -75,7 +58,7 @@ function getConfiguredLang() {
|
|
|
75
58
|
// circular import while #config is still bootstrapping)
|
|
76
59
|
}
|
|
77
60
|
if (!lang) {
|
|
78
|
-
lang =
|
|
61
|
+
lang = DEFAULT_LANG;
|
|
79
62
|
}
|
|
80
63
|
const filePath = path.join(LOCALES_DIR, `${lang}.json`);
|
|
81
64
|
if (!fs.existsSync(filePath)) {
|
|
@@ -125,11 +108,24 @@ function interpolate(str, context = {}) {
|
|
|
125
108
|
}
|
|
126
109
|
export function t(key, context = {}) {
|
|
127
110
|
ensureLoaded();
|
|
111
|
+
return translate(currentTranslations, fallbackTranslations, key, context);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Translates a key for an explicit language. This is useful for rendered
|
|
115
|
+
* content that accepts a language override, such as the command menu.
|
|
116
|
+
*/
|
|
117
|
+
export function tFor(lang, key, context = {}) {
|
|
118
|
+
ensureLoaded();
|
|
119
|
+
const targetLang = lang?.trim().toLowerCase() || currentLang || DEFAULT_LANG;
|
|
120
|
+
const targetTranslations = loadLocale(targetLang) || fallbackTranslations;
|
|
121
|
+
return translate(targetTranslations, fallbackTranslations, key, context);
|
|
122
|
+
}
|
|
123
|
+
function translate(targetTranslations, englishTranslations, key, context) {
|
|
128
124
|
// Try current language first
|
|
129
|
-
let value = getNestedValue(
|
|
125
|
+
let value = getNestedValue(targetTranslations, key);
|
|
130
126
|
// Fallback to English if not found
|
|
131
127
|
if (value === undefined) {
|
|
132
|
-
value = getNestedValue(
|
|
128
|
+
value = getNestedValue(englishTranslations, key);
|
|
133
129
|
}
|
|
134
130
|
// If still not found, return the key
|
|
135
131
|
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
|
+
}
|