@manybot/manybot 5.7.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +56 -5
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/drivers/baileys/adapter.js +58 -7
- package/dist/drivers/baileys/api/index.js +172 -24
- package/dist/drivers/baileys/index.js +62 -30
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +158 -4
- package/dist/drivers/baileys/messageHandler.test.js +203 -0
- package/dist/drivers/baileysAdapter.test.js +281 -0
- package/dist/drivers/jid.test.js +40 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +15 -2
- package/dist/kernel/activeDriverSend.js +21 -0
- package/dist/kernel/activeDriverSend.test.js +89 -0
- package/dist/kernel/alerts.js +3 -9
- package/dist/kernel/chatSession.js +65 -0
- package/dist/kernel/chatSession.test.js +46 -0
- package/dist/kernel/commandAccess.js +66 -0
- package/dist/kernel/commandAccess.test.js +74 -0
- package/dist/kernel/commandDeprecation.js +168 -0
- package/dist/kernel/commandDeprecation.test.js +107 -0
- package/dist/kernel/commandMenu.js +268 -0
- package/dist/kernel/commandMenu.test.js +234 -0
- package/dist/kernel/commandPermissions.js +125 -0
- package/dist/kernel/commandPermissions.test.js +159 -0
- package/dist/kernel/commandRegistry.js +459 -0
- package/dist/kernel/commandRegistry.test.js +156 -0
- package/dist/kernel/commandsConfig.js +517 -0
- package/dist/kernel/commandsConfig.test.js +236 -0
- package/dist/kernel/contactAutoSave.test.js +87 -0
- package/dist/kernel/driverManager.js +10 -6
- package/dist/kernel/driverManager.test.js +90 -0
- package/dist/kernel/integrationMode.js +88 -0
- package/dist/kernel/integrationMode.test.js +95 -0
- package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
- package/dist/kernel/pluginApi.test.js +583 -0
- package/dist/kernel/pluginGuard.js +15 -12
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +96 -1
- package/dist/kernel/pluginLoader.test.js +80 -0
- package/dist/kernel/runCommand.js +245 -0
- package/dist/kernel/runCommand.test.js +235 -0
- package/dist/kernel/sendFallbackGuard.js +19 -48
- package/dist/kernel/sendFallbackGuard.test.js +80 -0
- package/dist/kernel/sendGuard.js +38 -42
- package/dist/kernel/sendGuard.test.js +102 -0
- package/dist/kernel/settingsDb.js +4 -3
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +183 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +64 -13
- package/dist/locales/es.json +64 -13
- package/dist/locales/pt.json +64 -13
- package/dist/logger/logger.js +23 -3
- package/dist/logger/logger.test.js +45 -0
- package/dist/main.js +5 -76
- package/dist/plugins/__manybot_integration__/index.js +167 -0
- package/dist/plugins/__manybot_integration__/index.test.js +184 -0
- package/package.json +74 -17
- package/dist/drivers/whatsmeow/client.js +0 -252
- package/dist/drivers/whatsmeow/index.js +0 -79
- package/dist/drivers/whatsmeow/installer.js +0 -86
- package/dist/drivers/whatsmeow/supervisor.js +0 -328
- package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, beforeEach, describe, test } from "node:test";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "manybot-integration-loader-"));
|
|
7
|
+
process.env.MANYBOT_CONFIG_DIR = configDir;
|
|
8
|
+
const { loadIntegrationPlugin, pluginRegistry, cleanupPlugins, } = await import("#kernel/pluginLoader.js");
|
|
9
|
+
const { INTEGRATION_PLUGIN_NAME } = await import("#kernel/integrationMode.js");
|
|
10
|
+
const ORIGINAL_OPT_IN = process.env.MANYBOT_RUN_WHATSAPP_TESTS;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
await cleanupPlugins();
|
|
13
|
+
pluginRegistry.clear();
|
|
14
|
+
// Force-clear and re-apply the opt-in between tests; each test sets
|
|
15
|
+
// it explicitly so we know what the contract looks like.
|
|
16
|
+
delete process.env.MANYBOT_RUN_WHATSAPP_TESTS;
|
|
17
|
+
});
|
|
18
|
+
after(async () => {
|
|
19
|
+
await cleanupPlugins();
|
|
20
|
+
pluginRegistry.clear();
|
|
21
|
+
if (ORIGINAL_OPT_IN === undefined)
|
|
22
|
+
delete process.env.MANYBOT_RUN_WHATSAPP_TESTS;
|
|
23
|
+
else
|
|
24
|
+
process.env.MANYBOT_RUN_WHATSAPP_TESTS = ORIGINAL_OPT_IN;
|
|
25
|
+
await fs.rm(configDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
describe("kernel/pluginLoader — loadIntegrationPlugin", () => {
|
|
28
|
+
test("refuses to load without the opt-in flag", async () => {
|
|
29
|
+
// opt-in explicitly NOT set
|
|
30
|
+
await assert.rejects(loadIntegrationPlugin(), /MANYBOT_RUN_WHATSAPP_TESTS=1/);
|
|
31
|
+
assert.equal(pluginRegistry.has(INTEGRATION_PLUGIN_NAME), false);
|
|
32
|
+
});
|
|
33
|
+
test("registers the integration plugin when opt-in is set", async () => {
|
|
34
|
+
process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
|
|
35
|
+
const entry = await loadIntegrationPlugin();
|
|
36
|
+
assert.equal(entry.name, INTEGRATION_PLUGIN_NAME);
|
|
37
|
+
assert.equal(entry.status, "active");
|
|
38
|
+
assert.equal(typeof entry.run, "function");
|
|
39
|
+
assert.equal(entry.commands, null, "integration plugin must not register user commands");
|
|
40
|
+
assert.ok(pluginRegistry.has(INTEGRATION_PLUGIN_NAME));
|
|
41
|
+
});
|
|
42
|
+
test("is idempotent — second call returns the same entry without re-importing", async () => {
|
|
43
|
+
process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
|
|
44
|
+
const first = await loadIntegrationPlugin();
|
|
45
|
+
const second = await loadIntegrationPlugin();
|
|
46
|
+
assert.equal(first, second);
|
|
47
|
+
// Single registration, no duplicate.
|
|
48
|
+
assert.equal(pluginRegistry.size, 1);
|
|
49
|
+
});
|
|
50
|
+
test("exports a public API object via plugin.exports", async () => {
|
|
51
|
+
process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
|
|
52
|
+
const entry = await loadIntegrationPlugin();
|
|
53
|
+
const api = entry.exports;
|
|
54
|
+
assert.ok(api, "integration plugin must expose a public API");
|
|
55
|
+
assert.equal(typeof api.isTestChat, "function");
|
|
56
|
+
assert.equal(typeof api.waitForMarker, "function");
|
|
57
|
+
assert.equal(typeof api.recentBodies, "function");
|
|
58
|
+
// testChat is populated in setup(), not on registration.
|
|
59
|
+
assert.equal(api.testChat, "");
|
|
60
|
+
});
|
|
61
|
+
test("loads the plugin from the source path shipped with the repo", async () => {
|
|
62
|
+
process.env.MANYBOT_RUN_WHATSAPP_TESTS = "1";
|
|
63
|
+
const entry = await loadIntegrationPlugin();
|
|
64
|
+
// The plugin must be the real one (default export present).
|
|
65
|
+
assert.equal(typeof entry.run, "function");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,583 @@
|
|
|
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, "5516999999999@s.whatsapp.net");
|
|
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
|
+
const inviteLink = await ctx.admin.getInviteLink();
|
|
375
|
+
assert.match(inviteLink, /chat\.whatsapp\.com\/mock-invite-code-123/);
|
|
376
|
+
// Contacts helper
|
|
377
|
+
const contact = await ctx.contacts.get("5516999999999@s.whatsapp.net");
|
|
378
|
+
assert.ok(contact);
|
|
379
|
+
assert.equal(contact?.number, "5516999999999");
|
|
380
|
+
assert.equal(contact?.isMe, true);
|
|
381
|
+
await ctx.contacts.block("5516777777777@s.whatsapp.net");
|
|
382
|
+
assert.equal(calls.blockUpdates.length, 1);
|
|
383
|
+
assert.equal(calls.blockUpdates[0].action, "block");
|
|
384
|
+
// Platform escape hatch
|
|
385
|
+
assert.ok(ctx.wa);
|
|
386
|
+
assert.equal(ctx.wa?.contract, mockContract);
|
|
387
|
+
assert.equal(ctx.tg, null);
|
|
388
|
+
assert.equal(ctx.dc, null);
|
|
389
|
+
const mediaResult = await ctx.wa?.downloadMedia();
|
|
390
|
+
assert.ok(mediaResult?.data);
|
|
391
|
+
});
|
|
392
|
+
test("settings, poll, and unblock facets work correctly", async () => {
|
|
393
|
+
const msg = makeBotMessage();
|
|
394
|
+
const chat = {
|
|
395
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
396
|
+
name: "Test Group",
|
|
397
|
+
isGroup: true,
|
|
398
|
+
};
|
|
399
|
+
const ctx = buildApi({
|
|
400
|
+
msg,
|
|
401
|
+
chat,
|
|
402
|
+
contract: mockContract,
|
|
403
|
+
store,
|
|
404
|
+
pluginRegistry,
|
|
405
|
+
pluginName: "test_plugin",
|
|
406
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
407
|
+
});
|
|
408
|
+
// Settings API
|
|
409
|
+
assert.ok(ctx.settings);
|
|
410
|
+
ctx.settings.global.set("key1", "value1");
|
|
411
|
+
assert.equal(ctx.settings.global.get("key1"), "value1");
|
|
412
|
+
ctx.settings.forChat("chat123").set("chatKey", "chatValue");
|
|
413
|
+
assert.equal(ctx.settings.forChat("chat123").get("chatKey"), "chatValue");
|
|
414
|
+
// Unblock contact
|
|
415
|
+
await ctx.contacts.unblock("5516777777777@s.whatsapp.net");
|
|
416
|
+
assert.equal(calls.blockUpdates.length, 1);
|
|
417
|
+
assert.equal(calls.blockUpdates[0].action, "unblock");
|
|
418
|
+
// Send video
|
|
419
|
+
await ctx.send.video(Buffer.from("video-data"), "Video caption");
|
|
420
|
+
assert.equal(calls.sentVideos.length, 1);
|
|
421
|
+
// Poll API
|
|
422
|
+
assert.ok(ctx.poll);
|
|
423
|
+
assert.equal(typeof ctx.poll.create, "function");
|
|
424
|
+
// TargetableAction thenable (.then directly)
|
|
425
|
+
let directThenCalled = false;
|
|
426
|
+
await ctx.send.text("Direct thenable").then(() => {
|
|
427
|
+
directThenCalled = true;
|
|
428
|
+
});
|
|
429
|
+
assert.equal(directThenCalled, true);
|
|
430
|
+
});
|
|
431
|
+
test("ctx.session enforces one exclusive lock per chat across plugins (Phase 7)", async () => {
|
|
432
|
+
const msg = makeBotMessage();
|
|
433
|
+
const chat = {
|
|
434
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
435
|
+
name: "Test Group",
|
|
436
|
+
isGroup: true,
|
|
437
|
+
};
|
|
438
|
+
const gameCtx = buildApi({
|
|
439
|
+
msg, chat, contract: mockContract, store, pluginRegistry,
|
|
440
|
+
pluginName: "gamePlugin",
|
|
441
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
442
|
+
});
|
|
443
|
+
const figurinhaCtx = buildApi({
|
|
444
|
+
msg, chat, contract: mockContract, store, pluginRegistry,
|
|
445
|
+
pluginName: "figurinhaPlugin",
|
|
446
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
447
|
+
});
|
|
448
|
+
// Free chat: the first plugin to ask gets the lock.
|
|
449
|
+
assert.equal(gameCtx.session.isLocked(), false);
|
|
450
|
+
assert.equal(gameCtx.session.acquire(), true);
|
|
451
|
+
assert.equal(gameCtx.session.isMine(), true);
|
|
452
|
+
assert.equal(gameCtx.session.isLocked(), true);
|
|
453
|
+
// A second plugin in the SAME chat cannot also open a session.
|
|
454
|
+
assert.equal(figurinhaCtx.session.isLocked(), true);
|
|
455
|
+
assert.equal(figurinhaCtx.session.acquire(), false);
|
|
456
|
+
assert.equal(figurinhaCtx.session.isMine(), false);
|
|
457
|
+
// The holder re-acquiring its own session is a harmless no-op.
|
|
458
|
+
assert.equal(gameCtx.session.acquire(), true);
|
|
459
|
+
// The non-holder cannot release someone else's session.
|
|
460
|
+
figurinhaCtx.session.release();
|
|
461
|
+
assert.equal(gameCtx.session.isLocked(), true, "release from a non-holder must not affect the lock");
|
|
462
|
+
// Once the real holder releases it, another plugin can acquire it.
|
|
463
|
+
gameCtx.session.release();
|
|
464
|
+
assert.equal(gameCtx.session.isLocked(), false);
|
|
465
|
+
assert.equal(figurinhaCtx.session.acquire(), true);
|
|
466
|
+
assert.equal(figurinhaCtx.session.isMine(), true);
|
|
467
|
+
});
|
|
468
|
+
test("events.once and cleanup removes listeners", async () => {
|
|
469
|
+
let triggeredCount = 0;
|
|
470
|
+
const ctx = buildSetupApi(mockContract, store, pluginRegistry, "test_events_plugin");
|
|
471
|
+
// Test once
|
|
472
|
+
void ctx.events.once("messages.upsert").then(() => {
|
|
473
|
+
triggeredCount++;
|
|
474
|
+
});
|
|
475
|
+
// Clean up
|
|
476
|
+
cleanupPluginEvents("test_events_plugin", mockContract);
|
|
477
|
+
assert.equal(typeof ctx.events.cleanup, "function");
|
|
478
|
+
});
|
|
479
|
+
test("config, i18n, download, scheduler, plugins, chats, contacts pfp/about facets", async () => {
|
|
480
|
+
const msg = makeBotMessage();
|
|
481
|
+
const chat = {
|
|
482
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
483
|
+
name: "Test Group",
|
|
484
|
+
isGroup: true,
|
|
485
|
+
};
|
|
486
|
+
// Register a dependency plugin so plugins.get/require/exists have something real to resolve.
|
|
487
|
+
pluginRegistry.set("dep_plugin", {
|
|
488
|
+
name: "dep_plugin",
|
|
489
|
+
status: "active",
|
|
490
|
+
manifest: { name: "dep_plugin", version: "1.0.0" },
|
|
491
|
+
exports: { greet: () => "hi" },
|
|
492
|
+
});
|
|
493
|
+
store.hydrate({
|
|
494
|
+
chats: [{ id: "120363000000000@g.us", name: "Test Group", ephemeralExpiration: 0 }],
|
|
495
|
+
contacts: {},
|
|
496
|
+
lidMap: [],
|
|
497
|
+
});
|
|
498
|
+
const ctx = buildApi({
|
|
499
|
+
msg,
|
|
500
|
+
chat,
|
|
501
|
+
contract: mockContract,
|
|
502
|
+
store,
|
|
503
|
+
pluginRegistry,
|
|
504
|
+
pluginName: "test_plugin",
|
|
505
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
506
|
+
});
|
|
507
|
+
// config.get
|
|
508
|
+
assert.equal(ctx.config.get("__no_such_key__", "fallback"), "fallback");
|
|
509
|
+
// i18n.t — unknown key still returns a string, never throws
|
|
510
|
+
assert.equal(typeof ctx.i18n.t("__no_such_key__"), "string");
|
|
511
|
+
assert.equal(typeof ctx.t("__no_such_key__"), "string");
|
|
512
|
+
// download.enqueue runs the work function
|
|
513
|
+
let downloadRan = false;
|
|
514
|
+
await new Promise((resolve) => {
|
|
515
|
+
ctx.download.enqueue(async () => {
|
|
516
|
+
downloadRan = true;
|
|
517
|
+
resolve();
|
|
518
|
+
}, async () => resolve());
|
|
519
|
+
});
|
|
520
|
+
assert.equal(downloadRan, true);
|
|
521
|
+
// scheduler.schedule returns a handle with stop()
|
|
522
|
+
const handle = ctx.scheduler.schedule("0 9 * * 1", async () => { });
|
|
523
|
+
assert.equal(typeof handle.stop, "function");
|
|
524
|
+
handle.stop();
|
|
525
|
+
// plugins.get / require / exists
|
|
526
|
+
assert.ok(ctx.plugins.exists("dep_plugin"));
|
|
527
|
+
assert.equal(ctx.plugins.exists("missing_plugin"), false);
|
|
528
|
+
assert.equal(ctx.plugins.get("dep_plugin").greet(), "hi");
|
|
529
|
+
assert.equal(ctx.plugins.get("missing_plugin"), null);
|
|
530
|
+
assert.equal(ctx.plugins.require("dep_plugin").greet(), "hi");
|
|
531
|
+
assert.throws(() => ctx.plugins.require("missing_plugin"), /does not exist or is not active/);
|
|
532
|
+
// chats.all
|
|
533
|
+
const allChats = ctx.chats.all();
|
|
534
|
+
assert.equal(allChats.length, 1);
|
|
535
|
+
assert.equal(allChats[0].name, "Test Group");
|
|
536
|
+
assert.equal(allChats[0].isGroup, true);
|
|
537
|
+
// contacts pfp/about — no pfp/status configured for this jid
|
|
538
|
+
assert.equal(await ctx.contacts.getPfpUrl("5516777777777@s.whatsapp.net"), null);
|
|
539
|
+
assert.equal(await ctx.contacts.getPfpPath("5516777777777@s.whatsapp.net", "/tmp/x.jpg"), null);
|
|
540
|
+
assert.equal(await ctx.contacts.getAbout("5516777777777@s.whatsapp.net"), null);
|
|
541
|
+
// contacts pfp — jid the mock contract recognizes
|
|
542
|
+
assert.equal(await ctx.contacts.getPfpUrl("5516999999999-with-pfp@s.whatsapp.net"), "https://example.com/avatar.jpg");
|
|
543
|
+
// NOTE: WaContract.fetchStatus is typed Promise<string|null> (the
|
|
544
|
+
// Baileys adapter already unwraps the array/object USync shapes down
|
|
545
|
+
// to a plain string before returning). getAbout()'s array/object
|
|
546
|
+
// branches are therefore currently unreachable through the contract —
|
|
547
|
+
// any contract-conformant fetchStatus always lands on the `null`
|
|
548
|
+
// fallback here. Flagged in TEST_REVIEW.md rather than silently
|
|
549
|
+
// asserting a value that can't happen in practice.
|
|
550
|
+
assert.equal(await ctx.contacts.getAbout("5516999999999-with-status@s.whatsapp.net"), null);
|
|
551
|
+
});
|
|
552
|
+
test("admin demote/setSubject/setDescription/setProfilePic/revokeInvite, send.gif/poll, contacts.get resolves via contract.resolveLid", async () => {
|
|
553
|
+
const msg = makeBotMessage();
|
|
554
|
+
const chat = {
|
|
555
|
+
id: { _serialized: "120363000000000@c.us", user: "120363000000000" },
|
|
556
|
+
name: "Test Group",
|
|
557
|
+
isGroup: true,
|
|
558
|
+
};
|
|
559
|
+
const ctx = buildApi({
|
|
560
|
+
msg,
|
|
561
|
+
chat,
|
|
562
|
+
contract: mockContract,
|
|
563
|
+
store,
|
|
564
|
+
pluginRegistry,
|
|
565
|
+
pluginName: "test_plugin",
|
|
566
|
+
guardOptions: { cooldown: false, jitter: false },
|
|
567
|
+
});
|
|
568
|
+
await ctx.admin.demote("5516777777777@s.whatsapp.net");
|
|
569
|
+
assert.equal(calls.groupParticipantUpdates.at(-1)?.action, "demote");
|
|
570
|
+
await ctx.admin.setSubject("New Subject");
|
|
571
|
+
await ctx.admin.setDescription("New Description");
|
|
572
|
+
await ctx.admin.setProfilePic(Buffer.from("pic-data"));
|
|
573
|
+
const invite = await ctx.admin.revokeInvite();
|
|
574
|
+
assert.match(String(invite), /mock-new-invite-code-456/);
|
|
575
|
+
await ctx.send.gif(Buffer.from("already-mp4-bytes"), "gif caption");
|
|
576
|
+
await ctx.send.poll("Favorite color?", ["Red", "Blue"]);
|
|
577
|
+
assert.equal(calls.sentPolls.length, 1);
|
|
578
|
+
// contacts.get() with a raw @lid routes through contract.resolveLid()
|
|
579
|
+
// (mock: "12345@lid" -> "5516777777777@s.whatsapp.net").
|
|
580
|
+
const contact = await ctx.contacts.get("12345@lid");
|
|
581
|
+
assert.equal(contact?.id, "5516777777777@c.us");
|
|
582
|
+
});
|
|
583
|
+
});
|