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