@arcaelas/whatsapp 7.0.0 → 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.
- package/build/cjs/lib/message/index.d.ts +36 -2
- package/build/cjs/lib/message/index.js +75 -6
- package/build/cjs/lib/whatsapp/index.js +24 -2
- package/build/esm/lib/message/index.d.ts +36 -2
- package/build/esm/lib/message/index.js +76 -7
- package/build/esm/lib/whatsapp/index.js +24 -2
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. */
|
|
@@ -114,6 +114,16 @@ class WhatsApp {
|
|
|
114
114
|
logger: (0, pino_1.default)({ level: 'silent' }),
|
|
115
115
|
syncFullHistory: sync,
|
|
116
116
|
shouldSyncHistoryMessage: ({ syncType }) => sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
|
|
117
|
+
// Cuando el receptor no puede descifrar pide un retry; el cache interno de
|
|
118
|
+
// baileys indexa por JID pero el retry llega por LID y no lo encuentra: sin
|
|
119
|
+
// este fallback el mensaje muere en un solo check.
|
|
120
|
+
// When the receiver cannot decrypt it asks for a retry; the internal baileys
|
|
121
|
+
// cache indexes by JID but the retry arrives by LID and misses: without this
|
|
122
|
+
// fallback the message dies at a single check.
|
|
123
|
+
getMessage: async (key) => {
|
|
124
|
+
const found = key.remoteJid && key.id ? await locate(key.remoteJid, key.id) : null;
|
|
125
|
+
return found?.doc.raw.message ?? undefined;
|
|
126
|
+
},
|
|
117
127
|
markOnlineOnConnect: false,
|
|
118
128
|
});
|
|
119
129
|
alive = socket;
|
|
@@ -427,7 +437,13 @@ class WhatsApp {
|
|
|
427
437
|
}
|
|
428
438
|
if (kind === 'protocolMessage') {
|
|
429
439
|
const protocol = msg.message?.protocolMessage;
|
|
430
|
-
|
|
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;
|
|
431
447
|
if (found && protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
|
|
432
448
|
found.doc.raw.message = protocol.editedMessage;
|
|
433
449
|
found.doc.edited = true;
|
|
@@ -437,7 +453,12 @@ class WhatsApp {
|
|
|
437
453
|
this.emit('message:updated', instance, await instance.chat(), this);
|
|
438
454
|
}
|
|
439
455
|
else if (found && protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE) {
|
|
440
|
-
|
|
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);
|
|
441
462
|
const instance = new message_1.default(init, found.doc);
|
|
442
463
|
this.emit('message:deleted', instance, await instance.chat(), this);
|
|
443
464
|
}
|
|
@@ -448,6 +469,7 @@ class WhatsApp {
|
|
|
448
469
|
if (stored) {
|
|
449
470
|
doc.multiple = typeof stored.multiple === 'boolean' ? stored.multiple : doc.multiple;
|
|
450
471
|
doc.reactions = stored.reactions ?? doc.reactions;
|
|
472
|
+
doc.revoked_at = stored.revoked_at ?? doc.revoked_at;
|
|
451
473
|
const advanced = doc.status > stored.status;
|
|
452
474
|
doc.status = Math.max(stored.status, doc.status);
|
|
453
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. */
|
|
@@ -76,6 +76,16 @@ export default class WhatsApp {
|
|
|
76
76
|
logger: pino({ level: 'silent' }),
|
|
77
77
|
syncFullHistory: sync,
|
|
78
78
|
shouldSyncHistoryMessage: ({ syncType }) => sync || syncType !== proto.HistorySync.HistorySyncType.FULL,
|
|
79
|
+
// Cuando el receptor no puede descifrar pide un retry; el cache interno de
|
|
80
|
+
// baileys indexa por JID pero el retry llega por LID y no lo encuentra: sin
|
|
81
|
+
// este fallback el mensaje muere en un solo check.
|
|
82
|
+
// When the receiver cannot decrypt it asks for a retry; the internal baileys
|
|
83
|
+
// cache indexes by JID but the retry arrives by LID and misses: without this
|
|
84
|
+
// fallback the message dies at a single check.
|
|
85
|
+
getMessage: async (key) => {
|
|
86
|
+
const found = key.remoteJid && key.id ? await locate(key.remoteJid, key.id) : null;
|
|
87
|
+
return found?.doc.raw.message ?? undefined;
|
|
88
|
+
},
|
|
79
89
|
markOnlineOnConnect: false,
|
|
80
90
|
});
|
|
81
91
|
alive = socket;
|
|
@@ -389,7 +399,13 @@ export default class WhatsApp {
|
|
|
389
399
|
}
|
|
390
400
|
if (kind === 'protocolMessage') {
|
|
391
401
|
const protocol = msg.message?.protocolMessage;
|
|
392
|
-
|
|
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;
|
|
393
409
|
if (found && protocol?.type === proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
|
|
394
410
|
found.doc.raw.message = protocol.editedMessage;
|
|
395
411
|
found.doc.edited = true;
|
|
@@ -399,7 +415,12 @@ export default class WhatsApp {
|
|
|
399
415
|
this.emit('message:updated', instance, await instance.chat(), this);
|
|
400
416
|
}
|
|
401
417
|
else if (found && protocol?.type === proto.Message.ProtocolMessage.Type.REVOKE) {
|
|
402
|
-
|
|
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);
|
|
403
424
|
const instance = new Message(init, found.doc);
|
|
404
425
|
this.emit('message:deleted', instance, await instance.chat(), this);
|
|
405
426
|
}
|
|
@@ -410,6 +431,7 @@ export default class WhatsApp {
|
|
|
410
431
|
if (stored) {
|
|
411
432
|
doc.multiple = typeof stored.multiple === 'boolean' ? stored.multiple : doc.multiple;
|
|
412
433
|
doc.reactions = stored.reactions ?? doc.reactions;
|
|
434
|
+
doc.revoked_at = stored.revoked_at ?? doc.revoked_at;
|
|
413
435
|
const advanced = doc.status > stored.status;
|
|
414
436
|
doc.status = Math.max(stored.status, doc.status);
|
|
415
437
|
if (!advanced && stored.caption === doc.caption && stored.edited === doc.edited && stored.starred === doc.starred) {
|