@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 CHANGED
@@ -1,7 +1,59 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## 0.11.0 - Command discovery
5
+
6
+ - `CommandRouter.load()` carrega diretórios de comandos diretamente pelo router.
7
+ - `loadCommands()` agora aceita `URL` além de caminhos em string.
8
+ - Autoload reconhece `.ts`, `.mts` e `.cts` por padrão, além de JavaScript.
9
+ - `CommandContext` agora expõe `ctx.commands` (catálogo somente-leitura) e `ctx.prefix`.
10
+ - Menus e comandos de ajuda podem consultar o catálogo sem factory ou referência global ao app.
11
+ - Mantém compatibilidade com `defineCommand`, `defineCommands`, `defineSubcommand` e `defineCommandGroup`.
12
+
3
13
  Todas as mudanças relevantes do projeto serão registradas neste arquivo.
4
14
 
15
+ ## 0.10.0
16
+
17
+ ### Adicionado
18
+
19
+ - `CommandContext`, uma interação enriquecida que continua compatível com o modelo `Message` legado.
20
+ - `ctx.reply()`, `ctx.defer()`, `ctx.edit()`, `ctx.react()`, `ctx.unreact()`, `ctx.delete()` e respostas com exclusão programada.
21
+ - `option.string`, `number`, `boolean`, `enum`, `user` e `duration`, com inferência de tipos, obrigatoriedade e validações.
22
+ - `defineCommandGroup()` e `defineSubcommand()` para comandos como `&grupo abrir` e `&grupo fechar`.
23
+ - Guards reutilizáveis para grupo, privado, admin, bot admin e regras customizadas.
24
+ - Middleware global e por comando, além de hooks `beforeExecute`, `afterExecute` e `onError`.
25
+ - Cooldown por usuário, chat, usuário+chat ou global, com limpeza automática de entradas expiradas.
26
+ - Controle de concorrência com estratégias `parallel`, `reject`, `queue` e `replace`.
27
+ - `app.commands`, catálogo consultável, categorias, busca e help gerado por metadados.
28
+ - Localizações de nome, aliases e descrição sem duplicar definições.
29
+ - Códigos de erro `COMMAND_COOLDOWN` e `COMMAND_BUSY`.
30
+
31
+ ### Compatibilidade
32
+
33
+ - `app.router()` continua retornando o mesmo router disponível em `app.commands`.
34
+ - `onlyGroup`, `onlyPrivate`, `onlyAdmin` e `botMustBeAdmin` continuam suportados.
35
+ - Comandos legados com `execute(message, args)` continuam funcionando sem alteração.
36
+ - `loadCommands()` agora também reconhece grupos de comandos.
37
+
38
+ ## 0.9.0
39
+
40
+ ### Alterado
41
+
42
+ - Baileys atualizado de `7.0.0-rc13` para `7.0.0-rc14`.
43
+ - O provider agora fornece `cachedGroupMetadata` ao Baileys, evitando consultas redundantes ao WhatsApp no fan-out de mensagens em grupos.
44
+ - O cache interno usado pelo envio possui TTL, LRU limitado, deduplicação de buscas concorrentes e proteção contra a reinserção de resultados invalidados durante uma busca.
45
+ - `MemoryCache` agora promove corretamente chaves sobrescritas na ordem LRU.
46
+
47
+ ### Adicionado
48
+
49
+ - `MemoryCache.stats()` para observar hits, misses, sets, evictions e expirations.
50
+ - `MemoryCache.prune()` para remover entradas expiradas de forma explícita.
51
+
52
+ ### Compatibilidade
53
+
54
+ - Nenhuma alteração é necessária nos comandos ou na API pública existente.
55
+ - Sessões criadas pela v0.8 continuam compatíveis; o auth state multifile do Baileys 7 já persiste as chaves de LID, device list e TC token exigidas pela migração.
56
+
5
57
  ## 0.8.0
6
58
 
7
59
  ### Adicionado
package/README.md CHANGED
@@ -401,6 +401,137 @@ const app = await create({
401
401
 
402
402
  ## Comandos
403
403
 
404
+ O sistema moderno usa um contexto semelhante às interactions do Discord. `ctx` contém a mensagem normalizada, usuário, chat, grupo, services, options, sinal de cancelamento e helpers de resposta.
405
+
406
+ ```ts
407
+ import {
408
+ defineCommand,
409
+ guards,
410
+ option,
411
+ } from '@whanext/core';
412
+
413
+ app.commands.command(
414
+ defineCommand({
415
+ name: 'ban',
416
+ aliases: ['banir'],
417
+ description: 'Remove um membro do grupo.',
418
+ category: 'moderação',
419
+
420
+ guards: [
421
+ guards.group(),
422
+ guards.userAdmin(),
423
+ guards.botAdmin(),
424
+ ],
425
+
426
+ options: {
427
+ user: option.user({
428
+ description: 'Usuário que será removido.',
429
+ required: true,
430
+ }),
431
+ reason: option.string({
432
+ description: 'Motivo da remoção.',
433
+ rest: true,
434
+ }),
435
+ },
436
+
437
+ cooldown: {
438
+ durationMs: 5_000,
439
+ scope: 'user-chat',
440
+ },
441
+
442
+ concurrency: {
443
+ scope: 'chat',
444
+ max: 1,
445
+ strategy: 'queue',
446
+ },
447
+
448
+ async execute(ctx) {
449
+ const user = ctx.options.user('user');
450
+ const reason = ctx.options.string('reason') ?? 'Não informado';
451
+ const deferred = await ctx.defer('⏳ _Processando banimento..._');
452
+ const result = await ctx.members.remove(ctx.chatId, user);
453
+
454
+ await deferred.edit(
455
+ result.changed
456
+ ? `🔨 *Usuário banido*\n\n${user.mention} foi removido.\n• *Motivo:* ${reason}`
457
+ : `⚠️ O usuário não está mais no grupo.`,
458
+ );
459
+ },
460
+ }),
461
+ );
462
+ ```
463
+
464
+ ### Subcomandos
465
+
466
+ ```ts
467
+ import {
468
+ defineCommandGroup,
469
+ defineSubcommand,
470
+ guards,
471
+ } from '@whanext/core';
472
+
473
+ app.commands.command(defineCommandGroup({
474
+ name: 'grupo',
475
+ aliases: ['group'],
476
+ description: 'Gerencia o grupo.',
477
+ category: 'grupos',
478
+ guards: [guards.group(), guards.userAdmin(), guards.botAdmin()],
479
+
480
+ subcommands: [
481
+ defineSubcommand({
482
+ name: 'abrir',
483
+ aliases: ['open'],
484
+ description: 'Abre o grupo.',
485
+ async execute(ctx) {
486
+ await ctx.groups.open(ctx.chatId);
487
+ await ctx.reply('🔓 *Grupo aberto*');
488
+ },
489
+ }),
490
+ defineSubcommand({
491
+ name: 'fechar',
492
+ aliases: ['close'],
493
+ description: 'Fecha o grupo.',
494
+ async execute(ctx) {
495
+ await ctx.groups.close(ctx.chatId);
496
+ await ctx.reply('🔒 *Grupo fechado*');
497
+ },
498
+ }),
499
+ ],
500
+ }));
501
+ ```
502
+
503
+ Isso aceita `&grupo abrir`, `&group open`, `&grupo fechar` e `&group close` sem duplicar lógica.
504
+
505
+ ### Middleware e erros
506
+
507
+ ```ts
508
+ app.commands.use(async (ctx, next) => {
509
+ const startedAt = performance.now();
510
+ await next();
511
+ app.logger.debug('Command completed', {
512
+ command: ctx.command.path.join(' '),
513
+ durationMs: performance.now() - startedAt,
514
+ });
515
+ });
516
+
517
+ app.commands.onError(async (ctx, error) => {
518
+ if (error.code === 'COMMAND_COOLDOWN') {
519
+ await ctx.reply('⏱️ Aguarde um pouco antes de usar novamente.');
520
+ return;
521
+ }
522
+
523
+ await ctx.reply('⚠️ *Não foi possível concluir*');
524
+ });
525
+ ```
526
+
527
+ `app.commands.catalog()`, `categories()`, `find()`, `has()` e `values()` expõem a coleção registrada. `app.commands.help(ctx, { category: 'moderação' })` gera a ajuda usando descrição, usage, opções e visibilidade dos comandos.
528
+
529
+ Detalhes completos estão em [Comandos modernos](./docs/commands-v0.10.md).
530
+
531
+ ### API legada
532
+
533
+ Comandos existentes continuam válidos:
534
+
404
535
  ```ts
405
536
  app.router().command(
406
537
  defineCommand({
@@ -435,81 +566,83 @@ Restrições disponíveis:
435
566
 
436
567
  `ArgsParser` possui `string`, `number`, `boolean`, `enum`, `user`, `duration`, `peek`, `skip` e `rest`. `args.user()` retorna `User`, nunca uma string crua.
437
568
 
438
- ### Carregando comandos de uma pasta
569
+ ### Descoberta automática de comandos
439
570
 
440
- `loadCommands` importa e registra todos os comandos de um diretório, sem precisar listar cada arquivo manualmente. Um arquivo pode conter um ou vários comandos:
571
+ A forma recomendada agora é deixar cada arquivo de comando autossuficiente e pedir ao próprio router para descobrir a árvore inteira. Não é necessário manter `index.ts` por pasta nem um arquivo central de imports:
441
572
 
442
573
  ```ts
443
- import { loadCommands } from '@whanext/core';
574
+ const app = await create({ prefix: '&' });
444
575
 
445
- await loadCommands(app.router(), './commands');
576
+ await app.commands.load(new URL('./commands/', import.meta.url));
446
577
  ```
447
578
 
448
- Para um comando por arquivo, continue usando `defineCommand` normalmente:
579
+ O mesmo código funciona em desenvolvimento TypeScript (com um runtime/loader como `tsx`) e depois do build: o loader reconhece `.ts`, `.mts`, `.cts`, `.js`, `.mjs` e `.cjs` por padrão. O diretório é percorrido recursivamente.
449
580
 
450
- ```ts
451
- // commands/ping.js
452
- export default defineCommand({
453
- name: 'ping',
454
- description: 'Responde pong.',
455
- async execute(message) {
456
- await app.message.reply(message, 'pong');
457
- },
458
- });
581
+ ```text
582
+ src/commands/
583
+ ├── admin/
584
+ │ ├── ban.ts
585
+ │ ├── mute.ts
586
+ │ └── warn.ts
587
+ ├── group/
588
+ │ ├── access.ts
589
+ │ └── pin.ts
590
+ └── general/
591
+ ├── menu.ts
592
+ └── profile.ts
459
593
  ```
460
594
 
461
- Para manter comandos relacionados juntos, use `defineCommands`. Esse é o formato recomendado para módulos com vários comandos:
595
+ Um arquivo pode exportar um comando:
462
596
 
463
597
  ```ts
464
- // commands/moderation.ts
465
- import { defineCommand, defineCommands } from '@whanext/core';
466
-
467
- const mute = defineCommand({
468
- name: 'mute',
469
- description: 'Silencia um membro.',
470
- async execute(message, args) {
471
- // ...
472
- },
473
- });
474
-
475
- const unmute = defineCommand({
476
- name: 'unmute',
477
- description: 'Remove o silêncio de um membro.',
478
- async execute(message, args) {
479
- // ...
598
+ export default defineCommand({
599
+ name: 'ping',
600
+ description: 'Responde pong.',
601
+ async execute(ctx) {
602
+ await ctx.reply('pong');
480
603
  },
481
604
  });
482
-
483
- export default defineCommands(mute, unmute);
484
605
  ```
485
606
 
486
- Vários exports nomeados também são descobertos automaticamente:
607
+ Ou vários comandos relacionados no mesmo módulo:
487
608
 
488
609
  ```ts
489
610
  export const mute = defineCommand({ /* ... */ });
490
611
  export const unmute = defineCommand({ /* ... */ });
491
612
  ```
492
613
 
493
- Exports auxiliares, como constantes e metadados, são ignorados quando o módulo contém ao menos um comando válido. Se o mesmo objeto de comando aparecer em uma coleção e em um export nomeado, ele será registrado apenas uma vez.
614
+ Também é possível exportar uma coleção com `defineCommands(...)`. Exports auxiliares são ignorados quando o módulo contém pelo menos um comando válido, e o mesmo objeto não é registrado duas vezes.
494
615
 
495
- Por padrão, `loadCommands` procura arquivos `.js`, `.mjs` e `.cjs`, incluindo subpastas. Extensões como `.ts` não são carregadas por padrão, já que o `import()` dinâmico depende de um loader do TypeScript estar ativo no processo (`tsx`, `ts-node` ou similar); habilite explicitamente quando esse loader existir:
616
+ Para menus dinâmicos, `CommandContext` expõe uma visão somente-leitura do catálogo e o prefixo atual:
496
617
 
497
618
  ```ts
498
- await loadCommands(app.router(), './commands', { extensions: ['.ts'] });
619
+ export default defineCommand({
620
+ name: 'menu',
621
+ description: 'Mostra os comandos.',
622
+ async execute(ctx) {
623
+ const commands = ctx.commands.catalog({ category: 'administração' });
624
+ const lines = commands.map((command) =>
625
+ `${ctx.prefix}${command.path.join(' ')} — ${command.definition.description}`,
626
+ );
627
+
628
+ await ctx.reply(lines.join('\n'));
629
+ },
630
+ });
499
631
  ```
500
632
 
501
- `loadCommands` também aceita qualquer objeto com um método `command()`, não apenas o router retornado por `app.router()`.
633
+ `ctx.commands` fornece `catalog()`, `categories()`, `find()`, `has()`, `size` e `prefix`; ele não expõe detalhes internos do provider.
502
634
 
503
- O retorno informa os arquivos carregados e ignorados, além de cada comando registrado:
635
+ Se for necessário controlar extensões ou recursão:
504
636
 
505
637
  ```ts
506
- const result = await loadCommands(app.router(), './commands');
507
-
508
- console.log(result.loaded); // arquivos importados
509
- console.log(result.skipped); // extensões não habilitadas
510
- console.log(result.commands); // { name, filePath }[]
638
+ await app.commands.load(new URL('./commands/', import.meta.url), {
639
+ recursive: true,
640
+ extensions: ['.ts'],
641
+ });
511
642
  ```
512
643
 
644
+ `loadCommands(registrar, directory, options)` continua disponível como API de baixo nível para registradores customizados. O retorno contém arquivos carregados/ignorados e os comandos descobertos.
645
+
513
646
  ## Cache externo
514
647
 
515
648
  ```ts
@@ -524,12 +657,33 @@ const app = await create({
524
657
 
525
658
  O cache padrão vive na instância do app, usa LRU limitado e elimina entradas expiradas durante as leituras. Consultas simultâneas dos mesmos metadados são agrupadas em uma única chamada ao WhatsApp. Eventos e mutações de grupo invalidam automaticamente entradas relacionadas.
526
659
 
660
+ Os mesmos metadados também alimentam internamente o `cachedGroupMetadata` do Baileys. Em grupos já aquecidos, isso remove a consulta de metadados do caminho de envio; mensagens continuam aguardando somente criptografia, upload quando houver mídia e confirmação da rede.
661
+
662
+ Para observar um `MemoryCache` criado diretamente:
663
+
664
+ ```ts
665
+ import { MemoryCache } from '@whanext/core';
666
+
667
+ const cache = new MemoryCache({ maxEntries: 5_000 });
668
+
669
+ console.log(cache.stats());
670
+ cache.prune();
671
+ ```
672
+
673
+ `stats()` informa `size`, `maxEntries`, `hits`, `misses`, `sets`, `evictions` e `expirations`.
674
+
675
+ Para uma única instância, o cache padrão é suficiente mesmo com muitos grupos, desde que `memoryMaxEntries` seja dimensionado. Em várias instâncias/processos, use um `CacheStore` distribuído para o cache público; cada conexão mantém ainda um L1 local limitado para o caminho criptográfico do Baileys. Não compartilhe uma mesma sessão ativa entre processos.
676
+
527
677
  O cache interno de mensagens mantém até 1.000 mensagens por padrão para replies, reenvios do provider e downloads de mídia. Ajuste quando necessário:
528
678
 
529
679
  ```ts
530
680
  const app = await create({ messageCacheSize: 2_000 });
531
681
  ```
532
682
 
683
+ Não há delay artificial no envio. Presença é enviada diretamente e previews de alta qualidade permanecem desativados no provider; mídias ainda dependem do tempo de leitura, criptografia e upload ao WhatsApp.
684
+
685
+ Para limites, dimensionamento e decisões de produção, consulte [Desempenho e escala](./docs/performance-and-scale.md).
686
+
533
687
  ## Presença
534
688
 
535
689
  ```ts