@whanext/core 0.5.0 → 0.7.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
@@ -2,6 +2,49 @@
2
2
 
3
3
  Todas as mudanças relevantes do projeto serão registradas neste arquivo.
4
4
 
5
+ ## 0.7.0
6
+
7
+ ### Adicionado
8
+
9
+ - `app.message.react(message, emoji)` e `app.message.unreact(message)` para adicionar e remover reações sem expor tipos do provider.
10
+ - Evento `groupParticipantsChanged`, com grupo, ação, participantes afetados e autor quando informado pelo WhatsApp.
11
+
12
+ ### Alterado
13
+
14
+ - Alterações de participantes continuam invalidando automaticamente o cache de metadados do grupo antes da emissão do evento público.
15
+
16
+ ## 0.6.0
17
+
18
+ ### Adicionado
19
+
20
+ - `app.media.download(message)` para baixar imagens, vídeos, áudios, documentos e stickers recebidos como um `Buffer` com metadados normalizados.
21
+ - Renovação automática da URL de mídia quando o link original expira.
22
+ - Configurações `cache.memoryMaxEntries` e `messageCacheSize` para limitar a memória usada pelos caches internos.
23
+
24
+ ### Alterado
25
+
26
+ - O cache em memória agora usa LRU e agrupa buscas simultâneas dos metadados do mesmo grupo em uma única consulta ao WhatsApp.
27
+ - O cache de mensagens usa a chave completa da conversa, evitando colisões entre mensagens de chats distintos, e mantém até 1.000 entradas por padrão.
28
+ - Envio de presença deixou de fazer uma assinatura remota antes de cada atualização, reduzindo uma ida extra à rede para `typing`, `recording` e `paused`.
29
+
30
+ ## 0.5.1
31
+
32
+ ### Adicionado
33
+
34
+ - `defineCommands(...commands)` para declarar vários comandos no mesmo módulo com inferência completa de tipos.
35
+ - `LoadCommandsResult.commands`, com o nome e o arquivo de origem de cada comando registrado.
36
+
37
+ ### Alterado
38
+
39
+ - `loadCommands()` agora descobre todos os comandos exportados por um arquivo, incluindo vários exports nomeados e coleções exportadas por padrão.
40
+ - Exports auxiliares são ignorados quando o módulo possui comandos válidos.
41
+ - O mesmo objeto de comando não é registrado duas vezes quando aparece em mais de um export.
42
+ - A ordem de descoberta dos arquivos agora é determinística.
43
+
44
+ ### Corrigido
45
+
46
+ - Corrigido o autoload que registrava apenas o primeiro comando de arquivos como `mute/unmute`.
47
+
5
48
  ## 0.5.0
6
49
 
7
50
  ### Adicionado
package/CONTRIBUTING.md CHANGED
@@ -18,6 +18,9 @@ npm run check
18
18
  - A API pública usa modelos e erros do WhaNext.
19
19
  - Operações dependentes de estado retornam resultados tipados e idempotentes.
20
20
  - JID, LID e PN são resolvidos pela biblioteca.
21
+ - Mídias são expostas por modelos normalizados; tipos e objetos do provider não entram na API pública.
22
+ - APIs de presença não devem adicionar chamadas remotas além da atualização de estado solicitada.
23
+ - Caches internos devem ter limite de memória, TTL quando aplicável e cobertura para concorrência.
21
24
  - Novos comportamentos possuem testes sem conexão real com o WhatsApp.
22
25
  - Imports com vários nomes mantêm um nome por linha.
23
26
  - Código-fonte não contém comentários explicando implementação óbvia.
package/README.md CHANGED
@@ -35,7 +35,7 @@ await app.login({
35
35
  - Login por pairing code, sessão persistente e reconexão automática.
36
36
  - Prefixo global e comandos declarativos com argumentos tipados.
37
37
  - Usuários normalizados entre JID, LID e PN.
38
- - Mensagens, replies, edição, exclusão, menções e mídias.
38
+ - Mensagens, replies, edição, exclusão, menções, mídias e download normalizado.
39
39
  - Operações de grupos e membros com resultados idempotentes.
40
40
  - Cache de metadados transparente e substituível.
41
41
  - Mute permanente ou temporário com SQLite ou banco próprio.
@@ -261,10 +261,15 @@ await app.message.reply(message, {
261
261
 
262
262
  await app.message.edit(sent, 'Texto atualizado.');
263
263
  await app.message.delete(sent);
264
+
265
+ await app.message.react(sent, '👏🏻');
266
+ await app.message.unreact(sent);
264
267
  ```
265
268
 
266
269
  `delete()` aceita `Message`, `SentMessage` ou `MessageKey`.
267
270
 
271
+ `react()` aceita os mesmos tipos e adiciona uma reação à mensagem. `unreact()` remove a reação da conta conectada.
272
+
268
273
  ## Mídias
269
274
 
270
275
  ```ts
@@ -284,8 +289,14 @@ await app.media.audio(chatId, {
284
289
  mimetype: 'audio/ogg; codecs=opus',
285
290
  voice: true,
286
291
  });
292
+
293
+ const downloaded = await app.media.download(message);
294
+
295
+ await writeFile(`./downloads/${downloaded.fileName ?? message.id}`, downloaded.data);
287
296
  ```
288
297
 
298
+ `download()` aceita a `Message` recebida ou a sua `MessageKey` e devolve o buffer junto dos metadados normalizados. A mídia deve ser baixada enquanto a mensagem ainda está no cache da instância; o provider tenta renovar a URL de mídia automaticamente quando necessário.
299
+
289
300
  Texto, imagem e vídeo aceitam `User` diretamente em `mentions`.
290
301
 
291
302
  ## Grupos e membros
@@ -305,6 +316,19 @@ await app.member.demote(groupId, user);
305
316
 
306
317
  Estados como `already_open`, `already_admin`, `not_admin`, `not_in_group` e `already_removed` evitam mutações redundantes. A biblioteca escolhe automaticamente a identidade correta para grupos LID ou PN e só retorna sucesso depois da confirmação do WhatsApp.
307
318
 
319
+ Mudanças feitas por outros participantes podem ser acompanhadas sem lidar com o provider:
320
+
321
+ ```ts
322
+ app.on('groupParticipantsChanged', async (change) => {
323
+ console.log(change.groupId);
324
+ console.log(change.action);
325
+ console.log(change.participantIds);
326
+ console.log(change.authorId);
327
+ });
328
+ ```
329
+
330
+ `action` pode ser `add`, `remove`, `promote`, `demote` ou `modify`. O cache de metadados do grupo é invalidado antes desse evento ser emitido.
331
+
308
332
  ## Mute nativo
309
333
 
310
334
  Quando habilitado, o mute é aplicado antes dos eventos públicos e do router. Mensagens de um usuário mutado são apagadas automaticamente.
@@ -409,7 +433,7 @@ Restrições disponíveis:
409
433
 
410
434
  ### Carregando comandos de uma pasta
411
435
 
412
- `loadCommands` importa e registra todos os comandos de um diretório, sem precisar listar cada arquivo manualmente:
436
+ `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:
413
437
 
414
438
  ```ts
415
439
  import { loadCommands } from '@whanext/core';
@@ -417,7 +441,7 @@ import { loadCommands } from '@whanext/core';
417
441
  await loadCommands(app.router(), './commands');
418
442
  ```
419
443
 
420
- Cada arquivo deve exportar um `CommandDefinition`, por padrão ou nomeado:
444
+ Para um comando por arquivo, continue usando `defineCommand` normalmente:
421
445
 
422
446
  ```ts
423
447
  // commands/ping.js
@@ -430,6 +454,40 @@ export default defineCommand({
430
454
  });
431
455
  ```
432
456
 
457
+ Para manter comandos relacionados juntos, use `defineCommands`. Esse é o formato recomendado para módulos com vários comandos:
458
+
459
+ ```ts
460
+ // commands/moderation.ts
461
+ import { defineCommand, defineCommands } from '@whanext/core';
462
+
463
+ const mute = defineCommand({
464
+ name: 'mute',
465
+ description: 'Silencia um membro.',
466
+ async execute(message, args) {
467
+ // ...
468
+ },
469
+ });
470
+
471
+ const unmute = defineCommand({
472
+ name: 'unmute',
473
+ description: 'Remove o silêncio de um membro.',
474
+ async execute(message, args) {
475
+ // ...
476
+ },
477
+ });
478
+
479
+ export default defineCommands(mute, unmute);
480
+ ```
481
+
482
+ Vários exports nomeados também são descobertos automaticamente:
483
+
484
+ ```ts
485
+ export const mute = defineCommand({ /* ... */ });
486
+ export const unmute = defineCommand({ /* ... */ });
487
+ ```
488
+
489
+ 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.
490
+
433
491
  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 já estar ativo no processo (`tsx`, `ts-node` ou similar); habilite explicitamente quando esse loader existir:
434
492
 
435
493
  ```ts
@@ -438,6 +496,16 @@ await loadCommands(app.router(), './commands', { extensions: ['.ts'] });
438
496
 
439
497
  `loadCommands` também aceita qualquer objeto com um método `command()`, não apenas o router retornado por `app.router()`.
440
498
 
499
+ O retorno informa os arquivos carregados e ignorados, além de cada comando registrado:
500
+
501
+ ```ts
502
+ const result = await loadCommands(app.router(), './commands');
503
+
504
+ console.log(result.loaded); // arquivos importados
505
+ console.log(result.skipped); // extensões não habilitadas
506
+ console.log(result.commands); // { name, filePath }[]
507
+ ```
508
+
441
509
  ## Cache externo
442
510
 
443
511
  ```ts
@@ -445,11 +513,18 @@ const app = await create({
445
513
  cache: {
446
514
  store: myRedisStore,
447
515
  groupTtlMs: 300_000,
516
+ memoryMaxEntries: 1_000,
448
517
  },
449
518
  });
450
519
  ```
451
520
 
452
- O cache padrão vive na instância do app. Eventos e mutações de grupo invalidam automaticamente entradas relacionadas.
521
+ 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.
522
+
523
+ 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:
524
+
525
+ ```ts
526
+ const app = await create({ messageCacheSize: 2_000 });
527
+ ```
453
528
 
454
529
  ## Presença
455
530
 
@@ -459,6 +534,8 @@ await app.chat.recording(chatId);
459
534
  await app.chat.stopTyping(chatId);
460
535
  ```
461
536
 
537
+ `typing()` envia `composing`, `recording()` envia `recording` com mídia de áudio e `stopTyping()` envia `paused`, conforme o protocolo do WhatsApp. Nenhuma assinatura de presença desnecessária é feita antes do envio.
538
+
462
539
  ## Chamadas
463
540
 
464
541
  ```ts
@@ -489,8 +566,8 @@ Todos os erros públicos usam `WhaNextError` e códigos estáveis. O logger regi
489
566
 
490
567
  | Domínio | Responsabilidade |
491
568
  | --- | --- |
492
- | `app.message` | Envio, reply, edição, exclusão e texto |
493
- | `app.media` | Imagem, vídeo e áudio |
569
+ | `app.message` | Envio, reply, edição, exclusão, texto e reações |
570
+ | `app.media` | Envio e download de mídia |
494
571
  | `app.group` | Estado, convite, pin e metadados |
495
572
  | `app.member` | Remoção, promoção e rebaixamento |
496
573
  | `app.user` | Criação e resolução de usuários |
@@ -500,6 +577,7 @@ Todos os erros públicos usam `WhaNextError` e códigos estáveis. O logger regi
500
577
  | `app.router()` | Registro e despacho de comandos |
501
578
  | `loadCommands()` | Autoload de comandos a partir de uma pasta |
502
579
  | `app.health()` | Snapshot de saúde da aplicação |
580
+ | `app.on('groupParticipantsChanged')` | Alterações de participantes em grupos |
503
581
 
504
582
  ## Exemplo executável
505
583
 
package/SECURITY.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Versão | Suporte |
6
6
  | --- | --- |
7
- | 0.3.x | Sim |
7
+ | 0.7.x | Sim |
8
8
  | anteriores | Não |
9
9
 
10
10
  ## Reportando uma vulnerabilidade
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ interface CacheStore {
7
7
  interface CacheOptions {
8
8
  store?: CacheStore;
9
9
  groupTtlMs?: number;
10
+ memoryMaxEntries?: number;
10
11
  }
11
12
 
12
13
  interface UserData {
@@ -107,6 +108,12 @@ interface SentMessage {
107
108
  keys: MessageKey;
108
109
  timestamp: Date;
109
110
  }
111
+ interface DownloadedMedia {
112
+ data: Buffer;
113
+ kind: MediaKind;
114
+ mimetype?: string;
115
+ fileName?: string;
116
+ }
110
117
  type MediaSource = Uint8Array | {
111
118
  url: string;
112
119
  } | {
@@ -148,8 +155,9 @@ interface CommandDefinition {
148
155
  execute(message: Message, args: ArgsParser): void | Promise<void>;
149
156
  }
150
157
  declare function defineCommand<const Command extends CommandDefinition>(command: Command): Command;
158
+ declare function defineCommands<const Commands extends readonly CommandDefinition[]>(...commands: Commands): Commands;
151
159
 
152
- type WhaNextErrorCode = 'AUTH_INVALID_PHONE' | 'AUTH_EXPIRED' | 'CONNECTION_CLOSED' | 'CONNECTION_FAILED' | 'GROUP_NOT_FOUND' | 'MEMBER_NOT_FOUND' | 'BOT_NOT_ADMIN' | 'MESSAGE_NOT_FOUND' | 'MUTE_DISABLED' | 'STORAGE_ERROR' | 'ARGUMENT_MISSING' | 'ARGUMENT_INVALID' | 'COMMAND_NOT_ALLOWED' | 'COMMAND_LOAD_FAILED' | 'PROVIDER_ERROR' | 'UNKNOWN_ERROR';
160
+ type WhaNextErrorCode = 'AUTH_INVALID_PHONE' | 'AUTH_EXPIRED' | 'CONNECTION_CLOSED' | 'CONNECTION_FAILED' | 'GROUP_NOT_FOUND' | 'MEMBER_NOT_FOUND' | 'BOT_NOT_ADMIN' | 'MESSAGE_NOT_FOUND' | 'MEDIA_NOT_AVAILABLE' | 'MUTE_DISABLED' | 'STORAGE_ERROR' | 'ARGUMENT_MISSING' | 'ARGUMENT_INVALID' | 'COMMAND_NOT_ALLOWED' | 'COMMAND_LOAD_FAILED' | 'PROVIDER_ERROR' | 'UNKNOWN_ERROR';
153
161
  interface WhaNextErrorOptions {
154
162
  cause?: unknown;
155
163
  context?: Readonly<Record<string, unknown>>;
@@ -166,6 +174,13 @@ declare function toWhaNextError(error: unknown, context?: Readonly<Record<string
166
174
  type GroupAccess = 'open' | 'closed';
167
175
  type GroupRole = 'member' | 'admin' | 'owner';
168
176
  type GroupAddressingMode = 'lid' | 'pn';
177
+ type GroupParticipantAction = 'add' | 'remove' | 'promote' | 'demote' | 'modify';
178
+ interface GroupParticipantsChanged {
179
+ groupId: string;
180
+ action: GroupParticipantAction;
181
+ participantIds: string[];
182
+ authorId?: string;
183
+ }
169
184
  interface GroupParticipant {
170
185
  id: string;
171
186
  lid?: string;
@@ -227,6 +242,7 @@ interface ProviderEvents {
227
242
  groupChanged: {
228
243
  groupId: string;
229
244
  };
245
+ groupParticipantsChanged: GroupParticipantsChanged;
230
246
  call: CallEvent;
231
247
  }
232
248
  type Unsubscribe = () => void;
@@ -237,6 +253,8 @@ interface WhatsAppProvider {
237
253
  requestPairingCode(phone: string): Promise<string>;
238
254
  on<Event extends keyof ProviderEvents>(event: Event, listener: (payload: ProviderEvents[Event]) => void | Promise<void>): Unsubscribe;
239
255
  sendMessage(chatId: string, content: MessageContent, replyTo?: MessageKey): Promise<SentMessage>;
256
+ reactToMessage(key: MessageKey, emoji?: string): Promise<SentMessage>;
257
+ downloadMedia(key: MessageKey): Promise<DownloadedMedia>;
240
258
  editMessage(key: MessageKey, content: string): Promise<SentMessage>;
241
259
  deleteMessage(key: MessageKey): Promise<void>;
242
260
  getGroup(groupId: string): Promise<GroupSnapshot>;
@@ -392,6 +410,7 @@ declare class MediaService {
392
410
  image(chatId: string, content: ImageContent): Promise<SentMessage>;
393
411
  video(chatId: string, content: VideoContent): Promise<SentMessage>;
394
412
  audio(chatId: string, content: AudioContent): Promise<SentMessage>;
413
+ download(message: Message | MessageKey): Promise<DownloadedMedia>;
395
414
  }
396
415
 
397
416
  declare class MemberService {
@@ -409,6 +428,8 @@ declare class MessageService {
409
428
  reply(message: Message, content: MessageContent): Promise<SentMessage>;
410
429
  edit(message: Message | SentMessage | MessageKey, text: string): Promise<SentMessage>;
411
430
  delete(message: Message | SentMessage | MessageKey): Promise<void>;
431
+ react(message: Message | SentMessage | MessageKey, emoji: string): Promise<SentMessage>;
432
+ unreact(message: Message | SentMessage | MessageKey): Promise<SentMessage>;
412
433
  text(chatId: string, text: string, mentions?: MentionTarget[]): Promise<SentMessage>;
413
434
  }
414
435
 
@@ -425,6 +446,7 @@ interface AppEvents {
425
446
  error: WhaNextError;
426
447
  mute: MuteEnforcement;
427
448
  call: CallEvent;
449
+ groupParticipantsChanged: GroupParticipantsChanged;
428
450
  }
429
451
  interface LoginOptions {
430
452
  onCode?: (code: string) => void | Promise<void>;
@@ -490,12 +512,16 @@ interface CreateOptions {
490
512
  mute?: MuteOptions;
491
513
  router?: Omit<RouterOptions, 'prefix'>;
492
514
  reconnect?: ReconnectOptions;
515
+ messageCacheSize?: number;
493
516
  provider?: WhatsAppProvider;
494
517
  }
495
518
  declare function create(options?: CreateOptions): Promise<WhaNextApp>;
496
519
 
497
520
  declare class MemoryCache implements CacheStore {
498
521
  #private;
522
+ constructor(options?: {
523
+ maxEntries?: number;
524
+ });
499
525
  get<T>(key: string): Promise<T | undefined>;
500
526
  set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
501
527
  delete(key: string): Promise<void>;
@@ -512,6 +538,11 @@ interface LoadCommandsOptions {
512
538
  interface LoadCommandsResult {
513
539
  loaded: readonly string[];
514
540
  skipped: readonly string[];
541
+ commands: readonly LoadedCommand[];
542
+ }
543
+ interface LoadedCommand {
544
+ name: string;
545
+ filePath: string;
515
546
  }
516
547
  declare function loadCommands(registrar: CommandRegistrar, dirPath: string, options?: LoadCommandsOptions): Promise<LoadCommandsResult>;
517
548
 
@@ -525,4 +556,4 @@ declare class SqliteMuteStore implements MuteStore {
525
556
  close(): void;
526
557
  }
527
558
 
528
- export { type AddMuteOptions, type AddMuteResult, type AppEvents, type AppHealth, type AppHealthStatus, ArgsParser, type AudioContent, Browser, type CacheOptions, type CacheStore, type CallEvent, type CallStatus, type ChangeResult, type CommandDefinition, type CommandRegistrar, CommandRouter, type ConnectionState, type ConnectionUpdate, type CreateOptions, type GroupAccess, type GroupAddressingMode, type GroupParticipant, type GroupRole, type GroupSnapshot, type ImageContent, type InviteResult, type LoadCommandsOptions, type LoadCommandsResult, type LogContext, type LogEntry, type LogFormat, type LogLevel, type LogWriter, Logger, type LoggerConfig, type LoggerOptions, type LoginOptions, type MediaKind, type MediaSource, type MemberActionState, MemoryCache, type MentionTarget, type Message, type MessageContent, type MessageKey, type MessageMedia, type MuteChangeResult, type MuteEnforcement, type MuteOptions, type MuteRecord, MuteService, type MuteStore, type ParticipantUpdateResult, type PresenceState, type QuotedMessage, type ReconnectOptions, type RemoveMuteResult, type RouterOptions, type SentMessage, SqliteMuteStore, type StoredMute, type TextContent, User, type UserData, type VideoContent, WhaNextApp, WhaNextError, type WhaNextErrorCode, type WhatsAppProvider, create, defineCommand, loadCommands, toWhaNextError };
559
+ export { type AddMuteOptions, type AddMuteResult, type AppEvents, type AppHealth, type AppHealthStatus, ArgsParser, type AudioContent, Browser, type CacheOptions, type CacheStore, type CallEvent, type CallStatus, type ChangeResult, type CommandDefinition, type CommandRegistrar, CommandRouter, type ConnectionState, type ConnectionUpdate, type CreateOptions, type DownloadedMedia, type GroupAccess, type GroupAddressingMode, type GroupParticipant, type GroupParticipantAction, type GroupParticipantsChanged, type GroupRole, type GroupSnapshot, type ImageContent, type InviteResult, type LoadCommandsOptions, type LoadCommandsResult, type LoadedCommand, type LogContext, type LogEntry, type LogFormat, type LogLevel, type LogWriter, Logger, type LoggerConfig, type LoggerOptions, type LoginOptions, type MediaKind, type MediaSource, type MemberActionState, MemoryCache, type MentionTarget, type Message, type MessageContent, type MessageKey, type MessageMedia, type MuteChangeResult, type MuteEnforcement, type MuteOptions, type MuteRecord, MuteService, type MuteStore, type ParticipantUpdateResult, type PresenceState, type QuotedMessage, type ReconnectOptions, type RemoveMuteResult, type RouterOptions, type SentMessage, SqliteMuteStore, type StoredMute, type TextContent, User, type UserData, type VideoContent, WhaNextApp, WhaNextError, type WhaNextErrorCode, type WhatsAppProvider, create, defineCommand, defineCommands, loadCommands, toWhaNextError };