@arcaelas/whatsapp 6.2.0 → 7.0.1

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 (42) hide show
  1. package/README.md +19 -15
  2. package/build/cjs/index.d.ts +9 -5
  3. package/build/cjs/index.js +8 -7
  4. package/build/cjs/lib/bot/decorator.d.ts +2 -9
  5. package/build/cjs/lib/bot/decorator.js +0 -1
  6. package/build/cjs/lib/bot/decorators.d.ts +48 -4
  7. package/build/cjs/lib/bot/decorators.js +2 -2
  8. package/build/cjs/lib/chat/index.d.ts +132 -152
  9. package/build/cjs/lib/chat/index.js +138 -219
  10. package/build/cjs/lib/contact/index.d.ts +114 -80
  11. package/build/cjs/lib/contact/index.js +187 -94
  12. package/build/cjs/lib/message/index.d.ts +400 -321
  13. package/build/cjs/lib/message/index.js +425 -913
  14. package/build/cjs/lib/status/index.d.ts +22 -33
  15. package/build/cjs/lib/status/index.js +52 -93
  16. package/build/cjs/lib/store/index.d.ts +16 -0
  17. package/build/cjs/lib/store/index.js +29 -3
  18. package/build/cjs/lib/whatsapp/index.d.ts +44 -212
  19. package/build/cjs/lib/whatsapp/index.js +483 -1099
  20. package/build/esm/index.d.ts +9 -5
  21. package/build/esm/index.js +6 -4
  22. package/build/esm/lib/bot/decorator.d.ts +2 -9
  23. package/build/esm/lib/bot/decorator.js +1 -1
  24. package/build/esm/lib/bot/decorators.d.ts +48 -4
  25. package/build/esm/lib/bot/decorators.js +2 -2
  26. package/build/esm/lib/chat/index.d.ts +132 -152
  27. package/build/esm/lib/chat/index.js +139 -219
  28. package/build/esm/lib/contact/index.d.ts +114 -80
  29. package/build/esm/lib/contact/index.js +186 -94
  30. package/build/esm/lib/message/index.d.ts +400 -321
  31. package/build/esm/lib/message/index.js +422 -913
  32. package/build/esm/lib/status/index.d.ts +22 -33
  33. package/build/esm/lib/status/index.js +49 -93
  34. package/build/esm/lib/store/index.d.ts +16 -0
  35. package/build/esm/lib/store/index.js +25 -0
  36. package/build/esm/lib/whatsapp/index.d.ts +44 -212
  37. package/build/esm/lib/whatsapp/index.js +486 -1101
  38. package/package.json +1 -1
  39. package/build/cjs/lib/internal.d.ts +0 -40
  40. package/build/cjs/lib/internal.js +0 -38
  41. package/build/esm/lib/internal.d.ts +0 -40
  42. package/build/esm/lib/internal.js +0 -34
@@ -1,9 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * @file whatsapp/index.ts
4
- * @description Orquestador principal del cliente WhatsApp v3.
5
- * Main orchestrator of the WhatsApp v3 client.
6
- */
7
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
3
  if (k2 === undefined) k2 = k;
9
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -41,244 +36,57 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
42
37
  };
43
38
  Object.defineProperty(exports, "__esModule", { value: true });
44
- exports.WhatsApp = void 0;
45
39
  const baileys_1 = require("baileys");
46
40
  const node_events_1 = require("node:events");
47
41
  const pino_1 = __importDefault(require("pino"));
48
42
  const QRCode = __importStar(require("qrcode"));
49
43
  const chat_1 = require("../../lib/chat");
50
44
  const contact_1 = require("../../lib/contact");
51
- const internal_1 = require("../../lib/internal");
52
- const message_1 = require("../../lib/message");
45
+ const message_1 = __importStar(require("../../lib/message"));
53
46
  const status_1 = require("../../lib/status");
54
47
  const store_1 = require("../../lib/store");
55
- /** Rechazo del servidor: estado terminal, el único que puede retroceder el avance. / Server rejection: terminal state, the only one allowed to move the state backwards. */
56
- const ERROR = 0;
57
- /** Estados legibles del mensaje que el receipt puede avanzar. / Readable message states a receipt can advance to. */
58
- const READ = 4;
59
- const PLAYED = 5;
60
- /**
61
- * Deduce el MIME de un binario por su firma, acotado a lo que WhatsApp acepta en un estado.
62
- * Infers a binary's MIME from its signature, limited to what WhatsApp accepts in a status.
63
- *
64
- * @param data - Binario a inspeccionar / Binary to inspect
65
- * @returns MIME reconocido, o null / Recognized MIME, or null
66
- */
67
- function sniff_media(data) {
68
- if (data.subarray(0, 3).toString('hex') === 'ffd8ff')
69
- return 'image/jpeg';
70
- if (data.subarray(0, 8).toString('hex') === '89504e470d0a1a0a')
71
- return 'image/png';
72
- if (data.subarray(0, 4).toString() === 'RIFF' && data.subarray(8, 12).toString() === 'WEBP')
73
- return 'image/webp';
74
- if (data.subarray(4, 8).toString() === 'ftyp')
75
- return 'video/mp4';
76
- return null;
77
- }
78
- /**
79
- * Cliente principal de WhatsApp. No inicia la conexión al instanciar.
80
- * Main WhatsApp client. Does not connect on instantiation.
81
- *
82
- * @example
83
- * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 5491112345678 });
84
- * wa.on('message:created', (msg) => console.log(msg.caption));
85
- * await wa.connect((code) => console.log(code));
86
- */
87
48
  class WhatsApp {
88
- /** @internal Emisor de los eventos del cliente. / Client event emitter. */
89
49
  #event = new node_events_1.EventEmitter();
90
- /** @internal Estado compartido con las entidades (socket, resolución de JIDs). / State shared with the entities. */
91
- #internals;
92
- #phone;
93
- #method;
94
- #autoclean;
95
- #reconnect;
96
- #sync;
97
- #intentional_close = false;
98
- #silent_close = false;
99
- #has_connected = false;
100
- #retry_timer = null;
101
- #retry_count = 0;
102
- /**
103
- * @internal
104
- * Cadena que serializa los handlers de eventos de baileys: dos eventos sobre el mismo
105
- * documento ya no se intercalan (lost updates por read-modify-write concurrente).
106
- * Chain serializing baileys event handlers: two events over the same document no longer
107
- * interleave (lost updates from concurrent read-modify-write).
108
- */
109
- #chain = Promise.resolve();
50
+ #options;
51
+ #close = null;
110
52
  constructor(options) {
111
53
  this.engine = options.engine;
112
- this.#phone = options.phone !== undefined ? String(options.phone).replace(/\D+/g, '') : undefined;
113
- this.#method = options.method;
114
- this.#autoclean = options.autoclean ?? true;
115
- this.#sync = options.sync ?? true;
116
- this.#reconnect =
117
- options.reconnect === false ? { max: 0, interval_ms: 60_000 }
118
- : options.reconnect === undefined || options.reconnect === true ? { max: null, interval_ms: 60_000 }
119
- : typeof options.reconnect === 'number' ? { max: options.reconnect, interval_ms: 60_000 }
120
- : { max: options.reconnect.max ?? null, interval_ms: (options.reconnect.interval ?? 60) * 1_000 };
121
- this.#internals = { socket: null, resolve_jid: (uid) => this.#resolve_jid(uid) };
122
- (0, internal_1.bind)(this, this.#internals);
123
- this.Contact = (0, contact_1.contact)(this);
124
- this.Chat = (0, chat_1.chat)(this);
125
- this.Message = {
126
- get: (cid, mid) => message_1.Message.get(this, cid, mid),
127
- list: (cid, offset, limit) => message_1.Message.list(this, cid, offset, limit),
128
- text: (cid, ...rest) => message_1.Message.text(this, cid, ...rest),
129
- image: (cid, ...rest) => message_1.Message.image(this, cid, ...rest),
130
- video: (cid, ...rest) => message_1.Message.video(this, cid, ...rest),
131
- audio: (cid, ...rest) => message_1.Message.audio(this, cid, ...rest),
132
- location: (cid, ...rest) => message_1.Message.location(this, cid, ...rest),
133
- poll: (cid, ...rest) => message_1.Message.poll(this, cid, ...rest),
134
- document: (cid, ...rest) => message_1.Message.document(this, cid, ...rest),
135
- vcard: (cid, ...rest) => message_1.Message.vcard(this, cid, ...rest),
136
- event: (cid, ...rest) => message_1.Message.event(this, cid, ...rest),
137
- react: (cid, mid, emoji) => message_1.Message.react(this, cid, mid, emoji),
138
- star: (cid, mid, value) => message_1.Message.star(this, cid, mid, value),
139
- seen: (cid, mid) => message_1.Message.seen(this, cid, mid),
140
- edit: (cid, mid, caption) => message_1.Message.edit(this, cid, mid, caption),
141
- forward: (cid, mid, target) => message_1.Message.forward(this, cid, mid, target),
142
- delete: (cid, mid, all) => message_1.Message.delete(this, cid, mid, all),
143
- reactions: (cid, mid) => message_1.Message.reactions(this, cid, mid),
144
- 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,
145
- };
146
- }
147
- /**
148
- * Contacto de la cuenta autenticada, o null mientras no hay sesión abierta.
149
- * Authenticated account's contact, or null while there is no open session.
150
- */
151
- get contact() {
152
- const user = this.#internals.socket?.user;
153
- if (user) {
154
- const jid = (0, baileys_1.jidNormalizedUser)(user.id);
155
- return new this.Contact({ id: jid, phone_number: jid, lid: user.lid ?? null, name: user.name ?? null });
156
- }
157
- return null;
54
+ this.#options = options;
158
55
  }
159
- /**
160
- * @internal
161
- * Persiste un binario: crudo cuando el driver lo soporta, JSON con base64 si no.
162
- * Persists a binary: raw when the driver supports it, base64 JSON otherwise.
163
- */
164
- async #write_content(path, data) {
165
- if (this.engine.set_buffer) {
166
- await this.engine.set_buffer(path, data);
167
- }
168
- else {
169
- await this.engine.set(path, (0, store_1.serialize)({ data: data.toString('base64') }));
170
- }
171
- }
172
- /** @internal Encola una tarea en la cadena serial de handlers. / Queues a task on the serial handler chain. */
173
- #enqueue(task) {
174
- this.#chain = this.#chain.then(task).catch(() => { });
175
- }
176
- /**
177
- * Emite un evento del cliente. Lo usan las entidades de la librería para propagar los
178
- * cambios que provocan; el consumidor puede emitir los suyos para pruebas.
179
- * Emits a client event. Library entities use it to propagate the changes they cause;
180
- * consumers may emit their own for testing.
181
- *
182
- * @param event - Nombre del evento / Event name
183
- * @param args - Argumentos del evento / Event arguments
184
- * @returns true si había listeners / true when listeners were present
185
- */
186
56
  emit(event, ...args) {
187
57
  return this.#event.emit(event, ...args);
188
58
  }
189
- /**
190
- * Registra un listener de evento. Retorna función para desuscribirse.
191
- * Registers an event listener. Returns an unsubscribe function.
192
- */
193
59
  on(event, handler) {
194
60
  this.#event.on(event, handler);
195
61
  return () => { this.#event.off(event, handler); };
196
62
  }
197
- /**
198
- * Quita un listener previamente registrado.
199
- * Removes a previously registered listener.
200
- */
201
- off(event, handler) {
202
- this.#event.off(event, handler);
203
- return this;
204
- }
205
- /**
206
- * Registra un listener one-shot. Retorna función para desuscribirse antes de que dispare.
207
- * Registers a one-shot listener. Returns an unsubscribe function.
208
- */
209
63
  once(event, handler) {
210
64
  this.#event.once(event, handler);
211
65
  return () => { this.#event.off(event, handler); };
212
66
  }
213
- /**
214
- * @internal
215
- * Normaliza cualquier identificador (JID, LID, número, etc.) a JID canónico. Las
216
- * entidades lo alcanzan por el canal interno, no por la instancia.
217
- * Normalizes any identifier (JID, LID, number…) into a canonical JID. Entities reach it
218
- * through the internal channel, not through the instance.
219
- */
220
- async #resolve_jid(uid) {
221
- let result = null;
222
- if (uid.endsWith('@g.us') || uid.endsWith('@s.whatsapp.net')) {
223
- result = uid;
224
- }
225
- else if (uid.endsWith('@lid')) {
226
- // Los receipts direccionan por dispositivo (`…:9@lid`); el índice se guarda sin él.
227
- // Receipts address per device (`…:9@lid`); the index is stored without it.
228
- const lid = (0, baileys_1.jidNormalizedUser)(uid);
229
- const direct = (0, store_1.deserialize)(await this.engine.get(`/lid/${lid}`));
230
- if (direct) {
231
- result = direct.includes('@') ? direct : `${direct}@s.whatsapp.net`;
232
- }
233
- else {
234
- const reverse = (0, store_1.deserialize)(await this.engine.get(`/lid/${lid.split('@')[0]}_reverse`));
235
- if (reverse != null) {
236
- result = `${reverse}@s.whatsapp.net`;
237
- }
238
- else {
239
- // El store local puede no tener el mapping (sesión sin upsert del contacto);
240
- // baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
241
- // (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
242
- // realmente está guardado, y el mensaje/poll no se encuentra.
243
- const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null);
244
- if (pn) {
245
- // getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
246
- // que el JID coincida con el que usa el store (sin device).
247
- result = (0, baileys_1.jidNormalizedUser)(pn.includes('@') ? pn : `${pn}@s.whatsapp.net`);
248
- }
249
- }
250
- }
251
- }
252
- else {
253
- const cleaned = uid.replace(/\D/g, '');
254
- if (cleaned) {
255
- result = `${cleaned}@s.whatsapp.net`;
256
- }
257
- }
258
- return result;
67
+ off(event, handler) {
68
+ this.#event.off(event, handler);
69
+ return this;
259
70
  }
260
- /**
261
- * Inicia la conexión. El callback recibe el PIN (string) si se configuró `phone`, o el QR (Buffer PNG) si no.
262
- * Resuelve cuando la sesión sincroniza; reintenta automáticamente en cierres no-loggedOut.
263
- *
264
- * Starts the connection. Callback receives the PIN (string) when `phone` is configured, or the QR (PNG Buffer) otherwise.
265
- * Resolves once the session is synced; retries on non-loggedOut disconnects.
266
- */
267
71
  async connect(callback) {
268
- if (this.#internals.socket) {
269
- await this.disconnect({ silent: true });
270
- }
72
+ const { engine } = this;
73
+ const { phone, method, autoclean = true, sync = true, reconnect = true } = this.#options;
74
+ const digits = phone !== undefined ? String(phone).replace(/\D+/g, '') : '';
75
+ const budget = reconnect === false ? 0 : reconnect === true ? null : typeof reconnect === 'number' ? reconnect : reconnect.max ?? null;
76
+ const wait = typeof reconnect === 'object' ? (reconnect.interval ?? 60) * 1_000 : 60_000;
77
+ await this.#close?.(true);
271
78
  const { version } = await (0, baileys_1.fetchLatestBaileysVersion)();
272
- this.#intentional_close = false;
273
- this.#silent_close = false;
274
- this.#has_connected = false;
79
+ let connected = false;
80
+ let retries = 0;
81
+ let intentional = false;
82
+ let silent = false;
83
+ let alive = null;
84
+ let timer = null;
85
+ let chain = Promise.resolve();
275
86
  return new Promise((resolve, reject) => {
276
87
  const start = async () => {
277
- // Re-lee creds en cada start() para que limpiezas del engine tomen efecto
278
- // en reintentos (permite al consumer forzar nueva sesión borrando /session/creds).
279
- const stored = await this.engine.get('/session/creds');
280
- const creds = (0, store_1.deserialize)(stored) ?? (0, baileys_1.initAuthCreds)();
281
- this.#internals.socket = (0, baileys_1.makeWASocket)({
88
+ const creds = (0, store_1.deserialize)(await engine.get('/session/creds')) ?? (0, baileys_1.initAuthCreds)();
89
+ const socket = (0, baileys_1.makeWASocket)({
282
90
  version,
283
91
  auth: {
284
92
  creds,
@@ -286,950 +94,526 @@ class WhatsApp {
286
94
  get: async (type, ids) => {
287
95
  const data = {};
288
96
  await Promise.all(ids.map(async (id) => {
289
- const value = (0, store_1.deserialize)(await this.engine.get(`/session/${type}/${id}`));
97
+ const value = (0, store_1.deserialize)(await engine.get(`/session/${type}/${id}`));
290
98
  if (value) {
291
- data[id] =
292
- type === 'app-state-sync-key'
293
- ? baileys_1.proto.Message.AppStateSyncKeyData.create(value)
294
- : value;
99
+ data[id] = type === 'app-state-sync-key'
100
+ ? baileys_1.proto.Message.AppStateSyncKeyData.create(value)
101
+ : value;
295
102
  }
296
103
  }));
297
104
  return data;
298
105
  },
299
106
  set: async (data) => {
300
107
  await Promise.all(Object.entries(data).flatMap(([category, entries]) => Object.entries(entries).map(([id, value]) => value != null
301
- ? this.engine.set(`/session/${category}/${id}`, (0, store_1.serialize)(value))
302
- : this.engine.unset(`/session/${category}/${id}`))));
108
+ ? engine.set(`/session/${category}/${id}`, (0, store_1.serialize)(value))
109
+ : engine.unset(`/session/${category}/${id}`))));
303
110
  },
304
111
  },
305
112
  },
306
113
  browser: baileys_1.Browsers.windows('Chrome'),
307
114
  logger: (0, pino_1.default)({ level: 'silent' }),
308
- syncFullHistory: this.#sync,
309
- // Los syncs no-FULL cargan las LID mappings y los tctokens (trusted-contact
310
- // tokens) que rc13 exige para enviar: apagarlos todos con `() => this.#sync`
311
- // deja la sesión sin tctoken y el server rechaza los mensajes con
312
- // "error 463: account restricted or missing tctoken". Por eso se procesan
313
- // siempre los no-FULL; `sync` solo decide si además se trae el historial FULL.
314
- // Non-FULL syncs carry the LID mappings and tctokens rc13 requires to send;
315
- // disabling them all made the server reject messages with error 463. We always
316
- // process non-FULL; `sync` only gates whether FULL history is pulled too.
317
- shouldSyncHistoryMessage: ({ syncType }) => this.#sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
115
+ syncFullHistory: sync,
116
+ shouldSyncHistoryMessage: ({ syncType }) => sync || syncType !== baileys_1.proto.HistorySync.HistorySyncType.FULL,
117
+ // Cuando el receptor no puede descifrar pide un retry; el cache interno de
118
+ // baileys indexa por JID pero el retry llega por LID y no lo encuentra: sin
119
+ // este fallback el mensaje muere en un solo check.
120
+ // When the receiver cannot decrypt it asks for a retry; the internal baileys
121
+ // cache indexes by JID but the retry arrives by LID and misses: without this
122
+ // fallback the message dies at a single check.
123
+ getMessage: async (key) => {
124
+ const found = key.remoteJid && key.id ? await locate(key.remoteJid, key.id) : null;
125
+ return found?.doc.raw.message ?? undefined;
126
+ },
318
127
  markOnlineOnConnect: false,
319
128
  });
320
- const socket = this.#internals.socket;
321
- socket.ev.on('creds.update', () => this.engine.set('/session/creds', (0, store_1.serialize)(creds)));
322
- socket.ev.on('connection.update', async (update) => {
323
- const { connection, lastDisconnect, qr } = update;
324
- if (qr && !creds.registered) {
325
- // Baileys refresca QR periódicamente (~20s). Emitimos nuevo pair
326
- // code / QR en cada refresh para que el usuario pueda renovar si
327
- // el anterior expiró.
328
- // El PIN necesita número: sin `phone` la vinculación es siempre QR, y
329
- // con `phone` manda `method` (OTP por defecto).
330
- // A PIN requires a number: without `phone` linking is always QR, and with
331
- // `phone` the `method` option decides (OTP by default).
332
- if (this.#phone && (this.#method ?? 'otp') === 'otp') {
333
- await callback(await socket.requestPairingCode(this.#phone));
334
- }
335
- else {
336
- await callback(await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
129
+ alive = socket;
130
+ const init = { wa: this, engine, socket };
131
+ this.Contact = (0, contact_1.contact)(init);
132
+ this.Chat = (0, chat_1.chat)(init);
133
+ this.Message = (0, message_1.message)(init);
134
+ this.account = async () => {
135
+ const user = socket.user;
136
+ if (!user)
137
+ return null;
138
+ const id = (0, baileys_1.jidNormalizedUser)(user.id);
139
+ const card = (0, store_1.deserialize)(await engine.get(`/contact/${id}`));
140
+ return new contact_1.Account(init, {
141
+ id,
142
+ phone_number: id,
143
+ lid: user.lid ?? card?.lid ?? null,
144
+ name: user.name ?? card?.name ?? null,
145
+ notify: card?.notify ?? null,
146
+ verified_name: card?.verified_name ?? null,
147
+ img_url: (await socket.profilePictureUrl(id, 'image').catch(() => null)) ?? card?.img_url ?? null,
148
+ status: card?.status ?? null,
149
+ });
150
+ };
151
+ const locate = async (cid, mid) => {
152
+ const lid = cid.endsWith('@lid') ? (0, baileys_1.jidNormalizedUser)(cid) : '';
153
+ const resolved = await (0, store_1.jid_of)(engine, cid, socket);
154
+ for (const candidate of new Set([resolved, cid, lid].filter(Boolean))) {
155
+ const path = `/chat/${candidate}/message/${mid}`;
156
+ const doc = (0, store_1.deserialize)(await engine.get(path));
157
+ if (doc) {
158
+ return { path, doc };
337
159
  }
338
160
  }
161
+ return null;
162
+ };
163
+ socket.ev.on('creds.update', () => engine.set('/session/creds', (0, store_1.serialize)(creds)));
164
+ socket.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
165
+ if (qr && !creds.registered) {
166
+ await callback(digits && (method ?? 'otp') === 'otp'
167
+ ? await socket.requestPairingCode(digits)
168
+ : await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
169
+ }
339
170
  if (connection === 'open') {
340
- this.#has_connected = true;
341
- this.#retry_count = 0;
171
+ connected = true;
172
+ retries = 0;
342
173
  this.emit('connected', this);
343
174
  resolve();
344
175
  }
345
176
  else if (connection === 'close') {
346
- this.#internals.socket = null;
347
- const status_code = lastDisconnect?.error?.output?.statusCode;
348
- // `restartRequired` (515) es una reconexión exigida por el protocolo
349
- // tras el sync inicial — no es un disconnect "real".
350
- const is_transient = status_code === baileys_1.DisconnectReason.restartRequired;
351
- // Limpieza del engine ANTES de emitir `disconnected` para que los
352
- // listeners vean el estado final (engine vaciado o creds borradas).
353
- if (status_code === baileys_1.DisconnectReason.loggedOut) {
354
- if (this.#autoclean) {
355
- await this.engine.clear();
356
- }
357
- else {
358
- await this.engine.unset('/session/creds');
359
- }
177
+ const code = lastDisconnect?.error?.output?.statusCode;
178
+ const transient = code === baileys_1.DisconnectReason.restartRequired;
179
+ if (code === baileys_1.DisconnectReason.loggedOut) {
180
+ await (autoclean ? engine.clear() : engine.unset('/session/creds'));
360
181
  }
361
- // `disconnect({ silent: true })` calla el evento de este cierre concreto.
362
- // `disconnect({ silent: true })` mutes this specific close's event.
363
- if (this.#has_connected && !is_transient && !this.#silent_close) {
182
+ if (connected && !transient && !silent) {
364
183
  this.emit('disconnected', this);
365
184
  }
366
- if (!this.#intentional_close) {
367
- if (status_code === baileys_1.DisconnectReason.loggedOut) {
368
- reject(new Error('Logged out'));
369
- }
370
- else {
371
- const max = this.#reconnect.max;
372
- // Transient closes (restartRequired) son parte del protocolo,
373
- // no cuentan contra el límite de reintentos por fallo.
374
- const exhausted = !is_transient && max !== null && this.#retry_count >= max;
375
- if (exhausted) {
376
- reject(new Error(`Reconnect attempts exhausted (${max})`));
377
- }
378
- else {
379
- if (!is_transient) {
380
- this.#retry_count++;
185
+ if (intentional) {
186
+ /* cierre pedido por disconnect(): sin reintentos / close requested by disconnect(): no retries */
187
+ }
188
+ else if (code === baileys_1.DisconnectReason.loggedOut) {
189
+ reject(new Error('Logged out'));
190
+ }
191
+ else if (!transient && budget !== null && retries >= budget) {
192
+ reject(new Error(`Reconnect attempts exhausted (${budget})`));
193
+ }
194
+ else {
195
+ retries += transient ? 0 : 1;
196
+ timer = setTimeout(() => {
197
+ timer = null;
198
+ start().catch(reject);
199
+ }, transient ? 0 : wait);
200
+ }
201
+ }
202
+ });
203
+ socket.ev.on('messaging-history.set', ({ chats, contacts, messages }) => {
204
+ socket.ev.emit('contacts.upsert', contacts);
205
+ socket.ev.emit('chats.upsert', chats);
206
+ socket.ev.emit('messages.upsert', { messages, type: 'append' });
207
+ });
208
+ socket.ev.on('contacts.upsert', (rows) => {
209
+ chain = chain.then(async () => {
210
+ for (const row of rows) {
211
+ if (row.id) {
212
+ const current = (0, store_1.deserialize)(await engine.get(`/contact/${row.id}`));
213
+ const doc = {
214
+ id: row.id,
215
+ lid: row.lid ?? current?.lid ?? null,
216
+ name: row.name ?? current?.name ?? null,
217
+ notify: row.notify ?? current?.notify ?? null,
218
+ verified_name: row.verifiedName ?? current?.verified_name ?? null,
219
+ img_url: (typeof row.imgUrl === 'string' ? row.imgUrl : null) ?? current?.img_url ?? null,
220
+ status: row.status ?? current?.status ?? null,
221
+ };
222
+ if (!current || JSON.stringify(current) !== JSON.stringify(doc)) {
223
+ await engine.set(`/contact/${row.id}`, (0, store_1.serialize)(doc));
224
+ if (doc.lid) {
225
+ await engine.set(`/lid/${doc.lid}`, (0, store_1.serialize)(doc.id));
381
226
  }
382
- const delay = is_transient ? 0 : this.#reconnect.interval_ms;
383
- this.#retry_timer = setTimeout(() => {
384
- this.#retry_timer = null;
385
- start().catch(reject);
386
- }, delay);
227
+ const person = new this.Contact(doc);
228
+ const owner = (0, store_1.deserialize)(await engine.get(`/chat/${doc.id}`));
229
+ this.emit(current ? 'contact:updated' : 'contact:created', person, new this.Chat(owner ?? { id: doc.id, name: person.name }), this);
387
230
  }
388
231
  }
389
232
  }
390
- }
391
- });
392
- this.#attach_business_handlers(socket);
393
- };
394
- start().catch(reject);
395
- });
396
- }
397
- /**
398
- * Actualiza el perfil de la cuenta en WhatsApp: nombre público, bio y/o foto. Sólo se
399
- * envía lo que llega en el parche; `photo: null` elimina la foto actual.
400
- * Updates the account profile on WhatsApp: public name, bio and/or picture. Only the
401
- * given fields are sent; `photo: null` removes the current picture.
402
- *
403
- * @param patch - Campos a actualizar / Fields to update
404
- * @returns true si todo lo pedido se envió / true when everything requested was sent
405
- * @throws ERR_PROFILE_PICTURE_LIB si falta `sharp` o `jimp` para procesar la foto / when `sharp` or `jimp` is missing to process the picture
406
- */
407
- async profile(patch) {
408
- const socket = this.#internals.socket;
409
- let ok = false;
410
- if (socket) {
411
- const self = (0, baileys_1.jidNormalizedUser)(socket.user?.id ?? '');
412
- if (patch.name !== undefined) {
413
- await socket.updateProfileName(patch.name);
414
- }
415
- if (patch.content !== undefined) {
416
- await socket.updateProfileStatus(patch.content);
417
- }
418
- if (patch.photo === null) {
419
- await socket.removeProfilePicture(self);
420
- }
421
- else if (patch.photo !== undefined) {
422
- // baileys redimensiona la foto con `sharp` o `jimp`; ninguna es dependencia
423
- // nuestra, así que su ausencia se traduce a un error accionable.
424
- // baileys resizes the picture with `sharp` or `jimp`; neither is a dependency
425
- // of ours, so their absence is translated into an actionable error.
426
- await socket
427
- .updateProfilePicture(self, typeof patch.photo === 'string' ? { url: patch.photo } : patch.photo)
428
- .catch((error) => {
429
- if (/image processing library/i.test(error.message)) {
430
- throw new Error('ERR_PROFILE_PICTURE_LIB');
431
- }
432
- throw error;
233
+ }).catch(() => { });
433
234
  });
434
- }
435
- ok = true;
436
- }
437
- return ok;
438
- }
439
- /**
440
- * Publica un estado (status broadcast). Con sólo `caption` publica texto; con `content`
441
- * publica imagen o video (el tipo se deduce del binario) usando `caption` como pie.
442
- * `contacts` es la audiencia: WhatsApp no reparte el estado a quien no esté en la lista.
443
- * Publishes a status broadcast. With only `caption` it posts text; with `content` it
444
- * posts an image or video (type inferred from the binary) using `caption` as its footer.
445
- * `contacts` is the audience: WhatsApp does not deliver the status to anyone outside it.
446
- *
447
- * @param post - Contenido, pie y audiencia / Content, caption and audience
448
- * @returns Publicación creada, o null si no hay sesión / Created post, or null without a session
449
- * @throws ERR_FEED_EMPTY sin `content` ni `caption` / when neither `content` nor `caption` is given
450
- * @throws ERR_FEED_MEDIA si el binario no es imagen ni video / when the binary is neither image nor video
451
- */
452
- async feed(post) {
453
- const socket = this.#internals.socket;
454
- let result = null;
455
- if (socket) {
456
- const audience = (await Promise.all(post.contacts.map((uid) => this.#resolve_jid(String(uid))))).filter((jid) => jid !== null);
457
- const mime = post.content ? sniff_media(post.content) : null;
458
- if (post.content && !mime) {
459
- throw new Error('ERR_FEED_MEDIA');
460
- }
461
- if (!post.content && !post.caption) {
462
- throw new Error('ERR_FEED_EMPTY');
463
- }
464
- const kind = mime?.startsWith('video/') ? 'video' : mime ? 'image' : 'text';
465
- const sent = await socket.sendMessage('status@broadcast', (post.content
466
- ? { [kind]: post.content, caption: post.caption }
467
- : { text: post.caption }), { statusJidList: audience });
468
- if (sent?.key?.id) {
469
- const created_at = (Number(sent.messageTimestamp) || Math.floor(Date.now() / 1_000)) * 1_000;
470
- const doc = {
471
- id: sent.key.id,
472
- author_jid: (0, baileys_1.jidNormalizedUser)(socket.user?.id ?? ''),
473
- type: kind,
474
- caption: post.caption ?? '',
475
- mime: mime ?? 'text/plain',
476
- created_at,
477
- expires_at: created_at + status_1.TTL_MS,
478
- viewed: true,
479
- raw: sent,
480
- };
481
- await this.engine.set(`/status/${doc.id}`, (0, store_1.serialize)(doc), created_at);
482
- if (post.content) {
483
- await this.#write_content(`/status/${doc.id}/content`, post.content);
484
- }
485
- result = new status_1.Feed(this, doc);
486
- this.emit('feed:created', result, this);
487
- }
488
- }
489
- return result;
490
- }
491
- /**
492
- * Cierra la conexión. Con `destroy: true` vacía el engine completo.
493
- * Closes the connection. With `destroy: true` clears the engine entirely.
494
- */
495
- async disconnect(options = {}) {
496
- this.#intentional_close = true;
497
- this.#silent_close = options.silent === true;
498
- // Cancela cualquier retry programado por un close anterior, para no resucitar
499
- // el socket después de una desconexión manual.
500
- if (this.#retry_timer) {
501
- clearTimeout(this.#retry_timer);
502
- this.#retry_timer = null;
503
- }
504
- if (this.#internals.socket) {
505
- try {
506
- // Pasa un error Boom-like con statusCode=connectionClosed (428) para que
507
- // `lastDisconnect.error.output.statusCode` quede explícito en el close
508
- // en lugar de `undefined`.
509
- const intentional = Object.assign(new Error('intentional close'), {
510
- output: { statusCode: baileys_1.DisconnectReason.connectionClosed },
235
+ socket.ev.on('contacts.update', (rows) => {
236
+ chain = chain.then(async () => {
237
+ for (const row of rows) {
238
+ const current = row.id ? (0, store_1.deserialize)(await engine.get(`/contact/${row.id}`)) : null;
239
+ const patch = {
240
+ ...(row.notify && { notify: row.notify }),
241
+ ...(row.name && { name: row.name }),
242
+ ...(row.verifiedName && { verified_name: row.verifiedName }),
243
+ ...(typeof row.imgUrl === 'string' && { img_url: row.imgUrl }),
244
+ ...(row.status && { status: row.status }),
245
+ ...(row.lid && { lid: row.lid }),
246
+ };
247
+ if (current && row.id && Object.keys(patch).length > 0) {
248
+ const doc = { ...current, ...patch };
249
+ await engine.set(`/contact/${row.id}`, (0, store_1.serialize)(doc));
250
+ if (patch.lid) {
251
+ await engine.set(`/lid/${patch.lid}`, (0, store_1.serialize)(row.id));
252
+ }
253
+ const person = new this.Contact(doc);
254
+ const owner = (0, store_1.deserialize)(await engine.get(`/chat/${row.id}`));
255
+ this.emit('contact:updated', person, new this.Chat(owner ?? { id: row.id, name: person.name }), this);
256
+ }
257
+ }
258
+ }).catch(() => { });
511
259
  });
512
- await this.#internals.socket.end(intentional);
513
- }
514
- catch {
515
- /* socket may already be closed */
516
- }
517
- this.#internals.socket = null;
518
- }
519
- if (options.destroy) {
520
- await this.engine.clear();
521
- }
522
- }
523
- /**
524
- * Conecta los handlers de eventos de baileys: contactos, chats y mensajes.
525
- * Wires baileys event handlers: contacts, chats and messages.
526
- *
527
- * @internal
528
- */
529
- #attach_business_handlers(socket) {
530
- socket.ev.on('messaging-history.set', ({ chats, contacts, messages }) => {
531
- this.#enqueue(async () => {
532
- await this.#handle_contacts_upsert(contacts);
533
- await this.#handle_chats_upsert(chats);
534
- await this.#handle_messages_upsert(messages);
535
- });
536
- });
537
- socket.ev.on('contacts.upsert', (contacts) => {
538
- this.#enqueue(() => this.#handle_contacts_upsert(contacts));
539
- });
540
- socket.ev.on('contacts.update', (contacts) => {
541
- this.#enqueue(() => this.#handle_contacts_update(contacts));
542
- });
543
- socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
544
- this.#enqueue(() => this.#handle_lid_mapping(lid, pn));
545
- });
546
- socket.ev.on('chats.upsert', (chats) => {
547
- this.#enqueue(() => this.#handle_chats_upsert(chats));
548
- });
549
- socket.ev.on('chats.update', (chats) => {
550
- this.#enqueue(() => this.#handle_chats_update(chats));
551
- });
552
- socket.ev.on('chats.delete', (ids) => {
553
- this.#enqueue(() => this.#handle_chats_delete(ids));
554
- });
555
- socket.ev.on('messages.upsert', ({ messages }) => {
556
- this.#enqueue(() => this.#handle_messages_upsert(messages));
557
- });
558
- socket.ev.on('messages.update', (updates) => {
559
- this.#enqueue(() => this.#handle_messages_update(updates));
560
- });
561
- socket.ev.on('message-receipt.update', (updates) => {
562
- this.#enqueue(() => this.#handle_message_receipt(updates));
563
- });
564
- // Las reacciones llegan duplicadas por `messages.reaction` Y `messages.upsert`
565
- // (como `reactionMessage`). Se usa solo `messages.upsert` para evitar el doble disparo.
566
- // socket.ev.on('messages.reaction', (reactions) => {
567
- // void this.#handle_messages_reaction(reactions);
568
- // });
569
- }
570
- /**
571
- * Persiste un contacto y su índice LID; emite `contact:created` solo si es nuevo.
572
- * Persists a contact and its LID index; emits `contact:created` only when new.
573
- *
574
- * @param raw - Documento del contacto a persistir / Contact document to persist
575
- * @internal
576
- */
577
- async #persist_contact(raw) {
578
- const current = (0, store_1.deserialize)(await this.engine.get(`/contact/${raw.id}`));
579
- // Los upserts del re-sync llegan con los campos vacíos: sin fusionar borran el nombre
580
- // que ya se conocía y el chat pasa a mostrar el número pelado.
581
- // Re-sync upserts arrive with empty fields: without merging they wipe the name already
582
- // known and the chat falls back to showing the bare number.
583
- const doc = current
584
- ? {
585
- id: raw.id,
586
- lid: raw.lid ?? current.lid,
587
- name: raw.name ?? current.name,
588
- notify: raw.notify ?? current.notify,
589
- verified_name: raw.verified_name ?? current.verified_name,
590
- img_url: raw.img_url ?? current.img_url,
591
- status: raw.status ?? current.status,
592
- }
593
- : raw;
594
- await this.engine.set(`/contact/${raw.id}`, (0, store_1.serialize)(doc));
595
- if (doc.lid) {
596
- await this.engine.set(`/lid/${doc.lid}`, (0, store_1.serialize)(doc.id));
597
- }
598
- // Una ficha que existía vacía y ahora tiene nombre es un cambio que el consumidor
599
- // necesita: sin avisar, quien memorice el contacto sigue mostrando el número.
600
- // A card that existed empty and now has a name is a change the consumer needs: without
601
- // notifying, whoever memoized the contact keeps showing the bare number.
602
- const changed = current && ['lid', 'name', 'notify', 'verified_name', 'img_url', 'status'].some((key) => current[key] !== doc[key]);
603
- if (!current || changed) {
604
- const person = new this.Contact(doc);
605
- const cached_chat = (0, store_1.deserialize)(await this.engine.get(`/chat/${doc.id}`));
606
- const chat = new this.Chat(cached_chat ?? { id: doc.id, name: person.name });
607
- this.emit(current ? 'contact:updated' : 'contact:created', person, chat, this);
608
- }
609
- }
610
- /** @internal */
611
- async #handle_contacts_upsert(contacts) {
612
- for (const c of contacts) {
613
- if (c.id) {
614
- await this.#persist_contact({
615
- id: c.id,
616
- lid: c.lid ?? null,
617
- name: c.name ?? null,
618
- notify: c.notify ?? null,
619
- verified_name: c.verifiedName ?? null,
620
- img_url: c.imgUrl ?? null,
621
- status: c.status ?? null,
260
+ socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
261
+ chain = chain.then(async () => {
262
+ await engine.set(`/lid/${lid}`, (0, store_1.serialize)(pn));
263
+ await engine.set(`/lid/${pn}`, (0, store_1.serialize)(lid));
264
+ }).catch(() => { });
622
265
  });
623
- }
624
- }
625
- }
626
- /** @internal */
627
- async #handle_contacts_update(contacts) {
628
- for (const c of contacts) {
629
- if (c.id) {
630
- const current = (0, store_1.deserialize)(await this.engine.get(`/contact/${c.id}`));
631
- if (current) {
632
- const patch = {
633
- ...(c.notify && { notify: c.notify }),
634
- ...(c.name && { name: c.name }),
635
- ...(c.verifiedName && { verified_name: c.verifiedName }),
636
- ...(c.imgUrl && { img_url: c.imgUrl }),
637
- ...(c.status && { status: c.status }),
638
- ...(c.lid && { lid: c.lid }),
639
- };
640
- if (Object.keys(patch).length > 0) {
641
- const merged = { ...current, ...patch };
642
- await this.engine.set(`/contact/${c.id}`, (0, store_1.serialize)(merged));
643
- if (patch.lid) {
644
- await this.engine.set(`/lid/${patch.lid}`, (0, store_1.serialize)(c.id));
645
- }
646
- const person = new this.Contact(merged);
647
- const cached_chat = (0, store_1.deserialize)(await this.engine.get(`/chat/${c.id}`));
648
- this.emit('contact:updated', person, new this.Chat(cached_chat ?? { id: c.id, name: person.name }), this);
649
- }
650
- }
651
- }
652
- }
653
- }
654
- /** @internal */
655
- async #handle_lid_mapping(lid, pn) {
656
- await this.engine.set(`/lid/${lid}`, (0, store_1.serialize)(pn));
657
- await this.engine.set(`/lid/${pn}`, (0, store_1.serialize)(lid));
658
- }
659
- /**
660
- * Actividad del chat según su último mensaje persistido. Es el respaldo para los
661
- * documentos que se guardaron antes de que el chat llevara su propia marca.
662
- * Chat activity from its last persisted message. It is the fallback for documents stored
663
- * before the chat carried its own stamp.
664
- *
665
- * @param cid - Identificador del chat / Chat identifier
666
- * @returns Epoch ms del último mensaje, o 0 si el chat no tiene ninguno / Last message epoch ms, or 0 when the chat has none
667
- * @internal
668
- */
669
- async #activity(cid) {
670
- const [raw] = await this.engine.list(`/chat/${cid}/message`, 0, 1);
671
- return (0, store_1.deserialize)(raw ?? null)?.created_at ?? 0;
672
- }
673
- /** @internal */
674
- async #handle_chats_upsert(chats) {
675
- for (const ch of chats) {
676
- if (ch.id) {
677
- const current = (0, store_1.deserialize)(await this.engine.get(`/chat/${ch.id}`));
678
- const raw = current ?? {
679
- id: ch.id,
680
- name: ch.name ?? null,
681
- archived: ch.archived ?? null,
682
- pinned: ch.pinned ?? null,
683
- mute_end_time: ch.muteEndTime != null ? Number(ch.muteEndTime) : null,
684
- unread_count: ch.unreadCount ?? null,
685
- };
686
- if (ch.name) {
687
- raw.name = ch.name;
688
- }
689
- // El sync trae la última actividad del chat; sin ella la lista quedaría ordenada
690
- // por el momento en que se escribió cada documento.
691
- // The sync carries the chat's last activity; without it the list would be ordered
692
- // by the moment each document happened to be written.
693
- const stamp = ch.conversationTimestamp != null ? Number(ch.conversationTimestamp) * 1_000 : null;
694
- raw.activity = Math.max(stamp ?? 0, raw.activity ?? 0, await this.#activity(ch.id)) || null;
695
- await this.engine.set(`/chat/${ch.id}`, (0, store_1.serialize)(raw), raw.activity ?? 0);
696
- if (current === null) {
697
- this.emit('chat:created', new this.Chat(raw), this);
698
- }
699
- }
700
- }
701
- }
702
- /** @internal */
703
- async #handle_chats_update(chats) {
704
- for (const ch of chats) {
705
- if (ch.id && ch.id !== 'status@broadcast') {
706
- const current = (0, store_1.deserialize)(await this.engine.get(`/chat/${ch.id}`)) ?? {
707
- id: ch.id,
708
- name: ch.name ?? null,
709
- };
710
- const patch = {};
711
- const pinned_changed = 'pinned' in ch;
712
- const archived_changed = ch.archived !== undefined;
713
- const mute_changed = 'muteEndTime' in ch;
714
- if (ch.name) {
715
- patch.name = ch.name;
716
- }
717
- if (pinned_changed) {
718
- patch.pinned = ch.pinned ?? null;
719
- }
720
- if (archived_changed) {
721
- patch.archived = ch.archived ?? false;
722
- }
723
- if (mute_changed) {
724
- patch.mute_end_time = ch.muteEndTime != null ? Number(ch.muteEndTime) : null;
725
- }
726
- if (ch.unreadCount != null) {
727
- patch.unread_count = ch.unreadCount;
728
- }
729
- if (Object.keys(patch).length > 0) {
730
- const merged = { ...current, ...patch };
731
- // Fijar, archivar o silenciar no es actividad: el chat conserva su posición.
732
- // Pinning, archiving or muting is not activity: the chat keeps its position.
733
- await this.engine.set(`/chat/${ch.id}`, (0, store_1.serialize)(merged), merged.activity ?? undefined);
734
- if (pinned_changed) {
735
- this.emit(ch.pinned != null ? 'chat:pinned' : 'chat:unpinned', new this.Chat(merged), this);
736
- }
737
- if (archived_changed) {
738
- this.emit(ch.archived ? 'chat:archived' : 'chat:unarchived', new this.Chat(merged), this);
739
- }
740
- if (mute_changed) {
741
- const is_muted = patch.mute_end_time != null && patch.mute_end_time > Date.now();
742
- this.emit(is_muted ? 'chat:muted' : 'chat:unmuted', new this.Chat(merged), this);
743
- }
744
- }
745
- }
746
- }
747
- }
748
- /** @internal */
749
- async #handle_chats_delete(ids) {
750
- for (const cid of ids) {
751
- const raw = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}`)) ?? { id: cid };
752
- await this.engine.unset(`/chat/${cid}`);
753
- this.emit('chat:deleted', new this.Chat(raw), this);
754
- }
755
- }
756
- /** @internal */
757
- async #handle_message_receipt(updates) {
758
- for (const { key, receipt } of updates) {
759
- // Receipt sobre status@broadcast → marca el feed como visto y emite feed:updated.
760
- // Receipt on status@broadcast → marks feed viewed and emits feed:updated.
761
- if (key.remoteJid === 'status@broadcast' && key.id) {
762
- const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${key.id}`));
763
- if (feed_raw && !feed_raw.viewed) {
764
- feed_raw.viewed = true;
765
- await this.engine.set(`/status/${key.id}`, (0, store_1.serialize)(feed_raw));
766
- this.emit('feed:updated', new status_1.Feed(this, feed_raw), this);
767
- }
768
- continue;
769
- }
770
- if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
771
- const found = await this.#locate(key.remoteJid, key.id);
772
- if (found) {
773
- const { path, doc } = found;
774
- const next = receipt.playedTimestamp != null ? PLAYED : READ;
775
- if (doc.status < next) {
776
- doc.status = next;
777
- doc.raw.status = next;
778
- await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
779
- }
780
- const msg_instance = (0, message_1.message)(this, doc);
781
- this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
782
- }
783
- }
784
- }
785
- }
786
- /** @internal */
787
- async #handle_messages_upsert(messages) {
788
- for (const msg of messages) {
789
- if (msg.key?.remoteJid && msg.key.id) {
790
- const cid = msg.key.remoteJidAlt ?? msg.key.remoteJid;
791
- const mid = msg.key.id;
792
- const content_type = (0, baileys_1.getContentType)(msg.message ?? {});
793
- if (content_type === 'reactionMessage') {
794
- // Canal único para reacciones: se procesa aquí y se ignora `messages.reaction`.
795
- // Single channel for reactions: handled here; `messages.reaction` is disabled.
796
- const reaction = msg.message?.reactionMessage;
797
- if (reaction?.key?.id && reaction.key.remoteJid) {
798
- const target_cid = (await this.#resolve_jid(reaction.key.remoteJid)) ?? reaction.key.remoteJid;
799
- await this.#handle_messages_reaction([{
800
- key: {
801
- remoteJid: target_cid,
802
- id: reaction.key.id,
803
- participant: msg.key.fromMe ? (this.#internals.socket?.user?.id ?? null) : (msg.key.participant ?? cid),
804
- },
805
- reaction: { text: reaction.text ?? '' },
806
- }]);
807
- }
808
- continue;
809
- }
810
- // Status broadcast — flujo dedicado. Nunca emite `message:*`.
811
- // Status broadcast — dedicated flow. Never emits `message:*`.
812
- if (msg.key.remoteJid === 'status@broadcast') {
813
- if (content_type === 'protocolMessage') {
814
- const protocol = msg.message?.protocolMessage;
815
- if (protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE &&
816
- protocol.key?.id) {
817
- const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${protocol.key.id}`));
818
- if (feed_raw) {
819
- await this.engine.unset(`/status/${protocol.key.id}`);
820
- this.emit('feed:deleted', new status_1.Feed(this, feed_raw), this);
266
+ socket.ev.on('chats.upsert', (rows) => {
267
+ chain = chain.then(async () => {
268
+ for (const row of rows) {
269
+ if (row.id) {
270
+ const current = (0, store_1.deserialize)(await engine.get(`/chat/${row.id}`));
271
+ const doc = current ?? {
272
+ id: row.id,
273
+ name: row.name ?? null,
274
+ archived: row.archived ?? null,
275
+ pinned: row.pinned ?? null,
276
+ mute_end_time: row.muteEndTime != null ? Number(row.muteEndTime) : null,
277
+ unread_count: row.unreadCount ?? null,
278
+ };
279
+ if (row.name) {
280
+ doc.name = row.name;
281
+ }
282
+ const [newest] = await engine.list(`/chat/${row.id}/message`, 0, 1);
283
+ doc.activity = Math.max(row.conversationTimestamp != null ? Number(row.conversationTimestamp) * 1_000 : 0, doc.activity ?? 0, (0, store_1.deserialize)(newest ?? null)?.created_at ?? 0) || null;
284
+ await engine.set(`/chat/${row.id}`, (0, store_1.serialize)(doc), doc.activity ?? 0);
285
+ if (!current) {
286
+ this.emit('chat:created', new this.Chat(doc), this);
287
+ }
821
288
  }
822
289
  }
823
- continue;
824
- }
825
- const FEED_TYPE_MAP = {
826
- conversation: 'text',
827
- extendedTextMessage: 'text',
828
- imageMessage: 'image',
829
- videoMessage: 'video',
830
- audioMessage: 'audio',
831
- };
832
- const feed_type = FEED_TYPE_MAP[content_type ?? ''];
833
- const author = msg.key.participant ?? '';
834
- if (!feed_type || !author) {
835
- continue;
836
- }
837
- const msg_content = msg.message?.[content_type];
838
- let caption = '';
839
- let mime = 'text/plain';
840
- if (typeof msg_content === 'string') {
841
- caption = msg_content;
842
- }
843
- else if (msg_content && typeof msg_content === 'object') {
844
- caption =
845
- msg_content.caption ??
846
- msg_content.text ??
847
- '';
848
- if (feed_type !== 'text') {
849
- mime = msg_content.mimetype ?? 'application/octet-stream';
850
- }
851
- }
852
- const created_at = (Number(msg.messageTimestamp) || Math.floor(Date.now() / 1000)) * 1000;
853
- const feed_raw = {
854
- id: mid,
855
- author_jid: author,
856
- type: feed_type,
857
- caption,
858
- mime,
859
- created_at,
860
- expires_at: created_at + status_1.TTL_MS,
861
- viewed: false,
862
- raw: msg,
863
- };
864
- let content_buf = Buffer.alloc(0);
865
- if (feed_type === 'text') {
866
- content_buf = Buffer.from(caption, 'utf-8');
867
- }
868
- else if (this.#internals.socket) {
869
- try {
870
- const buf = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {});
871
- if (Buffer.isBuffer(buf)) {
872
- content_buf = buf;
290
+ }).catch(() => { });
291
+ });
292
+ socket.ev.on('chats.update', (rows) => {
293
+ chain = chain.then(async () => {
294
+ for (const row of rows) {
295
+ if (row.id && row.id !== 'status@broadcast') {
296
+ const current = (0, store_1.deserialize)(await engine.get(`/chat/${row.id}`)) ?? { id: row.id, name: row.name ?? null };
297
+ const patch = {};
298
+ const events = [];
299
+ if (row.name) {
300
+ patch.name = row.name;
301
+ }
302
+ if (row.unreadCount != null) {
303
+ patch.unread_count = row.unreadCount;
304
+ }
305
+ if ('pinned' in row) {
306
+ patch.pinned = row.pinned ?? null;
307
+ events.push(row.pinned != null ? 'chat:pinned' : 'chat:unpinned');
308
+ }
309
+ if (row.archived !== undefined) {
310
+ patch.archived = row.archived ?? false;
311
+ events.push(row.archived ? 'chat:archived' : 'chat:unarchived');
312
+ }
313
+ if ('muteEndTime' in row) {
314
+ patch.mute_end_time = row.muteEndTime != null ? Number(row.muteEndTime) : null;
315
+ events.push(patch.mute_end_time != null && patch.mute_end_time > Date.now() ? 'chat:muted' : 'chat:unmuted');
316
+ }
317
+ if (Object.keys(patch).length > 0) {
318
+ const doc = { ...current, ...patch };
319
+ await engine.set(`/chat/${row.id}`, (0, store_1.serialize)(doc), doc.activity ?? undefined);
320
+ for (const event of events) {
321
+ this.emit(event, new this.Chat(doc), this);
322
+ }
323
+ }
873
324
  }
874
325
  }
875
- catch {
876
- /* media download may fail */
326
+ }).catch(() => { });
327
+ });
328
+ socket.ev.on('chats.delete', (ids) => {
329
+ chain = chain.then(async () => {
330
+ for (const cid of ids) {
331
+ const doc = (0, store_1.deserialize)(await engine.get(`/chat/${cid}`)) ?? { id: cid };
332
+ await engine.unset(`/chat/${cid}`);
333
+ this.emit('chat:deleted', new this.Chat(doc), this);
877
334
  }
878
- }
879
- await this.engine.set(`/status/${mid}`, (0, store_1.serialize)(feed_raw));
880
- if (content_buf.length > 0) {
881
- await this.#write_content(`/status/${mid}/content`, content_buf);
882
- }
883
- this.emit('feed:created', new status_1.Feed(this, feed_raw), this);
884
- continue;
885
- }
886
- if (content_type === 'pollUpdateMessage') {
887
- const update = msg.message?.pollUpdateMessage;
888
- const creation_key = update?.pollCreationMessageKey;
889
- if (creation_key?.id &&
890
- creation_key.remoteJid &&
891
- update?.vote?.encPayload &&
892
- update.vote.encIv) {
893
- const resolved_cid = (await this.#resolve_jid(creation_key.remoteJid)) ?? creation_key.remoteJid;
894
- const target_mid = creation_key.id;
895
- const poll_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${resolved_cid}/message/${target_mid}`));
896
- const secret_raw = poll_doc?.raw.message?.messageContextInfo?.messageSecret;
897
- const message_secret = typeof secret_raw === 'string' ? Buffer.from(secret_raw, 'base64') : secret_raw;
898
- if (poll_doc && message_secret) {
899
- try {
900
- const poll_key = poll_doc.raw.key ?? {};
901
- // La identidad propia del HMAC depende del addressing del chat
902
- // (LID en @lid, PN en @s.whatsapp.net), así que para las posiciones
903
- // fromMe se intenta descifrar con ambas: AES-GCM autentica, la
904
- // clave equivocada lanza y se prueba la siguiente.
905
- // Own HMAC identity depends on chat addressing (LID on @lid, PN on
906
- // @s.whatsapp.net), so fromMe positions try both candidates:
907
- // AES-GCM authenticates, a wrong key throws and the next is tried.
908
- const self_id = this.#internals.socket?.user?.id ?? '';
909
- const self_lid = this.#internals.socket?.user?.lid ?? '';
910
- const selves = [...new Set([self_lid, self_id].filter(Boolean))];
911
- // Candidatos foráneos: todas las formas de identidad del key (LID,
912
- // participant, alt, remoteJid); se prueban todas porque el addressing
913
- // del stanza varía (LID vs PN) según la migración del contacto.
914
- const foreign_of = (k) => [...new Set([k.remoteJid, k.participant, k.remoteJidAlt, k.remoteJid].filter((x) => Boolean(x)))];
915
- const voters = msg.key.fromMe ? selves : foreign_of(msg.key);
916
- const creators = poll_key.fromMe ? selves : foreign_of(poll_key);
917
- let decrypted = null;
918
- for (const voter of voters) {
919
- for (const creator of creators) {
335
+ }).catch(() => { });
336
+ });
337
+ socket.ev.on('messages.upsert', ({ messages }) => {
338
+ chain = chain.then(async () => {
339
+ for (const msg of messages) {
340
+ const cid = msg.key?.remoteJidAlt ?? msg.key?.remoteJid;
341
+ const mid = msg.key?.id;
342
+ if (!cid || !mid) {
343
+ continue;
344
+ }
345
+ const kind = (0, baileys_1.getContentType)(msg.message ?? {});
346
+ if (kind === 'reactionMessage') {
347
+ const target = msg.message?.reactionMessage;
348
+ const found = target?.key?.id && target.key.remoteJid ? await locate(target.key.remoteJid, target.key.id) : null;
349
+ if (found && target) {
350
+ const author = (0, baileys_1.jidNormalizedUser)((msg.key.fromMe ? socket.user?.id : msg.key.participant ?? cid) ?? cid);
351
+ const emoji = target.text ?? '';
352
+ found.doc.reactions = [
353
+ ...(found.doc.reactions ?? []).filter((entry) => entry.author !== author),
354
+ ...(emoji ? [{ author, emoji, at: Date.now() }] : []),
355
+ ];
356
+ await engine.set(found.path, (0, store_1.serialize)(found.doc), found.doc.created_at);
357
+ const instance = new message_1.default(init, found.doc);
358
+ this.emit('message:reacted', instance, await instance.chat(), emoji, this);
359
+ }
360
+ continue;
361
+ }
362
+ if (msg.key.remoteJid === 'status@broadcast') {
363
+ const revoked = kind === 'protocolMessage' && msg.message?.protocolMessage?.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE
364
+ ? msg.message.protocolMessage.key?.id
365
+ : null;
366
+ if (revoked) {
367
+ const gone = (0, store_1.deserialize)(await engine.get(`/status/${revoked}`));
368
+ if (gone) {
369
+ await engine.unset(`/status/${revoked}`);
370
+ this.emit('feed:deleted', new status_1.Feed(init, gone), this);
371
+ }
372
+ continue;
373
+ }
374
+ const type = { conversation: 'text', extendedTextMessage: 'text', imageMessage: 'image', videoMessage: 'video', audioMessage: 'audio' }[kind ?? ''];
375
+ const author = msg.key.participant ?? '';
376
+ if (!type || !author) {
377
+ continue;
378
+ }
379
+ const body = msg.message?.[kind];
380
+ const caption = typeof body === 'string' ? body : (body?.caption ?? body?.text ?? '');
381
+ const created_at = (Number(msg.messageTimestamp) || Math.floor(Date.now() / 1_000)) * 1_000;
382
+ const doc = {
383
+ id: mid,
384
+ author_jid: author,
385
+ type,
386
+ caption,
387
+ mime: type === 'text' ? 'text/plain' : ((typeof body === 'object' && body?.mimetype) || 'application/octet-stream'),
388
+ created_at,
389
+ expires_at: created_at + status_1.TTL_MS,
390
+ viewed: false,
391
+ raw: msg,
392
+ };
393
+ const binary = type === 'text'
394
+ ? Buffer.from(caption, 'utf-8')
395
+ : await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {}).catch(() => Buffer.alloc(0));
396
+ await engine.set(`/status/${mid}`, (0, store_1.serialize)(doc));
397
+ if (binary.length > 0) {
398
+ await (engine.set_buffer?.(`/status/${mid}/content`, binary) ?? engine.set(`/status/${mid}/content`, (0, store_1.serialize)({ data: binary.toString('base64') })));
399
+ }
400
+ this.emit('feed:created', new status_1.Feed(init, doc), this);
401
+ continue;
402
+ }
403
+ if (kind === 'pollUpdateMessage') {
404
+ const key = msg.message?.pollUpdateMessage?.pollCreationMessageKey;
405
+ const vote = msg.message?.pollUpdateMessage?.vote;
406
+ const found = key?.id && key.remoteJid ? await locate(key.remoteJid, key.id) : null;
407
+ const raw_secret = found?.doc.raw.message?.messageContextInfo?.messageSecret;
408
+ const secret = typeof raw_secret === 'string' ? Buffer.from(raw_secret, 'base64') : raw_secret;
409
+ if (found && secret && vote?.encPayload && vote.encIv) {
410
+ const mine = [socket.user?.lid, socket.user?.id];
411
+ const theirs = (from) => [from.remoteJid, from.participant, from.remoteJidAlt];
412
+ const voters = (msg.key.fromMe ? mine : theirs(msg.key)).filter((id) => Boolean(id));
413
+ const creators = (found.doc.raw.key?.fromMe ? mine : theirs(found.doc.raw.key ?? {})).filter((id) => Boolean(id));
414
+ for (const pair of voters.flatMap((who) => creators.map((creator) => [who, creator]))) {
920
415
  try {
921
- decrypted = (0, baileys_1.decryptPollVote)({ encPayload: update.vote.encPayload, encIv: update.vote.encIv }, {
922
- pollCreatorJid: (0, baileys_1.jidNormalizedUser)(creator),
923
- pollMsgId: target_mid,
924
- pollEncKey: message_secret,
925
- voterJid: (0, baileys_1.jidNormalizedUser)(voter),
416
+ (0, baileys_1.updateMessageWithPollUpdate)(found.doc.raw, {
417
+ pollUpdateMessageKey: msg.key,
418
+ vote: (0, baileys_1.decryptPollVote)({ encPayload: vote.encPayload, encIv: vote.encIv }, {
419
+ pollCreatorJid: (0, baileys_1.jidNormalizedUser)(pair[1]),
420
+ pollMsgId: found.doc.id,
421
+ pollEncKey: secret,
422
+ voterJid: (0, baileys_1.jidNormalizedUser)(pair[0]),
423
+ }),
424
+ senderTimestampMs: Number(msg.messageTimestamp) || Date.now(),
926
425
  });
426
+ await engine.set(found.path, (0, store_1.serialize)(found.doc), found.doc.created_at);
427
+ const instance = new message_1.default(init, found.doc);
428
+ this.emit('message:updated', instance, await instance.chat(), this);
927
429
  break;
928
430
  }
929
431
  catch {
930
- /* identidad equivocada: probar la siguiente */
432
+ /* identidad equivocada / wrong identity */
931
433
  }
932
434
  }
933
- if (decrypted) {
934
- break;
935
- }
936
435
  }
937
- if (decrypted) {
938
- (0, baileys_1.updateMessageWithPollUpdate)(poll_doc.raw, {
939
- pollUpdateMessageKey: msg.key,
940
- vote: decrypted,
941
- senderTimestampMs: Number(msg.messageTimestamp) || Date.now(),
942
- });
943
- await this.engine.set(`/chat/${resolved_cid}/message/${target_mid}`, (0, store_1.serialize)(poll_doc), poll_doc.created_at);
944
- const msg_instance = (0, message_1.message)(this, poll_doc);
945
- this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
436
+ continue;
437
+ }
438
+ if (kind === 'protocolMessage') {
439
+ const protocol = msg.message?.protocolMessage;
440
+ const found = protocol?.key?.id ? await locate(protocol.key.remoteJid ?? cid, protocol.key.id) : null;
441
+ if (found && protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
442
+ found.doc.raw.message = protocol.editedMessage;
443
+ found.doc.edited = true;
444
+ found.doc.caption = new message_1.default(init, found.doc.raw).caption;
445
+ await engine.set(found.path, (0, store_1.serialize)(found.doc), found.doc.created_at);
446
+ const instance = new message_1.default(init, found.doc);
447
+ this.emit('message:updated', instance, await instance.chat(), this);
946
448
  }
449
+ else if (found && protocol?.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE) {
450
+ await engine.unset(found.path);
451
+ const instance = new message_1.default(init, found.doc);
452
+ this.emit('message:deleted', instance, await instance.chat(), this);
453
+ }
454
+ continue;
947
455
  }
948
- catch {
949
- /* decrypt may fail */
456
+ const doc = new message_1.default(init, msg)._raw;
457
+ const stored = (0, store_1.deserialize)(await engine.get(`/chat/${cid}/message/${mid}`));
458
+ if (stored) {
459
+ doc.multiple = typeof stored.multiple === 'boolean' ? stored.multiple : doc.multiple;
460
+ doc.reactions = stored.reactions ?? doc.reactions;
461
+ const advanced = doc.status > stored.status;
462
+ doc.status = Math.max(stored.status, doc.status);
463
+ if (!advanced && stored.caption === doc.caption && stored.edited === doc.edited && stored.starred === doc.starred) {
464
+ continue;
465
+ }
950
466
  }
951
- }
952
- }
953
- continue;
954
- }
955
- if (content_type === 'protocolMessage') {
956
- const protocol = msg.message?.protocolMessage;
957
- if (protocol?.key?.id) {
958
- const target_mid = protocol.key.id;
959
- const target_cid = protocol.key.remoteJid ?? cid;
960
- const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${target_cid}/message/${target_mid}`));
961
- if (protocol.type === baileys_1.proto.Message.ProtocolMessage.Type.MESSAGE_EDIT &&
962
- protocol.editedMessage &&
963
- doc) {
964
- doc.raw.message = protocol.editedMessage;
965
- doc.edited = true;
966
- doc.caption = (0, message_1.message)(this, doc.raw).caption;
967
- await this.engine.set(`/chat/${target_cid}/message/${target_mid}`, (0, store_1.serialize)(doc), doc.created_at);
968
- const msg_instance = (0, message_1.message)(this, doc);
969
- this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
970
- }
971
- else if (protocol.type === baileys_1.proto.Message.ProtocolMessage.Type.REVOKE) {
972
- await this.engine.unset(`/chat/${target_cid}/message/${target_mid}`);
973
- if (doc) {
974
- const msg_instance = (0, message_1.message)(this, doc);
975
- this.emit('message:deleted', msg_instance, await msg_instance.chat(), this);
467
+ if (!stored && !doc.me) {
468
+ const known = (0, store_1.deserialize)(await engine.get(`/contact/${doc.author}`));
469
+ if (doc.author && !(known?.name ?? known?.notify ?? known?.verified_name)) {
470
+ socket.ev.emit('contacts.upsert', [{
471
+ id: doc.author,
472
+ lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : undefined,
473
+ notify: msg.pushName ?? undefined,
474
+ verifiedName: msg.verifiedBizName ?? undefined,
475
+ }]);
476
+ }
477
+ if (!(await engine.get(`/chat/${cid}`))) {
478
+ const owner = { id: cid, name: cid.endsWith('@g.us') ? null : msg.pushName ?? null, activity: doc.created_at };
479
+ await engine.set(`/chat/${cid}`, (0, store_1.serialize)(owner), doc.created_at);
480
+ this.emit('chat:created', new this.Chat(owner), this);
481
+ }
482
+ }
483
+ await engine.set(`/chat/${cid}/message/${mid}`, (0, store_1.serialize)(doc), doc.created_at);
484
+ const owner = (0, store_1.deserialize)(await engine.get(`/chat/${cid}`));
485
+ if (owner && doc.created_at > (owner.activity ?? 0)) {
486
+ owner.activity = doc.created_at;
487
+ await engine.set(`/chat/${cid}`, (0, store_1.serialize)(owner), doc.created_at);
488
+ }
489
+ if (!stored) {
490
+ const place = msg.message?.locationMessage ?? msg.message?.liveLocationMessage;
491
+ const poll = msg.message?.pollCreationMessage ?? msg.message?.pollCreationMessageV2 ?? msg.message?.pollCreationMessageV3;
492
+ const cards = msg.message?.contactsArrayMessage?.contacts ?? (msg.message?.contactMessage ? [msg.message.contactMessage] : []);
493
+ const body = doc.type === 'text' ? doc.caption
494
+ : doc.type === 'location' ? JSON.stringify({ lat: place?.degreesLatitude, lng: place?.degreesLongitude })
495
+ : doc.type === 'poll' ? JSON.stringify({ content: poll?.name ?? '', options: poll?.options?.map((option) => ({ content: option.optionName })) ?? [] })
496
+ : doc.type === 'vcard' ? cards.map((card) => card.vcard ?? '').join('\n')
497
+ : doc.type === 'event' ? JSON.stringify(msg.message?.eventMessage ?? {})
498
+ : null;
499
+ const binary = body !== null
500
+ ? Buffer.from(body, 'utf-8')
501
+ : ['image', 'video', 'audio', 'document'].includes(doc.type)
502
+ ? await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {}).catch(() => Buffer.alloc(0))
503
+ : Buffer.alloc(0);
504
+ if (binary.length > 0) {
505
+ await (engine.set_buffer?.(`/chat/${cid}/message/${mid}/content`, binary) ?? engine.set(`/chat/${cid}/message/${mid}/content`, (0, store_1.serialize)({ data: binary.toString('base64') })));
506
+ }
507
+ }
508
+ const instance = new message_1.default(init, doc);
509
+ const owner_chat = await instance.chat();
510
+ this.emit('message:created', instance, owner_chat, this);
511
+ if (doc.forwarded) {
512
+ this.emit('message:forwarded', instance, owner_chat, this);
976
513
  }
977
514
  }
978
- }
979
- continue;
980
- }
981
- const doc = (0, message_1.message)(this, msg)._raw;
982
- // Cada reconexión re-entrega el historial completo: reescribir documentos
983
- // idénticos contamina la cronología, re-descarga la media y spamea eventos,
984
- // así que un doc ya persistido sin cambios visibles se salta entero.
985
- // Every reconnect re-delivers the full history: rewriting identical documents
986
- // pollutes chronology, re-downloads media and spams events, so an already
987
- // persisted doc without visible changes is skipped entirely.
988
- const existing_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}/message/${mid}`));
989
- if (existing_doc) {
990
- if (typeof existing_doc.multiple === 'boolean') {
991
- doc.multiple = existing_doc.multiple;
992
- }
993
- if (existing_doc.reactions) {
994
- doc.reactions = existing_doc.reactions;
995
- }
996
- // El historial reporta el estado que tenía al sincronizarse: si acá se reescribe
997
- // por otro cambio (edición, destacado), el estado ya conocido se conserva.
998
- // History reports the state it had when synced: if the doc gets rewritten here
999
- // for another change (edit, star), the state already known is kept.
1000
- if (existing_doc.status > doc.status) {
1001
- doc.status = existing_doc.status;
1002
- }
1003
- if (existing_doc.status >= doc.status &&
1004
- existing_doc.caption === doc.caption &&
1005
- existing_doc.edited === doc.edited &&
1006
- existing_doc.starred === doc.starred) {
1007
- continue;
1008
- }
1009
- }
1010
- // Autocreación de contacto/chat desde pushName cuando baileys no emite upsert previo
1011
- if (!existing_doc && !doc.me) {
1012
- const push_name = msg.pushName ?? null;
1013
- const is_group = cid.endsWith('@g.us');
1014
- // El contacto se completa cuando no existe y también cuando existe sin ningún
1015
- // nombre: el mensaje trae el pushName y el nombre del negocio verificado, que
1016
- // es lo único que queda si un re-sync anterior dejó la ficha en blanco.
1017
- // The contact is filled in when missing and also when it exists with no name at
1018
- // all: the message carries the pushName and the verified business name, the only
1019
- // thing left when an earlier re-sync blanked the card.
1020
- if (doc.author) {
1021
- const known = (0, store_1.deserialize)(await this.engine.get(`/contact/${doc.author}`));
1022
- if (!known || !(known.name ?? known.notify ?? known.verified_name)) {
1023
- await this.#persist_contact({
1024
- id: doc.author,
1025
- lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : null,
1026
- name: null,
1027
- notify: push_name,
1028
- verified_name: msg.verifiedBizName ?? null,
1029
- img_url: null,
1030
- status: null,
1031
- });
515
+ }).catch(() => { });
516
+ });
517
+ socket.ev.on('messages.update', (updates) => {
518
+ chain = chain.then(async () => {
519
+ for (const { key, update } of updates) {
520
+ const found = key.remoteJid && key.id && key.remoteJid !== 'status@broadcast'
521
+ ? await locate(key.remoteJid, key.id)
522
+ : null;
523
+ if (found) {
524
+ const { path, doc } = found;
525
+ const patch = update;
526
+ const raw = doc.raw ?? { key };
527
+ if (patch.message) {
528
+ const edited = patch.message.editedMessage?.message;
529
+ raw.message = edited ?? { ...raw.message, ...patch.message };
530
+ doc.edited = doc.edited || Boolean(edited);
531
+ doc.caption = new message_1.default(init, raw).caption;
532
+ doc.raw = raw;
533
+ await engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
534
+ const instance = new message_1.default(init, doc);
535
+ this.emit('message:updated', instance, await instance.chat(), this);
536
+ }
537
+ else if (patch.starred !== undefined) {
538
+ doc.starred = patch.starred === true;
539
+ raw.starred = doc.starred;
540
+ doc.raw = raw;
541
+ await engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
542
+ const instance = new message_1.default(init, doc);
543
+ this.emit(doc.starred ? 'message:starred' : 'message:unstarred', instance, await instance.chat(), this);
544
+ }
545
+ else if (patch.status !== undefined && (patch.status > doc.status || patch.status === baileys_1.proto.WebMessageInfo.Status.ERROR)) {
546
+ raw.status = patch.status;
547
+ doc.status = patch.status;
548
+ raw.messageStubParameters = patch.messageStubParameters ?? raw.messageStubParameters;
549
+ doc.raw = raw;
550
+ await engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
551
+ const instance = new message_1.default(init, doc);
552
+ this.emit('message:updated', instance, await instance.chat(), this);
553
+ }
554
+ }
1032
555
  }
1033
- }
1034
- if (!(await this.engine.get(`/chat/${cid}`))) {
1035
- const chat_raw = {
1036
- id: cid,
1037
- name: is_group ? null : push_name,
1038
- activity: doc.created_at,
1039
- };
1040
- await this.engine.set(`/chat/${cid}`, (0, store_1.serialize)(chat_raw), doc.created_at);
1041
- this.emit('chat:created', new this.Chat(chat_raw), this);
1042
- }
1043
- }
1044
- await this.engine.set(`/chat/${cid}/message/${mid}`, (0, store_1.serialize)(doc), doc.created_at);
1045
- // El mensaje más nuevo define la posición del chat en la lista; un mensaje viejo
1046
- // que llega en un re-sync no la altera.
1047
- // The newest message defines the chat's position in the list; an old message
1048
- // arriving in a re-sync does not move it.
1049
- const chat_doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${cid}`));
1050
- if (chat_doc && doc.created_at > (chat_doc.activity ?? 0)) {
1051
- chat_doc.activity = doc.created_at;
1052
- await this.engine.set(`/chat/${cid}`, (0, store_1.serialize)(chat_doc), doc.created_at);
1053
- }
1054
- // El binario solo se materializa en la primera entrega; en re-syncs ya vive en el engine.
1055
- // The binary is only materialized on first delivery; on re-syncs it already lives in the engine.
1056
- let content_buf = Buffer.alloc(0);
1057
- if (existing_doc) {
1058
- /* ya materializado / already materialized */
1059
- }
1060
- else if (doc.type === 'text') {
1061
- content_buf = Buffer.from(doc.caption, 'utf-8');
1062
- }
1063
- else if (doc.type === 'location') {
1064
- const loc = msg.message?.locationMessage ?? msg.message?.liveLocationMessage;
1065
- content_buf = Buffer.from(JSON.stringify({ lat: loc?.degreesLatitude, lng: loc?.degreesLongitude }), 'utf-8');
1066
- }
1067
- else if (doc.type === 'poll') {
1068
- const poll = msg.message?.pollCreationMessage ??
1069
- msg.message?.pollCreationMessageV2 ??
1070
- msg.message?.pollCreationMessageV3;
1071
- content_buf = Buffer.from(JSON.stringify({
1072
- content: poll?.name ?? '',
1073
- options: poll?.options?.map((o) => ({ content: o.optionName })) ?? [],
1074
- }), 'utf-8');
1075
- }
1076
- else if (doc.type === 'vcard') {
1077
- const cards = msg.message?.contactsArrayMessage?.contacts ?? (msg.message?.contactMessage ? [msg.message.contactMessage] : []);
1078
- content_buf = Buffer.from(cards.map((c) => c.vcard ?? '').join('\n'), 'utf-8');
1079
- }
1080
- else if (doc.type === 'event') {
1081
- content_buf = Buffer.from(JSON.stringify(msg.message?.eventMessage ?? {}), 'utf-8');
1082
- }
1083
- else if (this.#internals.socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
1084
- try {
1085
- const buffer = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {});
1086
- if (Buffer.isBuffer(buffer)) {
1087
- content_buf = buffer;
556
+ }).catch(() => { });
557
+ });
558
+ socket.ev.on('message-receipt.update', (updates) => {
559
+ chain = chain.then(async () => {
560
+ for (const { key, receipt } of updates) {
561
+ if (key.remoteJid === 'status@broadcast' && key.id) {
562
+ const doc = (0, store_1.deserialize)(await engine.get(`/status/${key.id}`));
563
+ if (doc && !doc.viewed) {
564
+ doc.viewed = true;
565
+ await engine.set(`/status/${key.id}`, (0, store_1.serialize)(doc));
566
+ this.emit('feed:updated', new status_1.Feed(init, doc), this);
567
+ }
568
+ continue;
569
+ }
570
+ const played = receipt.playedTimestamp != null;
571
+ const found = (played || receipt.readTimestamp != null) && key.remoteJid && key.id
572
+ ? await locate(key.remoteJid, key.id)
573
+ : null;
574
+ if (found) {
575
+ const next = played ? baileys_1.proto.WebMessageInfo.Status.PLAYED : baileys_1.proto.WebMessageInfo.Status.READ;
576
+ if (found.doc.status < next) {
577
+ found.doc.status = next;
578
+ found.doc.raw.status = next;
579
+ await engine.set(found.path, (0, store_1.serialize)(found.doc), found.doc.created_at);
580
+ }
581
+ const instance = new message_1.default(init, found.doc);
582
+ this.emit('message:seen', instance, await instance.chat(), this);
583
+ }
1088
584
  }
1089
- }
1090
- catch {
1091
- /* media download may fail */
1092
- }
585
+ }).catch(() => { });
586
+ });
587
+ };
588
+ this.#close = async (quiet) => {
589
+ intentional = true;
590
+ silent = quiet;
591
+ if (timer) {
592
+ clearTimeout(timer);
593
+ timer = null;
1093
594
  }
1094
- if (content_buf.length > 0) {
1095
- await this.#write_content(`/chat/${cid}/message/${mid}/content`, content_buf);
595
+ try {
596
+ alive?.end(Object.assign(new Error('intentional close'), { output: { statusCode: baileys_1.DisconnectReason.connectionClosed } }));
1096
597
  }
1097
- const instance = (0, message_1.message)(this, doc);
1098
- const chat_instance = await instance.chat();
1099
- this.emit('message:created', instance, chat_instance, this);
1100
- if (doc.forwarded) {
1101
- this.emit('message:forwarded', instance, chat_instance, this);
598
+ catch {
599
+ /* el socket ya estaba cerrado / socket already closed */
1102
600
  }
1103
- }
1104
- }
601
+ alive = null;
602
+ };
603
+ start().catch(reject);
604
+ });
1105
605
  }
1106
606
  /**
1107
- * Ubica el documento de un mensaje partiendo del chat crudo del key. Los updates y
1108
- * receipts llegan direccionados por LID —con o sin dispositivo— mientras el documento
1109
- * vive bajo el JID con el que se guardó, así que se prueban ambas formas.
1110
- * Locates a message document from the raw chat in the key. Updates and receipts arrive
1111
- * LID-addressed —with or without device— while the document lives under the JID it was
1112
- * stored with, so both forms are tried.
607
+ * Cierra la sesión: cancela el reintento pendiente y termina el socket.
608
+ * Closes the session: cancels the pending retry and ends the socket.
1113
609
  *
1114
- * @param cid - Chat tal como viene en el key / Chat as it comes in the key
1115
- * @param mid - Identificador del mensaje / Message identifier
1116
- * @returns Ruta y documento, o null si no existe / Path and document, or null when missing
1117
- * @internal
610
+ * @param options - `silent` calla el evento `disconnected`; `destroy` vacía el engine / `silent` mutes the `disconnected` event; `destroy` clears the engine
1118
611
  */
1119
- async #locate(cid, mid) {
1120
- const tried = new Set();
1121
- for (const candidate of [await this.#resolve_jid(cid), cid, (0, baileys_1.jidNormalizedUser)(cid)]) {
1122
- if (candidate && !tried.has(candidate)) {
1123
- tried.add(candidate);
1124
- const path = `/chat/${candidate}/message/${mid}`;
1125
- const doc = (0, store_1.deserialize)(await this.engine.get(path));
1126
- if (doc) {
1127
- return { path, doc };
1128
- }
1129
- }
1130
- }
1131
- return null;
1132
- }
1133
- /** @internal */
1134
- async #handle_messages_update(updates) {
1135
- for (const { key, update: upd } of updates) {
1136
- if (key.remoteJid && key.id) {
1137
- // Updates sobre `status@broadcast` se descartan: el feed sólo se
1138
- // muta vía reacciones (`messages.reaction`), `Feed.view()` o REVOKE.
1139
- // Updates on `status@broadcast` are discarded: feed mutates only via
1140
- // reactions, `Feed.view()` or REVOKE.
1141
- if (key.remoteJid === 'status@broadcast') {
1142
- continue;
1143
- }
1144
- const found = await this.#locate(key.remoteJid, key.id);
1145
- if (found) {
1146
- const { path, doc } = found;
1147
- const raw = doc.raw ?? { key };
1148
- const upd_any = upd;
1149
- const edited_message = upd_any.message?.editedMessage?.message;
1150
- const content_update = upd_any.message;
1151
- const status = upd_any.status;
1152
- const starred_changed = upd_any.starred !== undefined;
1153
- if (edited_message) {
1154
- raw.message = edited_message;
1155
- doc.raw = raw;
1156
- doc.edited = true;
1157
- doc.caption = (0, message_1.message)(this, raw).caption;
1158
- await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1159
- const msg_instance = (0, message_1.message)(this, doc);
1160
- this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1161
- }
1162
- else if (content_update) {
1163
- // Actualización de contenido (ej: live location). Mergea sobre el raw existente.
1164
- raw.message = { ...raw.message, ...content_update };
1165
- doc.raw = raw;
1166
- doc.caption = (0, message_1.message)(this, raw).caption;
1167
- await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1168
- const msg_instance = (0, message_1.message)(this, doc);
1169
- this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1170
- }
1171
- else if (starred_changed) {
1172
- doc.starred = upd_any.starred === true;
1173
- raw.starred = doc.starred;
1174
- doc.raw = raw;
1175
- await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1176
- const msg_instance = (0, message_1.message)(this, doc);
1177
- this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
1178
- // WhatsApp reemite los acks desordenados al reconectar (un `sent` después
1179
- // de un `delivered`), así que el estado solo avanza; el rechazo (`error`)
1180
- // es terminal y sí puede pisar lo que hubiera.
1181
- // WhatsApp re-emits acks out of order on reconnect (a `sent` after a
1182
- // `delivered`), so the state only moves forward; a rejection (`error`) is
1183
- // terminal and may override whatever was there.
1184
- }
1185
- else if (status !== undefined && (status > doc.status || status === ERROR)) {
1186
- raw.status = status;
1187
- doc.status = status;
1188
- // El rechazo del servidor viaja como stub del update; sin persistirlo el
1189
- // mensaje queda en error sin decir por qué.
1190
- // The server rejection travels as an update stub; without persisting it the
1191
- // message stays in error without saying why.
1192
- if (upd_any.messageStubParameters) {
1193
- raw.messageStubParameters = upd_any.messageStubParameters;
1194
- }
1195
- doc.raw = raw;
1196
- await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1197
- const msg_instance = (0, message_1.message)(this, doc);
1198
- this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1199
- }
1200
- }
1201
- }
1202
- }
1203
- }
1204
- /** @internal */
1205
- async #handle_messages_reaction(reactions) {
1206
- for (const { key, reaction } of reactions) {
1207
- if (key.remoteJid && key.id) {
1208
- // Reacciones sobre status@broadcast → feed:updated (no message:reacted).
1209
- // Reactions on status@broadcast → feed:updated (not message:reacted).
1210
- if (key.remoteJid === 'status@broadcast') {
1211
- const feed_raw = (0, store_1.deserialize)(await this.engine.get(`/status/${key.id}`));
1212
- if (feed_raw) {
1213
- this.emit('feed:updated', new status_1.Feed(this, feed_raw), this);
1214
- }
1215
- continue;
1216
- }
1217
- const found = await this.#locate(key.remoteJid, key.id);
1218
- if (found) {
1219
- const { path, doc } = found;
1220
- const reactor = (0, baileys_1.jidNormalizedUser)(key.participant ?? key.remoteJid);
1221
- const emoji = reaction.text ?? '';
1222
- doc.reactions = [
1223
- ...(doc.reactions ?? []).filter((r) => r.author !== reactor),
1224
- ...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
1225
- ];
1226
- await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1227
- const msg_instance = (0, message_1.message)(this, doc);
1228
- this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
1229
- }
1230
- }
612
+ async disconnect(options = {}) {
613
+ await this.#close?.(options.silent === true);
614
+ if (options.destroy) {
615
+ await this.engine.clear();
1231
616
  }
1232
617
  }
1233
618
  }
1234
- exports.WhatsApp = WhatsApp;
1235
619
  exports.default = WhatsApp;