@whanext/core 0.5.1 → 0.8.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,37 @@
2
2
 
3
3
  Todas as mudanças relevantes do projeto serão registradas neste arquivo.
4
4
 
5
+ ## 0.8.0
6
+
7
+ ### Adicionado
8
+
9
+ - `app.media.sticker(chatId, { sticker })` para enviar stickers WebP estáticos ou animados a partir de bytes, URL ou arquivo local.
10
+
11
+ ## 0.7.0
12
+
13
+ ### Adicionado
14
+
15
+ - `app.message.react(message, emoji)` e `app.message.unreact(message)` para adicionar e remover reações sem expor tipos do provider.
16
+ - Evento `groupParticipantsChanged`, com grupo, ação, participantes afetados e autor quando informado pelo WhatsApp.
17
+
18
+ ### Alterado
19
+
20
+ - Alterações de participantes continuam invalidando automaticamente o cache de metadados do grupo antes da emissão do evento público.
21
+
22
+ ## 0.6.0
23
+
24
+ ### Adicionado
25
+
26
+ - `app.media.download(message)` para baixar imagens, vídeos, áudios, documentos e stickers recebidos como um `Buffer` com metadados normalizados.
27
+ - Renovação automática da URL de mídia quando o link original expira.
28
+ - Configurações `cache.memoryMaxEntries` e `messageCacheSize` para limitar a memória usada pelos caches internos.
29
+
30
+ ### Alterado
31
+
32
+ - O cache em memória agora usa LRU e agrupa buscas simultâneas dos metadados do mesmo grupo em uma única consulta ao WhatsApp.
33
+ - 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.
34
+ - 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`.
35
+
5
36
  ## 0.5.1
6
37
 
7
38
  ### 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,9 +289,19 @@ await app.media.audio(chatId, {
284
289
  mimetype: 'audio/ogg; codecs=opus',
285
290
  voice: true,
286
291
  });
292
+
293
+ await app.media.sticker(chatId, {
294
+ sticker: { path: './sticker.webp' },
295
+ });
296
+
297
+ const downloaded = await app.media.download(message);
298
+
299
+ await writeFile(`./downloads/${downloaded.fileName ?? message.id}`, downloaded.data);
287
300
  ```
288
301
 
289
- Texto, imagem e vídeo aceitam `User` diretamente em `mentions`.
302
+ `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.
303
+
304
+ Stickers aceitam `Uint8Array`, URL ou caminho local e devem estar em WebP, inclusive para animações. Texto, imagem e vídeo aceitam `User` diretamente em `mentions`.
290
305
 
291
306
  ## Grupos e membros
292
307
 
@@ -305,6 +320,19 @@ await app.member.demote(groupId, user);
305
320
 
306
321
  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
322
 
323
+ Mudanças feitas por outros participantes podem ser acompanhadas sem lidar com o provider:
324
+
325
+ ```ts
326
+ app.on('groupParticipantsChanged', async (change) => {
327
+ console.log(change.groupId);
328
+ console.log(change.action);
329
+ console.log(change.participantIds);
330
+ console.log(change.authorId);
331
+ });
332
+ ```
333
+
334
+ `action` pode ser `add`, `remove`, `promote`, `demote` ou `modify`. O cache de metadados do grupo é invalidado antes desse evento ser emitido.
335
+
308
336
  ## Mute nativo
309
337
 
310
338
  Quando habilitado, o mute é aplicado antes dos eventos públicos e do router. Mensagens de um usuário mutado são apagadas automaticamente.
@@ -489,11 +517,18 @@ const app = await create({
489
517
  cache: {
490
518
  store: myRedisStore,
491
519
  groupTtlMs: 300_000,
520
+ memoryMaxEntries: 1_000,
492
521
  },
493
522
  });
494
523
  ```
495
524
 
496
- O cache padrão vive na instância do app. Eventos e mutações de grupo invalidam automaticamente entradas relacionadas.
525
+ 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
+
527
+ 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
+
529
+ ```ts
530
+ const app = await create({ messageCacheSize: 2_000 });
531
+ ```
497
532
 
498
533
  ## Presença
499
534
 
@@ -503,6 +538,8 @@ await app.chat.recording(chatId);
503
538
  await app.chat.stopTyping(chatId);
504
539
  ```
505
540
 
541
+ `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.
542
+
506
543
  ## Chamadas
507
544
 
508
545
  ```ts
@@ -533,8 +570,8 @@ Todos os erros públicos usam `WhaNextError` e códigos estáveis. O logger regi
533
570
 
534
571
  | Domínio | Responsabilidade |
535
572
  | --- | --- |
536
- | `app.message` | Envio, reply, edição, exclusão e texto |
537
- | `app.media` | Imagem, vídeo e áudio |
573
+ | `app.message` | Envio, reply, edição, exclusão, texto e reações |
574
+ | `app.media` | Envio, download e stickers |
538
575
  | `app.group` | Estado, convite, pin e metadados |
539
576
  | `app.member` | Remoção, promoção e rebaixamento |
540
577
  | `app.user` | Criação e resolução de usuários |
@@ -544,6 +581,7 @@ Todos os erros públicos usam `WhaNextError` e códigos estáveis. O logger regi
544
581
  | `app.router()` | Registro e despacho de comandos |
545
582
  | `loadCommands()` | Autoload de comandos a partir de uma pasta |
546
583
  | `app.health()` | Snapshot de saúde da aplicação |
584
+ | `app.on('groupParticipantsChanged')` | Alterações de participantes em grupos |
547
585
 
548
586
  ## Exemplo executável
549
587
 
package/SECURITY.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Versão | Suporte |
6
6
  | --- | --- |
7
- | 0.3.x | Sim |
7
+ | 0.8.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
  } | {
@@ -135,7 +142,10 @@ interface AudioContent {
135
142
  mimetype?: string;
136
143
  voice?: boolean;
137
144
  }
138
- type MessageContent = TextContent | ImageContent | VideoContent | AudioContent;
145
+ interface StickerContent {
146
+ sticker: MediaSource;
147
+ }
148
+ type MessageContent = TextContent | ImageContent | VideoContent | AudioContent | StickerContent;
139
149
 
140
150
  interface CommandDefinition {
141
151
  name: string;
@@ -150,7 +160,7 @@ interface CommandDefinition {
150
160
  declare function defineCommand<const Command extends CommandDefinition>(command: Command): Command;
151
161
  declare function defineCommands<const Commands extends readonly CommandDefinition[]>(...commands: Commands): Commands;
152
162
 
153
- 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';
163
+ 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';
154
164
  interface WhaNextErrorOptions {
155
165
  cause?: unknown;
156
166
  context?: Readonly<Record<string, unknown>>;
@@ -167,6 +177,13 @@ declare function toWhaNextError(error: unknown, context?: Readonly<Record<string
167
177
  type GroupAccess = 'open' | 'closed';
168
178
  type GroupRole = 'member' | 'admin' | 'owner';
169
179
  type GroupAddressingMode = 'lid' | 'pn';
180
+ type GroupParticipantAction = 'add' | 'remove' | 'promote' | 'demote' | 'modify';
181
+ interface GroupParticipantsChanged {
182
+ groupId: string;
183
+ action: GroupParticipantAction;
184
+ participantIds: string[];
185
+ authorId?: string;
186
+ }
170
187
  interface GroupParticipant {
171
188
  id: string;
172
189
  lid?: string;
@@ -228,6 +245,7 @@ interface ProviderEvents {
228
245
  groupChanged: {
229
246
  groupId: string;
230
247
  };
248
+ groupParticipantsChanged: GroupParticipantsChanged;
231
249
  call: CallEvent;
232
250
  }
233
251
  type Unsubscribe = () => void;
@@ -238,6 +256,8 @@ interface WhatsAppProvider {
238
256
  requestPairingCode(phone: string): Promise<string>;
239
257
  on<Event extends keyof ProviderEvents>(event: Event, listener: (payload: ProviderEvents[Event]) => void | Promise<void>): Unsubscribe;
240
258
  sendMessage(chatId: string, content: MessageContent, replyTo?: MessageKey): Promise<SentMessage>;
259
+ reactToMessage(key: MessageKey, emoji?: string): Promise<SentMessage>;
260
+ downloadMedia(key: MessageKey): Promise<DownloadedMedia>;
241
261
  editMessage(key: MessageKey, content: string): Promise<SentMessage>;
242
262
  deleteMessage(key: MessageKey): Promise<void>;
243
263
  getGroup(groupId: string): Promise<GroupSnapshot>;
@@ -393,6 +413,8 @@ declare class MediaService {
393
413
  image(chatId: string, content: ImageContent): Promise<SentMessage>;
394
414
  video(chatId: string, content: VideoContent): Promise<SentMessage>;
395
415
  audio(chatId: string, content: AudioContent): Promise<SentMessage>;
416
+ sticker(chatId: string, content: StickerContent): Promise<SentMessage>;
417
+ download(message: Message | MessageKey): Promise<DownloadedMedia>;
396
418
  }
397
419
 
398
420
  declare class MemberService {
@@ -410,6 +432,8 @@ declare class MessageService {
410
432
  reply(message: Message, content: MessageContent): Promise<SentMessage>;
411
433
  edit(message: Message | SentMessage | MessageKey, text: string): Promise<SentMessage>;
412
434
  delete(message: Message | SentMessage | MessageKey): Promise<void>;
435
+ react(message: Message | SentMessage | MessageKey, emoji: string): Promise<SentMessage>;
436
+ unreact(message: Message | SentMessage | MessageKey): Promise<SentMessage>;
413
437
  text(chatId: string, text: string, mentions?: MentionTarget[]): Promise<SentMessage>;
414
438
  }
415
439
 
@@ -426,6 +450,7 @@ interface AppEvents {
426
450
  error: WhaNextError;
427
451
  mute: MuteEnforcement;
428
452
  call: CallEvent;
453
+ groupParticipantsChanged: GroupParticipantsChanged;
429
454
  }
430
455
  interface LoginOptions {
431
456
  onCode?: (code: string) => void | Promise<void>;
@@ -491,12 +516,16 @@ interface CreateOptions {
491
516
  mute?: MuteOptions;
492
517
  router?: Omit<RouterOptions, 'prefix'>;
493
518
  reconnect?: ReconnectOptions;
519
+ messageCacheSize?: number;
494
520
  provider?: WhatsAppProvider;
495
521
  }
496
522
  declare function create(options?: CreateOptions): Promise<WhaNextApp>;
497
523
 
498
524
  declare class MemoryCache implements CacheStore {
499
525
  #private;
526
+ constructor(options?: {
527
+ maxEntries?: number;
528
+ });
500
529
  get<T>(key: string): Promise<T | undefined>;
501
530
  set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
502
531
  delete(key: string): Promise<void>;
@@ -531,4 +560,4 @@ declare class SqliteMuteStore implements MuteStore {
531
560
  close(): void;
532
561
  }
533
562
 
534
- 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 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 };
563
+ 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 StickerContent, type StoredMute, type TextContent, User, type UserData, type VideoContent, WhaNextApp, WhaNextError, type WhaNextErrorCode, type WhatsAppProvider, create, defineCommand, defineCommands, loadCommands, toWhaNextError };
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // src/cache/memory-cache.ts
2
2
  var MemoryCache = class {
3
3
  #entries = /* @__PURE__ */ new Map();
4
+ #maxEntries;
5
+ constructor(options = {}) {
6
+ this.#maxEntries = Math.max(1, options.maxEntries ?? 1e3);
7
+ }
4
8
  async get(key) {
5
9
  const entry = this.#entries.get(key);
6
10
  if (!entry) {
@@ -10,6 +14,8 @@ var MemoryCache = class {
10
14
  this.#entries.delete(key);
11
15
  return void 0;
12
16
  }
17
+ this.#entries.delete(key);
18
+ this.#entries.set(key, entry);
13
19
  return entry.value;
14
20
  }
15
21
  async set(key, value, ttlMs) {
@@ -18,6 +24,11 @@ var MemoryCache = class {
18
24
  entry.expiresAt = Date.now() + ttlMs;
19
25
  }
20
26
  this.#entries.set(key, entry);
27
+ while (this.#entries.size > this.#maxEntries) {
28
+ const oldest = this.#entries.keys().next().value;
29
+ if (oldest === void 0) return;
30
+ this.#entries.delete(oldest);
31
+ }
21
32
  }
22
33
  async delete(key) {
23
34
  this.#entries.delete(key);
@@ -932,6 +943,7 @@ var GroupService = class {
932
943
  #provider;
933
944
  #cache;
934
945
  #ttlMs;
946
+ #requests = /* @__PURE__ */ new Map();
935
947
  constructor(provider, cache, ttlMs = 3e5) {
936
948
  this.#provider = provider;
937
949
  this.#cache = cache;
@@ -979,9 +991,17 @@ var GroupService = class {
979
991
  return cached;
980
992
  }
981
993
  }
982
- const group = await this.#provider.getGroup(groupId);
983
- await this.#cache.set(key, group, this.#ttlMs);
984
- return group;
994
+ const pending = this.#requests.get(key);
995
+ if (pending) {
996
+ return pending;
997
+ }
998
+ const request = this.#loadMetadata(key, groupId);
999
+ this.#requests.set(key, request);
1000
+ try {
1001
+ return await request;
1002
+ } finally {
1003
+ this.#requests.delete(key);
1004
+ }
985
1005
  }
986
1006
  async isAdmin(groupId, memberIds) {
987
1007
  if (!groupId.endsWith("@g.us")) {
@@ -1030,6 +1050,11 @@ var GroupService = class {
1030
1050
  #key(groupId) {
1031
1051
  return `group:${groupId}`;
1032
1052
  }
1053
+ async #loadMetadata(key, groupId) {
1054
+ const group = await this.#provider.getGroup(groupId);
1055
+ await this.#cache.set(key, group, this.#ttlMs);
1056
+ return group;
1057
+ }
1033
1058
  #matchesParticipant(participant, identities) {
1034
1059
  const participantIds = [participant.id, participant.lid, participant.phoneNumber].filter((identity) => identity !== void 0);
1035
1060
  return identities.some((identity) => participantIds.some((participantId) => identitiesMatch(identity, participantId)));
@@ -1051,6 +1076,13 @@ var MediaService = class {
1051
1076
  audio(chatId, content) {
1052
1077
  return this.#provider.sendMessage(chatId, content);
1053
1078
  }
1079
+ sticker(chatId, content) {
1080
+ return this.#provider.sendMessage(chatId, content);
1081
+ }
1082
+ download(message) {
1083
+ const key = "keys" in message ? message.keys : message;
1084
+ return this.#provider.downloadMedia(key);
1085
+ }
1054
1086
  };
1055
1087
 
1056
1088
  // src/services/member-service.ts
@@ -1141,6 +1173,14 @@ var MessageService = class {
1141
1173
  const key = "keys" in message ? message.keys : message;
1142
1174
  return this.#provider.deleteMessage(key);
1143
1175
  }
1176
+ react(message, emoji) {
1177
+ const key = "keys" in message ? message.keys : message;
1178
+ return this.#provider.reactToMessage(key, emoji);
1179
+ }
1180
+ unreact(message) {
1181
+ const key = "keys" in message ? message.keys : message;
1182
+ return this.#provider.reactToMessage(key);
1183
+ }
1144
1184
  text(chatId, text, mentions) {
1145
1185
  const content = { text };
1146
1186
  if (mentions !== void 0) {
@@ -1199,7 +1239,9 @@ var WhaNextApp = class {
1199
1239
  this.#provider = provider;
1200
1240
  this.#phone = options.phone;
1201
1241
  this.logger = logger;
1202
- const cache = options.cache?.store ?? new MemoryCache();
1242
+ const cache = options.cache?.store ?? new MemoryCache(
1243
+ options.cache?.memoryMaxEntries === void 0 ? void 0 : { maxEntries: options.cache.memoryMaxEntries }
1244
+ );
1203
1245
  this.group = new GroupService(provider, cache, options.cache?.groupTtlMs);
1204
1246
  this.member = new MemberService(provider, this.group);
1205
1247
  this.message = new MessageService(provider);
@@ -1307,6 +1349,10 @@ var WhaNextApp = class {
1307
1349
  await this.#events.emit("connection", update);
1308
1350
  });
1309
1351
  this.#provider.on("groupChanged", ({ groupId }) => this.group.invalidate(groupId));
1352
+ this.#provider.on("groupParticipantsChanged", async (change) => {
1353
+ await this.group.invalidate(change.groupId);
1354
+ await this.#events.emit("groupParticipantsChanged", change);
1355
+ });
1310
1356
  this.#provider.on("call", async (call) => {
1311
1357
  this.logger.debug("Call received", {
1312
1358
  callId: call.id,
@@ -1401,6 +1447,7 @@ var Browser = /* @__PURE__ */ ((Browser2) => {
1401
1447
  import {
1402
1448
  Browsers,
1403
1449
  DisconnectReason,
1450
+ downloadMediaMessage,
1404
1451
  makeWASocket,
1405
1452
  proto,
1406
1453
  useMultiFileAuthState
@@ -1611,6 +1658,7 @@ var BaileysProvider = class {
1611
1658
  #events = new TypedEventEmitter();
1612
1659
  #logger;
1613
1660
  #messageStore = /* @__PURE__ */ new Map();
1661
+ #messageCacheSize;
1614
1662
  #socket;
1615
1663
  #saveCredentials;
1616
1664
  #saveQueue = Promise.resolve();
@@ -1621,6 +1669,7 @@ var BaileysProvider = class {
1621
1669
  constructor(options) {
1622
1670
  this.#options = options;
1623
1671
  this.#logger = options.logger ?? new Logger("silent");
1672
+ this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
1624
1673
  }
1625
1674
  on(event, listener) {
1626
1675
  return this.#events.on(event, listener);
@@ -1645,7 +1694,7 @@ var BaileysProvider = class {
1645
1694
  markOnlineOnConnect: false,
1646
1695
  enableAutoSessionRecreation: true,
1647
1696
  enableRecentMessageCache: true,
1648
- getMessage: async (key) => key.id ? this.#messageStore.get(key.id) : void 0
1697
+ getMessage: async (key) => this.#messageStore.get(this.#messageStoreKey(key))?.message ?? void 0
1649
1698
  });
1650
1699
  this.#socket = socket;
1651
1700
  this.#bind(socket);
@@ -1724,6 +1773,39 @@ var BaileysProvider = class {
1724
1773
  const result = await socket.sendMessage(chatId, this.#toContent(content), options);
1725
1774
  return this.#sent(result);
1726
1775
  }
1776
+ async reactToMessage(key, emoji) {
1777
+ const result = await this.#requireSocket().sendMessage(key.chatId, {
1778
+ react: {
1779
+ text: emoji ?? "",
1780
+ key: this.#toWaKey(key)
1781
+ }
1782
+ });
1783
+ return this.#sent(result);
1784
+ }
1785
+ async downloadMedia(key) {
1786
+ const message = this.#messageStore.get(this.#messageStoreKey(key));
1787
+ if (!message?.message) {
1788
+ throw new WhaNextError(
1789
+ "MEDIA_NOT_AVAILABLE",
1790
+ "The media is unavailable. Download it from the received message event while it is cached.",
1791
+ { recoverable: true }
1792
+ );
1793
+ }
1794
+ const data = await downloadMediaMessage(message, "buffer", {}, {
1795
+ reuploadRequest: (current) => this.#requireSocket().updateMediaMessage(current),
1796
+ logger: createBaileysLogger(this.#logger.child("media"))
1797
+ });
1798
+ const media = normalizeBaileysMessage(message)?.media;
1799
+ if (!media) {
1800
+ throw new WhaNextError("MEDIA_NOT_AVAILABLE", "The selected message does not contain media.");
1801
+ }
1802
+ return {
1803
+ data,
1804
+ kind: media.kind,
1805
+ ...media.mimetype ? { mimetype: media.mimetype } : {},
1806
+ ...media.fileName ? { fileName: media.fileName } : {}
1807
+ };
1808
+ }
1727
1809
  async editMessage(key, content) {
1728
1810
  const result = await this.#requireSocket().sendMessage(key.chatId, {
1729
1811
  text: content,
@@ -1792,7 +1874,6 @@ var BaileysProvider = class {
1792
1874
  }
1793
1875
  async setPresence(chatId, state) {
1794
1876
  const socket = this.#requireSocket();
1795
- await socket.presenceSubscribe(chatId);
1796
1877
  const presence = state === "typing" ? "composing" : state === "recording" ? "recording" : "paused";
1797
1878
  await socket.sendPresenceUpdate(presence, chatId);
1798
1879
  }
@@ -1809,7 +1890,7 @@ var BaileysProvider = class {
1809
1890
  socket.ev.on("messages.upsert", ({ messages, type }) => {
1810
1891
  if (type !== "notify") return;
1811
1892
  for (const raw of messages) {
1812
- if (raw.key.id && raw.message) this.#remember(raw.key.id, raw.message);
1893
+ if (raw.key.id && raw.message) this.#remember(raw);
1813
1894
  const message = normalizeBaileysMessage(raw);
1814
1895
  if (message) void this.#events.emit("message", message);
1815
1896
  }
@@ -1819,7 +1900,10 @@ var BaileysProvider = class {
1819
1900
  if (group.id) void this.#events.emit("groupChanged", { groupId: group.id });
1820
1901
  }
1821
1902
  });
1822
- socket.ev.on("group-participants.update", ({ id }) => {
1903
+ socket.ev.on("group-participants.update", (update) => {
1904
+ const change = this.#groupParticipantsChanged(update);
1905
+ void this.#events.emit("groupParticipantsChanged", change);
1906
+ const { id } = update;
1823
1907
  void this.#events.emit("groupChanged", { groupId: id });
1824
1908
  });
1825
1909
  socket.ev.on("call", (calls) => {
@@ -1919,6 +2003,9 @@ var BaileysProvider = class {
1919
2003
  ...content.gif !== void 0 ? { gifPlayback: content.gif } : {}
1920
2004
  };
1921
2005
  }
2006
+ if ("sticker" in content) {
2007
+ return { sticker: this.#media(content.sticker) };
2008
+ }
1922
2009
  return {
1923
2010
  audio: this.#media(content.audio),
1924
2011
  ...content.mimetype ? { mimetype: content.mimetype } : {},
@@ -1949,7 +2036,7 @@ var BaileysProvider = class {
1949
2036
  if (!message?.key.id || !message.key.remoteJid) {
1950
2037
  throw new WhaNextError("PROVIDER_ERROR", "WhatsApp did not confirm the sent message.");
1951
2038
  }
1952
- if (message.message) this.#remember(message.key.id, message.message);
2039
+ if (message.message) this.#remember(message);
1953
2040
  return {
1954
2041
  id: message.key.id,
1955
2042
  chatId: message.key.remoteJid,
@@ -1968,6 +2055,15 @@ var BaileysProvider = class {
1968
2055
  date: call.date ?? /* @__PURE__ */ new Date()
1969
2056
  };
1970
2057
  }
2058
+ #groupParticipantsChanged(change) {
2059
+ const participantIds = change.participants.map((participant) => participant.id).filter((id) => Boolean(id));
2060
+ return {
2061
+ groupId: change.id,
2062
+ action: change.action,
2063
+ participantIds,
2064
+ ...change.author ? { authorId: change.author } : {}
2065
+ };
2066
+ }
1971
2067
  #callStatus(status) {
1972
2068
  const known = [
1973
2069
  "offer",
@@ -1979,13 +2075,19 @@ var BaileysProvider = class {
1979
2075
  ];
1980
2076
  return known.find((value) => value === status) ?? "timeout";
1981
2077
  }
1982
- #remember(id, message) {
1983
- this.#messageStore.set(id, message);
1984
- if (this.#messageStore.size > 500) {
2078
+ #remember(message) {
2079
+ const key = this.#messageStoreKey(message.key);
2080
+ this.#messageStore.delete(key);
2081
+ this.#messageStore.set(key, message);
2082
+ while (this.#messageStore.size > this.#messageCacheSize) {
1985
2083
  const oldest = this.#messageStore.keys().next().value;
1986
2084
  if (oldest) this.#messageStore.delete(oldest);
1987
2085
  }
1988
2086
  }
2087
+ #messageStoreKey(key) {
2088
+ const chatId = "chatId" in key ? key.chatId : key.remoteJid;
2089
+ return `${chatId ?? ""}:${key.id ?? ""}`;
2090
+ }
1989
2091
  };
1990
2092
 
1991
2093
  // src/app/create.ts
@@ -1995,6 +2097,7 @@ async function create(options = {}) {
1995
2097
  auth: options.auth ?? "./session",
1996
2098
  browser: options.browser ?? "windows" /* Windows */,
1997
2099
  logger: logger.child("provider"),
2100
+ ...options.messageCacheSize !== void 0 ? { messageCacheSize: options.messageCacheSize } : {},
1998
2101
  ...options.reconnect ? { reconnect: options.reconnect } : {}
1999
2102
  });
2000
2103
  return new WhaNextApp(provider, {