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