@whanext/core 0.8.0 → 0.10.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/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,752 @@ 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
+ options;
399
+ args;
400
+ locale;
401
+ signal;
402
+ client;
403
+ messages;
404
+ mediaService;
405
+ groups;
406
+ members;
407
+ chats;
408
+ users;
409
+ muteService;
410
+ #lastReply;
411
+ constructor(options) {
412
+ this.message = options.message;
413
+ this.user = options.message.sender;
414
+ this.chat = { id: options.message.chatId, isGroup: options.message.isGroup };
415
+ this.command = options.command;
416
+ this.options = options.options;
417
+ this.args = options.args;
418
+ this.locale = options.locale;
419
+ this.signal = options.signal;
420
+ this.client = options.services;
421
+ this.messages = options.services.messages;
422
+ this.mediaService = options.services.media;
423
+ this.groups = options.services.groups;
424
+ this.members = options.services.members;
425
+ this.chats = options.services.chats;
426
+ this.users = options.services.users;
427
+ this.muteService = options.services.mute;
428
+ this.group = options.message.isGroup ? this.#createGroupContext(options.message) : void 0;
429
+ Object.assign(this, options.message);
430
+ }
431
+ async reply(content, options = {}) {
432
+ const sent = await this.messages.reply(this.message, normalizeContent(content));
433
+ this.#lastReply = sent;
434
+ this.#scheduleDeletion(sent, options.deleteAfterMs);
435
+ return sent;
436
+ }
437
+ async defer(content = "\u23F3 _Processando..._") {
438
+ const sent = await this.reply(content);
439
+ return new DeferredReply(this.messages, sent, (message) => {
440
+ this.#lastReply = message;
441
+ });
442
+ }
443
+ async edit(content) {
444
+ if (!this.#lastReply) {
445
+ throw new WhaNextError(
446
+ "MESSAGE_NOT_FOUND",
447
+ "There is no command reply to edit. Call reply() or defer() first."
448
+ );
449
+ }
450
+ const edited = await this.messages.edit(this.#lastReply, content);
451
+ this.#lastReply = edited;
452
+ return edited;
453
+ }
454
+ react(emoji) {
455
+ return this.messages.react(this.message, emoji);
456
+ }
457
+ unreact() {
458
+ return this.messages.unreact(this.message);
459
+ }
460
+ delete() {
461
+ return this.messages.delete(this.message);
462
+ }
463
+ async deleteReply(options = {}) {
464
+ if (!this.#lastReply) return;
465
+ if (options.deleteAfterMs !== void 0 && options.deleteAfterMs > 0) {
466
+ this.#scheduleDeletion(this.#lastReply, options.deleteAfterMs);
467
+ return;
468
+ }
469
+ await this.messages.delete(this.#lastReply);
470
+ this.#lastReply = void 0;
471
+ }
472
+ #createGroupContext(message) {
473
+ return {
474
+ id: message.chatId,
475
+ metadata: (refresh) => this.groups.metadata(message.chatId, refresh),
476
+ isUserAdmin: () => this.groups.isAdmin(message.chatId, message.senderIds),
477
+ isBotAdmin: () => this.groups.isCurrentUserAdmin(message.chatId)
478
+ };
479
+ }
480
+ #scheduleDeletion(message, delayMs) {
481
+ if (delayMs === void 0 || delayMs <= 0) return;
482
+ const timer = setTimeout(() => {
483
+ void this.messages.delete(message).catch(() => void 0);
484
+ }, delayMs);
485
+ timer.unref?.();
486
+ }
487
+ };
488
+ var DeferredReply = class {
489
+ #messages;
490
+ #message;
491
+ #onEdit;
492
+ constructor(messages, message, onEdit) {
493
+ this.#messages = messages;
494
+ this.#message = message;
495
+ this.#onEdit = onEdit;
496
+ }
497
+ async edit(content) {
498
+ this.#message = await this.#messages.edit(this.#message, content);
499
+ this.#onEdit(this.#message);
500
+ return this.#message;
501
+ }
502
+ delete() {
503
+ return this.#messages.delete(this.#message);
504
+ }
505
+ };
506
+ function createCommandContext(options) {
507
+ return new CommandContextImplementation(options);
508
+ }
509
+ function normalizeContent(content) {
510
+ return typeof content === "string" ? { text: content } : content;
511
+ }
512
+
513
+ // src/commands/options.ts
514
+ var option = {
515
+ string(definition) {
516
+ return { kind: "string", ...definition };
517
+ },
518
+ number(definition) {
519
+ return { kind: "number", ...definition };
520
+ },
521
+ boolean(definition) {
522
+ return { kind: "boolean", ...definition };
523
+ },
524
+ user(definition) {
525
+ return { kind: "user", ...definition };
526
+ },
527
+ duration(definition) {
528
+ return { kind: "duration", ...definition };
529
+ },
530
+ enum(values, definition) {
531
+ return { kind: "enum", values, ...definition };
532
+ }
533
+ };
534
+ var ParsedCommandOptions = class {
535
+ #values;
536
+ constructor(values) {
537
+ this.#values = values;
538
+ }
539
+ get(name) {
540
+ return this.#values[name];
541
+ }
542
+ string(name) {
543
+ return this.get(name);
544
+ }
545
+ number(name) {
546
+ return this.get(name);
547
+ }
548
+ boolean(name) {
549
+ return this.get(name);
550
+ }
551
+ user(name) {
552
+ return this.get(name);
553
+ }
554
+ duration(name) {
555
+ return this.get(name);
556
+ }
557
+ enum(name) {
558
+ return this.get(name);
559
+ }
560
+ toJSON() {
561
+ return { ...this.#values };
562
+ }
563
+ };
564
+ async function parseCommandOptions(schema, tokens, message, users) {
565
+ const args = new ArgsParser(tokens);
566
+ const values = {};
567
+ for (const [name, definition] of Object.entries(schema ?? {})) {
568
+ const optional = definition.required !== true;
569
+ if (definition.kind === "user") {
570
+ const hasImplicitUser = message.mentionedUsers.length > 0 || message.quoted?.sender !== void 0;
571
+ if (optional && !hasImplicitUser && args.remaining === 0) {
572
+ values[name] = void 0;
573
+ } else {
574
+ values[name] = await users.resolve(message, args);
575
+ }
576
+ continue;
577
+ }
578
+ const argumentOptions = optional ? { optional: true } : void 0;
579
+ if (definition.kind === "string") {
580
+ const value = definition.rest ? args.rest() : args.string(name, argumentOptions);
581
+ if (definition.required && !value) {
582
+ throw missing(name);
583
+ }
584
+ if (value !== void 0 && definition.minLength !== void 0 && value.length < definition.minLength) {
585
+ throw invalid(name, value, `at least ${definition.minLength} characters`);
586
+ }
587
+ if (value !== void 0 && definition.maxLength !== void 0 && value.length > definition.maxLength) {
588
+ throw invalid(name, value, `at most ${definition.maxLength} characters`);
589
+ }
590
+ values[name] = value || void 0;
591
+ } else if (definition.kind === "number") {
592
+ const value = args.number(name, argumentOptions);
593
+ if (value !== void 0 && definition.min !== void 0 && value < definition.min) {
594
+ throw invalid(name, value, `at least ${definition.min}`);
595
+ }
596
+ if (value !== void 0 && definition.max !== void 0 && value > definition.max) {
597
+ throw invalid(name, value, `at most ${definition.max}`);
598
+ }
599
+ values[name] = value;
600
+ } else if (definition.kind === "boolean") {
601
+ values[name] = args.boolean(name, argumentOptions);
602
+ } else if (definition.kind === "duration") {
603
+ values[name] = args.duration(name, argumentOptions);
604
+ } else {
605
+ values[name] = args.enum(definition.values, name, argumentOptions);
606
+ }
607
+ }
608
+ if (schema !== void 0 && args.remaining > 0) {
609
+ throw new WhaNextError("ARGUMENT_INVALID", "Too many arguments were provided.", {
610
+ context: { remaining: args.remaining }
611
+ });
612
+ }
613
+ return new ParsedCommandOptions(values);
614
+ }
615
+ function missing(name) {
616
+ return new WhaNextError("ARGUMENT_MISSING", `The argument "${name}" is required.`, {
617
+ context: { name }
618
+ });
619
+ }
620
+ function invalid(name, received, expected) {
621
+ return new WhaNextError("ARGUMENT_INVALID", `The argument "${name}" must be ${expected}.`, {
622
+ context: { name, received, expected }
623
+ });
624
+ }
625
+
626
+ // src/services/user-service.ts
627
+ var UserService = class {
628
+ #group;
629
+ constructor(group) {
630
+ this.#group = group;
631
+ }
632
+ async resolve(message, args) {
633
+ const mentioned = message.mentionedUsers[0];
634
+ let user;
635
+ if (mentioned) {
636
+ if (args.peek()?.startsWith("@")) {
637
+ args.skip();
638
+ }
639
+ user = mentioned;
640
+ } else if (message.quoted?.sender) {
641
+ user = message.quoted.sender;
642
+ } else {
643
+ user = args.user("membro");
644
+ }
645
+ return this.#group.resolveUser(message.chatId, user);
646
+ }
647
+ from(identity) {
648
+ if (!identity.includes("@")) {
649
+ return User.fromPhoneNumber(identity);
650
+ }
651
+ return User.fromIdentities([identity]);
652
+ }
653
+ };
654
+
301
655
  // src/commands/router.ts
302
656
  var CommandRouter = class {
303
- #commands = /* @__PURE__ */ new Map();
657
+ #roots = /* @__PURE__ */ new Map();
658
+ #definitions = /* @__PURE__ */ new Set();
304
659
  #prefix;
305
- #group;
306
- #onError;
307
- constructor(group, options = {}) {
308
- this.#group = group;
660
+ #services;
661
+ #legacyOnError;
662
+ #globalMiddleware = [];
663
+ #errorHandlers = [];
664
+ #cooldowns = /* @__PURE__ */ new Map();
665
+ #concurrency = new CommandConcurrencyController();
666
+ #beforeExecute;
667
+ #afterExecute;
668
+ #cooldownOperations = 0;
669
+ constructor(servicesOrGroup, options = {}) {
670
+ this.#services = isRuntimeServices(servicesOrGroup) ? servicesOrGroup : createLegacyServices(servicesOrGroup);
309
671
  this.#prefix = options.prefix ?? "!";
310
- this.#onError = options.onError;
672
+ this.#legacyOnError = options.onError;
673
+ this.#beforeExecute = options.beforeExecute;
674
+ this.#afterExecute = options.afterExecute;
675
+ if (options.onCommandError) this.#errorHandlers.push(options.onCommandError);
311
676
  if (this.#prefix.length === 0 || /\s/.test(this.#prefix)) {
312
677
  throw new WhaNextError(
313
678
  "ARGUMENT_INVALID",
314
679
  "The command prefix cannot be empty or contain whitespace.",
315
- {
316
- context: { prefix: this.#prefix }
317
- }
680
+ { context: { prefix: this.#prefix } }
318
681
  );
319
682
  }
320
683
  }
684
+ get prefix() {
685
+ return this.#prefix;
686
+ }
687
+ get size() {
688
+ return this.catalog({ includeHidden: true }).length;
689
+ }
321
690
  command(definition) {
322
- const names = [definition.name, ...definition.aliases ?? []];
323
- for (const name of names) {
324
- const normalized = name.toLowerCase();
325
- if (this.#commands.has(normalized)) {
691
+ this.#validateTree(definition, []);
692
+ for (const name of commandNames(definition)) {
693
+ const normalized = name.value.toLowerCase();
694
+ if (this.#roots.has(normalized)) {
326
695
  throw new WhaNextError(
327
696
  "ARGUMENT_INVALID",
328
697
  `The command "${normalized}" is already registered.`
329
698
  );
330
699
  }
331
- this.#commands.set(normalized, definition);
700
+ this.#roots.set(normalized, {
701
+ definition,
702
+ ...name.locale ? { locale: name.locale } : {}
703
+ });
332
704
  }
705
+ this.#definitions.add(definition);
333
706
  return this;
334
707
  }
708
+ use(middleware) {
709
+ this.#globalMiddleware.push(middleware);
710
+ return this;
711
+ }
712
+ onError(handler) {
713
+ this.#errorHandlers.push(handler);
714
+ return () => {
715
+ const index = this.#errorHandlers.indexOf(handler);
716
+ if (index >= 0) this.#errorHandlers.splice(index, 1);
717
+ };
718
+ }
719
+ catalog(options = {}) {
720
+ const commands = [...this.#definitions].flatMap((definition) => flattenCommands(definition));
721
+ return commands.filter((command) => (options.includeHidden || !command.definition.hidden) && (!options.category || command.category === options.category));
722
+ }
723
+ categories() {
724
+ return [...new Set(this.catalog().map((command) => command.category))].sort();
725
+ }
726
+ has(path2) {
727
+ return this.find(path2) !== void 0;
728
+ }
729
+ values() {
730
+ return this.catalog({ includeHidden: true });
731
+ }
732
+ find(path2) {
733
+ const normalized = path2.trim().toLowerCase().split(/\s+/);
734
+ return this.catalog({ includeHidden: true }).find((command) => command.path.join(" ").toLowerCase() === normalized.join(" ") || command.aliases.some((alias) => alias.toLowerCase() === normalized.at(-1)));
735
+ }
736
+ async help(context, options = {}) {
737
+ const commands = this.catalog(options);
738
+ const title = options.title ?? (options.category ? `\u{1F4DA} *${options.category}*` : "\u{1F4DA} *Comandos*");
739
+ const lines = commands.map((command) => {
740
+ const usage = command.definition.usage ?? `${this.#prefix}${command.path.join(" ")}${formatOptions(command.definition)}`;
741
+ const description = context.locale ? command.definition.localizations?.[context.locale]?.description ?? command.definition.description : command.definition.description;
742
+ return `\u2022 *${usage}*
743
+ ${description}`;
744
+ });
745
+ const text = lines.length > 0 ? `${title}
746
+
747
+ ${lines.join("\n\n")}` : `${title}
748
+
749
+ _Nenhum comando dispon\xEDvel._`;
750
+ return context.reply(text);
751
+ }
335
752
  async dispatch(message) {
336
753
  const text = message.text?.trim();
337
- if (!text?.startsWith(this.#prefix)) {
338
- return false;
339
- }
754
+ if (!text?.startsWith(this.#prefix)) return false;
340
755
  const tokens = tokenize(text.slice(this.#prefix.length));
341
- const name = tokens.shift()?.toLowerCase();
342
- if (!name) {
343
- return false;
344
- }
345
- const command = this.#commands.get(name);
346
- if (!command) {
347
- return false;
348
- }
756
+ const rootName = tokens.shift()?.toLowerCase();
757
+ if (!rootName) return false;
758
+ const root = this.#roots.get(rootName);
759
+ if (!root) return false;
760
+ let resolved;
349
761
  try {
350
- await this.#authorize(command, message);
351
- await command.execute(message, new ArgsParser(tokens));
352
- return true;
762
+ resolved = this.#resolve(root, tokens);
353
763
  } catch (error) {
354
- const normalized = toWhaNextError(error, { command: command.name, messageId: message.id });
355
- if (this.#onError) {
356
- await this.#onError(normalized, message);
764
+ const normalized = toWhaNextError(error, { command: root.definition.name, messageId: message.id });
765
+ const fallbackDefinition = {
766
+ ...root.definition,
767
+ execute: () => void 0
768
+ };
769
+ const context2 = createCommandContext({
770
+ message,
771
+ command: {
772
+ definition: fallbackDefinition,
773
+ root: root.definition,
774
+ path: [root.definition.name],
775
+ aliases: root.definition.aliases ?? [],
776
+ category: root.definition.category ?? "general"
777
+ },
778
+ options: new ParsedCommandOptions({}),
779
+ args: new ArgsParser(tokens),
780
+ services: this.#services,
781
+ signal: new AbortController().signal,
782
+ ...root.locale ? { locale: root.locale } : {}
783
+ });
784
+ if (root.definition.hooks?.onError) {
785
+ await root.definition.hooks.onError(context2, normalized);
786
+ return true;
787
+ }
788
+ if (this.#errorHandlers.length > 0) {
789
+ for (const handler of this.#errorHandlers) await handler(context2, normalized);
790
+ return true;
791
+ }
792
+ if (this.#legacyOnError) {
793
+ await this.#legacyOnError(normalized, message);
357
794
  return true;
358
795
  }
359
796
  throw normalized;
360
797
  }
798
+ const legacyArgs = new ArgsParser(resolved.tokens);
799
+ let context;
800
+ try {
801
+ const parsedOptions = await parseCommandOptions(
802
+ resolved.registered.definition.options,
803
+ resolved.tokens,
804
+ message,
805
+ this.#services.users
806
+ );
807
+ const concurrency = [...resolved.layers].reverse().find((layer) => layer.concurrency)?.concurrency;
808
+ const concurrencyKey = this.#executionKey(
809
+ resolved,
810
+ message,
811
+ concurrency?.scope ?? "user-chat"
812
+ );
813
+ await this.#concurrency.run(
814
+ concurrencyKey,
815
+ concurrency,
816
+ async (signal) => {
817
+ context = createCommandContext({
818
+ message,
819
+ command: resolved.registered,
820
+ options: parsedOptions,
821
+ args: legacyArgs,
822
+ services: this.#services,
823
+ signal,
824
+ ...resolved.locale ? { locale: resolved.locale } : {}
825
+ });
826
+ await this.#authorize(resolved.layers, context);
827
+ this.#consumeCooldown(resolved, context);
828
+ await this.#execute(resolved, context);
829
+ }
830
+ );
831
+ return true;
832
+ } catch (error) {
833
+ const normalized = toWhaNextError(error, {
834
+ command: resolved.registered.path.join(" "),
835
+ messageId: message.id
836
+ });
837
+ context ??= createCommandContext({
838
+ message,
839
+ command: resolved.registered,
840
+ options: new ParsedCommandOptions({}),
841
+ args: legacyArgs,
842
+ services: this.#services,
843
+ signal: new AbortController().signal,
844
+ ...resolved.locale ? { locale: resolved.locale } : {}
845
+ });
846
+ if (await this.#handleError(resolved, context, normalized)) return true;
847
+ throw normalized;
848
+ }
849
+ }
850
+ #resolve(root, inputTokens) {
851
+ const tokens = [...inputTokens];
852
+ const layers = [root.definition];
853
+ let current = root.definition;
854
+ let locale = root.locale;
855
+ while (isCommandGroup(current)) {
856
+ const name = tokens.shift()?.toLowerCase();
857
+ if (!name) {
858
+ throw new WhaNextError("ARGUMENT_MISSING", `Choose a subcommand for "${current.name}".`, {
859
+ context: { command: current.name }
860
+ });
861
+ }
862
+ const found = findChild(current.subcommands, name);
863
+ if (!found) {
864
+ throw new WhaNextError("ARGUMENT_INVALID", `The subcommand "${name}" does not exist.`, {
865
+ context: { command: current.name, subcommand: name }
866
+ });
867
+ }
868
+ current = found.definition;
869
+ locale ??= found.locale;
870
+ layers.push(current);
871
+ }
872
+ const path2 = layers.map((definition) => definition.name);
873
+ return {
874
+ registered: {
875
+ definition: current,
876
+ root: root.definition,
877
+ path: path2,
878
+ aliases: current.aliases ?? [],
879
+ category: current.category ?? root.definition.category ?? "general"
880
+ },
881
+ layers,
882
+ tokens,
883
+ ...locale ? { locale } : {}
884
+ };
361
885
  }
362
- async #authorize(command, message) {
363
- if (command.onlyGroup && !message.isGroup) {
886
+ async #authorize(layers, context) {
887
+ for (const command of layers) {
888
+ await this.#authorizeLegacy(command, context);
889
+ for (const guard of command.guards ?? []) await runGuard(guard, context);
890
+ }
891
+ }
892
+ async #authorizeLegacy(command, context) {
893
+ if (command.onlyGroup && !context.isGroup) {
364
894
  throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used in groups.");
365
895
  }
366
- if (command.onlyPrivate && message.isGroup) {
367
- throw new WhaNextError(
368
- "COMMAND_NOT_ALLOWED",
369
- "This command can only be used in private chats."
370
- );
896
+ if (command.onlyPrivate && context.isGroup) {
897
+ throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used in private chats.");
371
898
  }
372
- if (command.onlyAdmin && !await this.#group.isAdmin(message.chatId, message.senderIds)) {
373
- throw new WhaNextError(
374
- "COMMAND_NOT_ALLOWED",
375
- "This command can only be used by group administrators."
376
- );
899
+ if (command.onlyAdmin && !await this.#services.groups.isAdmin(context.chatId, context.senderIds)) {
900
+ throw new WhaNextError("COMMAND_NOT_ALLOWED", "This command can only be used by group administrators.");
377
901
  }
378
- if (command.botMustBeAdmin && !await this.#group.isCurrentUserAdmin(message.chatId)) {
379
- throw new WhaNextError(
380
- "BOT_NOT_ADMIN",
381
- "The connected WhatsApp account must be a group administrator."
382
- );
902
+ if (command.botMustBeAdmin && !await this.#services.groups.isCurrentUserAdmin(context.chatId)) {
903
+ throw new WhaNextError("BOT_NOT_ADMIN", "The connected WhatsApp account must be a group administrator.");
904
+ }
905
+ }
906
+ async #execute(resolved, context) {
907
+ const hooks = resolved.layers.map((layer) => layer.hooks).filter(Boolean);
908
+ if (this.#beforeExecute) await this.#beforeExecute(context);
909
+ for (const hook of hooks) await hook.beforeExecute?.(context);
910
+ const middleware = [
911
+ ...this.#globalMiddleware,
912
+ ...resolved.layers.flatMap((layer) => layer.middleware ?? [])
913
+ ];
914
+ await composeMiddleware(middleware, context, async () => {
915
+ await resolved.registered.definition.execute(context, context.args);
916
+ });
917
+ for (const hook of [...hooks].reverse()) await hook.afterExecute?.(context);
918
+ if (this.#afterExecute) await this.#afterExecute(context);
919
+ }
920
+ #consumeCooldown(resolved, context) {
921
+ const config = [...resolved.layers].reverse().find((layer) => layer.cooldown)?.cooldown;
922
+ if (!config || config.durationMs <= 0) return;
923
+ const key = this.#executionKey(resolved, context, config.scope ?? "user");
924
+ const now = Date.now();
925
+ this.#cooldownOperations += 1;
926
+ if (this.#cooldownOperations % 256 === 0) this.#pruneCooldowns(now);
927
+ const expiresAt = this.#cooldowns.get(key) ?? 0;
928
+ if (expiresAt > now) {
929
+ throw new WhaNextError("COMMAND_COOLDOWN", "This command is on cooldown.", {
930
+ context: { retryAfterMs: expiresAt - now, key },
931
+ recoverable: true
932
+ });
933
+ }
934
+ this.#cooldowns.set(key, now + config.durationMs);
935
+ }
936
+ #pruneCooldowns(now) {
937
+ for (const [key, expiresAt] of this.#cooldowns) {
938
+ if (expiresAt <= now) this.#cooldowns.delete(key);
939
+ }
940
+ }
941
+ #executionKey(resolved, message, scope) {
942
+ const command = resolved.registered.path.join("/").toLowerCase();
943
+ if (scope === "global") return command;
944
+ if (scope === "user") return `${command}:user:${message.senderId}`;
945
+ if (scope === "chat") return `${command}:chat:${message.chatId}`;
946
+ return `${command}:user-chat:${message.senderId}:${message.chatId}`;
947
+ }
948
+ async #handleError(resolved, context, error) {
949
+ for (const layer of [...resolved.layers].reverse()) {
950
+ if (layer.hooks?.onError) {
951
+ await layer.hooks.onError(context, error);
952
+ return true;
953
+ }
954
+ }
955
+ if (this.#errorHandlers.length > 0) {
956
+ for (const handler of this.#errorHandlers) await handler(context, error);
957
+ return true;
958
+ }
959
+ if (this.#legacyOnError) {
960
+ await this.#legacyOnError(error, context.message);
961
+ return true;
962
+ }
963
+ return false;
964
+ }
965
+ #validateTree(definition, parents) {
966
+ if (!definition.name.trim() || /\s/.test(definition.name)) {
967
+ throw new WhaNextError("ARGUMENT_INVALID", "Command names cannot be empty or contain whitespace.", {
968
+ context: { name: definition.name }
969
+ });
970
+ }
971
+ for (const name of commandNames(definition)) {
972
+ if (!name.value.trim() || /\s/.test(name.value)) {
973
+ throw new WhaNextError(
974
+ "ARGUMENT_INVALID",
975
+ "Command names and aliases cannot be empty or contain whitespace.",
976
+ { context: { name: name.value } }
977
+ );
978
+ }
979
+ }
980
+ if (!isCommandGroup(definition)) return;
981
+ if (definition.subcommands.length === 0) {
982
+ throw new WhaNextError("ARGUMENT_INVALID", `The command group "${definition.name}" is empty.`);
983
+ }
984
+ const names = /* @__PURE__ */ new Set();
985
+ for (const child of definition.subcommands) {
986
+ for (const name of commandNames(child)) {
987
+ const normalized = name.value.toLowerCase();
988
+ if (names.has(normalized)) {
989
+ throw new WhaNextError("ARGUMENT_INVALID", `Duplicate subcommand "${normalized}".`, {
990
+ context: { path: [...parents, definition.name].join(" ") }
991
+ });
992
+ }
993
+ names.add(normalized);
994
+ }
995
+ this.#validateTree(child, [...parents, definition.name]);
383
996
  }
384
997
  }
385
998
  };
999
+ async function runGuard(guard, context) {
1000
+ const result = await guard(context);
1001
+ if (result === void 0 || result === true) return;
1002
+ const normalized = result === false ? { allowed: false } : result;
1003
+ if (normalized.allowed) return;
1004
+ throw new WhaNextError(
1005
+ normalized.code ?? "COMMAND_NOT_ALLOWED",
1006
+ normalized.message ?? "This command is not allowed in the current context."
1007
+ );
1008
+ }
1009
+ async function composeMiddleware(middleware, context, execute) {
1010
+ let index = -1;
1011
+ const dispatch = async (position) => {
1012
+ if (position <= index) throw new Error("next() was called more than once.");
1013
+ index = position;
1014
+ const current = middleware[position];
1015
+ if (!current) return execute();
1016
+ await current(context, () => dispatch(position + 1));
1017
+ };
1018
+ await dispatch(0);
1019
+ }
1020
+ function commandNames(definition) {
1021
+ const names = [
1022
+ definition.name,
1023
+ ...definition.aliases ?? []
1024
+ ].map((value) => ({ value }));
1025
+ for (const [locale, localization] of Object.entries(definition.localizations ?? {})) {
1026
+ if (localization.name) names.push({ value: localization.name, locale });
1027
+ for (const alias of localization.aliases ?? []) names.push({ value: alias, locale });
1028
+ }
1029
+ return names;
1030
+ }
1031
+ function findChild(definitions, name) {
1032
+ for (const definition of definitions) {
1033
+ const found = commandNames(definition).find((candidate) => candidate.value.toLowerCase() === name);
1034
+ if (found) return { definition, ...found.locale ? { locale: found.locale } : {} };
1035
+ }
1036
+ return void 0;
1037
+ }
1038
+ function flattenCommands(root, parents = []) {
1039
+ if (isCommandGroup(root)) {
1040
+ return root.subcommands.flatMap((child) => flattenCommands(child, [...parents, root]));
1041
+ }
1042
+ const pathDefinitions = [...parents, root];
1043
+ return [{
1044
+ definition: root,
1045
+ root: pathDefinitions[0] ?? root,
1046
+ path: pathDefinitions.map((definition) => definition.name),
1047
+ aliases: root.aliases ?? [],
1048
+ category: root.category ?? [...parents].reverse().find((parent) => parent.category)?.category ?? "general"
1049
+ }];
1050
+ }
1051
+ function formatOptions(definition) {
1052
+ return Object.entries(definition.options ?? {}).map(([name, option2]) => option2.required ? ` <${name}>` : ` [${name}]`).join("");
1053
+ }
386
1054
  function tokenize(input) {
387
1055
  return input.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((token) => token.replace(/^(["'])(.*)\1$/, "$2")) ?? [];
388
1056
  }
1057
+ function isRuntimeServices(value) {
1058
+ return "groups" in value && "messages" in value;
1059
+ }
1060
+ function createLegacyServices(group) {
1061
+ const unavailable = new Proxy({}, {
1062
+ get() {
1063
+ return () => {
1064
+ throw new WhaNextError(
1065
+ "PROVIDER_ERROR",
1066
+ "This CommandRouter was created without the full application services."
1067
+ );
1068
+ };
1069
+ }
1070
+ });
1071
+ return {
1072
+ groups: group,
1073
+ users: new UserService(group),
1074
+ messages: unavailable,
1075
+ media: unavailable,
1076
+ members: unavailable,
1077
+ chats: unavailable,
1078
+ mute: unavailable
1079
+ };
1080
+ }
389
1081
 
390
1082
  // src/logger/logger.ts
391
1083
  var priorities = {
@@ -1190,35 +1882,6 @@ var MessageService = class {
1190
1882
  }
1191
1883
  };
1192
1884
 
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
1885
  // src/app/whanext-app.ts
1223
1886
  var WhaNextApp = class {
1224
1887
  message;
@@ -1229,6 +1892,7 @@ var WhaNextApp = class {
1229
1892
  user;
1230
1893
  mute;
1231
1894
  logger;
1895
+ commands;
1232
1896
  #provider;
1233
1897
  #phone;
1234
1898
  #events = new TypedEventEmitter();
@@ -1251,10 +1915,19 @@ var WhaNextApp = class {
1251
1915
  const muteEnabled = options.mute?.enabled === true || options.mute?.store !== void 0;
1252
1916
  const muteStore = muteEnabled ? options.mute?.store ?? new SqliteMuteStore(options.mute?.database) : void 0;
1253
1917
  this.mute = new MuteService(provider, muteStore);
1254
- this.#router = new CommandRouter(this.group, {
1918
+ this.commands = new CommandRouter({
1919
+ messages: this.message,
1920
+ media: this.media,
1921
+ groups: this.group,
1922
+ members: this.member,
1923
+ chats: this.chat,
1924
+ users: this.user,
1925
+ mute: this.mute
1926
+ }, {
1255
1927
  ...options.router,
1256
1928
  ...options.prefix !== void 0 ? { prefix: options.prefix } : {}
1257
1929
  });
1930
+ this.#router = this.commands;
1258
1931
  this.#bind();
1259
1932
  this.logger.debug("Application initialized", {
1260
1933
  muteEnabled: this.mute.enabled,
@@ -1659,6 +2332,12 @@ var BaileysProvider = class {
1659
2332
  #logger;
1660
2333
  #messageStore = /* @__PURE__ */ new Map();
1661
2334
  #messageCacheSize;
2335
+ #groupMetadataCache = /* @__PURE__ */ new Map();
2336
+ #groupMetadataRequests = /* @__PURE__ */ new Map();
2337
+ #groupMetadataGenerations = /* @__PURE__ */ new Map();
2338
+ #groupMetadataCacheEnabled;
2339
+ #groupMetadataCacheTtlMs;
2340
+ #groupMetadataCacheSize;
1662
2341
  #socket;
1663
2342
  #saveCredentials;
1664
2343
  #saveQueue = Promise.resolve();
@@ -1670,6 +2349,9 @@ var BaileysProvider = class {
1670
2349
  this.#options = options;
1671
2350
  this.#logger = options.logger ?? new Logger("silent");
1672
2351
  this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
2352
+ this.#groupMetadataCacheEnabled = options.groupMetadataCache?.enabled !== false;
2353
+ this.#groupMetadataCacheTtlMs = Math.max(1, options.groupMetadataCache?.ttlMs ?? 3e5);
2354
+ this.#groupMetadataCacheSize = Math.max(1, options.groupMetadataCache?.maxEntries ?? 1e3);
1673
2355
  }
1674
2356
  on(event, listener) {
1675
2357
  return this.#events.on(event, listener);
@@ -1694,6 +2376,7 @@ var BaileysProvider = class {
1694
2376
  markOnlineOnConnect: false,
1695
2377
  enableAutoSessionRecreation: true,
1696
2378
  enableRecentMessageCache: true,
2379
+ cachedGroupMetadata: async (jid) => this.#getGroupMetadata(jid),
1697
2380
  getMessage: async (key) => this.#messageStore.get(this.#messageStoreKey(key))?.message ?? void 0
1698
2381
  });
1699
2382
  this.#socket = socket;
@@ -1817,7 +2500,7 @@ var BaileysProvider = class {
1817
2500
  await this.#requireSocket().sendMessage(key.chatId, { delete: this.#toWaKey(key) });
1818
2501
  }
1819
2502
  async getGroup(groupId) {
1820
- const metadata = await this.#requireSocket().groupMetadata(groupId);
2503
+ const metadata = await this.#getGroupMetadata(groupId);
1821
2504
  return {
1822
2505
  id: metadata.id,
1823
2506
  subject: metadata.subject,
@@ -1835,6 +2518,7 @@ var BaileysProvider = class {
1835
2518
  async setGroupAccess(groupId, access) {
1836
2519
  const setting = access === "closed" ? "announcement" : "not_announcement";
1837
2520
  await this.#requireSocket().groupSettingUpdate(groupId, setting);
2521
+ this.#invalidateGroupMetadata(groupId);
1838
2522
  }
1839
2523
  async getGroupInviteCode(groupId) {
1840
2524
  const code = await this.#requireSocket().groupInviteCode(groupId);
@@ -1865,6 +2549,7 @@ var BaileysProvider = class {
1865
2549
  }
1866
2550
  async updateParticipant(groupId, memberId, action) {
1867
2551
  const [result] = await this.#requireSocket().groupParticipantsUpdate(groupId, [memberId], action);
2552
+ this.#invalidateGroupMetadata(groupId);
1868
2553
  const status = result?.status ?? "unknown";
1869
2554
  return {
1870
2555
  success: status === "200",
@@ -1897,10 +2582,14 @@ var BaileysProvider = class {
1897
2582
  });
1898
2583
  socket.ev.on("groups.update", (groups) => {
1899
2584
  for (const group of groups) {
1900
- if (group.id) void this.#events.emit("groupChanged", { groupId: group.id });
2585
+ if (group.id) {
2586
+ this.#invalidateGroupMetadata(group.id);
2587
+ void this.#events.emit("groupChanged", { groupId: group.id });
2588
+ }
1901
2589
  }
1902
2590
  });
1903
2591
  socket.ev.on("group-participants.update", (update) => {
2592
+ this.#invalidateGroupMetadata(update.id);
1904
2593
  const change = this.#groupParticipantsChanged(update);
1905
2594
  void this.#events.emit("groupParticipantsChanged", change);
1906
2595
  const { id } = update;
@@ -2084,6 +2773,52 @@ var BaileysProvider = class {
2084
2773
  if (oldest) this.#messageStore.delete(oldest);
2085
2774
  }
2086
2775
  }
2776
+ async #getGroupMetadata(groupId) {
2777
+ if (this.#groupMetadataCacheEnabled) {
2778
+ const cached = this.#groupMetadataCache.get(groupId);
2779
+ if (cached && cached.expiresAt > Date.now()) {
2780
+ this.#groupMetadataCache.delete(groupId);
2781
+ this.#groupMetadataCache.set(groupId, cached);
2782
+ return cached.value;
2783
+ }
2784
+ if (cached) this.#groupMetadataCache.delete(groupId);
2785
+ }
2786
+ const generation = this.#groupMetadataGenerations.get(groupId) ?? 0;
2787
+ const pending = this.#groupMetadataRequests.get(groupId);
2788
+ if (pending?.generation === generation) return pending.promise;
2789
+ const request = this.#requireSocket().groupMetadata(groupId);
2790
+ const requestEntry = { generation, promise: request };
2791
+ this.#groupMetadataRequests.set(groupId, requestEntry);
2792
+ try {
2793
+ const metadata = await request;
2794
+ if ((this.#groupMetadataGenerations.get(groupId) ?? 0) === generation) {
2795
+ this.#rememberGroupMetadata(groupId, metadata);
2796
+ }
2797
+ return metadata;
2798
+ } finally {
2799
+ if (this.#groupMetadataRequests.get(groupId) === requestEntry) {
2800
+ this.#groupMetadataRequests.delete(groupId);
2801
+ }
2802
+ }
2803
+ }
2804
+ #rememberGroupMetadata(groupId, metadata) {
2805
+ if (!this.#groupMetadataCacheEnabled) return;
2806
+ this.#groupMetadataCache.delete(groupId);
2807
+ this.#groupMetadataCache.set(groupId, {
2808
+ value: metadata,
2809
+ expiresAt: Date.now() + this.#groupMetadataCacheTtlMs
2810
+ });
2811
+ while (this.#groupMetadataCache.size > this.#groupMetadataCacheSize) {
2812
+ const oldest = this.#groupMetadataCache.keys().next().value;
2813
+ if (oldest === void 0) return;
2814
+ this.#groupMetadataCache.delete(oldest);
2815
+ }
2816
+ }
2817
+ #invalidateGroupMetadata(groupId) {
2818
+ this.#groupMetadataCache.delete(groupId);
2819
+ const generation = this.#groupMetadataGenerations.get(groupId) ?? 0;
2820
+ this.#groupMetadataGenerations.set(groupId, generation + 1);
2821
+ }
2087
2822
  #messageStoreKey(key) {
2088
2823
  const chatId = "chatId" in key ? key.chatId : key.remoteJid;
2089
2824
  return `${chatId ?? ""}:${key.id ?? ""}`;
@@ -2098,6 +2833,10 @@ async function create(options = {}) {
2098
2833
  browser: options.browser ?? "windows" /* Windows */,
2099
2834
  logger: logger.child("provider"),
2100
2835
  ...options.messageCacheSize !== void 0 ? { messageCacheSize: options.messageCacheSize } : {},
2836
+ groupMetadataCache: {
2837
+ ...options.cache?.groupTtlMs !== void 0 ? { ttlMs: options.cache.groupTtlMs } : {},
2838
+ ...options.cache?.memoryMaxEntries !== void 0 ? { maxEntries: options.cache.memoryMaxEntries } : {}
2839
+ },
2101
2840
  ...options.reconnect ? { reconnect: options.reconnect } : {}
2102
2841
  });
2103
2842
  return new WhaNextApp(provider, {
@@ -2110,13 +2849,47 @@ async function create(options = {}) {
2110
2849
  }, logger);
2111
2850
  }
2112
2851
 
2113
- // src/commands/command.ts
2114
- function defineCommand(command) {
2115
- return command;
2116
- }
2117
- function defineCommands(...commands) {
2118
- return commands;
2119
- }
2852
+ // src/commands/guards.ts
2853
+ var guards = {
2854
+ group() {
2855
+ return (context) => context.isGroup || {
2856
+ allowed: false,
2857
+ code: "COMMAND_NOT_ALLOWED",
2858
+ message: "This command can only be used in groups."
2859
+ };
2860
+ },
2861
+ private() {
2862
+ return (context) => !context.isGroup || {
2863
+ allowed: false,
2864
+ code: "COMMAND_NOT_ALLOWED",
2865
+ message: "This command can only be used in private chats."
2866
+ };
2867
+ },
2868
+ userAdmin() {
2869
+ return async (context) => context.isGroup && await context.groups.isAdmin(context.chatId, context.senderIds) || {
2870
+ allowed: false,
2871
+ code: "COMMAND_NOT_ALLOWED",
2872
+ message: "This command can only be used by group administrators."
2873
+ };
2874
+ },
2875
+ botAdmin() {
2876
+ return async (context) => context.isGroup && await context.groups.isCurrentUserAdmin(context.chatId) || {
2877
+ allowed: false,
2878
+ code: "BOT_NOT_ADMIN",
2879
+ message: "The connected WhatsApp account must be a group administrator."
2880
+ };
2881
+ },
2882
+ botEnabled(check) {
2883
+ return async (context) => await check(context) || {
2884
+ allowed: false,
2885
+ code: "COMMAND_NOT_ALLOWED",
2886
+ message: "Commands are disabled in this chat."
2887
+ };
2888
+ },
2889
+ custom(guard) {
2890
+ return guard;
2891
+ }
2892
+ };
2120
2893
 
2121
2894
  // src/commands/load-commands.ts
2122
2895
  import { readdir } from "fs/promises";
@@ -2189,23 +2962,31 @@ async function importCommands(filePath) {
2189
2962
  return definitions;
2190
2963
  }
2191
2964
  function isCommandDefinition(value) {
2192
- return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.execute === "function";
2965
+ const candidate = value;
2966
+ return typeof value === "object" && value !== null && typeof candidate.name === "string" && (typeof candidate.execute === "function" || Array.isArray(candidate.subcommands));
2193
2967
  }
2194
2968
  export {
2195
2969
  ArgsParser,
2196
2970
  Browser,
2197
2971
  CommandRouter,
2972
+ DeferredReply,
2198
2973
  Logger,
2199
2974
  MemoryCache,
2200
2975
  MuteService,
2976
+ ParsedCommandOptions,
2201
2977
  SqliteMuteStore,
2202
2978
  User,
2203
2979
  WhaNextApp,
2204
2980
  WhaNextError,
2205
2981
  create,
2206
2982
  defineCommand,
2983
+ defineCommandGroup,
2207
2984
  defineCommands,
2985
+ defineSubcommand,
2986
+ guards,
2987
+ isCommandGroup,
2208
2988
  loadCommands,
2989
+ option,
2209
2990
  toWhaNextError
2210
2991
  };
2211
2992
  //# sourceMappingURL=index.js.map