@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.
- 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 +207 -8
- package/dist/drivers/baileys/api/index.js +300 -130
- 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 +75 -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,517 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
import { logger } from "#logger";
|
|
5
|
+
import { t } from "#i18n";
|
|
6
|
+
import { PATHS } from "#config";
|
|
7
|
+
const COMMANDS_FILE = path.join(PATHS.HOME, "commands.yaml");
|
|
8
|
+
function asString(value) {
|
|
9
|
+
if (typeof value !== "string")
|
|
10
|
+
return null;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
13
|
+
}
|
|
14
|
+
function asAliasList(value) {
|
|
15
|
+
if (value === undefined || value === null)
|
|
16
|
+
return [];
|
|
17
|
+
if (!Array.isArray(value))
|
|
18
|
+
return [];
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const item of value) {
|
|
21
|
+
if (typeof item !== "string")
|
|
22
|
+
continue;
|
|
23
|
+
const trimmed = item.trim();
|
|
24
|
+
if (trimmed.length > 0)
|
|
25
|
+
out.push(trimmed);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function asImportList(value) {
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return trimmed.length > 0 ? [trimmed] : [];
|
|
33
|
+
}
|
|
34
|
+
return asAliasList(value);
|
|
35
|
+
}
|
|
36
|
+
function asBool(value) {
|
|
37
|
+
if (typeof value === "boolean")
|
|
38
|
+
return value;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function asPositiveInt(value, fallback) {
|
|
42
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
43
|
+
return fallback;
|
|
44
|
+
return Math.floor(value);
|
|
45
|
+
}
|
|
46
|
+
export function parseLocalizedString(raw) {
|
|
47
|
+
if (typeof raw === "string") {
|
|
48
|
+
const trimmed = raw.trim();
|
|
49
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
50
|
+
}
|
|
51
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const [lang, val] of Object.entries(raw)) {
|
|
54
|
+
if (typeof val === "string") {
|
|
55
|
+
const trimmed = val.trim();
|
|
56
|
+
if (trimmed.length > 0)
|
|
57
|
+
out[lang] = trimmed;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
export async function resolveFileRef(value) {
|
|
65
|
+
if (!value)
|
|
66
|
+
return null;
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
const trimmed = value.trim();
|
|
69
|
+
if (trimmed.startsWith("file:")) {
|
|
70
|
+
const relativePath = trimmed.slice(5).trim();
|
|
71
|
+
const fullPath = path.resolve(PATHS.HOME, relativePath);
|
|
72
|
+
try {
|
|
73
|
+
return await fs.readFile(fullPath, "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
const err = e;
|
|
77
|
+
logger.warn(`commandsConfig: failed to read file ref "${trimmed}" (${fullPath}): ${err.message}`);
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return trimmed;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === "object" && value !== null) {
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const [lang, val] of Object.entries(value)) {
|
|
86
|
+
if (typeof val === "string") {
|
|
87
|
+
const resolved = await resolveFileRef(val);
|
|
88
|
+
if (resolved && typeof resolved === "string") {
|
|
89
|
+
out[lang] = resolved;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function parseGroupUserList(raw) {
|
|
98
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
99
|
+
return undefined;
|
|
100
|
+
const obj = raw;
|
|
101
|
+
const groups = asAliasList(obj.groups);
|
|
102
|
+
const users = asAliasList(obj.users);
|
|
103
|
+
if (groups.length === 0 && users.length === 0)
|
|
104
|
+
return undefined;
|
|
105
|
+
return {
|
|
106
|
+
groups: groups.length > 0 ? groups : undefined,
|
|
107
|
+
users: users.length > 0 ? users : undefined,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function parsePermissions(raw) {
|
|
111
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
112
|
+
return null;
|
|
113
|
+
const obj = raw;
|
|
114
|
+
const admin = asBool(obj.admin) ?? undefined;
|
|
115
|
+
const botAdmin = asBool(obj.botAdmin) ?? undefined;
|
|
116
|
+
const owner = asBool(obj.owner) ?? undefined;
|
|
117
|
+
let scope = undefined;
|
|
118
|
+
const scopeStr = asString(obj.scope)?.toLowerCase();
|
|
119
|
+
if (scopeStr === "group" || scopeStr === "dm" || scopeStr === "any") {
|
|
120
|
+
scope = scopeStr;
|
|
121
|
+
}
|
|
122
|
+
let cooldownSeconds = undefined;
|
|
123
|
+
if (typeof obj.cooldownSeconds === "number" && Number.isFinite(obj.cooldownSeconds) && obj.cooldownSeconds >= 0) {
|
|
124
|
+
cooldownSeconds = obj.cooldownSeconds;
|
|
125
|
+
}
|
|
126
|
+
const whitelist = parseGroupUserList(obj.whitelist);
|
|
127
|
+
const blacklist = parseGroupUserList(obj.blacklist);
|
|
128
|
+
if (admin === undefined &&
|
|
129
|
+
botAdmin === undefined &&
|
|
130
|
+
owner === undefined &&
|
|
131
|
+
scope === undefined &&
|
|
132
|
+
cooldownSeconds === undefined &&
|
|
133
|
+
whitelist === undefined &&
|
|
134
|
+
blacklist === undefined) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
admin,
|
|
139
|
+
botAdmin,
|
|
140
|
+
owner,
|
|
141
|
+
scope,
|
|
142
|
+
cooldownSeconds,
|
|
143
|
+
whitelist,
|
|
144
|
+
blacklist,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function parseMessages(raw) {
|
|
148
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
149
|
+
return null;
|
|
150
|
+
const obj = raw;
|
|
151
|
+
const botNotAdmin = asString(obj.botNotAdmin) ?? undefined;
|
|
152
|
+
const senderNotAdmin = asString(obj.senderNotAdmin) ?? undefined;
|
|
153
|
+
const ownerOnly = asString(obj.ownerOnly) ?? undefined;
|
|
154
|
+
const wrongScope = asString(obj.wrongScope) ?? undefined;
|
|
155
|
+
const cooldown = asString(obj.cooldown) ?? undefined;
|
|
156
|
+
if (!botNotAdmin && !senderNotAdmin && !ownerOnly && !wrongScope && !cooldown) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
botNotAdmin,
|
|
161
|
+
senderNotAdmin,
|
|
162
|
+
ownerOnly,
|
|
163
|
+
wrongScope,
|
|
164
|
+
cooldown,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const ARGUMENT_TYPES = new Set([
|
|
168
|
+
"mention", "url", "media_direct", "media_reply", "number",
|
|
169
|
+
"duration", "choice", "boolean", "quoted_text", "reply",
|
|
170
|
+
]);
|
|
171
|
+
function parseArgument(raw, parentId) {
|
|
172
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
173
|
+
return null;
|
|
174
|
+
const obj = raw;
|
|
175
|
+
const name = asString(obj.name);
|
|
176
|
+
if (!name) {
|
|
177
|
+
logger.warn(t("system.commandsConfigArgumentMissingName", { id: parentId }));
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const typeStr = asString(obj.type);
|
|
181
|
+
if (!typeStr) {
|
|
182
|
+
logger.warn(t("system.commandsConfigArgumentMissingType", { id: parentId, name }));
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (!ARGUMENT_TYPES.has(typeStr)) {
|
|
186
|
+
logger.warn(t("system.commandsConfigUnknownArgType", { id: parentId, name, type: typeStr }));
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const required = asBool(obj.required) ?? false;
|
|
190
|
+
let choices;
|
|
191
|
+
if (typeStr === "choice") {
|
|
192
|
+
choices = asAliasList(obj.choices);
|
|
193
|
+
if (choices.length === 0) {
|
|
194
|
+
logger.warn(t("system.commandsConfigChoiceArgumentNoChoices", { id: parentId, name }));
|
|
195
|
+
// Still allow the argument; renderUsage just falls back to "<name>".
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
name,
|
|
200
|
+
type: typeStr,
|
|
201
|
+
required,
|
|
202
|
+
choices,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function parseArguments(raw, parentId) {
|
|
206
|
+
if (raw === undefined || raw === null)
|
|
207
|
+
return [];
|
|
208
|
+
if (!Array.isArray(raw)) {
|
|
209
|
+
logger.warn(t("system.commandsConfigArgumentsNotList", { id: parentId }));
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const item of raw) {
|
|
214
|
+
const arg = parseArgument(item, parentId);
|
|
215
|
+
if (arg)
|
|
216
|
+
out.push(arg);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
async function parseSubcommand(parentId, raw) {
|
|
221
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
222
|
+
return null;
|
|
223
|
+
const obj = raw;
|
|
224
|
+
const cmd = asString(obj.cmd);
|
|
225
|
+
if (!cmd) {
|
|
226
|
+
logger.warn(t("system.commandsConfigSubcommandMissingCmd", { id: parentId }));
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const id = `${parentId}::${cmd}`;
|
|
230
|
+
const rawManual = parseLocalizedString(obj.manual);
|
|
231
|
+
const manual = rawManual ? await resolveFileRef(rawManual) : null;
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
cmd,
|
|
235
|
+
aliases: asAliasList(obj.aliases),
|
|
236
|
+
function: asString(obj.function),
|
|
237
|
+
desc: parseLocalizedString(obj.desc),
|
|
238
|
+
manual,
|
|
239
|
+
arguments: parseArguments(obj.arguments, id),
|
|
240
|
+
permissions: parsePermissions(obj.permissions),
|
|
241
|
+
messages: parseMessages(obj.messages),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async function parseSubcommands(raw, parentId) {
|
|
245
|
+
if (raw === undefined || raw === null)
|
|
246
|
+
return [];
|
|
247
|
+
if (!Array.isArray(raw)) {
|
|
248
|
+
logger.warn(t("system.commandsConfigSubcommandsNotList", { id: parentId }));
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
const out = [];
|
|
252
|
+
for (const item of raw) {
|
|
253
|
+
const sub = await parseSubcommand(parentId, item);
|
|
254
|
+
if (sub)
|
|
255
|
+
out.push(sub);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
const DEFAULT_MENU_CONFIG = {
|
|
260
|
+
title: "🤖 ManyBot — Menu",
|
|
261
|
+
intro: {
|
|
262
|
+
en: "Use {prefix}<command> to run it or {prefix}help <command> to view its manual.",
|
|
263
|
+
pt: "Use {prefix}<comando> para executar ou {prefix}help <comando> para ver o manual.",
|
|
264
|
+
es: "Usa {prefix}<comando> para ejecutarlo o {prefix}help <comando> para ver el manual.",
|
|
265
|
+
},
|
|
266
|
+
footer: null,
|
|
267
|
+
cmd: "menu",
|
|
268
|
+
aliases: ["help", "man", "menu", "bot", "?"],
|
|
269
|
+
notFoundFallback: false,
|
|
270
|
+
welcomeMessage: null,
|
|
271
|
+
welcomeWindowDays: 3,
|
|
272
|
+
pageSize: 15,
|
|
273
|
+
};
|
|
274
|
+
function parseMenu(raw) {
|
|
275
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
276
|
+
return { ...DEFAULT_MENU_CONFIG };
|
|
277
|
+
}
|
|
278
|
+
const obj = raw;
|
|
279
|
+
const aliases = asAliasList(obj.aliases);
|
|
280
|
+
return {
|
|
281
|
+
title: parseLocalizedString(obj.title) ?? DEFAULT_MENU_CONFIG.title,
|
|
282
|
+
intro: parseLocalizedString(obj.intro) ?? DEFAULT_MENU_CONFIG.intro,
|
|
283
|
+
footer: parseLocalizedString(obj.footer) ?? DEFAULT_MENU_CONFIG.footer,
|
|
284
|
+
cmd: asString(obj.cmd) ?? DEFAULT_MENU_CONFIG.cmd,
|
|
285
|
+
aliases: aliases.length > 0 ? aliases : [...DEFAULT_MENU_CONFIG.aliases],
|
|
286
|
+
notFoundFallback: asBool(obj.notFoundFallback) ?? DEFAULT_MENU_CONFIG.notFoundFallback,
|
|
287
|
+
welcomeMessage: parseLocalizedString(obj.welcomeMessage) ?? DEFAULT_MENU_CONFIG.welcomeMessage,
|
|
288
|
+
welcomeWindowDays: asPositiveInt(obj.welcomeWindowDays, DEFAULT_MENU_CONFIG.welcomeWindowDays),
|
|
289
|
+
pageSize: asPositiveInt(obj.pageSize, DEFAULT_MENU_CONFIG.pageSize),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function parseScopeValue(raw) {
|
|
293
|
+
const s = asString(raw)?.toLowerCase();
|
|
294
|
+
if (s === "group" || s === "dm" || s === "any")
|
|
295
|
+
return s;
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
function parseCategories(raw) {
|
|
299
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
300
|
+
return {};
|
|
301
|
+
const obj = raw;
|
|
302
|
+
const out = {};
|
|
303
|
+
for (const [catKey, value] of Object.entries(obj)) {
|
|
304
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
305
|
+
continue;
|
|
306
|
+
const catObj = value;
|
|
307
|
+
const label = parseLocalizedString(catObj.label) ?? catKey;
|
|
308
|
+
const order = typeof catObj.order === "number" && Number.isFinite(catObj.order) ? catObj.order : 999;
|
|
309
|
+
const scope = parseScopeValue(catObj.scope);
|
|
310
|
+
const hiddenInScope = parseScopeValue(catObj.hiddenInScope);
|
|
311
|
+
out[catKey] = {
|
|
312
|
+
label,
|
|
313
|
+
order,
|
|
314
|
+
scope: scope ?? null,
|
|
315
|
+
hiddenInScope: hiddenInScope ?? null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
async function parseManuals(raw) {
|
|
321
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
322
|
+
return {};
|
|
323
|
+
const obj = raw;
|
|
324
|
+
const out = {};
|
|
325
|
+
for (const [id, value] of Object.entries(obj)) {
|
|
326
|
+
const parsed = parseLocalizedString(value);
|
|
327
|
+
if (parsed) {
|
|
328
|
+
const resolved = await resolveFileRef(parsed);
|
|
329
|
+
if (resolved)
|
|
330
|
+
out[id] = resolved;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
const DEFAULT_NOTIFY_CHANGES = true;
|
|
336
|
+
const DEFAULT_NOTIFY_PERIOD_DAYS = 7;
|
|
337
|
+
function parseDefaults(raw) {
|
|
338
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
339
|
+
return {
|
|
340
|
+
notifyChanges: DEFAULT_NOTIFY_CHANGES,
|
|
341
|
+
notifyPeriodDays: DEFAULT_NOTIFY_PERIOD_DAYS,
|
|
342
|
+
notifyMessage: null,
|
|
343
|
+
permissions: null,
|
|
344
|
+
messages: null,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const obj = raw;
|
|
348
|
+
return {
|
|
349
|
+
notifyChanges: asBool(obj.notifyChanges) ?? DEFAULT_NOTIFY_CHANGES,
|
|
350
|
+
notifyPeriodDays: asPositiveInt(obj.notifyPeriodDays, DEFAULT_NOTIFY_PERIOD_DAYS),
|
|
351
|
+
notifyMessage: asString(obj.notifyMessage),
|
|
352
|
+
permissions: parsePermissions(obj.permissions),
|
|
353
|
+
messages: parseMessages(obj.messages),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async function parseEntry(id, raw) {
|
|
357
|
+
const cmd = asString(raw.cmd);
|
|
358
|
+
if (!cmd) {
|
|
359
|
+
logger.warn(t("system.commandsConfigMissingCmd", {
|
|
360
|
+
id,
|
|
361
|
+
path: COMMANDS_FILE
|
|
362
|
+
}));
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const rawText = parseLocalizedString(raw.text);
|
|
366
|
+
const text = rawText ? await resolveFileRef(rawText) : null;
|
|
367
|
+
const rawManual = parseLocalizedString(raw.manual);
|
|
368
|
+
const manual = rawManual ? await resolveFileRef(rawManual) : null;
|
|
369
|
+
return {
|
|
370
|
+
id,
|
|
371
|
+
cmd,
|
|
372
|
+
aliases: asAliasList(raw.aliases),
|
|
373
|
+
plugin: asString(raw.plugin),
|
|
374
|
+
function: asString(raw.function),
|
|
375
|
+
text,
|
|
376
|
+
desc: parseLocalizedString(raw.desc),
|
|
377
|
+
category: asString(raw.category),
|
|
378
|
+
group: asString(raw.group),
|
|
379
|
+
manual,
|
|
380
|
+
deprecatedMessage: asString(raw.deprecatedMessage),
|
|
381
|
+
notifyChanges: asBool(raw.notifyChanges),
|
|
382
|
+
permissions: parsePermissions(raw.permissions),
|
|
383
|
+
messages: parseMessages(raw.messages),
|
|
384
|
+
arguments: parseArguments(raw.arguments, id),
|
|
385
|
+
subcommands: await parseSubcommands(raw.subcommands, id),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const RESERVED_KEYS = new Set(["defaults", "menu", "categories", "manuals", "import"]);
|
|
389
|
+
/**
|
|
390
|
+
* Resolves `import:` (a path or list of paths, relative to PATHS.HOME) by
|
|
391
|
+
* reading each referenced YAML file and folding its top-level sections
|
|
392
|
+
* into the main root object. Each top-level key (`menu`, `manuals`, a
|
|
393
|
+
* command id, ...) may be owned by exactly one source file — no deep
|
|
394
|
+
* merge, first owner wins and any later collision is reported as an
|
|
395
|
+
* error and skipped, keeping the rest of that import file usable.
|
|
396
|
+
*/
|
|
397
|
+
async function resolveImports(root) {
|
|
398
|
+
const importPaths = asImportList(root.import);
|
|
399
|
+
if (importPaths.length === 0)
|
|
400
|
+
return root;
|
|
401
|
+
const merged = { ...root };
|
|
402
|
+
delete merged.import;
|
|
403
|
+
const owner = new Map();
|
|
404
|
+
for (const key of Object.keys(merged))
|
|
405
|
+
owner.set(key, COMMANDS_FILE);
|
|
406
|
+
for (const relPath of importPaths) {
|
|
407
|
+
const fullPath = path.resolve(PATHS.HOME, relPath);
|
|
408
|
+
let raw;
|
|
409
|
+
try {
|
|
410
|
+
raw = await fs.readFile(fullPath, "utf8");
|
|
411
|
+
}
|
|
412
|
+
catch (e) {
|
|
413
|
+
const err = e;
|
|
414
|
+
logger.error(t("system.commandsConfigImportReadFailed", { path: fullPath, message: err.message }));
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
let parsed;
|
|
418
|
+
try {
|
|
419
|
+
parsed = yaml.load(raw, { filename: fullPath });
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
const err = e;
|
|
423
|
+
logger.error(t("system.commandsConfigImportParseFailed", { path: fullPath, message: err.message }));
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (parsed === null || parsed === undefined)
|
|
427
|
+
continue;
|
|
428
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
429
|
+
logger.error(t("system.commandsConfigImportInvalidRoot", { path: fullPath }));
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
433
|
+
if (key === "import") {
|
|
434
|
+
logger.warn(t("system.commandsConfigImportNested", { path: fullPath }));
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const existingOwner = owner.get(key);
|
|
438
|
+
if (existingOwner !== undefined) {
|
|
439
|
+
logger.error(t("system.commandsConfigImportKeyConflict", { key, path: fullPath, owner: existingOwner }));
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
merged[key] = value;
|
|
443
|
+
owner.set(key, fullPath);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return merged;
|
|
447
|
+
}
|
|
448
|
+
export async function loadCommandsConfig() {
|
|
449
|
+
let raw;
|
|
450
|
+
try {
|
|
451
|
+
raw = await fs.readFile(COMMANDS_FILE, "utf8");
|
|
452
|
+
}
|
|
453
|
+
catch (e) {
|
|
454
|
+
const err = e;
|
|
455
|
+
if (err.code === "ENOENT")
|
|
456
|
+
return null;
|
|
457
|
+
logger.error(t("system.commandsConfigReadFailed", {
|
|
458
|
+
path: COMMANDS_FILE,
|
|
459
|
+
message: err.message
|
|
460
|
+
}));
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
let parsed;
|
|
464
|
+
try {
|
|
465
|
+
parsed = yaml.load(raw, { filename: COMMANDS_FILE });
|
|
466
|
+
}
|
|
467
|
+
catch (e) {
|
|
468
|
+
const err = e;
|
|
469
|
+
logger.error(t("system.commandsConfigParseFailed", {
|
|
470
|
+
path: COMMANDS_FILE,
|
|
471
|
+
message: err.message
|
|
472
|
+
}));
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
if (parsed === null || parsed === undefined) {
|
|
476
|
+
return {
|
|
477
|
+
defaults: {
|
|
478
|
+
notifyChanges: DEFAULT_NOTIFY_CHANGES,
|
|
479
|
+
notifyPeriodDays: DEFAULT_NOTIFY_PERIOD_DAYS,
|
|
480
|
+
notifyMessage: null,
|
|
481
|
+
permissions: null,
|
|
482
|
+
messages: null,
|
|
483
|
+
},
|
|
484
|
+
menu: { ...DEFAULT_MENU_CONFIG },
|
|
485
|
+
categories: {},
|
|
486
|
+
manuals: {},
|
|
487
|
+
specs: [],
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
491
|
+
logger.error(t("system.commandsConfigInvalidRoot", {
|
|
492
|
+
path: COMMANDS_FILE
|
|
493
|
+
}));
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
const root = await resolveImports(parsed);
|
|
497
|
+
const defaults = parseDefaults(root.defaults);
|
|
498
|
+
const menu = parseMenu(root.menu);
|
|
499
|
+
const categories = parseCategories(root.categories);
|
|
500
|
+
const manuals = await parseManuals(root.manuals);
|
|
501
|
+
const out = [];
|
|
502
|
+
for (const [id, value] of Object.entries(root)) {
|
|
503
|
+
if (RESERVED_KEYS.has(id))
|
|
504
|
+
continue;
|
|
505
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
506
|
+
logger.warn(t("system.commandsConfigInvalidEntry", {
|
|
507
|
+
id,
|
|
508
|
+
path: COMMANDS_FILE
|
|
509
|
+
}));
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const spec = await parseEntry(id, value);
|
|
513
|
+
if (spec)
|
|
514
|
+
out.push(spec);
|
|
515
|
+
}
|
|
516
|
+
return { defaults, menu, categories, manuals, specs: out };
|
|
517
|
+
}
|