@manybot/manybot 5.8.0 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -7
- package/dist/client/store.js +35 -1
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +75 -8
- package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
- package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
- package/dist/drivers/baileys/api/index.js +212 -38
- package/dist/drivers/baileys/index.js +30 -6
- package/dist/drivers/baileys/messageHandler.js +207 -21
- package/dist/drivers/baileys/messageHandler.test.js +256 -14
- package/dist/drivers/baileysAdapter.test.js +97 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +35 -1
- package/dist/i18n/index.js +5 -22
- package/dist/kernel/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- package/dist/kernel/commandAccess.test.js +2 -2
- package/dist/kernel/commandDeprecation.js +4 -2
- package/dist/kernel/commandDeprecation.test.js +8 -1
- package/dist/kernel/commandMenu.js +91 -2
- package/dist/kernel/commandMenu.test.js +131 -2
- package/dist/kernel/commandPermissions.js +69 -23
- package/dist/kernel/commandPermissions.test.js +77 -9
- package/dist/kernel/commandRegistry.js +167 -43
- package/dist/kernel/commandRegistry.test.js +4 -2
- package/dist/kernel/commandsConfig.js +470 -38
- package/dist/kernel/commandsConfig.test.js +249 -3
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/coreCommands.js +62 -0
- package/dist/kernel/pluginApi.test.js +20 -3
- package/dist/kernel/pluginGuard.js +5 -3
- package/dist/kernel/pluginLoader.js +73 -10
- package/dist/kernel/pluginLoader.test.js +111 -1
- package/dist/kernel/runCommand.js +57 -18
- package/dist/kernel/runCommand.test.js +269 -7
- package/dist/kernel/settingsDb.js +15 -2
- package/dist/kernel/testConfig.js +9 -0
- package/dist/locales/en.json +14 -1
- package/dist/locales/es.json +17 -4
- package/dist/locales/pt.json +18 -5
- package/dist/plugins/__manybot_integration__/index.js +33 -16
- package/dist/plugins/__manybot_integration__/index.test.js +42 -8
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +4 -3
|
@@ -4,6 +4,12 @@ import * as yaml from "js-yaml";
|
|
|
4
4
|
import { logger } from "#logger";
|
|
5
5
|
import { t } from "#i18n";
|
|
6
6
|
import { PATHS } from "#config";
|
|
7
|
+
/**
|
|
8
|
+
* Sentinel returned from a command function to short-circuit the chain of
|
|
9
|
+
* `functions:`. Anything else (including `undefined` and a void return)
|
|
10
|
+
* lets the next function in the list run with the same args.
|
|
11
|
+
*/
|
|
12
|
+
export const STOP_CHAIN = Symbol.for("manybot.stopChain");
|
|
7
13
|
const COMMANDS_FILE = path.join(PATHS.HOME, "commands.yaml");
|
|
8
14
|
function asString(value) {
|
|
9
15
|
if (typeof value !== "string")
|
|
@@ -107,6 +113,29 @@ function parseGroupUserList(raw) {
|
|
|
107
113
|
users: users.length > 0 ? users : undefined,
|
|
108
114
|
};
|
|
109
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Map the flat-form scope aliases (`group_only` / `dm_only`) plus an
|
|
118
|
+
* explicit `scope:` into a single resolved "group" | "dm" | "any" | undefined
|
|
119
|
+
* value. `group_only: true` and `dm_only: true` are mutually exclusive —
|
|
120
|
+
* when both are set the function logs a warning and prefers the explicit
|
|
121
|
+
* `scope:` (or `group_only` if `scope` is also absent).
|
|
122
|
+
*/
|
|
123
|
+
function parseScopeFromFlat(obj) {
|
|
124
|
+
const explicit = asString(obj.scope)?.toLowerCase();
|
|
125
|
+
if (explicit === "group" || explicit === "dm" || explicit === "any")
|
|
126
|
+
return explicit;
|
|
127
|
+
const groupOnly = obj.group_only;
|
|
128
|
+
const dmOnly = obj.dm_only;
|
|
129
|
+
if (asBool(groupOnly) === true && asBool(dmOnly) === true) {
|
|
130
|
+
logger.warn(t("system.commandsConfigConflictingScopeFlags", { id: "(permissions)" }));
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
if (asBool(groupOnly) === true)
|
|
134
|
+
return "group";
|
|
135
|
+
if (asBool(dmOnly) === true)
|
|
136
|
+
return "dm";
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
110
139
|
function parsePermissions(raw) {
|
|
111
140
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
112
141
|
return null;
|
|
@@ -114,24 +143,46 @@ function parsePermissions(raw) {
|
|
|
114
143
|
const admin = asBool(obj.admin) ?? undefined;
|
|
115
144
|
const botAdmin = asBool(obj.botAdmin) ?? undefined;
|
|
116
145
|
const owner = asBool(obj.owner) ?? undefined;
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
if (scopeStr === "group" || scopeStr === "dm" || scopeStr === "any") {
|
|
120
|
-
scope = scopeStr;
|
|
121
|
-
}
|
|
146
|
+
const scope = parseScopeFromFlat(obj);
|
|
147
|
+
const dono = asString(obj.dono) ?? undefined;
|
|
122
148
|
let cooldownSeconds = undefined;
|
|
123
149
|
if (typeof obj.cooldownSeconds === "number" && Number.isFinite(obj.cooldownSeconds) && obj.cooldownSeconds >= 0) {
|
|
124
150
|
cooldownSeconds = obj.cooldownSeconds;
|
|
125
151
|
}
|
|
126
|
-
|
|
127
|
-
|
|
152
|
+
// Canonical nested whitelist/blacklist, with the flat-form
|
|
153
|
+
// `whitelist_groups` / `blacklist_users` fields merged in.
|
|
154
|
+
const nestedWhitelist = parseGroupUserList(obj.whitelist);
|
|
155
|
+
const nestedBlacklist = parseGroupUserList(obj.blacklist);
|
|
156
|
+
const flatWhitelistGroups = asAliasList(obj.whitelist_groups);
|
|
157
|
+
const flatBlacklistUsers = asAliasList(obj.blacklist_users);
|
|
158
|
+
let whitelist = nestedWhitelist;
|
|
159
|
+
if (flatWhitelistGroups.length > 0) {
|
|
160
|
+
whitelist = {
|
|
161
|
+
groups: [...(nestedWhitelist?.groups ?? []), ...flatWhitelistGroups],
|
|
162
|
+
users: nestedWhitelist?.users,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
let blacklist = nestedBlacklist;
|
|
166
|
+
if (flatBlacklistUsers.length > 0) {
|
|
167
|
+
blacklist = {
|
|
168
|
+
groups: nestedBlacklist?.groups,
|
|
169
|
+
users: [...(nestedBlacklist?.users ?? []), ...flatBlacklistUsers],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const allowedChats = asAliasList(obj.allowed_chats);
|
|
173
|
+
const hiddenOutsideScope = asBool(obj.hidden_outside_scope) ?? undefined;
|
|
128
174
|
if (admin === undefined &&
|
|
129
175
|
botAdmin === undefined &&
|
|
130
176
|
owner === undefined &&
|
|
131
177
|
scope === undefined &&
|
|
132
178
|
cooldownSeconds === undefined &&
|
|
133
179
|
whitelist === undefined &&
|
|
134
|
-
blacklist === undefined
|
|
180
|
+
blacklist === undefined &&
|
|
181
|
+
dono === undefined &&
|
|
182
|
+
allowedChats.length === 0 &&
|
|
183
|
+
hiddenOutsideScope === undefined &&
|
|
184
|
+
obj.group_only === undefined &&
|
|
185
|
+
obj.dm_only === undefined) {
|
|
135
186
|
return null;
|
|
136
187
|
}
|
|
137
188
|
return {
|
|
@@ -142,6 +193,13 @@ function parsePermissions(raw) {
|
|
|
142
193
|
cooldownSeconds,
|
|
143
194
|
whitelist,
|
|
144
195
|
blacklist,
|
|
196
|
+
dono: dono ?? undefined,
|
|
197
|
+
groupOnly: obj.group_only !== undefined ? asBool(obj.group_only) ?? undefined : undefined,
|
|
198
|
+
dmOnly: obj.dm_only !== undefined ? asBool(obj.dm_only) ?? undefined : undefined,
|
|
199
|
+
whitelistGroups: flatWhitelistGroups.length > 0 ? flatWhitelistGroups : undefined,
|
|
200
|
+
blacklistUsers: flatBlacklistUsers.length > 0 ? flatBlacklistUsers : undefined,
|
|
201
|
+
allowedChats: allowedChats.length > 0 ? allowedChats : undefined,
|
|
202
|
+
hiddenOutsideScope,
|
|
145
203
|
};
|
|
146
204
|
}
|
|
147
205
|
function parseMessages(raw) {
|
|
@@ -151,19 +209,150 @@ function parseMessages(raw) {
|
|
|
151
209
|
const botNotAdmin = asString(obj.botNotAdmin) ?? undefined;
|
|
152
210
|
const senderNotAdmin = asString(obj.senderNotAdmin) ?? undefined;
|
|
153
211
|
const ownerOnly = asString(obj.ownerOnly) ?? undefined;
|
|
212
|
+
const donoOnly = asString(obj.donoOnly) ?? undefined;
|
|
154
213
|
const wrongScope = asString(obj.wrongScope) ?? undefined;
|
|
155
214
|
const cooldown = asString(obj.cooldown) ?? undefined;
|
|
156
|
-
|
|
215
|
+
const blacklist = asString(obj.blacklist) ?? undefined;
|
|
216
|
+
const allowedChats = asString(obj.allowedChats) ?? undefined;
|
|
217
|
+
if (!botNotAdmin && !senderNotAdmin && !ownerOnly && !donoOnly && !wrongScope && !cooldown && !blacklist && !allowedChats) {
|
|
157
218
|
return null;
|
|
158
219
|
}
|
|
159
220
|
return {
|
|
160
221
|
botNotAdmin,
|
|
161
222
|
senderNotAdmin,
|
|
162
223
|
ownerOnly,
|
|
224
|
+
donoOnly,
|
|
163
225
|
wrongScope,
|
|
164
226
|
cooldown,
|
|
227
|
+
blacklist,
|
|
228
|
+
allowedChats,
|
|
165
229
|
};
|
|
166
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Translate the reference yaml's flat `permission_messages:` block into a
|
|
233
|
+
* `CommandMessages` shape — same keys, just renamed. Stored alongside the
|
|
234
|
+
* rest of `defaults.messages` so per-command `messages:` overrides still
|
|
235
|
+
* take precedence.
|
|
236
|
+
*/
|
|
237
|
+
function parsePermissionMessagesBlock(raw) {
|
|
238
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
239
|
+
return null;
|
|
240
|
+
const obj = raw;
|
|
241
|
+
return parseMessages({
|
|
242
|
+
botNotAdmin: obj.admin_only,
|
|
243
|
+
senderNotAdmin: obj.admin_only,
|
|
244
|
+
ownerOnly: obj.dono_only,
|
|
245
|
+
donoOnly: obj.dono_only,
|
|
246
|
+
wrongScope: obj.group_only ?? obj.dm_only,
|
|
247
|
+
cooldown: obj.cooldown,
|
|
248
|
+
blacklist: obj.blacklist,
|
|
249
|
+
allowedChats: obj.allowed_chats,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// ── Loading indicator parsing ────────────────────────────────────────────────
|
|
253
|
+
const LOADING_TYPES = new Set([
|
|
254
|
+
"reaction", "typing", "recording_audio", "spinner", "none",
|
|
255
|
+
]);
|
|
256
|
+
const LOADING_PROPS_BY_TYPE = {
|
|
257
|
+
reaction: ["icon", "onSuccess", "on_success", "onError", "on_error"],
|
|
258
|
+
typing: [],
|
|
259
|
+
recording_audio: [],
|
|
260
|
+
spinner: ["frames", "intervalMs", "interval_ms", "onSuccess", "on_success", "onError", "on_error"],
|
|
261
|
+
none: [],
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Parse a single `loading:` value, which can be:
|
|
265
|
+
* - a preset name (string) — looked up in `presets`
|
|
266
|
+
* - an inline object: `{ type, ... }`
|
|
267
|
+
* Returns the resolved spec, or `null` when the value is absent. Throws
|
|
268
|
+
* nothing: malformed entries log a warning and are dropped (we keep going
|
|
269
|
+
* with `null`, which means "no override; fall back to the next level in
|
|
270
|
+
* the inheritance chain").
|
|
271
|
+
*/
|
|
272
|
+
function parseLoadingSpec(raw, contextId, presets) {
|
|
273
|
+
if (raw === undefined || raw === null)
|
|
274
|
+
return null;
|
|
275
|
+
// `loading: spinner_classico` (preset reference)
|
|
276
|
+
if (typeof raw === "string") {
|
|
277
|
+
const trimmed = raw.trim();
|
|
278
|
+
if (trimmed.length === 0)
|
|
279
|
+
return null;
|
|
280
|
+
const preset = presets[trimmed];
|
|
281
|
+
if (!preset) {
|
|
282
|
+
logger.warn(t("system.commandsConfigLoadingPresetMissing", { id: contextId, name: trimmed }));
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return preset;
|
|
286
|
+
}
|
|
287
|
+
// Inline: `loading: { type: reaction, icon: "⏳", ... }`
|
|
288
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
289
|
+
logger.warn(t("system.commandsConfigLoadingInvalid", { id: contextId }));
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const obj = raw;
|
|
293
|
+
const typeStr = asString(obj.type)?.toLowerCase();
|
|
294
|
+
if (!typeStr || !LOADING_TYPES.has(typeStr)) {
|
|
295
|
+
logger.warn(t("system.commandsConfigLoadingUnknownType", { id: contextId, type: typeStr ?? "(none)" }));
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const type = typeStr;
|
|
299
|
+
const allowed = new Set(LOADING_PROPS_BY_TYPE[type]);
|
|
300
|
+
const recognized = [];
|
|
301
|
+
for (const key of Object.keys(obj)) {
|
|
302
|
+
if (key === "type")
|
|
303
|
+
continue;
|
|
304
|
+
if (!allowed.has(key)) {
|
|
305
|
+
logger.error(t("system.commandsConfigLoadingUnknownProp", { id: contextId, type, key }));
|
|
306
|
+
return null; // malformed config — fail closed
|
|
307
|
+
}
|
|
308
|
+
recognized.push(key);
|
|
309
|
+
}
|
|
310
|
+
const spec = { type };
|
|
311
|
+
if (type === "reaction" || type === "spinner") {
|
|
312
|
+
if (obj.icon !== undefined) {
|
|
313
|
+
const icon = asString(obj.icon);
|
|
314
|
+
if (icon)
|
|
315
|
+
spec.icon = icon;
|
|
316
|
+
}
|
|
317
|
+
if (obj.onSuccess !== undefined || obj.on_success !== undefined) {
|
|
318
|
+
const s = asString(obj.onSuccess ?? obj.on_success);
|
|
319
|
+
if (s)
|
|
320
|
+
spec.onSuccess = s;
|
|
321
|
+
}
|
|
322
|
+
if (obj.onError !== undefined || obj.on_error !== undefined) {
|
|
323
|
+
const s = asString(obj.onError ?? obj.on_error);
|
|
324
|
+
if (s)
|
|
325
|
+
spec.onError = s;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (type === "spinner") {
|
|
329
|
+
if (obj.frames !== undefined) {
|
|
330
|
+
const frames = asAliasList(obj.frames);
|
|
331
|
+
if (frames.length > 0)
|
|
332
|
+
spec.frames = frames;
|
|
333
|
+
}
|
|
334
|
+
if (obj.intervalMs !== undefined || obj.interval_ms !== undefined) {
|
|
335
|
+
const rawVal = obj.intervalMs ?? obj.interval_ms;
|
|
336
|
+
const n = typeof rawVal === "number" && Number.isFinite(rawVal) && rawVal >= 100
|
|
337
|
+
? Math.max(1000, Math.floor(rawVal))
|
|
338
|
+
: null;
|
|
339
|
+
if (n !== null)
|
|
340
|
+
spec.intervalMs = n;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return spec;
|
|
344
|
+
}
|
|
345
|
+
function parseLoadingPresets(raw) {
|
|
346
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
347
|
+
return {};
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
350
|
+
const spec = parseLoadingSpec(value, `loading_presets.${name}`, {});
|
|
351
|
+
if (spec)
|
|
352
|
+
out[name] = spec;
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
167
356
|
const ARGUMENT_TYPES = new Set([
|
|
168
357
|
"mention", "url", "media_direct", "media_reply", "number",
|
|
169
358
|
"duration", "choice", "boolean", "quoted_text", "reply",
|
|
@@ -182,13 +371,28 @@ function parseArgument(raw, parentId) {
|
|
|
182
371
|
logger.warn(t("system.commandsConfigArgumentMissingType", { id: parentId, name }));
|
|
183
372
|
return null;
|
|
184
373
|
}
|
|
185
|
-
|
|
374
|
+
// Reference yaml uses `media_direct_or_reply` (the bot accepts either a
|
|
375
|
+
// direct media attachment or a reply-with-media). It's a superset of
|
|
376
|
+
// `media_reply`, so we silently normalize it down.
|
|
377
|
+
const normalizedType = typeStr === "media_direct_or_reply" ? "media_reply" : typeStr;
|
|
378
|
+
if (!ARGUMENT_TYPES.has(normalizedType)) {
|
|
186
379
|
logger.warn(t("system.commandsConfigUnknownArgType", { id: parentId, name, type: typeStr }));
|
|
187
380
|
return null;
|
|
188
381
|
}
|
|
189
|
-
|
|
382
|
+
// `required: true` and `optional: true` are two ways to say the same
|
|
383
|
+
// thing in opposite directions. The reference yaml prefers `optional:`.
|
|
384
|
+
let required;
|
|
385
|
+
if (asBool(obj.required) !== null) {
|
|
386
|
+
required = asBool(obj.required) ?? false;
|
|
387
|
+
}
|
|
388
|
+
else if (asBool(obj.optional) !== null) {
|
|
389
|
+
required = !(asBool(obj.optional) ?? false);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
required = false;
|
|
393
|
+
}
|
|
190
394
|
let choices;
|
|
191
|
-
if (
|
|
395
|
+
if (normalizedType === "choice") {
|
|
192
396
|
choices = asAliasList(obj.choices);
|
|
193
397
|
if (choices.length === 0) {
|
|
194
398
|
logger.warn(t("system.commandsConfigChoiceArgumentNoChoices", { id: parentId, name }));
|
|
@@ -197,7 +401,7 @@ function parseArgument(raw, parentId) {
|
|
|
197
401
|
}
|
|
198
402
|
return {
|
|
199
403
|
name,
|
|
200
|
-
type:
|
|
404
|
+
type: normalizedType,
|
|
201
405
|
required,
|
|
202
406
|
choices,
|
|
203
407
|
};
|
|
@@ -217,7 +421,7 @@ function parseArguments(raw, parentId) {
|
|
|
217
421
|
}
|
|
218
422
|
return out;
|
|
219
423
|
}
|
|
220
|
-
async function parseSubcommand(parentId, raw) {
|
|
424
|
+
async function parseSubcommand(parentId, raw, presets, onPlugin) {
|
|
221
425
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
222
426
|
return null;
|
|
223
427
|
const obj = raw;
|
|
@@ -233,15 +437,20 @@ async function parseSubcommand(parentId, raw) {
|
|
|
233
437
|
id,
|
|
234
438
|
cmd,
|
|
235
439
|
aliases: asAliasList(obj.aliases),
|
|
236
|
-
function:
|
|
440
|
+
// Per-sub function chain. `functions: [...]` wins; a single
|
|
441
|
+
// `function: "x"` becomes a one-element list. `null` means "inherit
|
|
442
|
+
// the parent's chain at build time" — the kernel resolves that in
|
|
443
|
+
// commandRegistry, not here.
|
|
444
|
+
functions: parseFunctionList(obj.function, obj.functions, onPlugin),
|
|
445
|
+
loading: parseLoadingSpec(obj.loading, id, presets),
|
|
237
446
|
desc: parseLocalizedString(obj.desc),
|
|
238
447
|
manual,
|
|
239
|
-
arguments: parseArguments(obj.arguments, id),
|
|
448
|
+
arguments: parseArguments(obj.arguments ?? obj.args, id),
|
|
240
449
|
permissions: parsePermissions(obj.permissions),
|
|
241
450
|
messages: parseMessages(obj.messages),
|
|
242
451
|
};
|
|
243
452
|
}
|
|
244
|
-
async function parseSubcommands(raw, parentId) {
|
|
453
|
+
async function parseSubcommands(raw, parentId, presets, onPlugin) {
|
|
245
454
|
if (raw === undefined || raw === null)
|
|
246
455
|
return [];
|
|
247
456
|
if (!Array.isArray(raw)) {
|
|
@@ -250,13 +459,62 @@ async function parseSubcommands(raw, parentId) {
|
|
|
250
459
|
}
|
|
251
460
|
const out = [];
|
|
252
461
|
for (const item of raw) {
|
|
253
|
-
const sub = await parseSubcommand(parentId, item);
|
|
462
|
+
const sub = await parseSubcommand(parentId, item, presets, onPlugin);
|
|
254
463
|
if (sub)
|
|
255
464
|
out.push(sub);
|
|
256
465
|
}
|
|
257
466
|
return out;
|
|
258
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Resolve `function:` (single name) and/or `functions:` (ordered list)
|
|
470
|
+
* into a single ordered chain. Reference yaml mixes both forms per command.
|
|
471
|
+
* Empty result means "inherit the parent's chain at build time" — the
|
|
472
|
+
* spec carries `null` to flag that, and the registry resolves it.
|
|
473
|
+
*
|
|
474
|
+
* Qualified items use the `plugin.function` form. `core` is the reserved
|
|
475
|
+
* kernel namespace; external plugins use their canonical `owner/plugin` key.
|
|
476
|
+
* The registry stores and dispatches only the function portion because a
|
|
477
|
+
* command has one owning plugin.
|
|
478
|
+
*/
|
|
479
|
+
function splitQualifiedFunction(value) {
|
|
480
|
+
const dot = value.indexOf(".");
|
|
481
|
+
if (dot <= 0 || dot === value.length - 1) {
|
|
482
|
+
return { plugin: null, functionName: value };
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
plugin: value.slice(0, dot),
|
|
486
|
+
functionName: value.slice(dot + 1),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function parseFunctionList(single, list, onPlugin) {
|
|
490
|
+
if (list !== undefined && list !== null) {
|
|
491
|
+
if (!Array.isArray(list))
|
|
492
|
+
return [];
|
|
493
|
+
const out = [];
|
|
494
|
+
for (const item of list) {
|
|
495
|
+
const s = asString(item);
|
|
496
|
+
if (s) {
|
|
497
|
+
const qualified = splitQualifiedFunction(s);
|
|
498
|
+
if (qualified.plugin)
|
|
499
|
+
onPlugin?.(qualified.plugin);
|
|
500
|
+
out.push(qualified.functionName);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
if (single !== undefined && single !== null) {
|
|
506
|
+
const s = asString(single);
|
|
507
|
+
if (!s)
|
|
508
|
+
return [];
|
|
509
|
+
const qualified = splitQualifiedFunction(s);
|
|
510
|
+
if (qualified.plugin)
|
|
511
|
+
onPlugin?.(qualified.plugin);
|
|
512
|
+
return [qualified.functionName];
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
259
516
|
const DEFAULT_MENU_CONFIG = {
|
|
517
|
+
enabled: false,
|
|
260
518
|
title: "🤖 ManyBot — Menu",
|
|
261
519
|
intro: {
|
|
262
520
|
en: "Use {prefix}<command> to run it or {prefix}help <command> to view its manual.",
|
|
@@ -267,23 +525,34 @@ const DEFAULT_MENU_CONFIG = {
|
|
|
267
525
|
cmd: "menu",
|
|
268
526
|
aliases: ["help", "man", "menu", "bot", "?"],
|
|
269
527
|
notFoundFallback: false,
|
|
528
|
+
suggestSimilar: false,
|
|
529
|
+
suggestMaxDistance: 2,
|
|
270
530
|
welcomeMessage: null,
|
|
271
531
|
welcomeWindowDays: 3,
|
|
272
532
|
pageSize: 15,
|
|
273
533
|
};
|
|
274
534
|
function parseMenu(raw) {
|
|
535
|
+
if (raw === undefined) {
|
|
536
|
+
return { ...DEFAULT_MENU_CONFIG };
|
|
537
|
+
}
|
|
275
538
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
276
539
|
return { ...DEFAULT_MENU_CONFIG };
|
|
277
540
|
}
|
|
278
541
|
const obj = raw;
|
|
279
542
|
const aliases = asAliasList(obj.aliases);
|
|
280
543
|
return {
|
|
544
|
+
// Block present in commands.yaml → enabled by default; `enabled: false`
|
|
545
|
+
// inside it still opts back out (e.g. keeping notFoundFallback/welcome
|
|
546
|
+
// config without claiming the menu/help invocations).
|
|
547
|
+
enabled: asBool(obj.enabled) ?? true,
|
|
281
548
|
title: parseLocalizedString(obj.title) ?? DEFAULT_MENU_CONFIG.title,
|
|
282
549
|
intro: parseLocalizedString(obj.intro) ?? DEFAULT_MENU_CONFIG.intro,
|
|
283
550
|
footer: parseLocalizedString(obj.footer) ?? DEFAULT_MENU_CONFIG.footer,
|
|
284
551
|
cmd: asString(obj.cmd) ?? DEFAULT_MENU_CONFIG.cmd,
|
|
285
|
-
aliases: aliases
|
|
552
|
+
aliases: obj.aliases !== undefined ? aliases : [...DEFAULT_MENU_CONFIG.aliases],
|
|
286
553
|
notFoundFallback: asBool(obj.notFoundFallback) ?? DEFAULT_MENU_CONFIG.notFoundFallback,
|
|
554
|
+
suggestSimilar: asBool(obj.suggestSimilar) ?? DEFAULT_MENU_CONFIG.suggestSimilar,
|
|
555
|
+
suggestMaxDistance: asPositiveInt(obj.suggestMaxDistance, DEFAULT_MENU_CONFIG.suggestMaxDistance),
|
|
287
556
|
welcomeMessage: parseLocalizedString(obj.welcomeMessage) ?? DEFAULT_MENU_CONFIG.welcomeMessage,
|
|
288
557
|
welcomeWindowDays: asPositiveInt(obj.welcomeWindowDays, DEFAULT_MENU_CONFIG.welcomeWindowDays),
|
|
289
558
|
pageSize: asPositiveInt(obj.pageSize, DEFAULT_MENU_CONFIG.pageSize),
|
|
@@ -295,11 +564,13 @@ function parseScopeValue(raw) {
|
|
|
295
564
|
return s;
|
|
296
565
|
return null;
|
|
297
566
|
}
|
|
298
|
-
function parseCategories(raw) {
|
|
299
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
300
|
-
return {};
|
|
567
|
+
function parseCategories(raw, presets) {
|
|
568
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
569
|
+
return { categories: {}, categoryLoading: {} };
|
|
570
|
+
}
|
|
301
571
|
const obj = raw;
|
|
302
|
-
const
|
|
572
|
+
const categories = {};
|
|
573
|
+
const categoryLoading = {};
|
|
303
574
|
for (const [catKey, value] of Object.entries(obj)) {
|
|
304
575
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
305
576
|
continue;
|
|
@@ -308,14 +579,17 @@ function parseCategories(raw) {
|
|
|
308
579
|
const order = typeof catObj.order === "number" && Number.isFinite(catObj.order) ? catObj.order : 999;
|
|
309
580
|
const scope = parseScopeValue(catObj.scope);
|
|
310
581
|
const hiddenInScope = parseScopeValue(catObj.hiddenInScope);
|
|
311
|
-
|
|
582
|
+
categories[catKey] = {
|
|
312
583
|
label,
|
|
313
584
|
order,
|
|
314
585
|
scope: scope ?? null,
|
|
315
586
|
hiddenInScope: hiddenInScope ?? null,
|
|
316
587
|
};
|
|
588
|
+
const loading = parseLoadingSpec(catObj.loading, `categories.${catKey}`, presets);
|
|
589
|
+
if (loading)
|
|
590
|
+
categoryLoading[catKey] = loading;
|
|
317
591
|
}
|
|
318
|
-
return
|
|
592
|
+
return { categories, categoryLoading };
|
|
319
593
|
}
|
|
320
594
|
async function parseManuals(raw) {
|
|
321
595
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -334,7 +608,7 @@ async function parseManuals(raw) {
|
|
|
334
608
|
}
|
|
335
609
|
const DEFAULT_NOTIFY_CHANGES = true;
|
|
336
610
|
const DEFAULT_NOTIFY_PERIOD_DAYS = 7;
|
|
337
|
-
function parseDefaults(raw) {
|
|
611
|
+
function parseDefaults(raw, presets) {
|
|
338
612
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
339
613
|
return {
|
|
340
614
|
notifyChanges: DEFAULT_NOTIFY_CHANGES,
|
|
@@ -342,6 +616,7 @@ function parseDefaults(raw) {
|
|
|
342
616
|
notifyMessage: null,
|
|
343
617
|
permissions: null,
|
|
344
618
|
messages: null,
|
|
619
|
+
loading: null,
|
|
345
620
|
};
|
|
346
621
|
}
|
|
347
622
|
const obj = raw;
|
|
@@ -351,9 +626,47 @@ function parseDefaults(raw) {
|
|
|
351
626
|
notifyMessage: asString(obj.notifyMessage),
|
|
352
627
|
permissions: parsePermissions(obj.permissions),
|
|
353
628
|
messages: parseMessages(obj.messages),
|
|
629
|
+
loading: parseLoadingSpec(obj.loading, "defaults", presets),
|
|
354
630
|
};
|
|
355
631
|
}
|
|
356
|
-
|
|
632
|
+
/**
|
|
633
|
+
* Normalize the value of a `plugin:` entry against the live
|
|
634
|
+
* `pluginRegistry`. The parser runs after `loadPlugins()` has populated
|
|
635
|
+
* the registry, so by the time this fires every active plugin is
|
|
636
|
+
* reachable under its full `owner/repo` key.
|
|
637
|
+
*
|
|
638
|
+
* Accepted shapes:
|
|
639
|
+
* - full registry key `owner/repo` — used verbatim if it matches;
|
|
640
|
+
* - bare `name` (no slash) — resolved to the unique `.../name` key
|
|
641
|
+
* in the registry. If zero keys match, the original `name` is
|
|
642
|
+
* returned. If more than one matches, the original `name` is also
|
|
643
|
+
* returned so the ambiguity surfaces at dispatch instead of being
|
|
644
|
+
* silently resolved by picking one owner;
|
|
645
|
+
* - inline `owner/repo.fn` or `name.fn` — split on the first dot by
|
|
646
|
+
* `parseEntry` before reaching here; only the prefix half is
|
|
647
|
+
* passed in.
|
|
648
|
+
*
|
|
649
|
+
* Pure: no I/O, no module imports. The registry is passed in because
|
|
650
|
+
* importing `pluginLoader.ts` from this file would close a
|
|
651
|
+
* commandsConfig to pluginLoader to commandRegistry to commandsConfig
|
|
652
|
+
* cycle (this module already imports types from commandRegistry).
|
|
653
|
+
*/
|
|
654
|
+
function resolvePluginKey(raw, validPluginKeys) {
|
|
655
|
+
if (validPluginKeys.has(raw))
|
|
656
|
+
return raw;
|
|
657
|
+
if (raw.includes("/"))
|
|
658
|
+
return raw;
|
|
659
|
+
let match = null;
|
|
660
|
+
for (const key of validPluginKeys) {
|
|
661
|
+
if (key.endsWith("/" + raw)) {
|
|
662
|
+
if (match !== null)
|
|
663
|
+
return raw;
|
|
664
|
+
match = key;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return match ?? raw;
|
|
668
|
+
}
|
|
669
|
+
async function parseEntry(id, raw, presets, validPluginKeys) {
|
|
357
670
|
const cmd = asString(raw.cmd);
|
|
358
671
|
if (!cmd) {
|
|
359
672
|
logger.warn(t("system.commandsConfigMissingCmd", {
|
|
@@ -366,12 +679,52 @@ async function parseEntry(id, raw) {
|
|
|
366
679
|
const text = rawText ? await resolveFileRef(rawText) : null;
|
|
367
680
|
const rawManual = parseLocalizedString(raw.manual);
|
|
368
681
|
const manual = rawManual ? await resolveFileRef(rawManual) : null;
|
|
682
|
+
// `plugin:` accepts both an `owner/repo` registry key (the canonical
|
|
683
|
+
// form) and the legacy shorthand `name` (resolved against the active
|
|
684
|
+
// `pluginRegistry` keys passed in via `validPluginKeys`). It also
|
|
685
|
+
// accepts an inline form split on the first dot, where everything
|
|
686
|
+
// after the dot is a function name treated as the head of the
|
|
687
|
+
// `functions:` chain. When `validPluginKeys` is provided the parser
|
|
688
|
+
// prefers an exact registry key and falls back to a `/<plugin>` suffix
|
|
689
|
+
// match (one plugin per name across owners); without it the value is
|
|
690
|
+
// used verbatim and the caller is responsible for resolving it.
|
|
691
|
+
const pluginFull = asString(raw.plugin);
|
|
692
|
+
let plugin = null;
|
|
693
|
+
let inlineFn = null;
|
|
694
|
+
if (pluginFull) {
|
|
695
|
+
const dot = pluginFull.indexOf(".");
|
|
696
|
+
if (dot >= 0) {
|
|
697
|
+
plugin = pluginFull.slice(0, dot);
|
|
698
|
+
inlineFn = pluginFull.slice(dot + 1);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
plugin = pluginFull;
|
|
702
|
+
}
|
|
703
|
+
if (validPluginKeys)
|
|
704
|
+
plugin = resolvePluginKey(plugin, validPluginKeys);
|
|
705
|
+
}
|
|
706
|
+
// `function:` → `functions: [fn]`, `functions: [...]` → as-is,
|
|
707
|
+
// "core.ping" inline above → `[ping]`, no fields → null (inherit).
|
|
708
|
+
let functionPlugin = null;
|
|
709
|
+
const inlineFunctions = parseFunctionList(raw.function, raw.functions, (name) => {
|
|
710
|
+
functionPlugin ??= name;
|
|
711
|
+
});
|
|
712
|
+
const functions = inlineFn !== null
|
|
713
|
+
? [inlineFn, ...(inlineFunctions ?? [])]
|
|
714
|
+
: inlineFunctions;
|
|
715
|
+
const subcommands = await parseSubcommands(raw.subcommands, id, presets, (name) => {
|
|
716
|
+
functionPlugin ??= name;
|
|
717
|
+
});
|
|
718
|
+
if (!plugin && functionPlugin) {
|
|
719
|
+
plugin = resolvePluginKey(functionPlugin, validPluginKeys ?? new Set());
|
|
720
|
+
}
|
|
369
721
|
return {
|
|
370
722
|
id,
|
|
371
723
|
cmd,
|
|
372
724
|
aliases: asAliasList(raw.aliases),
|
|
373
|
-
plugin
|
|
374
|
-
|
|
725
|
+
plugin,
|
|
726
|
+
functions: functions ?? [],
|
|
727
|
+
loading: parseLoadingSpec(raw.loading, id, presets),
|
|
375
728
|
text,
|
|
376
729
|
desc: parseLocalizedString(raw.desc),
|
|
377
730
|
category: asString(raw.category),
|
|
@@ -381,11 +734,17 @@ async function parseEntry(id, raw) {
|
|
|
381
734
|
notifyChanges: asBool(raw.notifyChanges),
|
|
382
735
|
permissions: parsePermissions(raw.permissions),
|
|
383
736
|
messages: parseMessages(raw.messages),
|
|
384
|
-
arguments: parseArguments(raw.arguments, id),
|
|
385
|
-
subcommands
|
|
737
|
+
arguments: parseArguments(raw.arguments ?? raw.args, id),
|
|
738
|
+
subcommands,
|
|
386
739
|
};
|
|
387
740
|
}
|
|
388
|
-
const RESERVED_KEYS = new Set([
|
|
741
|
+
const RESERVED_KEYS = new Set([
|
|
742
|
+
"defaults", "menu", "categories", "manuals", "import",
|
|
743
|
+
"loading_presets", "loading",
|
|
744
|
+
"prefix",
|
|
745
|
+
"notify_changes", "notify_period_days", "deprecation_message", "permission_messages",
|
|
746
|
+
"commands",
|
|
747
|
+
]);
|
|
389
748
|
/**
|
|
390
749
|
* Resolves `import:` (a path or list of paths, relative to PATHS.HOME) by
|
|
391
750
|
* reading each referenced YAML file and folding its top-level sections
|
|
@@ -445,7 +804,45 @@ async function resolveImports(root) {
|
|
|
445
804
|
}
|
|
446
805
|
return merged;
|
|
447
806
|
}
|
|
448
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Unwrap a `commands:` wrapper block, if present, so the rest of the
|
|
809
|
+
* loader doesn't need to know whether the user wrote
|
|
810
|
+
*
|
|
811
|
+
* mycommand:
|
|
812
|
+
* cmd: foo
|
|
813
|
+
*
|
|
814
|
+
* or
|
|
815
|
+
*
|
|
816
|
+
* commands:
|
|
817
|
+
* mycommand:
|
|
818
|
+
* cmd: foo
|
|
819
|
+
*
|
|
820
|
+
* Both forms get parsed identically. The reference yaml uses the wrapper
|
|
821
|
+
* because it's clearer once you also have a `defaults:` block at the top
|
|
822
|
+
* (otherwise the command ids sit at the same indent as `defaults` and the
|
|
823
|
+
* file looks ambiguous).
|
|
824
|
+
*/
|
|
825
|
+
function unwrapCommandsWrapper(root) {
|
|
826
|
+
const wrapper = root.commands;
|
|
827
|
+
if (wrapper === undefined || wrapper === null)
|
|
828
|
+
return root;
|
|
829
|
+
if (typeof wrapper !== "object" || Array.isArray(wrapper)) {
|
|
830
|
+
logger.error(t("system.commandsConfigCommandsWrapperInvalid"));
|
|
831
|
+
return root;
|
|
832
|
+
}
|
|
833
|
+
const inner = wrapper;
|
|
834
|
+
const out = { ...root };
|
|
835
|
+
delete out.commands;
|
|
836
|
+
for (const [key, value] of Object.entries(inner)) {
|
|
837
|
+
if (key in out) {
|
|
838
|
+
logger.error(t("system.commandsConfigCommandsWrapperCollision", { key }));
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
out[key] = value;
|
|
842
|
+
}
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
export async function loadCommandsConfig(validPluginKeys) {
|
|
449
846
|
let raw;
|
|
450
847
|
try {
|
|
451
848
|
raw = await fs.readFile(COMMANDS_FILE, "utf8");
|
|
@@ -474,16 +871,20 @@ export async function loadCommandsConfig() {
|
|
|
474
871
|
}
|
|
475
872
|
if (parsed === null || parsed === undefined) {
|
|
476
873
|
return {
|
|
874
|
+
prefix: null,
|
|
477
875
|
defaults: {
|
|
478
876
|
notifyChanges: DEFAULT_NOTIFY_CHANGES,
|
|
479
877
|
notifyPeriodDays: DEFAULT_NOTIFY_PERIOD_DAYS,
|
|
480
878
|
notifyMessage: null,
|
|
481
879
|
permissions: null,
|
|
482
880
|
messages: null,
|
|
881
|
+
loading: null,
|
|
483
882
|
},
|
|
484
883
|
menu: { ...DEFAULT_MENU_CONFIG },
|
|
485
884
|
categories: {},
|
|
486
885
|
manuals: {},
|
|
886
|
+
loadingPresets: {},
|
|
887
|
+
categoryLoading: {},
|
|
487
888
|
specs: [],
|
|
488
889
|
};
|
|
489
890
|
}
|
|
@@ -493,10 +894,41 @@ export async function loadCommandsConfig() {
|
|
|
493
894
|
}));
|
|
494
895
|
return null;
|
|
495
896
|
}
|
|
496
|
-
const
|
|
497
|
-
const
|
|
897
|
+
const imported = await resolveImports(parsed);
|
|
898
|
+
const root = unwrapCommandsWrapper(imported);
|
|
899
|
+
const prefix = asString(root.prefix) ?? null;
|
|
900
|
+
const loadingPresets = parseLoadingPresets(root.loading_presets);
|
|
901
|
+
// Top-level notify_*/deprecation_message/permission_messages/loading act
|
|
902
|
+
// as a shallow overlay over `defaults:` — when present, each key wins
|
|
903
|
+
// over the same key under `defaults:`. Same shape, no deep merge (same
|
|
904
|
+
// rule as imports).
|
|
905
|
+
const defaultsRaw = (root.defaults ?? {});
|
|
906
|
+
const notifyChangesTop = asBool(root.notify_changes);
|
|
907
|
+
const notifyPeriodTop = typeof root.notify_period_days === "number" && Number.isFinite(root.notify_period_days)
|
|
908
|
+
? root.notify_period_days
|
|
909
|
+
: null;
|
|
910
|
+
const deprecationMessageTop = asString(root.deprecation_message);
|
|
911
|
+
const permissionMessagesTop = parsePermissionMessagesBlock(root.permission_messages);
|
|
912
|
+
const mergedDefaults = { ...defaultsRaw };
|
|
913
|
+
if (notifyChangesTop !== null)
|
|
914
|
+
mergedDefaults.notifyChanges = notifyChangesTop;
|
|
915
|
+
if (notifyPeriodTop !== null)
|
|
916
|
+
mergedDefaults.notifyPeriodDays = notifyPeriodTop;
|
|
917
|
+
if (deprecationMessageTop !== null)
|
|
918
|
+
mergedDefaults.notifyMessage = deprecationMessageTop;
|
|
919
|
+
if (permissionMessagesTop)
|
|
920
|
+
mergedDefaults.messages = {
|
|
921
|
+
...(defaultsRaw.messages ?? {}),
|
|
922
|
+
...permissionMessagesTop,
|
|
923
|
+
};
|
|
924
|
+
// Top-level `loading: <preset-name-or-inline-spec>` — reference yaml's
|
|
925
|
+
// global default (`loading: padrao`). Only overlays when actually
|
|
926
|
+
// present; `defaults.loading` (nested form) still works on its own.
|
|
927
|
+
if (root.loading !== undefined)
|
|
928
|
+
mergedDefaults.loading = root.loading;
|
|
929
|
+
const defaults = parseDefaults(mergedDefaults, loadingPresets);
|
|
498
930
|
const menu = parseMenu(root.menu);
|
|
499
|
-
const categories = parseCategories(root.categories);
|
|
931
|
+
const { categories, categoryLoading } = parseCategories(root.categories, loadingPresets);
|
|
500
932
|
const manuals = await parseManuals(root.manuals);
|
|
501
933
|
const out = [];
|
|
502
934
|
for (const [id, value] of Object.entries(root)) {
|
|
@@ -509,9 +941,9 @@ export async function loadCommandsConfig() {
|
|
|
509
941
|
}));
|
|
510
942
|
continue;
|
|
511
943
|
}
|
|
512
|
-
const spec = await parseEntry(id, value);
|
|
944
|
+
const spec = await parseEntry(id, value, loadingPresets, validPluginKeys);
|
|
513
945
|
if (spec)
|
|
514
946
|
out.push(spec);
|
|
515
947
|
}
|
|
516
|
-
return { defaults, menu, categories, manuals, specs: out };
|
|
948
|
+
return { prefix, defaults, menu, categories, manuals, loadingPresets, categoryLoading, specs: out };
|
|
517
949
|
}
|