@manybot/manybot 5.6.1 → 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.
Files changed (71) hide show
  1. package/README.md +20 -3
  2. package/dist/client/banner.js +10 -0
  3. package/dist/client/banner.test.js +31 -0
  4. package/dist/client/store.js +56 -5
  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/drivers/baileys/adapter.js +207 -8
  9. package/dist/drivers/baileys/api/index.js +300 -130
  10. package/dist/drivers/baileys/index.js +62 -30
  11. package/dist/drivers/baileys/loginPrompt.js +0 -2
  12. package/dist/drivers/baileys/messageHandler.js +158 -4
  13. package/dist/drivers/baileys/messageHandler.test.js +203 -0
  14. package/dist/drivers/baileysAdapter.test.js +281 -0
  15. package/dist/drivers/jid.test.js +40 -0
  16. package/dist/drivers/types.js +5 -5
  17. package/dist/i18n/index.js +15 -2
  18. package/dist/kernel/activeDriverSend.js +21 -0
  19. package/dist/kernel/activeDriverSend.test.js +89 -0
  20. package/dist/kernel/alerts.js +3 -9
  21. package/dist/kernel/chatSession.js +65 -0
  22. package/dist/kernel/chatSession.test.js +46 -0
  23. package/dist/kernel/commandAccess.js +66 -0
  24. package/dist/kernel/commandAccess.test.js +74 -0
  25. package/dist/kernel/commandDeprecation.js +168 -0
  26. package/dist/kernel/commandDeprecation.test.js +107 -0
  27. package/dist/kernel/commandMenu.js +268 -0
  28. package/dist/kernel/commandMenu.test.js +234 -0
  29. package/dist/kernel/commandPermissions.js +125 -0
  30. package/dist/kernel/commandPermissions.test.js +159 -0
  31. package/dist/kernel/commandRegistry.js +459 -0
  32. package/dist/kernel/commandRegistry.test.js +156 -0
  33. package/dist/kernel/commandsConfig.js +517 -0
  34. package/dist/kernel/commandsConfig.test.js +236 -0
  35. package/dist/kernel/contactAutoSave.test.js +87 -0
  36. package/dist/kernel/driverManager.js +10 -6
  37. package/dist/kernel/driverManager.test.js +90 -0
  38. package/dist/kernel/integrationMode.js +88 -0
  39. package/dist/kernel/integrationMode.test.js +95 -0
  40. package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
  41. package/dist/kernel/pluginApi.test.js +583 -0
  42. package/dist/kernel/pluginGuard.js +15 -12
  43. package/dist/kernel/pluginGuard.test.js +39 -0
  44. package/dist/kernel/pluginLoader.js +96 -1
  45. package/dist/kernel/pluginLoader.test.js +80 -0
  46. package/dist/kernel/runCommand.js +245 -0
  47. package/dist/kernel/runCommand.test.js +235 -0
  48. package/dist/kernel/sendFallbackGuard.js +19 -48
  49. package/dist/kernel/sendFallbackGuard.test.js +80 -0
  50. package/dist/kernel/sendGuard.js +38 -42
  51. package/dist/kernel/sendGuard.test.js +102 -0
  52. package/dist/kernel/settingsDb.js +4 -3
  53. package/dist/kernel/statusServer.js +9 -2
  54. package/dist/kernel/statusServer.test.js +70 -0
  55. package/dist/kernel/testConfig.js +183 -0
  56. package/dist/kernel/testConfig.test.js +181 -0
  57. package/dist/kernel/updateCheck.js +33 -10
  58. package/dist/locales/en.json +64 -13
  59. package/dist/locales/es.json +64 -13
  60. package/dist/locales/pt.json +64 -13
  61. package/dist/logger/logger.js +23 -3
  62. package/dist/logger/logger.test.js +45 -0
  63. package/dist/main.js +5 -76
  64. package/dist/plugins/__manybot_integration__/index.js +167 -0
  65. package/dist/plugins/__manybot_integration__/index.test.js +184 -0
  66. package/package.json +75 -18
  67. package/dist/drivers/whatsmeow/client.js +0 -252
  68. package/dist/drivers/whatsmeow/index.js +0 -79
  69. package/dist/drivers/whatsmeow/installer.js +0 -86
  70. package/dist/drivers/whatsmeow/supervisor.js +0 -328
  71. package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
@@ -0,0 +1,234 @@
1
+ import test, { describe } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { resolveLocalizedString, renderOverview, renderCategory, renderManual, handleMenuCommand, checkAndTriggerWelcomeMessage } from "#kernel/commandMenu.js";
4
+ import { buildCommandRegistry } from "#kernel/commandRegistry.js";
5
+ import { buildSettingsApi } from "#kernel/settingsDb.js";
6
+ function createTestRegistry() {
7
+ const plugins = new Map([
8
+ [
9
+ "utilPlugin",
10
+ {
11
+ name: "utilPlugin",
12
+ status: "active",
13
+ manifest: { name: "utilPlugin", version: "1.0.0" },
14
+ commands: {
15
+ pingFn: {
16
+ cmd: "ping",
17
+ aliases: ["p"],
18
+ desc: { pt: "Testa a latência", en: "Tests latency" },
19
+ category: "utils",
20
+ manual: "Uso: !ping",
21
+ },
22
+ infoFn: {
23
+ cmd: "info",
24
+ aliases: [],
25
+ desc: "Informações do bot",
26
+ category: "utils",
27
+ },
28
+ adminCmd: {
29
+ cmd: "adminonly",
30
+ aliases: [],
31
+ desc: "Comando restrito",
32
+ category: "adminCat",
33
+ },
34
+ },
35
+ },
36
+ ],
37
+ ]);
38
+ const categories = {
39
+ utils: { label: { pt: "Utilitários", en: "Utilities" }, order: 1 },
40
+ adminCat: { label: { pt: "Administração", en: "Administration" }, order: 2, scope: "group" },
41
+ };
42
+ const menu = {
43
+ title: "ManyBot — Menu",
44
+ intro: "Help menu",
45
+ footer: "Footer text",
46
+ cmd: "help",
47
+ aliases: ["help", "menu"],
48
+ notFoundFallback: false,
49
+ welcomeMessage: { pt: "Bem-vindo ao bot! Use {prefix}help para o menu.", en: "Welcome! Use {prefix}help." },
50
+ welcomeWindowDays: 3,
51
+ pageSize: 2,
52
+ };
53
+ return buildCommandRegistry(null, plugins, undefined, menu, categories);
54
+ }
55
+ describe("kernel/commandMenu", () => {
56
+ describe("resolveLocalizedString", () => {
57
+ test("returns string directly if plain string", () => {
58
+ assert.equal(resolveLocalizedString("hello"), "hello");
59
+ });
60
+ test("resolves requested language from object map", () => {
61
+ const localized = { en: "Hello", pt: "Olá", es: "Hola" };
62
+ assert.equal(resolveLocalizedString(localized, "pt"), "Olá");
63
+ assert.equal(resolveLocalizedString(localized, "en"), "Hello");
64
+ });
65
+ test("falls back to en or first key if target language missing", () => {
66
+ const localized = { en: "English fallback", es: "Hola" };
67
+ assert.equal(resolveLocalizedString(localized, "fr"), "English fallback");
68
+ });
69
+ test("returns null for nullish inputs", () => {
70
+ assert.equal(resolveLocalizedString(null), null);
71
+ assert.equal(resolveLocalizedString(undefined), null);
72
+ });
73
+ });
74
+ describe("renderOverview", () => {
75
+ test("uses English built-in labels by default in tests", () => {
76
+ const registry = createTestRegistry();
77
+ const overview = renderOverview(registry, "en");
78
+ assert.match(overview, /Help menu/);
79
+ });
80
+ test("renders categorized menu overview", () => {
81
+ const registry = createTestRegistry();
82
+ const overview = renderOverview(registry, "pt");
83
+ assert.match(overview, /ManyBot — Menu/);
84
+ assert.match(overview, /📁 \*Utilitários\*/);
85
+ assert.match(overview, /!ping — Testa a latência/);
86
+ assert.match(overview, /!info — Informações do bot/);
87
+ });
88
+ test("filters out category when outside defined scope", () => {
89
+ const registry = createTestRegistry();
90
+ const overviewGroup = renderOverview(registry, "pt", undefined, "group");
91
+ assert.match(overviewGroup, /📁 \*Administração\*/);
92
+ const overviewDm = renderOverview(registry, "pt", undefined, "dm");
93
+ assert.doesNotMatch(overviewDm, /📁 \*Administração\*/);
94
+ });
95
+ test("handles flat command list with pagination", () => {
96
+ const plugins = new Map([
97
+ [
98
+ "utilPlugin",
99
+ {
100
+ name: "utilPlugin",
101
+ status: "active",
102
+ manifest: { name: "utilPlugin", version: "1.0.0" },
103
+ commands: {
104
+ cmd1: { cmd: "c1", aliases: [], desc: "desc 1" },
105
+ cmd2: { cmd: "c2", aliases: [], desc: "desc 2" },
106
+ cmd3: { cmd: "c3", aliases: [], desc: "desc 3" },
107
+ },
108
+ },
109
+ ],
110
+ ]);
111
+ const menu = {
112
+ title: "Flat",
113
+ intro: null,
114
+ footer: null,
115
+ cmd: "help",
116
+ aliases: ["help"],
117
+ notFoundFallback: false,
118
+ welcomeMessage: null,
119
+ welcomeWindowDays: 3,
120
+ pageSize: 2,
121
+ };
122
+ const flatRegistry = buildCommandRegistry(null, plugins, undefined, menu, {});
123
+ const page1 = renderOverview(flatRegistry, "en", 1);
124
+ assert.match(page1, /!c1/);
125
+ assert.match(page1, /!c2/);
126
+ assert.doesNotMatch(page1, /!c3/);
127
+ const page2 = renderOverview(flatRegistry, "en", 2);
128
+ assert.match(page2, /!c3/);
129
+ assert.doesNotMatch(page2, /!c1/);
130
+ });
131
+ });
132
+ describe("renderCategory", () => {
133
+ test("renders commands under a specific category", () => {
134
+ const registry = createTestRegistry();
135
+ const output = renderCategory(registry, "utils", "pt");
136
+ assert.ok(output);
137
+ assert.match(output, /Categoria: Utilitários/);
138
+ assert.match(output, /!ping/);
139
+ assert.match(output, /!info/);
140
+ });
141
+ test("respects scope filtering in renderCategory", () => {
142
+ const registry = createTestRegistry();
143
+ const dmOutput = renderCategory(registry, "adminCat", "pt", "dm");
144
+ assert.equal(dmOutput, null);
145
+ const groupOutput = renderCategory(registry, "adminCat", "pt", "group");
146
+ assert.ok(groupOutput);
147
+ assert.match(groupOutput, /Categoria: Administração/);
148
+ });
149
+ test("returns null for unknown category", () => {
150
+ const registry = createTestRegistry();
151
+ assert.equal(renderCategory(registry, "unknown"), null);
152
+ });
153
+ });
154
+ describe("renderManual", () => {
155
+ test("renders command manual with aliases and description", () => {
156
+ const registry = createTestRegistry();
157
+ const entry = registry.byId.get("utilPlugin::pingFn");
158
+ const manual = renderManual(entry, registry, "pt");
159
+ assert.match(manual, /Manual: !ping/);
160
+ assert.match(manual, /\*Aliases:\* !p/);
161
+ assert.match(manual, /\*Categoria:\* Utilitários/);
162
+ assert.match(manual, /\*Descrição:\* Testa a latência/);
163
+ assert.match(manual, /Uso: !ping/);
164
+ });
165
+ });
166
+ describe("handleMenuCommand", () => {
167
+ test("shows overview when no args provided", () => {
168
+ const registry = createTestRegistry();
169
+ const result = handleMenuCommand("help", "", registry, "pt");
170
+ assert.match(result, /ManyBot — Menu/);
171
+ });
172
+ test("handles page number argument", () => {
173
+ const plugins = new Map([
174
+ [
175
+ "utilPlugin",
176
+ {
177
+ name: "utilPlugin",
178
+ status: "active",
179
+ manifest: { name: "utilPlugin", version: "1.0.0" },
180
+ commands: {
181
+ cmd1: { cmd: "c1", aliases: [] },
182
+ cmd2: { cmd: "c2", aliases: [] },
183
+ cmd3: { cmd: "c3", aliases: [] },
184
+ },
185
+ },
186
+ ],
187
+ ]);
188
+ const menu = {
189
+ title: null,
190
+ intro: null,
191
+ footer: null,
192
+ cmd: "help",
193
+ aliases: ["help"],
194
+ notFoundFallback: false,
195
+ welcomeMessage: null,
196
+ welcomeWindowDays: 3,
197
+ pageSize: 1,
198
+ };
199
+ const flatRegistry = buildCommandRegistry(null, plugins, undefined, menu, {});
200
+ const result = handleMenuCommand("help", "page 2", flatRegistry, "pt");
201
+ assert.match(result, /!c2/);
202
+ assert.doesNotMatch(result, /!c1/);
203
+ });
204
+ test("routes to category if arg matches category name", () => {
205
+ const registry = createTestRegistry();
206
+ const result = handleMenuCommand("help", "utils", registry, "pt");
207
+ assert.match(result, /Categoria: Utilitários/);
208
+ });
209
+ test("routes to manual if arg matches command invocation", () => {
210
+ const registry = createTestRegistry();
211
+ const result = handleMenuCommand("help", "ping", registry, "pt");
212
+ assert.match(result, /Manual: !ping/);
213
+ });
214
+ test("returns not found message for unknown query", () => {
215
+ const registry = createTestRegistry();
216
+ const result = handleMenuCommand("help", "nonexistent", registry, "pt");
217
+ assert.match(result, /nonexistent/);
218
+ });
219
+ });
220
+ describe("checkAndTriggerWelcomeMessage", () => {
221
+ test("triggers welcome message for new user and tracks seen timestamp", () => {
222
+ const registry = createTestRegistry();
223
+ const userId = "test_user_welcome_1";
224
+ const settings = buildSettingsApi("kernel", userId);
225
+ settings.delete("last_welcome_seen");
226
+ const msg = checkAndTriggerWelcomeMessage(userId, registry, "pt");
227
+ assert.ok(msg);
228
+ assert.match(msg, /Bem-vindo ao bot! Use !help para o menu\./);
229
+ // Immediately checking again returns null
230
+ const secondCheck = checkAndTriggerWelcomeMessage(userId, registry, "pt");
231
+ assert.equal(secondCheck, null);
232
+ });
233
+ });
234
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * commandPermissions.ts
3
+ *
4
+ * Permission engine for ManyBot commands.
5
+ * Checks owner, scope, blacklist, whitelist, botAdmin, admin, and cooldown.
6
+ *
7
+ * Order of evaluation:
8
+ * 1. owner
9
+ * 2. scope
10
+ * 3. blacklist
11
+ * 4. whitelist
12
+ * 5. botAdmin
13
+ * 6. admin
14
+ * 7. cooldown (only consumed if all prior checks pass)
15
+ */
16
+ import { OWNER_NUMBER } from "#config";
17
+ import { normalizeJid } from "#drivers/jid.js";
18
+ const cooldownMap = new Map();
19
+ export function clearCooldowns() {
20
+ cooldownMap.clear();
21
+ }
22
+ export function getCooldownMap() {
23
+ return cooldownMap;
24
+ }
25
+ export function matchId(targetId, candidate) {
26
+ if (!targetId || !candidate)
27
+ return false;
28
+ const normTarget = normalizeJid(targetId.trim());
29
+ const normCandidate = normalizeJid(candidate.trim());
30
+ if (normTarget === normCandidate)
31
+ return true;
32
+ const digitsTarget = targetId.replace(/\D/g, "");
33
+ const digitsCandidate = candidate.replace(/\D/g, "");
34
+ if (digitsTarget.length > 0 && digitsTarget === digitsCandidate) {
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ export function matchesAny(targetId, candidates) {
40
+ return candidates.some(candidate => matchId(targetId, candidate));
41
+ }
42
+ export async function checkPermission(entry, ctx) {
43
+ const perms = entry.permissions;
44
+ const msgs = perms.messages;
45
+ // 1. Owner check
46
+ if (perms.owner) {
47
+ if (!OWNER_NUMBER || !matchId(ctx.senderId, OWNER_NUMBER)) {
48
+ return { allowed: false, message: msgs.ownerOnly };
49
+ }
50
+ }
51
+ // 2. Scope check (group | dm | any)
52
+ if (perms.scope === "group" && !ctx.isGroup) {
53
+ return { allowed: false, message: msgs.wrongScope };
54
+ }
55
+ if (perms.scope === "dm" && ctx.isGroup) {
56
+ return { allowed: false, message: msgs.wrongScope };
57
+ }
58
+ // 3. Blacklist check
59
+ if (perms.blacklist) {
60
+ if (ctx.isGroup && perms.blacklist.groups.length > 0) {
61
+ if (matchesAny(ctx.chatId, perms.blacklist.groups)) {
62
+ return { allowed: false, message: msgs.wrongScope };
63
+ }
64
+ }
65
+ if (perms.blacklist.users.length > 0) {
66
+ if (matchesAny(ctx.senderId, perms.blacklist.users)) {
67
+ return { allowed: false, message: msgs.wrongScope };
68
+ }
69
+ }
70
+ }
71
+ // 4. Whitelist check (defined list and outside it -> deny)
72
+ if (perms.whitelist) {
73
+ const hasGroupList = perms.whitelist.groups.length > 0;
74
+ const hasUserList = perms.whitelist.users.length > 0;
75
+ if (ctx.isGroup && hasGroupList) {
76
+ if (!matchesAny(ctx.chatId, perms.whitelist.groups)) {
77
+ return { allowed: false, message: msgs.wrongScope };
78
+ }
79
+ }
80
+ else if (!ctx.isGroup && hasGroupList && !hasUserList) {
81
+ return { allowed: false, message: msgs.wrongScope };
82
+ }
83
+ if (hasUserList) {
84
+ if (!matchesAny(ctx.senderId, perms.whitelist.users)) {
85
+ return { allowed: false, message: msgs.wrongScope };
86
+ }
87
+ }
88
+ }
89
+ // 5. botAdmin check
90
+ if (perms.botAdmin) {
91
+ if (!ctx.isGroup) {
92
+ return { allowed: false, message: msgs.botNotAdmin };
93
+ }
94
+ const isBotAdmin = await ctx.isBotAdmin();
95
+ if (!isBotAdmin) {
96
+ return { allowed: false, message: msgs.botNotAdmin };
97
+ }
98
+ }
99
+ // 6. admin check
100
+ if (perms.admin) {
101
+ if (!ctx.isGroup) {
102
+ return { allowed: false, message: msgs.senderNotAdmin };
103
+ }
104
+ const isSenderAdmin = await ctx.isSenderAdmin();
105
+ if (!isSenderAdmin) {
106
+ return { allowed: false, message: msgs.senderNotAdmin };
107
+ }
108
+ }
109
+ // 7. Cooldown check (only consumed if all other checks pass)
110
+ if (perms.cooldownSeconds > 0) {
111
+ const key = `${entry.id}:${ctx.senderId}`;
112
+ const now = Date.now();
113
+ const lastUsed = cooldownMap.get(key) ?? 0;
114
+ const elapsedSeconds = (now - lastUsed) / 1000;
115
+ if (elapsedSeconds < perms.cooldownSeconds) {
116
+ const remaining = Math.ceil(perms.cooldownSeconds - elapsedSeconds);
117
+ const msg = msgs.cooldown
118
+ .replace(/\{\{seconds\}\}/g, String(remaining))
119
+ .replace(/\{\{time\}\}/g, String(remaining));
120
+ return { allowed: false, message: msg };
121
+ }
122
+ cooldownMap.set(key, now);
123
+ }
124
+ return { allowed: true };
125
+ }
@@ -0,0 +1,159 @@
1
+ import test, { describe } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { checkPermission, clearCooldowns, matchId, matchesAny } from "./commandPermissions.js";
4
+ import { resolvePermissions } from "./commandRegistry.js";
5
+ function createMockContext(overrides) {
6
+ return {
7
+ isGroup: false,
8
+ chatId: "123456789@c.us",
9
+ senderId: "5511999999999@c.us",
10
+ isSenderAdmin: async () => false,
11
+ isBotAdmin: async () => false,
12
+ ...overrides,
13
+ };
14
+ }
15
+ function createMockEntry(specPerms, specMsgs, pluginPerms, defaultsPerms, defaultsMsgs) {
16
+ const permissions = resolvePermissions(specPerms, specMsgs, pluginPerms, defaultsPerms, defaultsMsgs);
17
+ return {
18
+ id: "test::command",
19
+ cmd: "test",
20
+ aliases: [],
21
+ desc: "Test command",
22
+ category: null,
23
+ group: null,
24
+ manual: null,
25
+ source: "plugin",
26
+ pluginName: "testPlugin",
27
+ function: null,
28
+ handler: async () => "ok",
29
+ text: null,
30
+ permissions,
31
+ arguments: [],
32
+ subcommands: {},
33
+ categoryHiddenInScope: null,
34
+ };
35
+ }
36
+ describe("commandPermissions", () => {
37
+ test("matching utilities (matchId, matchesAny)", () => {
38
+ assert.equal(matchId("5511999999999@c.us", "5511999999999"), true);
39
+ assert.equal(matchId("5511999999999@s.whatsapp.net", "5511999999999@c.us"), true);
40
+ assert.equal(matchId("5511999999999", "5511888888888"), false);
41
+ assert.equal(matchesAny("5511999999999@c.us", ["5511888888888", "5511999999999"]), true);
42
+ assert.equal(matchesAny("5511777777777@c.us", ["5511888888888", "5511999999999"]), false);
43
+ });
44
+ test("permission resolution hierarchy (yaml > plugin > defaults)", () => {
45
+ const entry = createMockEntry({ admin: true, cooldownSeconds: 10 }, { senderNotAdmin: "Admin only!" }, { admin: false, botAdmin: true }, { cooldownSeconds: 60 }, { senderNotAdmin: "Default admin msg", botNotAdmin: "Default bot admin msg" });
46
+ assert.equal(entry.permissions.admin, true); // yaml overrides plugin
47
+ assert.equal(entry.permissions.botAdmin, true); // from plugin
48
+ assert.equal(entry.permissions.cooldownSeconds, 10); // yaml overrides global default
49
+ assert.equal(entry.permissions.messages.senderNotAdmin, "Admin only!"); // yaml msg overrides default
50
+ assert.equal(entry.permissions.messages.botNotAdmin, "Default bot admin msg"); // fallback to global default msg
51
+ });
52
+ test("owner permission check", async () => {
53
+ const ownerEntry = createMockEntry({ owner: true }, { ownerOnly: "Only owner allowed" });
54
+ const nonOwnerCtx = createMockContext({ senderId: "5511888888888@c.us" });
55
+ // When OWNER_NUMBER is not set or sender does not match
56
+ const res = await checkPermission(ownerEntry, nonOwnerCtx);
57
+ assert.equal(res.allowed, false);
58
+ if (!res.allowed)
59
+ assert.equal(res.message, "Only owner allowed");
60
+ });
61
+ test("scope permission check", async () => {
62
+ const groupEntry = createMockEntry({ scope: "group" }, { wrongScope: "Group only" });
63
+ const dmEntry = createMockEntry({ scope: "dm" }, { wrongScope: "DM only" });
64
+ const dmCtx = createMockContext({ isGroup: false });
65
+ const groupCtx = createMockContext({ isGroup: true, chatId: "12036301234567890@g.us" });
66
+ const res1 = await checkPermission(groupEntry, dmCtx);
67
+ assert.equal(res1.allowed, false);
68
+ if (!res1.allowed)
69
+ assert.equal(res1.message, "Group only");
70
+ const res2 = await checkPermission(groupEntry, groupCtx);
71
+ assert.equal(res2.allowed, true);
72
+ const res3 = await checkPermission(dmEntry, groupCtx);
73
+ assert.equal(res3.allowed, false);
74
+ if (!res3.allowed)
75
+ assert.equal(res3.message, "DM only");
76
+ const res4 = await checkPermission(dmEntry, dmCtx);
77
+ assert.equal(res4.allowed, true);
78
+ });
79
+ test("blacklist check", async () => {
80
+ const entry = createMockEntry({
81
+ blacklist: {
82
+ groups: ["120363999999999999@g.us"],
83
+ users: ["5511888888888@c.us"],
84
+ },
85
+ });
86
+ const blacklistedUserCtx = createMockContext({ senderId: "5511888888888@c.us" });
87
+ const blacklistedGroupCtx = createMockContext({ isGroup: true, chatId: "120363999999999999@g.us" });
88
+ const allowedCtx = createMockContext({ senderId: "5511999999999@c.us" });
89
+ assert.equal((await checkPermission(entry, blacklistedUserCtx)).allowed, false);
90
+ assert.equal((await checkPermission(entry, blacklistedGroupCtx)).allowed, false);
91
+ assert.equal((await checkPermission(entry, allowedCtx)).allowed, true);
92
+ });
93
+ test("whitelist check", async () => {
94
+ const entry = createMockEntry({
95
+ whitelist: {
96
+ groups: ["120363111111111111@g.us"],
97
+ users: ["5511999999999@c.us"],
98
+ },
99
+ });
100
+ const whitelistedUserCtx = createMockContext({ senderId: "5511999999999@c.us" });
101
+ const unwhitelistedUserCtx = createMockContext({ senderId: "5511888888888@c.us" });
102
+ assert.equal((await checkPermission(entry, whitelistedUserCtx)).allowed, true);
103
+ assert.equal((await checkPermission(entry, unwhitelistedUserCtx)).allowed, false);
104
+ });
105
+ test("admin and botAdmin checks", async () => {
106
+ const adminEntry = createMockEntry({ admin: true }, { senderNotAdmin: "Need admin" });
107
+ const botAdminEntry = createMockEntry({ botAdmin: true }, { botNotAdmin: "Need bot admin" });
108
+ const nonAdminGroupCtx = createMockContext({
109
+ isGroup: true,
110
+ isSenderAdmin: async () => false,
111
+ isBotAdmin: async () => false,
112
+ });
113
+ const adminGroupCtx = createMockContext({
114
+ isGroup: true,
115
+ isSenderAdmin: async () => true,
116
+ isBotAdmin: async () => true,
117
+ });
118
+ const resAdmin = await checkPermission(adminEntry, nonAdminGroupCtx);
119
+ assert.equal(resAdmin.allowed, false);
120
+ if (!resAdmin.allowed)
121
+ assert.equal(resAdmin.message, "Need admin");
122
+ const resBotAdmin = await checkPermission(botAdminEntry, nonAdminGroupCtx);
123
+ assert.equal(resBotAdmin.allowed, false);
124
+ if (!resBotAdmin.allowed)
125
+ assert.equal(resBotAdmin.message, "Need bot admin");
126
+ assert.equal((await checkPermission(adminEntry, adminGroupCtx)).allowed, true);
127
+ assert.equal((await checkPermission(botAdminEntry, adminGroupCtx)).allowed, true);
128
+ });
129
+ test("cooldown check and state consumption", async () => {
130
+ clearCooldowns();
131
+ const entry = createMockEntry({ cooldownSeconds: 30 }, { cooldown: "Wait {{seconds}}s" });
132
+ const ctx = createMockContext({ senderId: "5511999999999@c.us" });
133
+ // First invocation passes and sets cooldown
134
+ const res1 = await checkPermission(entry, ctx);
135
+ assert.equal(res1.allowed, true);
136
+ // Immediate second invocation fails
137
+ const res2 = await checkPermission(entry, ctx);
138
+ assert.equal(res2.allowed, false);
139
+ if (!res2.allowed) {
140
+ assert.match(res2.message ?? "", /Wait 30s/);
141
+ }
142
+ // Resetting cooldown allows next call
143
+ clearCooldowns();
144
+ const res3 = await checkPermission(entry, ctx);
145
+ assert.equal(res3.allowed, true);
146
+ });
147
+ test("cooldown is NOT consumed when an earlier check fails", async () => {
148
+ clearCooldowns();
149
+ const entry = createMockEntry({ scope: "group", cooldownSeconds: 30 }, { wrongScope: "Group only" });
150
+ const dmCtx = createMockContext({ isGroup: false });
151
+ // Scope check fails
152
+ const res1 = await checkPermission(entry, dmCtx);
153
+ assert.equal(res1.allowed, false);
154
+ // Now test in group context — first group call should pass because cooldown was never recorded
155
+ const groupCtx = createMockContext({ isGroup: true, chatId: "120363111@g.us" });
156
+ const res2 = await checkPermission(entry, groupCtx);
157
+ assert.equal(res2.allowed, true);
158
+ });
159
+ });