@arcaelas/whatsapp 4.5.0 → 6.0.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 +282 -239
- package/build/cjs/decorators.js +0 -1
- package/build/cjs/index.d.ts +6 -4
- package/build/cjs/index.js +22 -3
- package/build/cjs/lib/bot/decorator.d.ts +2 -0
- package/build/cjs/lib/bot/decorator.js +6 -1
- package/build/cjs/lib/bot/decorators.d.ts +2 -2
- package/build/cjs/lib/bot/decorators.js +4 -4
- package/build/cjs/lib/bot/index.js +0 -1
- package/build/cjs/lib/chat/index.d.ts +381 -204
- package/build/cjs/lib/chat/index.js +243 -212
- package/build/cjs/lib/contact/index.d.ts +162 -179
- package/build/cjs/lib/contact/index.js +96 -191
- package/build/cjs/lib/internal.d.ts +40 -0
- package/build/cjs/lib/internal.js +38 -0
- package/build/cjs/lib/message/index.d.ts +468 -289
- package/build/cjs/lib/message/index.js +871 -743
- package/build/cjs/lib/status/index.d.ts +60 -70
- package/build/cjs/lib/status/index.js +106 -112
- package/build/cjs/lib/store/engine/index.d.ts +35 -7
- package/build/cjs/lib/store/engine/index.js +6 -4
- package/build/cjs/lib/store/engine/lib/file_system/index.d.ts +48 -8
- package/build/cjs/lib/store/engine/lib/file_system/index.js +117 -66
- package/build/cjs/lib/store/engine/lib/index.d.ts +112 -0
- package/build/cjs/lib/store/engine/lib/index.js +212 -0
- package/build/cjs/lib/store/engine/lib/redis/index.d.ts +44 -15
- package/build/cjs/lib/store/engine/lib/redis/index.js +75 -43
- package/build/cjs/lib/store/engine/lib/s3/index.d.ts +44 -1
- package/build/cjs/lib/store/engine/lib/s3/index.js +112 -65
- package/build/cjs/lib/store/engine/lib/sqlite/index.d.ts +110 -0
- package/build/cjs/lib/store/engine/lib/sqlite/index.js +132 -0
- package/build/cjs/lib/store/index.d.ts +7 -3
- package/build/cjs/lib/store/index.js +14 -5
- package/build/cjs/lib/whatsapp/index.d.ts +106 -68
- package/build/cjs/lib/whatsapp/index.js +393 -167
- package/build/esm/decorators.js +0 -1
- package/build/esm/index.d.ts +6 -4
- package/build/esm/index.js +5 -3
- package/build/esm/lib/bot/decorator.d.ts +2 -0
- package/build/esm/lib/bot/decorator.js +6 -1
- package/build/esm/lib/bot/decorators.d.ts +2 -2
- package/build/esm/lib/bot/decorators.js +4 -4
- package/build/esm/lib/bot/index.js +0 -1
- package/build/esm/lib/chat/index.d.ts +381 -204
- package/build/esm/lib/chat/index.js +243 -212
- package/build/esm/lib/contact/index.d.ts +162 -179
- package/build/esm/lib/contact/index.js +96 -190
- package/build/esm/lib/internal.d.ts +40 -0
- package/build/esm/lib/internal.js +34 -0
- package/build/esm/lib/message/index.d.ts +468 -289
- package/build/esm/lib/message/index.js +866 -738
- package/build/esm/lib/status/index.d.ts +60 -70
- package/build/esm/lib/status/index.js +105 -111
- package/build/esm/lib/store/engine/index.d.ts +35 -7
- package/build/esm/lib/store/engine/index.js +4 -3
- package/build/esm/lib/store/engine/lib/file_system/index.d.ts +48 -8
- package/build/esm/lib/store/engine/lib/file_system/index.js +117 -66
- package/build/esm/lib/store/engine/lib/index.d.ts +112 -0
- package/build/esm/lib/store/engine/lib/index.js +205 -0
- package/build/esm/lib/store/engine/lib/redis/index.d.ts +44 -15
- package/build/esm/lib/store/engine/lib/redis/index.js +73 -41
- package/build/esm/lib/store/engine/lib/s3/index.d.ts +44 -1
- package/build/esm/lib/store/engine/lib/s3/index.js +111 -64
- package/build/esm/lib/store/engine/lib/sqlite/index.d.ts +110 -0
- package/build/esm/lib/store/engine/lib/sqlite/index.js +128 -0
- package/build/esm/lib/store/index.d.ts +7 -3
- package/build/esm/lib/store/index.js +13 -5
- package/build/esm/lib/whatsapp/index.d.ts +106 -68
- package/build/esm/lib/whatsapp/index.js +395 -169
- package/package.json +25 -6
- package/build/cjs/decorators.js.map +0 -1
- package/build/cjs/index.js.map +0 -1
- package/build/cjs/lib/bot/decorator.js.map +0 -1
- package/build/cjs/lib/bot/decorators.js.map +0 -1
- package/build/cjs/lib/bot/index.js.map +0 -1
- package/build/cjs/lib/chat/index.js.map +0 -1
- package/build/cjs/lib/contact/index.js.map +0 -1
- package/build/cjs/lib/message/index.js.map +0 -1
- package/build/cjs/lib/status/index.js.map +0 -1
- package/build/cjs/lib/store/engine/index.js.map +0 -1
- package/build/cjs/lib/store/engine/lib/file_system/index.js.map +0 -1
- package/build/cjs/lib/store/engine/lib/redis/index.js.map +0 -1
- package/build/cjs/lib/store/engine/lib/s3/index.js.map +0 -1
- package/build/cjs/lib/store/index.js.map +0 -1
- package/build/cjs/lib/whatsapp/index.js.map +0 -1
- package/build/cjs/test.d.ts +0 -1
- package/build/cjs/test.js +0 -71
- package/build/cjs/test.js.map +0 -1
- package/build/esm/decorators.js.map +0 -1
- package/build/esm/index.js.map +0 -1
- package/build/esm/lib/bot/decorator.js.map +0 -1
- package/build/esm/lib/bot/decorators.js.map +0 -1
- package/build/esm/lib/bot/index.js.map +0 -1
- package/build/esm/lib/chat/index.js.map +0 -1
- package/build/esm/lib/contact/index.js.map +0 -1
- package/build/esm/lib/message/index.js.map +0 -1
- package/build/esm/lib/status/index.js.map +0 -1
- package/build/esm/lib/store/engine/index.js.map +0 -1
- package/build/esm/lib/store/engine/lib/file_system/index.js.map +0 -1
- package/build/esm/lib/store/engine/lib/redis/index.js.map +0 -1
- package/build/esm/lib/store/engine/lib/s3/index.js.map +0 -1
- package/build/esm/lib/store/index.js.map +0 -1
- package/build/esm/lib/whatsapp/index.js.map +0 -1
- package/build/esm/test.d.ts +0 -1
- package/build/esm/test.js +0 -66
- package/build/esm/test.js.map +0 -1
|
@@ -8,10 +8,32 @@ import { EventEmitter } from 'node:events';
|
|
|
8
8
|
import pino from 'pino';
|
|
9
9
|
import * as QRCode from 'qrcode';
|
|
10
10
|
import { chat } from '../../lib/chat/index.js';
|
|
11
|
-
import { contact
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
11
|
+
import { contact } from '../../lib/contact/index.js';
|
|
12
|
+
import { bind } from '../../lib/internal.js';
|
|
13
|
+
import { Audio, Document, Event, Image, Location, message, Message, Poll, Sticker, Text, VCard, Video, } from '../../lib/message/index.js';
|
|
14
|
+
import { Feed, TTL_MS as FEED_TTL_MS } from '../../lib/status/index.js';
|
|
14
15
|
import { deserialize, serialize } from '../../lib/store/index.js';
|
|
16
|
+
/** Estados legibles del mensaje que el receipt puede avanzar. / Readable message states a receipt can advance to. */
|
|
17
|
+
const READ = 4;
|
|
18
|
+
const PLAYED = 5;
|
|
19
|
+
/**
|
|
20
|
+
* Deduce el MIME de un binario por su firma, acotado a lo que WhatsApp acepta en un estado.
|
|
21
|
+
* Infers a binary's MIME from its signature, limited to what WhatsApp accepts in a status.
|
|
22
|
+
*
|
|
23
|
+
* @param data - Binario a inspeccionar / Binary to inspect
|
|
24
|
+
* @returns MIME reconocido, o null / Recognized MIME, or null
|
|
25
|
+
*/
|
|
26
|
+
function sniff_media(data) {
|
|
27
|
+
if (data.subarray(0, 3).toString('hex') === 'ffd8ff')
|
|
28
|
+
return 'image/jpeg';
|
|
29
|
+
if (data.subarray(0, 8).toString('hex') === '89504e470d0a1a0a')
|
|
30
|
+
return 'image/png';
|
|
31
|
+
if (data.subarray(0, 4).toString() === 'RIFF' && data.subarray(8, 12).toString() === 'WEBP')
|
|
32
|
+
return 'image/webp';
|
|
33
|
+
if (data.subarray(4, 8).toString() === 'ftyp')
|
|
34
|
+
return 'video/mp4';
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
15
37
|
/**
|
|
16
38
|
* Cliente principal de WhatsApp. No inicia la conexión al instanciar.
|
|
17
39
|
* Main WhatsApp client. Does not connect on instantiation.
|
|
@@ -22,55 +44,121 @@ import { deserialize, serialize } from '../../lib/store/index.js';
|
|
|
22
44
|
* await wa.connect((code) => console.log(code));
|
|
23
45
|
*/
|
|
24
46
|
export class WhatsApp {
|
|
47
|
+
/** @internal Emisor de los eventos del cliente. / Client event emitter. */
|
|
48
|
+
#event = new EventEmitter();
|
|
49
|
+
/** @internal Estado compartido con las entidades (socket, resolución de JIDs). / State shared with the entities. */
|
|
50
|
+
#internals;
|
|
51
|
+
#phone;
|
|
52
|
+
#method;
|
|
53
|
+
#autoclean;
|
|
54
|
+
#reconnect;
|
|
55
|
+
#sync;
|
|
56
|
+
#intentional_close = false;
|
|
57
|
+
#silent_close = false;
|
|
58
|
+
#has_connected = false;
|
|
59
|
+
#retry_timer = null;
|
|
60
|
+
#retry_count = 0;
|
|
61
|
+
/**
|
|
62
|
+
* @internal
|
|
63
|
+
* Cadena que serializa los handlers de eventos de baileys: dos eventos sobre el mismo
|
|
64
|
+
* documento ya no se intercalan (lost updates por read-modify-write concurrente).
|
|
65
|
+
* Chain serializing baileys event handlers: two events over the same document no longer
|
|
66
|
+
* interleave (lost updates from concurrent read-modify-write).
|
|
67
|
+
*/
|
|
68
|
+
#chain = Promise.resolve();
|
|
25
69
|
constructor(options) {
|
|
26
|
-
/** @internal */
|
|
27
|
-
this._socket = null;
|
|
28
|
-
this._intentional_close = false;
|
|
29
|
-
this._has_connected = false;
|
|
30
|
-
this._retry_timer = null;
|
|
31
|
-
this._retry_count = 0;
|
|
32
70
|
this.engine = options.engine;
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
this
|
|
36
|
-
this
|
|
37
|
-
this
|
|
38
|
-
|
|
39
|
-
|
|
71
|
+
this.#phone = options.phone !== undefined ? String(options.phone).replace(/\D+/g, '') : undefined;
|
|
72
|
+
this.#method = options.method;
|
|
73
|
+
this.#autoclean = options.autoclean ?? true;
|
|
74
|
+
this.#sync = options.sync ?? true;
|
|
75
|
+
this.#reconnect =
|
|
76
|
+
options.reconnect === false ? { max: 0, interval_ms: 60_000 }
|
|
77
|
+
: options.reconnect === undefined || options.reconnect === true ? { max: null, interval_ms: 60_000 }
|
|
78
|
+
: typeof options.reconnect === 'number' ? { max: options.reconnect, interval_ms: 60_000 }
|
|
79
|
+
: { max: options.reconnect.max ?? null, interval_ms: (options.reconnect.interval ?? 60) * 1_000 };
|
|
80
|
+
this.#internals = { socket: null, resolve_jid: (uid) => this.#resolve_jid(uid) };
|
|
81
|
+
bind(this, this.#internals);
|
|
40
82
|
this.Contact = contact(this);
|
|
41
83
|
this.Chat = chat(this);
|
|
42
|
-
this.Message =
|
|
84
|
+
this.Message = {
|
|
85
|
+
get: (cid, mid) => Message.get(this, cid, mid),
|
|
86
|
+
list: (cid, offset, limit) => Message.list(this, cid, offset, limit),
|
|
87
|
+
text: (cid, ...rest) => Message.text(this, cid, ...rest),
|
|
88
|
+
image: (cid, ...rest) => Message.image(this, cid, ...rest),
|
|
89
|
+
video: (cid, ...rest) => Message.video(this, cid, ...rest),
|
|
90
|
+
audio: (cid, ...rest) => Message.audio(this, cid, ...rest),
|
|
91
|
+
location: (cid, ...rest) => Message.location(this, cid, ...rest),
|
|
92
|
+
poll: (cid, ...rest) => Message.poll(this, cid, ...rest),
|
|
93
|
+
document: (cid, ...rest) => Message.document(this, cid, ...rest),
|
|
94
|
+
vcard: (cid, ...rest) => Message.vcard(this, cid, ...rest),
|
|
95
|
+
event: (cid, ...rest) => Message.event(this, cid, ...rest),
|
|
96
|
+
react: (cid, mid, emoji) => Message.react(this, cid, mid, emoji),
|
|
97
|
+
star: (cid, mid, value) => Message.star(this, cid, mid, value),
|
|
98
|
+
seen: (cid, mid) => Message.seen(this, cid, mid),
|
|
99
|
+
edit: (cid, mid, caption) => Message.edit(this, cid, mid, caption),
|
|
100
|
+
forward: (cid, mid, target) => Message.forward(this, cid, mid, target),
|
|
101
|
+
delete: (cid, mid, all) => Message.delete(this, cid, mid, all),
|
|
102
|
+
reactions: (cid, mid) => Message.reactions(this, cid, mid),
|
|
103
|
+
Text, Image, Video, Audio, Sticker, Document, Location, Poll, VCard, Event,
|
|
104
|
+
};
|
|
43
105
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Contacto de la cuenta autenticada, o null mientras no hay sesión abierta.
|
|
108
|
+
* Authenticated account's contact, or null while there is no open session.
|
|
109
|
+
*/
|
|
110
|
+
get contact() {
|
|
111
|
+
const user = this.#internals.socket?.user;
|
|
112
|
+
if (user) {
|
|
113
|
+
const jid = jidNormalizedUser(user.id);
|
|
114
|
+
return new this.Contact({ id: jid, phone_number: jid, lid: user.lid ?? null, name: user.name ?? null });
|
|
48
115
|
}
|
|
49
|
-
|
|
50
|
-
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* @internal
|
|
120
|
+
* Persiste un binario: crudo cuando el driver lo soporta, JSON con base64 si no.
|
|
121
|
+
* Persists a binary: raw when the driver supports it, base64 JSON otherwise.
|
|
122
|
+
*/
|
|
123
|
+
async #write_content(path, data) {
|
|
124
|
+
if (this.engine.set_buffer) {
|
|
125
|
+
await this.engine.set_buffer(path, data);
|
|
51
126
|
}
|
|
52
|
-
|
|
53
|
-
|
|
127
|
+
else {
|
|
128
|
+
await this.engine.set(path, serialize({ data: data.toString('base64') }));
|
|
54
129
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
130
|
+
}
|
|
131
|
+
/** @internal Encola una tarea en la cadena serial de handlers. / Queues a task on the serial handler chain. */
|
|
132
|
+
#enqueue(task) {
|
|
133
|
+
this.#chain = this.#chain.then(task).catch(() => { });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Emite un evento del cliente. Lo usan las entidades de la librería para propagar los
|
|
137
|
+
* cambios que provocan; el consumidor puede emitir los suyos para pruebas.
|
|
138
|
+
* Emits a client event. Library entities use it to propagate the changes they cause;
|
|
139
|
+
* consumers may emit their own for testing.
|
|
140
|
+
*
|
|
141
|
+
* @param event - Nombre del evento / Event name
|
|
142
|
+
* @param args - Argumentos del evento / Event arguments
|
|
143
|
+
* @returns true si había listeners / true when listeners were present
|
|
144
|
+
*/
|
|
145
|
+
emit(event, ...args) {
|
|
146
|
+
return this.#event.emit(event, ...args);
|
|
59
147
|
}
|
|
60
148
|
/**
|
|
61
149
|
* Registra un listener de evento. Retorna función para desuscribirse.
|
|
62
150
|
* Registers an event listener. Returns an unsubscribe function.
|
|
63
151
|
*/
|
|
64
152
|
on(event, handler) {
|
|
65
|
-
this.
|
|
66
|
-
return () => { this.
|
|
153
|
+
this.#event.on(event, handler);
|
|
154
|
+
return () => { this.#event.off(event, handler); };
|
|
67
155
|
}
|
|
68
156
|
/**
|
|
69
157
|
* Quita un listener previamente registrado.
|
|
70
158
|
* Removes a previously registered listener.
|
|
71
159
|
*/
|
|
72
160
|
off(event, handler) {
|
|
73
|
-
this.
|
|
161
|
+
this.#event.off(event, handler);
|
|
74
162
|
return this;
|
|
75
163
|
}
|
|
76
164
|
/**
|
|
@@ -78,15 +166,17 @@ export class WhatsApp {
|
|
|
78
166
|
* Registers a one-shot listener. Returns an unsubscribe function.
|
|
79
167
|
*/
|
|
80
168
|
once(event, handler) {
|
|
81
|
-
this.
|
|
82
|
-
return () => { this.
|
|
169
|
+
this.#event.once(event, handler);
|
|
170
|
+
return () => { this.#event.off(event, handler); };
|
|
83
171
|
}
|
|
84
172
|
/**
|
|
85
173
|
* @internal
|
|
86
|
-
* Normaliza cualquier identificador (JID, LID, número, etc.) a JID canónico.
|
|
87
|
-
*
|
|
174
|
+
* Normaliza cualquier identificador (JID, LID, número, etc.) a JID canónico. Las
|
|
175
|
+
* entidades lo alcanzan por el canal interno, no por la instancia.
|
|
176
|
+
* Normalizes any identifier (JID, LID, number…) into a canonical JID. Entities reach it
|
|
177
|
+
* through the internal channel, not through the instance.
|
|
88
178
|
*/
|
|
89
|
-
async
|
|
179
|
+
async #resolve_jid(uid) {
|
|
90
180
|
let result = null;
|
|
91
181
|
if (uid.endsWith('@g.us') || uid.endsWith('@s.whatsapp.net')) {
|
|
92
182
|
result = uid;
|
|
@@ -106,7 +196,7 @@ export class WhatsApp {
|
|
|
106
196
|
// baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
|
|
107
197
|
// (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
|
|
108
198
|
// realmente está guardado, y el mensaje/poll no se encuentra.
|
|
109
|
-
const pn = await this.
|
|
199
|
+
const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
|
|
110
200
|
if (pn) {
|
|
111
201
|
// getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
|
|
112
202
|
// que el JID coincida con el que usa el store (sin device).
|
|
@@ -131,16 +221,20 @@ export class WhatsApp {
|
|
|
131
221
|
* Resolves once the session is synced; retries on non-loggedOut disconnects.
|
|
132
222
|
*/
|
|
133
223
|
async connect(callback) {
|
|
224
|
+
if (this.#internals.socket) {
|
|
225
|
+
await this.disconnect({ silent: true });
|
|
226
|
+
}
|
|
134
227
|
const { version } = await fetchLatestBaileysVersion();
|
|
135
|
-
this
|
|
136
|
-
this
|
|
228
|
+
this.#intentional_close = false;
|
|
229
|
+
this.#silent_close = false;
|
|
230
|
+
this.#has_connected = false;
|
|
137
231
|
return new Promise((resolve, reject) => {
|
|
138
232
|
const start = async () => {
|
|
139
233
|
// Re-lee creds en cada start() para que limpiezas del engine tomen efecto
|
|
140
234
|
// en reintentos (permite al consumer forzar nueva sesión borrando /session/creds).
|
|
141
235
|
const stored = await this.engine.get('/session/creds');
|
|
142
236
|
const creds = deserialize(stored) ?? initAuthCreds();
|
|
143
|
-
this.
|
|
237
|
+
this.#internals.socket = makeWASocket({
|
|
144
238
|
version,
|
|
145
239
|
auth: {
|
|
146
240
|
creds,
|
|
@@ -167,19 +261,19 @@ export class WhatsApp {
|
|
|
167
261
|
},
|
|
168
262
|
browser: Browsers.windows('Chrome'),
|
|
169
263
|
logger: pino({ level: 'silent' }),
|
|
170
|
-
syncFullHistory: this
|
|
264
|
+
syncFullHistory: this.#sync,
|
|
171
265
|
// Los syncs no-FULL cargan las LID mappings y los tctokens (trusted-contact
|
|
172
|
-
// tokens) que rc13 exige para enviar: apagarlos todos con `() => this
|
|
266
|
+
// tokens) que rc13 exige para enviar: apagarlos todos con `() => this.#sync`
|
|
173
267
|
// deja la sesión sin tctoken y el server rechaza los mensajes con
|
|
174
268
|
// "error 463: account restricted or missing tctoken". Por eso se procesan
|
|
175
269
|
// siempre los no-FULL; `sync` solo decide si además se trae el historial FULL.
|
|
176
270
|
// Non-FULL syncs carry the LID mappings and tctokens rc13 requires to send;
|
|
177
271
|
// disabling them all made the server reject messages with error 463. We always
|
|
178
272
|
// process non-FULL; `sync` only gates whether FULL history is pulled too.
|
|
179
|
-
shouldSyncHistoryMessage: ({ syncType }) => this
|
|
273
|
+
shouldSyncHistoryMessage: ({ syncType }) => this.#sync || syncType !== proto.HistorySync.HistorySyncType.FULL,
|
|
180
274
|
markOnlineOnConnect: false,
|
|
181
275
|
});
|
|
182
|
-
const socket = this.
|
|
276
|
+
const socket = this.#internals.socket;
|
|
183
277
|
socket.ev.on('creds.update', () => this.engine.set('/session/creds', serialize(creds)));
|
|
184
278
|
socket.ev.on('connection.update', async (update) => {
|
|
185
279
|
const { connection, lastDisconnect, qr } = update;
|
|
@@ -187,24 +281,25 @@ export class WhatsApp {
|
|
|
187
281
|
// Baileys refresca QR periódicamente (~20s). Emitimos nuevo pair
|
|
188
282
|
// code / QR en cada refresh para que el usuario pueda renovar si
|
|
189
283
|
// el anterior expiró.
|
|
190
|
-
//
|
|
191
|
-
// `
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
284
|
+
// El PIN necesita número: sin `phone` la vinculación es siempre QR, y
|
|
285
|
+
// con `phone` manda `method` (OTP por defecto).
|
|
286
|
+
// A PIN requires a number: without `phone` linking is always QR, and with
|
|
287
|
+
// `phone` the `method` option decides (OTP by default).
|
|
288
|
+
if (this.#phone && (this.#method ?? 'otp') === 'otp') {
|
|
289
|
+
await callback(await socket.requestPairingCode(this.#phone));
|
|
195
290
|
}
|
|
196
291
|
else {
|
|
197
292
|
await callback(await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
|
|
198
293
|
}
|
|
199
294
|
}
|
|
200
295
|
if (connection === 'open') {
|
|
201
|
-
this
|
|
202
|
-
this
|
|
203
|
-
this.
|
|
296
|
+
this.#has_connected = true;
|
|
297
|
+
this.#retry_count = 0;
|
|
298
|
+
this.emit('connected', this);
|
|
204
299
|
resolve();
|
|
205
300
|
}
|
|
206
301
|
else if (connection === 'close') {
|
|
207
|
-
this.
|
|
302
|
+
this.#internals.socket = null;
|
|
208
303
|
const status_code = lastDisconnect?.error?.output?.statusCode;
|
|
209
304
|
// `restartRequired` (515) es una reconexión exigida por el protocolo
|
|
210
305
|
// tras el sync inicial — no es un disconnect "real".
|
|
@@ -212,35 +307,37 @@ export class WhatsApp {
|
|
|
212
307
|
// Limpieza del engine ANTES de emitir `disconnected` para que los
|
|
213
308
|
// listeners vean el estado final (engine vaciado o creds borradas).
|
|
214
309
|
if (status_code === DisconnectReason.loggedOut) {
|
|
215
|
-
if (this
|
|
310
|
+
if (this.#autoclean) {
|
|
216
311
|
await this.engine.clear();
|
|
217
312
|
}
|
|
218
313
|
else {
|
|
219
314
|
await this.engine.unset('/session/creds');
|
|
220
315
|
}
|
|
221
316
|
}
|
|
222
|
-
|
|
223
|
-
|
|
317
|
+
// `disconnect({ silent: true })` calla el evento de este cierre concreto.
|
|
318
|
+
// `disconnect({ silent: true })` mutes this specific close's event.
|
|
319
|
+
if (this.#has_connected && !is_transient && !this.#silent_close) {
|
|
320
|
+
this.emit('disconnected', this);
|
|
224
321
|
}
|
|
225
|
-
if (!this
|
|
322
|
+
if (!this.#intentional_close) {
|
|
226
323
|
if (status_code === DisconnectReason.loggedOut) {
|
|
227
324
|
reject(new Error('Logged out'));
|
|
228
325
|
}
|
|
229
326
|
else {
|
|
230
|
-
const max = this.
|
|
327
|
+
const max = this.#reconnect.max;
|
|
231
328
|
// Transient closes (restartRequired) son parte del protocolo,
|
|
232
329
|
// no cuentan contra el límite de reintentos por fallo.
|
|
233
|
-
const exhausted = !is_transient && max !== null && this
|
|
330
|
+
const exhausted = !is_transient && max !== null && this.#retry_count >= max;
|
|
234
331
|
if (exhausted) {
|
|
235
332
|
reject(new Error(`Reconnect attempts exhausted (${max})`));
|
|
236
333
|
}
|
|
237
334
|
else {
|
|
238
335
|
if (!is_transient) {
|
|
239
|
-
this
|
|
336
|
+
this.#retry_count++;
|
|
240
337
|
}
|
|
241
|
-
const delay = is_transient ? 0 : this.
|
|
242
|
-
this
|
|
243
|
-
this
|
|
338
|
+
const delay = is_transient ? 0 : this.#reconnect.interval_ms;
|
|
339
|
+
this.#retry_timer = setTimeout(() => {
|
|
340
|
+
this.#retry_timer = null;
|
|
244
341
|
start().catch(reject);
|
|
245
342
|
}, delay);
|
|
246
343
|
}
|
|
@@ -248,24 +345,119 @@ export class WhatsApp {
|
|
|
248
345
|
}
|
|
249
346
|
}
|
|
250
347
|
});
|
|
251
|
-
this
|
|
348
|
+
this.#attach_business_handlers(socket);
|
|
252
349
|
};
|
|
253
350
|
start().catch(reject);
|
|
254
351
|
});
|
|
255
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Actualiza el perfil de la cuenta en WhatsApp: nombre público, bio y/o foto. Sólo se
|
|
355
|
+
* envía lo que llega en el parche; `photo: null` elimina la foto actual.
|
|
356
|
+
* Updates the account profile on WhatsApp: public name, bio and/or picture. Only the
|
|
357
|
+
* given fields are sent; `photo: null` removes the current picture.
|
|
358
|
+
*
|
|
359
|
+
* @param patch - Campos a actualizar / Fields to update
|
|
360
|
+
* @returns true si todo lo pedido se envió / true when everything requested was sent
|
|
361
|
+
* @throws ERR_PROFILE_PICTURE_LIB si falta `sharp` o `jimp` para procesar la foto / when `sharp` or `jimp` is missing to process the picture
|
|
362
|
+
*/
|
|
363
|
+
async profile(patch) {
|
|
364
|
+
const socket = this.#internals.socket;
|
|
365
|
+
let ok = false;
|
|
366
|
+
if (socket) {
|
|
367
|
+
const self = jidNormalizedUser(socket.user?.id ?? '');
|
|
368
|
+
if (patch.name !== undefined) {
|
|
369
|
+
await socket.updateProfileName(patch.name);
|
|
370
|
+
}
|
|
371
|
+
if (patch.content !== undefined) {
|
|
372
|
+
await socket.updateProfileStatus(patch.content);
|
|
373
|
+
}
|
|
374
|
+
if (patch.photo === null) {
|
|
375
|
+
await socket.removeProfilePicture(self);
|
|
376
|
+
}
|
|
377
|
+
else if (patch.photo !== undefined) {
|
|
378
|
+
// baileys redimensiona la foto con `sharp` o `jimp`; ninguna es dependencia
|
|
379
|
+
// nuestra, así que su ausencia se traduce a un error accionable.
|
|
380
|
+
// baileys resizes the picture with `sharp` or `jimp`; neither is a dependency
|
|
381
|
+
// of ours, so their absence is translated into an actionable error.
|
|
382
|
+
await socket
|
|
383
|
+
.updateProfilePicture(self, typeof patch.photo === 'string' ? { url: patch.photo } : patch.photo)
|
|
384
|
+
.catch((error) => {
|
|
385
|
+
if (/image processing library/i.test(error.message)) {
|
|
386
|
+
throw new Error('ERR_PROFILE_PICTURE_LIB');
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
ok = true;
|
|
392
|
+
}
|
|
393
|
+
return ok;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Publica un estado (status broadcast). Con sólo `caption` publica texto; con `content`
|
|
397
|
+
* publica imagen o video (el tipo se deduce del binario) usando `caption` como pie.
|
|
398
|
+
* `contacts` es la audiencia: WhatsApp no reparte el estado a quien no esté en la lista.
|
|
399
|
+
* Publishes a status broadcast. With only `caption` it posts text; with `content` it
|
|
400
|
+
* posts an image or video (type inferred from the binary) using `caption` as its footer.
|
|
401
|
+
* `contacts` is the audience: WhatsApp does not deliver the status to anyone outside it.
|
|
402
|
+
*
|
|
403
|
+
* @param post - Contenido, pie y audiencia / Content, caption and audience
|
|
404
|
+
* @returns Publicación creada, o null si no hay sesión / Created post, or null without a session
|
|
405
|
+
* @throws ERR_FEED_EMPTY sin `content` ni `caption` / when neither `content` nor `caption` is given
|
|
406
|
+
* @throws ERR_FEED_MEDIA si el binario no es imagen ni video / when the binary is neither image nor video
|
|
407
|
+
*/
|
|
408
|
+
async feed(post) {
|
|
409
|
+
const socket = this.#internals.socket;
|
|
410
|
+
let result = null;
|
|
411
|
+
if (socket) {
|
|
412
|
+
const audience = (await Promise.all(post.contacts.map((uid) => this.#resolve_jid(String(uid))))).filter((jid) => jid !== null);
|
|
413
|
+
const mime = post.content ? sniff_media(post.content) : null;
|
|
414
|
+
if (post.content && !mime) {
|
|
415
|
+
throw new Error('ERR_FEED_MEDIA');
|
|
416
|
+
}
|
|
417
|
+
if (!post.content && !post.caption) {
|
|
418
|
+
throw new Error('ERR_FEED_EMPTY');
|
|
419
|
+
}
|
|
420
|
+
const kind = mime?.startsWith('video/') ? 'video' : mime ? 'image' : 'text';
|
|
421
|
+
const sent = await socket.sendMessage('status@broadcast', (post.content
|
|
422
|
+
? { [kind]: post.content, caption: post.caption }
|
|
423
|
+
: { text: post.caption }), { statusJidList: audience });
|
|
424
|
+
if (sent?.key?.id) {
|
|
425
|
+
const created_at = (Number(sent.messageTimestamp) || Math.floor(Date.now() / 1_000)) * 1_000;
|
|
426
|
+
const doc = {
|
|
427
|
+
id: sent.key.id,
|
|
428
|
+
author_jid: jidNormalizedUser(socket.user?.id ?? ''),
|
|
429
|
+
type: kind,
|
|
430
|
+
caption: post.caption ?? '',
|
|
431
|
+
mime: mime ?? 'text/plain',
|
|
432
|
+
created_at,
|
|
433
|
+
expires_at: created_at + FEED_TTL_MS,
|
|
434
|
+
viewed: true,
|
|
435
|
+
raw: sent,
|
|
436
|
+
};
|
|
437
|
+
await this.engine.set(`/status/${doc.id}`, serialize(doc), created_at);
|
|
438
|
+
if (post.content) {
|
|
439
|
+
await this.#write_content(`/status/${doc.id}/content`, post.content);
|
|
440
|
+
}
|
|
441
|
+
result = new Feed(this, doc);
|
|
442
|
+
this.emit('feed:created', result, this);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
256
447
|
/**
|
|
257
448
|
* Cierra la conexión. Con `destroy: true` vacía el engine completo.
|
|
258
449
|
* Closes the connection. With `destroy: true` clears the engine entirely.
|
|
259
450
|
*/
|
|
260
451
|
async disconnect(options = {}) {
|
|
261
|
-
this
|
|
452
|
+
this.#intentional_close = true;
|
|
453
|
+
this.#silent_close = options.silent === true;
|
|
262
454
|
// Cancela cualquier retry programado por un close anterior, para no resucitar
|
|
263
455
|
// el socket después de una desconexión manual.
|
|
264
|
-
if (this
|
|
265
|
-
clearTimeout(this
|
|
266
|
-
this
|
|
456
|
+
if (this.#retry_timer) {
|
|
457
|
+
clearTimeout(this.#retry_timer);
|
|
458
|
+
this.#retry_timer = null;
|
|
267
459
|
}
|
|
268
|
-
if (this.
|
|
460
|
+
if (this.#internals.socket) {
|
|
269
461
|
try {
|
|
270
462
|
// Pasa un error Boom-like con statusCode=connectionClosed (428) para que
|
|
271
463
|
// `lastDisconnect.error.output.statusCode` quede explícito en el close
|
|
@@ -273,17 +465,16 @@ export class WhatsApp {
|
|
|
273
465
|
const intentional = Object.assign(new Error('intentional close'), {
|
|
274
466
|
output: { statusCode: DisconnectReason.connectionClosed },
|
|
275
467
|
});
|
|
276
|
-
await this.
|
|
468
|
+
await this.#internals.socket.end(intentional);
|
|
277
469
|
}
|
|
278
470
|
catch {
|
|
279
471
|
/* socket may already be closed */
|
|
280
472
|
}
|
|
281
|
-
this.
|
|
473
|
+
this.#internals.socket = null;
|
|
282
474
|
}
|
|
283
475
|
if (options.destroy) {
|
|
284
476
|
await this.engine.clear();
|
|
285
477
|
}
|
|
286
|
-
void options.silent;
|
|
287
478
|
}
|
|
288
479
|
/**
|
|
289
480
|
* Conecta los handlers de eventos de baileys: contactos, chats y mensajes.
|
|
@@ -291,43 +482,45 @@ export class WhatsApp {
|
|
|
291
482
|
*
|
|
292
483
|
* @internal
|
|
293
484
|
*/
|
|
294
|
-
|
|
295
|
-
socket.ev.on('messaging-history.set',
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
485
|
+
#attach_business_handlers(socket) {
|
|
486
|
+
socket.ev.on('messaging-history.set', ({ chats, contacts, messages }) => {
|
|
487
|
+
this.#enqueue(async () => {
|
|
488
|
+
await this.#handle_contacts_upsert(contacts);
|
|
489
|
+
await this.#handle_chats_upsert(chats);
|
|
490
|
+
await this.#handle_messages_upsert(messages);
|
|
491
|
+
});
|
|
299
492
|
});
|
|
300
493
|
socket.ev.on('contacts.upsert', (contacts) => {
|
|
301
|
-
|
|
494
|
+
this.#enqueue(() => this.#handle_contacts_upsert(contacts));
|
|
302
495
|
});
|
|
303
496
|
socket.ev.on('contacts.update', (contacts) => {
|
|
304
|
-
|
|
497
|
+
this.#enqueue(() => this.#handle_contacts_update(contacts));
|
|
305
498
|
});
|
|
306
499
|
socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
|
|
307
|
-
|
|
500
|
+
this.#enqueue(() => this.#handle_lid_mapping(lid, pn));
|
|
308
501
|
});
|
|
309
502
|
socket.ev.on('chats.upsert', (chats) => {
|
|
310
|
-
|
|
503
|
+
this.#enqueue(() => this.#handle_chats_upsert(chats));
|
|
311
504
|
});
|
|
312
505
|
socket.ev.on('chats.update', (chats) => {
|
|
313
|
-
|
|
506
|
+
this.#enqueue(() => this.#handle_chats_update(chats));
|
|
314
507
|
});
|
|
315
508
|
socket.ev.on('chats.delete', (ids) => {
|
|
316
|
-
|
|
509
|
+
this.#enqueue(() => this.#handle_chats_delete(ids));
|
|
317
510
|
});
|
|
318
511
|
socket.ev.on('messages.upsert', ({ messages }) => {
|
|
319
|
-
|
|
512
|
+
this.#enqueue(() => this.#handle_messages_upsert(messages));
|
|
320
513
|
});
|
|
321
514
|
socket.ev.on('messages.update', (updates) => {
|
|
322
|
-
|
|
515
|
+
this.#enqueue(() => this.#handle_messages_update(updates));
|
|
323
516
|
});
|
|
324
517
|
socket.ev.on('message-receipt.update', (updates) => {
|
|
325
|
-
|
|
518
|
+
this.#enqueue(() => this.#handle_message_receipt(updates));
|
|
326
519
|
});
|
|
327
520
|
// Las reacciones llegan duplicadas por `messages.reaction` Y `messages.upsert`
|
|
328
521
|
// (como `reactionMessage`). Se usa solo `messages.upsert` para evitar el doble disparo.
|
|
329
522
|
// socket.ev.on('messages.reaction', (reactions) => {
|
|
330
|
-
// void this
|
|
523
|
+
// void this.#handle_messages_reaction(reactions);
|
|
331
524
|
// });
|
|
332
525
|
}
|
|
333
526
|
/**
|
|
@@ -337,23 +530,23 @@ export class WhatsApp {
|
|
|
337
530
|
* @param raw - Documento del contacto a persistir / Contact document to persist
|
|
338
531
|
* @internal
|
|
339
532
|
*/
|
|
340
|
-
async
|
|
533
|
+
async #persist_contact(raw) {
|
|
341
534
|
const existing = await this.engine.get(`/contact/${raw.id}`);
|
|
342
535
|
await this.engine.set(`/contact/${raw.id}`, serialize(raw));
|
|
343
536
|
if (raw.lid) {
|
|
344
537
|
await this.engine.set(`/lid/${raw.lid}`, serialize(raw.id));
|
|
345
538
|
}
|
|
346
539
|
if (existing === null) {
|
|
540
|
+
const person = new this.Contact(raw);
|
|
347
541
|
const cached_chat = deserialize(await this.engine.get(`/chat/${raw.id}`));
|
|
348
|
-
|
|
349
|
-
this._event.emit('contact:created', new this.Contact(raw, chat_instance), chat_instance, this);
|
|
542
|
+
this.emit('contact:created', person, new this.Chat(cached_chat ?? { id: raw.id, name: person.name }), this);
|
|
350
543
|
}
|
|
351
544
|
}
|
|
352
545
|
/** @internal */
|
|
353
|
-
async
|
|
546
|
+
async #handle_contacts_upsert(contacts) {
|
|
354
547
|
for (const c of contacts) {
|
|
355
548
|
if (c.id) {
|
|
356
|
-
await this
|
|
549
|
+
await this.#persist_contact({
|
|
357
550
|
id: c.id,
|
|
358
551
|
lid: c.lid ?? null,
|
|
359
552
|
name: c.name ?? null,
|
|
@@ -366,7 +559,7 @@ export class WhatsApp {
|
|
|
366
559
|
}
|
|
367
560
|
}
|
|
368
561
|
/** @internal */
|
|
369
|
-
async
|
|
562
|
+
async #handle_contacts_update(contacts) {
|
|
370
563
|
for (const c of contacts) {
|
|
371
564
|
if (c.id) {
|
|
372
565
|
const current = deserialize(await this.engine.get(`/contact/${c.id}`));
|
|
@@ -384,21 +577,21 @@ export class WhatsApp {
|
|
|
384
577
|
if (patch.lid) {
|
|
385
578
|
await this.engine.set(`/lid/${patch.lid}`, serialize(c.id));
|
|
386
579
|
}
|
|
580
|
+
const person = new this.Contact(merged);
|
|
387
581
|
const cached_chat = deserialize(await this.engine.get(`/chat/${c.id}`));
|
|
388
|
-
|
|
389
|
-
this._event.emit('contact:updated', new this.Contact(merged, chat_instance), chat_instance, this);
|
|
582
|
+
this.emit('contact:updated', person, new this.Chat(cached_chat ?? { id: c.id, name: person.name }), this);
|
|
390
583
|
}
|
|
391
584
|
}
|
|
392
585
|
}
|
|
393
586
|
}
|
|
394
587
|
}
|
|
395
588
|
/** @internal */
|
|
396
|
-
async
|
|
589
|
+
async #handle_lid_mapping(lid, pn) {
|
|
397
590
|
await this.engine.set(`/lid/${lid}`, serialize(pn));
|
|
398
591
|
await this.engine.set(`/lid/${pn}`, serialize(lid));
|
|
399
592
|
}
|
|
400
593
|
/** @internal */
|
|
401
|
-
async
|
|
594
|
+
async #handle_chats_upsert(chats) {
|
|
402
595
|
for (const ch of chats) {
|
|
403
596
|
if (ch.id) {
|
|
404
597
|
const current = deserialize(await this.engine.get(`/chat/${ch.id}`));
|
|
@@ -409,20 +602,19 @@ export class WhatsApp {
|
|
|
409
602
|
pinned: ch.pinned ?? null,
|
|
410
603
|
mute_end_time: ch.muteEndTime != null ? Number(ch.muteEndTime) : null,
|
|
411
604
|
unread_count: ch.unreadCount ?? null,
|
|
412
|
-
read_only: ch.readOnly ?? null,
|
|
413
605
|
};
|
|
414
606
|
if (ch.name) {
|
|
415
607
|
raw.name = ch.name;
|
|
416
608
|
}
|
|
417
609
|
await this.engine.set(`/chat/${ch.id}`, serialize(raw));
|
|
418
610
|
if (current === null) {
|
|
419
|
-
this.
|
|
611
|
+
this.emit('chat:created', new this.Chat(raw), this);
|
|
420
612
|
}
|
|
421
613
|
}
|
|
422
614
|
}
|
|
423
615
|
}
|
|
424
616
|
/** @internal */
|
|
425
|
-
async
|
|
617
|
+
async #handle_chats_update(chats) {
|
|
426
618
|
for (const ch of chats) {
|
|
427
619
|
if (ch.id && ch.id !== 'status@broadcast') {
|
|
428
620
|
const current = deserialize(await this.engine.get(`/chat/${ch.id}`)) ?? {
|
|
@@ -452,29 +644,29 @@ export class WhatsApp {
|
|
|
452
644
|
const merged = { ...current, ...patch };
|
|
453
645
|
await this.engine.set(`/chat/${ch.id}`, serialize(merged));
|
|
454
646
|
if (pinned_changed) {
|
|
455
|
-
this.
|
|
647
|
+
this.emit(ch.pinned != null ? 'chat:pinned' : 'chat:unpinned', new this.Chat(merged), this);
|
|
456
648
|
}
|
|
457
649
|
if (archived_changed) {
|
|
458
|
-
this.
|
|
650
|
+
this.emit(ch.archived ? 'chat:archived' : 'chat:unarchived', new this.Chat(merged), this);
|
|
459
651
|
}
|
|
460
652
|
if (mute_changed) {
|
|
461
653
|
const is_muted = patch.mute_end_time != null && patch.mute_end_time > Date.now();
|
|
462
|
-
this.
|
|
654
|
+
this.emit(is_muted ? 'chat:muted' : 'chat:unmuted', new this.Chat(merged), this);
|
|
463
655
|
}
|
|
464
656
|
}
|
|
465
657
|
}
|
|
466
658
|
}
|
|
467
659
|
}
|
|
468
660
|
/** @internal */
|
|
469
|
-
async
|
|
661
|
+
async #handle_chats_delete(ids) {
|
|
470
662
|
for (const cid of ids) {
|
|
471
663
|
const raw = deserialize(await this.engine.get(`/chat/${cid}`)) ?? { id: cid };
|
|
472
664
|
await this.engine.unset(`/chat/${cid}`);
|
|
473
|
-
this.
|
|
665
|
+
this.emit('chat:deleted', new this.Chat(raw), this);
|
|
474
666
|
}
|
|
475
667
|
}
|
|
476
668
|
/** @internal */
|
|
477
|
-
async
|
|
669
|
+
async #handle_message_receipt(updates) {
|
|
478
670
|
for (const { key, receipt } of updates) {
|
|
479
671
|
// Receipt sobre status@broadcast → marca el feed como visto y emite feed:updated.
|
|
480
672
|
// Receipt on status@broadcast → marks feed viewed and emits feed:updated.
|
|
@@ -483,21 +675,27 @@ export class WhatsApp {
|
|
|
483
675
|
if (feed_raw && !feed_raw.viewed) {
|
|
484
676
|
feed_raw.viewed = true;
|
|
485
677
|
await this.engine.set(`/status/${key.id}`, serialize(feed_raw));
|
|
486
|
-
this.
|
|
678
|
+
this.emit('feed:updated', new Feed(this, feed_raw), this);
|
|
487
679
|
}
|
|
488
680
|
continue;
|
|
489
681
|
}
|
|
490
682
|
if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
|
|
491
683
|
const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
|
|
492
684
|
if (doc) {
|
|
493
|
-
const
|
|
494
|
-
|
|
685
|
+
const next = receipt.playedTimestamp != null ? PLAYED : READ;
|
|
686
|
+
if (doc.status < next) {
|
|
687
|
+
doc.status = next;
|
|
688
|
+
doc.raw.status = next;
|
|
689
|
+
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
|
|
690
|
+
}
|
|
691
|
+
const msg_instance = message(this, doc);
|
|
692
|
+
this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
|
|
495
693
|
}
|
|
496
694
|
}
|
|
497
695
|
}
|
|
498
696
|
}
|
|
499
697
|
/** @internal */
|
|
500
|
-
async
|
|
698
|
+
async #handle_messages_upsert(messages) {
|
|
501
699
|
for (const msg of messages) {
|
|
502
700
|
if (msg.key?.remoteJid && msg.key.id) {
|
|
503
701
|
const cid = msg.key.remoteJidAlt ?? msg.key.remoteJid;
|
|
@@ -508,9 +706,13 @@ export class WhatsApp {
|
|
|
508
706
|
// Single channel for reactions: handled here; `messages.reaction` is disabled.
|
|
509
707
|
const reaction = msg.message?.reactionMessage;
|
|
510
708
|
if (reaction?.key?.id && reaction.key.remoteJid) {
|
|
511
|
-
const target_cid = (await this
|
|
512
|
-
await this
|
|
513
|
-
key: {
|
|
709
|
+
const target_cid = (await this.#resolve_jid(reaction.key.remoteJid)) ?? reaction.key.remoteJid;
|
|
710
|
+
await this.#handle_messages_reaction([{
|
|
711
|
+
key: {
|
|
712
|
+
remoteJid: target_cid,
|
|
713
|
+
id: reaction.key.id,
|
|
714
|
+
participant: msg.key.fromMe ? (this.#internals.socket?.user?.id ?? null) : (msg.key.participant ?? cid),
|
|
715
|
+
},
|
|
514
716
|
reaction: { text: reaction.text ?? '' },
|
|
515
717
|
}]);
|
|
516
718
|
}
|
|
@@ -526,7 +728,7 @@ export class WhatsApp {
|
|
|
526
728
|
const feed_raw = deserialize(await this.engine.get(`/status/${protocol.key.id}`));
|
|
527
729
|
if (feed_raw) {
|
|
528
730
|
await this.engine.unset(`/status/${protocol.key.id}`);
|
|
529
|
-
this.
|
|
731
|
+
this.emit('feed:deleted', new Feed(this, feed_raw), this);
|
|
530
732
|
}
|
|
531
733
|
}
|
|
532
734
|
continue;
|
|
@@ -574,7 +776,7 @@ export class WhatsApp {
|
|
|
574
776
|
if (feed_type === 'text') {
|
|
575
777
|
content_buf = Buffer.from(caption, 'utf-8');
|
|
576
778
|
}
|
|
577
|
-
else if (this.
|
|
779
|
+
else if (this.#internals.socket) {
|
|
578
780
|
try {
|
|
579
781
|
const buf = await downloadMediaMessage(msg, 'buffer', {});
|
|
580
782
|
if (Buffer.isBuffer(buf)) {
|
|
@@ -587,9 +789,9 @@ export class WhatsApp {
|
|
|
587
789
|
}
|
|
588
790
|
await this.engine.set(`/status/${mid}`, serialize(feed_raw));
|
|
589
791
|
if (content_buf.length > 0) {
|
|
590
|
-
await this
|
|
792
|
+
await this.#write_content(`/status/${mid}/content`, content_buf);
|
|
591
793
|
}
|
|
592
|
-
this.
|
|
794
|
+
this.emit('feed:created', new Feed(this, feed_raw), this);
|
|
593
795
|
continue;
|
|
594
796
|
}
|
|
595
797
|
if (content_type === 'pollUpdateMessage') {
|
|
@@ -599,7 +801,7 @@ export class WhatsApp {
|
|
|
599
801
|
creation_key.remoteJid &&
|
|
600
802
|
update?.vote?.encPayload &&
|
|
601
803
|
update.vote.encIv) {
|
|
602
|
-
const resolved_cid = (await this
|
|
804
|
+
const resolved_cid = (await this.#resolve_jid(creation_key.remoteJid)) ?? creation_key.remoteJid;
|
|
603
805
|
const target_mid = creation_key.id;
|
|
604
806
|
const poll_doc = deserialize(await this.engine.get(`/chat/${resolved_cid}/message/${target_mid}`));
|
|
605
807
|
const secret_raw = poll_doc?.raw.message?.messageContextInfo?.messageSecret;
|
|
@@ -614,8 +816,8 @@ export class WhatsApp {
|
|
|
614
816
|
// Own HMAC identity depends on chat addressing (LID on @lid, PN on
|
|
615
817
|
// @s.whatsapp.net), so fromMe positions try both candidates:
|
|
616
818
|
// AES-GCM authenticates, a wrong key throws and the next is tried.
|
|
617
|
-
const self_id = this.
|
|
618
|
-
const self_lid = this.
|
|
819
|
+
const self_id = this.#internals.socket?.user?.id ?? '';
|
|
820
|
+
const self_lid = this.#internals.socket?.user?.lid ?? '';
|
|
619
821
|
const selves = [...new Set([self_lid, self_id].filter(Boolean))];
|
|
620
822
|
// Candidatos foráneos: todas las formas de identidad del key (LID,
|
|
621
823
|
// participant, alt, remoteJid); se prueban todas porque el addressing
|
|
@@ -649,9 +851,9 @@ export class WhatsApp {
|
|
|
649
851
|
vote: decrypted,
|
|
650
852
|
senderTimestampMs: Number(msg.messageTimestamp) || Date.now(),
|
|
651
853
|
});
|
|
652
|
-
await this.engine.set(`/chat/${resolved_cid}/message/${target_mid}`, serialize(poll_doc));
|
|
653
|
-
const msg_instance =
|
|
654
|
-
this.
|
|
854
|
+
await this.engine.set(`/chat/${resolved_cid}/message/${target_mid}`, serialize(poll_doc), poll_doc.created_at);
|
|
855
|
+
const msg_instance = message(this, poll_doc);
|
|
856
|
+
this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
|
|
655
857
|
}
|
|
656
858
|
}
|
|
657
859
|
catch {
|
|
@@ -672,28 +874,49 @@ export class WhatsApp {
|
|
|
672
874
|
doc) {
|
|
673
875
|
doc.raw.message = protocol.editedMessage;
|
|
674
876
|
doc.edited = true;
|
|
675
|
-
doc.caption = this
|
|
676
|
-
await this.engine.set(`/chat/${target_cid}/message/${target_mid}`, serialize(doc));
|
|
677
|
-
const msg_instance =
|
|
678
|
-
this.
|
|
877
|
+
doc.caption = message(this, doc.raw).caption;
|
|
878
|
+
await this.engine.set(`/chat/${target_cid}/message/${target_mid}`, serialize(doc), doc.created_at);
|
|
879
|
+
const msg_instance = message(this, doc);
|
|
880
|
+
this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
|
|
679
881
|
}
|
|
680
882
|
else if (protocol.type === proto.Message.ProtocolMessage.Type.REVOKE) {
|
|
681
883
|
await this.engine.unset(`/chat/${target_cid}/message/${target_mid}`);
|
|
682
884
|
if (doc) {
|
|
683
|
-
const msg_instance =
|
|
684
|
-
this.
|
|
885
|
+
const msg_instance = message(this, doc);
|
|
886
|
+
this.emit('message:deleted', msg_instance, await msg_instance.chat(), this);
|
|
685
887
|
}
|
|
686
888
|
}
|
|
687
889
|
}
|
|
688
890
|
continue;
|
|
689
891
|
}
|
|
690
|
-
const doc = this
|
|
892
|
+
const doc = message(this, msg)._raw;
|
|
893
|
+
// Cada reconexión re-entrega el historial completo: reescribir documentos
|
|
894
|
+
// idénticos contamina la cronología, re-descarga la media y spamea eventos,
|
|
895
|
+
// así que un doc ya persistido sin cambios visibles se salta entero.
|
|
896
|
+
// Every reconnect re-delivers the full history: rewriting identical documents
|
|
897
|
+
// pollutes chronology, re-downloads media and spams events, so an already
|
|
898
|
+
// persisted doc without visible changes is skipped entirely.
|
|
899
|
+
const existing_doc = deserialize(await this.engine.get(`/chat/${cid}/message/${mid}`));
|
|
900
|
+
if (existing_doc) {
|
|
901
|
+
if (typeof existing_doc.multiple === 'boolean') {
|
|
902
|
+
doc.multiple = existing_doc.multiple;
|
|
903
|
+
}
|
|
904
|
+
if (existing_doc.reactions) {
|
|
905
|
+
doc.reactions = existing_doc.reactions;
|
|
906
|
+
}
|
|
907
|
+
if (existing_doc.status >= doc.status &&
|
|
908
|
+
existing_doc.caption === doc.caption &&
|
|
909
|
+
existing_doc.edited === doc.edited &&
|
|
910
|
+
existing_doc.starred === doc.starred) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
691
914
|
// Autocreación de contacto/chat desde pushName cuando baileys no emite upsert previo
|
|
692
|
-
if (!doc.me) {
|
|
915
|
+
if (!existing_doc && !doc.me) {
|
|
693
916
|
const push_name = msg.pushName ?? null;
|
|
694
917
|
const is_group = cid.endsWith('@g.us');
|
|
695
918
|
if (doc.author && !(await this.engine.get(`/contact/${doc.author}`))) {
|
|
696
|
-
await this
|
|
919
|
+
await this.#persist_contact({
|
|
697
920
|
id: doc.author,
|
|
698
921
|
lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : null,
|
|
699
922
|
name: null,
|
|
@@ -709,20 +932,17 @@ export class WhatsApp {
|
|
|
709
932
|
name: is_group ? null : push_name,
|
|
710
933
|
};
|
|
711
934
|
await this.engine.set(`/chat/${cid}`, serialize(chat_raw));
|
|
712
|
-
this.
|
|
935
|
+
this.emit('chat:created', new this.Chat(chat_raw), this);
|
|
713
936
|
}
|
|
714
937
|
}
|
|
715
|
-
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
// the proto discriminator on echo).
|
|
719
|
-
const existing_doc = deserialize(await this.engine.get(`/chat/${cid}/message/${mid}`));
|
|
720
|
-
if (existing_doc && typeof existing_doc.multiple === 'boolean') {
|
|
721
|
-
doc.multiple = existing_doc.multiple;
|
|
722
|
-
}
|
|
723
|
-
await this.engine.set(`/chat/${cid}/message/${mid}`, serialize(doc));
|
|
938
|
+
await this.engine.set(`/chat/${cid}/message/${mid}`, serialize(doc), doc.created_at);
|
|
939
|
+
// El binario solo se materializa en la primera entrega; en re-syncs ya vive en el engine.
|
|
940
|
+
// The binary is only materialized on first delivery; on re-syncs it already lives in the engine.
|
|
724
941
|
let content_buf = Buffer.alloc(0);
|
|
725
|
-
if (
|
|
942
|
+
if (existing_doc) {
|
|
943
|
+
/* ya materializado / already materialized */
|
|
944
|
+
}
|
|
945
|
+
else if (doc.type === 'text') {
|
|
726
946
|
content_buf = Buffer.from(doc.caption, 'utf-8');
|
|
727
947
|
}
|
|
728
948
|
else if (doc.type === 'location') {
|
|
@@ -745,7 +965,7 @@ export class WhatsApp {
|
|
|
745
965
|
else if (doc.type === 'event') {
|
|
746
966
|
content_buf = Buffer.from(JSON.stringify(msg.message?.eventMessage ?? {}), 'utf-8');
|
|
747
967
|
}
|
|
748
|
-
else if (this.
|
|
968
|
+
else if (this.#internals.socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
|
|
749
969
|
try {
|
|
750
970
|
const buffer = await downloadMediaMessage(msg, 'buffer', {});
|
|
751
971
|
if (Buffer.isBuffer(buffer)) {
|
|
@@ -757,19 +977,19 @@ export class WhatsApp {
|
|
|
757
977
|
}
|
|
758
978
|
}
|
|
759
979
|
if (content_buf.length > 0) {
|
|
760
|
-
await this
|
|
980
|
+
await this.#write_content(`/chat/${cid}/message/${mid}/content`, content_buf);
|
|
761
981
|
}
|
|
762
|
-
const instance =
|
|
982
|
+
const instance = message(this, doc);
|
|
763
983
|
const chat_instance = await instance.chat();
|
|
764
|
-
this.
|
|
984
|
+
this.emit('message:created', instance, chat_instance, this);
|
|
765
985
|
if (doc.forwarded) {
|
|
766
|
-
this.
|
|
986
|
+
this.emit('message:forwarded', instance, chat_instance, this);
|
|
767
987
|
}
|
|
768
988
|
}
|
|
769
989
|
}
|
|
770
990
|
}
|
|
771
991
|
/** @internal */
|
|
772
|
-
async
|
|
992
|
+
async #handle_messages_update(updates) {
|
|
773
993
|
for (const { key, update: upd } of updates) {
|
|
774
994
|
if (key.remoteJid && key.id) {
|
|
775
995
|
// Updates sobre `status@broadcast` se descartan: el feed sólo se
|
|
@@ -791,42 +1011,42 @@ export class WhatsApp {
|
|
|
791
1011
|
raw.message = edited_message;
|
|
792
1012
|
doc.raw = raw;
|
|
793
1013
|
doc.edited = true;
|
|
794
|
-
doc.caption =
|
|
795
|
-
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc));
|
|
796
|
-
const msg_instance =
|
|
797
|
-
this.
|
|
1014
|
+
doc.caption = message(this, raw).caption;
|
|
1015
|
+
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
|
|
1016
|
+
const msg_instance = message(this, doc);
|
|
1017
|
+
this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
|
|
798
1018
|
}
|
|
799
1019
|
else if (content_update) {
|
|
800
1020
|
// Actualización de contenido (ej: live location). Mergea sobre el raw existente.
|
|
801
1021
|
raw.message = { ...raw.message, ...content_update };
|
|
802
1022
|
doc.raw = raw;
|
|
803
|
-
doc.caption =
|
|
804
|
-
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc));
|
|
805
|
-
const msg_instance =
|
|
806
|
-
this.
|
|
1023
|
+
doc.caption = message(this, raw).caption;
|
|
1024
|
+
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
|
|
1025
|
+
const msg_instance = message(this, doc);
|
|
1026
|
+
this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
|
|
807
1027
|
}
|
|
808
1028
|
else if (starred_changed) {
|
|
809
1029
|
doc.starred = upd_any.starred === true;
|
|
810
1030
|
raw.starred = doc.starred;
|
|
811
1031
|
doc.raw = raw;
|
|
812
|
-
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc));
|
|
813
|
-
const msg_instance =
|
|
814
|
-
this.
|
|
1032
|
+
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
|
|
1033
|
+
const msg_instance = message(this, doc);
|
|
1034
|
+
this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
|
|
815
1035
|
}
|
|
816
1036
|
else if (status !== undefined) {
|
|
817
1037
|
raw.status = status;
|
|
818
1038
|
doc.status = status;
|
|
819
1039
|
doc.raw = raw;
|
|
820
|
-
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc));
|
|
821
|
-
const msg_instance =
|
|
822
|
-
this.
|
|
1040
|
+
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
|
|
1041
|
+
const msg_instance = message(this, doc);
|
|
1042
|
+
this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
|
|
823
1043
|
}
|
|
824
1044
|
}
|
|
825
1045
|
}
|
|
826
1046
|
}
|
|
827
1047
|
}
|
|
828
1048
|
/** @internal */
|
|
829
|
-
async
|
|
1049
|
+
async #handle_messages_reaction(reactions) {
|
|
830
1050
|
for (const { key, reaction } of reactions) {
|
|
831
1051
|
if (key.remoteJid && key.id) {
|
|
832
1052
|
// Reacciones sobre status@broadcast → feed:updated (no message:reacted).
|
|
@@ -834,18 +1054,24 @@ export class WhatsApp {
|
|
|
834
1054
|
if (key.remoteJid === 'status@broadcast') {
|
|
835
1055
|
const feed_raw = deserialize(await this.engine.get(`/status/${key.id}`));
|
|
836
1056
|
if (feed_raw) {
|
|
837
|
-
this.
|
|
1057
|
+
this.emit('feed:updated', new Feed(this, feed_raw), this);
|
|
838
1058
|
}
|
|
839
1059
|
continue;
|
|
840
1060
|
}
|
|
841
1061
|
const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
|
|
842
1062
|
if (doc) {
|
|
843
|
-
const
|
|
844
|
-
|
|
1063
|
+
const reactor = jidNormalizedUser(key.participant ?? key.remoteJid);
|
|
1064
|
+
const emoji = reaction.text ?? '';
|
|
1065
|
+
doc.reactions = [
|
|
1066
|
+
...(doc.reactions ?? []).filter((r) => r.author !== reactor),
|
|
1067
|
+
...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
|
|
1068
|
+
];
|
|
1069
|
+
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc), doc.created_at);
|
|
1070
|
+
const msg_instance = message(this, doc);
|
|
1071
|
+
this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
|
|
845
1072
|
}
|
|
846
1073
|
}
|
|
847
1074
|
}
|
|
848
1075
|
}
|
|
849
1076
|
}
|
|
850
1077
|
export default WhatsApp;
|
|
851
|
-
//# sourceMappingURL=index.js.map
|