@arcaelas/whatsapp 4.4.0 → 5.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.
Files changed (97) hide show
  1. package/build/cjs/index.d.ts +6 -4
  2. package/build/cjs/index.js +22 -2
  3. package/build/cjs/index.js.map +1 -1
  4. package/build/cjs/lib/bot/decorator.d.ts +2 -0
  5. package/build/cjs/lib/bot/decorator.js +6 -0
  6. package/build/cjs/lib/bot/decorator.js.map +1 -1
  7. package/build/cjs/lib/chat/index.d.ts +340 -204
  8. package/build/cjs/lib/chat/index.js +225 -213
  9. package/build/cjs/lib/chat/index.js.map +1 -1
  10. package/build/cjs/lib/contact/index.d.ts +162 -179
  11. package/build/cjs/lib/contact/index.js +96 -190
  12. package/build/cjs/lib/contact/index.js.map +1 -1
  13. package/build/cjs/lib/internal.d.ts +40 -0
  14. package/build/cjs/lib/internal.js +39 -0
  15. package/build/cjs/lib/internal.js.map +1 -0
  16. package/build/cjs/lib/message/index.d.ts +463 -278
  17. package/build/cjs/lib/message/index.js +830 -710
  18. package/build/cjs/lib/message/index.js.map +1 -1
  19. package/build/cjs/lib/status/index.d.ts +60 -70
  20. package/build/cjs/lib/status/index.js +106 -111
  21. package/build/cjs/lib/status/index.js.map +1 -1
  22. package/build/cjs/lib/store/engine/index.d.ts +21 -7
  23. package/build/cjs/lib/store/engine/index.js +6 -3
  24. package/build/cjs/lib/store/engine/index.js.map +1 -1
  25. package/build/cjs/lib/store/engine/lib/file_system/index.d.ts +38 -8
  26. package/build/cjs/lib/store/engine/lib/file_system/index.js +92 -67
  27. package/build/cjs/lib/store/engine/lib/file_system/index.js.map +1 -1
  28. package/build/cjs/lib/store/engine/lib/index.d.ts +112 -0
  29. package/build/cjs/lib/store/engine/lib/index.js +213 -0
  30. package/build/cjs/lib/store/engine/lib/index.js.map +1 -0
  31. package/build/cjs/lib/store/engine/lib/redis/index.d.ts +28 -13
  32. package/build/cjs/lib/store/engine/lib/redis/index.js +56 -42
  33. package/build/cjs/lib/store/engine/lib/redis/index.js.map +1 -1
  34. package/build/cjs/lib/store/engine/lib/s3/index.d.ts +44 -1
  35. package/build/cjs/lib/store/engine/lib/s3/index.js +112 -64
  36. package/build/cjs/lib/store/engine/lib/s3/index.js.map +1 -1
  37. package/build/cjs/lib/store/engine/lib/sqlite/index.d.ts +98 -0
  38. package/build/cjs/lib/store/engine/lib/sqlite/index.js +114 -0
  39. package/build/cjs/lib/store/engine/lib/sqlite/index.js.map +1 -0
  40. package/build/cjs/lib/store/index.d.ts +7 -3
  41. package/build/cjs/lib/store/index.js +14 -4
  42. package/build/cjs/lib/store/index.js.map +1 -1
  43. package/build/cjs/lib/whatsapp/index.d.ts +117 -63
  44. package/build/cjs/lib/whatsapp/index.js +404 -184
  45. package/build/cjs/lib/whatsapp/index.js.map +1 -1
  46. package/build/cjs/test.d.ts +1 -0
  47. package/build/cjs/test.js +72 -0
  48. package/build/cjs/test.js.map +1 -0
  49. package/build/esm/index.d.ts +6 -4
  50. package/build/esm/index.js +5 -2
  51. package/build/esm/index.js.map +1 -1
  52. package/build/esm/lib/bot/decorator.d.ts +2 -0
  53. package/build/esm/lib/bot/decorator.js +6 -0
  54. package/build/esm/lib/bot/decorator.js.map +1 -1
  55. package/build/esm/lib/chat/index.d.ts +340 -204
  56. package/build/esm/lib/chat/index.js +225 -213
  57. package/build/esm/lib/chat/index.js.map +1 -1
  58. package/build/esm/lib/contact/index.d.ts +162 -179
  59. package/build/esm/lib/contact/index.js +96 -189
  60. package/build/esm/lib/contact/index.js.map +1 -1
  61. package/build/esm/lib/internal.d.ts +40 -0
  62. package/build/esm/lib/internal.js +35 -0
  63. package/build/esm/lib/internal.js.map +1 -0
  64. package/build/esm/lib/message/index.d.ts +463 -278
  65. package/build/esm/lib/message/index.js +825 -705
  66. package/build/esm/lib/message/index.js.map +1 -1
  67. package/build/esm/lib/status/index.d.ts +60 -70
  68. package/build/esm/lib/status/index.js +105 -110
  69. package/build/esm/lib/status/index.js.map +1 -1
  70. package/build/esm/lib/store/engine/index.d.ts +21 -7
  71. package/build/esm/lib/store/engine/index.js +4 -2
  72. package/build/esm/lib/store/engine/index.js.map +1 -1
  73. package/build/esm/lib/store/engine/lib/file_system/index.d.ts +38 -8
  74. package/build/esm/lib/store/engine/lib/file_system/index.js +92 -67
  75. package/build/esm/lib/store/engine/lib/file_system/index.js.map +1 -1
  76. package/build/esm/lib/store/engine/lib/index.d.ts +112 -0
  77. package/build/esm/lib/store/engine/lib/index.js +206 -0
  78. package/build/esm/lib/store/engine/lib/index.js.map +1 -0
  79. package/build/esm/lib/store/engine/lib/redis/index.d.ts +28 -13
  80. package/build/esm/lib/store/engine/lib/redis/index.js +54 -40
  81. package/build/esm/lib/store/engine/lib/redis/index.js.map +1 -1
  82. package/build/esm/lib/store/engine/lib/s3/index.d.ts +44 -1
  83. package/build/esm/lib/store/engine/lib/s3/index.js +111 -63
  84. package/build/esm/lib/store/engine/lib/s3/index.js.map +1 -1
  85. package/build/esm/lib/store/engine/lib/sqlite/index.d.ts +98 -0
  86. package/build/esm/lib/store/engine/lib/sqlite/index.js +110 -0
  87. package/build/esm/lib/store/engine/lib/sqlite/index.js.map +1 -0
  88. package/build/esm/lib/store/index.d.ts +7 -3
  89. package/build/esm/lib/store/index.js +13 -4
  90. package/build/esm/lib/store/index.js.map +1 -1
  91. package/build/esm/lib/whatsapp/index.d.ts +117 -63
  92. package/build/esm/lib/whatsapp/index.js +406 -186
  93. package/build/esm/lib/whatsapp/index.js.map +1 -1
  94. package/build/esm/test.d.ts +1 -0
  95. package/build/esm/test.js +67 -0
  96. package/build/esm/test.js.map +1 -0
  97. package/package.json +2 -2
@@ -48,9 +48,31 @@ const pino_1 = __importDefault(require("pino"));
48
48
  const QRCode = __importStar(require("qrcode"));
49
49
  const chat_1 = require("../../lib/chat");
50
50
  const contact_1 = require("../../lib/contact");
51
+ const internal_1 = require("../../lib/internal");
51
52
  const message_1 = require("../../lib/message");
52
- const status_1 = __importStar(require("../../lib/status"));
53
+ const status_1 = require("../../lib/status");
53
54
  const store_1 = require("../../lib/store");
55
+ /** Estados legibles del mensaje que el receipt puede avanzar. / Readable message states a receipt can advance to. */
56
+ const READ = 4;
57
+ const PLAYED = 5;
58
+ /**
59
+ * Deduce el MIME de un binario por su firma, acotado a lo que WhatsApp acepta en un estado.
60
+ * Infers a binary's MIME from its signature, limited to what WhatsApp accepts in a status.
61
+ *
62
+ * @param data - Binario a inspeccionar / Binary to inspect
63
+ * @returns MIME reconocido, o null / Recognized MIME, or null
64
+ */
65
+ function sniff_media(data) {
66
+ if (data.subarray(0, 3).toString('hex') === 'ffd8ff')
67
+ return 'image/jpeg';
68
+ if (data.subarray(0, 8).toString('hex') === '89504e470d0a1a0a')
69
+ return 'image/png';
70
+ if (data.subarray(0, 4).toString() === 'RIFF' && data.subarray(8, 12).toString() === 'WEBP')
71
+ return 'image/webp';
72
+ if (data.subarray(4, 8).toString() === 'ftyp')
73
+ return 'video/mp4';
74
+ return null;
75
+ }
54
76
  /**
55
77
  * Cliente principal de WhatsApp. No inicia la conexión al instanciar.
56
78
  * Main WhatsApp client. Does not connect on instantiation.
@@ -61,54 +83,108 @@ const store_1 = require("../../lib/store");
61
83
  * await wa.connect((code) => console.log(code));
62
84
  */
63
85
  class WhatsApp {
86
+ /** @internal Emisor de los eventos del cliente. / Client event emitter. */
87
+ #event = new node_events_1.EventEmitter();
88
+ /** @internal Estado compartido con las entidades (socket, resolución de JIDs). / State shared with the entities. */
89
+ #internals;
90
+ #phone;
91
+ #method;
92
+ #autoclean;
93
+ #reconnect;
94
+ #sync;
95
+ #intentional_close = false;
96
+ #silent_close = false;
97
+ #has_connected = false;
98
+ #retry_timer = null;
99
+ #retry_count = 0;
100
+ /**
101
+ * @internal
102
+ * Cadena que serializa los handlers de eventos de baileys: dos eventos sobre el mismo
103
+ * documento ya no se intercalan (lost updates por read-modify-write concurrente).
104
+ * Chain serializing baileys event handlers: two events over the same document no longer
105
+ * interleave (lost updates from concurrent read-modify-write).
106
+ */
107
+ #chain = Promise.resolve();
64
108
  constructor(options) {
65
- /** @internal */
66
- this._socket = null;
67
- this._intentional_close = false;
68
- this._has_connected = false;
69
- this._retry_timer = null;
70
- this._retry_count = 0;
71
109
  this.engine = options.engine;
72
- this._phone =
73
- options.phone !== undefined ? String(options.phone).replace(/\D+/g, '') : undefined;
74
- this._autoclean = options.autoclean ?? true;
75
- this._sync = options.sync ?? false;
76
- this._reconnect = this._parse_reconnect(options.reconnect);
77
- this._event = new node_events_1.EventEmitter();
110
+ this.#phone = options.phone !== undefined ? String(options.phone).replace(/\D+/g, '') : undefined;
111
+ this.#method = options.method;
112
+ this.#autoclean = options.autoclean ?? true;
113
+ this.#sync = options.sync ?? true;
114
+ this.#reconnect =
115
+ options.reconnect === false ? { max: 0, interval_ms: 60_000 }
116
+ : options.reconnect === undefined || options.reconnect === true ? { max: null, interval_ms: 60_000 }
117
+ : typeof options.reconnect === 'number' ? { max: options.reconnect, interval_ms: 60_000 }
118
+ : { max: options.reconnect.max ?? null, interval_ms: (options.reconnect.interval ?? 60) * 1_000 };
119
+ this.#internals = { socket: null, resolve_jid: (uid) => this.#resolve_jid(uid) };
120
+ (0, internal_1.bind)(this, this.#internals);
78
121
  this.Contact = (0, contact_1.contact)(this);
79
122
  this.Chat = (0, chat_1.chat)(this);
80
- this.Message = (0, message_1.message)(this);
123
+ this.Message = {
124
+ get: (cid, mid) => message_1.Message.get(this, cid, mid),
125
+ list: (cid, offset, limit) => message_1.Message.list(this, cid, offset, limit),
126
+ text: (cid, ...rest) => message_1.Message.text(this, cid, ...rest),
127
+ image: (cid, ...rest) => message_1.Message.image(this, cid, ...rest),
128
+ video: (cid, ...rest) => message_1.Message.video(this, cid, ...rest),
129
+ audio: (cid, ...rest) => message_1.Message.audio(this, cid, ...rest),
130
+ location: (cid, ...rest) => message_1.Message.location(this, cid, ...rest),
131
+ poll: (cid, ...rest) => message_1.Message.poll(this, cid, ...rest),
132
+ document: (cid, ...rest) => message_1.Message.document(this, cid, ...rest),
133
+ vcard: (cid, ...rest) => message_1.Message.vcard(this, cid, ...rest),
134
+ event: (cid, ...rest) => message_1.Message.event(this, cid, ...rest),
135
+ react: (cid, mid, emoji) => message_1.Message.react(this, cid, mid, emoji),
136
+ star: (cid, mid, value) => message_1.Message.star(this, cid, mid, value),
137
+ seen: (cid, mid) => message_1.Message.seen(this, cid, mid),
138
+ edit: (cid, mid, caption) => message_1.Message.edit(this, cid, mid, caption),
139
+ forward: (cid, mid, target) => message_1.Message.forward(this, cid, mid, target),
140
+ delete: (cid, mid, all) => message_1.Message.delete(this, cid, mid, all),
141
+ reactions: (cid, mid) => message_1.Message.reactions(this, cid, mid),
142
+ Text: message_1.Text, Image: message_1.Image, Video: message_1.Video, Audio: message_1.Audio, Sticker: message_1.Sticker, Document: message_1.Document, Location: message_1.Location, Poll: message_1.Poll, VCard: message_1.VCard, Event: message_1.Event,
143
+ };
81
144
  }
82
- /** @internal */
83
- _parse_reconnect(option) {
84
- if (option === false) {
85
- return { max: 0, interval_ms: 60_000 };
86
- }
87
- if (option === undefined || option === true) {
88
- return { max: null, interval_ms: 60_000 };
89
- }
90
- if (typeof option === 'number') {
91
- return { max: option, interval_ms: 60_000 };
145
+ /**
146
+ * Contacto de la cuenta autenticada, o null mientras no hay sesión abierta.
147
+ * Authenticated account's contact, or null while there is no open session.
148
+ */
149
+ get contact() {
150
+ const user = this.#internals.socket?.user;
151
+ if (user) {
152
+ const jid = (0, baileys_1.jidNormalizedUser)(user.id);
153
+ return new this.Contact({ id: jid, phone_number: jid, lid: user.lid ?? null, name: user.name ?? null });
92
154
  }
93
- return {
94
- max: option.max ?? null,
95
- interval_ms: (option.interval ?? 60) * 1000,
96
- };
155
+ return null;
156
+ }
157
+ /** @internal Encola una tarea en la cadena serial de handlers. / Queues a task on the serial handler chain. */
158
+ #enqueue(task) {
159
+ this.#chain = this.#chain.then(task).catch(() => { });
160
+ }
161
+ /**
162
+ * Emite un evento del cliente. Lo usan las entidades de la librería para propagar los
163
+ * cambios que provocan; el consumidor puede emitir los suyos para pruebas.
164
+ * Emits a client event. Library entities use it to propagate the changes they cause;
165
+ * consumers may emit their own for testing.
166
+ *
167
+ * @param event - Nombre del evento / Event name
168
+ * @param args - Argumentos del evento / Event arguments
169
+ * @returns true si había listeners / true when listeners were present
170
+ */
171
+ emit(event, ...args) {
172
+ return this.#event.emit(event, ...args);
97
173
  }
98
174
  /**
99
175
  * Registra un listener de evento. Retorna función para desuscribirse.
100
176
  * Registers an event listener. Returns an unsubscribe function.
101
177
  */
102
178
  on(event, handler) {
103
- this._event.on(event, handler);
104
- return () => { this._event.off(event, handler); };
179
+ this.#event.on(event, handler);
180
+ return () => { this.#event.off(event, handler); };
105
181
  }
106
182
  /**
107
183
  * Quita un listener previamente registrado.
108
184
  * Removes a previously registered listener.
109
185
  */
110
186
  off(event, handler) {
111
- this._event.off(event, handler);
187
+ this.#event.off(event, handler);
112
188
  return this;
113
189
  }
114
190
  /**
@@ -116,15 +192,17 @@ class WhatsApp {
116
192
  * Registers a one-shot listener. Returns an unsubscribe function.
117
193
  */
118
194
  once(event, handler) {
119
- this._event.once(event, handler);
120
- return () => { this._event.off(event, handler); };
195
+ this.#event.once(event, handler);
196
+ return () => { this.#event.off(event, handler); };
121
197
  }
122
198
  /**
123
199
  * @internal
124
- * Normaliza cualquier identificador (JID, LID, número, etc.) a JID canónico.
125
- * Uso interno de la librería no forma parte de la API pública.
200
+ * Normaliza cualquier identificador (JID, LID, número, etc.) a JID canónico. Las
201
+ * entidades lo alcanzan por el canal interno, no por la instancia.
202
+ * Normalizes any identifier (JID, LID, number…) into a canonical JID. Entities reach it
203
+ * through the internal channel, not through the instance.
126
204
  */
127
- async _resolve_jid(uid) {
205
+ async #resolve_jid(uid) {
128
206
  let result = null;
129
207
  if (uid.endsWith('@g.us') || uid.endsWith('@s.whatsapp.net')) {
130
208
  result = uid;
@@ -139,6 +217,18 @@ class WhatsApp {
139
217
  if (reverse != null) {
140
218
  result = `${reverse}@s.whatsapp.net`;
141
219
  }
220
+ else {
221
+ // El store local puede no tener el mapping (sesión sin upsert del contacto);
222
+ // baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
223
+ // (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
224
+ // realmente está guardado, y el mensaje/poll no se encuentra.
225
+ const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
226
+ if (pn) {
227
+ // getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
228
+ // que el JID coincida con el que usa el store (sin device).
229
+ result = (0, baileys_1.jidNormalizedUser)(pn.includes('@') ? pn : `${pn}@s.whatsapp.net`);
230
+ }
231
+ }
142
232
  }
143
233
  }
144
234
  else {
@@ -157,16 +247,20 @@ class WhatsApp {
157
247
  * Resolves once the session is synced; retries on non-loggedOut disconnects.
158
248
  */
159
249
  async connect(callback) {
250
+ if (this.#internals.socket) {
251
+ await this.disconnect({ silent: true });
252
+ }
160
253
  const { version } = await (0, baileys_1.fetchLatestBaileysVersion)();
161
- this._intentional_close = false;
162
- this._has_connected = false;
254
+ this.#intentional_close = false;
255
+ this.#silent_close = false;
256
+ this.#has_connected = false;
163
257
  return new Promise((resolve, reject) => {
164
258
  const start = async () => {
165
259
  // Re-lee creds en cada start() para que limpiezas del engine tomen efecto
166
260
  // en reintentos (permite al consumer forzar nueva sesión borrando /session/creds).
167
261
  const stored = await this.engine.get('/session/creds');
168
262
  const creds = (0, store_1.deserialize)(stored) ?? (0, baileys_1.initAuthCreds)();
169
- this._socket = (0, baileys_1.makeWASocket)({
263
+ this.#internals.socket = (0, baileys_1.makeWASocket)({
170
264
  version,
171
265
  auth: {
172
266
  creds,
@@ -193,17 +287,19 @@ class WhatsApp {
193
287
  },
194
288
  browser: baileys_1.Browsers.windows('Chrome'),
195
289
  logger: (0, pino_1.default)({ level: 'silent' }),
196
- syncFullHistory: this._sync,
197
- // rc13 cambió el default de `shouldSyncHistoryMessage` a
198
- // `({ syncType }) => syncType !== FULL` (independiente de `syncFullHistory`).
199
- // Replicamos el comportamiento previo (rc.9), donde el sync de historial
200
- // dependía únicamente de `sync`.
201
- // rc13 changed the `shouldSyncHistoryMessage` default; we keep the rc.9
202
- // behavior where history sync depended solely on `sync`.
203
- shouldSyncHistoryMessage: () => this._sync,
290
+ syncFullHistory: this.#sync,
291
+ // Los syncs no-FULL cargan las LID mappings y los tctokens (trusted-contact
292
+ // tokens) que rc13 exige para enviar: apagarlos todos con `() => this.#sync`
293
+ // deja la sesión sin tctoken y el server rechaza los mensajes con
294
+ // "error 463: account restricted or missing tctoken". Por eso se procesan
295
+ // siempre los no-FULL; `sync` solo decide si además se trae el historial FULL.
296
+ // Non-FULL syncs carry the LID mappings and tctokens rc13 requires to send;
297
+ // disabling them all made the server reject messages with error 463. We always
298
+ // process non-FULL; `sync` only gates whether FULL history is pulled too.
299
+ shouldSyncHistoryMessage: ({ syncType }) => this.#sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
204
300
  markOnlineOnConnect: false,
205
301
  });
206
- const socket = this._socket;
302
+ const socket = this.#internals.socket;
207
303
  socket.ev.on('creds.update', () => this.engine.set('/session/creds', (0, store_1.serialize)(creds)));
208
304
  socket.ev.on('connection.update', async (update) => {
209
305
  const { connection, lastDisconnect, qr } = update;
@@ -211,21 +307,25 @@ class WhatsApp {
211
307
  // Baileys refresca QR periódicamente (~20s). Emitimos nuevo pair
212
308
  // code / QR en cada refresh para que el usuario pueda renovar si
213
309
  // el anterior expiró.
214
- if (this._phone) {
215
- await callback(await socket.requestPairingCode(this._phone));
310
+ // El PIN necesita número: sin `phone` la vinculación es siempre QR, y
311
+ // con `phone` manda `method` (OTP por defecto).
312
+ // A PIN requires a number: without `phone` linking is always QR, and with
313
+ // `phone` the `method` option decides (OTP by default).
314
+ if (this.#phone && (this.#method ?? 'otp') === 'otp') {
315
+ await callback(await socket.requestPairingCode(this.#phone));
216
316
  }
217
317
  else {
218
318
  await callback(await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
219
319
  }
220
320
  }
221
321
  if (connection === 'open') {
222
- this._has_connected = true;
223
- this._retry_count = 0;
224
- this._event.emit('connected', this);
322
+ this.#has_connected = true;
323
+ this.#retry_count = 0;
324
+ this.emit('connected', this);
225
325
  resolve();
226
326
  }
227
327
  else if (connection === 'close') {
228
- this._socket = null;
328
+ this.#internals.socket = null;
229
329
  const status_code = lastDisconnect?.error?.output?.statusCode;
230
330
  // `restartRequired` (515) es una reconexión exigida por el protocolo
231
331
  // tras el sync inicial — no es un disconnect "real".
@@ -233,35 +333,37 @@ class WhatsApp {
233
333
  // Limpieza del engine ANTES de emitir `disconnected` para que los
234
334
  // listeners vean el estado final (engine vaciado o creds borradas).
235
335
  if (status_code === baileys_1.DisconnectReason.loggedOut) {
236
- if (this._autoclean) {
336
+ if (this.#autoclean) {
237
337
  await this.engine.clear();
238
338
  }
239
339
  else {
240
340
  await this.engine.unset('/session/creds');
241
341
  }
242
342
  }
243
- if (this._has_connected && !is_transient) {
244
- this._event.emit('disconnected', this);
343
+ // `disconnect({ silent: true })` calla el evento de este cierre concreto.
344
+ // `disconnect({ silent: true })` mutes this specific close's event.
345
+ if (this.#has_connected && !is_transient && !this.#silent_close) {
346
+ this.emit('disconnected', this);
245
347
  }
246
- if (!this._intentional_close) {
348
+ if (!this.#intentional_close) {
247
349
  if (status_code === baileys_1.DisconnectReason.loggedOut) {
248
350
  reject(new Error('Logged out'));
249
351
  }
250
352
  else {
251
- const max = this._reconnect.max;
353
+ const max = this.#reconnect.max;
252
354
  // Transient closes (restartRequired) son parte del protocolo,
253
355
  // no cuentan contra el límite de reintentos por fallo.
254
- const exhausted = !is_transient && max !== null && this._retry_count >= max;
356
+ const exhausted = !is_transient && max !== null && this.#retry_count >= max;
255
357
  if (exhausted) {
256
358
  reject(new Error(`Reconnect attempts exhausted (${max})`));
257
359
  }
258
360
  else {
259
361
  if (!is_transient) {
260
- this._retry_count++;
362
+ this.#retry_count++;
261
363
  }
262
- const delay = is_transient ? 0 : this._reconnect.interval_ms;
263
- this._retry_timer = setTimeout(() => {
264
- this._retry_timer = null;
364
+ const delay = is_transient ? 0 : this.#reconnect.interval_ms;
365
+ this.#retry_timer = setTimeout(() => {
366
+ this.#retry_timer = null;
265
367
  start().catch(reject);
266
368
  }, delay);
267
369
  }
@@ -269,24 +371,119 @@ class WhatsApp {
269
371
  }
270
372
  }
271
373
  });
272
- this._attach_business_handlers(socket);
374
+ this.#attach_business_handlers(socket);
273
375
  };
274
376
  start().catch(reject);
275
377
  });
276
378
  }
379
+ /**
380
+ * Actualiza el perfil de la cuenta en WhatsApp: nombre público, bio y/o foto. Sólo se
381
+ * envía lo que llega en el parche; `photo: null` elimina la foto actual.
382
+ * Updates the account profile on WhatsApp: public name, bio and/or picture. Only the
383
+ * given fields are sent; `photo: null` removes the current picture.
384
+ *
385
+ * @param patch - Campos a actualizar / Fields to update
386
+ * @returns true si todo lo pedido se envió / true when everything requested was sent
387
+ * @throws ERR_PROFILE_PICTURE_LIB si falta `sharp` o `jimp` para procesar la foto / when `sharp` or `jimp` is missing to process the picture
388
+ */
389
+ async profile(patch) {
390
+ const socket = this.#internals.socket;
391
+ let ok = false;
392
+ if (socket) {
393
+ const self = (0, baileys_1.jidNormalizedUser)(socket.user?.id ?? '');
394
+ if (patch.name !== undefined) {
395
+ await socket.updateProfileName(patch.name);
396
+ }
397
+ if (patch.content !== undefined) {
398
+ await socket.updateProfileStatus(patch.content);
399
+ }
400
+ if (patch.photo === null) {
401
+ await socket.removeProfilePicture(self);
402
+ }
403
+ else if (patch.photo !== undefined) {
404
+ // baileys redimensiona la foto con `sharp` o `jimp`; ninguna es dependencia
405
+ // nuestra, así que su ausencia se traduce a un error accionable.
406
+ // baileys resizes the picture with `sharp` or `jimp`; neither is a dependency
407
+ // of ours, so their absence is translated into an actionable error.
408
+ await socket
409
+ .updateProfilePicture(self, typeof patch.photo === 'string' ? { url: patch.photo } : patch.photo)
410
+ .catch((error) => {
411
+ if (/image processing library/i.test(error.message)) {
412
+ throw new Error('ERR_PROFILE_PICTURE_LIB');
413
+ }
414
+ throw error;
415
+ });
416
+ }
417
+ ok = true;
418
+ }
419
+ return ok;
420
+ }
421
+ /**
422
+ * Publica un estado (status broadcast). Con sólo `caption` publica texto; con `content`
423
+ * publica imagen o video (el tipo se deduce del binario) usando `caption` como pie.
424
+ * `contacts` es la audiencia: WhatsApp no reparte el estado a quien no esté en la lista.
425
+ * Publishes a status broadcast. With only `caption` it posts text; with `content` it
426
+ * posts an image or video (type inferred from the binary) using `caption` as its footer.
427
+ * `contacts` is the audience: WhatsApp does not deliver the status to anyone outside it.
428
+ *
429
+ * @param post - Contenido, pie y audiencia / Content, caption and audience
430
+ * @returns Publicación creada, o null si no hay sesión / Created post, or null without a session
431
+ * @throws ERR_FEED_EMPTY sin `content` ni `caption` / when neither `content` nor `caption` is given
432
+ * @throws ERR_FEED_MEDIA si el binario no es imagen ni video / when the binary is neither image nor video
433
+ */
434
+ async feed(post) {
435
+ const socket = this.#internals.socket;
436
+ let result = null;
437
+ if (socket) {
438
+ const audience = (await Promise.all(post.contacts.map((uid) => this.#resolve_jid(String(uid))))).filter((jid) => jid !== null);
439
+ const mime = post.content ? sniff_media(post.content) : null;
440
+ if (post.content && !mime) {
441
+ throw new Error('ERR_FEED_MEDIA');
442
+ }
443
+ if (!post.content && !post.caption) {
444
+ throw new Error('ERR_FEED_EMPTY');
445
+ }
446
+ const kind = mime?.startsWith('video/') ? 'video' : mime ? 'image' : 'text';
447
+ const sent = await socket.sendMessage('status@broadcast', (post.content
448
+ ? { [kind]: post.content, caption: post.caption }
449
+ : { text: post.caption }), { statusJidList: audience });
450
+ if (sent?.key?.id) {
451
+ const created_at = (Number(sent.messageTimestamp) || Math.floor(Date.now() / 1_000)) * 1_000;
452
+ const doc = {
453
+ id: sent.key.id,
454
+ author_jid: (0, baileys_1.jidNormalizedUser)(socket.user?.id ?? ''),
455
+ type: kind,
456
+ caption: post.caption ?? '',
457
+ mime: mime ?? 'text/plain',
458
+ created_at,
459
+ expires_at: created_at + status_1.TTL_MS,
460
+ viewed: true,
461
+ raw: sent,
462
+ };
463
+ await this.engine.set(`/status/${doc.id}`, (0, store_1.serialize)(doc), created_at);
464
+ if (post.content) {
465
+ await this.engine.set(`/status/${doc.id}/content`, (0, store_1.serialize)({ data: post.content.toString('base64') }));
466
+ }
467
+ result = new status_1.Feed(this, doc);
468
+ this.emit('feed:created', result, this);
469
+ }
470
+ }
471
+ return result;
472
+ }
277
473
  /**
278
474
  * Cierra la conexión. Con `destroy: true` vacía el engine completo.
279
475
  * Closes the connection. With `destroy: true` clears the engine entirely.
280
476
  */
281
477
  async disconnect(options = {}) {
282
- this._intentional_close = true;
478
+ this.#intentional_close = true;
479
+ this.#silent_close = options.silent === true;
283
480
  // Cancela cualquier retry programado por un close anterior, para no resucitar
284
481
  // el socket después de una desconexión manual.
285
- if (this._retry_timer) {
286
- clearTimeout(this._retry_timer);
287
- this._retry_timer = null;
482
+ if (this.#retry_timer) {
483
+ clearTimeout(this.#retry_timer);
484
+ this.#retry_timer = null;
288
485
  }
289
- if (this._socket) {
486
+ if (this.#internals.socket) {
290
487
  try {
291
488
  // Pasa un error Boom-like con statusCode=connectionClosed (428) para que
292
489
  // `lastDisconnect.error.output.statusCode` quede explícito en el close
@@ -294,17 +491,16 @@ class WhatsApp {
294
491
  const intentional = Object.assign(new Error('intentional close'), {
295
492
  output: { statusCode: baileys_1.DisconnectReason.connectionClosed },
296
493
  });
297
- await this._socket.end(intentional);
494
+ await this.#internals.socket.end(intentional);
298
495
  }
299
496
  catch {
300
497
  /* socket may already be closed */
301
498
  }
302
- this._socket = null;
499
+ this.#internals.socket = null;
303
500
  }
304
501
  if (options.destroy) {
305
502
  await this.engine.clear();
306
503
  }
307
- void options.silent;
308
504
  }
309
505
  /**
310
506
  * Conecta los handlers de eventos de baileys: contactos, chats y mensajes.
@@ -312,43 +508,45 @@ class WhatsApp {
312
508
  *
313
509
  * @internal
314
510
  */
315
- _attach_business_handlers(socket) {
316
- socket.ev.on('messaging-history.set', async ({ chats, contacts, messages }) => {
317
- await this._handle_contacts_upsert(contacts);
318
- await this._handle_chats_upsert(chats);
319
- await this._handle_messages_upsert(messages);
511
+ #attach_business_handlers(socket) {
512
+ socket.ev.on('messaging-history.set', ({ chats, contacts, messages }) => {
513
+ this.#enqueue(async () => {
514
+ await this.#handle_contacts_upsert(contacts);
515
+ await this.#handle_chats_upsert(chats);
516
+ await this.#handle_messages_upsert(messages);
517
+ });
320
518
  });
321
519
  socket.ev.on('contacts.upsert', (contacts) => {
322
- void this._handle_contacts_upsert(contacts);
520
+ this.#enqueue(() => this.#handle_contacts_upsert(contacts));
323
521
  });
324
522
  socket.ev.on('contacts.update', (contacts) => {
325
- void this._handle_contacts_update(contacts);
523
+ this.#enqueue(() => this.#handle_contacts_update(contacts));
326
524
  });
327
525
  socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
328
- void this._handle_lid_mapping(lid, pn);
526
+ this.#enqueue(() => this.#handle_lid_mapping(lid, pn));
329
527
  });
330
528
  socket.ev.on('chats.upsert', (chats) => {
331
- void this._handle_chats_upsert(chats);
529
+ this.#enqueue(() => this.#handle_chats_upsert(chats));
332
530
  });
333
531
  socket.ev.on('chats.update', (chats) => {
334
- void this._handle_chats_update(chats);
532
+ this.#enqueue(() => this.#handle_chats_update(chats));
335
533
  });
336
534
  socket.ev.on('chats.delete', (ids) => {
337
- void this._handle_chats_delete(ids);
535
+ this.#enqueue(() => this.#handle_chats_delete(ids));
338
536
  });
339
537
  socket.ev.on('messages.upsert', ({ messages }) => {
340
- void this._handle_messages_upsert(messages);
538
+ this.#enqueue(() => this.#handle_messages_upsert(messages));
341
539
  });
342
540
  socket.ev.on('messages.update', (updates) => {
343
- void this._handle_messages_update(updates);
541
+ this.#enqueue(() => this.#handle_messages_update(updates));
344
542
  });
345
543
  socket.ev.on('message-receipt.update', (updates) => {
346
- void this._handle_message_receipt(updates);
544
+ this.#enqueue(() => this.#handle_message_receipt(updates));
347
545
  });
348
546
  // Las reacciones llegan duplicadas por `messages.reaction` Y `messages.upsert`
349
547
  // (como `reactionMessage`). Se usa solo `messages.upsert` para evitar el doble disparo.
350
548
  // socket.ev.on('messages.reaction', (reactions) => {
351
- // void this._handle_messages_reaction(reactions);
549
+ // void this.#handle_messages_reaction(reactions);
352
550
  // });
353
551
  }
354
552
  /**
@@ -358,23 +556,23 @@ class WhatsApp {
358
556
  * @param raw - Documento del contacto a persistir / Contact document to persist
359
557
  * @internal
360
558
  */
361
- async _persist_contact(raw) {
559
+ async #persist_contact(raw) {
362
560
  const existing = await this.engine.get(`/contact/${raw.id}`);
363
561
  await this.engine.set(`/contact/${raw.id}`, (0, store_1.serialize)(raw));
364
562
  if (raw.lid) {
365
563
  await this.engine.set(`/lid/${raw.lid}`, (0, store_1.serialize)(raw.id));
366
564
  }
367
565
  if (existing === null) {
566
+ const person = new this.Contact(raw);
368
567
  const cached_chat = (0, store_1.deserialize)(await this.engine.get(`/chat/${raw.id}`));
369
- const chat_instance = new this.Chat(cached_chat ?? { id: raw.id, name: (0, contact_1.contact_name)(raw) });
370
- this._event.emit('contact:created', new this.Contact(raw, chat_instance), chat_instance, this);
568
+ this.emit('contact:created', person, new this.Chat(cached_chat ?? { id: raw.id, name: person.name }), this);
371
569
  }
372
570
  }
373
571
  /** @internal */
374
- async _handle_contacts_upsert(contacts) {
572
+ async #handle_contacts_upsert(contacts) {
375
573
  for (const c of contacts) {
376
574
  if (c.id) {
377
- await this._persist_contact({
575
+ await this.#persist_contact({
378
576
  id: c.id,
379
577
  lid: c.lid ?? null,
380
578
  name: c.name ?? null,
@@ -387,7 +585,7 @@ class WhatsApp {
387
585
  }
388
586
  }
389
587
  /** @internal */
390
- async _handle_contacts_update(contacts) {
588
+ async #handle_contacts_update(contacts) {
391
589
  for (const c of contacts) {
392
590
  if (c.id) {
393
591
  const current = (0, store_1.deserialize)(await this.engine.get(`/contact/${c.id}`));
@@ -405,21 +603,21 @@ class WhatsApp {
405
603
  if (patch.lid) {
406
604
  await this.engine.set(`/lid/${patch.lid}`, (0, store_1.serialize)(c.id));
407
605
  }
606
+ const person = new this.Contact(merged);
408
607
  const cached_chat = (0, store_1.deserialize)(await this.engine.get(`/chat/${c.id}`));
409
- const chat_instance = new this.Chat(cached_chat ?? { id: c.id, name: (0, contact_1.contact_name)(merged) });
410
- this._event.emit('contact:updated', new this.Contact(merged, chat_instance), chat_instance, this);
608
+ this.emit('contact:updated', person, new this.Chat(cached_chat ?? { id: c.id, name: person.name }), this);
411
609
  }
412
610
  }
413
611
  }
414
612
  }
415
613
  }
416
614
  /** @internal */
417
- async _handle_lid_mapping(lid, pn) {
615
+ async #handle_lid_mapping(lid, pn) {
418
616
  await this.engine.set(`/lid/${lid}`, (0, store_1.serialize)(pn));
419
617
  await this.engine.set(`/lid/${pn}`, (0, store_1.serialize)(lid));
420
618
  }
421
619
  /** @internal */
422
- async _handle_chats_upsert(chats) {
620
+ async #handle_chats_upsert(chats) {
423
621
  for (const ch of chats) {
424
622
  if (ch.id) {
425
623
  const current = (0, store_1.deserialize)(await this.engine.get(`/chat/${ch.id}`));
@@ -429,21 +627,19 @@ class WhatsApp {
429
627
  archived: ch.archived ?? null,
430
628
  pinned: ch.pinned ?? null,
431
629
  mute_end_time: ch.muteEndTime != null ? Number(ch.muteEndTime) : null,
432
- unread_count: ch.unreadCount ?? null,
433
- read_only: ch.readOnly ?? null,
434
630
  };
435
631
  if (ch.name) {
436
632
  raw.name = ch.name;
437
633
  }
438
634
  await this.engine.set(`/chat/${ch.id}`, (0, store_1.serialize)(raw));
439
635
  if (current === null) {
440
- this._event.emit('chat:created', new this.Chat(raw), this);
636
+ this.emit('chat:created', new this.Chat(raw), this);
441
637
  }
442
638
  }
443
639
  }
444
640
  }
445
641
  /** @internal */
446
- async _handle_chats_update(chats) {
642
+ async #handle_chats_update(chats) {
447
643
  for (const ch of chats) {
448
644
  if (ch.id && ch.id !== 'status@broadcast') {
449
645
  const current = (0, store_1.deserialize)(await this.engine.get(`/chat/${ch.id}`)) ?? {
@@ -466,36 +662,33 @@ class WhatsApp {
466
662
  if (mute_changed) {
467
663
  patch.mute_end_time = ch.muteEndTime != null ? Number(ch.muteEndTime) : null;
468
664
  }
469
- if (ch.unreadCount != null) {
470
- patch.unread_count = ch.unreadCount;
471
- }
472
665
  if (Object.keys(patch).length > 0) {
473
666
  const merged = { ...current, ...patch };
474
667
  await this.engine.set(`/chat/${ch.id}`, (0, store_1.serialize)(merged));
475
668
  if (pinned_changed) {
476
- this._event.emit(ch.pinned != null ? 'chat:pinned' : 'chat:unpinned', new this.Chat(merged), this);
669
+ this.emit(ch.pinned != null ? 'chat:pinned' : 'chat:unpinned', new this.Chat(merged), this);
477
670
  }
478
671
  if (archived_changed) {
479
- this._event.emit(ch.archived ? 'chat:archived' : 'chat:unarchived', new this.Chat(merged), this);
672
+ this.emit(ch.archived ? 'chat:archived' : 'chat:unarchived', new this.Chat(merged), this);
480
673
  }
481
674
  if (mute_changed) {
482
675
  const is_muted = patch.mute_end_time != null && patch.mute_end_time > Date.now();
483
- this._event.emit(is_muted ? 'chat:muted' : 'chat:unmuted', new this.Chat(merged), this);
676
+ this.emit(is_muted ? 'chat:muted' : 'chat:unmuted', new this.Chat(merged), this);
484
677
  }
485
678
  }
486
679
  }
487
680
  }
488
681
  }
489
682
  /** @internal */
490
- async _handle_chats_delete(ids) {
683
+ async #handle_chats_delete(ids) {
491
684
  for (const cid of ids) {
492
685
  const raw = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}`)) ?? { id: cid };
493
686
  await this.engine.unset(`/chat/${cid}`);
494
- this._event.emit('chat:deleted', new this.Chat(raw), this);
687
+ this.emit('chat:deleted', new this.Chat(raw), this);
495
688
  }
496
689
  }
497
690
  /** @internal */
498
- async _handle_message_receipt(updates) {
691
+ async #handle_message_receipt(updates) {
499
692
  for (const { key, receipt } of updates) {
500
693
  // Receipt sobre status@broadcast → marca el feed como visto y emite feed:updated.
501
694
  // Receipt on status@broadcast → marks feed viewed and emits feed:updated.
@@ -504,21 +697,27 @@ class WhatsApp {
504
697
  if (feed_raw && !feed_raw.viewed) {
505
698
  feed_raw.viewed = true;
506
699
  await this.engine.set(`/status/${key.id}`, (0, store_1.serialize)(feed_raw));
507
- this._event.emit('feed:updated', new status_1.default({ wa: this, doc: feed_raw }), this);
700
+ this.emit('feed:updated', new status_1.Feed(this, feed_raw), this);
508
701
  }
509
702
  continue;
510
703
  }
511
704
  if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
512
705
  const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
513
706
  if (doc) {
514
- const msg_instance = await this.Message.build_instance(doc);
515
- this._event.emit('message:seen', msg_instance, await msg_instance.chat(), this);
707
+ const next = receipt.playedTimestamp != null ? PLAYED : READ;
708
+ if (doc.status < next) {
709
+ doc.status = next;
710
+ doc.raw.status = next;
711
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
712
+ }
713
+ const msg_instance = (0, message_1.message)(this, doc);
714
+ this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
516
715
  }
517
716
  }
518
717
  }
519
718
  }
520
719
  /** @internal */
521
- async _handle_messages_upsert(messages) {
720
+ async #handle_messages_upsert(messages) {
522
721
  for (const msg of messages) {
523
722
  if (msg.key?.remoteJid && msg.key.id) {
524
723
  const cid = msg.key.remoteJidAlt ?? msg.key.remoteJid;
@@ -529,9 +728,13 @@ class WhatsApp {
529
728
  // Single channel for reactions: handled here; `messages.reaction` is disabled.
530
729
  const reaction = msg.message?.reactionMessage;
531
730
  if (reaction?.key?.id && reaction.key.remoteJid) {
532
- const target_cid = (await this._resolve_jid(reaction.key.remoteJid)) ?? reaction.key.remoteJid;
533
- await this._handle_messages_reaction([{
534
- key: { remoteJid: target_cid, id: reaction.key.id },
731
+ const target_cid = (await this.#resolve_jid(reaction.key.remoteJid)) ?? reaction.key.remoteJid;
732
+ await this.#handle_messages_reaction([{
733
+ key: {
734
+ remoteJid: target_cid,
735
+ id: reaction.key.id,
736
+ participant: msg.key.fromMe ? (this.#internals.socket?.user?.id ?? null) : (msg.key.participant ?? cid),
737
+ },
535
738
  reaction: { text: reaction.text ?? '' },
536
739
  }]);
537
740
  }
@@ -547,7 +750,7 @@ class WhatsApp {
547
750
  const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${protocol.key.id}`));
548
751
  if (feed_raw) {
549
752
  await this.engine.unset(`/status/${protocol.key.id}`);
550
- this._event.emit('feed:deleted', new status_1.default({ wa: this, doc: feed_raw }), this);
753
+ this.emit('feed:deleted', new status_1.Feed(this, feed_raw), this);
551
754
  }
552
755
  }
553
756
  continue;
@@ -595,7 +798,7 @@ class WhatsApp {
595
798
  if (feed_type === 'text') {
596
799
  content_buf = Buffer.from(caption, 'utf-8');
597
800
  }
598
- else if (this._socket) {
801
+ else if (this.#internals.socket) {
599
802
  try {
600
803
  const buf = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {});
601
804
  if (Buffer.isBuffer(buf)) {
@@ -610,7 +813,7 @@ class WhatsApp {
610
813
  if (content_buf.length > 0) {
611
814
  await this.engine.set(`/status/${mid}/content`, (0, store_1.serialize)({ data: content_buf.toString('base64') }));
612
815
  }
613
- this._event.emit('feed:created', new status_1.default({ wa: this, doc: feed_raw }), this);
816
+ this.emit('feed:created', new status_1.Feed(this, feed_raw), this);
614
817
  continue;
615
818
  }
616
819
  if (content_type === 'pollUpdateMessage') {
@@ -620,7 +823,7 @@ class WhatsApp {
620
823
  creation_key.remoteJid &&
621
824
  update?.vote?.encPayload &&
622
825
  update.vote.encIv) {
623
- const resolved_cid = (await this._resolve_jid(creation_key.remoteJid)) ?? creation_key.remoteJid;
826
+ const resolved_cid = (await this.#resolve_jid(creation_key.remoteJid)) ?? creation_key.remoteJid;
624
827
  const target_mid = creation_key.id;
625
828
  const poll_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${resolved_cid}/message/${target_mid}`));
626
829
  const secret_raw = poll_doc?.raw.message?.messageContextInfo?.messageSecret;
@@ -635,23 +838,15 @@ class WhatsApp {
635
838
  // Own HMAC identity depends on chat addressing (LID on @lid, PN on
636
839
  // @s.whatsapp.net), so fromMe positions try both candidates:
637
840
  // AES-GCM authenticates, a wrong key throws and the next is tried.
638
- const self_id = this._socket?.user?.id ?? '';
639
- const self_lid = this._socket?.user?.lid ?? '';
841
+ const self_id = this.#internals.socket?.user?.id ?? '';
842
+ const self_lid = this.#internals.socket?.user?.lid ?? '';
640
843
  const selves = [...new Set([self_lid, self_id].filter(Boolean))];
641
- const foreign_voter = msg.key.remoteJid?.endsWith('@lid')
642
- ? msg.key.remoteJid
643
- : msg.key.participant ||
644
- msg.key.remoteJidAlt ||
645
- msg.key.remoteJid ||
646
- '';
647
- const foreign_creator = poll_key.remoteJid?.endsWith('@lid')
648
- ? poll_key.remoteJid
649
- : poll_key.participant ||
650
- poll_key.remoteJidAlt ||
651
- poll_key.remoteJid ||
652
- '';
653
- const voters = msg.key.fromMe ? selves : [foreign_voter];
654
- const creators = poll_key.fromMe ? selves : [foreign_creator];
844
+ // Candidatos foráneos: todas las formas de identidad del key (LID,
845
+ // participant, alt, remoteJid); se prueban todas porque el addressing
846
+ // del stanza varía (LID vs PN) según la migración del contacto.
847
+ const foreign_of = (k) => [...new Set([k.remoteJid, k.participant, k.remoteJidAlt, k.remoteJid].filter((x) => Boolean(x)))];
848
+ const voters = msg.key.fromMe ? selves : foreign_of(msg.key);
849
+ const creators = poll_key.fromMe ? selves : foreign_of(poll_key);
655
850
  let decrypted = null;
656
851
  for (const voter of voters) {
657
852
  for (const creator of creators) {
@@ -678,9 +873,9 @@ class WhatsApp {
678
873
  vote: decrypted,
679
874
  senderTimestampMs: Number(msg.messageTimestamp) || Date.now(),
680
875
  });
681
- await this.engine.set(`/chat/${resolved_cid}/message/${target_mid}`, (0, store_1.serialize)(poll_doc));
682
- const msg_instance = await this.Message.build_instance(poll_doc);
683
- this._event.emit('message:updated', msg_instance, await msg_instance.chat(), this);
876
+ await this.engine.set(`/chat/${resolved_cid}/message/${target_mid}`, (0, store_1.serialize)(poll_doc), poll_doc.created_at);
877
+ const msg_instance = (0, message_1.message)(this, poll_doc);
878
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
684
879
  }
685
880
  }
686
881
  catch {
@@ -701,28 +896,49 @@ class WhatsApp {
701
896
  doc) {
702
897
  doc.raw.message = protocol.editedMessage;
703
898
  doc.edited = true;
704
- doc.caption = this.Message.build_message_doc(doc.raw, this._socket?.user?.id, true).caption;
705
- await this.engine.set(`/chat/${target_cid}/message/${target_mid}`, (0, store_1.serialize)(doc));
706
- const msg_instance = await this.Message.build_instance(doc);
707
- this._event.emit('message:updated', msg_instance, await msg_instance.chat(), this);
899
+ doc.caption = (0, message_1.message)(this, doc.raw).caption;
900
+ await this.engine.set(`/chat/${target_cid}/message/${target_mid}`, (0, store_1.serialize)(doc), doc.created_at);
901
+ const msg_instance = (0, message_1.message)(this, doc);
902
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
708
903
  }
709
904
  else if (protocol.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE) {
710
905
  await this.engine.unset(`/chat/${target_cid}/message/${target_mid}`);
711
906
  if (doc) {
712
- const msg_instance = await this.Message.build_instance(doc);
713
- this._event.emit('message:deleted', msg_instance, await msg_instance.chat(), this);
907
+ const msg_instance = (0, message_1.message)(this, doc);
908
+ this.emit('message:deleted', msg_instance, await msg_instance.chat(), this);
714
909
  }
715
910
  }
716
911
  }
717
912
  continue;
718
913
  }
719
- const doc = this.Message.build_message_doc(msg, this._socket?.user?.id);
914
+ const doc = (0, message_1.message)(this, msg)._raw;
915
+ // Cada reconexión re-entrega el historial completo: reescribir documentos
916
+ // idénticos contamina la cronología, re-descarga la media y spamea eventos,
917
+ // así que un doc ya persistido sin cambios visibles se salta entero.
918
+ // Every reconnect re-delivers the full history: rewriting identical documents
919
+ // pollutes chronology, re-downloads media and spams events, so an already
920
+ // persisted doc without visible changes is skipped entirely.
921
+ const existing_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}/message/${mid}`));
922
+ if (existing_doc) {
923
+ if (typeof existing_doc.multiple === 'boolean') {
924
+ doc.multiple = existing_doc.multiple;
925
+ }
926
+ if (existing_doc.reactions) {
927
+ doc.reactions = existing_doc.reactions;
928
+ }
929
+ if (existing_doc.status >= doc.status &&
930
+ existing_doc.caption === doc.caption &&
931
+ existing_doc.edited === doc.edited &&
932
+ existing_doc.starred === doc.starred) {
933
+ continue;
934
+ }
935
+ }
720
936
  // Autocreación de contacto/chat desde pushName cuando baileys no emite upsert previo
721
- if (!doc.me) {
937
+ if (!existing_doc && !doc.me) {
722
938
  const push_name = msg.pushName ?? null;
723
939
  const is_group = cid.endsWith('@g.us');
724
940
  if (doc.author && !(await this.engine.get(`/contact/${doc.author}`))) {
725
- await this._persist_contact({
941
+ await this.#persist_contact({
726
942
  id: doc.author,
727
943
  lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : null,
728
944
  name: null,
@@ -738,20 +954,17 @@ class WhatsApp {
738
954
  name: is_group ? null : push_name,
739
955
  };
740
956
  await this.engine.set(`/chat/${cid}`, (0, store_1.serialize)(chat_raw));
741
- this._event.emit('chat:created', new this.Chat(chat_raw), this);
957
+ this.emit('chat:created', new this.Chat(chat_raw), this);
742
958
  }
743
959
  }
744
- // Preservar flags explícitos persistidos por el send (ej: `multiple` en polls
745
- // auto-emitidos, donde el echo de WhatsApp pierde el discriminador del proto).
746
- // Preserve send-time flags (e.g. `multiple` on own polls, since WhatsApp drops
747
- // the proto discriminator on echo).
748
- const existing_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}/message/${mid}`));
749
- if (existing_doc && typeof existing_doc.multiple === 'boolean') {
750
- doc.multiple = existing_doc.multiple;
751
- }
752
- await this.engine.set(`/chat/${cid}/message/${mid}`, (0, store_1.serialize)(doc));
960
+ await this.engine.set(`/chat/${cid}/message/${mid}`, (0, store_1.serialize)(doc), doc.created_at);
961
+ // El binario solo se materializa en la primera entrega; en re-syncs ya vive en el engine.
962
+ // The binary is only materialized on first delivery; on re-syncs it already lives in the engine.
753
963
  let content_buf = Buffer.alloc(0);
754
- if (doc.type === 'text') {
964
+ if (existing_doc) {
965
+ /* ya materializado / already materialized */
966
+ }
967
+ else if (doc.type === 'text') {
755
968
  content_buf = Buffer.from(doc.caption, 'utf-8');
756
969
  }
757
970
  else if (doc.type === 'location') {
@@ -774,7 +987,7 @@ class WhatsApp {
774
987
  else if (doc.type === 'event') {
775
988
  content_buf = Buffer.from(JSON.stringify(msg.message?.eventMessage ?? {}), 'utf-8');
776
989
  }
777
- else if (this._socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
990
+ else if (this.#internals.socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
778
991
  try {
779
992
  const buffer = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {});
780
993
  if (Buffer.isBuffer(buffer)) {
@@ -788,17 +1001,17 @@ class WhatsApp {
788
1001
  if (content_buf.length > 0) {
789
1002
  await this.engine.set(`/chat/${cid}/message/${mid}/content`, (0, store_1.serialize)({ data: content_buf.toString('base64') }));
790
1003
  }
791
- const instance = await this.Message.build_instance(doc);
1004
+ const instance = (0, message_1.message)(this, doc);
792
1005
  const chat_instance = await instance.chat();
793
- this._event.emit('message:created', instance, chat_instance, this);
1006
+ this.emit('message:created', instance, chat_instance, this);
794
1007
  if (doc.forwarded) {
795
- this._event.emit('message:forwarded', instance, chat_instance, this);
1008
+ this.emit('message:forwarded', instance, chat_instance, this);
796
1009
  }
797
1010
  }
798
1011
  }
799
1012
  }
800
1013
  /** @internal */
801
- async _handle_messages_update(updates) {
1014
+ async #handle_messages_update(updates) {
802
1015
  for (const { key, update: upd } of updates) {
803
1016
  if (key.remoteJid && key.id) {
804
1017
  // Updates sobre `status@broadcast` se descartan: el feed sólo se
@@ -820,42 +1033,42 @@ class WhatsApp {
820
1033
  raw.message = edited_message;
821
1034
  doc.raw = raw;
822
1035
  doc.edited = true;
823
- doc.caption = this.Message.build_message_doc(raw, this._socket?.user?.id, true).caption;
824
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc));
825
- const msg_instance = await this.Message.build_instance(doc);
826
- this._event.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1036
+ doc.caption = (0, message_1.message)(this, raw).caption;
1037
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1038
+ const msg_instance = (0, message_1.message)(this, doc);
1039
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
827
1040
  }
828
1041
  else if (content_update) {
829
1042
  // Actualización de contenido (ej: live location). Mergea sobre el raw existente.
830
1043
  raw.message = { ...raw.message, ...content_update };
831
1044
  doc.raw = raw;
832
- doc.caption = this.Message.build_message_doc(raw, this._socket?.user?.id, doc.edited).caption;
833
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc));
834
- const msg_instance = await this.Message.build_instance(doc);
835
- this._event.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1045
+ doc.caption = (0, message_1.message)(this, raw).caption;
1046
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1047
+ const msg_instance = (0, message_1.message)(this, doc);
1048
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
836
1049
  }
837
1050
  else if (starred_changed) {
838
1051
  doc.starred = upd_any.starred === true;
839
1052
  raw.starred = doc.starred;
840
1053
  doc.raw = raw;
841
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc));
842
- const msg_instance = await this.Message.build_instance(doc);
843
- this._event.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
1054
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1055
+ const msg_instance = (0, message_1.message)(this, doc);
1056
+ this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
844
1057
  }
845
1058
  else if (status !== undefined) {
846
1059
  raw.status = status;
847
1060
  doc.status = status;
848
1061
  doc.raw = raw;
849
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc));
850
- const msg_instance = await this.Message.build_instance(doc);
851
- this._event.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1062
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1063
+ const msg_instance = (0, message_1.message)(this, doc);
1064
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
852
1065
  }
853
1066
  }
854
1067
  }
855
1068
  }
856
1069
  }
857
1070
  /** @internal */
858
- async _handle_messages_reaction(reactions) {
1071
+ async #handle_messages_reaction(reactions) {
859
1072
  for (const { key, reaction } of reactions) {
860
1073
  if (key.remoteJid && key.id) {
861
1074
  // Reacciones sobre status@broadcast → feed:updated (no message:reacted).
@@ -863,14 +1076,21 @@ class WhatsApp {
863
1076
  if (key.remoteJid === 'status@broadcast') {
864
1077
  const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${key.id}`));
865
1078
  if (feed_raw) {
866
- this._event.emit('feed:updated', new status_1.default({ wa: this, doc: feed_raw }), this);
1079
+ this.emit('feed:updated', new status_1.Feed(this, feed_raw), this);
867
1080
  }
868
1081
  continue;
869
1082
  }
870
1083
  const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
871
1084
  if (doc) {
872
- const msg_instance = await this.Message.build_instance(doc);
873
- this._event.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
1085
+ const reactor = (0, baileys_1.jidNormalizedUser)(key.participant ?? key.remoteJid);
1086
+ const emoji = reaction.text ?? '';
1087
+ doc.reactions = [
1088
+ ...(doc.reactions ?? []).filter((r) => r.author !== reactor),
1089
+ ...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
1090
+ ];
1091
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1092
+ const msg_instance = (0, message_1.message)(this, doc);
1093
+ this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
874
1094
  }
875
1095
  }
876
1096
  }