@arcaelas/whatsapp 4.5.0 → 6.0.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 (106) hide show
  1. package/README.md +282 -239
  2. package/build/cjs/decorators.js +0 -1
  3. package/build/cjs/index.d.ts +6 -4
  4. package/build/cjs/index.js +22 -3
  5. package/build/cjs/lib/bot/decorator.d.ts +2 -0
  6. package/build/cjs/lib/bot/decorator.js +6 -1
  7. package/build/cjs/lib/bot/decorators.d.ts +2 -2
  8. package/build/cjs/lib/bot/decorators.js +4 -4
  9. package/build/cjs/lib/bot/index.js +0 -1
  10. package/build/cjs/lib/chat/index.d.ts +381 -204
  11. package/build/cjs/lib/chat/index.js +243 -212
  12. package/build/cjs/lib/contact/index.d.ts +162 -179
  13. package/build/cjs/lib/contact/index.js +96 -191
  14. package/build/cjs/lib/internal.d.ts +40 -0
  15. package/build/cjs/lib/internal.js +38 -0
  16. package/build/cjs/lib/message/index.d.ts +468 -289
  17. package/build/cjs/lib/message/index.js +871 -743
  18. package/build/cjs/lib/status/index.d.ts +60 -70
  19. package/build/cjs/lib/status/index.js +106 -112
  20. package/build/cjs/lib/store/engine/index.d.ts +35 -7
  21. package/build/cjs/lib/store/engine/index.js +6 -4
  22. package/build/cjs/lib/store/engine/lib/file_system/index.d.ts +48 -8
  23. package/build/cjs/lib/store/engine/lib/file_system/index.js +117 -66
  24. package/build/cjs/lib/store/engine/lib/index.d.ts +112 -0
  25. package/build/cjs/lib/store/engine/lib/index.js +212 -0
  26. package/build/cjs/lib/store/engine/lib/redis/index.d.ts +44 -15
  27. package/build/cjs/lib/store/engine/lib/redis/index.js +75 -43
  28. package/build/cjs/lib/store/engine/lib/s3/index.d.ts +44 -1
  29. package/build/cjs/lib/store/engine/lib/s3/index.js +112 -65
  30. package/build/cjs/lib/store/engine/lib/sqlite/index.d.ts +110 -0
  31. package/build/cjs/lib/store/engine/lib/sqlite/index.js +132 -0
  32. package/build/cjs/lib/store/index.d.ts +7 -3
  33. package/build/cjs/lib/store/index.js +14 -5
  34. package/build/cjs/lib/whatsapp/index.d.ts +106 -68
  35. package/build/cjs/lib/whatsapp/index.js +393 -167
  36. package/build/esm/decorators.js +0 -1
  37. package/build/esm/index.d.ts +6 -4
  38. package/build/esm/index.js +5 -3
  39. package/build/esm/lib/bot/decorator.d.ts +2 -0
  40. package/build/esm/lib/bot/decorator.js +6 -1
  41. package/build/esm/lib/bot/decorators.d.ts +2 -2
  42. package/build/esm/lib/bot/decorators.js +4 -4
  43. package/build/esm/lib/bot/index.js +0 -1
  44. package/build/esm/lib/chat/index.d.ts +381 -204
  45. package/build/esm/lib/chat/index.js +243 -212
  46. package/build/esm/lib/contact/index.d.ts +162 -179
  47. package/build/esm/lib/contact/index.js +96 -190
  48. package/build/esm/lib/internal.d.ts +40 -0
  49. package/build/esm/lib/internal.js +34 -0
  50. package/build/esm/lib/message/index.d.ts +468 -289
  51. package/build/esm/lib/message/index.js +866 -738
  52. package/build/esm/lib/status/index.d.ts +60 -70
  53. package/build/esm/lib/status/index.js +105 -111
  54. package/build/esm/lib/store/engine/index.d.ts +35 -7
  55. package/build/esm/lib/store/engine/index.js +4 -3
  56. package/build/esm/lib/store/engine/lib/file_system/index.d.ts +48 -8
  57. package/build/esm/lib/store/engine/lib/file_system/index.js +117 -66
  58. package/build/esm/lib/store/engine/lib/index.d.ts +112 -0
  59. package/build/esm/lib/store/engine/lib/index.js +205 -0
  60. package/build/esm/lib/store/engine/lib/redis/index.d.ts +44 -15
  61. package/build/esm/lib/store/engine/lib/redis/index.js +73 -41
  62. package/build/esm/lib/store/engine/lib/s3/index.d.ts +44 -1
  63. package/build/esm/lib/store/engine/lib/s3/index.js +111 -64
  64. package/build/esm/lib/store/engine/lib/sqlite/index.d.ts +110 -0
  65. package/build/esm/lib/store/engine/lib/sqlite/index.js +128 -0
  66. package/build/esm/lib/store/index.d.ts +7 -3
  67. package/build/esm/lib/store/index.js +13 -5
  68. package/build/esm/lib/whatsapp/index.d.ts +106 -68
  69. package/build/esm/lib/whatsapp/index.js +395 -169
  70. package/package.json +25 -6
  71. package/build/cjs/decorators.js.map +0 -1
  72. package/build/cjs/index.js.map +0 -1
  73. package/build/cjs/lib/bot/decorator.js.map +0 -1
  74. package/build/cjs/lib/bot/decorators.js.map +0 -1
  75. package/build/cjs/lib/bot/index.js.map +0 -1
  76. package/build/cjs/lib/chat/index.js.map +0 -1
  77. package/build/cjs/lib/contact/index.js.map +0 -1
  78. package/build/cjs/lib/message/index.js.map +0 -1
  79. package/build/cjs/lib/status/index.js.map +0 -1
  80. package/build/cjs/lib/store/engine/index.js.map +0 -1
  81. package/build/cjs/lib/store/engine/lib/file_system/index.js.map +0 -1
  82. package/build/cjs/lib/store/engine/lib/redis/index.js.map +0 -1
  83. package/build/cjs/lib/store/engine/lib/s3/index.js.map +0 -1
  84. package/build/cjs/lib/store/index.js.map +0 -1
  85. package/build/cjs/lib/whatsapp/index.js.map +0 -1
  86. package/build/cjs/test.d.ts +0 -1
  87. package/build/cjs/test.js +0 -71
  88. package/build/cjs/test.js.map +0 -1
  89. package/build/esm/decorators.js.map +0 -1
  90. package/build/esm/index.js.map +0 -1
  91. package/build/esm/lib/bot/decorator.js.map +0 -1
  92. package/build/esm/lib/bot/decorators.js.map +0 -1
  93. package/build/esm/lib/bot/index.js.map +0 -1
  94. package/build/esm/lib/chat/index.js.map +0 -1
  95. package/build/esm/lib/contact/index.js.map +0 -1
  96. package/build/esm/lib/message/index.js.map +0 -1
  97. package/build/esm/lib/status/index.js.map +0 -1
  98. package/build/esm/lib/store/engine/index.js.map +0 -1
  99. package/build/esm/lib/store/engine/lib/file_system/index.js.map +0 -1
  100. package/build/esm/lib/store/engine/lib/redis/index.js.map +0 -1
  101. package/build/esm/lib/store/engine/lib/s3/index.js.map +0 -1
  102. package/build/esm/lib/store/index.js.map +0 -1
  103. package/build/esm/lib/whatsapp/index.js.map +0 -1
  104. package/build/esm/test.d.ts +0 -1
  105. package/build/esm/test.js +0 -66
  106. package/build/esm/test.js.map +0 -1
@@ -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,121 @@ 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 };
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 });
87
154
  }
88
- if (option === undefined || option === true) {
89
- return { max: null, interval_ms: 60_000 };
155
+ return null;
156
+ }
157
+ /**
158
+ * @internal
159
+ * Persiste un binario: crudo cuando el driver lo soporta, JSON con base64 si no.
160
+ * Persists a binary: raw when the driver supports it, base64 JSON otherwise.
161
+ */
162
+ async #write_content(path, data) {
163
+ if (this.engine.set_buffer) {
164
+ await this.engine.set_buffer(path, data);
90
165
  }
91
- if (typeof option === 'number') {
92
- return { max: option, interval_ms: 60_000 };
166
+ else {
167
+ await this.engine.set(path, (0, store_1.serialize)({ data: data.toString('base64') }));
93
168
  }
94
- return {
95
- max: option.max ?? null,
96
- interval_ms: (option.interval ?? 60) * 1000,
97
- };
169
+ }
170
+ /** @internal Encola una tarea en la cadena serial de handlers. / Queues a task on the serial handler chain. */
171
+ #enqueue(task) {
172
+ this.#chain = this.#chain.then(task).catch(() => { });
173
+ }
174
+ /**
175
+ * Emite un evento del cliente. Lo usan las entidades de la librería para propagar los
176
+ * cambios que provocan; el consumidor puede emitir los suyos para pruebas.
177
+ * Emits a client event. Library entities use it to propagate the changes they cause;
178
+ * consumers may emit their own for testing.
179
+ *
180
+ * @param event - Nombre del evento / Event name
181
+ * @param args - Argumentos del evento / Event arguments
182
+ * @returns true si había listeners / true when listeners were present
183
+ */
184
+ emit(event, ...args) {
185
+ return this.#event.emit(event, ...args);
98
186
  }
99
187
  /**
100
188
  * Registra un listener de evento. Retorna función para desuscribirse.
101
189
  * Registers an event listener. Returns an unsubscribe function.
102
190
  */
103
191
  on(event, handler) {
104
- this._event.on(event, handler);
105
- return () => { this._event.off(event, handler); };
192
+ this.#event.on(event, handler);
193
+ return () => { this.#event.off(event, handler); };
106
194
  }
107
195
  /**
108
196
  * Quita un listener previamente registrado.
109
197
  * Removes a previously registered listener.
110
198
  */
111
199
  off(event, handler) {
112
- this._event.off(event, handler);
200
+ this.#event.off(event, handler);
113
201
  return this;
114
202
  }
115
203
  /**
@@ -117,15 +205,17 @@ class WhatsApp {
117
205
  * Registers a one-shot listener. Returns an unsubscribe function.
118
206
  */
119
207
  once(event, handler) {
120
- this._event.once(event, handler);
121
- return () => { this._event.off(event, handler); };
208
+ this.#event.once(event, handler);
209
+ return () => { this.#event.off(event, handler); };
122
210
  }
123
211
  /**
124
212
  * @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.
213
+ * Normaliza cualquier identificador (JID, LID, número, etc.) a JID canónico. Las
214
+ * entidades lo alcanzan por el canal interno, no por la instancia.
215
+ * Normalizes any identifier (JID, LID, number…) into a canonical JID. Entities reach it
216
+ * through the internal channel, not through the instance.
127
217
  */
128
- async _resolve_jid(uid) {
218
+ async #resolve_jid(uid) {
129
219
  let result = null;
130
220
  if (uid.endsWith('@g.us') || uid.endsWith('@s.whatsapp.net')) {
131
221
  result = uid;
@@ -145,7 +235,7 @@ class WhatsApp {
145
235
  // baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
146
236
  // (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
147
237
  // realmente está guardado, y el mensaje/poll no se encuentra.
148
- const pn = await this._socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
238
+ const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
149
239
  if (pn) {
150
240
  // getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
151
241
  // que el JID coincida con el que usa el store (sin device).
@@ -170,16 +260,20 @@ class WhatsApp {
170
260
  * Resolves once the session is synced; retries on non-loggedOut disconnects.
171
261
  */
172
262
  async connect(callback) {
263
+ if (this.#internals.socket) {
264
+ await this.disconnect({ silent: true });
265
+ }
173
266
  const { version } = await (0, baileys_1.fetchLatestBaileysVersion)();
174
- this._intentional_close = false;
175
- this._has_connected = false;
267
+ this.#intentional_close = false;
268
+ this.#silent_close = false;
269
+ this.#has_connected = false;
176
270
  return new Promise((resolve, reject) => {
177
271
  const start = async () => {
178
272
  // Re-lee creds en cada start() para que limpiezas del engine tomen efecto
179
273
  // en reintentos (permite al consumer forzar nueva sesión borrando /session/creds).
180
274
  const stored = await this.engine.get('/session/creds');
181
275
  const creds = (0, store_1.deserialize)(stored) ?? (0, baileys_1.initAuthCreds)();
182
- this._socket = (0, baileys_1.makeWASocket)({
276
+ this.#internals.socket = (0, baileys_1.makeWASocket)({
183
277
  version,
184
278
  auth: {
185
279
  creds,
@@ -206,19 +300,19 @@ class WhatsApp {
206
300
  },
207
301
  browser: baileys_1.Browsers.windows('Chrome'),
208
302
  logger: (0, pino_1.default)({ level: 'silent' }),
209
- syncFullHistory: this._sync,
303
+ syncFullHistory: this.#sync,
210
304
  // 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`
305
+ // tokens) que rc13 exige para enviar: apagarlos todos con `() => this.#sync`
212
306
  // deja la sesión sin tctoken y el server rechaza los mensajes con
213
307
  // "error 463: account restricted or missing tctoken". Por eso se procesan
214
308
  // siempre los no-FULL; `sync` solo decide si además se trae el historial FULL.
215
309
  // Non-FULL syncs carry the LID mappings and tctokens rc13 requires to send;
216
310
  // disabling them all made the server reject messages with error 463. We always
217
311
  // process non-FULL; `sync` only gates whether FULL history is pulled too.
218
- shouldSyncHistoryMessage: ({ syncType }) => this._sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
312
+ shouldSyncHistoryMessage: ({ syncType }) => this.#sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
219
313
  markOnlineOnConnect: false,
220
314
  });
221
- const socket = this._socket;
315
+ const socket = this.#internals.socket;
222
316
  socket.ev.on('creds.update', () => this.engine.set('/session/creds', (0, store_1.serialize)(creds)));
223
317
  socket.ev.on('connection.update', async (update) => {
224
318
  const { connection, lastDisconnect, qr } = update;
@@ -226,24 +320,25 @@ class WhatsApp {
226
320
  // Baileys refresca QR periódicamente (~20s). Emitimos nuevo pair
227
321
  // code / QR en cada refresh para que el usuario pueda renovar si
228
322
  // 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));
323
+ // El PIN necesita número: sin `phone` la vinculación es siempre QR, y
324
+ // con `phone` manda `method` (OTP por defecto).
325
+ // A PIN requires a number: without `phone` linking is always QR, and with
326
+ // `phone` the `method` option decides (OTP by default).
327
+ if (this.#phone && (this.#method ?? 'otp') === 'otp') {
328
+ await callback(await socket.requestPairingCode(this.#phone));
234
329
  }
235
330
  else {
236
331
  await callback(await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
237
332
  }
238
333
  }
239
334
  if (connection === 'open') {
240
- this._has_connected = true;
241
- this._retry_count = 0;
242
- this._event.emit('connected', this);
335
+ this.#has_connected = true;
336
+ this.#retry_count = 0;
337
+ this.emit('connected', this);
243
338
  resolve();
244
339
  }
245
340
  else if (connection === 'close') {
246
- this._socket = null;
341
+ this.#internals.socket = null;
247
342
  const status_code = lastDisconnect?.error?.output?.statusCode;
248
343
  // `restartRequired` (515) es una reconexión exigida por el protocolo
249
344
  // tras el sync inicial — no es un disconnect "real".
@@ -251,35 +346,37 @@ class WhatsApp {
251
346
  // Limpieza del engine ANTES de emitir `disconnected` para que los
252
347
  // listeners vean el estado final (engine vaciado o creds borradas).
253
348
  if (status_code === baileys_1.DisconnectReason.loggedOut) {
254
- if (this._autoclean) {
349
+ if (this.#autoclean) {
255
350
  await this.engine.clear();
256
351
  }
257
352
  else {
258
353
  await this.engine.unset('/session/creds');
259
354
  }
260
355
  }
261
- if (this._has_connected && !is_transient) {
262
- this._event.emit('disconnected', this);
356
+ // `disconnect({ silent: true })` calla el evento de este cierre concreto.
357
+ // `disconnect({ silent: true })` mutes this specific close's event.
358
+ if (this.#has_connected && !is_transient && !this.#silent_close) {
359
+ this.emit('disconnected', this);
263
360
  }
264
- if (!this._intentional_close) {
361
+ if (!this.#intentional_close) {
265
362
  if (status_code === baileys_1.DisconnectReason.loggedOut) {
266
363
  reject(new Error('Logged out'));
267
364
  }
268
365
  else {
269
- const max = this._reconnect.max;
366
+ const max = this.#reconnect.max;
270
367
  // Transient closes (restartRequired) son parte del protocolo,
271
368
  // no cuentan contra el límite de reintentos por fallo.
272
- const exhausted = !is_transient && max !== null && this._retry_count >= max;
369
+ const exhausted = !is_transient && max !== null && this.#retry_count >= max;
273
370
  if (exhausted) {
274
371
  reject(new Error(`Reconnect attempts exhausted (${max})`));
275
372
  }
276
373
  else {
277
374
  if (!is_transient) {
278
- this._retry_count++;
375
+ this.#retry_count++;
279
376
  }
280
- const delay = is_transient ? 0 : this._reconnect.interval_ms;
281
- this._retry_timer = setTimeout(() => {
282
- this._retry_timer = null;
377
+ const delay = is_transient ? 0 : this.#reconnect.interval_ms;
378
+ this.#retry_timer = setTimeout(() => {
379
+ this.#retry_timer = null;
283
380
  start().catch(reject);
284
381
  }, delay);
285
382
  }
@@ -287,24 +384,119 @@ class WhatsApp {
287
384
  }
288
385
  }
289
386
  });
290
- this._attach_business_handlers(socket);
387
+ this.#attach_business_handlers(socket);
291
388
  };
292
389
  start().catch(reject);
293
390
  });
294
391
  }
392
+ /**
393
+ * Actualiza el perfil de la cuenta en WhatsApp: nombre público, bio y/o foto. Sólo se
394
+ * envía lo que llega en el parche; `photo: null` elimina la foto actual.
395
+ * Updates the account profile on WhatsApp: public name, bio and/or picture. Only the
396
+ * given fields are sent; `photo: null` removes the current picture.
397
+ *
398
+ * @param patch - Campos a actualizar / Fields to update
399
+ * @returns true si todo lo pedido se envió / true when everything requested was sent
400
+ * @throws ERR_PROFILE_PICTURE_LIB si falta `sharp` o `jimp` para procesar la foto / when `sharp` or `jimp` is missing to process the picture
401
+ */
402
+ async profile(patch) {
403
+ const socket = this.#internals.socket;
404
+ let ok = false;
405
+ if (socket) {
406
+ const self = (0, baileys_1.jidNormalizedUser)(socket.user?.id ?? '');
407
+ if (patch.name !== undefined) {
408
+ await socket.updateProfileName(patch.name);
409
+ }
410
+ if (patch.content !== undefined) {
411
+ await socket.updateProfileStatus(patch.content);
412
+ }
413
+ if (patch.photo === null) {
414
+ await socket.removeProfilePicture(self);
415
+ }
416
+ else if (patch.photo !== undefined) {
417
+ // baileys redimensiona la foto con `sharp` o `jimp`; ninguna es dependencia
418
+ // nuestra, así que su ausencia se traduce a un error accionable.
419
+ // baileys resizes the picture with `sharp` or `jimp`; neither is a dependency
420
+ // of ours, so their absence is translated into an actionable error.
421
+ await socket
422
+ .updateProfilePicture(self, typeof patch.photo === 'string' ? { url: patch.photo } : patch.photo)
423
+ .catch((error) => {
424
+ if (/image processing library/i.test(error.message)) {
425
+ throw new Error('ERR_PROFILE_PICTURE_LIB');
426
+ }
427
+ throw error;
428
+ });
429
+ }
430
+ ok = true;
431
+ }
432
+ return ok;
433
+ }
434
+ /**
435
+ * Publica un estado (status broadcast). Con sólo `caption` publica texto; con `content`
436
+ * publica imagen o video (el tipo se deduce del binario) usando `caption` como pie.
437
+ * `contacts` es la audiencia: WhatsApp no reparte el estado a quien no esté en la lista.
438
+ * Publishes a status broadcast. With only `caption` it posts text; with `content` it
439
+ * posts an image or video (type inferred from the binary) using `caption` as its footer.
440
+ * `contacts` is the audience: WhatsApp does not deliver the status to anyone outside it.
441
+ *
442
+ * @param post - Contenido, pie y audiencia / Content, caption and audience
443
+ * @returns Publicación creada, o null si no hay sesión / Created post, or null without a session
444
+ * @throws ERR_FEED_EMPTY sin `content` ni `caption` / when neither `content` nor `caption` is given
445
+ * @throws ERR_FEED_MEDIA si el binario no es imagen ni video / when the binary is neither image nor video
446
+ */
447
+ async feed(post) {
448
+ const socket = this.#internals.socket;
449
+ let result = null;
450
+ if (socket) {
451
+ const audience = (await Promise.all(post.contacts.map((uid) => this.#resolve_jid(String(uid))))).filter((jid) => jid !== null);
452
+ const mime = post.content ? sniff_media(post.content) : null;
453
+ if (post.content && !mime) {
454
+ throw new Error('ERR_FEED_MEDIA');
455
+ }
456
+ if (!post.content && !post.caption) {
457
+ throw new Error('ERR_FEED_EMPTY');
458
+ }
459
+ const kind = mime?.startsWith('video/') ? 'video' : mime ? 'image' : 'text';
460
+ const sent = await socket.sendMessage('status@broadcast', (post.content
461
+ ? { [kind]: post.content, caption: post.caption }
462
+ : { text: post.caption }), { statusJidList: audience });
463
+ if (sent?.key?.id) {
464
+ const created_at = (Number(sent.messageTimestamp) || Math.floor(Date.now() / 1_000)) * 1_000;
465
+ const doc = {
466
+ id: sent.key.id,
467
+ author_jid: (0, baileys_1.jidNormalizedUser)(socket.user?.id ?? ''),
468
+ type: kind,
469
+ caption: post.caption ?? '',
470
+ mime: mime ?? 'text/plain',
471
+ created_at,
472
+ expires_at: created_at + status_1.TTL_MS,
473
+ viewed: true,
474
+ raw: sent,
475
+ };
476
+ await this.engine.set(`/status/${doc.id}`, (0, store_1.serialize)(doc), created_at);
477
+ if (post.content) {
478
+ await this.#write_content(`/status/${doc.id}/content`, post.content);
479
+ }
480
+ result = new status_1.Feed(this, doc);
481
+ this.emit('feed:created', result, this);
482
+ }
483
+ }
484
+ return result;
485
+ }
295
486
  /**
296
487
  * Cierra la conexión. Con `destroy: true` vacía el engine completo.
297
488
  * Closes the connection. With `destroy: true` clears the engine entirely.
298
489
  */
299
490
  async disconnect(options = {}) {
300
- this._intentional_close = true;
491
+ this.#intentional_close = true;
492
+ this.#silent_close = options.silent === true;
301
493
  // Cancela cualquier retry programado por un close anterior, para no resucitar
302
494
  // el socket después de una desconexión manual.
303
- if (this._retry_timer) {
304
- clearTimeout(this._retry_timer);
305
- this._retry_timer = null;
495
+ if (this.#retry_timer) {
496
+ clearTimeout(this.#retry_timer);
497
+ this.#retry_timer = null;
306
498
  }
307
- if (this._socket) {
499
+ if (this.#internals.socket) {
308
500
  try {
309
501
  // Pasa un error Boom-like con statusCode=connectionClosed (428) para que
310
502
  // `lastDisconnect.error.output.statusCode` quede explícito en el close
@@ -312,17 +504,16 @@ class WhatsApp {
312
504
  const intentional = Object.assign(new Error('intentional close'), {
313
505
  output: { statusCode: baileys_1.DisconnectReason.connectionClosed },
314
506
  });
315
- await this._socket.end(intentional);
507
+ await this.#internals.socket.end(intentional);
316
508
  }
317
509
  catch {
318
510
  /* socket may already be closed */
319
511
  }
320
- this._socket = null;
512
+ this.#internals.socket = null;
321
513
  }
322
514
  if (options.destroy) {
323
515
  await this.engine.clear();
324
516
  }
325
- void options.silent;
326
517
  }
327
518
  /**
328
519
  * Conecta los handlers de eventos de baileys: contactos, chats y mensajes.
@@ -330,43 +521,45 @@ class WhatsApp {
330
521
  *
331
522
  * @internal
332
523
  */
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);
524
+ #attach_business_handlers(socket) {
525
+ socket.ev.on('messaging-history.set', ({ chats, contacts, messages }) => {
526
+ this.#enqueue(async () => {
527
+ await this.#handle_contacts_upsert(contacts);
528
+ await this.#handle_chats_upsert(chats);
529
+ await this.#handle_messages_upsert(messages);
530
+ });
338
531
  });
339
532
  socket.ev.on('contacts.upsert', (contacts) => {
340
- void this._handle_contacts_upsert(contacts);
533
+ this.#enqueue(() => this.#handle_contacts_upsert(contacts));
341
534
  });
342
535
  socket.ev.on('contacts.update', (contacts) => {
343
- void this._handle_contacts_update(contacts);
536
+ this.#enqueue(() => this.#handle_contacts_update(contacts));
344
537
  });
345
538
  socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
346
- void this._handle_lid_mapping(lid, pn);
539
+ this.#enqueue(() => this.#handle_lid_mapping(lid, pn));
347
540
  });
348
541
  socket.ev.on('chats.upsert', (chats) => {
349
- void this._handle_chats_upsert(chats);
542
+ this.#enqueue(() => this.#handle_chats_upsert(chats));
350
543
  });
351
544
  socket.ev.on('chats.update', (chats) => {
352
- void this._handle_chats_update(chats);
545
+ this.#enqueue(() => this.#handle_chats_update(chats));
353
546
  });
354
547
  socket.ev.on('chats.delete', (ids) => {
355
- void this._handle_chats_delete(ids);
548
+ this.#enqueue(() => this.#handle_chats_delete(ids));
356
549
  });
357
550
  socket.ev.on('messages.upsert', ({ messages }) => {
358
- void this._handle_messages_upsert(messages);
551
+ this.#enqueue(() => this.#handle_messages_upsert(messages));
359
552
  });
360
553
  socket.ev.on('messages.update', (updates) => {
361
- void this._handle_messages_update(updates);
554
+ this.#enqueue(() => this.#handle_messages_update(updates));
362
555
  });
363
556
  socket.ev.on('message-receipt.update', (updates) => {
364
- void this._handle_message_receipt(updates);
557
+ this.#enqueue(() => this.#handle_message_receipt(updates));
365
558
  });
366
559
  // Las reacciones llegan duplicadas por `messages.reaction` Y `messages.upsert`
367
560
  // (como `reactionMessage`). Se usa solo `messages.upsert` para evitar el doble disparo.
368
561
  // socket.ev.on('messages.reaction', (reactions) => {
369
- // void this._handle_messages_reaction(reactions);
562
+ // void this.#handle_messages_reaction(reactions);
370
563
  // });
371
564
  }
372
565
  /**
@@ -376,23 +569,23 @@ class WhatsApp {
376
569
  * @param raw - Documento del contacto a persistir / Contact document to persist
377
570
  * @internal
378
571
  */
379
- async _persist_contact(raw) {
572
+ async #persist_contact(raw) {
380
573
  const existing = await this.engine.get(`/contact/${raw.id}`);
381
574
  await this.engine.set(`/contact/${raw.id}`, (0, store_1.serialize)(raw));
382
575
  if (raw.lid) {
383
576
  await this.engine.set(`/lid/${raw.lid}`, (0, store_1.serialize)(raw.id));
384
577
  }
385
578
  if (existing === null) {
579
+ const person = new this.Contact(raw);
386
580
  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);
581
+ this.emit('contact:created', person, new this.Chat(cached_chat ?? { id: raw.id, name: person.name }), this);
389
582
  }
390
583
  }
391
584
  /** @internal */
392
- async _handle_contacts_upsert(contacts) {
585
+ async #handle_contacts_upsert(contacts) {
393
586
  for (const c of contacts) {
394
587
  if (c.id) {
395
- await this._persist_contact({
588
+ await this.#persist_contact({
396
589
  id: c.id,
397
590
  lid: c.lid ?? null,
398
591
  name: c.name ?? null,
@@ -405,7 +598,7 @@ class WhatsApp {
405
598
  }
406
599
  }
407
600
  /** @internal */
408
- async _handle_contacts_update(contacts) {
601
+ async #handle_contacts_update(contacts) {
409
602
  for (const c of contacts) {
410
603
  if (c.id) {
411
604
  const current = (0, store_1.deserialize)(await this.engine.get(`/contact/${c.id}`));
@@ -423,21 +616,21 @@ class WhatsApp {
423
616
  if (patch.lid) {
424
617
  await this.engine.set(`/lid/${patch.lid}`, (0, store_1.serialize)(c.id));
425
618
  }
619
+ const person = new this.Contact(merged);
426
620
  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);
621
+ this.emit('contact:updated', person, new this.Chat(cached_chat ?? { id: c.id, name: person.name }), this);
429
622
  }
430
623
  }
431
624
  }
432
625
  }
433
626
  }
434
627
  /** @internal */
435
- async _handle_lid_mapping(lid, pn) {
628
+ async #handle_lid_mapping(lid, pn) {
436
629
  await this.engine.set(`/lid/${lid}`, (0, store_1.serialize)(pn));
437
630
  await this.engine.set(`/lid/${pn}`, (0, store_1.serialize)(lid));
438
631
  }
439
632
  /** @internal */
440
- async _handle_chats_upsert(chats) {
633
+ async #handle_chats_upsert(chats) {
441
634
  for (const ch of chats) {
442
635
  if (ch.id) {
443
636
  const current = (0, store_1.deserialize)(await this.engine.get(`/chat/${ch.id}`));
@@ -448,20 +641,19 @@ class WhatsApp {
448
641
  pinned: ch.pinned ?? null,
449
642
  mute_end_time: ch.muteEndTime != null ? Number(ch.muteEndTime) : null,
450
643
  unread_count: ch.unreadCount ?? null,
451
- read_only: ch.readOnly ?? null,
452
644
  };
453
645
  if (ch.name) {
454
646
  raw.name = ch.name;
455
647
  }
456
648
  await this.engine.set(`/chat/${ch.id}`, (0, store_1.serialize)(raw));
457
649
  if (current === null) {
458
- this._event.emit('chat:created', new this.Chat(raw), this);
650
+ this.emit('chat:created', new this.Chat(raw), this);
459
651
  }
460
652
  }
461
653
  }
462
654
  }
463
655
  /** @internal */
464
- async _handle_chats_update(chats) {
656
+ async #handle_chats_update(chats) {
465
657
  for (const ch of chats) {
466
658
  if (ch.id && ch.id !== 'status@broadcast') {
467
659
  const current = (0, store_1.deserialize)(await this.engine.get(`/chat/${ch.id}`)) ?? {
@@ -491,29 +683,29 @@ class WhatsApp {
491
683
  const merged = { ...current, ...patch };
492
684
  await this.engine.set(`/chat/${ch.id}`, (0, store_1.serialize)(merged));
493
685
  if (pinned_changed) {
494
- this._event.emit(ch.pinned != null ? 'chat:pinned' : 'chat:unpinned', new this.Chat(merged), this);
686
+ this.emit(ch.pinned != null ? 'chat:pinned' : 'chat:unpinned', new this.Chat(merged), this);
495
687
  }
496
688
  if (archived_changed) {
497
- this._event.emit(ch.archived ? 'chat:archived' : 'chat:unarchived', new this.Chat(merged), this);
689
+ this.emit(ch.archived ? 'chat:archived' : 'chat:unarchived', new this.Chat(merged), this);
498
690
  }
499
691
  if (mute_changed) {
500
692
  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);
693
+ this.emit(is_muted ? 'chat:muted' : 'chat:unmuted', new this.Chat(merged), this);
502
694
  }
503
695
  }
504
696
  }
505
697
  }
506
698
  }
507
699
  /** @internal */
508
- async _handle_chats_delete(ids) {
700
+ async #handle_chats_delete(ids) {
509
701
  for (const cid of ids) {
510
702
  const raw = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}`)) ?? { id: cid };
511
703
  await this.engine.unset(`/chat/${cid}`);
512
- this._event.emit('chat:deleted', new this.Chat(raw), this);
704
+ this.emit('chat:deleted', new this.Chat(raw), this);
513
705
  }
514
706
  }
515
707
  /** @internal */
516
- async _handle_message_receipt(updates) {
708
+ async #handle_message_receipt(updates) {
517
709
  for (const { key, receipt } of updates) {
518
710
  // Receipt sobre status@broadcast → marca el feed como visto y emite feed:updated.
519
711
  // Receipt on status@broadcast → marks feed viewed and emits feed:updated.
@@ -522,21 +714,27 @@ class WhatsApp {
522
714
  if (feed_raw && !feed_raw.viewed) {
523
715
  feed_raw.viewed = true;
524
716
  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);
717
+ this.emit('feed:updated', new status_1.Feed(this, feed_raw), this);
526
718
  }
527
719
  continue;
528
720
  }
529
721
  if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
530
722
  const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
531
723
  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);
724
+ const next = receipt.playedTimestamp != null ? PLAYED : READ;
725
+ if (doc.status < next) {
726
+ doc.status = next;
727
+ doc.raw.status = next;
728
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
729
+ }
730
+ const msg_instance = (0, message_1.message)(this, doc);
731
+ this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
534
732
  }
535
733
  }
536
734
  }
537
735
  }
538
736
  /** @internal */
539
- async _handle_messages_upsert(messages) {
737
+ async #handle_messages_upsert(messages) {
540
738
  for (const msg of messages) {
541
739
  if (msg.key?.remoteJid && msg.key.id) {
542
740
  const cid = msg.key.remoteJidAlt ?? msg.key.remoteJid;
@@ -547,9 +745,13 @@ class WhatsApp {
547
745
  // Single channel for reactions: handled here; `messages.reaction` is disabled.
548
746
  const reaction = msg.message?.reactionMessage;
549
747
  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 },
748
+ const target_cid = (await this.#resolve_jid(reaction.key.remoteJid)) ?? reaction.key.remoteJid;
749
+ await this.#handle_messages_reaction([{
750
+ key: {
751
+ remoteJid: target_cid,
752
+ id: reaction.key.id,
753
+ participant: msg.key.fromMe ? (this.#internals.socket?.user?.id ?? null) : (msg.key.participant ?? cid),
754
+ },
553
755
  reaction: { text: reaction.text ?? '' },
554
756
  }]);
555
757
  }
@@ -565,7 +767,7 @@ class WhatsApp {
565
767
  const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${protocol.key.id}`));
566
768
  if (feed_raw) {
567
769
  await this.engine.unset(`/status/${protocol.key.id}`);
568
- this._event.emit('feed:deleted', new status_1.default({ wa: this, doc: feed_raw }), this);
770
+ this.emit('feed:deleted', new status_1.Feed(this, feed_raw), this);
569
771
  }
570
772
  }
571
773
  continue;
@@ -613,7 +815,7 @@ class WhatsApp {
613
815
  if (feed_type === 'text') {
614
816
  content_buf = Buffer.from(caption, 'utf-8');
615
817
  }
616
- else if (this._socket) {
818
+ else if (this.#internals.socket) {
617
819
  try {
618
820
  const buf = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {});
619
821
  if (Buffer.isBuffer(buf)) {
@@ -626,9 +828,9 @@ class WhatsApp {
626
828
  }
627
829
  await this.engine.set(`/status/${mid}`, (0, store_1.serialize)(feed_raw));
628
830
  if (content_buf.length > 0) {
629
- await this.engine.set(`/status/${mid}/content`, (0, store_1.serialize)({ data: content_buf.toString('base64') }));
831
+ await this.#write_content(`/status/${mid}/content`, content_buf);
630
832
  }
631
- this._event.emit('feed:created', new status_1.default({ wa: this, doc: feed_raw }), this);
833
+ this.emit('feed:created', new status_1.Feed(this, feed_raw), this);
632
834
  continue;
633
835
  }
634
836
  if (content_type === 'pollUpdateMessage') {
@@ -638,7 +840,7 @@ class WhatsApp {
638
840
  creation_key.remoteJid &&
639
841
  update?.vote?.encPayload &&
640
842
  update.vote.encIv) {
641
- const resolved_cid = (await this._resolve_jid(creation_key.remoteJid)) ?? creation_key.remoteJid;
843
+ const resolved_cid = (await this.#resolve_jid(creation_key.remoteJid)) ?? creation_key.remoteJid;
642
844
  const target_mid = creation_key.id;
643
845
  const poll_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${resolved_cid}/message/${target_mid}`));
644
846
  const secret_raw = poll_doc?.raw.message?.messageContextInfo?.messageSecret;
@@ -653,8 +855,8 @@ class WhatsApp {
653
855
  // Own HMAC identity depends on chat addressing (LID on @lid, PN on
654
856
  // @s.whatsapp.net), so fromMe positions try both candidates:
655
857
  // 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 ?? '';
858
+ const self_id = this.#internals.socket?.user?.id ?? '';
859
+ const self_lid = this.#internals.socket?.user?.lid ?? '';
658
860
  const selves = [...new Set([self_lid, self_id].filter(Boolean))];
659
861
  // Candidatos foráneos: todas las formas de identidad del key (LID,
660
862
  // participant, alt, remoteJid); se prueban todas porque el addressing
@@ -688,9 +890,9 @@ class WhatsApp {
688
890
  vote: decrypted,
689
891
  senderTimestampMs: Number(msg.messageTimestamp) || Date.now(),
690
892
  });
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);
893
+ await this.engine.set(`/chat/${resolved_cid}/message/${target_mid}`, (0, store_1.serialize)(poll_doc), poll_doc.created_at);
894
+ const msg_instance = (0, message_1.message)(this, poll_doc);
895
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
694
896
  }
695
897
  }
696
898
  catch {
@@ -711,28 +913,49 @@ class WhatsApp {
711
913
  doc) {
712
914
  doc.raw.message = protocol.editedMessage;
713
915
  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);
916
+ doc.caption = (0, message_1.message)(this, doc.raw).caption;
917
+ await this.engine.set(`/chat/${target_cid}/message/${target_mid}`, (0, store_1.serialize)(doc), doc.created_at);
918
+ const msg_instance = (0, message_1.message)(this, doc);
919
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
718
920
  }
719
921
  else if (protocol.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE) {
720
922
  await this.engine.unset(`/chat/${target_cid}/message/${target_mid}`);
721
923
  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);
924
+ const msg_instance = (0, message_1.message)(this, doc);
925
+ this.emit('message:deleted', msg_instance, await msg_instance.chat(), this);
724
926
  }
725
927
  }
726
928
  }
727
929
  continue;
728
930
  }
729
- const doc = this.Message.build_message_doc(msg, this._socket?.user?.id);
931
+ const doc = (0, message_1.message)(this, msg)._raw;
932
+ // Cada reconexión re-entrega el historial completo: reescribir documentos
933
+ // idénticos contamina la cronología, re-descarga la media y spamea eventos,
934
+ // así que un doc ya persistido sin cambios visibles se salta entero.
935
+ // Every reconnect re-delivers the full history: rewriting identical documents
936
+ // pollutes chronology, re-downloads media and spams events, so an already
937
+ // persisted doc without visible changes is skipped entirely.
938
+ const existing_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}/message/${mid}`));
939
+ if (existing_doc) {
940
+ if (typeof existing_doc.multiple === 'boolean') {
941
+ doc.multiple = existing_doc.multiple;
942
+ }
943
+ if (existing_doc.reactions) {
944
+ doc.reactions = existing_doc.reactions;
945
+ }
946
+ if (existing_doc.status >= doc.status &&
947
+ existing_doc.caption === doc.caption &&
948
+ existing_doc.edited === doc.edited &&
949
+ existing_doc.starred === doc.starred) {
950
+ continue;
951
+ }
952
+ }
730
953
  // Autocreación de contacto/chat desde pushName cuando baileys no emite upsert previo
731
- if (!doc.me) {
954
+ if (!existing_doc && !doc.me) {
732
955
  const push_name = msg.pushName ?? null;
733
956
  const is_group = cid.endsWith('@g.us');
734
957
  if (doc.author && !(await this.engine.get(`/contact/${doc.author}`))) {
735
- await this._persist_contact({
958
+ await this.#persist_contact({
736
959
  id: doc.author,
737
960
  lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : null,
738
961
  name: null,
@@ -748,20 +971,17 @@ class WhatsApp {
748
971
  name: is_group ? null : push_name,
749
972
  };
750
973
  await this.engine.set(`/chat/${cid}`, (0, store_1.serialize)(chat_raw));
751
- this._event.emit('chat:created', new this.Chat(chat_raw), this);
974
+ this.emit('chat:created', new this.Chat(chat_raw), this);
752
975
  }
753
976
  }
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));
977
+ await this.engine.set(`/chat/${cid}/message/${mid}`, (0, store_1.serialize)(doc), doc.created_at);
978
+ // El binario solo se materializa en la primera entrega; en re-syncs ya vive en el engine.
979
+ // The binary is only materialized on first delivery; on re-syncs it already lives in the engine.
763
980
  let content_buf = Buffer.alloc(0);
764
- if (doc.type === 'text') {
981
+ if (existing_doc) {
982
+ /* ya materializado / already materialized */
983
+ }
984
+ else if (doc.type === 'text') {
765
985
  content_buf = Buffer.from(doc.caption, 'utf-8');
766
986
  }
767
987
  else if (doc.type === 'location') {
@@ -784,7 +1004,7 @@ class WhatsApp {
784
1004
  else if (doc.type === 'event') {
785
1005
  content_buf = Buffer.from(JSON.stringify(msg.message?.eventMessage ?? {}), 'utf-8');
786
1006
  }
787
- else if (this._socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
1007
+ else if (this.#internals.socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
788
1008
  try {
789
1009
  const buffer = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {});
790
1010
  if (Buffer.isBuffer(buffer)) {
@@ -796,19 +1016,19 @@ class WhatsApp {
796
1016
  }
797
1017
  }
798
1018
  if (content_buf.length > 0) {
799
- await this.engine.set(`/chat/${cid}/message/${mid}/content`, (0, store_1.serialize)({ data: content_buf.toString('base64') }));
1019
+ await this.#write_content(`/chat/${cid}/message/${mid}/content`, content_buf);
800
1020
  }
801
- const instance = await this.Message.build_instance(doc);
1021
+ const instance = (0, message_1.message)(this, doc);
802
1022
  const chat_instance = await instance.chat();
803
- this._event.emit('message:created', instance, chat_instance, this);
1023
+ this.emit('message:created', instance, chat_instance, this);
804
1024
  if (doc.forwarded) {
805
- this._event.emit('message:forwarded', instance, chat_instance, this);
1025
+ this.emit('message:forwarded', instance, chat_instance, this);
806
1026
  }
807
1027
  }
808
1028
  }
809
1029
  }
810
1030
  /** @internal */
811
- async _handle_messages_update(updates) {
1031
+ async #handle_messages_update(updates) {
812
1032
  for (const { key, update: upd } of updates) {
813
1033
  if (key.remoteJid && key.id) {
814
1034
  // Updates sobre `status@broadcast` se descartan: el feed sólo se
@@ -830,42 +1050,42 @@ class WhatsApp {
830
1050
  raw.message = edited_message;
831
1051
  doc.raw = raw;
832
1052
  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);
1053
+ doc.caption = (0, message_1.message)(this, raw).caption;
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('message:updated', msg_instance, await msg_instance.chat(), this);
837
1057
  }
838
1058
  else if (content_update) {
839
1059
  // Actualización de contenido (ej: live location). Mergea sobre el raw existente.
840
1060
  raw.message = { ...raw.message, ...content_update };
841
1061
  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);
1062
+ doc.caption = (0, message_1.message)(this, raw).caption;
1063
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1064
+ const msg_instance = (0, message_1.message)(this, doc);
1065
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
846
1066
  }
847
1067
  else if (starred_changed) {
848
1068
  doc.starred = upd_any.starred === true;
849
1069
  raw.starred = doc.starred;
850
1070
  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);
1071
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1072
+ const msg_instance = (0, message_1.message)(this, doc);
1073
+ this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
854
1074
  }
855
1075
  else if (status !== undefined) {
856
1076
  raw.status = status;
857
1077
  doc.status = status;
858
1078
  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);
1079
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1080
+ const msg_instance = (0, message_1.message)(this, doc);
1081
+ this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
862
1082
  }
863
1083
  }
864
1084
  }
865
1085
  }
866
1086
  }
867
1087
  /** @internal */
868
- async _handle_messages_reaction(reactions) {
1088
+ async #handle_messages_reaction(reactions) {
869
1089
  for (const { key, reaction } of reactions) {
870
1090
  if (key.remoteJid && key.id) {
871
1091
  // Reacciones sobre status@broadcast → feed:updated (no message:reacted).
@@ -873,14 +1093,21 @@ class WhatsApp {
873
1093
  if (key.remoteJid === 'status@broadcast') {
874
1094
  const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${key.id}`));
875
1095
  if (feed_raw) {
876
- this._event.emit('feed:updated', new status_1.default({ wa: this, doc: feed_raw }), this);
1096
+ this.emit('feed:updated', new status_1.Feed(this, feed_raw), this);
877
1097
  }
878
1098
  continue;
879
1099
  }
880
1100
  const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
881
1101
  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);
1102
+ const reactor = (0, baileys_1.jidNormalizedUser)(key.participant ?? key.remoteJid);
1103
+ const emoji = reaction.text ?? '';
1104
+ doc.reactions = [
1105
+ ...(doc.reactions ?? []).filter((r) => r.author !== reactor),
1106
+ ...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
1107
+ ];
1108
+ await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1109
+ const msg_instance = (0, message_1.message)(this, doc);
1110
+ this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
884
1111
  }
885
1112
  }
886
1113
  }
@@ -888,4 +1115,3 @@ class WhatsApp {
888
1115
  }
889
1116
  exports.WhatsApp = WhatsApp;
890
1117
  exports.default = WhatsApp;
891
- //# sourceMappingURL=index.js.map