@arcaelas/whatsapp 6.0.0 → 6.1.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.
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
  */
@@ -52,6 +52,8 @@ const internal_1 = require("../../lib/internal");
52
52
  const message_1 = require("../../lib/message");
53
53
  const status_1 = require("../../lib/status");
54
54
  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;
55
57
  /** Estados legibles del mensaje que el receipt puede avanzar. / Readable message states a receipt can advance to. */
56
58
  const READ = 4;
57
59
  const PLAYED = 5;
@@ -78,7 +80,7 @@ function sniff_media(data) {
78
80
  * Main WhatsApp client. Does not connect on instantiation.
79
81
  *
80
82
  * @example
81
- * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 584144709840 });
83
+ * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 5491112345678 });
82
84
  * wa.on('message:created', (msg) => console.log(msg.caption));
83
85
  * await wa.connect((code) => console.log(code));
84
86
  */
@@ -221,12 +223,15 @@ class WhatsApp {
221
223
  result = uid;
222
224
  }
223
225
  else if (uid.endsWith('@lid')) {
224
- const direct = (0, store_1.deserialize)(await this.engine.get(`/lid/${uid}`));
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}`));
225
230
  if (direct) {
226
231
  result = direct.includes('@') ? direct : `${direct}@s.whatsapp.net`;
227
232
  }
228
233
  else {
229
- const reverse = (0, store_1.deserialize)(await this.engine.get(`/lid/${uid.split('@')[0]}_reverse`));
234
+ const reverse = (0, store_1.deserialize)(await this.engine.get(`/lid/${lid.split('@')[0]}_reverse`));
230
235
  if (reverse != null) {
231
236
  result = `${reverse}@s.whatsapp.net`;
232
237
  }
@@ -235,7 +240,7 @@ class WhatsApp {
235
240
  // baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
236
241
  // (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
237
242
  // realmente está guardado, y el mensaje/poll no se encuentra.
238
- const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
243
+ const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null);
239
244
  if (pn) {
240
245
  // getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
241
246
  // que el JID coincida con el que usa el store (sin device).
@@ -719,13 +724,14 @@ class WhatsApp {
719
724
  continue;
720
725
  }
721
726
  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) {
727
+ const found = await this.#locate(key.remoteJid, key.id);
728
+ if (found) {
729
+ const { path, doc } = found;
724
730
  const next = receipt.playedTimestamp != null ? PLAYED : READ;
725
731
  if (doc.status < next) {
726
732
  doc.status = next;
727
733
  doc.raw.status = next;
728
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
734
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
729
735
  }
730
736
  const msg_instance = (0, message_1.message)(this, doc);
731
737
  this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
@@ -943,6 +949,13 @@ class WhatsApp {
943
949
  if (existing_doc.reactions) {
944
950
  doc.reactions = existing_doc.reactions;
945
951
  }
952
+ // El historial reporta el estado que tenía al sincronizarse: si acá se reescribe
953
+ // por otro cambio (edición, destacado), el estado ya conocido se conserva.
954
+ // History reports the state it had when synced: if the doc gets rewritten here
955
+ // for another change (edit, star), the state already known is kept.
956
+ if (existing_doc.status > doc.status) {
957
+ doc.status = existing_doc.status;
958
+ }
946
959
  if (existing_doc.status >= doc.status &&
947
960
  existing_doc.caption === doc.caption &&
948
961
  existing_doc.edited === doc.edited &&
@@ -1027,6 +1040,33 @@ class WhatsApp {
1027
1040
  }
1028
1041
  }
1029
1042
  }
1043
+ /**
1044
+ * Ubica el documento de un mensaje partiendo del chat crudo del key. Los updates y
1045
+ * receipts llegan direccionados por LID —con o sin dispositivo— mientras el documento
1046
+ * vive bajo el JID con el que se guardó, así que se prueban ambas formas.
1047
+ * Locates a message document from the raw chat in the key. Updates and receipts arrive
1048
+ * LID-addressed —with or without device— while the document lives under the JID it was
1049
+ * stored with, so both forms are tried.
1050
+ *
1051
+ * @param cid - Chat tal como viene en el key / Chat as it comes in the key
1052
+ * @param mid - Identificador del mensaje / Message identifier
1053
+ * @returns Ruta y documento, o null si no existe / Path and document, or null when missing
1054
+ * @internal
1055
+ */
1056
+ async #locate(cid, mid) {
1057
+ const tried = new Set();
1058
+ for (const candidate of [await this.#resolve_jid(cid), cid, (0, baileys_1.jidNormalizedUser)(cid)]) {
1059
+ if (candidate && !tried.has(candidate)) {
1060
+ tried.add(candidate);
1061
+ const path = `/chat/${candidate}/message/${mid}`;
1062
+ const doc = (0, store_1.deserialize)(await this.engine.get(path));
1063
+ if (doc) {
1064
+ return { path, doc };
1065
+ }
1066
+ }
1067
+ }
1068
+ return null;
1069
+ }
1030
1070
  /** @internal */
1031
1071
  async #handle_messages_update(updates) {
1032
1072
  for (const { key, update: upd } of updates) {
@@ -1038,8 +1078,9 @@ class WhatsApp {
1038
1078
  if (key.remoteJid === 'status@broadcast') {
1039
1079
  continue;
1040
1080
  }
1041
- const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
1042
- if (doc) {
1081
+ const found = await this.#locate(key.remoteJid, key.id);
1082
+ if (found) {
1083
+ const { path, doc } = found;
1043
1084
  const raw = doc.raw ?? { key };
1044
1085
  const upd_any = upd;
1045
1086
  const edited_message = upd_any.message?.editedMessage?.message;
@@ -1051,7 +1092,7 @@ class WhatsApp {
1051
1092
  doc.raw = raw;
1052
1093
  doc.edited = true;
1053
1094
  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);
1095
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1055
1096
  const msg_instance = (0, message_1.message)(this, doc);
1056
1097
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1057
1098
  }
@@ -1060,7 +1101,7 @@ class WhatsApp {
1060
1101
  raw.message = { ...raw.message, ...content_update };
1061
1102
  doc.raw = raw;
1062
1103
  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);
1104
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1064
1105
  const msg_instance = (0, message_1.message)(this, doc);
1065
1106
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1066
1107
  }
@@ -1068,15 +1109,28 @@ class WhatsApp {
1068
1109
  doc.starred = upd_any.starred === true;
1069
1110
  raw.starred = doc.starred;
1070
1111
  doc.raw = raw;
1071
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1112
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1072
1113
  const msg_instance = (0, message_1.message)(this, doc);
1073
1114
  this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
1115
+ // WhatsApp reemite los acks desordenados al reconectar (un `sent` después
1116
+ // de un `delivered`), así que el estado solo avanza; el rechazo (`error`)
1117
+ // es terminal y sí puede pisar lo que hubiera.
1118
+ // WhatsApp re-emits acks out of order on reconnect (a `sent` after a
1119
+ // `delivered`), so the state only moves forward; a rejection (`error`) is
1120
+ // terminal and may override whatever was there.
1074
1121
  }
1075
- else if (status !== undefined) {
1122
+ else if (status !== undefined && (status > doc.status || status === ERROR)) {
1076
1123
  raw.status = status;
1077
1124
  doc.status = status;
1125
+ // El rechazo del servidor viaja como stub del update; sin persistirlo el
1126
+ // mensaje queda en error sin decir por qué.
1127
+ // The server rejection travels as an update stub; without persisting it the
1128
+ // message stays in error without saying why.
1129
+ if (upd_any.messageStubParameters) {
1130
+ raw.messageStubParameters = upd_any.messageStubParameters;
1131
+ }
1078
1132
  doc.raw = raw;
1079
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1133
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1080
1134
  const msg_instance = (0, message_1.message)(this, doc);
1081
1135
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1082
1136
  }
@@ -1097,15 +1151,16 @@ class WhatsApp {
1097
1151
  }
1098
1152
  continue;
1099
1153
  }
1100
- const doc = (0, store_1.deserialize)(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
1101
- if (doc) {
1154
+ const found = await this.#locate(key.remoteJid, key.id);
1155
+ if (found) {
1156
+ const { path, doc } = found;
1102
1157
  const reactor = (0, baileys_1.jidNormalizedUser)(key.participant ?? key.remoteJid);
1103
1158
  const emoji = reaction.text ?? '';
1104
1159
  doc.reactions = [
1105
1160
  ...(doc.reactions ?? []).filter((r) => r.author !== reactor),
1106
1161
  ...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
1107
1162
  ];
1108
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, (0, store_1.serialize)(doc), doc.created_at);
1163
+ await this.engine.set(path, (0, store_1.serialize)(doc), doc.created_at);
1109
1164
  const msg_instance = (0, message_1.message)(this, doc);
1110
1165
  this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
1111
1166
  }
@@ -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
  */
@@ -13,6 +13,8 @@ import { bind } from '../../lib/internal.js';
13
13
  import { Audio, Document, Event, Image, Location, message, Message, Poll, Sticker, Text, VCard, Video, } from '../../lib/message/index.js';
14
14
  import { Feed, TTL_MS as FEED_TTL_MS } from '../../lib/status/index.js';
15
15
  import { deserialize, serialize } from '../../lib/store/index.js';
16
+ /** 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. */
17
+ const ERROR = 0;
16
18
  /** Estados legibles del mensaje que el receipt puede avanzar. / Readable message states a receipt can advance to. */
17
19
  const READ = 4;
18
20
  const PLAYED = 5;
@@ -39,7 +41,7 @@ function sniff_media(data) {
39
41
  * Main WhatsApp client. Does not connect on instantiation.
40
42
  *
41
43
  * @example
42
- * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 584144709840 });
44
+ * const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 5491112345678 });
43
45
  * wa.on('message:created', (msg) => console.log(msg.caption));
44
46
  * await wa.connect((code) => console.log(code));
45
47
  */
@@ -182,12 +184,15 @@ export class WhatsApp {
182
184
  result = uid;
183
185
  }
184
186
  else if (uid.endsWith('@lid')) {
185
- const direct = deserialize(await this.engine.get(`/lid/${uid}`));
187
+ // Los receipts direccionan por dispositivo (`…:9@lid`); el índice se guarda sin él.
188
+ // Receipts address per device (`…:9@lid`); the index is stored without it.
189
+ const lid = jidNormalizedUser(uid);
190
+ const direct = deserialize(await this.engine.get(`/lid/${lid}`));
186
191
  if (direct) {
187
192
  result = direct.includes('@') ? direct : `${direct}@s.whatsapp.net`;
188
193
  }
189
194
  else {
190
- const reverse = deserialize(await this.engine.get(`/lid/${uid.split('@')[0]}_reverse`));
195
+ const reverse = deserialize(await this.engine.get(`/lid/${lid.split('@')[0]}_reverse`));
191
196
  if (reverse != null) {
192
197
  result = `${reverse}@s.whatsapp.net`;
193
198
  }
@@ -196,7 +201,7 @@ export class WhatsApp {
196
201
  // baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
197
202
  // (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
198
203
  // realmente está guardado, y el mensaje/poll no se encuentra.
199
- const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
204
+ const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null);
200
205
  if (pn) {
201
206
  // getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
202
207
  // que el JID coincida con el que usa el store (sin device).
@@ -680,13 +685,14 @@ export class WhatsApp {
680
685
  continue;
681
686
  }
682
687
  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) {
688
+ const found = await this.#locate(key.remoteJid, key.id);
689
+ if (found) {
690
+ const { path, doc } = found;
685
691
  const next = receipt.playedTimestamp != null ? PLAYED : READ;
686
692
  if (doc.status < next) {
687
693
  doc.status = next;
688
694
  doc.raw.status = next;
689
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
695
+ await this.engine.set(path, serialize(doc), doc.created_at);
690
696
  }
691
697
  const msg_instance = message(this, doc);
692
698
  this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
@@ -904,6 +910,13 @@ export class WhatsApp {
904
910
  if (existing_doc.reactions) {
905
911
  doc.reactions = existing_doc.reactions;
906
912
  }
913
+ // El historial reporta el estado que tenía al sincronizarse: si acá se reescribe
914
+ // por otro cambio (edición, destacado), el estado ya conocido se conserva.
915
+ // History reports the state it had when synced: if the doc gets rewritten here
916
+ // for another change (edit, star), the state already known is kept.
917
+ if (existing_doc.status > doc.status) {
918
+ doc.status = existing_doc.status;
919
+ }
907
920
  if (existing_doc.status >= doc.status &&
908
921
  existing_doc.caption === doc.caption &&
909
922
  existing_doc.edited === doc.edited &&
@@ -988,6 +1001,33 @@ export class WhatsApp {
988
1001
  }
989
1002
  }
990
1003
  }
1004
+ /**
1005
+ * Ubica el documento de un mensaje partiendo del chat crudo del key. Los updates y
1006
+ * receipts llegan direccionados por LID —con o sin dispositivo— mientras el documento
1007
+ * vive bajo el JID con el que se guardó, así que se prueban ambas formas.
1008
+ * Locates a message document from the raw chat in the key. Updates and receipts arrive
1009
+ * LID-addressed —with or without device— while the document lives under the JID it was
1010
+ * stored with, so both forms are tried.
1011
+ *
1012
+ * @param cid - Chat tal como viene en el key / Chat as it comes in the key
1013
+ * @param mid - Identificador del mensaje / Message identifier
1014
+ * @returns Ruta y documento, o null si no existe / Path and document, or null when missing
1015
+ * @internal
1016
+ */
1017
+ async #locate(cid, mid) {
1018
+ const tried = new Set();
1019
+ for (const candidate of [await this.#resolve_jid(cid), cid, jidNormalizedUser(cid)]) {
1020
+ if (candidate && !tried.has(candidate)) {
1021
+ tried.add(candidate);
1022
+ const path = `/chat/${candidate}/message/${mid}`;
1023
+ const doc = deserialize(await this.engine.get(path));
1024
+ if (doc) {
1025
+ return { path, doc };
1026
+ }
1027
+ }
1028
+ }
1029
+ return null;
1030
+ }
991
1031
  /** @internal */
992
1032
  async #handle_messages_update(updates) {
993
1033
  for (const { key, update: upd } of updates) {
@@ -999,8 +1039,9 @@ export class WhatsApp {
999
1039
  if (key.remoteJid === 'status@broadcast') {
1000
1040
  continue;
1001
1041
  }
1002
- const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
1003
- if (doc) {
1042
+ const found = await this.#locate(key.remoteJid, key.id);
1043
+ if (found) {
1044
+ const { path, doc } = found;
1004
1045
  const raw = doc.raw ?? { key };
1005
1046
  const upd_any = upd;
1006
1047
  const edited_message = upd_any.message?.editedMessage?.message;
@@ -1012,7 +1053,7 @@ export class WhatsApp {
1012
1053
  doc.raw = raw;
1013
1054
  doc.edited = true;
1014
1055
  doc.caption = message(this, raw).caption;
1015
- 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);
1016
1057
  const msg_instance = message(this, doc);
1017
1058
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1018
1059
  }
@@ -1021,7 +1062,7 @@ export class WhatsApp {
1021
1062
  raw.message = { ...raw.message, ...content_update };
1022
1063
  doc.raw = raw;
1023
1064
  doc.caption = message(this, raw).caption;
1024
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1065
+ await this.engine.set(path, serialize(doc), doc.created_at);
1025
1066
  const msg_instance = message(this, doc);
1026
1067
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1027
1068
  }
@@ -1029,15 +1070,28 @@ export class WhatsApp {
1029
1070
  doc.starred = upd_any.starred === true;
1030
1071
  raw.starred = doc.starred;
1031
1072
  doc.raw = raw;
1032
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1073
+ await this.engine.set(path, serialize(doc), doc.created_at);
1033
1074
  const msg_instance = message(this, doc);
1034
1075
  this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
1076
+ // WhatsApp reemite los acks desordenados al reconectar (un `sent` después
1077
+ // de un `delivered`), así que el estado solo avanza; el rechazo (`error`)
1078
+ // es terminal y sí puede pisar lo que hubiera.
1079
+ // WhatsApp re-emits acks out of order on reconnect (a `sent` after a
1080
+ // `delivered`), so the state only moves forward; a rejection (`error`) is
1081
+ // terminal and may override whatever was there.
1035
1082
  }
1036
- else if (status !== undefined) {
1083
+ else if (status !== undefined && (status > doc.status || status === ERROR)) {
1037
1084
  raw.status = status;
1038
1085
  doc.status = status;
1086
+ // El rechazo del servidor viaja como stub del update; sin persistirlo el
1087
+ // mensaje queda en error sin decir por qué.
1088
+ // The server rejection travels as an update stub; without persisting it the
1089
+ // message stays in error without saying why.
1090
+ if (upd_any.messageStubParameters) {
1091
+ raw.messageStubParameters = upd_any.messageStubParameters;
1092
+ }
1039
1093
  doc.raw = raw;
1040
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1094
+ await this.engine.set(path, serialize(doc), doc.created_at);
1041
1095
  const msg_instance = message(this, doc);
1042
1096
  this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
1043
1097
  }
@@ -1058,15 +1112,16 @@ export class WhatsApp {
1058
1112
  }
1059
1113
  continue;
1060
1114
  }
1061
- const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
1062
- if (doc) {
1115
+ const found = await this.#locate(key.remoteJid, key.id);
1116
+ if (found) {
1117
+ const { path, doc } = found;
1063
1118
  const reactor = jidNormalizedUser(key.participant ?? key.remoteJid);
1064
1119
  const emoji = reaction.text ?? '';
1065
1120
  doc.reactions = [
1066
1121
  ...(doc.reactions ?? []).filter((r) => r.author !== reactor),
1067
1122
  ...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
1068
1123
  ];
1069
- await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
1124
+ await this.engine.set(path, serialize(doc), doc.created_at);
1070
1125
  const msg_instance = message(this, doc);
1071
1126
  this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
1072
1127
  }
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.1",
76
76
  "engines": {
77
77
  "node": ">=20"
78
78
  },