@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,363 @@
|
|
|
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
|
+
suggestSimilar: false,
|
|
50
|
+
suggestMaxDistance: 2,
|
|
51
|
+
welcomeMessage: { pt: "Bem-vindo ao bot! Use {prefix}help para o menu.", en: "Welcome! Use {prefix}help." },
|
|
52
|
+
welcomeWindowDays: 3,
|
|
53
|
+
pageSize: 2,
|
|
54
|
+
};
|
|
55
|
+
return buildCommandRegistry(null, plugins, undefined, menu, categories);
|
|
56
|
+
}
|
|
57
|
+
describe("kernel/commandMenu", () => {
|
|
58
|
+
describe("resolveLocalizedString", () => {
|
|
59
|
+
test("returns string directly if plain string", () => {
|
|
60
|
+
assert.equal(resolveLocalizedString("hello"), "hello");
|
|
61
|
+
});
|
|
62
|
+
test("resolves requested language from object map", () => {
|
|
63
|
+
const localized = { en: "Hello", pt: "Olá", es: "Hola" };
|
|
64
|
+
assert.equal(resolveLocalizedString(localized, "pt"), "Olá");
|
|
65
|
+
assert.equal(resolveLocalizedString(localized, "en"), "Hello");
|
|
66
|
+
});
|
|
67
|
+
test("falls back to en or first key if target language missing", () => {
|
|
68
|
+
const localized = { en: "English fallback", es: "Hola" };
|
|
69
|
+
assert.equal(resolveLocalizedString(localized, "fr"), "English fallback");
|
|
70
|
+
});
|
|
71
|
+
test("returns null for nullish inputs", () => {
|
|
72
|
+
assert.equal(resolveLocalizedString(null), null);
|
|
73
|
+
assert.equal(resolveLocalizedString(undefined), null);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("renderOverview", () => {
|
|
77
|
+
test("uses English built-in labels by default in tests", () => {
|
|
78
|
+
const registry = createTestRegistry();
|
|
79
|
+
const overview = renderOverview(registry, "en");
|
|
80
|
+
assert.match(overview, /Help menu/);
|
|
81
|
+
});
|
|
82
|
+
test("renders categorized menu overview", () => {
|
|
83
|
+
const registry = createTestRegistry();
|
|
84
|
+
const overview = renderOverview(registry, "pt");
|
|
85
|
+
assert.match(overview, /ManyBot — Menu/);
|
|
86
|
+
assert.match(overview, /📁 \*Utilitários\*/);
|
|
87
|
+
assert.match(overview, /!ping — Testa a latência/);
|
|
88
|
+
assert.match(overview, /!info — Informações do bot/);
|
|
89
|
+
});
|
|
90
|
+
test("filters out category when outside defined scope", () => {
|
|
91
|
+
const registry = createTestRegistry();
|
|
92
|
+
const overviewGroup = renderOverview(registry, "pt", undefined, "group");
|
|
93
|
+
assert.match(overviewGroup, /📁 \*Administração\*/);
|
|
94
|
+
const overviewDm = renderOverview(registry, "pt", undefined, "dm");
|
|
95
|
+
assert.doesNotMatch(overviewDm, /📁 \*Administração\*/);
|
|
96
|
+
});
|
|
97
|
+
test("handles flat command list with pagination", () => {
|
|
98
|
+
const plugins = new Map([
|
|
99
|
+
[
|
|
100
|
+
"utilPlugin",
|
|
101
|
+
{
|
|
102
|
+
name: "utilPlugin",
|
|
103
|
+
status: "active",
|
|
104
|
+
manifest: { name: "utilPlugin", version: "1.0.0" },
|
|
105
|
+
commands: {
|
|
106
|
+
cmd1: { cmd: "c1", aliases: [], desc: "desc 1" },
|
|
107
|
+
cmd2: { cmd: "c2", aliases: [], desc: "desc 2" },
|
|
108
|
+
cmd3: { cmd: "c3", aliases: [], desc: "desc 3" },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
]);
|
|
113
|
+
const menu = {
|
|
114
|
+
title: "Flat",
|
|
115
|
+
intro: null,
|
|
116
|
+
footer: null,
|
|
117
|
+
cmd: "help",
|
|
118
|
+
aliases: ["help"],
|
|
119
|
+
notFoundFallback: false,
|
|
120
|
+
suggestSimilar: false,
|
|
121
|
+
suggestMaxDistance: 2,
|
|
122
|
+
welcomeMessage: null,
|
|
123
|
+
welcomeWindowDays: 3,
|
|
124
|
+
pageSize: 2,
|
|
125
|
+
};
|
|
126
|
+
const flatRegistry = buildCommandRegistry(null, plugins, undefined, menu, {});
|
|
127
|
+
const page1 = renderOverview(flatRegistry, "en", 1);
|
|
128
|
+
assert.match(page1, /!c1/);
|
|
129
|
+
assert.match(page1, /!c2/);
|
|
130
|
+
assert.doesNotMatch(page1, /!c3/);
|
|
131
|
+
const page2 = renderOverview(flatRegistry, "en", 2);
|
|
132
|
+
assert.match(page2, /!c3/);
|
|
133
|
+
assert.doesNotMatch(page2, /!c1/);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe("renderCategory", () => {
|
|
137
|
+
test("renders commands under a specific category", () => {
|
|
138
|
+
const registry = createTestRegistry();
|
|
139
|
+
const output = renderCategory(registry, "utils", "pt");
|
|
140
|
+
assert.ok(output);
|
|
141
|
+
assert.match(output, /Categoria: Utilitários/);
|
|
142
|
+
assert.match(output, /!ping/);
|
|
143
|
+
assert.match(output, /!info/);
|
|
144
|
+
});
|
|
145
|
+
test("respects scope filtering in renderCategory", () => {
|
|
146
|
+
const registry = createTestRegistry();
|
|
147
|
+
const dmOutput = renderCategory(registry, "adminCat", "pt", "dm");
|
|
148
|
+
assert.equal(dmOutput, null);
|
|
149
|
+
const groupOutput = renderCategory(registry, "adminCat", "pt", "group");
|
|
150
|
+
assert.ok(groupOutput);
|
|
151
|
+
assert.match(groupOutput, /Categoria: Administração/);
|
|
152
|
+
});
|
|
153
|
+
test("returns null for unknown category", () => {
|
|
154
|
+
const registry = createTestRegistry();
|
|
155
|
+
assert.equal(renderCategory(registry, "unknown"), null);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe("renderManual", () => {
|
|
159
|
+
test("renders command manual with aliases and description", () => {
|
|
160
|
+
const registry = createTestRegistry();
|
|
161
|
+
const entry = registry.byId.get("utilPlugin::pingFn");
|
|
162
|
+
const manual = renderManual(entry, registry, "pt");
|
|
163
|
+
assert.match(manual, /Manual: !ping/);
|
|
164
|
+
assert.match(manual, /\*Aliases:\* !p/);
|
|
165
|
+
assert.match(manual, /\*Categoria:\* Utilitários/);
|
|
166
|
+
assert.match(manual, /\*Descrição:\* Testa a latência/);
|
|
167
|
+
assert.match(manual, /Uso: !ping/);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe("handleMenuCommand", () => {
|
|
171
|
+
test("shows overview when no args provided", () => {
|
|
172
|
+
const registry = createTestRegistry();
|
|
173
|
+
const result = handleMenuCommand("help", "", registry, "pt");
|
|
174
|
+
assert.match(result, /ManyBot — Menu/);
|
|
175
|
+
});
|
|
176
|
+
test("handles page number argument", () => {
|
|
177
|
+
const plugins = new Map([
|
|
178
|
+
[
|
|
179
|
+
"utilPlugin",
|
|
180
|
+
{
|
|
181
|
+
name: "utilPlugin",
|
|
182
|
+
status: "active",
|
|
183
|
+
manifest: { name: "utilPlugin", version: "1.0.0" },
|
|
184
|
+
commands: {
|
|
185
|
+
cmd1: { cmd: "c1", aliases: [] },
|
|
186
|
+
cmd2: { cmd: "c2", aliases: [] },
|
|
187
|
+
cmd3: { cmd: "c3", aliases: [] },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
]);
|
|
192
|
+
const menu = {
|
|
193
|
+
title: null,
|
|
194
|
+
intro: null,
|
|
195
|
+
footer: null,
|
|
196
|
+
cmd: "help",
|
|
197
|
+
aliases: ["help"],
|
|
198
|
+
notFoundFallback: false,
|
|
199
|
+
suggestSimilar: false,
|
|
200
|
+
suggestMaxDistance: 2,
|
|
201
|
+
welcomeMessage: null,
|
|
202
|
+
welcomeWindowDays: 3,
|
|
203
|
+
pageSize: 1,
|
|
204
|
+
};
|
|
205
|
+
const flatRegistry = buildCommandRegistry(null, plugins, undefined, menu, {});
|
|
206
|
+
const result = handleMenuCommand("help", "page 2", flatRegistry, "pt");
|
|
207
|
+
assert.match(result, /!c2/);
|
|
208
|
+
assert.doesNotMatch(result, /!c1/);
|
|
209
|
+
});
|
|
210
|
+
test("routes to category if arg matches category name", () => {
|
|
211
|
+
const registry = createTestRegistry();
|
|
212
|
+
const result = handleMenuCommand("help", "utils", registry, "pt");
|
|
213
|
+
assert.match(result, /Categoria: Utilitários/);
|
|
214
|
+
});
|
|
215
|
+
test("routes to manual if arg matches command invocation", () => {
|
|
216
|
+
const registry = createTestRegistry();
|
|
217
|
+
const result = handleMenuCommand("help", "ping", registry, "pt");
|
|
218
|
+
assert.match(result, /Manual: !ping/);
|
|
219
|
+
});
|
|
220
|
+
test("returns not found message for unknown query", () => {
|
|
221
|
+
const registry = createTestRegistry();
|
|
222
|
+
const result = handleMenuCommand("help", "nonexistent", registry, "pt");
|
|
223
|
+
assert.match(result, /nonexistent/);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe("checkAndTriggerWelcomeMessage", () => {
|
|
227
|
+
test("triggers welcome message for new user and tracks seen timestamp", () => {
|
|
228
|
+
const registry = createTestRegistry();
|
|
229
|
+
const userId = "test_user_welcome_1";
|
|
230
|
+
const settings = buildSettingsApi("kernel", userId);
|
|
231
|
+
settings.delete("last_welcome_seen");
|
|
232
|
+
const msg = checkAndTriggerWelcomeMessage(userId, registry, { body: "oi", timestamp: Date.now() }, "pt");
|
|
233
|
+
assert.ok(msg);
|
|
234
|
+
assert.match(msg, /Bem-vindo ao bot! Use !help para o menu\./);
|
|
235
|
+
// Immediately checking again returns null
|
|
236
|
+
const secondCheck = checkAndTriggerWelcomeMessage(userId, registry, { body: "oi", timestamp: Date.now() }, "pt");
|
|
237
|
+
assert.equal(secondCheck, null);
|
|
238
|
+
});
|
|
239
|
+
test("does not fire when the incoming message has no body (offline-flush receipt misclassification)", () => {
|
|
240
|
+
const registry = createTestRegistry();
|
|
241
|
+
const userId = "test_user_welcome_empty_body";
|
|
242
|
+
buildSettingsApi("kernel", userId).delete("last_welcome_seen");
|
|
243
|
+
const msg = checkAndTriggerWelcomeMessage(userId, registry, { body: "", timestamp: Date.now() });
|
|
244
|
+
assert.equal(msg, null, "no welcome for a body-less message");
|
|
245
|
+
const msg2 = checkAndTriggerWelcomeMessage(userId, registry, { body: " ", timestamp: Date.now() });
|
|
246
|
+
assert.equal(msg2, null, "no welcome for a whitespace-only body either");
|
|
247
|
+
});
|
|
248
|
+
test("does not fire when the incoming message is older than the freshness window (offline-flush delayed replay)", () => {
|
|
249
|
+
const registry = createTestRegistry();
|
|
250
|
+
const userId = "test_user_welcome_stale_replay";
|
|
251
|
+
buildSettingsApi("kernel", userId).delete("last_welcome_seen");
|
|
252
|
+
const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
|
|
253
|
+
const msg = checkAndTriggerWelcomeMessage(userId, registry, { body: "oi", timestamp: twoHoursAgo });
|
|
254
|
+
assert.equal(msg, null, "no welcome for a 2-hour-old replay");
|
|
255
|
+
});
|
|
256
|
+
test("does fire for a fresh in-window message (control case for the new gates)", () => {
|
|
257
|
+
const registry = createTestRegistry();
|
|
258
|
+
const userId = "test_user_welcome_fresh";
|
|
259
|
+
buildSettingsApi("kernel", userId).delete("last_welcome_seen");
|
|
260
|
+
const msg = checkAndTriggerWelcomeMessage(userId, registry, { body: "oi", timestamp: Date.now() });
|
|
261
|
+
assert.ok(msg, "welcome fires for a current, non-empty message");
|
|
262
|
+
});
|
|
263
|
+
test("does not fire when the msg envelope is missing (defensive — caller must always pass one)", () => {
|
|
264
|
+
const registry = createTestRegistry();
|
|
265
|
+
const userId = "test_user_welcome_no_envelope";
|
|
266
|
+
buildSettingsApi("kernel", userId).delete("last_welcome_seen");
|
|
267
|
+
const msg = checkAndTriggerWelcomeMessage(userId, registry);
|
|
268
|
+
assert.equal(msg, null);
|
|
269
|
+
});
|
|
270
|
+
test("uses the prefix override in the {prefix} placeholder instead of the global default", () => {
|
|
271
|
+
const registry = createTestRegistry();
|
|
272
|
+
const userId = "test_user_welcome_custom_prefix";
|
|
273
|
+
buildSettingsApi("kernel", userId).delete("last_welcome_seen");
|
|
274
|
+
const msg = checkAndTriggerWelcomeMessage(userId, registry, { body: "oi", timestamp: Date.now() }, "pt", "#");
|
|
275
|
+
assert.ok(msg);
|
|
276
|
+
assert.match(msg, /Use #help para o menu\./);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe("hiddenOutsideScope (group/dm suppression)", () => {
|
|
280
|
+
function buildScopeRegistry() {
|
|
281
|
+
const noop = async () => { };
|
|
282
|
+
const plugins = new Map([
|
|
283
|
+
[
|
|
284
|
+
"scopePlugin",
|
|
285
|
+
{
|
|
286
|
+
name: "scopePlugin",
|
|
287
|
+
status: "active",
|
|
288
|
+
manifest: { name: "scopePlugin", version: "1.0.0" },
|
|
289
|
+
commands: {
|
|
290
|
+
groupOnlyFn: {
|
|
291
|
+
cmd: "g",
|
|
292
|
+
aliases: [],
|
|
293
|
+
desc: "Group-only command",
|
|
294
|
+
category: "utils",
|
|
295
|
+
permissions: { groupOnly: true },
|
|
296
|
+
handler: noop,
|
|
297
|
+
},
|
|
298
|
+
dmOnlyFn: {
|
|
299
|
+
cmd: "d",
|
|
300
|
+
aliases: [],
|
|
301
|
+
desc: "DM-only command",
|
|
302
|
+
category: "utils",
|
|
303
|
+
permissions: { dmOnly: true },
|
|
304
|
+
handler: noop,
|
|
305
|
+
},
|
|
306
|
+
bothFn: {
|
|
307
|
+
cmd: "b",
|
|
308
|
+
aliases: [],
|
|
309
|
+
desc: "Both-scopes command",
|
|
310
|
+
category: "utils",
|
|
311
|
+
handler: noop,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
]);
|
|
317
|
+
const menu = {
|
|
318
|
+
title: "Scope",
|
|
319
|
+
intro: null,
|
|
320
|
+
footer: null,
|
|
321
|
+
cmd: "help",
|
|
322
|
+
aliases: ["help"],
|
|
323
|
+
notFoundFallback: false,
|
|
324
|
+
suggestSimilar: false,
|
|
325
|
+
suggestMaxDistance: 2,
|
|
326
|
+
welcomeMessage: null,
|
|
327
|
+
welcomeWindowDays: 3,
|
|
328
|
+
pageSize: 10,
|
|
329
|
+
};
|
|
330
|
+
return buildCommandRegistry(null, plugins, undefined, menu, {
|
|
331
|
+
utils: { label: "Utils", order: 1 },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
test("hides group-only commands from DM overview", () => {
|
|
335
|
+
const registry = buildScopeRegistry();
|
|
336
|
+
const dmOverview = renderOverview(registry, "en", undefined, "dm");
|
|
337
|
+
assert.doesNotMatch(dmOverview, /!g\b/);
|
|
338
|
+
assert.match(dmOverview, /!d\b/);
|
|
339
|
+
assert.match(dmOverview, /!b\b/);
|
|
340
|
+
});
|
|
341
|
+
test("hides DM-only commands from group overview", () => {
|
|
342
|
+
const registry = buildScopeRegistry();
|
|
343
|
+
const groupOverview = renderOverview(registry, "en", undefined, "group");
|
|
344
|
+
assert.match(groupOverview, /!g\b/);
|
|
345
|
+
assert.doesNotMatch(groupOverview, /!d\b/);
|
|
346
|
+
assert.match(groupOverview, /!b\b/);
|
|
347
|
+
});
|
|
348
|
+
test("no-scope overview shows all commands", () => {
|
|
349
|
+
const registry = buildScopeRegistry();
|
|
350
|
+
const overview = renderOverview(registry, "en");
|
|
351
|
+
assert.match(overview, /!g\b/);
|
|
352
|
+
assert.match(overview, /!d\b/);
|
|
353
|
+
assert.match(overview, /!b\b/);
|
|
354
|
+
});
|
|
355
|
+
test("renderCategory respects hiddenOutsideScope", () => {
|
|
356
|
+
const registry = buildScopeRegistry();
|
|
357
|
+
const dmCategory = renderCategory(registry, "utils", "en", "dm");
|
|
358
|
+
assert.ok(dmCategory);
|
|
359
|
+
assert.doesNotMatch(dmCategory, /!g\b/);
|
|
360
|
+
assert.match(dmCategory, /!d\b/);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commandPermissions.ts
|
|
3
|
+
*
|
|
4
|
+
* Permission engine for ManyBot commands.
|
|
5
|
+
* Checks dono, owner, scope, allowed_chats, blacklist, whitelist,
|
|
6
|
+
* botAdmin, admin, and cooldown.
|
|
7
|
+
*
|
|
8
|
+
* Order of evaluation:
|
|
9
|
+
* 1. dono (specific owner JID; otherwise falls through to owner)
|
|
10
|
+
* 2. owner
|
|
11
|
+
* 3. scope
|
|
12
|
+
* 4. allowed_chats (closed list of JIDs; deny outside)
|
|
13
|
+
* 5. blacklist
|
|
14
|
+
* 6. whitelist
|
|
15
|
+
* 7. botAdmin
|
|
16
|
+
* 8. admin
|
|
17
|
+
* 9. cooldown (only consumed if all prior checks pass)
|
|
18
|
+
*/
|
|
19
|
+
import { OWNER_NUMBER } from "#config";
|
|
20
|
+
import { normalizeJid } from "#drivers/jid.js";
|
|
21
|
+
const cooldownMap = new Map();
|
|
22
|
+
export function clearCooldowns() {
|
|
23
|
+
cooldownMap.clear();
|
|
24
|
+
}
|
|
25
|
+
export function getCooldownMap() {
|
|
26
|
+
return cooldownMap;
|
|
27
|
+
}
|
|
28
|
+
export function matchId(targetId, candidate) {
|
|
29
|
+
if (!targetId || !candidate)
|
|
30
|
+
return false;
|
|
31
|
+
const normTarget = normalizeJid(targetId.trim());
|
|
32
|
+
const normCandidate = normalizeJid(candidate.trim());
|
|
33
|
+
if (normTarget === normCandidate)
|
|
34
|
+
return true;
|
|
35
|
+
const digitsTarget = targetId.replace(/\D/g, "");
|
|
36
|
+
const digitsCandidate = candidate.replace(/\D/g, "");
|
|
37
|
+
if (digitsTarget.length > 0 && digitsTarget === digitsCandidate) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
export function matchesAny(targetId, candidates) {
|
|
43
|
+
if (!targetId)
|
|
44
|
+
return false;
|
|
45
|
+
return candidates.some(candidate => matchId(targetId, candidate));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Matches a sender against a list of configured ids (numbers or JIDs),
|
|
49
|
+
* trying both the LID and PN forms — config today is written in phone
|
|
50
|
+
* numbers, but a sender whose PN mapping isn't known yet only has a LID
|
|
51
|
+
* (or, rarely, only a PN when no LID has been learned). Either form
|
|
52
|
+
* matching is enough.
|
|
53
|
+
*/
|
|
54
|
+
export function matchesSender(sender, candidates) {
|
|
55
|
+
return matchesAny(sender.lid, candidates) || matchesAny(sender.pn, candidates);
|
|
56
|
+
}
|
|
57
|
+
/** Stable per-sender identity for cooldown/state keys — prefers LID (canonical), falls back to PN. */
|
|
58
|
+
function senderKey(sender) {
|
|
59
|
+
return sender.lid ?? sender.pn ?? "unknown";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Cooldown reset key. Format: `<pluginName>:<subId|cmd>` so distinct
|
|
63
|
+
* plugin-provided commands don't share buckets even when registered
|
|
64
|
+
* under the same cmd, and the same command under two cmd names doesn't
|
|
65
|
+
* either. Subcommands reuse the parent's plugin/cmd unless overridden.
|
|
66
|
+
*/
|
|
67
|
+
function cooldownKey(entry) {
|
|
68
|
+
return `${entry.pluginName ?? "text"}:${entry.cmd}`;
|
|
69
|
+
}
|
|
70
|
+
export async function checkPermission(entry, ctx) {
|
|
71
|
+
const perms = entry.permissions;
|
|
72
|
+
const msgs = perms.messages;
|
|
73
|
+
// 1. dono check (specific owner JID, overrides global OWNER_NUMBER)
|
|
74
|
+
if (perms.dono) {
|
|
75
|
+
if (!matchesSender(ctx.sender, [perms.dono])) {
|
|
76
|
+
return { allowed: false, message: msgs.donoOnly };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 2. Owner check
|
|
80
|
+
if (perms.owner) {
|
|
81
|
+
if (!OWNER_NUMBER || !matchesSender(ctx.sender, [OWNER_NUMBER])) {
|
|
82
|
+
return { allowed: false, message: msgs.ownerOnly };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 3. Scope check (group | dm | any)
|
|
86
|
+
if (perms.scope === "group" && !ctx.isGroup) {
|
|
87
|
+
return { allowed: false, message: msgs.wrongScope };
|
|
88
|
+
}
|
|
89
|
+
if (perms.scope === "dm" && ctx.isGroup) {
|
|
90
|
+
return { allowed: false, message: msgs.wrongScope };
|
|
91
|
+
}
|
|
92
|
+
// 4. allowed_chats check (closed list of JIDs the command may run in)
|
|
93
|
+
if (perms.allowedChats && perms.allowedChats.length > 0) {
|
|
94
|
+
if (!matchesAny(ctx.chatId, perms.allowedChats)) {
|
|
95
|
+
return { allowed: false, message: msgs.allowedChats };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 5. Blacklist check
|
|
99
|
+
if (perms.blacklist) {
|
|
100
|
+
if (ctx.isGroup && perms.blacklist.groups.length > 0) {
|
|
101
|
+
if (matchesAny(ctx.chatId, perms.blacklist.groups)) {
|
|
102
|
+
return { allowed: false, message: msgs.blacklist };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (perms.blacklist.users.length > 0) {
|
|
106
|
+
if (matchesSender(ctx.sender, perms.blacklist.users)) {
|
|
107
|
+
return { allowed: false, message: msgs.blacklist };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 6. Whitelist check (defined list and outside it -> deny)
|
|
112
|
+
if (perms.whitelist) {
|
|
113
|
+
const hasGroupList = perms.whitelist.groups.length > 0;
|
|
114
|
+
const hasUserList = perms.whitelist.users.length > 0;
|
|
115
|
+
if (ctx.isGroup && hasGroupList) {
|
|
116
|
+
if (!matchesAny(ctx.chatId, perms.whitelist.groups)) {
|
|
117
|
+
return { allowed: false, message: msgs.wrongScope };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (!ctx.isGroup && hasGroupList && !hasUserList) {
|
|
121
|
+
return { allowed: false, message: msgs.wrongScope };
|
|
122
|
+
}
|
|
123
|
+
if (hasUserList) {
|
|
124
|
+
if (!matchesSender(ctx.sender, perms.whitelist.users)) {
|
|
125
|
+
return { allowed: false, message: msgs.wrongScope };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 7. botAdmin check
|
|
130
|
+
if (perms.botAdmin) {
|
|
131
|
+
// Admin status only exists inside a group — outside one, this is a
|
|
132
|
+
// scope mismatch, not "the bot isn't admin", so use wrongScope
|
|
133
|
+
// rather than botNotAdmin (which would misleadingly imply the check
|
|
134
|
+
// was actually evaluated).
|
|
135
|
+
if (!ctx.isGroup) {
|
|
136
|
+
return { allowed: false, message: msgs.wrongScope };
|
|
137
|
+
}
|
|
138
|
+
const isBotAdmin = await ctx.isBotAdmin();
|
|
139
|
+
if (!isBotAdmin) {
|
|
140
|
+
return { allowed: false, message: msgs.botNotAdmin };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 8. admin check
|
|
144
|
+
if (perms.admin) {
|
|
145
|
+
// Same reasoning as botAdmin above: no group, no admin concept, so
|
|
146
|
+
// this is wrongScope, not senderNotAdmin.
|
|
147
|
+
if (!ctx.isGroup) {
|
|
148
|
+
return { allowed: false, message: msgs.wrongScope };
|
|
149
|
+
}
|
|
150
|
+
const isSenderAdmin = await ctx.isSenderAdmin();
|
|
151
|
+
if (!isSenderAdmin) {
|
|
152
|
+
return { allowed: false, message: msgs.senderNotAdmin };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 9. Cooldown check (only consumed if all other checks pass)
|
|
156
|
+
if (perms.cooldownSeconds > 0) {
|
|
157
|
+
const key = `${cooldownKey(entry)}:${senderKey(ctx.sender)}`;
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const lastUsed = cooldownMap.get(key) ?? 0;
|
|
160
|
+
const elapsedSeconds = (now - lastUsed) / 1000;
|
|
161
|
+
if (elapsedSeconds < perms.cooldownSeconds) {
|
|
162
|
+
const remaining = Math.ceil(perms.cooldownSeconds - elapsedSeconds);
|
|
163
|
+
const msg = msgs.cooldown
|
|
164
|
+
.replace(/\{\{seconds\}\}/g, String(remaining))
|
|
165
|
+
.replace(/\{\{time\}\}/g, String(remaining));
|
|
166
|
+
return { allowed: false, message: msg };
|
|
167
|
+
}
|
|
168
|
+
cooldownMap.set(key, now);
|
|
169
|
+
}
|
|
170
|
+
return { allowed: true };
|
|
171
|
+
}
|