@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 +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 +56 -16
- 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 +56 -16
- 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
|
*/
|
|
@@ -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:
|
|
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
|
-
|
|
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/${
|
|
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(
|
|
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
|
|
723
|
-
if (
|
|
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(
|
|
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
|
|
1042
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
1101
|
-
if (
|
|
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(
|
|
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
|
-
|
|
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
|
*/
|
|
@@ -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:
|
|
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
|
-
|
|
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/${
|
|
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(
|
|
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
|
|
684
|
-
if (
|
|
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(
|
|
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
|
|
1003
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
1062
|
-
if (
|
|
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(
|
|
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
|
}
|