@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/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
|
-
###
|
|
569
|
+
### Descoberta automática de comandos
|
|
439
570
|
|
|
440
|
-
|
|
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
|
-
|
|
574
|
+
const app = await create({ prefix: '&' });
|
|
444
575
|
|
|
445
|
-
await
|
|
576
|
+
await app.commands.load(new URL('./commands/', import.meta.url));
|
|
446
577
|
```
|
|
447
578
|
|
|
448
|
-
|
|
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
|
-
```
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
595
|
+
Um arquivo pode exportar um comando:
|
|
462
596
|
|
|
463
597
|
```ts
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
633
|
+
`ctx.commands` fornece `catalog()`, `categories()`, `find()`, `has()`, `size` e `prefix`; ele não expõe detalhes internos do provider.
|
|
502
634
|
|
|
503
|
-
|
|
635
|
+
Se for necessário controlar extensões ou recursão:
|
|
504
636
|
|
|
505
637
|
```ts
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|