@arcaelas/whatsapp 7.4.2 → 8.0.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.
@@ -7,7 +7,7 @@ import WhatsApp from './lib/whatsapp';
7
7
  export { WhatsApp };
8
8
  export default WhatsApp;
9
9
  export type IWhatsApp = ConstructorParameters<typeof WhatsApp>[0];
10
- export type { Farewell } from './lib/whatsapp';
10
+ export type { ChatWatch, ContactWatch, Farewell, MessageWatch, Presence, WatchEvent } from './lib/whatsapp';
11
11
  export type DisconnectOptions = NonNullable<Parameters<WhatsApp['disconnect']>[0]>;
12
12
  export type ReconnectOption = NonNullable<IWhatsApp['reconnect']>;
13
13
  export { FileSystemEngine, RedisEngine, S3Engine, SQLiteEngine, serialize, deserialize } from './lib/store';
@@ -8,6 +8,7 @@ import type Contact from '../../lib/contact';
8
8
  import type Message from '../../lib/message';
9
9
  import { type Engine } from '../../lib/store';
10
10
  import type WhatsApp from '../../lib/whatsapp';
11
+ import type { ChatWatch, WatchEvent } from '../../lib/whatsapp';
11
12
  /**
12
13
  * Chat: recibe el raw y deriva todo con getters.
13
14
  * Chat: receives the raw and derives everything via getters.
@@ -147,6 +148,23 @@ export declare function chat(init: {
147
148
  * Marca el chat completo como leído en la cuenta.
148
149
  * Marks the whole chat as read on the account.
149
150
  */
151
+ /**
152
+ * Supervisa la conversación entera: lo que hace la persona al otro lado —entra, sale,
153
+ * escribe, graba, deja de escribir, deja de grabar— y los mensajes que van llegando.
154
+ *
155
+ * No reimplementa nada: monta el `watch` del contacto dueño del chat y le suma los
156
+ * mensajes nuevos. En un grupo no hay una sola persona a la que seguir, así que sólo
157
+ * quedan los mensajes.
158
+ * Watches the whole conversation: what the person on the other end does —comes in, leaves,
159
+ * types, records, stops typing, stops recording— and the messages arriving.
160
+ *
161
+ * It reimplements nothing: it sets up the watch of the chat's owning contact and adds new
162
+ * messages to it. In a group there is no single person to follow, so only messages remain.
163
+ *
164
+ * @param handler - Recibe cada acción y cada mensaje / Receives every action and every message
165
+ * @returns Función para dejar de supervisar / Function to stop watching
166
+ */
167
+ watch(handler: (event: WatchEvent<ChatWatch, InstanceType<typeof init.wa.Contact> | Message>) => void): Promise<() => void>;
150
168
  seen(): Promise<boolean>;
151
169
  /**
152
170
  * Vacía los mensajes del chat en la cuenta y en el engine, conservando el chat.
@@ -264,6 +282,23 @@ export declare function chat(init: {
264
282
  * Marca el chat completo como leído en la cuenta.
265
283
  * Marks the whole chat as read on the account.
266
284
  */
285
+ /**
286
+ * Supervisa la conversación entera: lo que hace la persona al otro lado —entra, sale,
287
+ * escribe, graba, deja de escribir, deja de grabar— y los mensajes que van llegando.
288
+ *
289
+ * No reimplementa nada: monta el `watch` del contacto dueño del chat y le suma los
290
+ * mensajes nuevos. En un grupo no hay una sola persona a la que seguir, así que sólo
291
+ * quedan los mensajes.
292
+ * Watches the whole conversation: what the person on the other end does —comes in, leaves,
293
+ * types, records, stops typing, stops recording— and the messages arriving.
294
+ *
295
+ * It reimplements nothing: it sets up the watch of the chat's owning contact and adds new
296
+ * messages to it. In a group there is no single person to follow, so only messages remain.
297
+ *
298
+ * @param handler - Recibe cada acción y cada mensaje / Receives every action and every message
299
+ * @returns Función para dejar de supervisar / Function to stop watching
300
+ */
301
+ watch(handler: (event: WatchEvent<ChatWatch, InstanceType<typeof init.wa.Contact> | Message>) => void): Promise<() => void>;
267
302
  seen(): Promise<boolean>;
268
303
  /**
269
304
  * Vacía los mensajes del chat en la cuenta y en el engine, conservando el chat.
@@ -380,6 +415,23 @@ export declare function chat(init: {
380
415
  * Marca el chat completo como leído en la cuenta.
381
416
  * Marks the whole chat as read on the account.
382
417
  */
418
+ /**
419
+ * Supervisa la conversación entera: lo que hace la persona al otro lado —entra, sale,
420
+ * escribe, graba, deja de escribir, deja de grabar— y los mensajes que van llegando.
421
+ *
422
+ * No reimplementa nada: monta el `watch` del contacto dueño del chat y le suma los
423
+ * mensajes nuevos. En un grupo no hay una sola persona a la que seguir, así que sólo
424
+ * quedan los mensajes.
425
+ * Watches the whole conversation: what the person on the other end does —comes in, leaves,
426
+ * types, records, stops typing, stops recording— and the messages arriving.
427
+ *
428
+ * It reimplements nothing: it sets up the watch of the chat's owning contact and adds new
429
+ * messages to it. In a group there is no single person to follow, so only messages remain.
430
+ *
431
+ * @param handler - Recibe cada acción y cada mensaje / Receives every action and every message
432
+ * @returns Función para dejar de supervisar / Function to stop watching
433
+ */
434
+ watch(handler: (event: WatchEvent<ChatWatch, InstanceType<typeof init.wa.Contact> | Message>) => void): Promise<() => void>;
383
435
  seen(): Promise<boolean>;
384
436
  /**
385
437
  * Vacía los mensajes del chat en la cuenta y en el engine, conservando el chat.
@@ -213,6 +213,37 @@ function chat(init) {
213
213
  * Marca el chat completo como leído en la cuenta.
214
214
  * Marks the whole chat as read on the account.
215
215
  */
216
+ /**
217
+ * Supervisa la conversación entera: lo que hace la persona al otro lado —entra, sale,
218
+ * escribe, graba, deja de escribir, deja de grabar— y los mensajes que van llegando.
219
+ *
220
+ * No reimplementa nada: monta el `watch` del contacto dueño del chat y le suma los
221
+ * mensajes nuevos. En un grupo no hay una sola persona a la que seguir, así que sólo
222
+ * quedan los mensajes.
223
+ * Watches the whole conversation: what the person on the other end does —comes in, leaves,
224
+ * types, records, stops typing, stops recording— and the messages arriving.
225
+ *
226
+ * It reimplements nothing: it sets up the watch of the chat's owning contact and adds new
227
+ * messages to it. In a group there is no single person to follow, so only messages remain.
228
+ *
229
+ * @param handler - Recibe cada acción y cada mensaje / Receives every action and every message
230
+ * @returns Función para dejar de supervisar / Function to stop watching
231
+ */
232
+ async watch(handler) {
233
+ const off_message = init.wa.on('message:created', (msg) => {
234
+ if (msg.cid === this._raw.id)
235
+ handler({ name: 'message', payload: msg });
236
+ });
237
+ if (this.type === 'group') {
238
+ return off_message;
239
+ }
240
+ const who = await init.wa.Contact.get(this._raw.id);
241
+ const off_presence = (await who?.watch((event) => handler(event))) ?? (() => { });
242
+ return () => {
243
+ off_message();
244
+ off_presence();
245
+ };
246
+ }
216
247
  async seen() {
217
248
  const last = await tail(this._raw.id);
218
249
  // Marcar leído se acusa con recibos, no con una mutación del estado de la app.
@@ -7,6 +7,7 @@ import { type WASocket } from 'baileys';
7
7
  import { Feed } from '../../lib/status';
8
8
  import { type Engine } from '../../lib/store';
9
9
  import type WhatsApp from '../../lib/whatsapp';
10
+ import type { ContactWatch, WatchEvent } from '../../lib/whatsapp';
10
11
  /** Sesión activa que liga la entidad. / Active session binding the entity. */
11
12
  type Init = {
12
13
  wa: WhatsApp;
@@ -82,6 +83,24 @@ export declare function contact(init: Init): {
82
83
  * @returns Instancia de Chat / Chat instance
83
84
  */
84
85
  chat(): Promise<InstanceType<typeof init.wa.Chat>>;
86
+ /**
87
+ * Supervisa lo que hace el contacto: entra, sale, escribe, graba, deja de escribir o deja
88
+ * de grabar. Un aviso por acción, ya filtrado a esta persona.
89
+ *
90
+ * WhatsApp no difunde presencia por su cuenta —hay que pedirla contacto por contacto— y
91
+ * deja de mandarla al reconectar, así que la supervisión se vuelve a montar en cada
92
+ * sesión nueva.
93
+ * Watches what the contact does: comes in, leaves, types, records, stops typing or stops
94
+ * recording. One notice per action, already filtered down to this person.
95
+ *
96
+ * WhatsApp does not broadcast presence on its own —it must be asked for contact by
97
+ * contact— and stops sending it on reconnect, so the watch is set up again on every new
98
+ * session.
99
+ *
100
+ * @param handler - Recibe cada acción / Receives every action
101
+ * @returns Función para dejar de supervisar / Function to stop watching
102
+ */
103
+ watch(handler: (event: WatchEvent<ContactWatch, /*elided*/ any>) => void): Promise<() => void>;
85
104
  readonly _raw: {
86
105
  id: string;
87
106
  lid?: string | null;
@@ -120,6 +139,24 @@ export declare function contact(init: Init): {
120
139
  * @returns Instancia de Chat / Chat instance
121
140
  */
122
141
  chat(): Promise<InstanceType<typeof init.wa.Chat>>;
142
+ /**
143
+ * Supervisa lo que hace el contacto: entra, sale, escribe, graba, deja de escribir o deja
144
+ * de grabar. Un aviso por acción, ya filtrado a esta persona.
145
+ *
146
+ * WhatsApp no difunde presencia por su cuenta —hay que pedirla contacto por contacto— y
147
+ * deja de mandarla al reconectar, así que la supervisión se vuelve a montar en cada
148
+ * sesión nueva.
149
+ * Watches what the contact does: comes in, leaves, types, records, stops typing or stops
150
+ * recording. One notice per action, already filtered down to this person.
151
+ *
152
+ * WhatsApp does not broadcast presence on its own —it must be asked for contact by
153
+ * contact— and stops sending it on reconnect, so the watch is set up again on every new
154
+ * session.
155
+ *
156
+ * @param handler - Recibe cada acción / Receives every action
157
+ * @returns Función para dejar de supervisar / Function to stop watching
158
+ */
159
+ watch(handler: (event: WatchEvent<ContactWatch, /*elided*/ any>) => void): Promise<() => void>;
123
160
  readonly _raw: {
124
161
  id: string;
125
162
  lid?: string | null;
@@ -157,6 +194,24 @@ export declare function contact(init: Init): {
157
194
  * @returns Instancia de Chat / Chat instance
158
195
  */
159
196
  chat(): Promise<InstanceType<typeof init.wa.Chat>>;
197
+ /**
198
+ * Supervisa lo que hace el contacto: entra, sale, escribe, graba, deja de escribir o deja
199
+ * de grabar. Un aviso por acción, ya filtrado a esta persona.
200
+ *
201
+ * WhatsApp no difunde presencia por su cuenta —hay que pedirla contacto por contacto— y
202
+ * deja de mandarla al reconectar, así que la supervisión se vuelve a montar en cada
203
+ * sesión nueva.
204
+ * Watches what the contact does: comes in, leaves, types, records, stops typing or stops
205
+ * recording. One notice per action, already filtered down to this person.
206
+ *
207
+ * WhatsApp does not broadcast presence on its own —it must be asked for contact by
208
+ * contact— and stops sending it on reconnect, so the watch is set up again on every new
209
+ * session.
210
+ *
211
+ * @param handler - Recibe cada acción / Receives every action
212
+ * @returns Función para dejar de supervisar / Function to stop watching
213
+ */
214
+ watch(handler: (event: WatchEvent<ContactWatch, /*elided*/ any>) => void): Promise<() => void>;
160
215
  readonly _raw: {
161
216
  id: string;
162
217
  lid?: string | null;
@@ -68,6 +68,37 @@ function contact(init) {
68
68
  const cid = this.jid ?? this.lid ?? this._raw.id;
69
69
  return new init.wa.Chat((0, store_1.deserialize)(await init.engine.get(`/chat/${cid}`)) ?? { id: cid, name: this.name });
70
70
  }
71
+ /**
72
+ * Supervisa lo que hace el contacto: entra, sale, escribe, graba, deja de escribir o deja
73
+ * de grabar. Un aviso por acción, ya filtrado a esta persona.
74
+ *
75
+ * WhatsApp no difunde presencia por su cuenta —hay que pedirla contacto por contacto— y
76
+ * deja de mandarla al reconectar, así que la supervisión se vuelve a montar en cada
77
+ * sesión nueva.
78
+ * Watches what the contact does: comes in, leaves, types, records, stops typing or stops
79
+ * recording. One notice per action, already filtered down to this person.
80
+ *
81
+ * WhatsApp does not broadcast presence on its own —it must be asked for contact by
82
+ * contact— and stops sending it on reconnect, so the watch is set up again on every new
83
+ * session.
84
+ *
85
+ * @param handler - Recibe cada acción / Receives every action
86
+ * @returns Función para dejar de supervisar / Function to stop watching
87
+ */
88
+ async watch(handler) {
89
+ const jid = this.jid ?? this.lid ?? this._raw.id;
90
+ const mine = (jid.split('@')[0] ?? '').split(':')[0];
91
+ await init.socket.presenceSubscribe(jid);
92
+ return init.wa.on('contact:presence', (who, name) => {
93
+ // El mismo contacto llega unas veces por teléfono y otras por LID: comparar la
94
+ // parte identificadora es lo único que los reconoce como la misma persona.
95
+ // The same contact arrives sometimes by phone and sometimes by LID: comparing the
96
+ // identifying part is the only thing recognising them as the same person.
97
+ if ([who.jid, who.lid, who._raw.id].some((id) => (id ?? '').startsWith(mine))) {
98
+ handler({ name, payload: who });
99
+ }
100
+ });
101
+ }
71
102
  /**
72
103
  * Contacto por teléfono, JID o LID: primero el engine y, si no está persistido, se
73
104
  * descubre por red con su foto y su bio, y se materializa.
@@ -11,6 +11,7 @@ import Chat from '../../lib/chat';
11
11
  import type Contact from '../../lib/contact';
12
12
  import { type Engine } from '../../lib/store';
13
13
  import type WhatsApp from '../../lib/whatsapp';
14
+ import type { MessageWatch, WatchEvent } from '../../lib/whatsapp';
14
15
  /** Estados legibles indexados por el status numérico de baileys. / Readable states indexed by the baileys numeric status. */
15
16
  declare const STATUS: readonly ["error", "pending", "sent", "delivered", "read", "played"];
16
17
  /** Sesión activa que liga la entidad. / Active session binding the entity. */
@@ -154,6 +155,24 @@ export default class Message {
154
155
  * @param value - true destaca / true stars
155
156
  */
156
157
  star(value: boolean): Promise<boolean>;
158
+ /**
159
+ * Supervisa qué le pasa a este mensaje: que lo lean, que reproduzcan su audio o que lo
160
+ * retiren. Vive en la clase base, así que todo tipo de mensaje lo hereda.
161
+ *
162
+ * Por dentro escucha las actualizaciones del mensaje y las traduce: `message:updated` no
163
+ * distingue leído de reproducido —los dos son un cambio de estado— y quien supervisa un
164
+ * mensaje quiere saber cuál de los dos ocurrió, no que «algo cambió».
165
+ * Watches what happens to this message: that it gets read, that its audio gets played, or
166
+ * that it is retired. It lives on the base class, so every message type inherits it.
167
+ *
168
+ * Under the hood it listens to the message's updates and translates them: `message:updated`
169
+ * does not tell read from played —both are a status change— and whoever watches a message
170
+ * wants to know which of the two happened, not that «something changed».
171
+ *
172
+ * @param handler - Recibe cada cambio / Receives every change
173
+ * @returns Función para dejar de supervisar / Function to stop watching
174
+ */
175
+ watch(handler: (event: WatchEvent<MessageWatch, Message>) => void): () => void;
157
176
  /** Marca el mensaje como leído. / Marks the message as read. */
158
177
  seen(): Promise<boolean>;
159
178
  /**
@@ -292,6 +311,19 @@ export declare class Audio extends Media {
292
311
  get duration(): number;
293
312
  /** Forma de onda 0-100 lista para pintar. / Paint-ready 0-100 waveform. */
294
313
  get waveform(): number[];
314
+ /** true si ya fue reproducido: el micrófono azul que ve quien lo mandó. / true when already played: the blue mic its sender sees. */
315
+ get played(): boolean;
316
+ /**
317
+ * Acusa el audio como reproducido. Es un aviso aparte del de leído —abrir el chat no
318
+ * reproduce nada—, y por eso viaja como recibo propio: quien lo mandó ve el micrófono
319
+ * azul sólo después de esto.
320
+ * Acknowledges the audio as played. It is separate from the read receipt —opening the chat
321
+ * plays nothing— and so travels as its own receipt: whoever sent it sees the blue mic only
322
+ * after this.
323
+ *
324
+ * @returns true cuando el acuse salió / true once the receipt left
325
+ */
326
+ play(): Promise<boolean>;
295
327
  }
296
328
  /** Mensaje de sticker. / Sticker message. */
297
329
  export declare class Sticker extends Media {
@@ -511,6 +543,24 @@ export declare function message(init: Init): {
511
543
  * @param value - true destaca / true stars
512
544
  */
513
545
  star(value: boolean): Promise<boolean>;
546
+ /**
547
+ * Supervisa qué le pasa a este mensaje: que lo lean, que reproduzcan su audio o que lo
548
+ * retiren. Vive en la clase base, así que todo tipo de mensaje lo hereda.
549
+ *
550
+ * Por dentro escucha las actualizaciones del mensaje y las traduce: `message:updated` no
551
+ * distingue leído de reproducido —los dos son un cambio de estado— y quien supervisa un
552
+ * mensaje quiere saber cuál de los dos ocurrió, no que «algo cambió».
553
+ * Watches what happens to this message: that it gets read, that its audio gets played, or
554
+ * that it is retired. It lives on the base class, so every message type inherits it.
555
+ *
556
+ * Under the hood it listens to the message's updates and translates them: `message:updated`
557
+ * does not tell read from played —both are a status change— and whoever watches a message
558
+ * wants to know which of the two happened, not that «something changed».
559
+ *
560
+ * @param handler - Recibe cada cambio / Receives every change
561
+ * @returns Función para dejar de supervisar / Function to stop watching
562
+ */
563
+ watch(handler: (event: WatchEvent<MessageWatch, Message>) => void): () => void;
514
564
  /** Marca el mensaje como leído. / Marks the message as read. */
515
565
  seen(): Promise<boolean>;
516
566
  /**
@@ -295,6 +295,51 @@ class Message {
295
295
  await this._init.engine.set(`/chat/${doc.cid}/message/${doc.id}`, (0, store_1.serialize)(doc), doc.created_at);
296
296
  return true;
297
297
  }
298
+ /**
299
+ * Supervisa qué le pasa a este mensaje: que lo lean, que reproduzcan su audio o que lo
300
+ * retiren. Vive en la clase base, así que todo tipo de mensaje lo hereda.
301
+ *
302
+ * Por dentro escucha las actualizaciones del mensaje y las traduce: `message:updated` no
303
+ * distingue leído de reproducido —los dos son un cambio de estado— y quien supervisa un
304
+ * mensaje quiere saber cuál de los dos ocurrió, no que «algo cambió».
305
+ * Watches what happens to this message: that it gets read, that its audio gets played, or
306
+ * that it is retired. It lives on the base class, so every message type inherits it.
307
+ *
308
+ * Under the hood it listens to the message's updates and translates them: `message:updated`
309
+ * does not tell read from played —both are a status change— and whoever watches a message
310
+ * wants to know which of the two happened, not that «something changed».
311
+ *
312
+ * @param handler - Recibe cada cambio / Receives every change
313
+ * @returns Función para dejar de supervisar / Function to stop watching
314
+ */
315
+ watch(handler) {
316
+ const { wa } = this._init;
317
+ let seen = this._raw.status;
318
+ const off_updated = wa.on('message:updated', (msg) => {
319
+ if (msg.id === this._raw.id) {
320
+ const status = msg._raw.status;
321
+ // Sólo los avances cuentan: un mismo estado repetido no es una noticia, y sin
322
+ // este corte cada acuse reenviado por WhatsApp se contaría como una lectura más.
323
+ // Only advances count: a repeated status is not news, and without this cut every
324
+ // acknowledgement WhatsApp resends would count as another read.
325
+ if (status > seen) {
326
+ seen = status;
327
+ if (status >= baileys_1.proto.WebMessageInfo.Status.PLAYED)
328
+ handler({ name: 'played', payload: msg });
329
+ else if (status >= baileys_1.proto.WebMessageInfo.Status.READ)
330
+ handler({ name: 'read', payload: msg });
331
+ }
332
+ }
333
+ });
334
+ const off_deleted = wa.on('message:deleted', (msg) => {
335
+ if (msg.id === this._raw.id)
336
+ handler({ name: 'deleted', payload: msg });
337
+ });
338
+ return () => {
339
+ off_updated();
340
+ off_deleted();
341
+ };
342
+ }
298
343
  /** Marca el mensaje como leído. / Marks the message as read. */
299
344
  async seen() {
300
345
  const { cid, id, author } = this._raw;
@@ -478,6 +523,23 @@ class Audio extends Media {
478
523
  get duration() { return this._media?.seconds ?? 0; }
479
524
  /** Forma de onda 0-100 lista para pintar. / Paint-ready 0-100 waveform. */
480
525
  get waveform() { return Array.from(to_buffer(this._media?.waveform) ?? []); }
526
+ /** true si ya fue reproducido: el micrófono azul que ve quien lo mandó. / true when already played: the blue mic its sender sees. */
527
+ get played() { return this._raw.status >= baileys_1.proto.WebMessageInfo.Status.PLAYED; }
528
+ /**
529
+ * Acusa el audio como reproducido. Es un aviso aparte del de leído —abrir el chat no
530
+ * reproduce nada—, y por eso viaja como recibo propio: quien lo mandó ve el micrófono
531
+ * azul sólo después de esto.
532
+ * Acknowledges the audio as played. It is separate from the read receipt —opening the chat
533
+ * plays nothing— and so travels as its own receipt: whoever sent it sees the blue mic only
534
+ * after this.
535
+ *
536
+ * @returns true cuando el acuse salió / true once the receipt left
537
+ */
538
+ async play() {
539
+ const { cid, id, author } = this._raw;
540
+ await this._init.socket.sendReceipt(cid, cid.endsWith('@g.us') ? author : undefined, [id], 'played');
541
+ return true;
542
+ }
481
543
  }
482
544
  exports.Audio = Audio;
483
545
  /** Mensaje de sticker. / Sticker message. */
@@ -3,6 +3,33 @@ import { Account, contact } from '../../lib/contact';
3
3
  import Message, { message } from '../../lib/message';
4
4
  import { Feed } from '../../lib/status';
5
5
  import { type Engine } from '../../lib/store';
6
+ /**
7
+ * Lo que hace alguien al otro lado, en los términos en los que se mira: WhatsApp distingue
8
+ * «disponible» de «escribiendo» y de «grabando», y esa distinción es justo la que interesa
9
+ * vigilar. `paused` no se propaga: es dejar de escribir, no un estado en sí.
10
+ * What someone is doing on the other end, in the terms one watches it: WhatsApp tells «available»
11
+ * from «typing» and from «recording», and that distinction is exactly what is worth watching.
12
+ * `paused` is not propagated: it is stopping typing, not a state of its own.
13
+ */
14
+ export type Presence = 'online' | 'offline' | 'typing' | 'recording';
15
+ /**
16
+ * Lo que se supervisa con `watch()`, en un solo sobre: `name` dice qué pasó y `payload` trae la
17
+ * instancia a la que le pasó, ya resuelta. Quien escucha no tiene que volver a buscar de quién
18
+ * se trata ni filtrar lo que no le toca.
19
+ * What `watch()` supervises, in a single envelope: `name` says what happened and `payload` brings
20
+ * the already-resolved instance it happened to. Whoever listens does not have to look up who it
21
+ * was nor filter out what does not concern them.
22
+ */
23
+ export interface WatchEvent<N extends string, P> {
24
+ name: N;
25
+ payload: P;
26
+ }
27
+ /** Cada acción de alguien al otro lado, incluido dejar de escribir o de grabar. / Every action from the other end, including stopping typing or recording. */
28
+ export type ContactWatch = 'online' | 'offline' | 'typing' | 'recording' | 'stopped-typing' | 'stopped-recording';
29
+ /** Lo que le puede pasar a un mensaje ya enviado. / What can happen to an already-sent message. */
30
+ export type MessageWatch = 'read' | 'played' | 'deleted';
31
+ /** Un chat mezcla lo que hace la persona con lo que llega a la conversación. / A chat mixes what the person does with what arrives in the conversation. */
32
+ export type ChatWatch = ContactWatch | 'message';
6
33
  type ChatInstance = InstanceType<ReturnType<typeof chat>>;
7
34
  type ContactInstance = InstanceType<ReturnType<typeof contact>>;
8
35
  interface Options {
@@ -63,8 +90,14 @@ export interface Farewell {
63
90
  interface EventMap {
64
91
  connected: [WhatsApp];
65
92
  disconnected: [WhatsApp, Farewell];
93
+ /** Fallo que no tumba la conexión pero que quien la abrió necesita saber. / A failure that does not drop the connection but whoever opened it needs to know. */
94
+ error: [Error & {
95
+ code?: string;
96
+ }, WhatsApp];
66
97
  'contact:created': [ContactInstance, ChatInstance, WhatsApp];
67
98
  'contact:updated': [ContactInstance, ChatInstance, WhatsApp];
99
+ /** Alguien entró, salió, escribe o graba. Sólo llega de quien se esté vigilando con `Contact.watch()`. / Someone came in, left, is typing or recording. Only arrives for whoever is being watched with `Contact.watch()`. */
100
+ 'contact:presence': [ContactInstance, ContactWatch, WhatsApp];
68
101
  'chat:created': [ChatInstance, WhatsApp];
69
102
  'chat:deleted': [ChatInstance, WhatsApp];
70
103
  'chat:pinned': [ChatInstance, WhatsApp];
@@ -72,7 +72,18 @@ const queued = (locks, path, work) => {
72
72
  locks.set(path, next.catch(() => { }));
73
73
  return next;
74
74
  };
75
+ /** Traduce el vocabulario de baileys al que se vigila; lo que no está aquí no se propaga. / Translates baileys' vocabulary into the watched one; whatever is missing is not propagated. */
76
+ const PRESENCE = { available: 'online', unavailable: 'offline', composing: 'typing', recording: 'recording', paused: 'paused' };
75
77
  const readable = (value) => (value && !/^\+?[\d\s·•∙⋅]+$/.test(value) ? value : null);
78
+ /**
79
+ * Refrescos de QR que dura un PIN antes de darlo por caducado. WhatsApp no avisa de la
80
+ * expiración, así que se mide por los ciclos de refresco (~20 s cada uno): tres son el margen
81
+ * observado en el que un código sigue siendo aceptado.
82
+ * QR refreshes a PIN lasts before it is considered expired. WhatsApp gives no expiry notice, so
83
+ * it is measured in refresh cycles (~20s each): three is the observed window in which a code is
84
+ * still accepted.
85
+ */
86
+ const OTP_CYCLES = 3;
76
87
  class WhatsApp {
77
88
  #event = new node_events_1.EventEmitter();
78
89
  #options;
@@ -115,9 +126,13 @@ class WhatsApp {
115
126
  let retries = 0;
116
127
  let intentional = false;
117
128
  let silent = false;
129
+ let paired = false;
130
+ let cycles = 0;
118
131
  let alive = null;
119
132
  let timer = null;
120
133
  let chain = Promise.resolve();
134
+ /** Qué estaba haciendo cada quien, para poder leer su `paused`. / What each one was doing, so their `paused` can be read. */
135
+ const doing = new Map();
121
136
  const locks = new Map();
122
137
  return new Promise((resolve, reject) => {
123
138
  const start = async () => {
@@ -296,9 +311,33 @@ class WhatsApp {
296
311
  socket.ev.on('creds.update', () => queued(locks, '/session/creds', () => engine.set('/session/creds', (0, store_1.serialize)(creds))));
297
312
  socket.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
298
313
  if (qr && !creds.registered) {
299
- await callback(digits && (method ?? 'otp') === 'otp'
300
- ? await socket.requestPairingCode(digits)
301
- : await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
314
+ if (digits && (method ?? 'otp') === 'otp') {
315
+ // El QR se refresca cada ~20 s, pero el PIN vive más: pedir uno en
316
+ // cada refresco invalida el anterior y el que la persona está
317
+ // tecleando deja de servir a media escritura —el síntoma es un PIN
318
+ // correcto que «no lo toma»—. Por eso se cuenta el ciclo y sólo se
319
+ // renueva al caducar de verdad, gastando un reintento del presupuesto.
320
+ // The QR refreshes every ~20s, but the PIN lives longer: asking for
321
+ // one on each refresh invalidates the previous, and the one being
322
+ // typed stops working mid-typing —the symptom is a correct PIN that
323
+ // «is not accepted»—. So the cycle is counted and it is only renewed
324
+ // once it truly expires, spending one retry from the budget.
325
+ cycles = paired ? cycles + 1 : 0;
326
+ if (!paired || cycles >= OTP_CYCLES) {
327
+ if (paired && budget !== null && retries >= budget) {
328
+ this.emit('error', Object.assign(new Error('El código de vinculación expiró'), { code: 'ERR_OTP_EXPIRED' }), this);
329
+ }
330
+ else {
331
+ retries += paired ? 1 : 0;
332
+ cycles = 0;
333
+ paired = true;
334
+ await callback(await socket.requestPairingCode(digits));
335
+ }
336
+ }
337
+ }
338
+ else {
339
+ await callback(await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
340
+ }
302
341
  }
303
342
  if (connection === 'open') {
304
343
  connected = true;
@@ -409,6 +448,31 @@ class WhatsApp {
409
448
  }
410
449
  }).catch(() => { });
411
450
  });
451
+ socket.ev.on('presence.update', ({ id, presences }) => {
452
+ chain = chain.then(async () => {
453
+ const cid = await canonical(id);
454
+ for (const [participant, data] of Object.entries(presences)) {
455
+ const state = PRESENCE[data.lastKnownPresence];
456
+ if (state) {
457
+ const who = await canonical(participant || cid);
458
+ // `paused` es «dejó de hacer lo que hacía», y sólo el estado
459
+ // anterior dice qué era: sin recordarlo no se puede distinguir
460
+ // dejar de escribir de dejar de grabar.
461
+ // `paused` means «stopped doing what they were doing», and only
462
+ // the previous state says which: without remembering it there is
463
+ // no telling stopped-typing from stopped-recording.
464
+ const last = doing.get(who);
465
+ const name = state === 'paused' ? (last === 'recording' ? 'stopped-recording' : 'stopped-typing') : state;
466
+ if (state === 'typing' || state === 'recording')
467
+ doing.set(who, state);
468
+ else
469
+ doing.delete(who);
470
+ const card = (0, store_1.deserialize)(await engine.get(`/contact/${who}`));
471
+ this.emit('contact:presence', new this.Contact(card ?? { id: who, lid: null, name: null, notify: null, verified_name: null, img_url: null, status: null }), name, this);
472
+ }
473
+ }
474
+ }).catch(() => { });
475
+ });
412
476
  socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
413
477
  chain = chain.then(async () => {
414
478
  await remember(lid, pn);
@@ -531,7 +595,18 @@ class WhatsApp {
531
595
  continue;
532
596
  }
533
597
  const type = { conversation: 'text', extendedTextMessage: 'text', imageMessage: 'image', videoMessage: 'video', audioMessage: 'audio' }[kind ?? ''];
534
- const author = msg.key.participant ?? '';
598
+ // El autor de un estado puede llegar por `participant` o por su
599
+ // alterno según venga identificado por teléfono o por LID, y se
600
+ // guarda canónico como todo lo demás: con el LID crudo el estado
601
+ // queda a nombre de un número larguísimo que no case con ningún
602
+ // contacto, y para quien mira es un estado que no llegó.
603
+ // A status author can arrive via `participant` or its alternate
604
+ // depending on whether it is identified by phone or by LID, and
605
+ // is stored canonically like everything else: with the raw LID
606
+ // the status ends up under a long meaningless number matching no
607
+ // contact, and to whoever looks it is a status that never came.
608
+ const claimed = msg.key.participant ?? msg.key.participantAlt ?? '';
609
+ const author = claimed ? await canonical(claimed) : '';
535
610
  if (!type || !author) {
536
611
  continue;
537
612
  }
@@ -7,7 +7,7 @@ import WhatsApp from './lib/whatsapp/index.js';
7
7
  export { WhatsApp };
8
8
  export default WhatsApp;
9
9
  export type IWhatsApp = ConstructorParameters<typeof WhatsApp>[0];
10
- export type { Farewell } from './lib/whatsapp/index.js';
10
+ export type { ChatWatch, ContactWatch, Farewell, MessageWatch, Presence, WatchEvent } from './lib/whatsapp/index.js';
11
11
  export type DisconnectOptions = NonNullable<Parameters<WhatsApp['disconnect']>[0]>;
12
12
  export type ReconnectOption = NonNullable<IWhatsApp['reconnect']>;
13
13
  export { FileSystemEngine, RedisEngine, S3Engine, SQLiteEngine, serialize, deserialize } from './lib/store/index.js';
@@ -8,6 +8,7 @@ import type Contact from '../../lib/contact/index.js';
8
8
  import type Message from '../../lib/message/index.js';
9
9
  import { type Engine } from '../../lib/store/index.js';
10
10
  import type WhatsApp from '../../lib/whatsapp/index.js';
11
+ import type { ChatWatch, WatchEvent } from '../../lib/whatsapp/index.js';
11
12
  /**
12
13
  * Chat: recibe el raw y deriva todo con getters.
13
14
  * Chat: receives the raw and derives everything via getters.
@@ -147,6 +148,23 @@ export declare function chat(init: {
147
148
  * Marca el chat completo como leído en la cuenta.
148
149
  * Marks the whole chat as read on the account.
149
150
  */
151
+ /**
152
+ * Supervisa la conversación entera: lo que hace la persona al otro lado —entra, sale,
153
+ * escribe, graba, deja de escribir, deja de grabar— y los mensajes que van llegando.
154
+ *
155
+ * No reimplementa nada: monta el `watch` del contacto dueño del chat y le suma los
156
+ * mensajes nuevos. En un grupo no hay una sola persona a la que seguir, así que sólo
157
+ * quedan los mensajes.
158
+ * Watches the whole conversation: what the person on the other end does —comes in, leaves,
159
+ * types, records, stops typing, stops recording— and the messages arriving.
160
+ *
161
+ * It reimplements nothing: it sets up the watch of the chat's owning contact and adds new
162
+ * messages to it. In a group there is no single person to follow, so only messages remain.
163
+ *
164
+ * @param handler - Recibe cada acción y cada mensaje / Receives every action and every message
165
+ * @returns Función para dejar de supervisar / Function to stop watching
166
+ */
167
+ watch(handler: (event: WatchEvent<ChatWatch, InstanceType<typeof init.wa.Contact> | Message>) => void): Promise<() => void>;
150
168
  seen(): Promise<boolean>;
151
169
  /**
152
170
  * Vacía los mensajes del chat en la cuenta y en el engine, conservando el chat.
@@ -264,6 +282,23 @@ export declare function chat(init: {
264
282
  * Marca el chat completo como leído en la cuenta.
265
283
  * Marks the whole chat as read on the account.
266
284
  */
285
+ /**
286
+ * Supervisa la conversación entera: lo que hace la persona al otro lado —entra, sale,
287
+ * escribe, graba, deja de escribir, deja de grabar— y los mensajes que van llegando.
288
+ *
289
+ * No reimplementa nada: monta el `watch` del contacto dueño del chat y le suma los
290
+ * mensajes nuevos. En un grupo no hay una sola persona a la que seguir, así que sólo
291
+ * quedan los mensajes.
292
+ * Watches the whole conversation: what the person on the other end does —comes in, leaves,
293
+ * types, records, stops typing, stops recording— and the messages arriving.
294
+ *
295
+ * It reimplements nothing: it sets up the watch of the chat's owning contact and adds new
296
+ * messages to it. In a group there is no single person to follow, so only messages remain.
297
+ *
298
+ * @param handler - Recibe cada acción y cada mensaje / Receives every action and every message
299
+ * @returns Función para dejar de supervisar / Function to stop watching
300
+ */
301
+ watch(handler: (event: WatchEvent<ChatWatch, InstanceType<typeof init.wa.Contact> | Message>) => void): Promise<() => void>;
267
302
  seen(): Promise<boolean>;
268
303
  /**
269
304
  * Vacía los mensajes del chat en la cuenta y en el engine, conservando el chat.
@@ -380,6 +415,23 @@ export declare function chat(init: {
380
415
  * Marca el chat completo como leído en la cuenta.
381
416
  * Marks the whole chat as read on the account.
382
417
  */
418
+ /**
419
+ * Supervisa la conversación entera: lo que hace la persona al otro lado —entra, sale,
420
+ * escribe, graba, deja de escribir, deja de grabar— y los mensajes que van llegando.
421
+ *
422
+ * No reimplementa nada: monta el `watch` del contacto dueño del chat y le suma los
423
+ * mensajes nuevos. En un grupo no hay una sola persona a la que seguir, así que sólo
424
+ * quedan los mensajes.
425
+ * Watches the whole conversation: what the person on the other end does —comes in, leaves,
426
+ * types, records, stops typing, stops recording— and the messages arriving.
427
+ *
428
+ * It reimplements nothing: it sets up the watch of the chat's owning contact and adds new
429
+ * messages to it. In a group there is no single person to follow, so only messages remain.
430
+ *
431
+ * @param handler - Recibe cada acción y cada mensaje / Receives every action and every message
432
+ * @returns Función para dejar de supervisar / Function to stop watching
433
+ */
434
+ watch(handler: (event: WatchEvent<ChatWatch, InstanceType<typeof init.wa.Contact> | Message>) => void): Promise<() => void>;
383
435
  seen(): Promise<boolean>;
384
436
  /**
385
437
  * Vacía los mensajes del chat en la cuenta y en el engine, conservando el chat.
@@ -209,6 +209,37 @@ export function chat(init) {
209
209
  * Marca el chat completo como leído en la cuenta.
210
210
  * Marks the whole chat as read on the account.
211
211
  */
212
+ /**
213
+ * Supervisa la conversación entera: lo que hace la persona al otro lado —entra, sale,
214
+ * escribe, graba, deja de escribir, deja de grabar— y los mensajes que van llegando.
215
+ *
216
+ * No reimplementa nada: monta el `watch` del contacto dueño del chat y le suma los
217
+ * mensajes nuevos. En un grupo no hay una sola persona a la que seguir, así que sólo
218
+ * quedan los mensajes.
219
+ * Watches the whole conversation: what the person on the other end does —comes in, leaves,
220
+ * types, records, stops typing, stops recording— and the messages arriving.
221
+ *
222
+ * It reimplements nothing: it sets up the watch of the chat's owning contact and adds new
223
+ * messages to it. In a group there is no single person to follow, so only messages remain.
224
+ *
225
+ * @param handler - Recibe cada acción y cada mensaje / Receives every action and every message
226
+ * @returns Función para dejar de supervisar / Function to stop watching
227
+ */
228
+ async watch(handler) {
229
+ const off_message = init.wa.on('message:created', (msg) => {
230
+ if (msg.cid === this._raw.id)
231
+ handler({ name: 'message', payload: msg });
232
+ });
233
+ if (this.type === 'group') {
234
+ return off_message;
235
+ }
236
+ const who = await init.wa.Contact.get(this._raw.id);
237
+ const off_presence = (await who?.watch((event) => handler(event))) ?? (() => { });
238
+ return () => {
239
+ off_message();
240
+ off_presence();
241
+ };
242
+ }
212
243
  async seen() {
213
244
  const last = await tail(this._raw.id);
214
245
  // Marcar leído se acusa con recibos, no con una mutación del estado de la app.
@@ -7,6 +7,7 @@ import { type WASocket } from 'baileys';
7
7
  import { Feed } from '../../lib/status/index.js';
8
8
  import { type Engine } from '../../lib/store/index.js';
9
9
  import type WhatsApp from '../../lib/whatsapp/index.js';
10
+ import type { ContactWatch, WatchEvent } from '../../lib/whatsapp/index.js';
10
11
  /** Sesión activa que liga la entidad. / Active session binding the entity. */
11
12
  type Init = {
12
13
  wa: WhatsApp;
@@ -82,6 +83,24 @@ export declare function contact(init: Init): {
82
83
  * @returns Instancia de Chat / Chat instance
83
84
  */
84
85
  chat(): Promise<InstanceType<typeof init.wa.Chat>>;
86
+ /**
87
+ * Supervisa lo que hace el contacto: entra, sale, escribe, graba, deja de escribir o deja
88
+ * de grabar. Un aviso por acción, ya filtrado a esta persona.
89
+ *
90
+ * WhatsApp no difunde presencia por su cuenta —hay que pedirla contacto por contacto— y
91
+ * deja de mandarla al reconectar, así que la supervisión se vuelve a montar en cada
92
+ * sesión nueva.
93
+ * Watches what the contact does: comes in, leaves, types, records, stops typing or stops
94
+ * recording. One notice per action, already filtered down to this person.
95
+ *
96
+ * WhatsApp does not broadcast presence on its own —it must be asked for contact by
97
+ * contact— and stops sending it on reconnect, so the watch is set up again on every new
98
+ * session.
99
+ *
100
+ * @param handler - Recibe cada acción / Receives every action
101
+ * @returns Función para dejar de supervisar / Function to stop watching
102
+ */
103
+ watch(handler: (event: WatchEvent<ContactWatch, /*elided*/ any>) => void): Promise<() => void>;
85
104
  readonly _raw: {
86
105
  id: string;
87
106
  lid?: string | null;
@@ -120,6 +139,24 @@ export declare function contact(init: Init): {
120
139
  * @returns Instancia de Chat / Chat instance
121
140
  */
122
141
  chat(): Promise<InstanceType<typeof init.wa.Chat>>;
142
+ /**
143
+ * Supervisa lo que hace el contacto: entra, sale, escribe, graba, deja de escribir o deja
144
+ * de grabar. Un aviso por acción, ya filtrado a esta persona.
145
+ *
146
+ * WhatsApp no difunde presencia por su cuenta —hay que pedirla contacto por contacto— y
147
+ * deja de mandarla al reconectar, así que la supervisión se vuelve a montar en cada
148
+ * sesión nueva.
149
+ * Watches what the contact does: comes in, leaves, types, records, stops typing or stops
150
+ * recording. One notice per action, already filtered down to this person.
151
+ *
152
+ * WhatsApp does not broadcast presence on its own —it must be asked for contact by
153
+ * contact— and stops sending it on reconnect, so the watch is set up again on every new
154
+ * session.
155
+ *
156
+ * @param handler - Recibe cada acción / Receives every action
157
+ * @returns Función para dejar de supervisar / Function to stop watching
158
+ */
159
+ watch(handler: (event: WatchEvent<ContactWatch, /*elided*/ any>) => void): Promise<() => void>;
123
160
  readonly _raw: {
124
161
  id: string;
125
162
  lid?: string | null;
@@ -157,6 +194,24 @@ export declare function contact(init: Init): {
157
194
  * @returns Instancia de Chat / Chat instance
158
195
  */
159
196
  chat(): Promise<InstanceType<typeof init.wa.Chat>>;
197
+ /**
198
+ * Supervisa lo que hace el contacto: entra, sale, escribe, graba, deja de escribir o deja
199
+ * de grabar. Un aviso por acción, ya filtrado a esta persona.
200
+ *
201
+ * WhatsApp no difunde presencia por su cuenta —hay que pedirla contacto por contacto— y
202
+ * deja de mandarla al reconectar, así que la supervisión se vuelve a montar en cada
203
+ * sesión nueva.
204
+ * Watches what the contact does: comes in, leaves, types, records, stops typing or stops
205
+ * recording. One notice per action, already filtered down to this person.
206
+ *
207
+ * WhatsApp does not broadcast presence on its own —it must be asked for contact by
208
+ * contact— and stops sending it on reconnect, so the watch is set up again on every new
209
+ * session.
210
+ *
211
+ * @param handler - Recibe cada acción / Receives every action
212
+ * @returns Función para dejar de supervisar / Function to stop watching
213
+ */
214
+ watch(handler: (event: WatchEvent<ContactWatch, /*elided*/ any>) => void): Promise<() => void>;
160
215
  readonly _raw: {
161
216
  id: string;
162
217
  lid?: string | null;
@@ -63,6 +63,37 @@ export function contact(init) {
63
63
  const cid = this.jid ?? this.lid ?? this._raw.id;
64
64
  return new init.wa.Chat(deserialize(await init.engine.get(`/chat/${cid}`)) ?? { id: cid, name: this.name });
65
65
  }
66
+ /**
67
+ * Supervisa lo que hace el contacto: entra, sale, escribe, graba, deja de escribir o deja
68
+ * de grabar. Un aviso por acción, ya filtrado a esta persona.
69
+ *
70
+ * WhatsApp no difunde presencia por su cuenta —hay que pedirla contacto por contacto— y
71
+ * deja de mandarla al reconectar, así que la supervisión se vuelve a montar en cada
72
+ * sesión nueva.
73
+ * Watches what the contact does: comes in, leaves, types, records, stops typing or stops
74
+ * recording. One notice per action, already filtered down to this person.
75
+ *
76
+ * WhatsApp does not broadcast presence on its own —it must be asked for contact by
77
+ * contact— and stops sending it on reconnect, so the watch is set up again on every new
78
+ * session.
79
+ *
80
+ * @param handler - Recibe cada acción / Receives every action
81
+ * @returns Función para dejar de supervisar / Function to stop watching
82
+ */
83
+ async watch(handler) {
84
+ const jid = this.jid ?? this.lid ?? this._raw.id;
85
+ const mine = (jid.split('@')[0] ?? '').split(':')[0];
86
+ await init.socket.presenceSubscribe(jid);
87
+ return init.wa.on('contact:presence', (who, name) => {
88
+ // El mismo contacto llega unas veces por teléfono y otras por LID: comparar la
89
+ // parte identificadora es lo único que los reconoce como la misma persona.
90
+ // The same contact arrives sometimes by phone and sometimes by LID: comparing the
91
+ // identifying part is the only thing recognising them as the same person.
92
+ if ([who.jid, who.lid, who._raw.id].some((id) => (id ?? '').startsWith(mine))) {
93
+ handler({ name, payload: who });
94
+ }
95
+ });
96
+ }
66
97
  /**
67
98
  * Contacto por teléfono, JID o LID: primero el engine y, si no está persistido, se
68
99
  * descubre por red con su foto y su bio, y se materializa.
@@ -11,6 +11,7 @@ import Chat from '../../lib/chat/index.js';
11
11
  import type Contact from '../../lib/contact/index.js';
12
12
  import { type Engine } from '../../lib/store/index.js';
13
13
  import type WhatsApp from '../../lib/whatsapp/index.js';
14
+ import type { MessageWatch, WatchEvent } from '../../lib/whatsapp/index.js';
14
15
  /** Estados legibles indexados por el status numérico de baileys. / Readable states indexed by the baileys numeric status. */
15
16
  declare const STATUS: readonly ["error", "pending", "sent", "delivered", "read", "played"];
16
17
  /** Sesión activa que liga la entidad. / Active session binding the entity. */
@@ -154,6 +155,24 @@ export default class Message {
154
155
  * @param value - true destaca / true stars
155
156
  */
156
157
  star(value: boolean): Promise<boolean>;
158
+ /**
159
+ * Supervisa qué le pasa a este mensaje: que lo lean, que reproduzcan su audio o que lo
160
+ * retiren. Vive en la clase base, así que todo tipo de mensaje lo hereda.
161
+ *
162
+ * Por dentro escucha las actualizaciones del mensaje y las traduce: `message:updated` no
163
+ * distingue leído de reproducido —los dos son un cambio de estado— y quien supervisa un
164
+ * mensaje quiere saber cuál de los dos ocurrió, no que «algo cambió».
165
+ * Watches what happens to this message: that it gets read, that its audio gets played, or
166
+ * that it is retired. It lives on the base class, so every message type inherits it.
167
+ *
168
+ * Under the hood it listens to the message's updates and translates them: `message:updated`
169
+ * does not tell read from played —both are a status change— and whoever watches a message
170
+ * wants to know which of the two happened, not that «something changed».
171
+ *
172
+ * @param handler - Recibe cada cambio / Receives every change
173
+ * @returns Función para dejar de supervisar / Function to stop watching
174
+ */
175
+ watch(handler: (event: WatchEvent<MessageWatch, Message>) => void): () => void;
157
176
  /** Marca el mensaje como leído. / Marks the message as read. */
158
177
  seen(): Promise<boolean>;
159
178
  /**
@@ -292,6 +311,19 @@ export declare class Audio extends Media {
292
311
  get duration(): number;
293
312
  /** Forma de onda 0-100 lista para pintar. / Paint-ready 0-100 waveform. */
294
313
  get waveform(): number[];
314
+ /** true si ya fue reproducido: el micrófono azul que ve quien lo mandó. / true when already played: the blue mic its sender sees. */
315
+ get played(): boolean;
316
+ /**
317
+ * Acusa el audio como reproducido. Es un aviso aparte del de leído —abrir el chat no
318
+ * reproduce nada—, y por eso viaja como recibo propio: quien lo mandó ve el micrófono
319
+ * azul sólo después de esto.
320
+ * Acknowledges the audio as played. It is separate from the read receipt —opening the chat
321
+ * plays nothing— and so travels as its own receipt: whoever sent it sees the blue mic only
322
+ * after this.
323
+ *
324
+ * @returns true cuando el acuse salió / true once the receipt left
325
+ */
326
+ play(): Promise<boolean>;
295
327
  }
296
328
  /** Mensaje de sticker. / Sticker message. */
297
329
  export declare class Sticker extends Media {
@@ -511,6 +543,24 @@ export declare function message(init: Init): {
511
543
  * @param value - true destaca / true stars
512
544
  */
513
545
  star(value: boolean): Promise<boolean>;
546
+ /**
547
+ * Supervisa qué le pasa a este mensaje: que lo lean, que reproduzcan su audio o que lo
548
+ * retiren. Vive en la clase base, así que todo tipo de mensaje lo hereda.
549
+ *
550
+ * Por dentro escucha las actualizaciones del mensaje y las traduce: `message:updated` no
551
+ * distingue leído de reproducido —los dos son un cambio de estado— y quien supervisa un
552
+ * mensaje quiere saber cuál de los dos ocurrió, no que «algo cambió».
553
+ * Watches what happens to this message: that it gets read, that its audio gets played, or
554
+ * that it is retired. It lives on the base class, so every message type inherits it.
555
+ *
556
+ * Under the hood it listens to the message's updates and translates them: `message:updated`
557
+ * does not tell read from played —both are a status change— and whoever watches a message
558
+ * wants to know which of the two happened, not that «something changed».
559
+ *
560
+ * @param handler - Recibe cada cambio / Receives every change
561
+ * @returns Función para dejar de supervisar / Function to stop watching
562
+ */
563
+ watch(handler: (event: WatchEvent<MessageWatch, Message>) => void): () => void;
514
564
  /** Marca el mensaje como leído. / Marks the message as read. */
515
565
  seen(): Promise<boolean>;
516
566
  /**
@@ -288,6 +288,51 @@ export default class Message {
288
288
  await this._init.engine.set(`/chat/${doc.cid}/message/${doc.id}`, serialize(doc), doc.created_at);
289
289
  return true;
290
290
  }
291
+ /**
292
+ * Supervisa qué le pasa a este mensaje: que lo lean, que reproduzcan su audio o que lo
293
+ * retiren. Vive en la clase base, así que todo tipo de mensaje lo hereda.
294
+ *
295
+ * Por dentro escucha las actualizaciones del mensaje y las traduce: `message:updated` no
296
+ * distingue leído de reproducido —los dos son un cambio de estado— y quien supervisa un
297
+ * mensaje quiere saber cuál de los dos ocurrió, no que «algo cambió».
298
+ * Watches what happens to this message: that it gets read, that its audio gets played, or
299
+ * that it is retired. It lives on the base class, so every message type inherits it.
300
+ *
301
+ * Under the hood it listens to the message's updates and translates them: `message:updated`
302
+ * does not tell read from played —both are a status change— and whoever watches a message
303
+ * wants to know which of the two happened, not that «something changed».
304
+ *
305
+ * @param handler - Recibe cada cambio / Receives every change
306
+ * @returns Función para dejar de supervisar / Function to stop watching
307
+ */
308
+ watch(handler) {
309
+ const { wa } = this._init;
310
+ let seen = this._raw.status;
311
+ const off_updated = wa.on('message:updated', (msg) => {
312
+ if (msg.id === this._raw.id) {
313
+ const status = msg._raw.status;
314
+ // Sólo los avances cuentan: un mismo estado repetido no es una noticia, y sin
315
+ // este corte cada acuse reenviado por WhatsApp se contaría como una lectura más.
316
+ // Only advances count: a repeated status is not news, and without this cut every
317
+ // acknowledgement WhatsApp resends would count as another read.
318
+ if (status > seen) {
319
+ seen = status;
320
+ if (status >= proto.WebMessageInfo.Status.PLAYED)
321
+ handler({ name: 'played', payload: msg });
322
+ else if (status >= proto.WebMessageInfo.Status.READ)
323
+ handler({ name: 'read', payload: msg });
324
+ }
325
+ }
326
+ });
327
+ const off_deleted = wa.on('message:deleted', (msg) => {
328
+ if (msg.id === this._raw.id)
329
+ handler({ name: 'deleted', payload: msg });
330
+ });
331
+ return () => {
332
+ off_updated();
333
+ off_deleted();
334
+ };
335
+ }
291
336
  /** Marca el mensaje como leído. / Marks the message as read. */
292
337
  async seen() {
293
338
  const { cid, id, author } = this._raw;
@@ -467,6 +512,23 @@ export class Audio extends Media {
467
512
  get duration() { return this._media?.seconds ?? 0; }
468
513
  /** Forma de onda 0-100 lista para pintar. / Paint-ready 0-100 waveform. */
469
514
  get waveform() { return Array.from(to_buffer(this._media?.waveform) ?? []); }
515
+ /** true si ya fue reproducido: el micrófono azul que ve quien lo mandó. / true when already played: the blue mic its sender sees. */
516
+ get played() { return this._raw.status >= proto.WebMessageInfo.Status.PLAYED; }
517
+ /**
518
+ * Acusa el audio como reproducido. Es un aviso aparte del de leído —abrir el chat no
519
+ * reproduce nada—, y por eso viaja como recibo propio: quien lo mandó ve el micrófono
520
+ * azul sólo después de esto.
521
+ * Acknowledges the audio as played. It is separate from the read receipt —opening the chat
522
+ * plays nothing— and so travels as its own receipt: whoever sent it sees the blue mic only
523
+ * after this.
524
+ *
525
+ * @returns true cuando el acuse salió / true once the receipt left
526
+ */
527
+ async play() {
528
+ const { cid, id, author } = this._raw;
529
+ await this._init.socket.sendReceipt(cid, cid.endsWith('@g.us') ? author : undefined, [id], 'played');
530
+ return true;
531
+ }
470
532
  }
471
533
  /** Mensaje de sticker. / Sticker message. */
472
534
  export class Sticker extends Media {
@@ -3,6 +3,33 @@ import { Account, contact } from '../../lib/contact/index.js';
3
3
  import Message, { message } from '../../lib/message/index.js';
4
4
  import { Feed } from '../../lib/status/index.js';
5
5
  import { type Engine } from '../../lib/store/index.js';
6
+ /**
7
+ * Lo que hace alguien al otro lado, en los términos en los que se mira: WhatsApp distingue
8
+ * «disponible» de «escribiendo» y de «grabando», y esa distinción es justo la que interesa
9
+ * vigilar. `paused` no se propaga: es dejar de escribir, no un estado en sí.
10
+ * What someone is doing on the other end, in the terms one watches it: WhatsApp tells «available»
11
+ * from «typing» and from «recording», and that distinction is exactly what is worth watching.
12
+ * `paused` is not propagated: it is stopping typing, not a state of its own.
13
+ */
14
+ export type Presence = 'online' | 'offline' | 'typing' | 'recording';
15
+ /**
16
+ * Lo que se supervisa con `watch()`, en un solo sobre: `name` dice qué pasó y `payload` trae la
17
+ * instancia a la que le pasó, ya resuelta. Quien escucha no tiene que volver a buscar de quién
18
+ * se trata ni filtrar lo que no le toca.
19
+ * What `watch()` supervises, in a single envelope: `name` says what happened and `payload` brings
20
+ * the already-resolved instance it happened to. Whoever listens does not have to look up who it
21
+ * was nor filter out what does not concern them.
22
+ */
23
+ export interface WatchEvent<N extends string, P> {
24
+ name: N;
25
+ payload: P;
26
+ }
27
+ /** Cada acción de alguien al otro lado, incluido dejar de escribir o de grabar. / Every action from the other end, including stopping typing or recording. */
28
+ export type ContactWatch = 'online' | 'offline' | 'typing' | 'recording' | 'stopped-typing' | 'stopped-recording';
29
+ /** Lo que le puede pasar a un mensaje ya enviado. / What can happen to an already-sent message. */
30
+ export type MessageWatch = 'read' | 'played' | 'deleted';
31
+ /** Un chat mezcla lo que hace la persona con lo que llega a la conversación. / A chat mixes what the person does with what arrives in the conversation. */
32
+ export type ChatWatch = ContactWatch | 'message';
6
33
  type ChatInstance = InstanceType<ReturnType<typeof chat>>;
7
34
  type ContactInstance = InstanceType<ReturnType<typeof contact>>;
8
35
  interface Options {
@@ -63,8 +90,14 @@ export interface Farewell {
63
90
  interface EventMap {
64
91
  connected: [WhatsApp];
65
92
  disconnected: [WhatsApp, Farewell];
93
+ /** Fallo que no tumba la conexión pero que quien la abrió necesita saber. / A failure that does not drop the connection but whoever opened it needs to know. */
94
+ error: [Error & {
95
+ code?: string;
96
+ }, WhatsApp];
66
97
  'contact:created': [ContactInstance, ChatInstance, WhatsApp];
67
98
  'contact:updated': [ContactInstance, ChatInstance, WhatsApp];
99
+ /** Alguien entró, salió, escribe o graba. Sólo llega de quien se esté vigilando con `Contact.watch()`. / Someone came in, left, is typing or recording. Only arrives for whoever is being watched with `Contact.watch()`. */
100
+ 'contact:presence': [ContactInstance, ContactWatch, WhatsApp];
68
101
  'chat:created': [ChatInstance, WhatsApp];
69
102
  'chat:deleted': [ChatInstance, WhatsApp];
70
103
  'chat:pinned': [ChatInstance, WhatsApp];
@@ -34,7 +34,18 @@ const queued = (locks, path, work) => {
34
34
  locks.set(path, next.catch(() => { }));
35
35
  return next;
36
36
  };
37
+ /** Traduce el vocabulario de baileys al que se vigila; lo que no está aquí no se propaga. / Translates baileys' vocabulary into the watched one; whatever is missing is not propagated. */
38
+ const PRESENCE = { available: 'online', unavailable: 'offline', composing: 'typing', recording: 'recording', paused: 'paused' };
37
39
  const readable = (value) => (value && !/^\+?[\d\s·•∙⋅]+$/.test(value) ? value : null);
40
+ /**
41
+ * Refrescos de QR que dura un PIN antes de darlo por caducado. WhatsApp no avisa de la
42
+ * expiración, así que se mide por los ciclos de refresco (~20 s cada uno): tres son el margen
43
+ * observado en el que un código sigue siendo aceptado.
44
+ * QR refreshes a PIN lasts before it is considered expired. WhatsApp gives no expiry notice, so
45
+ * it is measured in refresh cycles (~20s each): three is the observed window in which a code is
46
+ * still accepted.
47
+ */
48
+ const OTP_CYCLES = 3;
38
49
  export default class WhatsApp {
39
50
  #event = new EventEmitter();
40
51
  #options;
@@ -77,9 +88,13 @@ export default class WhatsApp {
77
88
  let retries = 0;
78
89
  let intentional = false;
79
90
  let silent = false;
91
+ let paired = false;
92
+ let cycles = 0;
80
93
  let alive = null;
81
94
  let timer = null;
82
95
  let chain = Promise.resolve();
96
+ /** Qué estaba haciendo cada quien, para poder leer su `paused`. / What each one was doing, so their `paused` can be read. */
97
+ const doing = new Map();
83
98
  const locks = new Map();
84
99
  return new Promise((resolve, reject) => {
85
100
  const start = async () => {
@@ -258,9 +273,33 @@ export default class WhatsApp {
258
273
  socket.ev.on('creds.update', () => queued(locks, '/session/creds', () => engine.set('/session/creds', serialize(creds))));
259
274
  socket.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
260
275
  if (qr && !creds.registered) {
261
- await callback(digits && (method ?? 'otp') === 'otp'
262
- ? await socket.requestPairingCode(digits)
263
- : await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
276
+ if (digits && (method ?? 'otp') === 'otp') {
277
+ // El QR se refresca cada ~20 s, pero el PIN vive más: pedir uno en
278
+ // cada refresco invalida el anterior y el que la persona está
279
+ // tecleando deja de servir a media escritura —el síntoma es un PIN
280
+ // correcto que «no lo toma»—. Por eso se cuenta el ciclo y sólo se
281
+ // renueva al caducar de verdad, gastando un reintento del presupuesto.
282
+ // The QR refreshes every ~20s, but the PIN lives longer: asking for
283
+ // one on each refresh invalidates the previous, and the one being
284
+ // typed stops working mid-typing —the symptom is a correct PIN that
285
+ // «is not accepted»—. So the cycle is counted and it is only renewed
286
+ // once it truly expires, spending one retry from the budget.
287
+ cycles = paired ? cycles + 1 : 0;
288
+ if (!paired || cycles >= OTP_CYCLES) {
289
+ if (paired && budget !== null && retries >= budget) {
290
+ this.emit('error', Object.assign(new Error('El código de vinculación expiró'), { code: 'ERR_OTP_EXPIRED' }), this);
291
+ }
292
+ else {
293
+ retries += paired ? 1 : 0;
294
+ cycles = 0;
295
+ paired = true;
296
+ await callback(await socket.requestPairingCode(digits));
297
+ }
298
+ }
299
+ }
300
+ else {
301
+ await callback(await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
302
+ }
264
303
  }
265
304
  if (connection === 'open') {
266
305
  connected = true;
@@ -371,6 +410,31 @@ export default class WhatsApp {
371
410
  }
372
411
  }).catch(() => { });
373
412
  });
413
+ socket.ev.on('presence.update', ({ id, presences }) => {
414
+ chain = chain.then(async () => {
415
+ const cid = await canonical(id);
416
+ for (const [participant, data] of Object.entries(presences)) {
417
+ const state = PRESENCE[data.lastKnownPresence];
418
+ if (state) {
419
+ const who = await canonical(participant || cid);
420
+ // `paused` es «dejó de hacer lo que hacía», y sólo el estado
421
+ // anterior dice qué era: sin recordarlo no se puede distinguir
422
+ // dejar de escribir de dejar de grabar.
423
+ // `paused` means «stopped doing what they were doing», and only
424
+ // the previous state says which: without remembering it there is
425
+ // no telling stopped-typing from stopped-recording.
426
+ const last = doing.get(who);
427
+ const name = state === 'paused' ? (last === 'recording' ? 'stopped-recording' : 'stopped-typing') : state;
428
+ if (state === 'typing' || state === 'recording')
429
+ doing.set(who, state);
430
+ else
431
+ doing.delete(who);
432
+ const card = deserialize(await engine.get(`/contact/${who}`));
433
+ this.emit('contact:presence', new this.Contact(card ?? { id: who, lid: null, name: null, notify: null, verified_name: null, img_url: null, status: null }), name, this);
434
+ }
435
+ }
436
+ }).catch(() => { });
437
+ });
374
438
  socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
375
439
  chain = chain.then(async () => {
376
440
  await remember(lid, pn);
@@ -493,7 +557,18 @@ export default class WhatsApp {
493
557
  continue;
494
558
  }
495
559
  const type = { conversation: 'text', extendedTextMessage: 'text', imageMessage: 'image', videoMessage: 'video', audioMessage: 'audio' }[kind ?? ''];
496
- const author = msg.key.participant ?? '';
560
+ // El autor de un estado puede llegar por `participant` o por su
561
+ // alterno según venga identificado por teléfono o por LID, y se
562
+ // guarda canónico como todo lo demás: con el LID crudo el estado
563
+ // queda a nombre de un número larguísimo que no case con ningún
564
+ // contacto, y para quien mira es un estado que no llegó.
565
+ // A status author can arrive via `participant` or its alternate
566
+ // depending on whether it is identified by phone or by LID, and
567
+ // is stored canonically like everything else: with the raw LID
568
+ // the status ends up under a long meaningless number matching no
569
+ // contact, and to whoever looks it is a status that never came.
570
+ const claimed = msg.key.participant ?? msg.key.participantAlt ?? '';
571
+ const author = claimed ? await canonical(claimed) : '';
497
572
  if (!type || !author) {
498
573
  continue;
499
574
  }
package/package.json CHANGED
@@ -72,7 +72,7 @@
72
72
  "release": "npm publish --access public"
73
73
  },
74
74
  "types": "./build/esm/index.d.ts",
75
- "version": "7.4.2",
75
+ "version": "8.0.0",
76
76
  "engines": {
77
77
  "node": ">=20"
78
78
  },