@arcaelas/whatsapp 4.5.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 (95) 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 +462 -289
  17. package/build/cjs/lib/message/index.js +825 -741
  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 +106 -68
  44. package/build/cjs/lib/whatsapp/index.js +380 -170
  45. package/build/cjs/lib/whatsapp/index.js.map +1 -1
  46. package/build/cjs/test.js +5 -4
  47. package/build/cjs/test.js.map +1 -1
  48. package/build/esm/index.d.ts +6 -4
  49. package/build/esm/index.js +5 -2
  50. package/build/esm/index.js.map +1 -1
  51. package/build/esm/lib/bot/decorator.d.ts +2 -0
  52. package/build/esm/lib/bot/decorator.js +6 -0
  53. package/build/esm/lib/bot/decorator.js.map +1 -1
  54. package/build/esm/lib/chat/index.d.ts +340 -204
  55. package/build/esm/lib/chat/index.js +225 -213
  56. package/build/esm/lib/chat/index.js.map +1 -1
  57. package/build/esm/lib/contact/index.d.ts +162 -179
  58. package/build/esm/lib/contact/index.js +96 -189
  59. package/build/esm/lib/contact/index.js.map +1 -1
  60. package/build/esm/lib/internal.d.ts +40 -0
  61. package/build/esm/lib/internal.js +35 -0
  62. package/build/esm/lib/internal.js.map +1 -0
  63. package/build/esm/lib/message/index.d.ts +462 -289
  64. package/build/esm/lib/message/index.js +820 -736
  65. package/build/esm/lib/message/index.js.map +1 -1
  66. package/build/esm/lib/status/index.d.ts +60 -70
  67. package/build/esm/lib/status/index.js +105 -110
  68. package/build/esm/lib/status/index.js.map +1 -1
  69. package/build/esm/lib/store/engine/index.d.ts +21 -7
  70. package/build/esm/lib/store/engine/index.js +4 -2
  71. package/build/esm/lib/store/engine/index.js.map +1 -1
  72. package/build/esm/lib/store/engine/lib/file_system/index.d.ts +38 -8
  73. package/build/esm/lib/store/engine/lib/file_system/index.js +92 -67
  74. package/build/esm/lib/store/engine/lib/file_system/index.js.map +1 -1
  75. package/build/esm/lib/store/engine/lib/index.d.ts +112 -0
  76. package/build/esm/lib/store/engine/lib/index.js +206 -0
  77. package/build/esm/lib/store/engine/lib/index.js.map +1 -0
  78. package/build/esm/lib/store/engine/lib/redis/index.d.ts +28 -13
  79. package/build/esm/lib/store/engine/lib/redis/index.js +54 -40
  80. package/build/esm/lib/store/engine/lib/redis/index.js.map +1 -1
  81. package/build/esm/lib/store/engine/lib/s3/index.d.ts +44 -1
  82. package/build/esm/lib/store/engine/lib/s3/index.js +111 -63
  83. package/build/esm/lib/store/engine/lib/s3/index.js.map +1 -1
  84. package/build/esm/lib/store/engine/lib/sqlite/index.d.ts +98 -0
  85. package/build/esm/lib/store/engine/lib/sqlite/index.js +110 -0
  86. package/build/esm/lib/store/engine/lib/sqlite/index.js.map +1 -0
  87. package/build/esm/lib/store/index.d.ts +7 -3
  88. package/build/esm/lib/store/index.js +13 -4
  89. package/build/esm/lib/store/index.js.map +1 -1
  90. package/build/esm/lib/whatsapp/index.d.ts +106 -68
  91. package/build/esm/lib/whatsapp/index.js +382 -172
  92. package/build/esm/lib/whatsapp/index.js.map +1 -1
  93. package/build/esm/test.js +5 -4
  94. package/build/esm/test.js.map +1 -1
  95. 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,55 +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._method = options.method;
75
- this._autoclean = options.autoclean ?? true;
76
- this._sync = options.sync ?? true;
77
- this._reconnect = this._parse_reconnect(options.reconnect);
78
- 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);
79
121
  this.Contact = (0, contact_1.contact)(this);
80
122
  this.Chat = (0, chat_1.chat)(this);
81
- 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
+ };
82
144
  }
83
- /** @internal */
84
- _parse_reconnect(option) {
85
- if (option === false) {
86
- return { max: 0, interval_ms: 60_000 };
87
- }
88
- if (option === undefined || option === true) {
89
- return { max: null, interval_ms: 60_000 };
90
- }
91
- if (typeof option === 'number') {
92
- 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 });
93
154
  }
94
- return {
95
- max: option.max ?? null,
96
- interval_ms: (option.interval ?? 60) * 1000,
97
- };
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);
98
173
  }
99
174
  /**
100
175
  * Registra un listener de evento. Retorna función para desuscribirse.
101
176
  * Registers an event listener. Returns an unsubscribe function.
102
177
  */
103
178
  on(event, handler) {
104
- this._event.on(event, handler);
105
- return () => { this._event.off(event, handler); };
179
+ this.#event.on(event, handler);
180
+ return () => { this.#event.off(event, handler); };
106
181
  }
107
182
  /**
108
183
  * Quita un listener previamente registrado.
109
184
  * Removes a previously registered listener.
110
185
  */
111
186
  off(event, handler) {
112
- this._event.off(event, handler);
187
+ this.#event.off(event, handler);
113
188
  return this;
114
189
  }
115
190
  /**
@@ -117,15 +192,17 @@ class WhatsApp {
117
192
  * Registers a one-shot listener. Returns an unsubscribe function.
118
193
  */
119
194
  once(event, handler) {
120
- this._event.once(event, handler);
121
- return () => { this._event.off(event, handler); };
195
+ this.#event.once(event, handler);
196
+ return () => { this.#event.off(event, handler); };
122
197
  }
123
198
  /**
124
199
  * @internal
125
- * Normaliza cualquier identificador (JID, LID, número, etc.) a JID canónico.
126
- * 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.
127
204
  */
128
- async _resolve_jid(uid) {
205
+ async #resolve_jid(uid) {
129
206
  let result = null;
130
207
  if (uid.endsWith('@g.us') || uid.endsWith('@s.whatsapp.net')) {
131
208
  result = uid;
@@ -145,7 +222,7 @@ class WhatsApp {
145
222
  // baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
146
223
  // (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
147
224
  // realmente está guardado, y el mensaje/poll no se encuentra.
148
- const pn = await this._socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
225
+ const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
149
226
  if (pn) {
150
227
  // getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
151
228
  // que el JID coincida con el que usa el store (sin device).
@@ -170,16 +247,20 @@ class WhatsApp {
170
247
  * Resolves once the session is synced; retries on non-loggedOut disconnects.
171
248
  */
172
249
  async connect(callback) {
250
+ if (this.#internals.socket) {
251
+ await this.disconnect({ silent: true });
252
+ }
173
253
  const { version } = await (0, baileys_1.fetchLatestBaileysVersion)();
174
- this._intentional_close = false;
175
- this._has_connected = false;
254
+ this.#intentional_close = false;
255
+ this.#silent_close = false;
256
+ this.#has_connected = false;
176
257
  return new Promise((resolve, reject) => {
177
258
  const start = async () => {
178
259
  // Re-lee creds en cada start() para que limpiezas del engine tomen efecto
179
260
  // en reintentos (permite al consumer forzar nueva sesión borrando /session/creds).
180
261
  const stored = await this.engine.get('/session/creds');
181
262
  const creds = (0, store_1.deserialize)(stored) ?? (0, baileys_1.initAuthCreds)();
182
- this._socket = (0, baileys_1.makeWASocket)({
263
+ this.#internals.socket = (0, baileys_1.makeWASocket)({
183
264
  version,
184
265
  auth: {
185
266
  creds,
@@ -206,19 +287,19 @@ class WhatsApp {
206
287
  },
207
288
  browser: baileys_1.Browsers.windows('Chrome'),
208
289
  logger: (0, pino_1.default)({ level: 'silent' }),
209
- syncFullHistory: this._sync,
290
+ syncFullHistory: this.#sync,
210
291
  // Los syncs no-FULL cargan las LID mappings y los tctokens (trusted-contact
211
- // tokens) que rc13 exige para enviar: apagarlos todos con `() => this._sync`
292
+ // tokens) que rc13 exige para enviar: apagarlos todos con `() => this.#sync`
212
293
  // deja la sesión sin tctoken y el server rechaza los mensajes con
213
294
  // "error 463: account restricted or missing tctoken". Por eso se procesan
214
295
  // siempre los no-FULL; `sync` solo decide si además se trae el historial FULL.
215
296
  // Non-FULL syncs carry the LID mappings and tctokens rc13 requires to send;
216
297
  // disabling them all made the server reject messages with error 463. We always
217
298
  // process non-FULL; `sync` only gates whether FULL history is pulled too.
218
- shouldSyncHistoryMessage: ({ syncType }) => this._sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
299
+ shouldSyncHistoryMessage: ({ syncType }) => this.#sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
219
300
  markOnlineOnConnect: false,
220
301
  });
221
- const socket = this._socket;
302
+ const socket = this.#internals.socket;
222
303
  socket.ev.on('creds.update', () => this.engine.set('/session/creds', (0, store_1.serialize)(creds)));
223
304
  socket.ev.on('connection.update', async (update) => {
224
305
  const { connection, lastDisconnect, qr } = update;
@@ -226,24 +307,25 @@ class WhatsApp {
226
307
  // Baileys refresca QR periódicamente (~20s). Emitimos nuevo pair
227
308
  // code / QR en cada refresh para que el usuario pueda renovar si
228
309
  // el anterior expiró.
229
- // `method` fuerza el canal; sin él, phone OTP y su ausencia ⇒ QR.
230
- // `otp` exige phone: si se pidió sin phone, se cae a QR.
231
- const auth = this._method ?? (this._phone ? 'otp' : 'qr');
232
- if (auth === 'otp' && this._phone) {
233
- 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));
234
316
  }
235
317
  else {
236
318
  await callback(await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
237
319
  }
238
320
  }
239
321
  if (connection === 'open') {
240
- this._has_connected = true;
241
- this._retry_count = 0;
242
- this._event.emit('connected', this);
322
+ this.#has_connected = true;
323
+ this.#retry_count = 0;
324
+ this.emit('connected', this);
243
325
  resolve();
244
326
  }
245
327
  else if (connection === 'close') {
246
- this._socket = null;
328
+ this.#internals.socket = null;
247
329
  const status_code = lastDisconnect?.error?.output?.statusCode;
248
330
  // `restartRequired` (515) es una reconexión exigida por el protocolo
249
331
  // tras el sync inicial — no es un disconnect "real".
@@ -251,35 +333,37 @@ class WhatsApp {
251
333
  // Limpieza del engine ANTES de emitir `disconnected` para que los
252
334
  // listeners vean el estado final (engine vaciado o creds borradas).
253
335
  if (status_code === baileys_1.DisconnectReason.loggedOut) {
254
- if (this._autoclean) {
336
+ if (this.#autoclean) {
255
337
  await this.engine.clear();
256
338
  }
257
339
  else {
258
340
  await this.engine.unset('/session/creds');
259
341
  }
260
342
  }
261
- if (this._has_connected && !is_transient) {
262
- 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);
263
347
  }
264
- if (!this._intentional_close) {
348
+ if (!this.#intentional_close) {
265
349
  if (status_code === baileys_1.DisconnectReason.loggedOut) {
266
350
  reject(new Error('Logged out'));
267
351
  }
268
352
  else {
269
- const max = this._reconnect.max;
353
+ const max = this.#reconnect.max;
270
354
  // Transient closes (restartRequired) son parte del protocolo,
271
355
  // no cuentan contra el límite de reintentos por fallo.
272
- const exhausted = !is_transient && max !== null && this._retry_count >= max;
356
+ const exhausted = !is_transient && max !== null && this.#retry_count >= max;
273
357
  if (exhausted) {
274
358
  reject(new Error(`Reconnect attempts exhausted (${max})`));
275
359
  }
276
360
  else {
277
361
  if (!is_transient) {
278
- this._retry_count++;
362
+ this.#retry_count++;
279
363
  }
280
- const delay = is_transient ? 0 : this._reconnect.interval_ms;
281
- this._retry_timer = setTimeout(() => {
282
- this._retry_timer = null;
364
+ const delay = is_transient ? 0 : this.#reconnect.interval_ms;
365
+ this.#retry_timer = setTimeout(() => {
366
+ this.#retry_timer = null;
283
367
  start().catch(reject);
284
368
  }, delay);
285
369
  }
@@ -287,24 +371,119 @@ class WhatsApp {
287
371
  }
288
372
  }
289
373
  });
290
- this._attach_business_handlers(socket);
374
+ this.#attach_business_handlers(socket);
291
375
  };
292
376
  start().catch(reject);
293
377
  });
294
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
+ }
295
473
  /**
296
474
  * Cierra la conexión. Con `destroy: true` vacía el engine completo.
297
475
  * Closes the connection. With `destroy: true` clears the engine entirely.
298
476
  */
299
477
  async disconnect(options = {}) {
300
- this._intentional_close = true;
478
+ this.#intentional_close = true;
479
+ this.#silent_close = options.silent === true;
301
480
  // Cancela cualquier retry programado por un close anterior, para no resucitar
302
481
  // el socket después de una desconexión manual.
303
- if (this._retry_timer) {
304
- clearTimeout(this._retry_timer);
305
- this._retry_timer = null;
482
+ if (this.#retry_timer) {
483
+ clearTimeout(this.#retry_timer);
484
+ this.#retry_timer = null;
306
485
  }
307
- if (this._socket) {
486
+ if (this.#internals.socket) {
308
487
  try {
309
488
  // Pasa un error Boom-like con statusCode=connectionClosed (428) para que
310
489
  // `lastDisconnect.error.output.statusCode` quede explícito en el close
@@ -312,17 +491,16 @@ class WhatsApp {
312
491
  const intentional = Object.assign(new Error('intentional close'), {
313
492
  output: { statusCode: baileys_1.DisconnectReason.connectionClosed },
314
493
  });
315
- await this._socket.end(intentional);
494
+ await this.#internals.socket.end(intentional);
316
495
  }
317
496
  catch {
318
497
  /* socket may already be closed */
319
498
  }
320
- this._socket = null;
499
+ this.#internals.socket = null;
321
500
  }
322
501
  if (options.destroy) {
323
502
  await this.engine.clear();
324
503
  }
325
- void options.silent;
326
504
  }
327
505
  /**
328
506
  * Conecta los handlers de eventos de baileys: contactos, chats y mensajes.
@@ -330,43 +508,45 @@ class WhatsApp {
330
508
  *
331
509
  * @internal
332
510
  */
333
- _attach_business_handlers(socket) {
334
- socket.ev.on('messaging-history.set', async ({ chats, contacts, messages }) => {
335
- await this._handle_contacts_upsert(contacts);
336
- await this._handle_chats_upsert(chats);
337
- 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
+ });
338
518
  });
339
519
  socket.ev.on('contacts.upsert', (contacts) => {
340
- void this._handle_contacts_upsert(contacts);
520
+ this.#enqueue(() => this.#handle_contacts_upsert(contacts));
341
521
  });
342
522
  socket.ev.on('contacts.update', (contacts) => {
343
- void this._handle_contacts_update(contacts);
523
+ this.#enqueue(() => this.#handle_contacts_update(contacts));
344
524
  });
345
525
  socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
346
- void this._handle_lid_mapping(lid, pn);
526
+ this.#enqueue(() => this.#handle_lid_mapping(lid, pn));
347
527
  });
348
528
  socket.ev.on('chats.upsert', (chats) => {
349
- void this._handle_chats_upsert(chats);
529
+ this.#enqueue(() => this.#handle_chats_upsert(chats));
350
530
  });
351
531
  socket.ev.on('chats.update', (chats) => {
352
- void this._handle_chats_update(chats);
532
+ this.#enqueue(() => this.#handle_chats_update(chats));
353
533
  });
354
534
  socket.ev.on('chats.delete', (ids) => {
355
- void this._handle_chats_delete(ids);
535
+ this.#enqueue(() => this.#handle_chats_delete(ids));
356
536
  });
357
537
  socket.ev.on('messages.upsert', ({ messages }) => {
358
- void this._handle_messages_upsert(messages);
538
+ this.#enqueue(() => this.#handle_messages_upsert(messages));
359
539
  });
360
540
  socket.ev.on('messages.update', (updates) => {
361
- void this._handle_messages_update(updates);
541
+ this.#enqueue(() => this.#handle_messages_update(updates));
362
542
  });
363
543
  socket.ev.on('message-receipt.update', (updates) => {
364
- void this._handle_message_receipt(updates);
544
+ this.#enqueue(() => this.#handle_message_receipt(updates));
365
545
  });
366
546
  // Las reacciones llegan duplicadas por `messages.reaction` Y `messages.upsert`
367
547
  // (como `reactionMessage`). Se usa solo `messages.upsert` para evitar el doble disparo.
368
548
  // socket.ev.on('messages.reaction', (reactions) => {
369
- // void this._handle_messages_reaction(reactions);
549
+ // void this.#handle_messages_reaction(reactions);
370
550
  // });
371
551
  }
372
552
  /**
@@ -376,23 +556,23 @@ class WhatsApp {
376
556
  * @param raw - Documento del contacto a persistir / Contact document to persist
377
557
  * @internal
378
558
  */
379
- async _persist_contact(raw) {
559
+ async #persist_contact(raw) {
380
560
  const existing = await this.engine.get(`/contact/${raw.id}`);
381
561
  await this.engine.set(`/contact/${raw.id}`, (0, store_1.serialize)(raw));
382
562
  if (raw.lid) {
383
563
  await this.engine.set(`/lid/${raw.lid}`, (0, store_1.serialize)(raw.id));
384
564
  }
385
565
  if (existing === null) {
566
+ const person = new this.Contact(raw);
386
567
  const cached_chat = (0, store_1.deserialize)(await this.engine.get(`/chat/${raw.id}`));
387
- const chat_instance = new this.Chat(cached_chat ?? { id: raw.id, name: (0, contact_1.contact_name)(raw) });
388
- 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);
389
569
  }
390
570
  }
391
571
  /** @internal */
392
- async _handle_contacts_upsert(contacts) {
572
+ async #handle_contacts_upsert(contacts) {
393
573
  for (const c of contacts) {
394
574
  if (c.id) {
395
- await this._persist_contact({
575
+ await this.#persist_contact({
396
576
  id: c.id,
397
577
  lid: c.lid ?? null,
398
578
  name: c.name ?? null,
@@ -405,7 +585,7 @@ class WhatsApp {
405
585
  }
406
586
  }
407
587
  /** @internal */
408
- async _handle_contacts_update(contacts) {
588
+ async #handle_contacts_update(contacts) {
409
589
  for (const c of contacts) {
410
590
  if (c.id) {
411
591
  const current = (0, store_1.deserialize)(await this.engine.get(`/contact/${c.id}`));
@@ -423,21 +603,21 @@ class WhatsApp {
423
603
  if (patch.lid) {
424
604
  await this.engine.set(`/lid/${patch.lid}`, (0, store_1.serialize)(c.id));
425
605
  }
606
+ const person = new this.Contact(merged);
426
607
  const cached_chat = (0, store_1.deserialize)(await this.engine.get(`/chat/${c.id}`));
427
- const chat_instance = new this.Chat(cached_chat ?? { id: c.id, name: (0, contact_1.contact_name)(merged) });
428
- 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);
429
609
  }
430
610
  }
431
611
  }
432
612
  }
433
613
  }
434
614
  /** @internal */
435
- async _handle_lid_mapping(lid, pn) {
615
+ async #handle_lid_mapping(lid, pn) {
436
616
  await this.engine.set(`/lid/${lid}`, (0, store_1.serialize)(pn));
437
617
  await this.engine.set(`/lid/${pn}`, (0, store_1.serialize)(lid));
438
618
  }
439
619
  /** @internal */
440
- async _handle_chats_upsert(chats) {
620
+ async #handle_chats_upsert(chats) {
441
621
  for (const ch of chats) {
442
622
  if (ch.id) {
443
623
  const current = (0, store_1.deserialize)(await this.engine.get(`/chat/${ch.id}`));
@@ -447,21 +627,19 @@ class WhatsApp {
447
627
  archived: ch.archived ?? null,
448
628
  pinned: ch.pinned ?? null,
449
629
  mute_end_time: ch.muteEndTime != null ? Number(ch.muteEndTime) : null,
450
- unread_count: ch.unreadCount ?? null,
451
- read_only: ch.readOnly ?? null,
452
630
  };
453
631
  if (ch.name) {
454
632
  raw.name = ch.name;
455
633
  }
456
634
  await this.engine.set(`/chat/${ch.id}`, (0, store_1.serialize)(raw));
457
635
  if (current === null) {
458
- this._event.emit('chat:created', new this.Chat(raw), this);
636
+ this.emit('chat:created', new this.Chat(raw), this);
459
637
  }
460
638
  }
461
639
  }
462
640
  }
463
641
  /** @internal */
464
- async _handle_chats_update(chats) {
642
+ async #handle_chats_update(chats) {
465
643
  for (const ch of chats) {
466
644
  if (ch.id && ch.id !== 'status@broadcast') {
467
645
  const current = (0, store_1.deserialize)(await this.engine.get(`/chat/${ch.id}`)) ?? {
@@ -484,36 +662,33 @@ class WhatsApp {
484
662
  if (mute_changed) {
485
663
  patch.mute_end_time = ch.muteEndTime != null ? Number(ch.muteEndTime) : null;
486
664
  }
487
- if (ch.unreadCount != null) {
488
- patch.unread_count = ch.unreadCount;
489
- }
490
665
  if (Object.keys(patch).length > 0) {
491
666
  const merged = { ...current, ...patch };
492
667
  await this.engine.set(`/chat/${ch.id}`, (0, store_1.serialize)(merged));
493
668
  if (pinned_changed) {
494
- 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);
495
670
  }
496
671
  if (archived_changed) {
497
- 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);
498
673
  }
499
674
  if (mute_changed) {
500
675
  const is_muted = patch.mute_end_time != null && patch.mute_end_time > Date.now();
501
- 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);
502
677
  }
503
678
  }
504
679
  }
505
680
  }
506
681
  }
507
682
  /** @internal */
508
- async _handle_chats_delete(ids) {
683
+ async #handle_chats_delete(ids) {
509
684
  for (const cid of ids) {
510
685
  const raw = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}`)) ?? { id: cid };
511
686
  await this.engine.unset(`/chat/${cid}`);
512
- this._event.emit('chat:deleted', new this.Chat(raw), this);
687
+ this.emit('chat:deleted', new this.Chat(raw), this);
513
688
  }
514
689
  }
515
690
  /** @internal */
516
- async _handle_message_receipt(updates) {
691
+ async #handle_message_receipt(updates) {
517
692
  for (const { key, receipt } of updates) {
518
693
  // Receipt sobre status@broadcast → marca el feed como visto y emite feed:updated.
519
694
  // Receipt on status@broadcast → marks feed viewed and emits feed:updated.
@@ -522,21 +697,27 @@ class WhatsApp {
522
697
  if (feed_raw && !feed_raw.viewed) {
523
698
  feed_raw.viewed = true;
524
699
  await this.engine.set(`/status/${key.id}`, (0, store_1.serialize)(feed_raw));
525
- 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);
526
701
  }
527
702
  continue;
528
703
  }
529
704
  if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
530
705
  const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
531
706
  if (doc) {
532
- const msg_instance = await this.Message.build_instance(doc);
533
- 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);
534
715
  }
535
716
  }
536
717
  }
537
718
  }
538
719
  /** @internal */
539
- async _handle_messages_upsert(messages) {
720
+ async #handle_messages_upsert(messages) {
540
721
  for (const msg of messages) {
541
722
  if (msg.key?.remoteJid && msg.key.id) {
542
723
  const cid = msg.key.remoteJidAlt ?? msg.key.remoteJid;
@@ -547,9 +728,13 @@ class WhatsApp {
547
728
  // Single channel for reactions: handled here; `messages.reaction` is disabled.
548
729
  const reaction = msg.message?.reactionMessage;
549
730
  if (reaction?.key?.id && reaction.key.remoteJid) {
550
- const target_cid = (await this._resolve_jid(reaction.key.remoteJid)) ?? reaction.key.remoteJid;
551
- await this._handle_messages_reaction([{
552
- 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
+ },
553
738
  reaction: { text: reaction.text ?? '' },
554
739
  }]);
555
740
  }
@@ -565,7 +750,7 @@ class WhatsApp {
565
750
  const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${protocol.key.id}`));
566
751
  if (feed_raw) {
567
752
  await this.engine.unset(`/status/${protocol.key.id}`);
568
- 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);
569
754
  }
570
755
  }
571
756
  continue;
@@ -613,7 +798,7 @@ class WhatsApp {
613
798
  if (feed_type === 'text') {
614
799
  content_buf = Buffer.from(caption, 'utf-8');
615
800
  }
616
- else if (this._socket) {
801
+ else if (this.#internals.socket) {
617
802
  try {
618
803
  const buf = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {});
619
804
  if (Buffer.isBuffer(buf)) {
@@ -628,7 +813,7 @@ class WhatsApp {
628
813
  if (content_buf.length > 0) {
629
814
  await this.engine.set(`/status/${mid}/content`, (0, store_1.serialize)({ data: content_buf.toString('base64') }));
630
815
  }
631
- 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);
632
817
  continue;
633
818
  }
634
819
  if (content_type === 'pollUpdateMessage') {
@@ -638,7 +823,7 @@ class WhatsApp {
638
823
  creation_key.remoteJid &&
639
824
  update?.vote?.encPayload &&
640
825
  update.vote.encIv) {
641
- 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;
642
827
  const target_mid = creation_key.id;
643
828
  const poll_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${resolved_cid}/message/${target_mid}`));
644
829
  const secret_raw = poll_doc?.raw.message?.messageContextInfo?.messageSecret;
@@ -653,8 +838,8 @@ class WhatsApp {
653
838
  // Own HMAC identity depends on chat addressing (LID on @lid, PN on
654
839
  // @s.whatsapp.net), so fromMe positions try both candidates:
655
840
  // AES-GCM authenticates, a wrong key throws and the next is tried.
656
- const self_id = this._socket?.user?.id ?? '';
657
- 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 ?? '';
658
843
  const selves = [...new Set([self_lid, self_id].filter(Boolean))];
659
844
  // Candidatos foráneos: todas las formas de identidad del key (LID,
660
845
  // participant, alt, remoteJid); se prueban todas porque el addressing
@@ -688,9 +873,9 @@ class WhatsApp {
688
873
  vote: decrypted,
689
874
  senderTimestampMs: Number(msg.messageTimestamp) || Date.now(),
690
875
  });
691
- await this.engine.set(`/chat/${resolved_cid}/message/${target_mid}`, (0, store_1.serialize)(poll_doc));
692
- const msg_instance = await this.Message.build_instance(poll_doc);
693
- 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);
694
879
  }
695
880
  }
696
881
  catch {
@@ -711,28 +896,49 @@ class WhatsApp {
711
896
  doc) {
712
897
  doc.raw.message = protocol.editedMessage;
713
898
  doc.edited = true;
714
- doc.caption = this.Message.build_message_doc(doc.raw, this._socket?.user?.id, true).caption;
715
- await this.engine.set(`/chat/${target_cid}/message/${target_mid}`, (0, store_1.serialize)(doc));
716
- const msg_instance = await this.Message.build_instance(doc);
717
- 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);
718
903
  }
719
904
  else if (protocol.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE) {
720
905
  await this.engine.unset(`/chat/${target_cid}/message/${target_mid}`);
721
906
  if (doc) {
722
- const msg_instance = await this.Message.build_instance(doc);
723
- 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);
724
909
  }
725
910
  }
726
911
  }
727
912
  continue;
728
913
  }
729
- 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
+ }
730
936
  // Autocreación de contacto/chat desde pushName cuando baileys no emite upsert previo
731
- if (!doc.me) {
937
+ if (!existing_doc && !doc.me) {
732
938
  const push_name = msg.pushName ?? null;
733
939
  const is_group = cid.endsWith('@g.us');
734
940
  if (doc.author && !(await this.engine.get(`/contact/${doc.author}`))) {
735
- await this._persist_contact({
941
+ await this.#persist_contact({
736
942
  id: doc.author,
737
943
  lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : null,
738
944
  name: null,
@@ -748,20 +954,17 @@ class WhatsApp {
748
954
  name: is_group ? null : push_name,
749
955
  };
750
956
  await this.engine.set(`/chat/${cid}`, (0, store_1.serialize)(chat_raw));
751
- this._event.emit('chat:created', new this.Chat(chat_raw), this);
957
+ this.emit('chat:created', new this.Chat(chat_raw), this);
752
958
  }
753
959
  }
754
- // Preservar flags explícitos persistidos por el send (ej: `multiple` en polls
755
- // auto-emitidos, donde el echo de WhatsApp pierde el discriminador del proto).
756
- // Preserve send-time flags (e.g. `multiple` on own polls, since WhatsApp drops
757
- // the proto discriminator on echo).
758
- const existing_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}/message/${mid}`));
759
- if (existing_doc && typeof existing_doc.multiple === 'boolean') {
760
- doc.multiple = existing_doc.multiple;
761
- }
762
- 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.
763
963
  let content_buf = Buffer.alloc(0);
764
- if (doc.type === 'text') {
964
+ if (existing_doc) {
965
+ /* ya materializado / already materialized */
966
+ }
967
+ else if (doc.type === 'text') {
765
968
  content_buf = Buffer.from(doc.caption, 'utf-8');
766
969
  }
767
970
  else if (doc.type === 'location') {
@@ -784,7 +987,7 @@ class WhatsApp {
784
987
  else if (doc.type === 'event') {
785
988
  content_buf = Buffer.from(JSON.stringify(msg.message?.eventMessage ?? {}), 'utf-8');
786
989
  }
787
- else if (this._socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
990
+ else if (this.#internals.socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
788
991
  try {
789
992
  const buffer = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {});
790
993
  if (Buffer.isBuffer(buffer)) {
@@ -798,17 +1001,17 @@ class WhatsApp {
798
1001
  if (content_buf.length > 0) {
799
1002
  await this.engine.set(`/chat/${cid}/message/${mid}/content`, (0, store_1.serialize)({ data: content_buf.toString('base64') }));
800
1003
  }
801
- const instance = await this.Message.build_instance(doc);
1004
+ const instance = (0, message_1.message)(this, doc);
802
1005
  const chat_instance = await instance.chat();
803
- this._event.emit('message:created', instance, chat_instance, this);
1006
+ this.emit('message:created', instance, chat_instance, this);
804
1007
  if (doc.forwarded) {
805
- this._event.emit('message:forwarded', instance, chat_instance, this);
1008
+ this.emit('message:forwarded', instance, chat_instance, this);
806
1009
  }
807
1010
  }
808
1011
  }
809
1012
  }
810
1013
  /** @internal */
811
- async _handle_messages_update(updates) {
1014
+ async #handle_messages_update(updates) {
812
1015
  for (const { key, update: upd } of updates) {
813
1016
  if (key.remoteJid && key.id) {
814
1017
  // Updates sobre `status@broadcast` se descartan: el feed sólo se
@@ -830,42 +1033,42 @@ class WhatsApp {
830
1033
  raw.message = edited_message;
831
1034
  doc.raw = raw;
832
1035
  doc.edited = true;
833
- doc.caption = this.Message.build_message_doc(raw, this._socket?.user?.id, true).caption;
834
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc));
835
- const msg_instance = await this.Message.build_instance(doc);
836
- 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);
837
1040
  }
838
1041
  else if (content_update) {
839
1042
  // Actualización de contenido (ej: live location). Mergea sobre el raw existente.
840
1043
  raw.message = { ...raw.message, ...content_update };
841
1044
  doc.raw = raw;
842
- doc.caption = this.Message.build_message_doc(raw, this._socket?.user?.id, doc.edited).caption;
843
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc));
844
- const msg_instance = await this.Message.build_instance(doc);
845
- 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);
846
1049
  }
847
1050
  else if (starred_changed) {
848
1051
  doc.starred = upd_any.starred === true;
849
1052
  raw.starred = doc.starred;
850
1053
  doc.raw = raw;
851
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc));
852
- const msg_instance = await this.Message.build_instance(doc);
853
- 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);
854
1057
  }
855
1058
  else if (status !== undefined) {
856
1059
  raw.status = status;
857
1060
  doc.status = status;
858
1061
  doc.raw = raw;
859
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc));
860
- const msg_instance = await this.Message.build_instance(doc);
861
- 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);
862
1065
  }
863
1066
  }
864
1067
  }
865
1068
  }
866
1069
  }
867
1070
  /** @internal */
868
- async _handle_messages_reaction(reactions) {
1071
+ async #handle_messages_reaction(reactions) {
869
1072
  for (const { key, reaction } of reactions) {
870
1073
  if (key.remoteJid && key.id) {
871
1074
  // Reacciones sobre status@broadcast → feed:updated (no message:reacted).
@@ -873,14 +1076,21 @@ class WhatsApp {
873
1076
  if (key.remoteJid === 'status@broadcast') {
874
1077
  const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${key.id}`));
875
1078
  if (feed_raw) {
876
- 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);
877
1080
  }
878
1081
  continue;
879
1082
  }
880
1083
  const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
881
1084
  if (doc) {
882
- const msg_instance = await this.Message.build_instance(doc);
883
- 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);
884
1094
  }
885
1095
  }
886
1096
  }