@arcaelas/whatsapp 7.0.1 → 7.1.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. */
@@ -437,7 +437,13 @@ class WhatsApp {
437
437
  }
438
438
  if (kind === 'protocolMessage') {
439
439
  const protocol = msg.message?.protocolMessage;
440
- const found = protocol?.key?.id ? await locate(protocol.key.remoteJid ?? cid, protocol.key.id) : null;
440
+ // El aviso puede venir direccionado por LID y el documento estar bajo el JID
441
+ // (o al revés): se busca por el chat que nombra el protocolo y por el del sobre.
442
+ // The notice may be LID-addressed while the document lives under the JID (or the
443
+ // other way around): it is looked up by the protocol's chat and by the envelope's.
444
+ const found = protocol?.key?.id
445
+ ? (await locate(protocol.key.remoteJid ?? cid, protocol.key.id)) ?? (await locate(cid, protocol.key.id))
446
+ : null;
441
447
  if (found && protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
442
448
  found.doc.raw.message = protocol.editedMessage;
443
449
  found.doc.edited = true;
@@ -447,7 +453,12 @@ class WhatsApp {
447
453
  this.emit('message:updated', instance, await instance.chat(), this);
448
454
  }
449
455
  else if (found && protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE) {
450
- await engine.unset(found.path);
456
+ // El mensaje retirado no se borra: se marca, y así la interfaz puede
457
+ // mostrar «se eliminó este mensaje» donde estaba en vez de un hueco.
458
+ // A revoked message is not removed: it gets flagged, so the interface can
459
+ // show "this message was deleted" in its place instead of a gap.
460
+ found.doc.revoked_at = Date.now();
461
+ await engine.set(found.path, (0, store_1.serialize)(found.doc), found.doc.created_at);
451
462
  const instance = new message_1.default(init, found.doc);
452
463
  this.emit('message:deleted', instance, await instance.chat(), this);
453
464
  }
@@ -458,6 +469,7 @@ class WhatsApp {
458
469
  if (stored) {
459
470
  doc.multiple = typeof stored.multiple === 'boolean' ? stored.multiple : doc.multiple;
460
471
  doc.reactions = stored.reactions ?? doc.reactions;
472
+ doc.revoked_at = stored.revoked_at ?? doc.revoked_at;
461
473
  const advanced = doc.status > stored.status;
462
474
  doc.status = Math.max(stored.status, doc.status);
463
475
  if (!advanced && stored.caption === doc.caption && stored.edited === doc.edited && stored.starred === doc.starred) {
@@ -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"]>>;
@@ -5,7 +5,7 @@
5
5
  * Message entity — pure-getter base class with per-type subclasses; `message(init)`
6
6
  * returns the session-bound class with sends and reads.
7
7
  */
8
- import { aesEncryptGCM, downloadMediaMessage, generateForwardMessageContent, generateMessageID, generateWAMessageFromContent, getAggregateVotesInPollMessage, getContentType, getKeyAuthor, hmacSign, jidNormalizedUser, proto, sha256, updateMessageWithPollUpdate, } from 'baileys';
8
+ import { aesEncryptGCM, downloadMediaMessage, generateForwardMessageContent, generateMessageID, generateWAMessage, generateWAMessageFromContent, getAggregateVotesInPollMessage, getContentType, getKeyAuthor, hmacSign, jidNormalizedUser, proto, sha256, updateMessageWithPollUpdate, } from 'baileys';
9
9
  import { randomBytes } from 'node:crypto';
10
10
  import { Readable } from 'node:stream';
11
11
  import Chat from '../../lib/chat/index.js';
@@ -16,6 +16,40 @@ const STATUS = ['error', 'pending', 'sent', 'delivered', 'read', 'played'];
16
16
  const unwrap = (msg) => msg.viewOnceMessage?.message ?? msg.viewOnceMessageV2?.message ?? msg.viewOnceMessageV2Extension?.message ?? msg.documentWithCaptionMessage?.message ?? msg;
17
17
  /** Bytes del proto (Uint8Array en runtime, base64 tras el engine) como Buffer. / Proto bytes (runtime Uint8Array, post-engine base64) as a Buffer. */
18
18
  const to_buffer = (value) => value instanceof Uint8Array && value.length ? Buffer.from(value) : typeof value === 'string' && value ? Buffer.from(value, 'base64') : null;
19
+ /**
20
+ * Envío de vista única con el proto que WhatsApp entrega: se genera el contenido normal (media
21
+ * plano, sin wrapper), se marca `viewOnce` en el propio nodo del media y `isViewOnce` en la
22
+ * llave, y se transmite tal cual.
23
+ * View-once send with the proto WhatsApp actually delivers: the regular content is generated
24
+ * (flat media, no wrapper), `viewOnce` is flagged on the media node itself and `isViewOnce` on
25
+ * the key, and it is relayed as is.
26
+ *
27
+ * @param init - Sesión activa / Active session
28
+ * @param jid - Destino resuelto / Resolved target
29
+ * @param content - Contenido proto / Proto content
30
+ * @param quoted - Mensaje citado / Quoted message
31
+ * @returns Mensaje transmitido / Relayed message
32
+ */
33
+ const once_of = async (init, jid, content, quoted) => {
34
+ const full = await generateWAMessage(jid, content, {
35
+ logger: init.socket.logger,
36
+ userJid: init.socket.user.id,
37
+ upload: init.socket.waUploadToServer,
38
+ ...(quoted && { quoted }),
39
+ });
40
+ // El media queda al nivel superior; sólo hay que marcarlo. / The media stays top level; it only needs flagging.
41
+ for (const node of Object.values(full.message ?? {}))
42
+ if (node && typeof node === 'object' && 'mimetype' in node)
43
+ node.viewOnce = true;
44
+ full.key.isViewOnce = true;
45
+ await init.socket.relayMessage(jid, full.message, { messageId: full.key.id });
46
+ // El relay no publica el mensaje: se emite el upsert que `sendMessage` haría, para que el
47
+ // resto de la sesión (eventos, cachés de los consumidores) lo vea como cualquier envío.
48
+ // The relay does not publish the message: the upsert `sendMessage` would emit is fired, so
49
+ // the rest of the session (events, consumer caches) sees it like any other send.
50
+ init.socket.ev.emit('messages.upsert', { messages: [full], type: 'append' });
51
+ return full;
52
+ };
19
53
  /**
20
54
  * Envío base: resuelve el destino, cita si hay `mid`, marca view-once, persiste el documento
21
55
  * con su binario y retorna la instancia del tipo correcto. La cita viaja en las opciones del
@@ -29,7 +63,17 @@ const send = async (init, cid, content, binary, extra = {}, overrides) => {
29
63
  if (!jid)
30
64
  return null;
31
65
  const quoted = extra.mid ? deserialize(await init.engine.get(`/chat/${jid}/message/${extra.mid}`))?.raw : undefined;
32
- const raw = await init.socket.sendMessage(jid, { ...content, ...(extra.once && { viewOnce: true }) }, { ...(quoted && { quoted }) });
66
+ // La vista única viaja como la manda WhatsApp: el media PLANO con `viewOnce` en su propio
67
+ // nodo y el sobre marcado con `isViewOnce`, sin envolverlo en `viewOnceMessage(V2)` —el
68
+ // wrapper que arma baileys con `viewOnce: true` queda en «enviado» y no se entrega.
69
+ // Comprobado contra el proto de un view-once entrante real.
70
+ // View-once travels the way WhatsApp sends it: the media FLAT with `viewOnce` on its own
71
+ // node and the key flagged `isViewOnce`, never wrapped in `viewOnceMessage(V2)` —the
72
+ // wrapper baileys builds from `viewOnce: true` stalls at "sent" and is never delivered.
73
+ // Checked against the proto of a real incoming view-once.
74
+ const raw = extra.once
75
+ ? await once_of(init, jid, content, quoted)
76
+ : await init.socket.sendMessage(jid, content, { ...(quoted && { quoted }) });
33
77
  if (!raw?.key?.id)
34
78
  return null;
35
79
  const sent = new Message(init, raw);
@@ -147,6 +191,15 @@ export default class Message {
147
191
  get forwarded() { return this._raw.forwarded; }
148
192
  /** true si fue editado. / true when edited. */
149
193
  get edited() { return this._raw.edited; }
194
+ /**
195
+ * true si se retiró para todos. El documento no se borra: el mensaje sigue en su sitio
196
+ * para que la interfaz muestre «se eliminó este mensaje» en vez de un hueco.
197
+ * true when revoked for everyone. The document is not removed: the message stays in place
198
+ * so the interface can show "this message was deleted" instead of a gap.
199
+ */
200
+ get revoked() { return this._raw.revoked_at != null; }
201
+ /** Fecha del retiro en ISO UTC, o null. / Revocation date as ISO UTC, or null. */
202
+ get revoked_at() { return this._raw.revoked_at != null ? new Date(this._raw.revoked_at).toISOString() : null; }
150
203
  /** Fecha de creación en ISO UTC. / Creation date as ISO UTC. */
151
204
  get created_at() { return new Date(this._raw.created_at).toISOString(); }
152
205
  /** Vencimiento del mensaje temporal en ISO UTC, o null. / Ephemeral expiration as ISO UTC, or null. */
@@ -165,10 +218,18 @@ export default class Message {
165
218
  }
166
219
  /** Nombre del negocio verificado que firma el mensaje, o null. / Verified business name signing the message, or null. */
167
220
  get business() { return this._raw.raw.verifiedBizName ?? null; }
168
- /** true si es de una sola lectura (view-once). / true when view-once. */
221
+ /**
222
+ * true si es de una sola lectura (view-once). La marca vive en tres sitios según quién
223
+ * lo envió: la llave del sobre, el propio nodo del media (forma actual) o el wrapper
224
+ * heredado.
225
+ * true when view-once. The flag lives in three places depending on the sender: the key,
226
+ * the media node itself (current form) or the legacy wrapper.
227
+ */
169
228
  get once() {
170
229
  const msg = this._raw.raw.message;
171
- return Boolean(msg?.viewOnceMessage ?? msg?.viewOnceMessageV2 ?? msg?.viewOnceMessageV2Extension);
230
+ const body = msg ? unwrap(msg) : {};
231
+ const media = body[getContentType(body)];
232
+ return Boolean(this._raw.raw.key?.isViewOnce ?? media?.viewOnce ?? msg?.viewOnceMessage ?? msg?.viewOnceMessageV2 ?? msg?.viewOnceMessageV2Extension);
172
233
  }
173
234
  /** Contacto autor, desde el engine (ficha mínima si no está persistido). / Author contact, from the engine (minimal card when not persisted). */
174
235
  async author() {
@@ -299,11 +360,19 @@ export default class Message {
299
360
  */
300
361
  async delete(all = false) {
301
362
  const doc = this._raw;
302
- if (all)
363
+ // Retirar para todos deja el mensaje en su sitio, marcado; borrarlo solo para uno lo
364
+ // saca del historial local, que es lo que significa cada acción en WhatsApp.
365
+ // Revoking for everyone leaves the message in place, flagged; deleting it just for
366
+ // oneself drops it from the local history, which is what each action means on WhatsApp.
367
+ if (all) {
303
368
  await this._init.socket.sendMessage(doc.cid, { delete: { remoteJid: doc.cid, id: doc.id, fromMe: doc.me } });
304
- else
369
+ doc.revoked_at = Date.now();
370
+ await this._init.engine.set(`/chat/${doc.cid}/message/${doc.id}`, serialize(doc), doc.created_at);
371
+ }
372
+ else {
305
373
  await this._init.socket.chatModify({ deleteForMe: { deleteMedia: false, key: { remoteJid: doc.cid, id: doc.id, fromMe: doc.me }, timestamp: Date.now() } }, doc.cid);
306
- await this._init.engine.unset(`/chat/${doc.cid}/message/${doc.id}`);
374
+ await this._init.engine.unset(`/chat/${doc.cid}/message/${doc.id}`);
375
+ }
307
376
  return true;
308
377
  }
309
378
  /** Responde con texto. / Replies with text. */
@@ -399,7 +399,13 @@ export default class WhatsApp {
399
399
  }
400
400
  if (kind === 'protocolMessage') {
401
401
  const protocol = msg.message?.protocolMessage;
402
- const found = protocol?.key?.id ? await locate(protocol.key.remoteJid ?? cid, protocol.key.id) : null;
402
+ // El aviso puede venir direccionado por LID y el documento estar bajo el JID
403
+ // (o al revés): se busca por el chat que nombra el protocolo y por el del sobre.
404
+ // The notice may be LID-addressed while the document lives under the JID (or the
405
+ // other way around): it is looked up by the protocol's chat and by the envelope's.
406
+ const found = protocol?.key?.id
407
+ ? (await locate(protocol.key.remoteJid ?? cid, protocol.key.id)) ?? (await locate(cid, protocol.key.id))
408
+ : null;
403
409
  if (found && protocol?.type === proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
404
410
  found.doc.raw.message = protocol.editedMessage;
405
411
  found.doc.edited = true;
@@ -409,7 +415,12 @@ export default class WhatsApp {
409
415
  this.emit('message:updated', instance, await instance.chat(), this);
410
416
  }
411
417
  else if (found && protocol?.type === proto.Message.ProtocolMessage.Type.REVOKE) {
412
- await engine.unset(found.path);
418
+ // El mensaje retirado no se borra: se marca, y así la interfaz puede
419
+ // mostrar «se eliminó este mensaje» donde estaba en vez de un hueco.
420
+ // A revoked message is not removed: it gets flagged, so the interface can
421
+ // show "this message was deleted" in its place instead of a gap.
422
+ found.doc.revoked_at = Date.now();
423
+ await engine.set(found.path, serialize(found.doc), found.doc.created_at);
413
424
  const instance = new Message(init, found.doc);
414
425
  this.emit('message:deleted', instance, await instance.chat(), this);
415
426
  }
@@ -420,6 +431,7 @@ export default class WhatsApp {
420
431
  if (stored) {
421
432
  doc.multiple = typeof stored.multiple === 'boolean' ? stored.multiple : doc.multiple;
422
433
  doc.reactions = stored.reactions ?? doc.reactions;
434
+ doc.revoked_at = stored.revoked_at ?? doc.revoked_at;
423
435
  const advanced = doc.status > stored.status;
424
436
  doc.status = Math.max(stored.status, doc.status);
425
437
  if (!advanced && stored.caption === doc.caption && stored.edited === doc.edited && stored.starred === doc.starred) {
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.0.1",
75
+ "version": "7.1.0",
76
76
  "engines": {
77
77
  "node": ">=20"
78
78
  },