@arcaelas/whatsapp 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,8 +56,8 @@ Node 20 o superior. El paquete se distribuye en ESM y CJS.
56
56
  import WhatsApp, { FileSystemEngine } from '@arcaelas/whatsapp';
57
57
 
58
58
  const wa = new WhatsApp({
59
- engine: new FileSystemEngine('.sessions/584144709840'),
60
- phone: 584144709840,
59
+ engine: new FileSystemEngine('.sessions/5491112345678'),
60
+ phone: 5491112345678,
61
61
  });
62
62
 
63
63
  wa.on('message:created', async (msg) => {
@@ -119,7 +119,7 @@ await wa.profile({ photo: null }); // elimina la foto
119
119
 
120
120
  const post = await wa.feed({
121
121
  caption: '¡Estamos en vivo!',
122
- contacts: ['584144709840', '56963091328'], // audiencia obligatoria
122
+ contacts: ['5491112345678', '584121234567'], // audiencia obligatoria
123
123
  });
124
124
  ```
125
125
 
@@ -134,13 +134,13 @@ Cada entidad expone getters puros sobre su documento y métodos que actúan cont
134
134
  ### Contact
135
135
 
136
136
  ```ts
137
- const person = await wa.Contact.get('584144709840'); // teléfono, JID o LID
137
+ const person = await wa.Contact.get('5491112345678'); // teléfono, JID o LID
138
138
  const page = await wa.Contact.list(0, 50);
139
139
 
140
140
  if (person) {
141
141
  person.name // agenda → nombre público → nombre verificado → teléfono
142
142
  person.phone // solo del JID PN, nunca del LID; null si no es determinable
143
- person.jid // '584144709840@s.whatsapp.net' | null
143
+ person.jid // '5491112345678@s.whatsapp.net' | null
144
144
  person.lid // '123456789@lid' | null
145
145
  person.photo // URL de la foto | null
146
146
  await person.chat();
@@ -152,7 +152,7 @@ if (person) {
152
152
  ### Chat
153
153
 
154
154
  ```ts
155
- const chat = await wa.Chat.get('584144709840');
155
+ const chat = await wa.Chat.get('5491112345678');
156
156
  const chats = await wa.Chat.list(0, 50);
157
157
 
158
158
  if (chat) {
@@ -198,6 +198,8 @@ const msg = await wa.Message.get(cid, mid);
198
198
  | `caption` | texto del mensaje o pie del media |
199
199
  | `status` | `'error'` `'pending'` `'sent'` `'delivered'` `'read'` `'played'` |
200
200
  | `read` `starred` `forwarded` `edited` `once` | banderas del mensaje |
201
+ | `reason` | motivo del rechazo cuando `status` es `'error'` (`restricted`, `invalid-session`, o el código del servidor), si no `null` |
202
+ | `business` | nombre del negocio verificado que firma el mensaje, o `null` |
201
203
  | `created_at` `expires_at` | fechas en ISO UTC (`expires_at` solo en mensajes temporales) |
202
204
 
203
205
  **Métodos**
@@ -319,10 +321,10 @@ import IORedis from 'ioredis';
319
321
  import { S3Client } from '@aws-sdk/client-s3';
320
322
  import { FileSystemEngine, SQLiteEngine, RedisEngine, S3Engine } from '@arcaelas/whatsapp';
321
323
 
322
- new FileSystemEngine('.sessions/584144709840');
323
- new SQLiteEngine(new Database('.sessions/584144709840.db'));
324
- new RedisEngine(new IORedis(), 'wa:584144709840');
325
- new S3Engine({ s3: new S3Client({}), bucket: 'sesiones', basedir: 'wa/584144709840' });
324
+ new FileSystemEngine('.sessions/5491112345678');
325
+ new SQLiteEngine(new Database('.sessions/5491112345678.db'));
326
+ new RedisEngine(new IORedis(), 'wa:5491112345678');
327
+ new S3Engine({ s3: new S3Client({}), bucket: 'sesiones', basedir: 'wa/5491112345678' });
326
328
  ```
327
329
 
328
330
  `SQLiteEngine` es el más eficiente de los integrados. Sobre un chat real de 55.146 mensajes, frente al filesystem: 220 MB → 64 MB en disco, primer `list` 115 ms → 0,6 ms, y dos archivos en total en lugar de ~110.000 inodes.
@@ -348,7 +350,7 @@ class Bot extends WhatsAppBot {
348
350
  await msg.text(`Consultando ${args[0] ?? 'el catálogo'}…`);
349
351
  }
350
352
 
351
- @from('584144709840')
353
+ @from('5491112345678')
352
354
  async only_admin(msg: Message) {
353
355
  await msg.react('👑');
354
356
  }
@@ -359,7 +361,7 @@ class Bot extends WhatsAppBot {
359
361
  }
360
362
  }
361
363
 
362
- const bot = new Bot({ engine: new FileSystemEngine('.sessions/bot'), phone: 584144709840 });
364
+ const bot = new Bot({ engine: new FileSystemEngine('.sessions/bot'), phone: 5491112345678 });
363
365
  await bot.connect((auth) => console.log(auth));
364
366
  ```
365
367
 
@@ -395,7 +397,7 @@ wa.on('message:created', async (msg, chat) => {
395
397
  **Reconectar con límite y cerrar en silencio**
396
398
 
397
399
  ```ts
398
- const wa = new WhatsApp({ engine, phone: 584144709840, reconnect: { max: 5, interval: 30 } });
400
+ const wa = new WhatsApp({ engine, phone: 5491112345678, reconnect: { max: 5, interval: 30 } });
399
401
  await wa.disconnect({ silent: true }); // no emite `disconnected`
400
402
  ```
401
403
 
@@ -103,6 +103,10 @@ export declare class Message {
103
103
  get status(): (typeof STATUS)[number];
104
104
  /** true si el mensaje fue visto. / true when the message was seen. */
105
105
  get read(): boolean;
106
+ /** Motivo del rechazo cuando `status` es `error` (`restricted`, `invalid-session`, o el código crudo); null en cualquier otro caso. / Rejection reason when `status` is `error` (`restricted`, `invalid-session`, or the raw code); null otherwise. */
107
+ get reason(): string | null;
108
+ /** Nombre del negocio verificado que firma el mensaje, o null. / Verified business name signing the message, or null. */
109
+ get business(): string | null;
106
110
  /** true si está destacado. / true when starred. */
107
111
  get starred(): boolean;
108
112
  /** true si fue reenviado. / true when forwarded. */
@@ -18,6 +18,16 @@ const chat_1 = require("../../lib/chat");
18
18
  const store_1 = require("../../lib/store");
19
19
  /** Estados legibles indexados por el status numérico de baileys. / Readable states indexed by the baileys numeric status. */
20
20
  const STATUS = ['error', 'pending', 'sent', 'delivered', 'read', 'played'];
21
+ /**
22
+ * Motivos legibles de los códigos con los que el servidor rechaza un mensaje. `restricted`
23
+ * es la cuenta limitada por WhatsApp: puede seguir chats existentes pero no abrir nuevos.
24
+ * Readable reasons for the codes the server rejects a message with. `restricted` is an
25
+ * account limited by WhatsApp: it can continue existing chats but not open new ones.
26
+ */
27
+ const REASON = {
28
+ 463: 'restricted',
29
+ 479: 'invalid-session',
30
+ };
21
31
  const MESSAGE_TYPE_MAP = {
22
32
  conversation: 'text',
23
33
  extendedTextMessage: 'text',
@@ -79,11 +89,11 @@ async function send(wa, cid, content, binary, extra = {}, doc_overrides) {
79
89
  const quoted = extra.mid
80
90
  ? ((0, store_1.deserialize)(await wa.engine.get(`/chat/${jid}/message/${extra.mid}`))?.raw ?? undefined)
81
91
  : undefined;
82
- const raw = await socket.sendMessage(jid, {
83
- ...content,
84
- ...(extra.once && { viewOnce: true }),
85
- ...(quoted && { quoted }),
86
- });
92
+ // La cita viaja en las opciones del socket, no en el contenido: dentro del contenido
93
+ // baileys la ignora y el mensaje sale sin `contextInfo`.
94
+ // The quote travels in the socket options, not in the content: inside the content
95
+ // baileys ignores it and the message goes out without `contextInfo`.
96
+ const raw = await socket.sendMessage(jid, { ...content, ...(extra.once && { viewOnce: true }) }, { ...(quoted && { quoted }) });
87
97
  const sent_id = raw?.key?.id;
88
98
  if (raw && sent_id) {
89
99
  const instance = message(wa, raw);
@@ -231,6 +241,17 @@ class Message {
231
241
  get read() {
232
242
  return this._raw.status >= 4;
233
243
  }
244
+ /** Motivo del rechazo cuando `status` es `error` (`restricted`, `invalid-session`, o el código crudo); null en cualquier otro caso. / Rejection reason when `status` is `error` (`restricted`, `invalid-session`, or the raw code); null otherwise. */
245
+ get reason() {
246
+ const code = this._raw.raw.messageStubParameters?.[0];
247
+ if (this._raw.status === 0 && code)
248
+ return REASON[code] ?? code;
249
+ return null;
250
+ }
251
+ /** Nombre del negocio verificado que firma el mensaje, o null. / Verified business name signing the message, or null. */
252
+ get business() {
253
+ return this._raw.raw.verifiedBizName ?? null;
254
+ }
234
255
  /** true si está destacado. / true when starred. */
235
256
  get starred() {
236
257
  return this._raw.starred;
@@ -44,11 +44,11 @@ export interface SQLiteDatabase {
44
44
  *
45
45
  * @example
46
46
  * import Database from 'better-sqlite3';
47
- * const engine = new SQLiteEngine(new Database('.sessions/584144709840.db'));
47
+ * const engine = new SQLiteEngine(new Database('.sessions/5491112345678.db'));
48
48
  *
49
49
  * @example
50
50
  * import { DatabaseSync } from 'node:sqlite';
51
- * const engine = new SQLiteEngine(new DatabaseSync('.sessions/584144709840.db'));
51
+ * const engine = new SQLiteEngine(new DatabaseSync('.sessions/5491112345678.db'));
52
52
  */
53
53
  export declare class SQLiteEngine implements Engine {
54
54
  private readonly _db;
@@ -26,11 +26,11 @@ const lib_1 = require("../../../../../lib/store/engine/lib");
26
26
  *
27
27
  * @example
28
28
  * import Database from 'better-sqlite3';
29
- * const engine = new SQLiteEngine(new Database('.sessions/584144709840.db'));
29
+ * const engine = new SQLiteEngine(new Database('.sessions/5491112345678.db'));
30
30
  *
31
31
  * @example
32
32
  * import { DatabaseSync } from 'node:sqlite';
33
- * const engine = new SQLiteEngine(new DatabaseSync('.sessions/584144709840.db'));
33
+ * const engine = new SQLiteEngine(new DatabaseSync('.sessions/5491112345678.db'));
34
34
  */
35
35
  class SQLiteEngine {
36
36
  /**
@@ -111,7 +111,7 @@ interface WhatsAppEventMap {
111
111
  * Main WhatsApp client. Does not connect on instantiation.
112
112
  *
113
113
  * @example
114
- * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 584144709840 });
114
+ * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 5491112345678 });
115
115
  * wa.on('message:created', (msg) => console.log(msg.caption));
116
116
  * await wa.connect((code) => console.log(code));
117
117
  */
@@ -78,7 +78,7 @@ function sniff_media(data) {
78
78
  * Main WhatsApp client. Does not connect on instantiation.
79
79
  *
80
80
  * @example
81
- * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 584144709840 });
81
+ * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 5491112345678 });
82
82
  * wa.on('message:created', (msg) => console.log(msg.caption));
83
83
  * await wa.connect((code) => console.log(code));
84
84
  */
@@ -221,12 +221,15 @@ class WhatsApp {
221
221
  result = uid;
222
222
  }
223
223
  else if (uid.endsWith('@lid')) {
224
- const direct = (0, store_1.deserialize)(await this.engine.get(`/lid/${uid}`));
224
+ // Los receipts direccionan por dispositivo (`…:9@lid`); el índice se guarda sin él.
225
+ // Receipts address per device (`…:9@lid`); the index is stored without it.
226
+ const lid = (0, baileys_1.jidNormalizedUser)(uid);
227
+ const direct = (0, store_1.deserialize)(await this.engine.get(`/lid/${lid}`));
225
228
  if (direct) {
226
229
  result = direct.includes('@') ? direct : `${direct}@s.whatsapp.net`;
227
230
  }
228
231
  else {
229
- const reverse = (0, store_1.deserialize)(await this.engine.get(`/lid/${uid.split('@')[0]}_reverse`));
232
+ const reverse = (0, store_1.deserialize)(await this.engine.get(`/lid/${lid.split('@')[0]}_reverse`));
230
233
  if (reverse != null) {
231
234
  result = `${reverse}@s.whatsapp.net`;
232
235
  }
@@ -235,7 +238,7 @@ class WhatsApp {
235
238
  // baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
236
239
  // (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
237
240
  // realmente está guardado, y el mensaje/poll no se encuentra.
238
- const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
241
+ const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null);
239
242
  if (pn) {
240
243
  // getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
241
244
  // que el JID coincida con el que usa el store (sin device).
@@ -719,13 +722,14 @@ class WhatsApp {
719
722
  continue;
720
723
  }
721
724
  if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
722
- const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
723
- if (doc) {
725
+ const found = await this.#locate(key.remoteJid, key.id);
726
+ if (found) {
727
+ const { path, doc } = found;
724
728
  const next = receipt.playedTimestamp != null ? PLAYED : READ;
725
729
  if (doc.status < next) {
726
730
  doc.status = next;
727
731
  doc.raw.status = next;
728
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
732
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
729
733
  }
730
734
  const msg_instance = (0, message_1.message)(this, doc);
731
735
  this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
@@ -1027,6 +1031,33 @@ class WhatsApp {
1027
1031
  }
1028
1032
  }
1029
1033
  }
1034
+ /**
1035
+ * Ubica el documento de un mensaje partiendo del chat crudo del key. Los updates y
1036
+ * receipts llegan direccionados por LID —con o sin dispositivo— mientras el documento
1037
+ * vive bajo el JID con el que se guardó, así que se prueban ambas formas.
1038
+ * Locates a message document from the raw chat in the key. Updates and receipts arrive
1039
+ * LID-addressed —with or without device— while the document lives under the JID it was
1040
+ * stored with, so both forms are tried.
1041
+ *
1042
+ * @param cid - Chat tal como viene en el key / Chat as it comes in the key
1043
+ * @param mid - Identificador del mensaje / Message identifier
1044
+ * @returns Ruta y documento, o null si no existe / Path and document, or null when missing
1045
+ * @internal
1046
+ */
1047
+ async #locate(cid, mid) {
1048
+ const tried = new Set();
1049
+ for (const candidate of [await this.#resolve_jid(cid), cid, (0, baileys_1.jidNormalizedUser)(cid)]) {
1050
+ if (candidate && !tried.has(candidate)) {
1051
+ tried.add(candidate);
1052
+ const path = `/chat/${candidate}/message/${mid}`;
1053
+ const doc = (0, store_1.deserialize)(await this.engine.get(path));
1054
+ if (doc) {
1055
+ return { path, doc };
1056
+ }
1057
+ }
1058
+ }
1059
+ return null;
1060
+ }
1030
1061
  /** @internal */
1031
1062
  async #handle_messages_update(updates) {
1032
1063
  for (const { key, update: upd } of updates) {
@@ -1038,8 +1069,9 @@ class WhatsApp {
1038
1069
  if (key.remoteJid === 'status@broadcast') {
1039
1070
  continue;
1040
1071
  }
1041
- const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
1042
- if (doc) {
1072
+ const found = await this.#locate(key.remoteJid, key.id);
1073
+ if (found) {
1074
+ const { path, doc } = found;
1043
1075
  const raw = doc.raw ?? { key };
1044
1076
  const upd_any = upd;
1045
1077
  const edited_message = upd_any.message?.editedMessage?.message;
@@ -1051,7 +1083,7 @@ class WhatsApp {
1051
1083
  doc.raw = raw;
1052
1084
  doc.edited = true;
1053
1085
  doc.caption = (0, message_1.message)(this, raw).caption;
1054
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1086
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1055
1087
  const msg_instance = (0, message_1.message)(this, doc);
1056
1088
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1057
1089
  }
@@ -1060,7 +1092,7 @@ class WhatsApp {
1060
1092
  raw.message = { ...raw.message, ...content_update };
1061
1093
  doc.raw = raw;
1062
1094
  doc.caption = (0, message_1.message)(this, raw).caption;
1063
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1095
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1064
1096
  const msg_instance = (0, message_1.message)(this, doc);
1065
1097
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1066
1098
  }
@@ -1068,15 +1100,22 @@ class WhatsApp {
1068
1100
  doc.starred = upd_any.starred === true;
1069
1101
  raw.starred = doc.starred;
1070
1102
  doc.raw = raw;
1071
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1103
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1072
1104
  const msg_instance = (0, message_1.message)(this, doc);
1073
1105
  this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
1074
1106
  }
1075
1107
  else if (status !== undefined) {
1076
1108
  raw.status = status;
1077
1109
  doc.status = status;
1110
+ // El rechazo del servidor viaja como stub del update; sin persistirlo el
1111
+ // mensaje queda en error sin decir por qué.
1112
+ // The server rejection travels as an update stub; without persisting it the
1113
+ // message stays in error without saying why.
1114
+ if (upd_any.messageStubParameters) {
1115
+ raw.messageStubParameters = upd_any.messageStubParameters;
1116
+ }
1078
1117
  doc.raw = raw;
1079
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1118
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1080
1119
  const msg_instance = (0, message_1.message)(this, doc);
1081
1120
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1082
1121
  }
@@ -1097,15 +1136,16 @@ class WhatsApp {
1097
1136
  }
1098
1137
  continue;
1099
1138
  }
1100
- const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
1101
- if (doc) {
1139
+ const found = await this.#locate(key.remoteJid, key.id);
1140
+ if (found) {
1141
+ const { path, doc } = found;
1102
1142
  const reactor = (0, baileys_1.jidNormalizedUser)(key.participant ?? key.remoteJid);
1103
1143
  const emoji = reaction.text ?? '';
1104
1144
  doc.reactions = [
1105
1145
  ...(doc.reactions ?? []).filter((r) => r.author !== reactor),
1106
1146
  ...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
1107
1147
  ];
1108
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1148
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1109
1149
  const msg_instance = (0, message_1.message)(this, doc);
1110
1150
  this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
1111
1151
  }
@@ -103,6 +103,10 @@ export declare class Message {
103
103
  get status(): (typeof STATUS)[number];
104
104
  /** true si el mensaje fue visto. / true when the message was seen. */
105
105
  get read(): boolean;
106
+ /** Motivo del rechazo cuando `status` es `error` (`restricted`, `invalid-session`, o el código crudo); null en cualquier otro caso. / Rejection reason when `status` is `error` (`restricted`, `invalid-session`, or the raw code); null otherwise. */
107
+ get reason(): string | null;
108
+ /** Nombre del negocio verificado que firma el mensaje, o null. / Verified business name signing the message, or null. */
109
+ get business(): string | null;
106
110
  /** true si está destacado. / true when starred. */
107
111
  get starred(): boolean;
108
112
  /** true si fue reenviado. / true when forwarded. */
@@ -14,6 +14,16 @@ import { Chat } from '../../lib/chat/index.js';
14
14
  import { deserialize, serialize } from '../../lib/store/index.js';
15
15
  /** Estados legibles indexados por el status numérico de baileys. / Readable states indexed by the baileys numeric status. */
16
16
  const STATUS = ['error', 'pending', 'sent', 'delivered', 'read', 'played'];
17
+ /**
18
+ * Motivos legibles de los códigos con los que el servidor rechaza un mensaje. `restricted`
19
+ * es la cuenta limitada por WhatsApp: puede seguir chats existentes pero no abrir nuevos.
20
+ * Readable reasons for the codes the server rejects a message with. `restricted` is an
21
+ * account limited by WhatsApp: it can continue existing chats but not open new ones.
22
+ */
23
+ const REASON = {
24
+ 463: 'restricted',
25
+ 479: 'invalid-session',
26
+ };
17
27
  const MESSAGE_TYPE_MAP = {
18
28
  conversation: 'text',
19
29
  extendedTextMessage: 'text',
@@ -75,11 +85,11 @@ async function send(wa, cid, content, binary, extra = {}, doc_overrides) {
75
85
  const quoted = extra.mid
76
86
  ? (deserialize(await wa.engine.get(`/chat/${jid}/message/${extra.mid}`))?.raw ?? undefined)
77
87
  : undefined;
78
- const raw = await socket.sendMessage(jid, {
79
- ...content,
80
- ...(extra.once && { viewOnce: true }),
81
- ...(quoted && { quoted }),
82
- });
88
+ // La cita viaja en las opciones del socket, no en el contenido: dentro del contenido
89
+ // baileys la ignora y el mensaje sale sin `contextInfo`.
90
+ // The quote travels in the socket options, not in the content: inside the content
91
+ // baileys ignores it and the message goes out without `contextInfo`.
92
+ const raw = await socket.sendMessage(jid, { ...content, ...(extra.once && { viewOnce: true }) }, { ...(quoted && { quoted }) });
83
93
  const sent_id = raw?.key?.id;
84
94
  if (raw && sent_id) {
85
95
  const instance = message(wa, raw);
@@ -227,6 +237,17 @@ export class Message {
227
237
  get read() {
228
238
  return this._raw.status >= 4;
229
239
  }
240
+ /** Motivo del rechazo cuando `status` es `error` (`restricted`, `invalid-session`, o el código crudo); null en cualquier otro caso. / Rejection reason when `status` is `error` (`restricted`, `invalid-session`, or the raw code); null otherwise. */
241
+ get reason() {
242
+ const code = this._raw.raw.messageStubParameters?.[0];
243
+ if (this._raw.status === 0 && code)
244
+ return REASON[code] ?? code;
245
+ return null;
246
+ }
247
+ /** Nombre del negocio verificado que firma el mensaje, o null. / Verified business name signing the message, or null. */
248
+ get business() {
249
+ return this._raw.raw.verifiedBizName ?? null;
250
+ }
230
251
  /** true si está destacado. / true when starred. */
231
252
  get starred() {
232
253
  return this._raw.starred;
@@ -44,11 +44,11 @@ export interface SQLiteDatabase {
44
44
  *
45
45
  * @example
46
46
  * import Database from 'better-sqlite3';
47
- * const engine = new SQLiteEngine(new Database('.sessions/584144709840.db'));
47
+ * const engine = new SQLiteEngine(new Database('.sessions/5491112345678.db'));
48
48
  *
49
49
  * @example
50
50
  * import { DatabaseSync } from 'node:sqlite';
51
- * const engine = new SQLiteEngine(new DatabaseSync('.sessions/584144709840.db'));
51
+ * const engine = new SQLiteEngine(new DatabaseSync('.sessions/5491112345678.db'));
52
52
  */
53
53
  export declare class SQLiteEngine implements Engine {
54
54
  private readonly _db;
@@ -23,11 +23,11 @@ import { normalize_path, split_path } from '../../../../../lib/store/engine/lib/
23
23
  *
24
24
  * @example
25
25
  * import Database from 'better-sqlite3';
26
- * const engine = new SQLiteEngine(new Database('.sessions/584144709840.db'));
26
+ * const engine = new SQLiteEngine(new Database('.sessions/5491112345678.db'));
27
27
  *
28
28
  * @example
29
29
  * import { DatabaseSync } from 'node:sqlite';
30
- * const engine = new SQLiteEngine(new DatabaseSync('.sessions/584144709840.db'));
30
+ * const engine = new SQLiteEngine(new DatabaseSync('.sessions/5491112345678.db'));
31
31
  */
32
32
  export class SQLiteEngine {
33
33
  /**
@@ -111,7 +111,7 @@ interface WhatsAppEventMap {
111
111
  * Main WhatsApp client. Does not connect on instantiation.
112
112
  *
113
113
  * @example
114
- * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 584144709840 });
114
+ * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 5491112345678 });
115
115
  * wa.on('message:created', (msg) => console.log(msg.caption));
116
116
  * await wa.connect((code) => console.log(code));
117
117
  */
@@ -39,7 +39,7 @@ function sniff_media(data) {
39
39
  * Main WhatsApp client. Does not connect on instantiation.
40
40
  *
41
41
  * @example
42
- * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 584144709840 });
42
+ * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 5491112345678 });
43
43
  * wa.on('message:created', (msg) => console.log(msg.caption));
44
44
  * await wa.connect((code) => console.log(code));
45
45
  */
@@ -182,12 +182,15 @@ export class WhatsApp {
182
182
  result = uid;
183
183
  }
184
184
  else if (uid.endsWith('@lid')) {
185
- const direct = deserialize(await this.engine.get(`/lid/${uid}`));
185
+ // Los receipts direccionan por dispositivo (`…:9@lid`); el índice se guarda sin él.
186
+ // Receipts address per device (`…:9@lid`); the index is stored without it.
187
+ const lid = jidNormalizedUser(uid);
188
+ const direct = deserialize(await this.engine.get(`/lid/${lid}`));
186
189
  if (direct) {
187
190
  result = direct.includes('@') ? direct : `${direct}@s.whatsapp.net`;
188
191
  }
189
192
  else {
190
- const reverse = deserialize(await this.engine.get(`/lid/${uid.split('@')[0]}_reverse`));
193
+ const reverse = deserialize(await this.engine.get(`/lid/${lid.split('@')[0]}_reverse`));
191
194
  if (reverse != null) {
192
195
  result = `${reverse}@s.whatsapp.net`;
193
196
  }
@@ -196,7 +199,7 @@ export class WhatsApp {
196
199
  // baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
197
200
  // (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
198
201
  // realmente está guardado, y el mensaje/poll no se encuentra.
199
- const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
202
+ const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null);
200
203
  if (pn) {
201
204
  // getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
202
205
  // que el JID coincida con el que usa el store (sin device).
@@ -680,13 +683,14 @@ export class WhatsApp {
680
683
  continue;
681
684
  }
682
685
  if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
683
- const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
684
- if (doc) {
686
+ const found = await this.#locate(key.remoteJid, key.id);
687
+ if (found) {
688
+ const { path, doc } = found;
685
689
  const next = receipt.playedTimestamp != null ? PLAYED : READ;
686
690
  if (doc.status < next) {
687
691
  doc.status = next;
688
692
  doc.raw.status = next;
689
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
693
+ await this.engine.set(path, serialize(doc), doc.created_at);
690
694
  }
691
695
  const msg_instance = message(this, doc);
692
696
  this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
@@ -988,6 +992,33 @@ export class WhatsApp {
988
992
  }
989
993
  }
990
994
  }
995
+ /**
996
+ * Ubica el documento de un mensaje partiendo del chat crudo del key. Los updates y
997
+ * receipts llegan direccionados por LID —con o sin dispositivo— mientras el documento
998
+ * vive bajo el JID con el que se guardó, así que se prueban ambas formas.
999
+ * Locates a message document from the raw chat in the key. Updates and receipts arrive
1000
+ * LID-addressed —with or without device— while the document lives under the JID it was
1001
+ * stored with, so both forms are tried.
1002
+ *
1003
+ * @param cid - Chat tal como viene en el key / Chat as it comes in the key
1004
+ * @param mid - Identificador del mensaje / Message identifier
1005
+ * @returns Ruta y documento, o null si no existe / Path and document, or null when missing
1006
+ * @internal
1007
+ */
1008
+ async #locate(cid, mid) {
1009
+ const tried = new Set();
1010
+ for (const candidate of [await this.#resolve_jid(cid), cid, jidNormalizedUser(cid)]) {
1011
+ if (candidate && !tried.has(candidate)) {
1012
+ tried.add(candidate);
1013
+ const path = `/chat/${candidate}/message/${mid}`;
1014
+ const doc = deserialize(await this.engine.get(path));
1015
+ if (doc) {
1016
+ return { path, doc };
1017
+ }
1018
+ }
1019
+ }
1020
+ return null;
1021
+ }
991
1022
  /** @internal */
992
1023
  async #handle_messages_update(updates) {
993
1024
  for (const { key, update: upd } of updates) {
@@ -999,8 +1030,9 @@ export class WhatsApp {
999
1030
  if (key.remoteJid === 'status@broadcast') {
1000
1031
  continue;
1001
1032
  }
1002
- const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
1003
- if (doc) {
1033
+ const found = await this.#locate(key.remoteJid, key.id);
1034
+ if (found) {
1035
+ const { path, doc } = found;
1004
1036
  const raw = doc.raw ?? { key };
1005
1037
  const upd_any = upd;
1006
1038
  const edited_message = upd_any.message?.editedMessage?.message;
@@ -1012,7 +1044,7 @@ export class WhatsApp {
1012
1044
  doc.raw = raw;
1013
1045
  doc.edited = true;
1014
1046
  doc.caption = message(this, raw).caption;
1015
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1047
+ await this.engine.set(path, serialize(doc), doc.created_at);
1016
1048
  const msg_instance = message(this, doc);
1017
1049
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1018
1050
  }
@@ -1021,7 +1053,7 @@ export class WhatsApp {
1021
1053
  raw.message = { ...raw.message, ...content_update };
1022
1054
  doc.raw = raw;
1023
1055
  doc.caption = message(this, raw).caption;
1024
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1056
+ await this.engine.set(path, serialize(doc), doc.created_at);
1025
1057
  const msg_instance = message(this, doc);
1026
1058
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1027
1059
  }
@@ -1029,15 +1061,22 @@ export class WhatsApp {
1029
1061
  doc.starred = upd_any.starred === true;
1030
1062
  raw.starred = doc.starred;
1031
1063
  doc.raw = raw;
1032
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1064
+ await this.engine.set(path, serialize(doc), doc.created_at);
1033
1065
  const msg_instance = message(this, doc);
1034
1066
  this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
1035
1067
  }
1036
1068
  else if (status !== undefined) {
1037
1069
  raw.status = status;
1038
1070
  doc.status = status;
1071
+ // El rechazo del servidor viaja como stub del update; sin persistirlo el
1072
+ // mensaje queda en error sin decir por qué.
1073
+ // The server rejection travels as an update stub; without persisting it the
1074
+ // message stays in error without saying why.
1075
+ if (upd_any.messageStubParameters) {
1076
+ raw.messageStubParameters = upd_any.messageStubParameters;
1077
+ }
1039
1078
  doc.raw = raw;
1040
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1079
+ await this.engine.set(path, serialize(doc), doc.created_at);
1041
1080
  const msg_instance = message(this, doc);
1042
1081
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1043
1082
  }
@@ -1058,15 +1097,16 @@ export class WhatsApp {
1058
1097
  }
1059
1098
  continue;
1060
1099
  }
1061
- const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
1062
- if (doc) {
1100
+ const found = await this.#locate(key.remoteJid, key.id);
1101
+ if (found) {
1102
+ const { path, doc } = found;
1063
1103
  const reactor = jidNormalizedUser(key.participant ?? key.remoteJid);
1064
1104
  const emoji = reaction.text ?? '';
1065
1105
  doc.reactions = [
1066
1106
  ...(doc.reactions ?? []).filter((r) => r.author !== reactor),
1067
1107
  ...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
1068
1108
  ];
1069
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1109
+ await this.engine.set(path, serialize(doc), doc.created_at);
1070
1110
  const msg_instance = message(this, doc);
1071
1111
  this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
1072
1112
  }
package/package.json CHANGED
@@ -72,7 +72,7 @@
72
72
  "release": "npm publish --access public"
73
73
  },
74
74
  "types": "./build/esm/index.d.ts",
75
- "version": "6.0.0",
75
+ "version": "6.1.0",
76
76
  "engines": {
77
77
  "node": ">=20"
78
78
  },