@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.
Files changed (81) hide show
  1. package/README.md +28 -3
  2. package/dist/client/banner.js +10 -0
  3. package/dist/client/banner.test.js +31 -0
  4. package/dist/client/store.js +91 -6
  5. package/dist/client/store.test.js +170 -0
  6. package/dist/config.js +28 -44
  7. package/dist/config.test.js +26 -0
  8. package/dist/download/queue.js +13 -4
  9. package/dist/drivers/baileys/adapter.js +133 -15
  10. package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
  11. package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
  12. package/dist/drivers/baileys/api/index.js +384 -62
  13. package/dist/drivers/baileys/index.js +92 -36
  14. package/dist/drivers/baileys/loginPrompt.js +0 -2
  15. package/dist/drivers/baileys/messageHandler.js +344 -4
  16. package/dist/drivers/baileys/messageHandler.test.js +445 -0
  17. package/dist/drivers/baileysAdapter.test.js +378 -0
  18. package/dist/drivers/jid.js +26 -0
  19. package/dist/drivers/jid.test.js +74 -0
  20. package/dist/drivers/types.js +5 -5
  21. package/dist/i18n/index.js +20 -24
  22. package/dist/kernel/activeDriverSend.js +21 -0
  23. package/dist/kernel/activeDriverSend.test.js +89 -0
  24. package/dist/kernel/alerts.js +3 -9
  25. package/dist/kernel/chatOverrides.js +46 -0
  26. package/dist/kernel/chatOverrides.test.js +59 -0
  27. package/dist/kernel/chatSession.js +65 -0
  28. package/dist/kernel/chatSession.test.js +46 -0
  29. package/dist/kernel/commandAccess.js +66 -0
  30. package/dist/kernel/commandAccess.test.js +74 -0
  31. package/dist/kernel/commandDeprecation.js +170 -0
  32. package/dist/kernel/commandDeprecation.test.js +114 -0
  33. package/dist/kernel/commandMenu.js +357 -0
  34. package/dist/kernel/commandMenu.test.js +363 -0
  35. package/dist/kernel/commandPermissions.js +171 -0
  36. package/dist/kernel/commandPermissions.test.js +227 -0
  37. package/dist/kernel/commandRegistry.js +583 -0
  38. package/dist/kernel/commandRegistry.test.js +158 -0
  39. package/dist/kernel/commandsConfig.js +949 -0
  40. package/dist/kernel/commandsConfig.test.js +482 -0
  41. package/dist/kernel/contactAutoSave.js +6 -6
  42. package/dist/kernel/contactAutoSave.test.js +87 -0
  43. package/dist/kernel/coreCommands.js +62 -0
  44. package/dist/kernel/driverManager.js +10 -6
  45. package/dist/kernel/driverManager.test.js +90 -0
  46. package/dist/kernel/integrationMode.js +88 -0
  47. package/dist/kernel/integrationMode.test.js +95 -0
  48. package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
  49. package/dist/kernel/pluginApi.test.js +600 -0
  50. package/dist/kernel/pluginGuard.js +18 -13
  51. package/dist/kernel/pluginGuard.test.js +39 -0
  52. package/dist/kernel/pluginLoader.js +169 -11
  53. package/dist/kernel/pluginLoader.test.js +190 -0
  54. package/dist/kernel/runCommand.js +284 -0
  55. package/dist/kernel/runCommand.test.js +497 -0
  56. package/dist/kernel/sendFallbackGuard.js +19 -48
  57. package/dist/kernel/sendFallbackGuard.test.js +80 -0
  58. package/dist/kernel/sendGuard.js +38 -42
  59. package/dist/kernel/sendGuard.test.js +102 -0
  60. package/dist/kernel/settingsDb.js +19 -5
  61. package/dist/kernel/statusServer.js +9 -2
  62. package/dist/kernel/statusServer.test.js +70 -0
  63. package/dist/kernel/testConfig.js +192 -0
  64. package/dist/kernel/testConfig.test.js +181 -0
  65. package/dist/kernel/updateCheck.js +33 -10
  66. package/dist/locales/en.json +77 -13
  67. package/dist/locales/es.json +77 -13
  68. package/dist/locales/pt.json +77 -13
  69. package/dist/logger/logger.js +23 -3
  70. package/dist/logger/logger.test.js +45 -0
  71. package/dist/main.js +5 -76
  72. package/dist/plugins/__manybot_integration__/index.js +184 -0
  73. package/dist/plugins/__manybot_integration__/index.test.js +218 -0
  74. package/dist/utils/phoneNumber.js +83 -0
  75. package/dist/utils/phoneNumber.test.js +53 -0
  76. package/package.json +76 -18
  77. package/dist/drivers/whatsmeow/client.js +0 -252
  78. package/dist/drivers/whatsmeow/index.js +0 -79
  79. package/dist/drivers/whatsmeow/installer.js +0 -86
  80. package/dist/drivers/whatsmeow/supervisor.js +0 -328
  81. 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
+ });
@@ -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
+ });
@@ -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, whatsmeow in a later phase). The
6
- * full driver surface (send, react, presence, contacts, groups,
7
- * profile, media) is declared as `WaContract` in `#kernel/waContract.js`
8
- * every driver implements it. This module holds only the message
9
- * shapes that flow across the driver boundary.
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.
@@ -42,27 +42,10 @@ function loadLocale(lang) {
42
42
  }
43
43
  }
44
44
  /**
45
- * Detects the OS locale without depending on a single env var, since LANG
46
- * isn't reliably set on macOS GUI sessions or Windows. Used as a fallback
47
- * when CONFIG.LANGUAGE isn't available yet (e.g. circular import during
48
- * config bootstrap) or isn't set.
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 = detectSystemLang();
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(currentTranslations, key);
125
+ let value = getNestedValue(targetTranslations, key);
130
126
  // Fallback to English if not found
131
127
  if (value === undefined) {
132
- value = getNestedValue(fallbackTranslations, key);
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
+ }