@arcaelas/whatsapp 7.0.1 → 7.2.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.
@@ -54,6 +54,8 @@ export default class Message {
54
54
  mime: string;
55
55
  caption: string;
56
56
  edited: boolean;
57
+ /** Momento en que se retiró para todos, o null si sigue vigente / When it was revoked for everyone, or null while it stands */
58
+ revoked_at?: number | null;
57
59
  multiple?: boolean;
58
60
  reactions?: {
59
61
  author: string;
@@ -96,6 +98,15 @@ export default class Message {
96
98
  get forwarded(): boolean;
97
99
  /** true si fue editado. / true when edited. */
98
100
  get edited(): boolean;
101
+ /**
102
+ * true si se retiró para todos. El documento no se borra: el mensaje sigue en su sitio
103
+ * para que la interfaz muestre «se eliminó este mensaje» en vez de un hueco.
104
+ * true when revoked for everyone. The document is not removed: the message stays in place
105
+ * so the interface can show "this message was deleted" instead of a gap.
106
+ */
107
+ get revoked(): boolean;
108
+ /** Fecha del retiro en ISO UTC, o null. / Revocation date as ISO UTC, or null. */
109
+ get revoked_at(): string | null;
99
110
  /** Fecha de creación en ISO UTC. / Creation date as ISO UTC. */
100
111
  get created_at(): string;
101
112
  /** Vencimiento del mensaje temporal en ISO UTC, o null. / Ephemeral expiration as ISO UTC, or null. */
@@ -106,7 +117,13 @@ export default class Message {
106
117
  get reason(): string | null;
107
118
  /** Nombre del negocio verificado que firma el mensaje, o null. / Verified business name signing the message, or null. */
108
119
  get business(): string | null;
109
- /** true si es de una sola lectura (view-once). / true when view-once. */
120
+ /**
121
+ * true si es de una sola lectura (view-once). La marca vive en tres sitios según quién
122
+ * lo envió: la llave del sobre, el propio nodo del media (forma actual) o el wrapper
123
+ * heredado.
124
+ * true when view-once. The flag lives in three places depending on the sender: the key,
125
+ * the media node itself (current form) or the legacy wrapper.
126
+ */
110
127
  get once(): boolean;
111
128
  /** Contacto autor, desde el engine (ficha mínima si no está persistido). / Author contact, from the engine (minimal card when not persisted). */
112
129
  author(): Promise<InstanceType<WhatsApp['Contact']>>;
@@ -403,6 +420,8 @@ export declare function message(init: Init): {
403
420
  mime: string;
404
421
  caption: string;
405
422
  edited: boolean;
423
+ /** Momento en que se retiró para todos, o null si sigue vigente / When it was revoked for everyone, or null while it stands */
424
+ revoked_at?: number | null;
406
425
  multiple?: boolean;
407
426
  reactions?: {
408
427
  author: string;
@@ -436,6 +455,15 @@ export declare function message(init: Init): {
436
455
  get forwarded(): boolean;
437
456
  /** true si fue editado. / true when edited. */
438
457
  get edited(): boolean;
458
+ /**
459
+ * true si se retiró para todos. El documento no se borra: el mensaje sigue en su sitio
460
+ * para que la interfaz muestre «se eliminó este mensaje» en vez de un hueco.
461
+ * true when revoked for everyone. The document is not removed: the message stays in place
462
+ * so the interface can show "this message was deleted" instead of a gap.
463
+ */
464
+ get revoked(): boolean;
465
+ /** Fecha del retiro en ISO UTC, o null. / Revocation date as ISO UTC, or null. */
466
+ get revoked_at(): string | null;
439
467
  /** Fecha de creación en ISO UTC. / Creation date as ISO UTC. */
440
468
  get created_at(): string;
441
469
  /** Vencimiento del mensaje temporal en ISO UTC, o null. / Ephemeral expiration as ISO UTC, or null. */
@@ -446,7 +474,13 @@ export declare function message(init: Init): {
446
474
  get reason(): string | null;
447
475
  /** Nombre del negocio verificado que firma el mensaje, o null. / Verified business name signing the message, or null. */
448
476
  get business(): string | null;
449
- /** true si es de una sola lectura (view-once). / true when view-once. */
477
+ /**
478
+ * true si es de una sola lectura (view-once). La marca vive en tres sitios según quién
479
+ * lo envió: la llave del sobre, el propio nodo del media (forma actual) o el wrapper
480
+ * heredado.
481
+ * true when view-once. The flag lives in three places depending on the sender: the key,
482
+ * the media node itself (current form) or the legacy wrapper.
483
+ */
450
484
  get once(): boolean;
451
485
  /** Contacto autor, desde el engine (ficha mínima si no está persistido). / Author contact, from the engine (minimal card when not persisted). */
452
486
  author(): Promise<InstanceType<WhatsApp["Contact"]>>;
@@ -23,6 +23,40 @@ const STATUS = ['error', 'pending', 'sent', 'delivered', 'read', 'played'];
23
23
  const unwrap = (msg) => msg.viewOnceMessage?.message ?? msg.viewOnceMessageV2?.message ?? msg.viewOnceMessageV2Extension?.message ?? msg.documentWithCaptionMessage?.message ?? msg;
24
24
  /** Bytes del proto (Uint8Array en runtime, base64 tras el engine) como Buffer. / Proto bytes (runtime Uint8Array, post-engine base64) as a Buffer. */
25
25
  const to_buffer = (value) => value instanceof Uint8Array && value.length ? Buffer.from(value) : typeof value === 'string' && value ? Buffer.from(value, 'base64') : null;
26
+ /**
27
+ * Envío de vista única con el proto que WhatsApp entrega: se genera el contenido normal (media
28
+ * plano, sin wrapper), se marca `viewOnce` en el propio nodo del media y `isViewOnce` en la
29
+ * llave, y se transmite tal cual.
30
+ * View-once send with the proto WhatsApp actually delivers: the regular content is generated
31
+ * (flat media, no wrapper), `viewOnce` is flagged on the media node itself and `isViewOnce` on
32
+ * the key, and it is relayed as is.
33
+ *
34
+ * @param init - Sesión activa / Active session
35
+ * @param jid - Destino resuelto / Resolved target
36
+ * @param content - Contenido proto / Proto content
37
+ * @param quoted - Mensaje citado / Quoted message
38
+ * @returns Mensaje transmitido / Relayed message
39
+ */
40
+ const once_of = async (init, jid, content, quoted) => {
41
+ const full = await (0, baileys_1.generateWAMessage)(jid, content, {
42
+ logger: init.socket.logger,
43
+ userJid: init.socket.user.id,
44
+ upload: init.socket.waUploadToServer,
45
+ ...(quoted && { quoted }),
46
+ });
47
+ // El media queda al nivel superior; sólo hay que marcarlo. / The media stays top level; it only needs flagging.
48
+ for (const node of Object.values(full.message ?? {}))
49
+ if (node && typeof node === 'object' && 'mimetype' in node)
50
+ node.viewOnce = true;
51
+ full.key.isViewOnce = true;
52
+ await init.socket.relayMessage(jid, full.message, { messageId: full.key.id });
53
+ // El relay no publica el mensaje: se emite el upsert que `sendMessage` haría, para que el
54
+ // resto de la sesión (eventos, cachés de los consumidores) lo vea como cualquier envío.
55
+ // The relay does not publish the message: the upsert `sendMessage` would emit is fired, so
56
+ // the rest of the session (events, consumer caches) sees it like any other send.
57
+ init.socket.ev.emit('messages.upsert', { messages: [full], type: 'append' });
58
+ return full;
59
+ };
26
60
  /**
27
61
  * Envío base: resuelve el destino, cita si hay `mid`, marca view-once, persiste el documento
28
62
  * con su binario y retorna la instancia del tipo correcto. La cita viaja en las opciones del
@@ -36,7 +70,17 @@ const send = async (init, cid, content, binary, extra = {}, overrides) => {
36
70
  if (!jid)
37
71
  return null;
38
72
  const quoted = extra.mid ? (0, store_1.deserialize)(await init.engine.get(`/chat/${jid}/message/${extra.mid}`))?.raw : undefined;
39
- const raw = await init.socket.sendMessage(jid, { ...content, ...(extra.once && { viewOnce: true }) }, { ...(quoted && { quoted }) });
73
+ // La vista única viaja como la manda WhatsApp: el media PLANO con `viewOnce` en su propio
74
+ // nodo y el sobre marcado con `isViewOnce`, sin envolverlo en `viewOnceMessage(V2)` —el
75
+ // wrapper que arma baileys con `viewOnce: true` queda en «enviado» y no se entrega.
76
+ // Comprobado contra el proto de un view-once entrante real.
77
+ // View-once travels the way WhatsApp sends it: the media FLAT with `viewOnce` on its own
78
+ // node and the key flagged `isViewOnce`, never wrapped in `viewOnceMessage(V2)` —the
79
+ // wrapper baileys builds from `viewOnce: true` stalls at "sent" and is never delivered.
80
+ // Checked against the proto of a real incoming view-once.
81
+ const raw = extra.once
82
+ ? await once_of(init, jid, content, quoted)
83
+ : await init.socket.sendMessage(jid, content, { ...(quoted && { quoted }) });
40
84
  if (!raw?.key?.id)
41
85
  return null;
42
86
  const sent = new Message(init, raw);
@@ -154,6 +198,15 @@ class Message {
154
198
  get forwarded() { return this._raw.forwarded; }
155
199
  /** true si fue editado. / true when edited. */
156
200
  get edited() { return this._raw.edited; }
201
+ /**
202
+ * true si se retiró para todos. El documento no se borra: el mensaje sigue en su sitio
203
+ * para que la interfaz muestre «se eliminó este mensaje» en vez de un hueco.
204
+ * true when revoked for everyone. The document is not removed: the message stays in place
205
+ * so the interface can show "this message was deleted" instead of a gap.
206
+ */
207
+ get revoked() { return this._raw.revoked_at != null; }
208
+ /** Fecha del retiro en ISO UTC, o null. / Revocation date as ISO UTC, or null. */
209
+ get revoked_at() { return this._raw.revoked_at != null ? new Date(this._raw.revoked_at).toISOString() : null; }
157
210
  /** Fecha de creación en ISO UTC. / Creation date as ISO UTC. */
158
211
  get created_at() { return new Date(this._raw.created_at).toISOString(); }
159
212
  /** Vencimiento del mensaje temporal en ISO UTC, o null. / Ephemeral expiration as ISO UTC, or null. */
@@ -172,10 +225,18 @@ class Message {
172
225
  }
173
226
  /** Nombre del negocio verificado que firma el mensaje, o null. / Verified business name signing the message, or null. */
174
227
  get business() { return this._raw.raw.verifiedBizName ?? null; }
175
- /** true si es de una sola lectura (view-once). / true when view-once. */
228
+ /**
229
+ * true si es de una sola lectura (view-once). La marca vive en tres sitios según quién
230
+ * lo envió: la llave del sobre, el propio nodo del media (forma actual) o el wrapper
231
+ * heredado.
232
+ * true when view-once. The flag lives in three places depending on the sender: the key,
233
+ * the media node itself (current form) or the legacy wrapper.
234
+ */
176
235
  get once() {
177
236
  const msg = this._raw.raw.message;
178
- return Boolean(msg?.viewOnceMessage ?? msg?.viewOnceMessageV2 ?? msg?.viewOnceMessageV2Extension);
237
+ const body = msg ? unwrap(msg) : {};
238
+ const media = body[(0, baileys_1.getContentType)(body)];
239
+ return Boolean(this._raw.raw.key?.isViewOnce ?? media?.viewOnce ?? msg?.viewOnceMessage ?? msg?.viewOnceMessageV2 ?? msg?.viewOnceMessageV2Extension);
179
240
  }
180
241
  /** Contacto autor, desde el engine (ficha mínima si no está persistido). / Author contact, from the engine (minimal card when not persisted). */
181
242
  async author() {
@@ -306,11 +367,19 @@ class Message {
306
367
  */
307
368
  async delete(all = false) {
308
369
  const doc = this._raw;
309
- if (all)
370
+ // Retirar para todos deja el mensaje en su sitio, marcado; borrarlo solo para uno lo
371
+ // saca del historial local, que es lo que significa cada acción en WhatsApp.
372
+ // Revoking for everyone leaves the message in place, flagged; deleting it just for
373
+ // oneself drops it from the local history, which is what each action means on WhatsApp.
374
+ if (all) {
310
375
  await this._init.socket.sendMessage(doc.cid, { delete: { remoteJid: doc.cid, id: doc.id, fromMe: doc.me } });
311
- else
376
+ doc.revoked_at = Date.now();
377
+ await this._init.engine.set(`/chat/${doc.cid}/message/${doc.id}`, (0, store_1.serialize)(doc), doc.created_at);
378
+ }
379
+ else {
312
380
  await this._init.socket.chatModify({ deleteForMe: { deleteMedia: false, key: { remoteJid: doc.cid, id: doc.id, fromMe: doc.me }, timestamp: Date.now() } }, doc.cid);
313
- await this._init.engine.unset(`/chat/${doc.cid}/message/${doc.id}`);
381
+ await this._init.engine.unset(`/chat/${doc.cid}/message/${doc.id}`);
382
+ }
314
383
  return true;
315
384
  }
316
385
  /** Responde con texto. / Replies with text. */
@@ -47,6 +47,15 @@ function deserialize(raw) {
47
47
  return null;
48
48
  }
49
49
  }
50
+ /**
51
+ * Un mapeo que lleva a otro LID no traduce nada, y al ser truthy corta la cadena de respaldos
52
+ * de `jid_of` justo antes de llegar al mapping de baileys —el único que sí conoce el teléfono—.
53
+ * Descartarlo es lo que permite que la cadena siga buscando.
54
+ * A mapping leading to another LID translates nothing, and being truthy it cuts `jid_of`'s
55
+ * fallback chain right before reaching the baileys mapping —the only one that does know the
56
+ * phone—. Discarding it is what lets the chain keep looking.
57
+ */
58
+ const phone_like = (value) => (value != null && !String(value).endsWith('@lid') ? String(value) : null);
50
59
  /**
51
60
  * JID canónico de un teléfono, JID o LID: los grupos y JIDs pasan tal cual, el LID se mapea
52
61
  * contra los índices `/lid` del engine (o contra baileys cuando hay socket) y el resto se
@@ -65,9 +74,9 @@ const jid_of = async (engine, uid, socket) => {
65
74
  return uid;
66
75
  const lid = uid.endsWith('@lid') ? (0, baileys_1.jidNormalizedUser)(uid) : '';
67
76
  const mapped = lid
68
- ? deserialize(await engine.get(`/lid/${lid}`))
69
- ?? deserialize(await engine.get(`/lid/${lid.split('@')[0]}_reverse`))
70
- ?? await socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null)
77
+ ? phone_like(deserialize(await engine.get(`/lid/${lid}`)))
78
+ ?? phone_like(deserialize(await engine.get(`/lid/${lid.split('@')[0]}_reverse`)))
79
+ ?? phone_like(await socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null))
71
80
  : uid.replace(/\D/g, '');
72
81
  return mapped ? (String(mapped).includes('@') ? (0, baileys_1.jidNormalizedUser)(String(mapped)) : `${mapped}@s.whatsapp.net`) : null;
73
82
  };
@@ -21,6 +21,15 @@ interface Options {
21
21
  };
22
22
  /** Descargar el historial de mensajes al vincular; contactos, credenciales, LID mappings y tctokens se sincronizan siempre. / Download the message history on link; contacts, credentials, LID mappings and tctokens always sync. */
23
23
  sync?: boolean;
24
+ /**
25
+ * Nombre con el que esta sesión aparece en «Dispositivos vinculados» del teléfono. Cuando
26
+ * una cuenta tiene varias sesiones, es lo ÚNICO que permite distinguirlas para cerrar la
27
+ * correcta: sin él todas se ven iguales y no hay forma de saber cuál sobra.
28
+ * Name this session shows under the phone's «Linked devices». When an account holds several
29
+ * sessions it is the ONLY thing telling them apart to close the right one: without it they
30
+ * all look alike and there is no way to know which one is spare.
31
+ */
32
+ device?: string;
24
33
  }
25
34
  interface EventMap {
26
35
  connected: [WhatsApp];
@@ -63,10 +72,28 @@ export default class WhatsApp {
63
72
  off<E extends keyof EventMap>(event: E, handler: (...args: EventMap[E]) => void): this;
64
73
  connect(callback: (auth: string | Buffer) => void | Promise<void>): Promise<void>;
65
74
  /**
66
- * Cierra la sesión: cancela el reintento pendiente y termina el socket.
67
- * Closes the session: cancels the pending retry and ends the socket.
75
+ * Cierra la sesión de verdad: desvincula el dispositivo del teléfono y termina el socket.
76
+ * La promesa no resuelve hasta que todo eso ocurrió.
77
+ *
78
+ * Los dos flags modulan efectos secundarios, nunca si la sesión muere: `silent` sólo calla
79
+ * el evento `disconnected` local, y `destroy` decide si el engine se vacía o conserva
80
+ * chats, mensajes y contactos para estudiarlos después. Las credenciales se borran en los
81
+ * dos casos: el dispositivo ya no existe, así que reconectar con ellas sólo devolvería un
82
+ * `loggedOut`.
83
+ *
84
+ * Closes the session for real: unlinks the device from the phone and ends the socket. The
85
+ * promise does not settle until all of that happened.
86
+ *
87
+ * Both flags modulate side effects, never whether the session dies: `silent` only mutes the
88
+ * local `disconnected` event, and `destroy` decides whether the engine is wiped or keeps
89
+ * chats, messages and contacts for later study. Credentials go in both cases: the device no
90
+ * longer exists, so reconnecting with them would only return a `loggedOut`.
91
+ *
92
+ * @param options - `silent` calla el evento; `destroy` vacía el engine entero / `silent` mutes the event; `destroy` wipes the whole engine
68
93
  *
69
- * @param options - `silent` calla el evento `disconnected`; `destroy` vacía el engine / `silent` mutes the `disconnected` event; `destroy` clears the engine
94
+ * @example
95
+ * await wa.disconnect(); // desvincula y conserva el historial
96
+ * await wa.disconnect({ destroy: true }); // desvincula y no queda nada
70
97
  */
71
98
  disconnect(options?: {
72
99
  silent?: boolean;