@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 +15 -13
- package/build/cjs/lib/message/index.d.ts +4 -0
- package/build/cjs/lib/message/index.js +26 -5
- package/build/cjs/lib/store/engine/lib/sqlite/index.d.ts +2 -2
- package/build/cjs/lib/store/engine/lib/sqlite/index.js +2 -2
- package/build/cjs/lib/whatsapp/index.d.ts +1 -1
- package/build/cjs/lib/whatsapp/index.js +72 -17
- package/build/esm/lib/message/index.d.ts +4 -0
- package/build/esm/lib/message/index.js +26 -5
- package/build/esm/lib/store/engine/lib/sqlite/index.d.ts +2 -2
- package/build/esm/lib/store/engine/lib/sqlite/index.js +2 -2
- package/build/esm/lib/whatsapp/index.d.ts +1 -1
- package/build/esm/lib/whatsapp/index.js +72 -17
- package/package.json +1 -1
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/
|
|
60
|
-
phone:
|
|
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: ['
|
|
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('
|
|
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 // '
|
|
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('
|
|
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/
|
|
323
|
-
new SQLiteEngine(new Database('.sessions/
|
|
324
|
-
new RedisEngine(new IORedis(), 'wa:
|
|
325
|
-
new S3Engine({ s3: new S3Client({}), bucket: 'sesiones', basedir: 'wa/
|
|
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('
|
|
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:
|
|
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:
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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/${
|
|
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(
|
|
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
|
|
723
|
-
if (
|
|
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(
|
|
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
|
|
1042
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
1101
|
-
if (
|
|
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(
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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/${
|
|
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(
|
|
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
|
|
684
|
-
if (
|
|
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(
|
|
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
|
|
1003
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
1062
|
-
if (
|
|
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(
|
|
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
|
}
|