@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,600 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
6
|
+
import { createStore } from "#client/store.js";
|
|
7
|
+
import { buildApi, buildSetupApi, buildStorageApi, cleanupPluginEvents, } from "#kernel/pluginApi.js";
|
|
8
|
+
import { getDriverManager, _resetDriverManagerForTests } from "#kernel/driverManager.js";
|
|
9
|
+
import { __resetSessionsForTests } from "#kernel/chatSession.js";
|
|
10
|
+
// Setup temp config directory for tests
|
|
11
|
+
const testTmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "manybot-pluginapi-test-"));
|
|
12
|
+
process.env.MANYBOT_CONFIG_DIR = testTmpDir;
|
|
13
|
+
const RAW_SOCK_SYM = Symbol.for("manybot.baileys.rawSocket");
|
|
14
|
+
function createMockContract() {
|
|
15
|
+
const calls = {
|
|
16
|
+
sentTexts: [],
|
|
17
|
+
sentImages: [],
|
|
18
|
+
sentVideos: [],
|
|
19
|
+
sentAudios: [],
|
|
20
|
+
sentStickers: [],
|
|
21
|
+
sentDocuments: [],
|
|
22
|
+
sentPolls: [],
|
|
23
|
+
reactions: [],
|
|
24
|
+
edits: [],
|
|
25
|
+
deletes: [],
|
|
26
|
+
blockUpdates: [],
|
|
27
|
+
groupParticipantUpdates: [],
|
|
28
|
+
nameUpdates: [],
|
|
29
|
+
statusUpdates: [],
|
|
30
|
+
};
|
|
31
|
+
const listeners = new Map();
|
|
32
|
+
const rawMockSock = {
|
|
33
|
+
user: { id: "5516999999999:0@s.whatsapp.net", name: "ManyBot" },
|
|
34
|
+
groupMetadata: async (jid) => ({
|
|
35
|
+
id: jid,
|
|
36
|
+
subject: "Test Mock Group",
|
|
37
|
+
participants: [
|
|
38
|
+
{ id: "5516999999999@s.whatsapp.net", admin: "admin", phoneNumber: "5516999999999@s.whatsapp.net" },
|
|
39
|
+
{ id: "5516888888888@s.whatsapp.net", admin: "superadmin", phoneNumber: "5516888888888@s.whatsapp.net" },
|
|
40
|
+
{ id: "5516777777777@s.whatsapp.net", admin: null, phoneNumber: "5516777777777@s.whatsapp.net" },
|
|
41
|
+
],
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
let msgSeq = 0;
|
|
45
|
+
const sentHistory = [];
|
|
46
|
+
const contract = {
|
|
47
|
+
name: "baileys",
|
|
48
|
+
connect: async () => { },
|
|
49
|
+
disconnect: async () => { },
|
|
50
|
+
isReady: () => true,
|
|
51
|
+
resolveLid: async (lid) => (lid === "12345@lid" ? "5516777777777@s.whatsapp.net" : null),
|
|
52
|
+
on: (event, handler) => {
|
|
53
|
+
if (!listeners.has(event))
|
|
54
|
+
listeners.set(event, new Set());
|
|
55
|
+
const set = listeners.get(event);
|
|
56
|
+
set.add(handler);
|
|
57
|
+
return () => {
|
|
58
|
+
set.delete(handler);
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
sendText: async (jid, text, opts) => {
|
|
62
|
+
const id = `msg-${++msgSeq}`;
|
|
63
|
+
calls.sentTexts.push({ jid, text, opts });
|
|
64
|
+
const ref = { id, chatId: jid, timestamp: Date.now() };
|
|
65
|
+
sentHistory.push({
|
|
66
|
+
id,
|
|
67
|
+
chatId: jid,
|
|
68
|
+
fromMe: true,
|
|
69
|
+
type: "text",
|
|
70
|
+
body: text,
|
|
71
|
+
contentHash: "hash-" + id,
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
return ref;
|
|
75
|
+
},
|
|
76
|
+
sendImage: async (jid, buffer, opts) => {
|
|
77
|
+
const id = `msg-${++msgSeq}`;
|
|
78
|
+
calls.sentImages.push({ jid, buffer, opts });
|
|
79
|
+
const ref = { id, chatId: jid, timestamp: Date.now() };
|
|
80
|
+
sentHistory.push({
|
|
81
|
+
id,
|
|
82
|
+
chatId: jid,
|
|
83
|
+
fromMe: true,
|
|
84
|
+
type: "image",
|
|
85
|
+
contentHash: "hash-" + id,
|
|
86
|
+
timestamp: Date.now(),
|
|
87
|
+
});
|
|
88
|
+
return ref;
|
|
89
|
+
},
|
|
90
|
+
sendVideo: async (jid, buffer, opts) => {
|
|
91
|
+
const id = `msg-${++msgSeq}`;
|
|
92
|
+
calls.sentVideos.push({ jid, buffer, opts });
|
|
93
|
+
const ref = { id, chatId: jid, timestamp: Date.now() };
|
|
94
|
+
sentHistory.push({
|
|
95
|
+
id,
|
|
96
|
+
chatId: jid,
|
|
97
|
+
fromMe: true,
|
|
98
|
+
type: "video",
|
|
99
|
+
contentHash: "hash-" + id,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
return ref;
|
|
103
|
+
},
|
|
104
|
+
sendAudio: async (jid, buffer, opts) => {
|
|
105
|
+
const id = `msg-${++msgSeq}`;
|
|
106
|
+
calls.sentAudios.push({ jid, buffer, opts });
|
|
107
|
+
const ref = { id, chatId: jid, timestamp: Date.now() };
|
|
108
|
+
sentHistory.push({
|
|
109
|
+
id,
|
|
110
|
+
chatId: jid,
|
|
111
|
+
fromMe: true,
|
|
112
|
+
type: "audio",
|
|
113
|
+
contentHash: "hash-" + id,
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
});
|
|
116
|
+
return ref;
|
|
117
|
+
},
|
|
118
|
+
sendSticker: async (jid, buffer, opts) => {
|
|
119
|
+
const id = `msg-${++msgSeq}`;
|
|
120
|
+
calls.sentStickers.push({ jid, buffer, opts });
|
|
121
|
+
const ref = { id, chatId: jid, timestamp: Date.now() };
|
|
122
|
+
sentHistory.push({
|
|
123
|
+
id,
|
|
124
|
+
chatId: jid,
|
|
125
|
+
fromMe: true,
|
|
126
|
+
type: "sticker",
|
|
127
|
+
contentHash: "hash-" + id,
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
return ref;
|
|
131
|
+
},
|
|
132
|
+
sendDocument: async (jid, buffer, filename, mimetype, opts) => {
|
|
133
|
+
const id = `msg-${++msgSeq}`;
|
|
134
|
+
calls.sentDocuments.push({ jid, buffer, filename, mimetype, opts });
|
|
135
|
+
const ref = { id, chatId: jid, timestamp: Date.now() };
|
|
136
|
+
sentHistory.push({
|
|
137
|
+
id,
|
|
138
|
+
chatId: jid,
|
|
139
|
+
fromMe: true,
|
|
140
|
+
type: "document",
|
|
141
|
+
contentHash: "hash-" + id,
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
});
|
|
144
|
+
return ref;
|
|
145
|
+
},
|
|
146
|
+
sendPoll: async (jid, opts) => {
|
|
147
|
+
const id = `msg-${++msgSeq}`;
|
|
148
|
+
calls.sentPolls.push({ jid, opts });
|
|
149
|
+
const ref = { id, chatId: jid, timestamp: Date.now() };
|
|
150
|
+
sentHistory.push({
|
|
151
|
+
id,
|
|
152
|
+
chatId: jid,
|
|
153
|
+
fromMe: true,
|
|
154
|
+
type: "other",
|
|
155
|
+
contentHash: "hash-" + id,
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
return ref;
|
|
159
|
+
},
|
|
160
|
+
react: async (jid, target, emoji) => {
|
|
161
|
+
calls.reactions.push({ jid, target, emoji });
|
|
162
|
+
},
|
|
163
|
+
deleteMessage: async (jid, target, forEveryone) => {
|
|
164
|
+
calls.deletes.push({ jid, target, forEveryone });
|
|
165
|
+
},
|
|
166
|
+
editMessage: async (jid, target, text) => {
|
|
167
|
+
calls.edits.push({ jid, target, text });
|
|
168
|
+
},
|
|
169
|
+
sendPresenceUpdate: async () => { },
|
|
170
|
+
readMessages: async () => { },
|
|
171
|
+
onWhatsApp: async (jid) => (jid.includes("999") || jid.includes("888") || jid.includes("777") ? [{ exists: true }] : [{ exists: false }]),
|
|
172
|
+
getBusinessProfile: async (jid) => (jid.includes("business") ? { description: "Test Business" } : null),
|
|
173
|
+
profilePictureUrl: async (jid) => (jid.includes("with-pfp") ? "https://example.com/avatar.jpg" : null),
|
|
174
|
+
fetchStatus: async (jid) => (jid.includes("with-status") ? "Available for testing" : null),
|
|
175
|
+
updateBlockStatus: async (jid, action) => {
|
|
176
|
+
calls.blockUpdates.push({ jid, action });
|
|
177
|
+
},
|
|
178
|
+
addOrEditContact: async () => { },
|
|
179
|
+
removeContact: async () => { },
|
|
180
|
+
groupMetadata: async (jid) => ({
|
|
181
|
+
subject: "Test Mock Group",
|
|
182
|
+
participants: [
|
|
183
|
+
{ id: "5516999999999@s.whatsapp.net", isAdmin: true, isSuperAdmin: false },
|
|
184
|
+
{ id: "5516888888888@s.whatsapp.net", isAdmin: true, isSuperAdmin: true },
|
|
185
|
+
{ id: "5516777777777@s.whatsapp.net", isAdmin: false, isSuperAdmin: false },
|
|
186
|
+
],
|
|
187
|
+
}),
|
|
188
|
+
groupParticipantsUpdate: async (jid, users, action) => {
|
|
189
|
+
calls.groupParticipantUpdates.push({ jid, users, action });
|
|
190
|
+
return users.map((u) => ({ status: "200", jid: u }));
|
|
191
|
+
},
|
|
192
|
+
groupUpdateSubject: async () => { },
|
|
193
|
+
groupUpdateDescription: async () => { },
|
|
194
|
+
groupInviteCode: async () => "mock-invite-code-123",
|
|
195
|
+
groupRevokeInvite: async () => "mock-new-invite-code-456",
|
|
196
|
+
updateProfilePicture: async () => { },
|
|
197
|
+
updateProfileName: async (name) => {
|
|
198
|
+
calls.nameUpdates.push(name);
|
|
199
|
+
},
|
|
200
|
+
updateProfileStatus: async (status) => {
|
|
201
|
+
calls.statusUpdates.push(status);
|
|
202
|
+
},
|
|
203
|
+
me: () => ({ id: "5516999999999@s.whatsapp.net", lid: "99999@lid" }),
|
|
204
|
+
downloadMedia: async () => ({ mimetype: "image/jpeg", data: Buffer.from("fake-image-bytes") }),
|
|
205
|
+
getHistory: async (jid) => sentHistory.filter((m) => m.chatId === jid),
|
|
206
|
+
};
|
|
207
|
+
contract[RAW_SOCK_SYM] = rawMockSock;
|
|
208
|
+
return { contract, calls };
|
|
209
|
+
}
|
|
210
|
+
function makeBotMessage(overrides = {}) {
|
|
211
|
+
return {
|
|
212
|
+
id: "msg-test-100",
|
|
213
|
+
chatId: "120363000000000@g.us",
|
|
214
|
+
fromMe: false,
|
|
215
|
+
contentHash: "mock-content-hash-123",
|
|
216
|
+
timestamp: 1700000000,
|
|
217
|
+
type: "text",
|
|
218
|
+
body: "!ping test arg",
|
|
219
|
+
participantAlt: "5516777777777@s.whatsapp.net",
|
|
220
|
+
fromPn: "5516777777777@s.whatsapp.net",
|
|
221
|
+
fromLid: "12345@lid",
|
|
222
|
+
...overrides,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
describe("kernel/pluginApi — storage facet", () => {
|
|
226
|
+
test("buildStorageApi creates isolated dir and enforces path sandbox", () => {
|
|
227
|
+
const storage = buildStorageApi("test_plugin");
|
|
228
|
+
assert.ok(storage.dir.includes("test_plugin"));
|
|
229
|
+
const safePath = storage.resolve("subdir/data.json");
|
|
230
|
+
assert.ok(safePath.startsWith(storage.dir));
|
|
231
|
+
assert.throws(() => storage.resolve("../escape.txt"), /path traversal/);
|
|
232
|
+
assert.throws(() => storage.resolve("/absolute/path"), /absolute paths are not allowed/);
|
|
233
|
+
assert.throws(() => storage.resolve("folder\\windows"), /Windows-style paths are not allowed/);
|
|
234
|
+
assert.throws(() => storage.resolve(""), /non-empty string/);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe("kernel/pluginApi — buildSetupApi with Mock WaContract", () => {
|
|
238
|
+
let store;
|
|
239
|
+
let pluginRegistry;
|
|
240
|
+
let mockContract;
|
|
241
|
+
let calls;
|
|
242
|
+
beforeEach(() => {
|
|
243
|
+
_resetDriverManagerForTests();
|
|
244
|
+
store = createStore();
|
|
245
|
+
pluginRegistry = new Map();
|
|
246
|
+
const mock = createMockContract();
|
|
247
|
+
mockContract = mock.contract;
|
|
248
|
+
calls = mock.calls;
|
|
249
|
+
getDriverManager().register(mockContract, { isPrimary: true });
|
|
250
|
+
});
|
|
251
|
+
afterEach(() => {
|
|
252
|
+
cleanupPluginEvents("test_plugin", mockContract);
|
|
253
|
+
_resetDriverManagerForTests();
|
|
254
|
+
});
|
|
255
|
+
test("exposes setup surface and base facets", async () => {
|
|
256
|
+
const ctx = buildSetupApi(mockContract, store, pluginRegistry, "test_plugin");
|
|
257
|
+
// Base facets
|
|
258
|
+
assert.ok(ctx.log);
|
|
259
|
+
assert.equal(typeof ctx.log.info, "function");
|
|
260
|
+
assert.equal(typeof ctx.t, "function");
|
|
261
|
+
assert.ok(ctx.config);
|
|
262
|
+
assert.ok(ctx.i18n);
|
|
263
|
+
assert.ok(ctx.utils);
|
|
264
|
+
assert.ok(ctx.download);
|
|
265
|
+
assert.ok(ctx.scheduler);
|
|
266
|
+
assert.ok(ctx.plugins);
|
|
267
|
+
assert.ok(ctx.chats);
|
|
268
|
+
assert.ok(ctx.contacts);
|
|
269
|
+
assert.ok(ctx.storage);
|
|
270
|
+
assert.equal(ctx.botId, "99999@lid");
|
|
271
|
+
// Setup send only has .to()
|
|
272
|
+
assert.ok(ctx.send.to);
|
|
273
|
+
assert.equal(typeof ctx.send.to, "function");
|
|
274
|
+
// Admin requires explicit .to()
|
|
275
|
+
assert.ok(ctx.admin);
|
|
276
|
+
assert.equal(typeof ctx.admin.add, "function");
|
|
277
|
+
// Me API
|
|
278
|
+
assert.ok(ctx.me);
|
|
279
|
+
await ctx.me.setName("New Bot Name");
|
|
280
|
+
assert.deepEqual(calls.nameUpdates, ["New Bot Name"]);
|
|
281
|
+
await ctx.me.setAbout("New About Text");
|
|
282
|
+
assert.deepEqual(calls.statusUpdates, ["New About Text"]);
|
|
283
|
+
// Events API
|
|
284
|
+
let eventPayload = null;
|
|
285
|
+
const unsub = ctx.events.on("messages.upsert", (payload) => {
|
|
286
|
+
eventPayload = payload;
|
|
287
|
+
});
|
|
288
|
+
assert.equal(typeof unsub, "function");
|
|
289
|
+
});
|
|
290
|
+
test("setup send.to() sends messages via WaContract", async () => {
|
|
291
|
+
const ctx = buildSetupApi(mockContract, store, pluginRegistry, "test_plugin");
|
|
292
|
+
await ctx.send.to("5516777777777@s.whatsapp.net").text("Hello from setup");
|
|
293
|
+
assert.equal(calls.sentTexts.length, 1);
|
|
294
|
+
assert.equal(calls.sentTexts[0].jid, "5516777777777@s.whatsapp.net");
|
|
295
|
+
assert.equal(calls.sentTexts[0].text, "Hello from setup");
|
|
296
|
+
});
|
|
297
|
+
test("setup admin.add().to() executes group member addition", async () => {
|
|
298
|
+
const ctx = buildSetupApi(mockContract, store, pluginRegistry, "test_plugin");
|
|
299
|
+
await ctx.admin.add("5516777777777@s.whatsapp.net").to("120363000000000@g.us");
|
|
300
|
+
assert.equal(calls.groupParticipantUpdates.length, 1);
|
|
301
|
+
assert.equal(calls.groupParticipantUpdates[0].action, "add");
|
|
302
|
+
assert.equal(calls.groupParticipantUpdates[0].jid, "120363000000000@g.us");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
describe("kernel/pluginApi — buildApi (Runtime) with Mock WaContract", () => {
|
|
306
|
+
let store;
|
|
307
|
+
let pluginRegistry;
|
|
308
|
+
let mockContract;
|
|
309
|
+
let calls;
|
|
310
|
+
beforeEach(() => {
|
|
311
|
+
_resetDriverManagerForTests();
|
|
312
|
+
store = createStore();
|
|
313
|
+
pluginRegistry = new Map();
|
|
314
|
+
const mock = createMockContract();
|
|
315
|
+
mockContract = mock.contract;
|
|
316
|
+
calls = mock.calls;
|
|
317
|
+
getDriverManager().register(mockContract, { isPrimary: true });
|
|
318
|
+
});
|
|
319
|
+
afterEach(() => {
|
|
320
|
+
cleanupPluginEvents("test_plugin", mockContract);
|
|
321
|
+
_resetDriverManagerForTests();
|
|
322
|
+
__resetSessionsForTests();
|
|
323
|
+
});
|
|
324
|
+
test("buildApi provides full runtime context and resolves group admin checks", async () => {
|
|
325
|
+
const msg = makeBotMessage();
|
|
326
|
+
const chat = {
|
|
327
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
328
|
+
name: "Test Group",
|
|
329
|
+
isGroup: true,
|
|
330
|
+
};
|
|
331
|
+
const ctx = buildApi({
|
|
332
|
+
msg,
|
|
333
|
+
chat,
|
|
334
|
+
contract: mockContract,
|
|
335
|
+
store,
|
|
336
|
+
pluginRegistry,
|
|
337
|
+
pluginName: "test_plugin",
|
|
338
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
339
|
+
});
|
|
340
|
+
// Chat properties & helpers
|
|
341
|
+
assert.equal(ctx.chat.isGroup, true);
|
|
342
|
+
assert.equal(ctx.chat.name, "Test Group");
|
|
343
|
+
const participants = await ctx.chat.getParticipants();
|
|
344
|
+
assert.equal(participants.length, 3);
|
|
345
|
+
assert.equal(participants[0].isAdmin, true);
|
|
346
|
+
const isSenderAdmin = await ctx.chat.isSenderAdmin();
|
|
347
|
+
assert.equal(isSenderAdmin, false); // 5516777777777 is not admin in mock
|
|
348
|
+
const isBotAdmin = await ctx.chat.isBotAdmin();
|
|
349
|
+
assert.equal(isBotAdmin, true); // 5516999999999 is admin in mock
|
|
350
|
+
// Send methods
|
|
351
|
+
await ctx.send.text("Test response");
|
|
352
|
+
assert.equal(calls.sentTexts.length, 1);
|
|
353
|
+
assert.equal(calls.sentTexts[0].text, "Test response");
|
|
354
|
+
await ctx.send.image(Buffer.from("image-data"), "Caption test");
|
|
355
|
+
assert.equal(calls.sentImages.length, 1);
|
|
356
|
+
assert.equal(calls.sentImages[0].opts && calls.sentImages[0].opts.caption, "Caption test");
|
|
357
|
+
await ctx.send.audio(Buffer.from("audio-data"));
|
|
358
|
+
assert.equal(calls.sentAudios.length, 1);
|
|
359
|
+
await ctx.send.sticker(Buffer.from("sticker-data"));
|
|
360
|
+
assert.equal(calls.sentStickers.length, 1);
|
|
361
|
+
await ctx.send.file(Buffer.from("file-data"), "test.pdf");
|
|
362
|
+
assert.equal(calls.sentDocuments.length, 1);
|
|
363
|
+
// Message reply helper
|
|
364
|
+
await ctx.msg.reply.text("Replying to message");
|
|
365
|
+
assert.equal(calls.sentTexts.length, 2);
|
|
366
|
+
assert.equal(calls.sentTexts[1].text, "Replying to message");
|
|
367
|
+
// Admin methods in bound chat
|
|
368
|
+
await ctx.admin.promote("5516777777777@s.whatsapp.net");
|
|
369
|
+
assert.equal(calls.groupParticipantUpdates.length, 1);
|
|
370
|
+
assert.equal(calls.groupParticipantUpdates[0].action, "promote");
|
|
371
|
+
await ctx.admin.kick("5516777777777@s.whatsapp.net");
|
|
372
|
+
assert.equal(calls.groupParticipantUpdates.length, 2);
|
|
373
|
+
assert.equal(calls.groupParticipantUpdates[1].action, "remove");
|
|
374
|
+
await assert.rejects(() => ctx.admin.kick("5516999999999@s.whatsapp.net"));
|
|
375
|
+
assert.equal(calls.groupParticipantUpdates.length, 2);
|
|
376
|
+
const inviteLink = await ctx.admin.getInviteLink();
|
|
377
|
+
assert.match(inviteLink, /chat\.whatsapp\.com\/mock-invite-code-123/);
|
|
378
|
+
// Contacts helper
|
|
379
|
+
const contact = await ctx.contacts.get("5516999999999@s.whatsapp.net");
|
|
380
|
+
assert.ok(contact);
|
|
381
|
+
// normalizeContact now returns E.164 (with leading "+") per the
|
|
382
|
+
// new contract invariant; assert accordingly.
|
|
383
|
+
assert.equal(contact?.number, "+5516999999999");
|
|
384
|
+
assert.equal(contact?.isMe, true);
|
|
385
|
+
// Looking up the bot by its own LID (ctx.botId) must resolve the
|
|
386
|
+
// same identity — number populated via the proactively-learned
|
|
387
|
+
// LID↔PN mapping, not left null for lack of a resolveLid() hit.
|
|
388
|
+
const selfByLid = await ctx.contacts.get(ctx.botId);
|
|
389
|
+
assert.ok(selfByLid);
|
|
390
|
+
assert.equal(selfByLid?.id, "99999@lid");
|
|
391
|
+
assert.equal(selfByLid?.number, "+5516999999999");
|
|
392
|
+
assert.equal(selfByLid?.isMe, true);
|
|
393
|
+
await ctx.contacts.block("5516777777777@s.whatsapp.net");
|
|
394
|
+
assert.equal(calls.blockUpdates.length, 1);
|
|
395
|
+
assert.equal(calls.blockUpdates[0].action, "block");
|
|
396
|
+
// Platform escape hatch
|
|
397
|
+
assert.ok(ctx.wa);
|
|
398
|
+
assert.equal(ctx.wa?.contract, mockContract);
|
|
399
|
+
assert.equal(ctx.tg, null);
|
|
400
|
+
assert.equal(ctx.dc, null);
|
|
401
|
+
const mediaResult = await ctx.wa?.downloadMedia();
|
|
402
|
+
assert.ok(mediaResult?.data);
|
|
403
|
+
});
|
|
404
|
+
test("settings, poll, and unblock facets work correctly", async () => {
|
|
405
|
+
const msg = makeBotMessage();
|
|
406
|
+
const chat = {
|
|
407
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
408
|
+
name: "Test Group",
|
|
409
|
+
isGroup: true,
|
|
410
|
+
};
|
|
411
|
+
const ctx = buildApi({
|
|
412
|
+
msg,
|
|
413
|
+
chat,
|
|
414
|
+
contract: mockContract,
|
|
415
|
+
store,
|
|
416
|
+
pluginRegistry,
|
|
417
|
+
pluginName: "test_plugin",
|
|
418
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
419
|
+
});
|
|
420
|
+
// Settings API
|
|
421
|
+
assert.ok(ctx.settings);
|
|
422
|
+
ctx.settings.global.set("key1", "value1");
|
|
423
|
+
assert.equal(ctx.settings.global.get("key1"), "value1");
|
|
424
|
+
ctx.settings.forChat("chat123").set("chatKey", "chatValue");
|
|
425
|
+
assert.equal(ctx.settings.forChat("chat123").get("chatKey"), "chatValue");
|
|
426
|
+
// Unblock contact
|
|
427
|
+
await ctx.contacts.unblock("5516777777777@s.whatsapp.net");
|
|
428
|
+
assert.equal(calls.blockUpdates.length, 1);
|
|
429
|
+
assert.equal(calls.blockUpdates[0].action, "unblock");
|
|
430
|
+
// Send video
|
|
431
|
+
await ctx.send.video(Buffer.from("video-data"), "Video caption");
|
|
432
|
+
assert.equal(calls.sentVideos.length, 1);
|
|
433
|
+
// Poll API
|
|
434
|
+
assert.ok(ctx.poll);
|
|
435
|
+
assert.equal(typeof ctx.poll.create, "function");
|
|
436
|
+
// TargetableAction thenable (.then directly)
|
|
437
|
+
let directThenCalled = false;
|
|
438
|
+
await ctx.send.text("Direct thenable").then(() => {
|
|
439
|
+
directThenCalled = true;
|
|
440
|
+
});
|
|
441
|
+
assert.equal(directThenCalled, true);
|
|
442
|
+
});
|
|
443
|
+
test("ctx.session enforces one exclusive lock per chat across plugins (Phase 7)", async () => {
|
|
444
|
+
const msg = makeBotMessage();
|
|
445
|
+
const chat = {
|
|
446
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
447
|
+
name: "Test Group",
|
|
448
|
+
isGroup: true,
|
|
449
|
+
};
|
|
450
|
+
const gameCtx = buildApi({
|
|
451
|
+
msg, chat, contract: mockContract, store, pluginRegistry,
|
|
452
|
+
pluginName: "gamePlugin",
|
|
453
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
454
|
+
});
|
|
455
|
+
const figurinhaCtx = buildApi({
|
|
456
|
+
msg, chat, contract: mockContract, store, pluginRegistry,
|
|
457
|
+
pluginName: "figurinhaPlugin",
|
|
458
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
459
|
+
});
|
|
460
|
+
// Free chat: the first plugin to ask gets the lock.
|
|
461
|
+
assert.equal(gameCtx.session.isLocked(), false);
|
|
462
|
+
assert.equal(gameCtx.session.acquire(), true);
|
|
463
|
+
assert.equal(gameCtx.session.isMine(), true);
|
|
464
|
+
assert.equal(gameCtx.session.isLocked(), true);
|
|
465
|
+
// A second plugin in the SAME chat cannot also open a session.
|
|
466
|
+
assert.equal(figurinhaCtx.session.isLocked(), true);
|
|
467
|
+
assert.equal(figurinhaCtx.session.acquire(), false);
|
|
468
|
+
assert.equal(figurinhaCtx.session.isMine(), false);
|
|
469
|
+
// The holder re-acquiring its own session is a harmless no-op.
|
|
470
|
+
assert.equal(gameCtx.session.acquire(), true);
|
|
471
|
+
// The non-holder cannot release someone else's session.
|
|
472
|
+
figurinhaCtx.session.release();
|
|
473
|
+
assert.equal(gameCtx.session.isLocked(), true, "release from a non-holder must not affect the lock");
|
|
474
|
+
// Once the real holder releases it, another plugin can acquire it.
|
|
475
|
+
gameCtx.session.release();
|
|
476
|
+
assert.equal(gameCtx.session.isLocked(), false);
|
|
477
|
+
assert.equal(figurinhaCtx.session.acquire(), true);
|
|
478
|
+
assert.equal(figurinhaCtx.session.isMine(), true);
|
|
479
|
+
});
|
|
480
|
+
test("events.once and cleanup removes listeners", async () => {
|
|
481
|
+
let triggeredCount = 0;
|
|
482
|
+
const ctx = buildSetupApi(mockContract, store, pluginRegistry, "test_events_plugin");
|
|
483
|
+
// Test once
|
|
484
|
+
void ctx.events.once("messages.upsert").then(() => {
|
|
485
|
+
triggeredCount++;
|
|
486
|
+
});
|
|
487
|
+
// Clean up
|
|
488
|
+
cleanupPluginEvents("test_events_plugin", mockContract);
|
|
489
|
+
assert.equal(typeof ctx.events.cleanup, "function");
|
|
490
|
+
});
|
|
491
|
+
test("config, i18n, download, scheduler, plugins, chats, contacts pfp/about facets", async () => {
|
|
492
|
+
const msg = makeBotMessage();
|
|
493
|
+
const chat = {
|
|
494
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
495
|
+
name: "Test Group",
|
|
496
|
+
isGroup: true,
|
|
497
|
+
};
|
|
498
|
+
// Register a dependency plugin so plugins.get/require/exists have something real to resolve.
|
|
499
|
+
pluginRegistry.set("dep_plugin", {
|
|
500
|
+
name: "dep_plugin",
|
|
501
|
+
status: "active",
|
|
502
|
+
manifest: { name: "dep_plugin", version: "1.0.0" },
|
|
503
|
+
exports: { greet: () => "hi" },
|
|
504
|
+
});
|
|
505
|
+
store.hydrate({
|
|
506
|
+
chats: [{ id: "120363000000000@g.us", name: "Test Group", ephemeralExpiration: 0 }],
|
|
507
|
+
contacts: {},
|
|
508
|
+
lidMap: [],
|
|
509
|
+
});
|
|
510
|
+
const ctx = buildApi({
|
|
511
|
+
msg,
|
|
512
|
+
chat,
|
|
513
|
+
contract: mockContract,
|
|
514
|
+
store,
|
|
515
|
+
pluginRegistry,
|
|
516
|
+
pluginName: "test_plugin",
|
|
517
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
518
|
+
});
|
|
519
|
+
// config.get
|
|
520
|
+
assert.equal(ctx.config.get("__no_such_key__", "fallback"), "fallback");
|
|
521
|
+
// i18n.t — unknown key still returns a string, never throws
|
|
522
|
+
assert.equal(typeof ctx.i18n.t("__no_such_key__"), "string");
|
|
523
|
+
assert.equal(typeof ctx.t("__no_such_key__"), "string");
|
|
524
|
+
// download.enqueue runs the work function
|
|
525
|
+
let downloadRan = false;
|
|
526
|
+
await new Promise((resolve) => {
|
|
527
|
+
ctx.download.enqueue(async () => {
|
|
528
|
+
downloadRan = true;
|
|
529
|
+
resolve();
|
|
530
|
+
}, async () => resolve());
|
|
531
|
+
});
|
|
532
|
+
assert.equal(downloadRan, true);
|
|
533
|
+
// scheduler.schedule returns a handle with stop()
|
|
534
|
+
const handle = ctx.scheduler.schedule("0 9 * * 1", async () => { });
|
|
535
|
+
assert.equal(typeof handle.stop, "function");
|
|
536
|
+
handle.stop();
|
|
537
|
+
// plugins.get / require / exists
|
|
538
|
+
assert.ok(ctx.plugins.exists("dep_plugin"));
|
|
539
|
+
assert.equal(ctx.plugins.exists("missing_plugin"), false);
|
|
540
|
+
assert.equal(ctx.plugins.get("dep_plugin").greet(), "hi");
|
|
541
|
+
assert.equal(ctx.plugins.get("missing_plugin"), null);
|
|
542
|
+
assert.equal(ctx.plugins.require("dep_plugin").greet(), "hi");
|
|
543
|
+
assert.throws(() => ctx.plugins.require("missing_plugin"), /does not exist or is not active/);
|
|
544
|
+
// chats.all
|
|
545
|
+
const allChats = ctx.chats.all();
|
|
546
|
+
assert.equal(allChats.length, 1);
|
|
547
|
+
assert.equal(allChats[0].name, "Test Group");
|
|
548
|
+
assert.equal(allChats[0].isGroup, true);
|
|
549
|
+
// contacts pfp/about — no pfp/status configured for this jid
|
|
550
|
+
assert.equal(await ctx.contacts.getPfpUrl("5516777777777@s.whatsapp.net"), null);
|
|
551
|
+
assert.equal(await ctx.contacts.getPfpPath("5516777777777@s.whatsapp.net", "/tmp/x.jpg"), null);
|
|
552
|
+
assert.equal(await ctx.contacts.getAbout("5516777777777@s.whatsapp.net"), null);
|
|
553
|
+
// contacts pfp — jid the mock contract recognizes
|
|
554
|
+
assert.equal(await ctx.contacts.getPfpUrl("5516999999999-with-pfp@s.whatsapp.net"), "https://example.com/avatar.jpg");
|
|
555
|
+
// NOTE: WaContract.fetchStatus is typed Promise<string|null> (the
|
|
556
|
+
// Baileys adapter already unwraps the array/object USync shapes down
|
|
557
|
+
// to a plain string before returning). getAbout()'s array/object
|
|
558
|
+
// branches are therefore currently unreachable through the contract —
|
|
559
|
+
// any contract-conformant fetchStatus always lands on the `null`
|
|
560
|
+
// fallback here. Flagged in TEST_REVIEW.md rather than silently
|
|
561
|
+
// asserting a value that can't happen in practice.
|
|
562
|
+
assert.equal(await ctx.contacts.getAbout("5516999999999-with-status@s.whatsapp.net"), null);
|
|
563
|
+
});
|
|
564
|
+
test("admin demote/setSubject/setDescription/setProfilePic/revokeInvite, send.gif/poll, contacts.get resolves via contract.resolveLid", async () => {
|
|
565
|
+
const msg = makeBotMessage();
|
|
566
|
+
const chat = {
|
|
567
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
568
|
+
name: "Test Group",
|
|
569
|
+
isGroup: true,
|
|
570
|
+
};
|
|
571
|
+
const ctx = buildApi({
|
|
572
|
+
msg,
|
|
573
|
+
chat,
|
|
574
|
+
contract: mockContract,
|
|
575
|
+
store,
|
|
576
|
+
pluginRegistry,
|
|
577
|
+
pluginName: "test_plugin",
|
|
578
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
579
|
+
});
|
|
580
|
+
await ctx.admin.demote("5516777777777@s.whatsapp.net");
|
|
581
|
+
assert.equal(calls.groupParticipantUpdates.at(-1)?.action, "demote");
|
|
582
|
+
await ctx.admin.setSubject("New Subject");
|
|
583
|
+
await ctx.admin.setDescription("New Description");
|
|
584
|
+
await ctx.admin.setProfilePic(Buffer.from("pic-data"));
|
|
585
|
+
const invite = await ctx.admin.revokeInvite();
|
|
586
|
+
assert.match(String(invite), /mock-new-invite-code-456/);
|
|
587
|
+
await ctx.send.gif(Buffer.from("already-mp4-bytes"), "gif caption");
|
|
588
|
+
await ctx.send.poll("Favorite color?", ["Red", "Blue"]);
|
|
589
|
+
assert.equal(calls.sentPolls.length, 1);
|
|
590
|
+
// contacts.get() with a raw @lid routes through contract.resolveLid()
|
|
591
|
+
// (mock: "12345@lid" -> "5516777777777@s.whatsapp.net").
|
|
592
|
+
const contact = await ctx.contacts.get("12345@lid");
|
|
593
|
+
// normalizeContact now keeps the ID as the LID form ("12345@lid")
|
|
594
|
+
// when known, per the invariant that user IDs are LID-or-null; plugins
|
|
595
|
+
// should treat contact.id as the canonical JID for addressing and
|
|
596
|
+
// contact.number/contact.numberRaw as the dialable string.
|
|
597
|
+
assert.equal(contact?.id, "12345@lid");
|
|
598
|
+
assert.equal(contact?.number, "+5516777777777");
|
|
599
|
+
});
|
|
600
|
+
});
|
|
@@ -36,22 +36,22 @@ function withTimeout(promise, ms, pluginName) {
|
|
|
36
36
|
});
|
|
37
37
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
* @param {object} plugin — pluginRegistry entry
|
|
41
|
-
* @param {object} context — buildApi ctx
|
|
42
|
-
*
|
|
43
|
-
* plugin.guardOptions (optional, read from plugin's own export):
|
|
44
|
-
* @param {boolean} [plugin.guardOptions.timeout=true]
|
|
45
|
-
*/
|
|
46
|
-
export async function runPlugin(plugin, context) {
|
|
39
|
+
export async function runPlugin(plugin, context, handler, input, options) {
|
|
47
40
|
if (plugin.status !== "active")
|
|
48
|
-
return;
|
|
41
|
+
return undefined;
|
|
49
42
|
const useTimeout = plugin.guardOptions?.timeout !== false;
|
|
50
43
|
try {
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
if (handler) {
|
|
45
|
+
const run = handler(context, input);
|
|
46
|
+
return await (useTimeout ? withTimeout(run, PLUGIN_TIMEOUT_MS, plugin.name) : run);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (!plugin.run)
|
|
50
|
+
return undefined;
|
|
51
|
+
const run = plugin.run(context);
|
|
52
|
+
await (useTimeout ? withTimeout(run, PLUGIN_TIMEOUT_MS, plugin.name) : run);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
55
|
}
|
|
56
56
|
catch (e) {
|
|
57
57
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
@@ -68,6 +68,8 @@ export async function runPlugin(plugin, context) {
|
|
|
68
68
|
const frame = error.stack?.split("\n")[1]?.trim() ?? "(no stack)";
|
|
69
69
|
logger.error(` at : ${frame}`);
|
|
70
70
|
}
|
|
71
|
+
if (options?.rethrow)
|
|
72
|
+
throw error;
|
|
71
73
|
}
|
|
72
74
|
else {
|
|
73
75
|
pluginRegistry.set(plugin.name, plugin);
|
|
@@ -77,6 +79,8 @@ export async function runPlugin(plugin, context) {
|
|
|
77
79
|
const frame = error.stack?.split("\n")[1]?.trim() ?? "(no stack)";
|
|
78
80
|
logger.warn(` at : ${frame}`);
|
|
79
81
|
}
|
|
82
|
+
if (options?.rethrow)
|
|
83
|
+
throw error;
|
|
80
84
|
// Reload the plugin dynamically to avoid circular dependency
|
|
81
85
|
import("#kernel/pluginLoader.js").then(({ reloadPlugin }) => {
|
|
82
86
|
reloadPlugin(plugin.name).catch(err => {
|
|
@@ -84,5 +88,6 @@ export async function runPlugin(plugin, context) {
|
|
|
84
88
|
});
|
|
85
89
|
});
|
|
86
90
|
}
|
|
91
|
+
return undefined;
|
|
87
92
|
}
|
|
88
93
|
}
|