@whanext/core 0.8.0 → 0.11.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/CHANGELOG.md +52 -0
- package/README.md +198 -44
- package/dist/index.d.ts +316 -85
- package/dist/index.js +955 -160
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
var MemoryCache = class {
|
|
3
3
|
#entries = /* @__PURE__ */ new Map();
|
|
4
4
|
#maxEntries;
|
|
5
|
+
#hits = 0;
|
|
6
|
+
#misses = 0;
|
|
7
|
+
#sets = 0;
|
|
8
|
+
#evictions = 0;
|
|
9
|
+
#expirations = 0;
|
|
5
10
|
constructor(options = {}) {
|
|
6
11
|
this.#maxEntries = Math.max(1, options.maxEntries ?? 1e3);
|
|
7
12
|
}
|
|
8
13
|
async get(key) {
|
|
9
14
|
const entry = this.#entries.get(key);
|
|
10
15
|
if (!entry) {
|
|
16
|
+
this.#misses += 1;
|
|
11
17
|
return void 0;
|
|
12
18
|
}
|
|
13
19
|
if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) {
|
|
14
20
|
this.#entries.delete(key);
|
|
21
|
+
this.#misses += 1;
|
|
22
|
+
this.#expirations += 1;
|
|
15
23
|
return void 0;
|
|
16
24
|
}
|
|
25
|
+
this.#hits += 1;
|
|
17
26
|
this.#entries.delete(key);
|
|
18
27
|
this.#entries.set(key, entry);
|
|
19
28
|
return entry.value;
|
|
@@ -23,11 +32,14 @@ var MemoryCache = class {
|
|
|
23
32
|
if (ttlMs !== void 0) {
|
|
24
33
|
entry.expiresAt = Date.now() + ttlMs;
|
|
25
34
|
}
|
|
35
|
+
this.#sets += 1;
|
|
36
|
+
this.#entries.delete(key);
|
|
26
37
|
this.#entries.set(key, entry);
|
|
27
38
|
while (this.#entries.size > this.#maxEntries) {
|
|
28
39
|
const oldest = this.#entries.keys().next().value;
|
|
29
40
|
if (oldest === void 0) return;
|
|
30
41
|
this.#entries.delete(oldest);
|
|
42
|
+
this.#evictions += 1;
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
async delete(key) {
|
|
@@ -36,6 +48,28 @@ var MemoryCache = class {
|
|
|
36
48
|
async clear() {
|
|
37
49
|
this.#entries.clear();
|
|
38
50
|
}
|
|
51
|
+
prune(now = Date.now()) {
|
|
52
|
+
let removed = 0;
|
|
53
|
+
for (const [key, entry] of this.#entries) {
|
|
54
|
+
if (entry.expiresAt !== void 0 && entry.expiresAt <= now) {
|
|
55
|
+
this.#entries.delete(key);
|
|
56
|
+
removed += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.#expirations += removed;
|
|
60
|
+
return removed;
|
|
61
|
+
}
|
|
62
|
+
stats() {
|
|
63
|
+
return {
|
|
64
|
+
size: this.#entries.size,
|
|
65
|
+
maxEntries: this.#maxEntries,
|
|
66
|
+
hits: this.#hits,
|
|
67
|
+
misses: this.#misses,
|
|
68
|
+
sets: this.#sets,
|
|
69
|
+
evictions: this.#evictions,
|
|
70
|
+
expirations: this.#expirations
|
|
71
|
+
};
|
|
72
|
+
}
|
|
39
73
|
};
|
|
40
74
|
|
|
41
75
|
// src/errors/error.ts
|
|
@@ -298,94 +332,841 @@ var ArgsParser = class {
|
|
|
298
332
|
}
|
|
299
333
|
};
|
|
300
334
|
|
|
335
|
+
// src/commands/command.ts
|
|
336
|
+
function defineCommand(command) {
|
|
337
|
+
return command;
|
|
338
|
+
}
|
|
339
|
+
function defineSubcommand(command) {
|
|
340
|
+
return command;
|
|
341
|
+
}
|
|
342
|
+
function defineCommandGroup(group) {
|
|
343
|
+
return group;
|
|
344
|
+
}
|
|
345
|
+
function defineCommands(...commands) {
|
|
346
|
+
return commands;
|
|
347
|
+
}
|
|
348
|
+
function isCommandGroup(definition) {
|
|
349
|
+
return "subcommands" in definition;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/commands/concurrency.ts
|
|
353
|
+
var CommandConcurrencyController = class {
|
|
354
|
+
#states = /* @__PURE__ */ new Map();
|
|
355
|
+
async run(key, options, execute) {
|
|
356
|
+
const strategy = options?.strategy ?? "parallel";
|
|
357
|
+
if (strategy === "parallel") {
|
|
358
|
+
await execute(new AbortController().signal);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const max = Math.max(1, options?.max ?? 1);
|
|
362
|
+
const state = this.#states.get(key) ?? { active: 0, queue: [], controllers: /* @__PURE__ */ new Set() };
|
|
363
|
+
this.#states.set(key, state);
|
|
364
|
+
if (strategy === "replace") {
|
|
365
|
+
for (const controller2 of state.controllers) controller2.abort();
|
|
366
|
+
state.controllers.clear();
|
|
367
|
+
} else if (strategy === "reject" && state.active >= max) {
|
|
368
|
+
throw new WhaNextError("COMMAND_BUSY", "This command is already running.", {
|
|
369
|
+
context: { key, max },
|
|
370
|
+
recoverable: true
|
|
371
|
+
});
|
|
372
|
+
} else if (strategy === "queue" && state.active >= max) {
|
|
373
|
+
await new Promise((resolve2) => state.queue.push(resolve2));
|
|
374
|
+
}
|
|
375
|
+
const controller = new AbortController();
|
|
376
|
+
state.controllers.add(controller);
|
|
377
|
+
state.active += 1;
|
|
378
|
+
try {
|
|
379
|
+
await execute(controller.signal);
|
|
380
|
+
} finally {
|
|
381
|
+
state.controllers.delete(controller);
|
|
382
|
+
state.active -= 1;
|
|
383
|
+
state.queue.shift()?.();
|
|
384
|
+
if (state.active === 0 && state.queue.length === 0) {
|
|
385
|
+
this.#states.delete(key);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/commands/context.ts
|
|
392
|
+
var CommandContextImplementation = class {
|
|
393
|
+
message;
|
|
394
|
+
user;
|
|
395
|
+
chat;
|
|
396
|
+
group;
|
|
397
|
+
command;
|
|
398
|
+
commands;
|
|
399
|
+
prefix;
|
|
400
|
+
options;
|
|
401
|
+
args;
|
|
402
|
+
locale;
|
|
403
|
+
signal;
|
|
404
|
+
client;
|
|
405
|
+
messages;
|
|
406
|
+
mediaService;
|
|
407
|
+
groups;
|
|
408
|
+
members;
|
|
409
|
+
chats;
|
|
410
|
+
users;
|
|
411
|
+
muteService;
|
|
412
|
+
#lastReply;
|
|
413
|
+
constructor(options) {
|
|
414
|
+
this.message = options.message;
|
|
415
|
+
this.user = options.message.sender;
|
|
416
|
+
this.chat = { id: options.message.chatId, isGroup: options.message.isGroup };
|
|
417
|
+
this.command = options.command;
|
|
418
|
+
this.commands = options.commands;
|
|
419
|
+
this.prefix = options.commands.prefix;
|
|
420
|
+
this.options = options.options;
|
|
421
|
+
this.args = options.args;
|
|
422
|
+
this.locale = options.locale;
|
|
423
|
+
this.signal = options.signal;
|
|
424
|
+
this.client = options.services;
|
|
425
|
+
this.messages = options.services.messages;
|
|
426
|
+
this.mediaService = options.services.media;
|
|
427
|
+
this.groups = options.services.groups;
|
|
428
|
+
this.members = options.services.members;
|
|
429
|
+
this.chats = options.services.chats;
|
|
430
|
+
this.users = options.services.users;
|
|
431
|
+
this.muteService = options.services.mute;
|
|
432
|
+
this.group = options.message.isGroup ? this.#createGroupContext(options.message) : void 0;
|
|
433
|
+
Object.assign(this, options.message);
|
|
434
|
+
}
|
|
435
|
+
async reply(content, options = {}) {
|
|
436
|
+
const sent = await this.messages.reply(this.message, normalizeContent(content));
|
|
437
|
+
this.#lastReply = sent;
|
|
438
|
+
this.#scheduleDeletion(sent, options.deleteAfterMs);
|
|
439
|
+
return sent;
|
|
440
|
+
}
|
|
441
|
+
async defer(content = "\u23F3 _Processando..._") {
|
|
442
|
+
const sent = await this.reply(content);
|
|
443
|
+
return new DeferredReply(this.messages, sent, (message) => {
|
|
444
|
+
this.#lastReply = message;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async edit(content) {
|
|
448
|
+
if (!this.#lastReply) {
|
|
449
|
+
throw new WhaNextError(
|
|
450
|
+
"MESSAGE_NOT_FOUND",
|
|
451
|
+
"There is no command reply to edit. Call reply() or defer() first."
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const edited = await this.messages.edit(this.#lastReply, content);
|
|
455
|
+
this.#lastReply = edited;
|
|
456
|
+
return edited;
|
|
457
|
+
}
|
|
458
|
+
react(emoji) {
|
|
459
|
+
return this.messages.react(this.message, emoji);
|
|
460
|
+
}
|
|
461
|
+
unreact() {
|
|
462
|
+
return this.messages.unreact(this.message);
|
|
463
|
+
}
|
|
464
|
+
delete() {
|
|
465
|
+
return this.messages.delete(this.message);
|
|
466
|
+
}
|
|
467
|
+
async deleteReply(options = {}) {
|
|
468
|
+
if (!this.#lastReply) return;
|
|
469
|
+
if (options.deleteAfterMs !== void 0 && options.deleteAfterMs > 0) {
|
|
470
|
+
this.#scheduleDeletion(this.#lastReply, options.deleteAfterMs);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
await this.messages.delete(this.#lastReply);
|
|
474
|
+
this.#lastReply = void 0;
|
|
475
|
+
}
|
|
476
|
+
#createGroupContext(message) {
|
|
477
|
+
return {
|
|
478
|
+
id: message.chatId,
|
|
479
|
+
metadata: (refresh) => this.groups.metadata(message.chatId, refresh),
|
|
480
|
+
isUserAdmin: () => this.groups.isAdmin(message.chatId, message.senderIds),
|
|
481
|
+
isBotAdmin: () => this.groups.isCurrentUserAdmin(message.chatId)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
#scheduleDeletion(message, delayMs) {
|
|
485
|
+
if (delayMs === void 0 || delayMs <= 0) return;
|
|
486
|
+
const timer = setTimeout(() => {
|
|
487
|
+
void this.messages.delete(message).catch(() => void 0);
|
|
488
|
+
}, delayMs);
|
|
489
|
+
timer.unref?.();
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
var DeferredReply = class {
|
|
493
|
+
#messages;
|
|
494
|
+
#message;
|
|
495
|
+
#onEdit;
|
|
496
|
+
constructor(messages, message, onEdit) {
|
|
497
|
+
this.#messages = messages;
|
|
498
|
+
this.#message = message;
|
|
499
|
+
this.#onEdit = onEdit;
|
|
500
|
+
}
|
|
501
|
+
async edit(content) {
|
|
502
|
+
this.#message = await this.#messages.edit(this.#message, content);
|
|
503
|
+
this.#onEdit(this.#message);
|
|
504
|
+
return this.#message;
|
|
505
|
+
}
|
|
506
|
+
delete() {
|
|
507
|
+
return this.#messages.delete(this.#message);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
function createCommandContext(options) {
|
|
511
|
+
return new CommandContextImplementation(options);
|
|
512
|
+
}
|
|
513
|
+
function normalizeContent(content) {
|
|
514
|
+
return typeof content === "string" ? { text: content } : content;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/commands/load-commands.ts
|
|
518
|
+
import { readdir } from "fs/promises";
|
|
519
|
+
import path from "path";
|
|
520
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
521
|
+
var DEFAULT_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
|
|
522
|
+
async function loadCommands(registrar, dirPath, options = {}) {
|
|
523
|
+
const extensions = options.extensions ?? DEFAULT_EXTENSIONS;
|
|
524
|
+
const recursive = options.recursive ?? true;
|
|
525
|
+
const resolvedDirPath = dirPath instanceof URL ? fileURLToPath(dirPath) : dirPath;
|
|
526
|
+
const entries = await readEntries(resolvedDirPath, recursive);
|
|
527
|
+
const loaded = [];
|
|
528
|
+
const skipped = [];
|
|
529
|
+
const commands = [];
|
|
530
|
+
for (const filePath of entries) {
|
|
531
|
+
if (isDeclarationFile(filePath) || !extensions.includes(path.extname(filePath))) {
|
|
532
|
+
skipped.push(filePath);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const definitions = await importCommands(filePath);
|
|
536
|
+
for (const definition of definitions) {
|
|
537
|
+
registrar.command(definition);
|
|
538
|
+
commands.push({ name: definition.name, filePath });
|
|
539
|
+
}
|
|
540
|
+
loaded.push(filePath);
|
|
541
|
+
}
|
|
542
|
+
return { loaded, skipped, commands };
|
|
543
|
+
}
|
|
544
|
+
async function readEntries(dirPath, recursive) {
|
|
545
|
+
let dirents;
|
|
546
|
+
try {
|
|
547
|
+
dirents = await readdir(dirPath, { recursive, withFileTypes: true });
|
|
548
|
+
} catch (error) {
|
|
549
|
+
throw new WhaNextError("COMMAND_LOAD_FAILED", `Could not read the commands directory "${dirPath}".`, {
|
|
550
|
+
cause: error,
|
|
551
|
+
context: { dirPath }
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return dirents.filter((dirent) => dirent.isFile()).map((dirent) => path.join(dirent.parentPath, dirent.name)).sort((left, right) => left.localeCompare(right));
|
|
555
|
+
}
|
|
556
|
+
async function importCommands(filePath) {
|
|
557
|
+
let module;
|
|
558
|
+
try {
|
|
559
|
+
module = await import(pathToFileURL(filePath).href);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
throw new WhaNextError("COMMAND_LOAD_FAILED", `Could not import the command file "${filePath}".`, {
|
|
562
|
+
cause: error,
|
|
563
|
+
context: { filePath }
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const definitions = [];
|
|
567
|
+
const seen = /* @__PURE__ */ new Set();
|
|
568
|
+
const exports = [
|
|
569
|
+
module.default,
|
|
570
|
+
...Object.entries(module).filter(([name]) => name !== "default").map(([, value]) => value)
|
|
571
|
+
];
|
|
572
|
+
for (const value of exports) {
|
|
573
|
+
const candidates = Array.isArray(value) ? value : [value];
|
|
574
|
+
for (const candidate of candidates) {
|
|
575
|
+
if (isCommandDefinition(candidate) && !seen.has(candidate)) {
|
|
576
|
+
seen.add(candidate);
|
|
577
|
+
definitions.push(candidate);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (definitions.length === 0) {
|
|
582
|
+
throw new WhaNextError("COMMAND_LOAD_FAILED", `The file "${filePath}" does not export any valid commands.`, {
|
|
583
|
+
context: { filePath }
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return definitions;
|
|
587
|
+
}
|
|
588
|
+
function isCommandDefinition(value) {
|
|
589
|
+
const candidate = value;
|
|
590
|
+
return typeof value === "object" && value !== null && typeof candidate.name === "string" && (typeof candidate.execute === "function" || Array.isArray(candidate.subcommands));
|
|
591
|
+
}
|
|
592
|
+
function isDeclarationFile(filePath) {
|
|
593
|
+
return /\.d\.(?:ts|mts|cts)$/.test(filePath);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/commands/options.ts
|
|
597
|
+
var option = {
|
|
598
|
+
string(definition) {
|
|
599
|
+
return { kind: "string", ...definition };
|
|
600
|
+
},
|
|
601
|
+
number(definition) {
|
|
602
|
+
return { kind: "number", ...definition };
|
|
603
|
+
},
|
|
604
|
+
boolean(definition) {
|
|
605
|
+
return { kind: "boolean", ...definition };
|
|
606
|
+
},
|
|
607
|
+
user(definition) {
|
|
608
|
+
return { kind: "user", ...definition };
|
|
609
|
+
},
|
|
610
|
+
duration(definition) {
|
|
611
|
+
return { kind: "duration", ...definition };
|
|
612
|
+
},
|
|
613
|
+
enum(values, definition) {
|
|
614
|
+
return { kind: "enum", values, ...definition };
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
var ParsedCommandOptions = class {
|
|
618
|
+
#values;
|
|
619
|
+
constructor(values) {
|
|
620
|
+
this.#values = values;
|
|
621
|
+
}
|
|
622
|
+
get(name) {
|
|
623
|
+
return this.#values[name];
|
|
624
|
+
}
|
|
625
|
+
string(name) {
|
|
626
|
+
return this.get(name);
|
|
627
|
+
}
|
|
628
|
+
number(name) {
|
|
629
|
+
return this.get(name);
|
|
630
|
+
}
|
|
631
|
+
boolean(name) {
|
|
632
|
+
return this.get(name);
|
|
633
|
+
}
|
|
634
|
+
user(name) {
|
|
635
|
+
return this.get(name);
|
|
636
|
+
}
|
|
637
|
+
duration(name) {
|
|
638
|
+
return this.get(name);
|
|
639
|
+
}
|
|
640
|
+
enum(name) {
|
|
641
|
+
return this.get(name);
|
|
642
|
+
}
|
|
643
|
+
toJSON() {
|
|
644
|
+
return { ...this.#values };
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
async function parseCommandOptions(schema, tokens, message, users) {
|
|
648
|
+
const args = new ArgsParser(tokens);
|
|
649
|
+
const values = {};
|
|
650
|
+
for (const [name, definition] of Object.entries(schema ?? {})) {
|
|
651
|
+
const optional = definition.required !== true;
|
|
652
|
+
if (definition.kind === "user") {
|
|
653
|
+
const hasImplicitUser = message.mentionedUsers.length > 0 || message.quoted?.sender !== void 0;
|
|
654
|
+
if (optional && !hasImplicitUser && args.remaining === 0) {
|
|
655
|
+
values[name] = void 0;
|
|
656
|
+
} else {
|
|
657
|
+
values[name] = await users.resolve(message, args);
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const argumentOptions = optional ? { optional: true } : void 0;
|
|
662
|
+
if (definition.kind === "string") {
|
|
663
|
+
const value = definition.rest ? args.rest() : args.string(name, argumentOptions);
|
|
664
|
+
if (definition.required && !value) {
|
|
665
|
+
throw missing(name);
|
|
666
|
+
}
|
|
667
|
+
if (value !== void 0 && definition.minLength !== void 0 && value.length < definition.minLength) {
|
|
668
|
+
throw invalid(name, value, `at least ${definition.minLength} characters`);
|
|
669
|
+
}
|
|
670
|
+
if (value !== void 0 && definition.maxLength !== void 0 && value.length > definition.maxLength) {
|
|
671
|
+
throw invalid(name, value, `at most ${definition.maxLength} characters`);
|
|
672
|
+
}
|
|
673
|
+
values[name] = value || void 0;
|
|
674
|
+
} else if (definition.kind === "number") {
|
|
675
|
+
const value = args.number(name, argumentOptions);
|
|
676
|
+
if (value !== void 0 && definition.min !== void 0 && value < definition.min) {
|
|
677
|
+
throw invalid(name, value, `at least ${definition.min}`);
|
|
678
|
+
}
|
|
679
|
+
if (value !== void 0 && definition.max !== void 0 && value > definition.max) {
|
|
680
|
+
throw invalid(name, value, `at most ${definition.max}`);
|
|
681
|
+
}
|
|
682
|
+
values[name] = value;
|
|
683
|
+
} else if (definition.kind === "boolean") {
|
|
684
|
+
values[name] = args.boolean(name, argumentOptions);
|
|
685
|
+
} else if (definition.kind === "duration") {
|
|
686
|
+
values[name] = args.duration(name, argumentOptions);
|
|
687
|
+
} else {
|
|
688
|
+
values[name] = args.enum(definition.values, name, argumentOptions);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (schema !== void 0 && args.remaining > 0) {
|
|
692
|
+
throw new WhaNextError("ARGUMENT_INVALID", "Too many arguments were provided.", {
|
|
693
|
+
context: { remaining: args.remaining }
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
return new ParsedCommandOptions(values);
|
|
697
|
+
}
|
|
698
|
+
function missing(name) {
|
|
699
|
+
return new WhaNextError("ARGUMENT_MISSING", `The argument "${name}" is required.`, {
|
|
700
|
+
context: { name }
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function invalid(name, received, expected) {
|
|
704
|
+
return new WhaNextError("ARGUMENT_INVALID", `The argument "${name}" must be ${expected}.`, {
|
|
705
|
+
context: { name, received, expected }
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/services/user-service.ts
|
|
710
|
+
var UserService = class {
|
|
711
|
+
#group;
|
|
712
|
+
constructor(group) {
|
|
713
|
+
this.#group = group;
|
|
714
|
+
}
|
|
715
|
+
async resolve(message, args) {
|
|
716
|
+
const mentioned = message.mentionedUsers[0];
|
|
717
|
+
let user;
|
|
718
|
+
if (mentioned) {
|
|
719
|
+
if (args.peek()?.startsWith("@")) {
|
|
720
|
+
args.skip();
|
|
721
|
+
}
|
|
722
|
+
user = mentioned;
|
|
723
|
+
} else if (message.quoted?.sender) {
|
|
724
|
+
user = message.quoted.sender;
|
|
725
|
+
} else {
|
|
726
|
+
user = args.user("membro");
|
|
727
|
+
}
|
|
728
|
+
return this.#group.resolveUser(message.chatId, user);
|
|
729
|
+
}
|
|
730
|
+
from(identity) {
|
|
731
|
+
if (!identity.includes("@")) {
|
|
732
|
+
return User.fromPhoneNumber(identity);
|
|
733
|
+
}
|
|
734
|
+
return User.fromIdentities([identity]);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
301
738
|
// src/commands/router.ts
|
|
302
739
|
var CommandRouter = class {
|
|
303
|
-
#
|
|
740
|
+
#roots = /* @__PURE__ */ new Map();
|
|
741
|
+
#definitions = /* @__PURE__ */ new Set();
|
|
304
742
|
#prefix;
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
|
|
308
|
-
|
|
743
|
+
#services;
|
|
744
|
+
#legacyOnError;
|
|
745
|
+
#globalMiddleware = [];
|
|
746
|
+
#errorHandlers = [];
|
|
747
|
+
#cooldowns = /* @__PURE__ */ new Map();
|
|
748
|
+
#concurrency = new CommandConcurrencyController();
|
|
749
|
+
#beforeExecute;
|
|
750
|
+
#afterExecute;
|
|
751
|
+
#cooldownOperations = 0;
|
|
752
|
+
constructor(servicesOrGroup, options = {}) {
|
|
753
|
+
this.#services = isRuntimeServices(servicesOrGroup) ? servicesOrGroup : createLegacyServices(servicesOrGroup);
|
|
309
754
|
this.#prefix = options.prefix ?? "!";
|
|
310
|
-
this.#
|
|
755
|
+
this.#legacyOnError = options.onError;
|
|
756
|
+
this.#beforeExecute = options.beforeExecute;
|
|
757
|
+
this.#afterExecute = options.afterExecute;
|
|
758
|
+
if (options.onCommandError) this.#errorHandlers.push(options.onCommandError);
|
|
311
759
|
if (this.#prefix.length === 0 || /\s/.test(this.#prefix)) {
|
|
312
760
|
throw new WhaNextError(
|
|
313
761
|
"ARGUMENT_INVALID",
|
|
314
762
|
"The command prefix cannot be empty or contain whitespace.",
|
|
315
|
-
{
|
|
316
|
-
context: { prefix: this.#prefix }
|
|
317
|
-
}
|
|
763
|
+
{ context: { prefix: this.#prefix } }
|
|
318
764
|
);
|
|
319
765
|
}
|
|
320
766
|
}
|
|
767
|
+
get prefix() {
|
|
768
|
+
return this.#prefix;
|
|
769
|
+
}
|
|
770
|
+
get size() {
|
|
771
|
+
return this.catalog({ includeHidden: true }).length;
|
|
772
|
+
}
|
|
321
773
|
command(definition) {
|
|
322
|
-
|
|
323
|
-
for (const name of
|
|
324
|
-
const normalized = name.toLowerCase();
|
|
325
|
-
if (this.#
|
|
774
|
+
this.#validateTree(definition, []);
|
|
775
|
+
for (const name of commandNames(definition)) {
|
|
776
|
+
const normalized = name.value.toLowerCase();
|
|
777
|
+
if (this.#roots.has(normalized)) {
|
|
326
778
|
throw new WhaNextError(
|
|
327
779
|
"ARGUMENT_INVALID",
|
|
328
780
|
`The command "${normalized}" is already registered.`
|
|
329
781
|
);
|
|
330
782
|
}
|
|
331
|
-
this.#
|
|
783
|
+
this.#roots.set(normalized, {
|
|
784
|
+
definition,
|
|
785
|
+
...name.locale ? { locale: name.locale } : {}
|
|
786
|
+
});
|
|
332
787
|
}
|
|
788
|
+
this.#definitions.add(definition);
|
|
789
|
+
return this;
|
|
790
|
+
}
|
|
791
|
+
load(dirPath, options = {}) {
|
|
792
|
+
return loadCommands(this, dirPath, options);
|
|
793
|
+
}
|
|
794
|
+
use(middleware) {
|
|
795
|
+
this.#globalMiddleware.push(middleware);
|
|
333
796
|
return this;
|
|
334
797
|
}
|
|
798
|
+
onError(handler) {
|
|
799
|
+
this.#errorHandlers.push(handler);
|
|
800
|
+
return () => {
|
|
801
|
+
const index = this.#errorHandlers.indexOf(handler);
|
|
802
|
+
if (index >= 0) this.#errorHandlers.splice(index, 1);
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
catalog(options = {}) {
|
|
806
|
+
const commands = [...this.#definitions].flatMap((definition) => flattenCommands(definition));
|
|
807
|
+
return commands.filter((command) => (options.includeHidden || !command.definition.hidden) && (!options.category || command.category === options.category));
|
|
808
|
+
}
|
|
809
|
+
categories() {
|
|
810
|
+
return [...new Set(this.catalog().map((command) => command.category))].sort();
|
|
811
|
+
}
|
|
812
|
+
has(path2) {
|
|
813
|
+
return this.find(path2) !== void 0;
|
|
814
|
+
}
|
|
815
|
+
values() {
|
|
816
|
+
return this.catalog({ includeHidden: true });
|
|
817
|
+
}
|
|
818
|
+
find(path2) {
|
|
819
|
+
const normalized = path2.trim().toLowerCase().split(/\s+/);
|
|
820
|
+
return this.catalog({ includeHidden: true }).find((command) => command.path.join(" ").toLowerCase() === normalized.join(" ") || command.aliases.some((alias) => alias.toLowerCase() === normalized.at(-1)));
|
|
821
|
+
}
|
|
822
|
+
async help(context, options = {}) {
|
|
823
|
+
const commands = this.catalog(options);
|
|
824
|
+
const title = options.title ?? (options.category ? `\u{1F4DA} *${options.category}*` : "\u{1F4DA} *Comandos*");
|
|
825
|
+
const lines = commands.map((command) => {
|
|
826
|
+
const usage = command.definition.usage ?? `${this.#prefix}${command.path.join(" ")}${formatOptions(command.definition)}`;
|
|
827
|
+
const description = context.locale ? command.definition.localizations?.[context.locale]?.description ?? command.definition.description : command.definition.description;
|
|
828
|
+
return `\u2022 *${usage}*
|
|
829
|
+
${description}`;
|
|
830
|
+
});
|
|
831
|
+
const text = lines.length > 0 ? `${title}
|
|
832
|
+
|
|
833
|
+
${lines.join("\n\n")}` : `${title}
|
|
834
|
+
|
|
835
|
+
_Nenhum comando dispon\xEDvel._`;
|
|
836
|
+
return context.reply(text);
|
|
837
|
+
}
|
|
335
838
|
async dispatch(message) {
|
|
336
839
|
const text = message.text?.trim();
|
|
337
|
-
if (!text?.startsWith(this.#prefix))
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
840
|
+
if (!text?.startsWith(this.#prefix)) return false;
|
|
340
841
|
const tokens = tokenize(text.slice(this.#prefix.length));
|
|
341
|
-
const
|
|
342
|
-
if (!
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (!command) {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
842
|
+
const rootName = tokens.shift()?.toLowerCase();
|
|
843
|
+
if (!rootName) return false;
|
|
844
|
+
const root = this.#roots.get(rootName);
|
|
845
|
+
if (!root) return false;
|
|
846
|
+
let resolved;
|
|
349
847
|
try {
|
|
350
|
-
|
|
351
|
-
await command.execute(message, new ArgsParser(tokens));
|
|
352
|
-
return true;
|
|
848
|
+
resolved = this.#resolve(root, tokens);
|
|
353
849
|
} catch (error) {
|
|
354
|
-
const normalized = toWhaNextError(error, { command:
|
|
355
|
-
|
|
356
|
-
|
|
850
|
+
const normalized = toWhaNextError(error, { command: root.definition.name, messageId: message.id });
|
|
851
|
+
const fallbackDefinition = {
|
|
852
|
+
...root.definition,
|
|
853
|
+
execute: () => void 0
|
|
854
|
+
};
|
|
855
|
+
const context2 = createCommandContext({
|
|
856
|
+
message,
|
|
857
|
+
command: {
|
|
858
|
+
definition: fallbackDefinition,
|
|
859
|
+
root: root.definition,
|
|
860
|
+
path: [root.definition.name],
|
|
861
|
+
aliases: root.definition.aliases ?? [],
|
|
862
|
+
category: root.definition.category ?? "general"
|
|
863
|
+
},
|
|
864
|
+
options: new ParsedCommandOptions({}),
|
|
865
|
+
args: new ArgsParser(tokens),
|
|
866
|
+
services: this.#services,
|
|
867
|
+
commands: this,
|
|
868
|
+
signal: new AbortController().signal,
|
|
869
|
+
...root.locale ? { locale: root.locale } : {}
|
|
870
|
+
});
|
|
871
|
+
if (root.definition.hooks?.onError) {
|
|
872
|
+
await root.definition.hooks.onError(context2, normalized);
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
if (this.#errorHandlers.length > 0) {
|
|
876
|
+
for (const handler of this.#errorHandlers) await handler(context2, normalized);
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
if (this.#legacyOnError) {
|
|
880
|
+
await this.#legacyOnError(normalized, message);
|
|
357
881
|
return true;
|
|
358
882
|
}
|
|
359
883
|
throw normalized;
|
|
360
884
|
}
|
|
885
|
+
const legacyArgs = new ArgsParser(resolved.tokens);
|
|
886
|
+
let context;
|
|
887
|
+
try {
|
|
888
|
+
const parsedOptions = await parseCommandOptions(
|
|
889
|
+
resolved.registered.definition.options,
|
|
890
|
+
resolved.tokens,
|
|
891
|
+
message,
|
|
892
|
+
this.#services.users
|
|
893
|
+
);
|
|
894
|
+
const concurrency = [...resolved.layers].reverse().find((layer) => layer.concurrency)?.concurrency;
|
|
895
|
+
const concurrencyKey = this.#executionKey(
|
|
896
|
+
resolved,
|
|
897
|
+
message,
|
|
898
|
+
concurrency?.scope ?? "user-chat"
|
|
899
|
+
);
|
|
900
|
+
await this.#concurrency.run(
|
|
901
|
+
concurrencyKey,
|
|
902
|
+
concurrency,
|
|
903
|
+
async (signal) => {
|
|
904
|
+
context = createCommandContext({
|
|
905
|
+
message,
|
|
906
|
+
command: resolved.registered,
|
|
907
|
+
options: parsedOptions,
|
|
908
|
+
args: legacyArgs,
|
|
909
|
+
services: this.#services,
|
|
910
|
+
commands: this,
|
|
911
|
+
signal,
|
|
912
|
+
...resolved.locale ? { locale: resolved.locale } : {}
|
|
913
|
+
});
|
|
914
|
+
await this.#authorize(resolved.layers, context);
|
|
915
|
+
this.#consumeCooldown(resolved, context);
|
|
916
|
+
await this.#execute(resolved, context);
|
|
917
|
+
}
|
|
918
|
+
);
|
|
919
|
+
return true;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
const normalized = toWhaNextError(error, {
|
|
922
|
+
command: resolved.registered.path.join(" "),
|
|
923
|
+
messageId: message.id
|
|
924
|
+
});
|
|
925
|
+
context ??= createCommandContext({
|
|
926
|
+
message,
|
|
927
|
+
command: resolved.registered,
|
|
928
|
+
options: new ParsedCommandOptions({}),
|
|
929
|
+
args: legacyArgs,
|
|
930
|
+
services: this.#services,
|
|
931
|
+
commands: this,
|
|
932
|
+
signal: new AbortController().signal,
|
|
933
|
+
...resolved.locale ? { locale: resolved.locale } : {}
|
|
934
|
+
});
|
|
935
|
+
if (await this.#handleError(resolved, context, normalized)) return true;
|
|
936
|
+
throw normalized;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
#resolve(root, inputTokens) {
|
|
940
|
+
const tokens = [...inputTokens];
|
|
941
|
+
const layers = [root.definition];
|
|
942
|
+
let current = root.definition;
|
|
943
|
+
let locale = root.locale;
|
|
944
|
+
while (isCommandGroup(current)) {
|
|
945
|
+
const name = tokens.shift()?.toLowerCase();
|
|
946
|
+
if (!name) {
|
|
947
|
+
throw new WhaNextError("ARGUMENT_MISSING", `Choose a subcommand for "${current.name}".`, {
|
|
948
|
+
context: { command: current.name }
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const found = findChild(current.subcommands, name);
|
|
952
|
+
if (!found) {
|
|
953
|
+
throw new WhaNextError("ARGUMENT_INVALID", `The subcommand "${name}" does not exist.`, {
|
|
954
|
+
context: { command: current.name, subcommand: name }
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
current = found.definition;
|
|
958
|
+
locale ??= found.locale;
|
|
959
|
+
layers.push(current);
|
|
960
|
+
}
|
|
961
|
+
const path2 = layers.map((definition) => definition.name);
|
|
962
|
+
return {
|
|
963
|
+
registered: {
|
|
964
|
+
definition: current,
|
|
965
|
+
root: root.definition,
|
|
966
|
+
path: path2,
|
|
967
|
+
aliases: current.aliases ?? [],
|
|
968
|
+
category: current.category ?? root.definition.category ?? "general"
|
|
969
|
+
},
|
|
970
|
+
layers,
|
|
971
|
+
tokens,
|
|
972
|
+
...locale ? { locale } : {}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
async #authorize(layers, context) {
|
|
976
|
+
for (const command of layers) {
|
|
977
|
+
await this.#authorizeLegacy(command, context);
|
|
978
|
+
for (const guard of command.guards ?? []) await runGuard(guard, context);
|
|
979
|
+
}
|
|
361
980
|
}
|
|
362
|
-
async #
|
|
363
|
-
if (command.onlyGroup && !
|
|
981
|
+
async #authorizeLegacy(command, context) {
|
|
982
|
+
if (command.onlyGroup && !context.isGroup) {
|
|
364
983
|
throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used in groups.");
|
|
365
984
|
}
|
|
366
|
-
if (command.onlyPrivate &&
|
|
367
|
-
throw new WhaNextError(
|
|
368
|
-
"COMMAND_NOT_ALLOWED",
|
|
369
|
-
"This command can only be used in private chats."
|
|
370
|
-
);
|
|
985
|
+
if (command.onlyPrivate && context.isGroup) {
|
|
986
|
+
throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used in private chats.");
|
|
371
987
|
}
|
|
372
|
-
if (command.onlyAdmin && !await this.#
|
|
373
|
-
throw new WhaNextError(
|
|
374
|
-
"COMMAND_NOT_ALLOWED",
|
|
375
|
-
"This command can only be used by group administrators."
|
|
376
|
-
);
|
|
988
|
+
if (command.onlyAdmin && !await this.#services.groups.isAdmin(context.chatId, context.senderIds)) {
|
|
989
|
+
throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used by group administrators.");
|
|
377
990
|
}
|
|
378
|
-
if (command.botMustBeAdmin && !await this.#
|
|
379
|
-
throw new WhaNextError(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
991
|
+
if (command.botMustBeAdmin && !await this.#services.groups.isCurrentUserAdmin(context.chatId)) {
|
|
992
|
+
throw new WhaNextError("BOT_NOT_ADMIN", "The connected WhatsApp account must be a group administrator.");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async #execute(resolved, context) {
|
|
996
|
+
const hooks = resolved.layers.map((layer) => layer.hooks).filter(Boolean);
|
|
997
|
+
if (this.#beforeExecute) await this.#beforeExecute(context);
|
|
998
|
+
for (const hook of hooks) await hook.beforeExecute?.(context);
|
|
999
|
+
const middleware = [
|
|
1000
|
+
...this.#globalMiddleware,
|
|
1001
|
+
...resolved.layers.flatMap((layer) => layer.middleware ?? [])
|
|
1002
|
+
];
|
|
1003
|
+
await composeMiddleware(middleware, context, async () => {
|
|
1004
|
+
await resolved.registered.definition.execute(context, context.args);
|
|
1005
|
+
});
|
|
1006
|
+
for (const hook of [...hooks].reverse()) await hook.afterExecute?.(context);
|
|
1007
|
+
if (this.#afterExecute) await this.#afterExecute(context);
|
|
1008
|
+
}
|
|
1009
|
+
#consumeCooldown(resolved, context) {
|
|
1010
|
+
const config = [...resolved.layers].reverse().find((layer) => layer.cooldown)?.cooldown;
|
|
1011
|
+
if (!config || config.durationMs <= 0) return;
|
|
1012
|
+
const key = this.#executionKey(resolved, context, config.scope ?? "user");
|
|
1013
|
+
const now = Date.now();
|
|
1014
|
+
this.#cooldownOperations += 1;
|
|
1015
|
+
if (this.#cooldownOperations % 256 === 0) this.#pruneCooldowns(now);
|
|
1016
|
+
const expiresAt = this.#cooldowns.get(key) ?? 0;
|
|
1017
|
+
if (expiresAt > now) {
|
|
1018
|
+
throw new WhaNextError("COMMAND_COOLDOWN", "This command is on cooldown.", {
|
|
1019
|
+
context: { retryAfterMs: expiresAt - now, key },
|
|
1020
|
+
recoverable: true
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
this.#cooldowns.set(key, now + config.durationMs);
|
|
1024
|
+
}
|
|
1025
|
+
#pruneCooldowns(now) {
|
|
1026
|
+
for (const [key, expiresAt] of this.#cooldowns) {
|
|
1027
|
+
if (expiresAt <= now) this.#cooldowns.delete(key);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
#executionKey(resolved, message, scope) {
|
|
1031
|
+
const command = resolved.registered.path.join("/").toLowerCase();
|
|
1032
|
+
if (scope === "global") return command;
|
|
1033
|
+
if (scope === "user") return `${command}:user:${message.senderId}`;
|
|
1034
|
+
if (scope === "chat") return `${command}:chat:${message.chatId}`;
|
|
1035
|
+
return `${command}:user-chat:${message.senderId}:${message.chatId}`;
|
|
1036
|
+
}
|
|
1037
|
+
async #handleError(resolved, context, error) {
|
|
1038
|
+
for (const layer of [...resolved.layers].reverse()) {
|
|
1039
|
+
if (layer.hooks?.onError) {
|
|
1040
|
+
await layer.hooks.onError(context, error);
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (this.#errorHandlers.length > 0) {
|
|
1045
|
+
for (const handler of this.#errorHandlers) await handler(context, error);
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
if (this.#legacyOnError) {
|
|
1049
|
+
await this.#legacyOnError(error, context.message);
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
#validateTree(definition, parents) {
|
|
1055
|
+
if (!definition.name.trim() || /\s/.test(definition.name)) {
|
|
1056
|
+
throw new WhaNextError("ARGUMENT_INVALID", "Command names cannot be empty or contain whitespace.", {
|
|
1057
|
+
context: { name: definition.name }
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
for (const name of commandNames(definition)) {
|
|
1061
|
+
if (!name.value.trim() || /\s/.test(name.value)) {
|
|
1062
|
+
throw new WhaNextError(
|
|
1063
|
+
"ARGUMENT_INVALID",
|
|
1064
|
+
"Command names and aliases cannot be empty or contain whitespace.",
|
|
1065
|
+
{ context: { name: name.value } }
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (!isCommandGroup(definition)) return;
|
|
1070
|
+
if (definition.subcommands.length === 0) {
|
|
1071
|
+
throw new WhaNextError("ARGUMENT_INVALID", `The command group "${definition.name}" is empty.`);
|
|
1072
|
+
}
|
|
1073
|
+
const names = /* @__PURE__ */ new Set();
|
|
1074
|
+
for (const child of definition.subcommands) {
|
|
1075
|
+
for (const name of commandNames(child)) {
|
|
1076
|
+
const normalized = name.value.toLowerCase();
|
|
1077
|
+
if (names.has(normalized)) {
|
|
1078
|
+
throw new WhaNextError("ARGUMENT_INVALID", `Duplicate subcommand "${normalized}".`, {
|
|
1079
|
+
context: { path: [...parents, definition.name].join(" ") }
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
names.add(normalized);
|
|
1083
|
+
}
|
|
1084
|
+
this.#validateTree(child, [...parents, definition.name]);
|
|
383
1085
|
}
|
|
384
1086
|
}
|
|
385
1087
|
};
|
|
1088
|
+
async function runGuard(guard, context) {
|
|
1089
|
+
const result = await guard(context);
|
|
1090
|
+
if (result === void 0 || result === true) return;
|
|
1091
|
+
const normalized = result === false ? { allowed: false } : result;
|
|
1092
|
+
if (normalized.allowed) return;
|
|
1093
|
+
throw new WhaNextError(
|
|
1094
|
+
normalized.code ?? "COMMAND_NOT_ALLOWED",
|
|
1095
|
+
normalized.message ?? "This command is not allowed in the current context."
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
async function composeMiddleware(middleware, context, execute) {
|
|
1099
|
+
let index = -1;
|
|
1100
|
+
const dispatch = async (position) => {
|
|
1101
|
+
if (position <= index) throw new Error("next() was called more than once.");
|
|
1102
|
+
index = position;
|
|
1103
|
+
const current = middleware[position];
|
|
1104
|
+
if (!current) return execute();
|
|
1105
|
+
await current(context, () => dispatch(position + 1));
|
|
1106
|
+
};
|
|
1107
|
+
await dispatch(0);
|
|
1108
|
+
}
|
|
1109
|
+
function commandNames(definition) {
|
|
1110
|
+
const names = [
|
|
1111
|
+
definition.name,
|
|
1112
|
+
...definition.aliases ?? []
|
|
1113
|
+
].map((value) => ({ value }));
|
|
1114
|
+
for (const [locale, localization] of Object.entries(definition.localizations ?? {})) {
|
|
1115
|
+
if (localization.name) names.push({ value: localization.name, locale });
|
|
1116
|
+
for (const alias of localization.aliases ?? []) names.push({ value: alias, locale });
|
|
1117
|
+
}
|
|
1118
|
+
return names;
|
|
1119
|
+
}
|
|
1120
|
+
function findChild(definitions, name) {
|
|
1121
|
+
for (const definition of definitions) {
|
|
1122
|
+
const found = commandNames(definition).find((candidate) => candidate.value.toLowerCase() === name);
|
|
1123
|
+
if (found) return { definition, ...found.locale ? { locale: found.locale } : {} };
|
|
1124
|
+
}
|
|
1125
|
+
return void 0;
|
|
1126
|
+
}
|
|
1127
|
+
function flattenCommands(root, parents = []) {
|
|
1128
|
+
if (isCommandGroup(root)) {
|
|
1129
|
+
return root.subcommands.flatMap((child) => flattenCommands(child, [...parents, root]));
|
|
1130
|
+
}
|
|
1131
|
+
const pathDefinitions = [...parents, root];
|
|
1132
|
+
return [{
|
|
1133
|
+
definition: root,
|
|
1134
|
+
root: pathDefinitions[0] ?? root,
|
|
1135
|
+
path: pathDefinitions.map((definition) => definition.name),
|
|
1136
|
+
aliases: root.aliases ?? [],
|
|
1137
|
+
category: root.category ?? [...parents].reverse().find((parent) => parent.category)?.category ?? "general"
|
|
1138
|
+
}];
|
|
1139
|
+
}
|
|
1140
|
+
function formatOptions(definition) {
|
|
1141
|
+
return Object.entries(definition.options ?? {}).map(([name, option2]) => option2.required ? ` <${name}>` : ` [${name}]`).join("");
|
|
1142
|
+
}
|
|
386
1143
|
function tokenize(input) {
|
|
387
1144
|
return input.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((token) => token.replace(/^(["'])(.*)\1$/, "$2")) ?? [];
|
|
388
1145
|
}
|
|
1146
|
+
function isRuntimeServices(value) {
|
|
1147
|
+
return "groups" in value && "messages" in value;
|
|
1148
|
+
}
|
|
1149
|
+
function createLegacyServices(group) {
|
|
1150
|
+
const unavailable = new Proxy({}, {
|
|
1151
|
+
get() {
|
|
1152
|
+
return () => {
|
|
1153
|
+
throw new WhaNextError(
|
|
1154
|
+
"PROVIDER_ERROR",
|
|
1155
|
+
"This CommandRouter was created without the full application services."
|
|
1156
|
+
);
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
return {
|
|
1161
|
+
groups: group,
|
|
1162
|
+
users: new UserService(group),
|
|
1163
|
+
messages: unavailable,
|
|
1164
|
+
media: unavailable,
|
|
1165
|
+
members: unavailable,
|
|
1166
|
+
chats: unavailable,
|
|
1167
|
+
mute: unavailable
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
389
1170
|
|
|
390
1171
|
// src/logger/logger.ts
|
|
391
1172
|
var priorities = {
|
|
@@ -1190,35 +1971,6 @@ var MessageService = class {
|
|
|
1190
1971
|
}
|
|
1191
1972
|
};
|
|
1192
1973
|
|
|
1193
|
-
// src/services/user-service.ts
|
|
1194
|
-
var UserService = class {
|
|
1195
|
-
#group;
|
|
1196
|
-
constructor(group) {
|
|
1197
|
-
this.#group = group;
|
|
1198
|
-
}
|
|
1199
|
-
async resolve(message, args) {
|
|
1200
|
-
const mentioned = message.mentionedUsers[0];
|
|
1201
|
-
let user;
|
|
1202
|
-
if (mentioned) {
|
|
1203
|
-
if (args.peek()?.startsWith("@")) {
|
|
1204
|
-
args.skip();
|
|
1205
|
-
}
|
|
1206
|
-
user = mentioned;
|
|
1207
|
-
} else if (message.quoted?.sender) {
|
|
1208
|
-
user = message.quoted.sender;
|
|
1209
|
-
} else {
|
|
1210
|
-
user = args.user("membro");
|
|
1211
|
-
}
|
|
1212
|
-
return this.#group.resolveUser(message.chatId, user);
|
|
1213
|
-
}
|
|
1214
|
-
from(identity) {
|
|
1215
|
-
if (!identity.includes("@")) {
|
|
1216
|
-
return User.fromPhoneNumber(identity);
|
|
1217
|
-
}
|
|
1218
|
-
return User.fromIdentities([identity]);
|
|
1219
|
-
}
|
|
1220
|
-
};
|
|
1221
|
-
|
|
1222
1974
|
// src/app/whanext-app.ts
|
|
1223
1975
|
var WhaNextApp = class {
|
|
1224
1976
|
message;
|
|
@@ -1229,6 +1981,7 @@ var WhaNextApp = class {
|
|
|
1229
1981
|
user;
|
|
1230
1982
|
mute;
|
|
1231
1983
|
logger;
|
|
1984
|
+
commands;
|
|
1232
1985
|
#provider;
|
|
1233
1986
|
#phone;
|
|
1234
1987
|
#events = new TypedEventEmitter();
|
|
@@ -1251,10 +2004,19 @@ var WhaNextApp = class {
|
|
|
1251
2004
|
const muteEnabled = options.mute?.enabled === true || options.mute?.store !== void 0;
|
|
1252
2005
|
const muteStore = muteEnabled ? options.mute?.store ?? new SqliteMuteStore(options.mute?.database) : void 0;
|
|
1253
2006
|
this.mute = new MuteService(provider, muteStore);
|
|
1254
|
-
this
|
|
2007
|
+
this.commands = new CommandRouter({
|
|
2008
|
+
messages: this.message,
|
|
2009
|
+
media: this.media,
|
|
2010
|
+
groups: this.group,
|
|
2011
|
+
members: this.member,
|
|
2012
|
+
chats: this.chat,
|
|
2013
|
+
users: this.user,
|
|
2014
|
+
mute: this.mute
|
|
2015
|
+
}, {
|
|
1255
2016
|
...options.router,
|
|
1256
2017
|
...options.prefix !== void 0 ? { prefix: options.prefix } : {}
|
|
1257
2018
|
});
|
|
2019
|
+
this.#router = this.commands;
|
|
1258
2020
|
this.#bind();
|
|
1259
2021
|
this.logger.debug("Application initialized", {
|
|
1260
2022
|
muteEnabled: this.mute.enabled,
|
|
@@ -1659,6 +2421,12 @@ var BaileysProvider = class {
|
|
|
1659
2421
|
#logger;
|
|
1660
2422
|
#messageStore = /* @__PURE__ */ new Map();
|
|
1661
2423
|
#messageCacheSize;
|
|
2424
|
+
#groupMetadataCache = /* @__PURE__ */ new Map();
|
|
2425
|
+
#groupMetadataRequests = /* @__PURE__ */ new Map();
|
|
2426
|
+
#groupMetadataGenerations = /* @__PURE__ */ new Map();
|
|
2427
|
+
#groupMetadataCacheEnabled;
|
|
2428
|
+
#groupMetadataCacheTtlMs;
|
|
2429
|
+
#groupMetadataCacheSize;
|
|
1662
2430
|
#socket;
|
|
1663
2431
|
#saveCredentials;
|
|
1664
2432
|
#saveQueue = Promise.resolve();
|
|
@@ -1670,6 +2438,9 @@ var BaileysProvider = class {
|
|
|
1670
2438
|
this.#options = options;
|
|
1671
2439
|
this.#logger = options.logger ?? new Logger("silent");
|
|
1672
2440
|
this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
|
|
2441
|
+
this.#groupMetadataCacheEnabled = options.groupMetadataCache?.enabled !== false;
|
|
2442
|
+
this.#groupMetadataCacheTtlMs = Math.max(1, options.groupMetadataCache?.ttlMs ?? 3e5);
|
|
2443
|
+
this.#groupMetadataCacheSize = Math.max(1, options.groupMetadataCache?.maxEntries ?? 1e3);
|
|
1673
2444
|
}
|
|
1674
2445
|
on(event, listener) {
|
|
1675
2446
|
return this.#events.on(event, listener);
|
|
@@ -1694,6 +2465,7 @@ var BaileysProvider = class {
|
|
|
1694
2465
|
markOnlineOnConnect: false,
|
|
1695
2466
|
enableAutoSessionRecreation: true,
|
|
1696
2467
|
enableRecentMessageCache: true,
|
|
2468
|
+
cachedGroupMetadata: async (jid) => this.#getGroupMetadata(jid),
|
|
1697
2469
|
getMessage: async (key) => this.#messageStore.get(this.#messageStoreKey(key))?.message ?? void 0
|
|
1698
2470
|
});
|
|
1699
2471
|
this.#socket = socket;
|
|
@@ -1817,7 +2589,7 @@ var BaileysProvider = class {
|
|
|
1817
2589
|
await this.#requireSocket().sendMessage(key.chatId, { delete: this.#toWaKey(key) });
|
|
1818
2590
|
}
|
|
1819
2591
|
async getGroup(groupId) {
|
|
1820
|
-
const metadata = await this.#
|
|
2592
|
+
const metadata = await this.#getGroupMetadata(groupId);
|
|
1821
2593
|
return {
|
|
1822
2594
|
id: metadata.id,
|
|
1823
2595
|
subject: metadata.subject,
|
|
@@ -1835,6 +2607,7 @@ var BaileysProvider = class {
|
|
|
1835
2607
|
async setGroupAccess(groupId, access) {
|
|
1836
2608
|
const setting = access === "closed" ? "announcement" : "not_announcement";
|
|
1837
2609
|
await this.#requireSocket().groupSettingUpdate(groupId, setting);
|
|
2610
|
+
this.#invalidateGroupMetadata(groupId);
|
|
1838
2611
|
}
|
|
1839
2612
|
async getGroupInviteCode(groupId) {
|
|
1840
2613
|
const code = await this.#requireSocket().groupInviteCode(groupId);
|
|
@@ -1865,6 +2638,7 @@ var BaileysProvider = class {
|
|
|
1865
2638
|
}
|
|
1866
2639
|
async updateParticipant(groupId, memberId, action) {
|
|
1867
2640
|
const [result] = await this.#requireSocket().groupParticipantsUpdate(groupId, [memberId], action);
|
|
2641
|
+
this.#invalidateGroupMetadata(groupId);
|
|
1868
2642
|
const status = result?.status ?? "unknown";
|
|
1869
2643
|
return {
|
|
1870
2644
|
success: status === "200",
|
|
@@ -1897,10 +2671,14 @@ var BaileysProvider = class {
|
|
|
1897
2671
|
});
|
|
1898
2672
|
socket.ev.on("groups.update", (groups) => {
|
|
1899
2673
|
for (const group of groups) {
|
|
1900
|
-
if (group.id)
|
|
2674
|
+
if (group.id) {
|
|
2675
|
+
this.#invalidateGroupMetadata(group.id);
|
|
2676
|
+
void this.#events.emit("groupChanged", { groupId: group.id });
|
|
2677
|
+
}
|
|
1901
2678
|
}
|
|
1902
2679
|
});
|
|
1903
2680
|
socket.ev.on("group-participants.update", (update) => {
|
|
2681
|
+
this.#invalidateGroupMetadata(update.id);
|
|
1904
2682
|
const change = this.#groupParticipantsChanged(update);
|
|
1905
2683
|
void this.#events.emit("groupParticipantsChanged", change);
|
|
1906
2684
|
const { id } = update;
|
|
@@ -2084,6 +2862,52 @@ var BaileysProvider = class {
|
|
|
2084
2862
|
if (oldest) this.#messageStore.delete(oldest);
|
|
2085
2863
|
}
|
|
2086
2864
|
}
|
|
2865
|
+
async #getGroupMetadata(groupId) {
|
|
2866
|
+
if (this.#groupMetadataCacheEnabled) {
|
|
2867
|
+
const cached = this.#groupMetadataCache.get(groupId);
|
|
2868
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
2869
|
+
this.#groupMetadataCache.delete(groupId);
|
|
2870
|
+
this.#groupMetadataCache.set(groupId, cached);
|
|
2871
|
+
return cached.value;
|
|
2872
|
+
}
|
|
2873
|
+
if (cached) this.#groupMetadataCache.delete(groupId);
|
|
2874
|
+
}
|
|
2875
|
+
const generation = this.#groupMetadataGenerations.get(groupId) ?? 0;
|
|
2876
|
+
const pending = this.#groupMetadataRequests.get(groupId);
|
|
2877
|
+
if (pending?.generation === generation) return pending.promise;
|
|
2878
|
+
const request = this.#requireSocket().groupMetadata(groupId);
|
|
2879
|
+
const requestEntry = { generation, promise: request };
|
|
2880
|
+
this.#groupMetadataRequests.set(groupId, requestEntry);
|
|
2881
|
+
try {
|
|
2882
|
+
const metadata = await request;
|
|
2883
|
+
if ((this.#groupMetadataGenerations.get(groupId) ?? 0) === generation) {
|
|
2884
|
+
this.#rememberGroupMetadata(groupId, metadata);
|
|
2885
|
+
}
|
|
2886
|
+
return metadata;
|
|
2887
|
+
} finally {
|
|
2888
|
+
if (this.#groupMetadataRequests.get(groupId) === requestEntry) {
|
|
2889
|
+
this.#groupMetadataRequests.delete(groupId);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
#rememberGroupMetadata(groupId, metadata) {
|
|
2894
|
+
if (!this.#groupMetadataCacheEnabled) return;
|
|
2895
|
+
this.#groupMetadataCache.delete(groupId);
|
|
2896
|
+
this.#groupMetadataCache.set(groupId, {
|
|
2897
|
+
value: metadata,
|
|
2898
|
+
expiresAt: Date.now() + this.#groupMetadataCacheTtlMs
|
|
2899
|
+
});
|
|
2900
|
+
while (this.#groupMetadataCache.size > this.#groupMetadataCacheSize) {
|
|
2901
|
+
const oldest = this.#groupMetadataCache.keys().next().value;
|
|
2902
|
+
if (oldest === void 0) return;
|
|
2903
|
+
this.#groupMetadataCache.delete(oldest);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
#invalidateGroupMetadata(groupId) {
|
|
2907
|
+
this.#groupMetadataCache.delete(groupId);
|
|
2908
|
+
const generation = this.#groupMetadataGenerations.get(groupId) ?? 0;
|
|
2909
|
+
this.#groupMetadataGenerations.set(groupId, generation + 1);
|
|
2910
|
+
}
|
|
2087
2911
|
#messageStoreKey(key) {
|
|
2088
2912
|
const chatId = "chatId" in key ? key.chatId : key.remoteJid;
|
|
2089
2913
|
return `${chatId ?? ""}:${key.id ?? ""}`;
|
|
@@ -2098,6 +2922,10 @@ async function create(options = {}) {
|
|
|
2098
2922
|
browser: options.browser ?? "windows" /* Windows */,
|
|
2099
2923
|
logger: logger.child("provider"),
|
|
2100
2924
|
...options.messageCacheSize !== void 0 ? { messageCacheSize: options.messageCacheSize } : {},
|
|
2925
|
+
groupMetadataCache: {
|
|
2926
|
+
...options.cache?.groupTtlMs !== void 0 ? { ttlMs: options.cache.groupTtlMs } : {},
|
|
2927
|
+
...options.cache?.memoryMaxEntries !== void 0 ? { maxEntries: options.cache.memoryMaxEntries } : {}
|
|
2928
|
+
},
|
|
2101
2929
|
...options.reconnect ? { reconnect: options.reconnect } : {}
|
|
2102
2930
|
});
|
|
2103
2931
|
return new WhaNextApp(provider, {
|
|
@@ -2110,102 +2938,69 @@ async function create(options = {}) {
|
|
|
2110
2938
|
}, logger);
|
|
2111
2939
|
}
|
|
2112
2940
|
|
|
2113
|
-
// src/commands/
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
throw new WhaNextError("COMMAND_LOAD_FAILED", `Could not read the commands directory "${dirPath}".`, {
|
|
2153
|
-
cause: error,
|
|
2154
|
-
context: { dirPath }
|
|
2155
|
-
});
|
|
2156
|
-
}
|
|
2157
|
-
return dirents.filter((dirent) => dirent.isFile()).map((dirent) => path.join(dirent.parentPath, dirent.name)).sort((left, right) => left.localeCompare(right));
|
|
2158
|
-
}
|
|
2159
|
-
async function importCommands(filePath) {
|
|
2160
|
-
let module;
|
|
2161
|
-
try {
|
|
2162
|
-
module = await import(pathToFileURL(filePath).href);
|
|
2163
|
-
} catch (error) {
|
|
2164
|
-
throw new WhaNextError("COMMAND_LOAD_FAILED", `Could not import the command file "${filePath}".`, {
|
|
2165
|
-
cause: error,
|
|
2166
|
-
context: { filePath }
|
|
2167
|
-
});
|
|
2168
|
-
}
|
|
2169
|
-
const definitions = [];
|
|
2170
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2171
|
-
const exports = [
|
|
2172
|
-
module.default,
|
|
2173
|
-
...Object.entries(module).filter(([name]) => name !== "default").map(([, value]) => value)
|
|
2174
|
-
];
|
|
2175
|
-
for (const value of exports) {
|
|
2176
|
-
const candidates = Array.isArray(value) ? value : [value];
|
|
2177
|
-
for (const candidate of candidates) {
|
|
2178
|
-
if (isCommandDefinition(candidate) && !seen.has(candidate)) {
|
|
2179
|
-
seen.add(candidate);
|
|
2180
|
-
definitions.push(candidate);
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
if (definitions.length === 0) {
|
|
2185
|
-
throw new WhaNextError("COMMAND_LOAD_FAILED", `The file "${filePath}" does not export any valid commands.`, {
|
|
2186
|
-
context: { filePath }
|
|
2187
|
-
});
|
|
2941
|
+
// src/commands/guards.ts
|
|
2942
|
+
var guards = {
|
|
2943
|
+
group() {
|
|
2944
|
+
return (context) => context.isGroup || {
|
|
2945
|
+
allowed: false,
|
|
2946
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
2947
|
+
message: "This command can only be used in groups."
|
|
2948
|
+
};
|
|
2949
|
+
},
|
|
2950
|
+
private() {
|
|
2951
|
+
return (context) => !context.isGroup || {
|
|
2952
|
+
allowed: false,
|
|
2953
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
2954
|
+
message: "This command can only be used in private chats."
|
|
2955
|
+
};
|
|
2956
|
+
},
|
|
2957
|
+
userAdmin() {
|
|
2958
|
+
return async (context) => context.isGroup && await context.groups.isAdmin(context.chatId, context.senderIds) || {
|
|
2959
|
+
allowed: false,
|
|
2960
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
2961
|
+
message: "This command can only be used by group administrators."
|
|
2962
|
+
};
|
|
2963
|
+
},
|
|
2964
|
+
botAdmin() {
|
|
2965
|
+
return async (context) => context.isGroup && await context.groups.isCurrentUserAdmin(context.chatId) || {
|
|
2966
|
+
allowed: false,
|
|
2967
|
+
code: "BOT_NOT_ADMIN",
|
|
2968
|
+
message: "The connected WhatsApp account must be a group administrator."
|
|
2969
|
+
};
|
|
2970
|
+
},
|
|
2971
|
+
botEnabled(check) {
|
|
2972
|
+
return async (context) => await check(context) || {
|
|
2973
|
+
allowed: false,
|
|
2974
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
2975
|
+
message: "Commands are disabled in this chat."
|
|
2976
|
+
};
|
|
2977
|
+
},
|
|
2978
|
+
custom(guard) {
|
|
2979
|
+
return guard;
|
|
2188
2980
|
}
|
|
2189
|
-
|
|
2190
|
-
}
|
|
2191
|
-
function isCommandDefinition(value) {
|
|
2192
|
-
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.execute === "function";
|
|
2193
|
-
}
|
|
2981
|
+
};
|
|
2194
2982
|
export {
|
|
2195
2983
|
ArgsParser,
|
|
2196
2984
|
Browser,
|
|
2197
2985
|
CommandRouter,
|
|
2986
|
+
DeferredReply,
|
|
2198
2987
|
Logger,
|
|
2199
2988
|
MemoryCache,
|
|
2200
2989
|
MuteService,
|
|
2990
|
+
ParsedCommandOptions,
|
|
2201
2991
|
SqliteMuteStore,
|
|
2202
2992
|
User,
|
|
2203
2993
|
WhaNextApp,
|
|
2204
2994
|
WhaNextError,
|
|
2205
2995
|
create,
|
|
2206
2996
|
defineCommand,
|
|
2997
|
+
defineCommandGroup,
|
|
2207
2998
|
defineCommands,
|
|
2999
|
+
defineSubcommand,
|
|
3000
|
+
guards,
|
|
3001
|
+
isCommandGroup,
|
|
2208
3002
|
loadCommands,
|
|
3003
|
+
option,
|
|
2209
3004
|
toWhaNextError
|
|
2210
3005
|
};
|
|
2211
3006
|
//# sourceMappingURL=index.js.map
|