@manybot/manybot 5.8.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 +15 -7
- package/dist/client/store.js +35 -1
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +75 -8
- 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 +212 -38
- package/dist/drivers/baileys/index.js +30 -6
- package/dist/drivers/baileys/messageHandler.js +207 -21
- package/dist/drivers/baileys/messageHandler.test.js +256 -14
- package/dist/drivers/baileysAdapter.test.js +97 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +35 -1
- package/dist/i18n/index.js +5 -22
- package/dist/kernel/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- package/dist/kernel/commandAccess.test.js +2 -2
- package/dist/kernel/commandDeprecation.js +4 -2
- package/dist/kernel/commandDeprecation.test.js +8 -1
- package/dist/kernel/commandMenu.js +91 -2
- package/dist/kernel/commandMenu.test.js +131 -2
- package/dist/kernel/commandPermissions.js +69 -23
- package/dist/kernel/commandPermissions.test.js +77 -9
- package/dist/kernel/commandRegistry.js +167 -43
- package/dist/kernel/commandRegistry.test.js +4 -2
- package/dist/kernel/commandsConfig.js +470 -38
- package/dist/kernel/commandsConfig.test.js +249 -3
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/coreCommands.js +62 -0
- package/dist/kernel/pluginApi.test.js +20 -3
- package/dist/kernel/pluginGuard.js +5 -3
- package/dist/kernel/pluginLoader.js +73 -10
- package/dist/kernel/pluginLoader.test.js +111 -1
- package/dist/kernel/runCommand.js +57 -18
- package/dist/kernel/runCommand.test.js +269 -7
- package/dist/kernel/settingsDb.js +15 -2
- package/dist/kernel/testConfig.js +9 -0
- package/dist/locales/en.json +14 -1
- package/dist/locales/es.json +17 -4
- package/dist/locales/pt.json +18 -5
- package/dist/plugins/__manybot_integration__/index.js +33 -16
- package/dist/plugins/__manybot_integration__/index.test.js +42 -8
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +4 -3
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test, beforeEach } from "node:test";
|
|
3
|
+
import { createStore } from "#client/store.js";
|
|
4
|
+
import { buildChatFromMsg, buildApi, __resetGroupMetaCacheForTests } from "#drivers/baileys/api/index.js";
|
|
5
|
+
import { normalizeJid } from "#drivers/jid.js";
|
|
6
|
+
const RAW_SOCK = Symbol.for("manybot.baileys.rawSocket");
|
|
7
|
+
const GROUP_JID = "120363000000000000@g.us";
|
|
8
|
+
const ADMIN_JID = "5511999999999@s.whatsapp.net";
|
|
9
|
+
const BOT_JID = "5511900000000@s.whatsapp.net";
|
|
10
|
+
function fakeMsg(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
id: "m1",
|
|
13
|
+
chatId: GROUP_JID,
|
|
14
|
+
fromMe: false,
|
|
15
|
+
contentHash: "h",
|
|
16
|
+
timestamp: Date.now(),
|
|
17
|
+
type: "text",
|
|
18
|
+
body: "hi",
|
|
19
|
+
fromPn: ADMIN_JID,
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Fake contract with a real listener registry (unlike a no-op mock) so we
|
|
24
|
+
* can actually exercise bindGroupMetaInvalidation's subscription.
|
|
25
|
+
*
|
|
26
|
+
* Two DIFFERENT `groupMetadata` implementations are wired in deliberately:
|
|
27
|
+
* - `contract.groupMetadata` satisfies `WaContract`'s neutral interface
|
|
28
|
+
* method (isAdmin/isSuperAdmin booleans) — required for the type, but
|
|
29
|
+
* NOT the path `getGroupMetadataFresh`/`getGroupMetadataCached` call.
|
|
30
|
+
* It returns an obviously-different, always-empty result so the test
|
|
31
|
+
* fails loudly if code under test ever starts reading it by mistake.
|
|
32
|
+
* - `[RAW_SOCK].groupMetadata` returns the raw Baileys shape (`admin`
|
|
33
|
+
* string enum) — this is the ONE actually read by isAdmin() and friends
|
|
34
|
+
* (via `rawSocketOf(contract)`), and the one tests should configure.
|
|
35
|
+
*/
|
|
36
|
+
function fakeContract(rawGroupMetadata) {
|
|
37
|
+
const handlers = new Map();
|
|
38
|
+
const neverCalledNeutralGroupMetadata = async () => {
|
|
39
|
+
throw new Error("contract.groupMetadata() (the neutral interface method) was called — " +
|
|
40
|
+
"isAdmin()/isSenderAdmin()/isBotAdmin() are expected to read " +
|
|
41
|
+
"[RAW_SOCK].groupMetadata() instead, via rawSocketOf(contract).");
|
|
42
|
+
};
|
|
43
|
+
const contract = {
|
|
44
|
+
name: "baileys",
|
|
45
|
+
connect: async () => { },
|
|
46
|
+
disconnect: async () => { },
|
|
47
|
+
isReady: () => true,
|
|
48
|
+
on: (event, handler) => {
|
|
49
|
+
if (!handlers.has(event))
|
|
50
|
+
handlers.set(event, new Set());
|
|
51
|
+
handlers.get(event).add(handler);
|
|
52
|
+
return () => handlers.get(event)?.delete(handler);
|
|
53
|
+
},
|
|
54
|
+
sendText: async () => ({ id: "x", chatId: GROUP_JID, timestamp: Date.now() }),
|
|
55
|
+
sendImage: async () => ({ id: "x", chatId: GROUP_JID, timestamp: Date.now() }),
|
|
56
|
+
sendVideo: async () => ({ id: "x", chatId: GROUP_JID, timestamp: Date.now() }),
|
|
57
|
+
sendAudio: async () => ({ id: "x", chatId: GROUP_JID, timestamp: Date.now() }),
|
|
58
|
+
sendSticker: async () => ({ id: "x", chatId: GROUP_JID, timestamp: Date.now() }),
|
|
59
|
+
sendDocument: async () => ({ id: "x", chatId: GROUP_JID, timestamp: Date.now() }),
|
|
60
|
+
sendPoll: async () => ({ id: "x", chatId: GROUP_JID, timestamp: Date.now() }),
|
|
61
|
+
react: async () => { },
|
|
62
|
+
deleteMessage: async () => { },
|
|
63
|
+
editMessage: async () => { },
|
|
64
|
+
sendPresenceUpdate: async () => { },
|
|
65
|
+
readMessages: async () => { },
|
|
66
|
+
onWhatsApp: async () => [{ exists: true }],
|
|
67
|
+
getBusinessProfile: async () => null,
|
|
68
|
+
profilePictureUrl: async () => null,
|
|
69
|
+
fetchStatus: async () => null,
|
|
70
|
+
updateBlockStatus: async () => { },
|
|
71
|
+
addOrEditContact: async () => { },
|
|
72
|
+
removeContact: async () => { },
|
|
73
|
+
groupMetadata: neverCalledNeutralGroupMetadata,
|
|
74
|
+
groupParticipantsUpdate: async () => [],
|
|
75
|
+
groupUpdateSubject: async () => { },
|
|
76
|
+
groupUpdateDescription: async () => { },
|
|
77
|
+
groupInviteCode: async () => "",
|
|
78
|
+
groupRevokeInvite: async () => "",
|
|
79
|
+
updateProfilePicture: async () => { },
|
|
80
|
+
updateProfileName: async () => { },
|
|
81
|
+
updateProfileStatus: async () => { },
|
|
82
|
+
me: () => ({ id: BOT_JID }),
|
|
83
|
+
downloadMedia: async () => null,
|
|
84
|
+
getHistory: async () => [],
|
|
85
|
+
};
|
|
86
|
+
contract[RAW_SOCK] = { groupMetadata: rawGroupMetadata };
|
|
87
|
+
return {
|
|
88
|
+
contract,
|
|
89
|
+
emitParticipantsUpdate: (evt) => {
|
|
90
|
+
for (const h of handlers.get("group-participants.update") ?? [])
|
|
91
|
+
h(evt);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function buildCtx(contract, msg = fakeMsg()) {
|
|
96
|
+
const store = createStore();
|
|
97
|
+
const wachat = await buildChatFromMsg(msg, store, contract);
|
|
98
|
+
return buildApi({
|
|
99
|
+
msg,
|
|
100
|
+
chat: wachat,
|
|
101
|
+
contract,
|
|
102
|
+
store,
|
|
103
|
+
pluginRegistry: new Map(),
|
|
104
|
+
pluginName: "testPlugin",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
describe("drivers/baileys/api — group metadata cache invalidation", () => {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
__resetGroupMetaCacheForTests();
|
|
110
|
+
});
|
|
111
|
+
test("isAdmin() reflects a demote immediately, even without an invalidating event", async () => {
|
|
112
|
+
let admin = "admin";
|
|
113
|
+
const { contract } = fakeContract(async () => ({
|
|
114
|
+
subject: "Group",
|
|
115
|
+
participants: [{ id: ADMIN_JID, admin }],
|
|
116
|
+
}));
|
|
117
|
+
const ctx = await buildCtx(contract);
|
|
118
|
+
assert.equal(await ctx.chat.isAdmin(ADMIN_JID), true);
|
|
119
|
+
// isAdmin()/isSenderAdmin()/isBotAdmin() bypass the group-metadata TTL
|
|
120
|
+
// cache entirely (they gate the `admin:`/`botAdmin:` command
|
|
121
|
+
// permission), so a demote is reflected on the very next call —
|
|
122
|
+
// no reliance on `group-participants.update` actually firing.
|
|
123
|
+
admin = null;
|
|
124
|
+
assert.equal(await ctx.chat.isAdmin(ADMIN_JID), false);
|
|
125
|
+
});
|
|
126
|
+
test("isAdmin() still correct if a group-participants.update happens to fire too (regression only — the event is NOT required)", async () => {
|
|
127
|
+
// This is a pure regression/documentation test: since isAdmin() no
|
|
128
|
+
// longer reads the cache at all, this exercises the exact same
|
|
129
|
+
// getGroupMetadataFresh() path as the test above — an event firing
|
|
130
|
+
// in between changes nothing. It exists only to pin down that wiring
|
|
131
|
+
// bindGroupMetaInvalidation's listener doesn't somehow interfere with
|
|
132
|
+
// (e.g. double-fetch, throw on) a fresh-path call. It does NOT prove
|
|
133
|
+
// isAdmin() depends on the event — it doesn't, by design.
|
|
134
|
+
let admin = "admin";
|
|
135
|
+
const { contract, emitParticipantsUpdate } = fakeContract(async () => ({
|
|
136
|
+
subject: "Group",
|
|
137
|
+
participants: [{ id: ADMIN_JID, admin }],
|
|
138
|
+
}));
|
|
139
|
+
const ctx = await buildCtx(contract);
|
|
140
|
+
assert.equal(await ctx.chat.isAdmin(ADMIN_JID), true);
|
|
141
|
+
admin = null;
|
|
142
|
+
emitParticipantsUpdate({
|
|
143
|
+
id: GROUP_JID,
|
|
144
|
+
author: BOT_JID,
|
|
145
|
+
participants: [ADMIN_JID],
|
|
146
|
+
action: "demote",
|
|
147
|
+
});
|
|
148
|
+
assert.equal(await ctx.chat.isAdmin(ADMIN_JID), false);
|
|
149
|
+
});
|
|
150
|
+
test("isSenderAdmin() matches when the sender is only known by PN (no LID in the message)", async () => {
|
|
151
|
+
let admin = "admin";
|
|
152
|
+
const { contract } = fakeContract(async () => ({
|
|
153
|
+
subject: "Group",
|
|
154
|
+
participants: [{ id: ADMIN_JID, admin }],
|
|
155
|
+
}));
|
|
156
|
+
// Only fromPn is set — no fromLid/participantAlt/remoteJidAlt at all —
|
|
157
|
+
// and the participant list is keyed by the same PN form. This forces
|
|
158
|
+
// isSenderAdmin() through its PN path rather than its LID path.
|
|
159
|
+
const ctx = await buildCtx(contract, fakeMsg({ fromPn: ADMIN_JID, fromLid: undefined, participantAlt: undefined }));
|
|
160
|
+
assert.equal(await ctx.chat.isSenderAdmin(), true);
|
|
161
|
+
admin = null;
|
|
162
|
+
assert.equal(await ctx.chat.isSenderAdmin(), false);
|
|
163
|
+
});
|
|
164
|
+
test("isSenderAdmin() matches when the sender is only known by LID (no PN in the message)", async () => {
|
|
165
|
+
const SENDER_LID = "234567890123456@lid";
|
|
166
|
+
let admin = "admin";
|
|
167
|
+
const { contract } = fakeContract(async () => ({
|
|
168
|
+
subject: "Group",
|
|
169
|
+
participants: [{ id: SENDER_LID, admin }],
|
|
170
|
+
}));
|
|
171
|
+
// Only fromLid is set — no fromPn at all — and the participant list is
|
|
172
|
+
// keyed by that same LID form. This forces isSenderAdmin() through its
|
|
173
|
+
// LID path rather than its PN path.
|
|
174
|
+
const ctx = await buildCtx(contract, fakeMsg({ fromPn: undefined, fromLid: SENDER_LID, participantAlt: undefined }));
|
|
175
|
+
assert.equal(await ctx.chat.isSenderAdmin(), true);
|
|
176
|
+
admin = null;
|
|
177
|
+
assert.equal(await ctx.chat.isSenderAdmin(), false);
|
|
178
|
+
});
|
|
179
|
+
test("isSenderAdmin() is false for a sender absent from the participant list", async () => {
|
|
180
|
+
const { contract } = fakeContract(async () => ({
|
|
181
|
+
subject: "Group",
|
|
182
|
+
participants: [{ id: "5511888888888@s.whatsapp.net", admin: "admin" }],
|
|
183
|
+
}));
|
|
184
|
+
const ctx = await buildCtx(contract, fakeMsg({ fromPn: ADMIN_JID }));
|
|
185
|
+
assert.equal(await ctx.chat.isSenderAdmin(), false);
|
|
186
|
+
});
|
|
187
|
+
test("isBotAdmin() reflects the bot's own demote", async () => {
|
|
188
|
+
let admin = "admin";
|
|
189
|
+
const { contract } = fakeContract(async () => ({
|
|
190
|
+
subject: "Group",
|
|
191
|
+
participants: [{ id: BOT_JID, admin }],
|
|
192
|
+
}));
|
|
193
|
+
const ctx = await buildCtx(contract);
|
|
194
|
+
assert.equal(await ctx.chat.isBotAdmin(), true);
|
|
195
|
+
admin = null;
|
|
196
|
+
assert.equal(await ctx.chat.isBotAdmin(), false);
|
|
197
|
+
});
|
|
198
|
+
test("isBotAdmin() is false when the bot has no usable id (empty candidates guard)", async () => {
|
|
199
|
+
// Reuses fakeContract (same full shape exercised everywhere else in
|
|
200
|
+
// this file) instead of a hand-rolled partial contract, so a missing
|
|
201
|
+
// method surfaces as an actual bug in the guard/call path, not as an
|
|
202
|
+
// incomplete mock. Only `me` is overridden afterward.
|
|
203
|
+
const { contract } = fakeContract(async () => ({
|
|
204
|
+
subject: "Group",
|
|
205
|
+
participants: [{ id: BOT_JID, admin: "admin" }],
|
|
206
|
+
}));
|
|
207
|
+
// fakeContract's default me() only ever returns `{ id }` (no `lid`
|
|
208
|
+
// field). Confirmed by the assert below (not by this comment): with
|
|
209
|
+
// id overridden to "", botCandidates ends up entirely empty in this
|
|
210
|
+
// scenario, whatever botLid's real source in index.ts turns out to
|
|
211
|
+
// be — this only needs both entries empty, not any specific mechanism.
|
|
212
|
+
contract.me = () => ({ id: "" });
|
|
213
|
+
const ctx = await buildCtx(contract);
|
|
214
|
+
assert.equal(await ctx.chat.isBotAdmin(), false);
|
|
215
|
+
});
|
|
216
|
+
test("getParticipants() (informational, not a permission gate) may still serve cached data", async () => {
|
|
217
|
+
let admin = "admin";
|
|
218
|
+
const { contract } = fakeContract(async () => ({
|
|
219
|
+
subject: "Group",
|
|
220
|
+
participants: [{ id: ADMIN_JID, admin }],
|
|
221
|
+
}));
|
|
222
|
+
const ctx = await buildCtx(contract);
|
|
223
|
+
// The first call is what populates groupMetaCache — that's the whole
|
|
224
|
+
// point being tested here. If buildChatFromMsg or buildApi ever start
|
|
225
|
+
// pre-warming the cache themselves before this point, this test would
|
|
226
|
+
// need the cache seeded a different way; keep this call first.
|
|
227
|
+
const first = await ctx.chat.getParticipants();
|
|
228
|
+
assert.equal(first.find(p => p.id === normalizeJid(ADMIN_JID))?.isAdmin, true);
|
|
229
|
+
// No invalidating event fired — this documents the intentional TTL
|
|
230
|
+
// tradeoff for the informational (non-permission-gating) read path.
|
|
231
|
+
admin = null;
|
|
232
|
+
const second = await ctx.chat.getParticipants();
|
|
233
|
+
assert.equal(second.find(p => p.id === normalizeJid(ADMIN_JID))?.isAdmin, true);
|
|
234
|
+
});
|
|
235
|
+
});
|