@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.
@@ -45,10 +45,26 @@ const contact_1 = require("../../lib/contact");
45
45
  const message_1 = __importStar(require("../../lib/message"));
46
46
  const status_1 = require("../../lib/status");
47
47
  const store_1 = require("../../lib/store");
48
+ /**
49
+ * WhatsApp devuelve el nombre de la propia cuenta enmascarado —«+58∙∙∙∙∙∙∙∙40»— cuando el perfil
50
+ * no viajó completo. Eso no es un nombre: aceptarlo tapa al verdadero, que sí está guardado en
51
+ * la ficha del contacto propio.
52
+ * WhatsApp returns the own account name masked —«+58∙∙∙∙∙∙∙∙40»— when the profile did not travel
53
+ * whole. That is not a name: taking it hides the real one, which is stored on the own contact
54
+ * card.
55
+ */
56
+ const readable = (value) => (value && !/^\+?[\d\s·•∙⋅]+$/.test(value) ? value : null);
48
57
  class WhatsApp {
49
58
  #event = new node_events_1.EventEmitter();
50
59
  #options;
51
60
  #close = null;
61
+ /**
62
+ * Cierre completo: desvincula del teléfono y termina el socket. Es distinto de `#close`,
63
+ * que sólo cuelga —lo que hace falta al reconectar, donde desvincular sería absurdo—.
64
+ * Full close: unlinks from the phone and ends the socket. Distinct from `#close`, which
65
+ * merely hangs up —what reconnecting needs, where unlinking would be absurd—.
66
+ */
67
+ #unlink = null;
52
68
  constructor(options) {
53
69
  this.engine = options.engine;
54
70
  this.#options = options;
@@ -70,7 +86,7 @@ class WhatsApp {
70
86
  }
71
87
  async connect(callback) {
72
88
  const { engine } = this;
73
- const { phone, method, autoclean = true, sync = true, reconnect = true } = this.#options;
89
+ const { phone, method, autoclean = true, sync = true, reconnect = true, device } = this.#options;
74
90
  const digits = phone !== undefined ? String(phone).replace(/\D+/g, '') : '';
75
91
  const budget = reconnect === false ? 0 : reconnect === true ? null : typeof reconnect === 'number' ? reconnect : reconnect.max ?? null;
76
92
  const wait = typeof reconnect === 'object' ? (reconnect.interval ?? 60) * 1_000 : 60_000;
@@ -110,7 +126,7 @@ class WhatsApp {
110
126
  },
111
127
  },
112
128
  },
113
- browser: baileys_1.Browsers.windows('Chrome'),
129
+ browser: baileys_1.Browsers.appropriate(device ?? 'Orchestrator'),
114
130
  logger: (0, pino_1.default)({ level: 'silent' }),
115
131
  syncFullHistory: sync,
116
132
  shouldSyncHistoryMessage: ({ syncType }) => sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
@@ -136,16 +152,29 @@ class WhatsApp {
136
152
  if (!user)
137
153
  return null;
138
154
  const id = (0, baileys_1.jidNormalizedUser)(user.id);
155
+ // La cuenta propia también se guarda por LID cuando el teléfono se anuncia
156
+ // así, y entonces la ficha del JID viene vacía: se leen las dos y gana la
157
+ // que tenga el dato.
158
+ // The own account is stored by LID too when the phone announces itself that
159
+ // way, and then the JID card comes back empty: both are read and whichever
160
+ // holds the data wins.
139
161
  const card = (0, store_1.deserialize)(await engine.get(`/contact/${id}`));
162
+ const alias = user.lid ? (0, store_1.deserialize)(await engine.get(`/contact/${(0, baileys_1.jidNormalizedUser)(user.lid)}`)) : null;
140
163
  return new contact_1.Account(init, {
141
164
  id,
142
165
  phone_number: id,
143
- lid: user.lid ?? card?.lid ?? null,
144
- name: user.name ?? card?.name ?? null,
145
- notify: card?.notify ?? null,
146
- verified_name: card?.verified_name ?? null,
147
- img_url: (await socket.profilePictureUrl(id, 'image').catch(() => null)) ?? card?.img_url ?? null,
148
- status: card?.status ?? null,
166
+ lid: user.lid ?? card?.lid ?? alias?.lid ?? null,
167
+ // `verified_name` es el nombre de una cuenta de empresa y `notify` el
168
+ // que la propia línea difunde en sus mensajes: cualquiera de los dos es
169
+ // el nombre real de la cuenta cuando el perfil no viajó en el login.
170
+ // `verified_name` is a business account's name and `notify` the one the
171
+ // line itself broadcasts in its messages: either is the account's real
172
+ // name when the profile did not travel in the login.
173
+ name: readable(user.name) ?? readable(card?.name) ?? readable(alias?.name) ?? card?.verified_name ?? alias?.verified_name ?? card?.notify ?? alias?.notify ?? null,
174
+ notify: card?.notify ?? alias?.notify ?? null,
175
+ verified_name: card?.verified_name ?? alias?.verified_name ?? null,
176
+ img_url: (await socket.profilePictureUrl(id, 'image').catch(() => null)) ?? card?.img_url ?? alias?.img_url ?? null,
177
+ status: card?.status ?? alias?.status ?? null,
149
178
  });
150
179
  };
151
180
  const locate = async (cid, mid) => {
@@ -160,6 +189,89 @@ class WhatsApp {
160
189
  }
161
190
  return null;
162
191
  };
192
+ /**
193
+ * Identidad con la que se guarda a alguien. El mismo contacto llega unas veces
194
+ * por teléfono y otras por LID, y tratar ambos como distintos le abre dos fichas
195
+ * y dos chats. El teléfono manda; el LID sólo se conserva cuando aún no hay
196
+ * forma de traducirlo.
197
+ * The identity someone is stored under. The same contact arrives sometimes by
198
+ * phone and sometimes by LID, and treating both as distinct opens two cards and
199
+ * two chats for them. The phone wins; the LID is only kept while there is still
200
+ * no way to translate it.
201
+ */
202
+ const canonical = async (uid) => (uid.endsWith('@lid') ? await (0, store_1.jid_of)(engine, uid, socket).catch(() => null) : null) ?? uid;
203
+ /** Índice LID↔teléfono, sólo cuando traduce de verdad. / LID↔phone index, only when it actually translates. */
204
+ const remember = async (lid, jid) => {
205
+ if (lid && !jid.endsWith('@lid')) {
206
+ await engine.set(`/lid/${lid}`, (0, store_1.serialize)(jid));
207
+ await engine.set(`/lid/${jid}`, (0, store_1.serialize)(lid));
208
+ }
209
+ };
210
+ /**
211
+ * Vuelca sobre el teléfono lo que se había guardado bajo el LID —ficha, chat y
212
+ * mensajes— y borra el duplicado. Los campos ya presentes en el destino ganan:
213
+ * son los que la cuenta viene usando.
214
+ * Pours whatever was stored under the LID —card, chat and messages— onto the
215
+ * phone and drops the duplicate. Fields already present on the target win: those
216
+ * are the ones the account has been using.
217
+ */
218
+ const absorb = async (lid, pn) => {
219
+ const [from, to] = [(0, baileys_1.jidNormalizedUser)(lid), (0, baileys_1.jidNormalizedUser)(pn)];
220
+ if (from !== to) {
221
+ const stale = (0, store_1.deserialize)(await engine.get(`/contact/${from}`));
222
+ if (stale) {
223
+ const target = (0, store_1.deserialize)(await engine.get(`/contact/${to}`));
224
+ await engine.set(`/contact/${to}`, (0, store_1.serialize)({ ...stale, ...target, id: to, lid: from }));
225
+ await engine.unset(`/contact/${from}`);
226
+ }
227
+ const orphan = (0, store_1.deserialize)(await engine.get(`/chat/${from}`));
228
+ if (orphan) {
229
+ const target = (0, store_1.deserialize)(await engine.get(`/chat/${to}`));
230
+ for (const raw of await engine.list(`/chat/${from}/message`, 0, 10_000)) {
231
+ const msg = (0, store_1.deserialize)(raw);
232
+ if (msg) {
233
+ await engine.set(`/chat/${to}/message/${msg.id}`, (0, store_1.serialize)({ ...msg, cid: to }), msg.created_at);
234
+ await engine.unset(`/chat/${from}/message/${msg.id}`);
235
+ }
236
+ }
237
+ const doc = { ...orphan, ...target, id: to, activity: Math.max(orphan.activity ?? 0, target?.activity ?? 0) || null };
238
+ await engine.set(`/chat/${to}`, (0, store_1.serialize)(doc), doc.activity ?? 0);
239
+ await engine.unset(`/chat/${from}`);
240
+ // Para quien escucha, el duplicado desaparece y el bueno aparece: es
241
+ // literalmente lo que pasó, y deja la lista sin la fila fantasma.
242
+ // To a listener the duplicate goes away and the good one shows up:
243
+ // that is literally what happened, and it leaves the list without
244
+ // the ghost row.
245
+ this.emit('chat:deleted', new this.Chat(orphan), this);
246
+ if (!target) {
247
+ this.emit('chat:created', new this.Chat(doc), this);
248
+ }
249
+ }
250
+ }
251
+ };
252
+ /**
253
+ * Pasa por todo lo guardado bajo un LID y lo une a su teléfono. Cubre lo que se
254
+ * escribió antes de que el mapeo existiera —o antes de que la librería supiera
255
+ * unirlo—, que es lo que deja la lista con el mismo contacto dos veces: una con
256
+ * su nombre y otra como un número largo sin sentido.
257
+ * Walks everything stored under a LID and joins it to its phone. It covers what
258
+ * was written before the mapping existed —or before the library knew how to join
259
+ * it—, which is what leaves the same contact twice in the list: once with a name
260
+ * and once as a long meaningless number.
261
+ */
262
+ const reconcile = async () => {
263
+ for (const path of ['/contact', '/chat']) {
264
+ for (const raw of await engine.list(path, 0, 10_000)) {
265
+ const id = (0, store_1.deserialize)(raw)?.id;
266
+ if (id?.endsWith('@lid')) {
267
+ const jid = await (0, store_1.jid_of)(engine, id, socket).catch(() => null);
268
+ if (jid) {
269
+ await absorb(id, jid);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ };
163
275
  socket.ev.on('creds.update', () => engine.set('/session/creds', (0, store_1.serialize)(creds)));
164
276
  socket.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
165
277
  if (qr && !creds.registered) {
@@ -170,6 +282,13 @@ class WhatsApp {
170
282
  if (connection === 'open') {
171
283
  connected = true;
172
284
  retries = 0;
285
+ // Los mapeos que faltaban ya viajaron en el handshake: recién ahora se
286
+ // puede unir lo que quedó partido en sesiones anteriores, cuando esos
287
+ // LID todavía eran intraducibles.
288
+ // The missing mappings already travelled in the handshake: only now can
289
+ // whatever stayed split in earlier sessions be joined, back when those
290
+ // LIDs were still untranslatable.
291
+ chain = chain.then(reconcile).catch(() => { });
173
292
  this.emit('connected', this);
174
293
  resolve();
175
294
  }
@@ -209,10 +328,11 @@ class WhatsApp {
209
328
  chain = chain.then(async () => {
210
329
  for (const row of rows) {
211
330
  if (row.id) {
212
- const current = (0, store_1.deserialize)(await engine.get(`/contact/${row.id}`));
331
+ const id = await canonical(row.id);
332
+ const current = (0, store_1.deserialize)(await engine.get(`/contact/${id}`));
213
333
  const doc = {
214
- id: row.id,
215
- lid: row.lid ?? current?.lid ?? null,
334
+ id,
335
+ lid: row.lid ?? (row.id.endsWith('@lid') ? row.id : null) ?? current?.lid ?? null,
216
336
  name: row.name ?? current?.name ?? null,
217
337
  notify: row.notify ?? current?.notify ?? null,
218
338
  verified_name: row.verifiedName ?? current?.verified_name ?? null,
@@ -220,13 +340,11 @@ class WhatsApp {
220
340
  status: row.status ?? current?.status ?? null,
221
341
  };
222
342
  if (!current || JSON.stringify(current) !== JSON.stringify(doc)) {
223
- await engine.set(`/contact/${row.id}`, (0, store_1.serialize)(doc));
224
- if (doc.lid) {
225
- await engine.set(`/lid/${doc.lid}`, (0, store_1.serialize)(doc.id));
226
- }
343
+ await engine.set(`/contact/${id}`, (0, store_1.serialize)(doc));
344
+ await remember(doc.lid, id);
227
345
  const person = new this.Contact(doc);
228
- const owner = (0, store_1.deserialize)(await engine.get(`/chat/${doc.id}`));
229
- this.emit(current ? 'contact:updated' : 'contact:created', person, new this.Chat(owner ?? { id: doc.id, name: person.name }), this);
346
+ const owner = (0, store_1.deserialize)(await engine.get(`/chat/${id}`));
347
+ this.emit(current ? 'contact:updated' : 'contact:created', person, new this.Chat(owner ?? { id, name: person.name }), this);
230
348
  }
231
349
  }
232
350
  }
@@ -235,38 +353,44 @@ class WhatsApp {
235
353
  socket.ev.on('contacts.update', (rows) => {
236
354
  chain = chain.then(async () => {
237
355
  for (const row of rows) {
238
- const current = row.id ? (0, store_1.deserialize)(await engine.get(`/contact/${row.id}`)) : null;
356
+ const id = row.id ? await canonical(row.id) : '';
357
+ const current = id ? (0, store_1.deserialize)(await engine.get(`/contact/${id}`)) : null;
239
358
  const patch = {
240
359
  ...(row.notify && { notify: row.notify }),
241
360
  ...(row.name && { name: row.name }),
242
361
  ...(row.verifiedName && { verified_name: row.verifiedName }),
243
362
  ...(typeof row.imgUrl === 'string' && { img_url: row.imgUrl }),
244
363
  ...(row.status && { status: row.status }),
245
- ...(row.lid && { lid: row.lid }),
364
+ ...((row.lid ?? (row.id?.endsWith('@lid') ? row.id : null)) && { lid: row.lid ?? row.id }),
246
365
  };
247
- if (current && row.id && Object.keys(patch).length > 0) {
366
+ if (current && Object.keys(patch).length > 0) {
248
367
  const doc = { ...current, ...patch };
249
- await engine.set(`/contact/${row.id}`, (0, store_1.serialize)(doc));
250
- if (patch.lid) {
251
- await engine.set(`/lid/${patch.lid}`, (0, store_1.serialize)(row.id));
252
- }
368
+ await engine.set(`/contact/${id}`, (0, store_1.serialize)(doc));
369
+ await remember(patch.lid, id);
253
370
  const person = new this.Contact(doc);
254
- const owner = (0, store_1.deserialize)(await engine.get(`/chat/${row.id}`));
255
- this.emit('contact:updated', person, new this.Chat(owner ?? { id: row.id, name: person.name }), this);
371
+ const owner = (0, store_1.deserialize)(await engine.get(`/chat/${id}`));
372
+ this.emit('contact:updated', person, new this.Chat(owner ?? { id, name: person.name }), this);
256
373
  }
257
374
  }
258
375
  }).catch(() => { });
259
376
  });
260
377
  socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
261
378
  chain = chain.then(async () => {
262
- await engine.set(`/lid/${lid}`, (0, store_1.serialize)(pn));
263
- await engine.set(`/lid/${pn}`, (0, store_1.serialize)(lid));
379
+ await remember(lid, pn);
380
+ // El mapeo recién llega: lo que se guardó bajo el LID mientras era
381
+ // irresoluble se une ahora a su teléfono, o el contacto queda partido
382
+ // en dos fichas y dos chats que nunca se vuelven a encontrar.
383
+ // The mapping just arrived: whatever was stored under the LID while it
384
+ // was unresolvable now joins its phone, or the contact stays split into
385
+ // two cards and two chats that never meet again.
386
+ await absorb(lid, pn);
264
387
  }).catch(() => { });
265
388
  });
266
389
  socket.ev.on('chats.upsert', (rows) => {
267
390
  chain = chain.then(async () => {
268
- for (const row of rows) {
269
- if (row.id) {
391
+ for (const raw of rows) {
392
+ if (raw.id) {
393
+ const row = { ...raw, id: await canonical(raw.id) };
270
394
  const current = (0, store_1.deserialize)(await engine.get(`/chat/${row.id}`));
271
395
  const doc = current ?? {
272
396
  id: row.id,
@@ -437,7 +561,13 @@ class WhatsApp {
437
561
  }
438
562
  if (kind === 'protocolMessage') {
439
563
  const protocol = msg.message?.protocolMessage;
440
- const found = protocol?.key?.id ? await locate(protocol.key.remoteJid ?? cid, protocol.key.id) : null;
564
+ // El aviso puede venir direccionado por LID y el documento estar bajo el JID
565
+ // (o al revés): se busca por el chat que nombra el protocolo y por el del sobre.
566
+ // The notice may be LID-addressed while the document lives under the JID (or the
567
+ // other way around): it is looked up by the protocol's chat and by the envelope's.
568
+ const found = protocol?.key?.id
569
+ ? (await locate(protocol.key.remoteJid ?? cid, protocol.key.id)) ?? (await locate(cid, protocol.key.id))
570
+ : null;
441
571
  if (found && protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
442
572
  found.doc.raw.message = protocol.editedMessage;
443
573
  found.doc.edited = true;
@@ -447,7 +577,12 @@ class WhatsApp {
447
577
  this.emit('message:updated', instance, await instance.chat(), this);
448
578
  }
449
579
  else if (found && protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE) {
450
- await engine.unset(found.path);
580
+ // El mensaje retirado no se borra: se marca, y así la interfaz puede
581
+ // mostrar «se eliminó este mensaje» donde estaba en vez de un hueco.
582
+ // A revoked message is not removed: it gets flagged, so the interface can
583
+ // show "this message was deleted" in its place instead of a gap.
584
+ found.doc.revoked_at = Date.now();
585
+ await engine.set(found.path, (0, store_1.serialize)(found.doc), found.doc.created_at);
451
586
  const instance = new message_1.default(init, found.doc);
452
587
  this.emit('message:deleted', instance, await instance.chat(), this);
453
588
  }
@@ -458,24 +593,40 @@ class WhatsApp {
458
593
  if (stored) {
459
594
  doc.multiple = typeof stored.multiple === 'boolean' ? stored.multiple : doc.multiple;
460
595
  doc.reactions = stored.reactions ?? doc.reactions;
596
+ doc.revoked_at = stored.revoked_at ?? doc.revoked_at;
461
597
  const advanced = doc.status > stored.status;
462
598
  doc.status = Math.max(stored.status, doc.status);
463
599
  if (!advanced && stored.caption === doc.caption && stored.edited === doc.edited && stored.starred === doc.starred) {
464
600
  continue;
465
601
  }
466
602
  }
603
+ if (!stored && doc.me && msg.pushName && socket.user) {
604
+ // Un mensaje propio lleva el nombre con el que la cuenta se
605
+ // anuncia al mundo. Cuando el perfil no viajó en el login —una
606
+ // reconexión, por ejemplo— esta es la única vía para conocerlo,
607
+ // y sin ella la línea se ve a sí misma como un número.
608
+ // An own message carries the name the account announces itself
609
+ // with. When the profile did not travel in the login —a
610
+ // reconnection, say— this is the only way to learn it, and
611
+ // without it the line sees itself as a number.
612
+ const own = (0, baileys_1.jidNormalizedUser)(socket.user.id);
613
+ const known = (0, store_1.deserialize)(await engine.get(`/contact/${own}`));
614
+ if (!(known?.name ?? known?.notify ?? known?.verified_name)) {
615
+ socket.ev.emit('contacts.upsert', [{ id: own, lid: socket.user.lid, notify: readable(msg.pushName) ?? undefined }]);
616
+ }
617
+ }
467
618
  if (!stored && !doc.me) {
468
619
  const known = (0, store_1.deserialize)(await engine.get(`/contact/${doc.author}`));
469
620
  if (doc.author && !(known?.name ?? known?.notify ?? known?.verified_name)) {
470
621
  socket.ev.emit('contacts.upsert', [{
471
622
  id: doc.author,
472
623
  lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : undefined,
473
- notify: msg.pushName ?? undefined,
624
+ notify: readable(msg.pushName) ?? undefined,
474
625
  verifiedName: msg.verifiedBizName ?? undefined,
475
626
  }]);
476
627
  }
477
628
  if (!(await engine.get(`/chat/${cid}`))) {
478
- const owner = { id: cid, name: cid.endsWith('@g.us') ? null : msg.pushName ?? null, activity: doc.created_at };
629
+ const owner = { id: cid, name: cid.endsWith('@g.us') ? null : readable(msg.pushName), activity: doc.created_at };
479
630
  await engine.set(`/chat/${cid}`, (0, store_1.serialize)(owner), doc.created_at);
480
631
  this.emit('chat:created', new this.Chat(owner), this);
481
632
  }
@@ -585,6 +736,34 @@ class WhatsApp {
585
736
  }).catch(() => { });
586
737
  });
587
738
  };
739
+ this.#unlink = async (quiet) => {
740
+ intentional = true;
741
+ silent = quiet;
742
+ if (timer) {
743
+ clearTimeout(timer);
744
+ timer = null;
745
+ }
746
+ try {
747
+ // `logout` avisa al teléfono y termina el socket; la promesa no resuelve
748
+ // hasta que el teléfono acusa, que es cuando el dispositivo ya no existe.
749
+ // `logout` notifies the phone and ends the socket; the promise does not
750
+ // settle until the phone acknowledges, which is when the device is gone.
751
+ await alive?.logout();
752
+ }
753
+ catch {
754
+ // Sin red o con el socket ya muerto no hay a quién avisar: se cierra de
755
+ // este lado para no dejar el proceso colgado de una sesión que no existe.
756
+ // With no network or an already dead socket there is nobody to notify: it
757
+ // closes on this side so the process is not left hanging on a dead session.
758
+ try {
759
+ alive?.end(Object.assign(new Error('intentional close'), { output: { statusCode: baileys_1.DisconnectReason.connectionClosed } }));
760
+ }
761
+ catch {
762
+ /* el socket ya estaba cerrado / socket already closed */
763
+ }
764
+ }
765
+ alive = null;
766
+ };
588
767
  this.#close = async (quiet) => {
589
768
  intentional = true;
590
769
  silent = quiet;
@@ -604,16 +783,34 @@ class WhatsApp {
604
783
  });
605
784
  }
606
785
  /**
607
- * Cierra la sesión: cancela el reintento pendiente y termina el socket.
608
- * Closes the session: cancels the pending retry and ends the socket.
786
+ * Cierra la sesión de verdad: desvincula el dispositivo del teléfono y termina el socket.
787
+ * La promesa no resuelve hasta que todo eso ocurrió.
788
+ *
789
+ * Los dos flags modulan efectos secundarios, nunca si la sesión muere: `silent` sólo calla
790
+ * el evento `disconnected` local, y `destroy` decide si el engine se vacía o conserva
791
+ * chats, mensajes y contactos para estudiarlos después. Las credenciales se borran en los
792
+ * dos casos: el dispositivo ya no existe, así que reconectar con ellas sólo devolvería un
793
+ * `loggedOut`.
794
+ *
795
+ * Closes the session for real: unlinks the device from the phone and ends the socket. The
796
+ * promise does not settle until all of that happened.
797
+ *
798
+ * Both flags modulate side effects, never whether the session dies: `silent` only mutes the
799
+ * local `disconnected` event, and `destroy` decides whether the engine is wiped or keeps
800
+ * chats, messages and contacts for later study. Credentials go in both cases: the device no
801
+ * longer exists, so reconnecting with them would only return a `loggedOut`.
802
+ *
803
+ * @param options - `silent` calla el evento; `destroy` vacía el engine entero / `silent` mutes the event; `destroy` wipes the whole engine
609
804
  *
610
- * @param options - `silent` calla el evento `disconnected`; `destroy` vacía el engine / `silent` mutes the `disconnected` event; `destroy` clears the engine
805
+ * @example
806
+ * await wa.disconnect(); // desvincula y conserva el historial
807
+ * await wa.disconnect({ destroy: true }); // desvincula y no queda nada
611
808
  */
612
809
  async disconnect(options = {}) {
613
- await this.#close?.(options.silent === true);
614
- if (options.destroy) {
615
- await this.engine.clear();
616
- }
810
+ await this.#unlink?.(options.silent === true);
811
+ this.#unlink = null;
812
+ this.#close = null;
813
+ await (options.destroy ? this.engine.clear() : this.engine.unset('/session'));
617
814
  }
618
815
  }
619
816
  exports.default = WhatsApp;
@@ -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. */