@arcaelas/whatsapp 4.5.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 +462 -289
- package/build/cjs/lib/message/index.js +825 -741
- 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 +106 -68
- package/build/cjs/lib/whatsapp/index.js +380 -170
- package/build/cjs/lib/whatsapp/index.js.map +1 -1
- package/build/cjs/test.js +5 -4
- package/build/cjs/test.js.map +1 -1
- 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 +462 -289
- package/build/esm/lib/message/index.js +820 -736
- 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 +106 -68
- package/build/esm/lib/whatsapp/index.js +382 -172
- package/build/esm/lib/whatsapp/index.js.map +1 -1
- package/build/esm/test.js +5 -4
- package/build/esm/test.js.map +1 -1
- 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,55 +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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
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 });
|
|
54
115
|
}
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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);
|
|
59
134
|
}
|
|
60
135
|
/**
|
|
61
136
|
* Registra un listener de evento. Retorna función para desuscribirse.
|
|
62
137
|
* Registers an event listener. Returns an unsubscribe function.
|
|
63
138
|
*/
|
|
64
139
|
on(event, handler) {
|
|
65
|
-
this.
|
|
66
|
-
return () => { this.
|
|
140
|
+
this.#event.on(event, handler);
|
|
141
|
+
return () => { this.#event.off(event, handler); };
|
|
67
142
|
}
|
|
68
143
|
/**
|
|
69
144
|
* Quita un listener previamente registrado.
|
|
70
145
|
* Removes a previously registered listener.
|
|
71
146
|
*/
|
|
72
147
|
off(event, handler) {
|
|
73
|
-
this.
|
|
148
|
+
this.#event.off(event, handler);
|
|
74
149
|
return this;
|
|
75
150
|
}
|
|
76
151
|
/**
|
|
@@ -78,15 +153,17 @@ export class WhatsApp {
|
|
|
78
153
|
* Registers a one-shot listener. Returns an unsubscribe function.
|
|
79
154
|
*/
|
|
80
155
|
once(event, handler) {
|
|
81
|
-
this.
|
|
82
|
-
return () => { this.
|
|
156
|
+
this.#event.once(event, handler);
|
|
157
|
+
return () => { this.#event.off(event, handler); };
|
|
83
158
|
}
|
|
84
159
|
/**
|
|
85
160
|
* @internal
|
|
86
|
-
* Normaliza cualquier identificador (JID, LID, número, etc.) a JID canónico.
|
|
87
|
-
*
|
|
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.
|
|
88
165
|
*/
|
|
89
|
-
async
|
|
166
|
+
async #resolve_jid(uid) {
|
|
90
167
|
let result = null;
|
|
91
168
|
if (uid.endsWith('@g.us') || uid.endsWith('@s.whatsapp.net')) {
|
|
92
169
|
result = uid;
|
|
@@ -106,7 +183,7 @@ export class WhatsApp {
|
|
|
106
183
|
// baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
|
|
107
184
|
// (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
|
|
108
185
|
// realmente está guardado, y el mensaje/poll no se encuentra.
|
|
109
|
-
const pn = await this.
|
|
186
|
+
const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(uid).catch(() => null);
|
|
110
187
|
if (pn) {
|
|
111
188
|
// getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
|
|
112
189
|
// que el JID coincida con el que usa el store (sin device).
|
|
@@ -131,16 +208,20 @@ export class WhatsApp {
|
|
|
131
208
|
* Resolves once the session is synced; retries on non-loggedOut disconnects.
|
|
132
209
|
*/
|
|
133
210
|
async connect(callback) {
|
|
211
|
+
if (this.#internals.socket) {
|
|
212
|
+
await this.disconnect({ silent: true });
|
|
213
|
+
}
|
|
134
214
|
const { version } = await fetchLatestBaileysVersion();
|
|
135
|
-
this
|
|
136
|
-
this
|
|
215
|
+
this.#intentional_close = false;
|
|
216
|
+
this.#silent_close = false;
|
|
217
|
+
this.#has_connected = false;
|
|
137
218
|
return new Promise((resolve, reject) => {
|
|
138
219
|
const start = async () => {
|
|
139
220
|
// Re-lee creds en cada start() para que limpiezas del engine tomen efecto
|
|
140
221
|
// en reintentos (permite al consumer forzar nueva sesión borrando /session/creds).
|
|
141
222
|
const stored = await this.engine.get('/session/creds');
|
|
142
223
|
const creds = deserialize(stored) ?? initAuthCreds();
|
|
143
|
-
this.
|
|
224
|
+
this.#internals.socket = makeWASocket({
|
|
144
225
|
version,
|
|
145
226
|
auth: {
|
|
146
227
|
creds,
|
|
@@ -167,19 +248,19 @@ export class WhatsApp {
|
|
|
167
248
|
},
|
|
168
249
|
browser: Browsers.windows('Chrome'),
|
|
169
250
|
logger: pino({ level: 'silent' }),
|
|
170
|
-
syncFullHistory: this
|
|
251
|
+
syncFullHistory: this.#sync,
|
|
171
252
|
// Los syncs no-FULL cargan las LID mappings y los tctokens (trusted-contact
|
|
172
|
-
// tokens) que rc13 exige para enviar: apagarlos todos con `() => this
|
|
253
|
+
// tokens) que rc13 exige para enviar: apagarlos todos con `() => this.#sync`
|
|
173
254
|
// deja la sesión sin tctoken y el server rechaza los mensajes con
|
|
174
255
|
// "error 463: account restricted or missing tctoken". Por eso se procesan
|
|
175
256
|
// siempre los no-FULL; `sync` solo decide si además se trae el historial FULL.
|
|
176
257
|
// Non-FULL syncs carry the LID mappings and tctokens rc13 requires to send;
|
|
177
258
|
// disabling them all made the server reject messages with error 463. We always
|
|
178
259
|
// process non-FULL; `sync` only gates whether FULL history is pulled too.
|
|
179
|
-
shouldSyncHistoryMessage: ({ syncType }) => this
|
|
260
|
+
shouldSyncHistoryMessage: ({ syncType }) => this.#sync || syncType !== proto.HistorySync.HistorySyncType.FULL,
|
|
180
261
|
markOnlineOnConnect: false,
|
|
181
262
|
});
|
|
182
|
-
const socket = this.
|
|
263
|
+
const socket = this.#internals.socket;
|
|
183
264
|
socket.ev.on('creds.update', () => this.engine.set('/session/creds', serialize(creds)));
|
|
184
265
|
socket.ev.on('connection.update', async (update) => {
|
|
185
266
|
const { connection, lastDisconnect, qr } = update;
|
|
@@ -187,24 +268,25 @@ export class WhatsApp {
|
|
|
187
268
|
// Baileys refresca QR periódicamente (~20s). Emitimos nuevo pair
|
|
188
269
|
// code / QR en cada refresh para que el usuario pueda renovar si
|
|
189
270
|
// el anterior expiró.
|
|
190
|
-
//
|
|
191
|
-
// `
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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));
|
|
195
277
|
}
|
|
196
278
|
else {
|
|
197
279
|
await callback(await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
|
|
198
280
|
}
|
|
199
281
|
}
|
|
200
282
|
if (connection === 'open') {
|
|
201
|
-
this
|
|
202
|
-
this
|
|
203
|
-
this.
|
|
283
|
+
this.#has_connected = true;
|
|
284
|
+
this.#retry_count = 0;
|
|
285
|
+
this.emit('connected', this);
|
|
204
286
|
resolve();
|
|
205
287
|
}
|
|
206
288
|
else if (connection === 'close') {
|
|
207
|
-
this.
|
|
289
|
+
this.#internals.socket = null;
|
|
208
290
|
const status_code = lastDisconnect?.error?.output?.statusCode;
|
|
209
291
|
// `restartRequired` (515) es una reconexión exigida por el protocolo
|
|
210
292
|
// tras el sync inicial — no es un disconnect "real".
|
|
@@ -212,35 +294,37 @@ export class WhatsApp {
|
|
|
212
294
|
// Limpieza del engine ANTES de emitir `disconnected` para que los
|
|
213
295
|
// listeners vean el estado final (engine vaciado o creds borradas).
|
|
214
296
|
if (status_code === DisconnectReason.loggedOut) {
|
|
215
|
-
if (this
|
|
297
|
+
if (this.#autoclean) {
|
|
216
298
|
await this.engine.clear();
|
|
217
299
|
}
|
|
218
300
|
else {
|
|
219
301
|
await this.engine.unset('/session/creds');
|
|
220
302
|
}
|
|
221
303
|
}
|
|
222
|
-
|
|
223
|
-
|
|
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);
|
|
224
308
|
}
|
|
225
|
-
if (!this
|
|
309
|
+
if (!this.#intentional_close) {
|
|
226
310
|
if (status_code === DisconnectReason.loggedOut) {
|
|
227
311
|
reject(new Error('Logged out'));
|
|
228
312
|
}
|
|
229
313
|
else {
|
|
230
|
-
const max = this.
|
|
314
|
+
const max = this.#reconnect.max;
|
|
231
315
|
// Transient closes (restartRequired) son parte del protocolo,
|
|
232
316
|
// no cuentan contra el límite de reintentos por fallo.
|
|
233
|
-
const exhausted = !is_transient && max !== null && this
|
|
317
|
+
const exhausted = !is_transient && max !== null && this.#retry_count >= max;
|
|
234
318
|
if (exhausted) {
|
|
235
319
|
reject(new Error(`Reconnect attempts exhausted (${max})`));
|
|
236
320
|
}
|
|
237
321
|
else {
|
|
238
322
|
if (!is_transient) {
|
|
239
|
-
this
|
|
323
|
+
this.#retry_count++;
|
|
240
324
|
}
|
|
241
|
-
const delay = is_transient ? 0 : this.
|
|
242
|
-
this
|
|
243
|
-
this
|
|
325
|
+
const delay = is_transient ? 0 : this.#reconnect.interval_ms;
|
|
326
|
+
this.#retry_timer = setTimeout(() => {
|
|
327
|
+
this.#retry_timer = null;
|
|
244
328
|
start().catch(reject);
|
|
245
329
|
}, delay);
|
|
246
330
|
}
|
|
@@ -248,24 +332,119 @@ export class WhatsApp {
|
|
|
248
332
|
}
|
|
249
333
|
}
|
|
250
334
|
});
|
|
251
|
-
this
|
|
335
|
+
this.#attach_business_handlers(socket);
|
|
252
336
|
};
|
|
253
337
|
start().catch(reject);
|
|
254
338
|
});
|
|
255
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
|
+
}
|
|
256
434
|
/**
|
|
257
435
|
* Cierra la conexión. Con `destroy: true` vacía el engine completo.
|
|
258
436
|
* Closes the connection. With `destroy: true` clears the engine entirely.
|
|
259
437
|
*/
|
|
260
438
|
async disconnect(options = {}) {
|
|
261
|
-
this
|
|
439
|
+
this.#intentional_close = true;
|
|
440
|
+
this.#silent_close = options.silent === true;
|
|
262
441
|
// Cancela cualquier retry programado por un close anterior, para no resucitar
|
|
263
442
|
// el socket después de una desconexión manual.
|
|
264
|
-
if (this
|
|
265
|
-
clearTimeout(this
|
|
266
|
-
this
|
|
443
|
+
if (this.#retry_timer) {
|
|
444
|
+
clearTimeout(this.#retry_timer);
|
|
445
|
+
this.#retry_timer = null;
|
|
267
446
|
}
|
|
268
|
-
if (this.
|
|
447
|
+
if (this.#internals.socket) {
|
|
269
448
|
try {
|
|
270
449
|
// Pasa un error Boom-like con statusCode=connectionClosed (428) para que
|
|
271
450
|
// `lastDisconnect.error.output.statusCode` quede explícito en el close
|
|
@@ -273,17 +452,16 @@ export class WhatsApp {
|
|
|
273
452
|
const intentional = Object.assign(new Error('intentional close'), {
|
|
274
453
|
output: { statusCode: DisconnectReason.connectionClosed },
|
|
275
454
|
});
|
|
276
|
-
await this.
|
|
455
|
+
await this.#internals.socket.end(intentional);
|
|
277
456
|
}
|
|
278
457
|
catch {
|
|
279
458
|
/* socket may already be closed */
|
|
280
459
|
}
|
|
281
|
-
this.
|
|
460
|
+
this.#internals.socket = null;
|
|
282
461
|
}
|
|
283
462
|
if (options.destroy) {
|
|
284
463
|
await this.engine.clear();
|
|
285
464
|
}
|
|
286
|
-
void options.silent;
|
|
287
465
|
}
|
|
288
466
|
/**
|
|
289
467
|
* Conecta los handlers de eventos de baileys: contactos, chats y mensajes.
|
|
@@ -291,43 +469,45 @@ export class WhatsApp {
|
|
|
291
469
|
*
|
|
292
470
|
* @internal
|
|
293
471
|
*/
|
|
294
|
-
|
|
295
|
-
socket.ev.on('messaging-history.set',
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
});
|
|
299
479
|
});
|
|
300
480
|
socket.ev.on('contacts.upsert', (contacts) => {
|
|
301
|
-
|
|
481
|
+
this.#enqueue(() => this.#handle_contacts_upsert(contacts));
|
|
302
482
|
});
|
|
303
483
|
socket.ev.on('contacts.update', (contacts) => {
|
|
304
|
-
|
|
484
|
+
this.#enqueue(() => this.#handle_contacts_update(contacts));
|
|
305
485
|
});
|
|
306
486
|
socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
|
|
307
|
-
|
|
487
|
+
this.#enqueue(() => this.#handle_lid_mapping(lid, pn));
|
|
308
488
|
});
|
|
309
489
|
socket.ev.on('chats.upsert', (chats) => {
|
|
310
|
-
|
|
490
|
+
this.#enqueue(() => this.#handle_chats_upsert(chats));
|
|
311
491
|
});
|
|
312
492
|
socket.ev.on('chats.update', (chats) => {
|
|
313
|
-
|
|
493
|
+
this.#enqueue(() => this.#handle_chats_update(chats));
|
|
314
494
|
});
|
|
315
495
|
socket.ev.on('chats.delete', (ids) => {
|
|
316
|
-
|
|
496
|
+
this.#enqueue(() => this.#handle_chats_delete(ids));
|
|
317
497
|
});
|
|
318
498
|
socket.ev.on('messages.upsert', ({ messages }) => {
|
|
319
|
-
|
|
499
|
+
this.#enqueue(() => this.#handle_messages_upsert(messages));
|
|
320
500
|
});
|
|
321
501
|
socket.ev.on('messages.update', (updates) => {
|
|
322
|
-
|
|
502
|
+
this.#enqueue(() => this.#handle_messages_update(updates));
|
|
323
503
|
});
|
|
324
504
|
socket.ev.on('message-receipt.update', (updates) => {
|
|
325
|
-
|
|
505
|
+
this.#enqueue(() => this.#handle_message_receipt(updates));
|
|
326
506
|
});
|
|
327
507
|
// Las reacciones llegan duplicadas por `messages.reaction` Y `messages.upsert`
|
|
328
508
|
// (como `reactionMessage`). Se usa solo `messages.upsert` para evitar el doble disparo.
|
|
329
509
|
// socket.ev.on('messages.reaction', (reactions) => {
|
|
330
|
-
// void this
|
|
510
|
+
// void this.#handle_messages_reaction(reactions);
|
|
331
511
|
// });
|
|
332
512
|
}
|
|
333
513
|
/**
|
|
@@ -337,23 +517,23 @@ export class WhatsApp {
|
|
|
337
517
|
* @param raw - Documento del contacto a persistir / Contact document to persist
|
|
338
518
|
* @internal
|
|
339
519
|
*/
|
|
340
|
-
async
|
|
520
|
+
async #persist_contact(raw) {
|
|
341
521
|
const existing = await this.engine.get(`/contact/${raw.id}`);
|
|
342
522
|
await this.engine.set(`/contact/${raw.id}`, serialize(raw));
|
|
343
523
|
if (raw.lid) {
|
|
344
524
|
await this.engine.set(`/lid/${raw.lid}`, serialize(raw.id));
|
|
345
525
|
}
|
|
346
526
|
if (existing === null) {
|
|
527
|
+
const person = new this.Contact(raw);
|
|
347
528
|
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);
|
|
529
|
+
this.emit('contact:created', person, new this.Chat(cached_chat ?? { id: raw.id, name: person.name }), this);
|
|
350
530
|
}
|
|
351
531
|
}
|
|
352
532
|
/** @internal */
|
|
353
|
-
async
|
|
533
|
+
async #handle_contacts_upsert(contacts) {
|
|
354
534
|
for (const c of contacts) {
|
|
355
535
|
if (c.id) {
|
|
356
|
-
await this
|
|
536
|
+
await this.#persist_contact({
|
|
357
537
|
id: c.id,
|
|
358
538
|
lid: c.lid ?? null,
|
|
359
539
|
name: c.name ?? null,
|
|
@@ -366,7 +546,7 @@ export class WhatsApp {
|
|
|
366
546
|
}
|
|
367
547
|
}
|
|
368
548
|
/** @internal */
|
|
369
|
-
async
|
|
549
|
+
async #handle_contacts_update(contacts) {
|
|
370
550
|
for (const c of contacts) {
|
|
371
551
|
if (c.id) {
|
|
372
552
|
const current = deserialize(await this.engine.get(`/contact/${c.id}`));
|
|
@@ -384,21 +564,21 @@ export class WhatsApp {
|
|
|
384
564
|
if (patch.lid) {
|
|
385
565
|
await this.engine.set(`/lid/${patch.lid}`, serialize(c.id));
|
|
386
566
|
}
|
|
567
|
+
const person = new this.Contact(merged);
|
|
387
568
|
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);
|
|
569
|
+
this.emit('contact:updated', person, new this.Chat(cached_chat ?? { id: c.id, name: person.name }), this);
|
|
390
570
|
}
|
|
391
571
|
}
|
|
392
572
|
}
|
|
393
573
|
}
|
|
394
574
|
}
|
|
395
575
|
/** @internal */
|
|
396
|
-
async
|
|
576
|
+
async #handle_lid_mapping(lid, pn) {
|
|
397
577
|
await this.engine.set(`/lid/${lid}`, serialize(pn));
|
|
398
578
|
await this.engine.set(`/lid/${pn}`, serialize(lid));
|
|
399
579
|
}
|
|
400
580
|
/** @internal */
|
|
401
|
-
async
|
|
581
|
+
async #handle_chats_upsert(chats) {
|
|
402
582
|
for (const ch of chats) {
|
|
403
583
|
if (ch.id) {
|
|
404
584
|
const current = deserialize(await this.engine.get(`/chat/${ch.id}`));
|
|
@@ -408,21 +588,19 @@ export class WhatsApp {
|
|
|
408
588
|
archived: ch.archived ?? null,
|
|
409
589
|
pinned: ch.pinned ?? null,
|
|
410
590
|
mute_end_time: ch.muteEndTime != null ? Number(ch.muteEndTime) : null,
|
|
411
|
-
unread_count: ch.unreadCount ?? null,
|
|
412
|
-
read_only: ch.readOnly ?? null,
|
|
413
591
|
};
|
|
414
592
|
if (ch.name) {
|
|
415
593
|
raw.name = ch.name;
|
|
416
594
|
}
|
|
417
595
|
await this.engine.set(`/chat/${ch.id}`, serialize(raw));
|
|
418
596
|
if (current === null) {
|
|
419
|
-
this.
|
|
597
|
+
this.emit('chat:created', new this.Chat(raw), this);
|
|
420
598
|
}
|
|
421
599
|
}
|
|
422
600
|
}
|
|
423
601
|
}
|
|
424
602
|
/** @internal */
|
|
425
|
-
async
|
|
603
|
+
async #handle_chats_update(chats) {
|
|
426
604
|
for (const ch of chats) {
|
|
427
605
|
if (ch.id && ch.id !== 'status@broadcast') {
|
|
428
606
|
const current = deserialize(await this.engine.get(`/chat/${ch.id}`)) ?? {
|
|
@@ -445,36 +623,33 @@ export class WhatsApp {
|
|
|
445
623
|
if (mute_changed) {
|
|
446
624
|
patch.mute_end_time = ch.muteEndTime != null ? Number(ch.muteEndTime) : null;
|
|
447
625
|
}
|
|
448
|
-
if (ch.unreadCount != null) {
|
|
449
|
-
patch.unread_count = ch.unreadCount;
|
|
450
|
-
}
|
|
451
626
|
if (Object.keys(patch).length > 0) {
|
|
452
627
|
const merged = { ...current, ...patch };
|
|
453
628
|
await this.engine.set(`/chat/${ch.id}`, serialize(merged));
|
|
454
629
|
if (pinned_changed) {
|
|
455
|
-
this.
|
|
630
|
+
this.emit(ch.pinned != null ? 'chat:pinned' : 'chat:unpinned', new this.Chat(merged), this);
|
|
456
631
|
}
|
|
457
632
|
if (archived_changed) {
|
|
458
|
-
this.
|
|
633
|
+
this.emit(ch.archived ? 'chat:archived' : 'chat:unarchived', new this.Chat(merged), this);
|
|
459
634
|
}
|
|
460
635
|
if (mute_changed) {
|
|
461
636
|
const is_muted = patch.mute_end_time != null && patch.mute_end_time > Date.now();
|
|
462
|
-
this.
|
|
637
|
+
this.emit(is_muted ? 'chat:muted' : 'chat:unmuted', new this.Chat(merged), this);
|
|
463
638
|
}
|
|
464
639
|
}
|
|
465
640
|
}
|
|
466
641
|
}
|
|
467
642
|
}
|
|
468
643
|
/** @internal */
|
|
469
|
-
async
|
|
644
|
+
async #handle_chats_delete(ids) {
|
|
470
645
|
for (const cid of ids) {
|
|
471
646
|
const raw = deserialize(await this.engine.get(`/chat/${cid}`)) ?? { id: cid };
|
|
472
647
|
await this.engine.unset(`/chat/${cid}`);
|
|
473
|
-
this.
|
|
648
|
+
this.emit('chat:deleted', new this.Chat(raw), this);
|
|
474
649
|
}
|
|
475
650
|
}
|
|
476
651
|
/** @internal */
|
|
477
|
-
async
|
|
652
|
+
async #handle_message_receipt(updates) {
|
|
478
653
|
for (const { key, receipt } of updates) {
|
|
479
654
|
// Receipt sobre status@broadcast → marca el feed como visto y emite feed:updated.
|
|
480
655
|
// Receipt on status@broadcast → marks feed viewed and emits feed:updated.
|
|
@@ -483,21 +658,27 @@ export class WhatsApp {
|
|
|
483
658
|
if (feed_raw && !feed_raw.viewed) {
|
|
484
659
|
feed_raw.viewed = true;
|
|
485
660
|
await this.engine.set(`/status/${key.id}`, serialize(feed_raw));
|
|
486
|
-
this.
|
|
661
|
+
this.emit('feed:updated', new Feed(this, feed_raw), this);
|
|
487
662
|
}
|
|
488
663
|
continue;
|
|
489
664
|
}
|
|
490
665
|
if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
|
|
491
666
|
const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
|
|
492
667
|
if (doc) {
|
|
493
|
-
const
|
|
494
|
-
|
|
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);
|
|
495
676
|
}
|
|
496
677
|
}
|
|
497
678
|
}
|
|
498
679
|
}
|
|
499
680
|
/** @internal */
|
|
500
|
-
async
|
|
681
|
+
async #handle_messages_upsert(messages) {
|
|
501
682
|
for (const msg of messages) {
|
|
502
683
|
if (msg.key?.remoteJid && msg.key.id) {
|
|
503
684
|
const cid = msg.key.remoteJidAlt ?? msg.key.remoteJid;
|
|
@@ -508,9 +689,13 @@ export class WhatsApp {
|
|
|
508
689
|
// Single channel for reactions: handled here; `messages.reaction` is disabled.
|
|
509
690
|
const reaction = msg.message?.reactionMessage;
|
|
510
691
|
if (reaction?.key?.id && reaction.key.remoteJid) {
|
|
511
|
-
const target_cid = (await this
|
|
512
|
-
await this
|
|
513
|
-
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
|
+
},
|
|
514
699
|
reaction: { text: reaction.text ?? '' },
|
|
515
700
|
}]);
|
|
516
701
|
}
|
|
@@ -526,7 +711,7 @@ export class WhatsApp {
|
|
|
526
711
|
const feed_raw = deserialize(await this.engine.get(`/status/${protocol.key.id}`));
|
|
527
712
|
if (feed_raw) {
|
|
528
713
|
await this.engine.unset(`/status/${protocol.key.id}`);
|
|
529
|
-
this.
|
|
714
|
+
this.emit('feed:deleted', new Feed(this, feed_raw), this);
|
|
530
715
|
}
|
|
531
716
|
}
|
|
532
717
|
continue;
|
|
@@ -574,7 +759,7 @@ export class WhatsApp {
|
|
|
574
759
|
if (feed_type === 'text') {
|
|
575
760
|
content_buf = Buffer.from(caption, 'utf-8');
|
|
576
761
|
}
|
|
577
|
-
else if (this.
|
|
762
|
+
else if (this.#internals.socket) {
|
|
578
763
|
try {
|
|
579
764
|
const buf = await downloadMediaMessage(msg, 'buffer', {});
|
|
580
765
|
if (Buffer.isBuffer(buf)) {
|
|
@@ -589,7 +774,7 @@ export class WhatsApp {
|
|
|
589
774
|
if (content_buf.length > 0) {
|
|
590
775
|
await this.engine.set(`/status/${mid}/content`, serialize({ data: content_buf.toString('base64') }));
|
|
591
776
|
}
|
|
592
|
-
this.
|
|
777
|
+
this.emit('feed:created', new Feed(this, feed_raw), this);
|
|
593
778
|
continue;
|
|
594
779
|
}
|
|
595
780
|
if (content_type === 'pollUpdateMessage') {
|
|
@@ -599,7 +784,7 @@ export class WhatsApp {
|
|
|
599
784
|
creation_key.remoteJid &&
|
|
600
785
|
update?.vote?.encPayload &&
|
|
601
786
|
update.vote.encIv) {
|
|
602
|
-
const resolved_cid = (await this
|
|
787
|
+
const resolved_cid = (await this.#resolve_jid(creation_key.remoteJid)) ?? creation_key.remoteJid;
|
|
603
788
|
const target_mid = creation_key.id;
|
|
604
789
|
const poll_doc = deserialize(await this.engine.get(`/chat/${resolved_cid}/message/${target_mid}`));
|
|
605
790
|
const secret_raw = poll_doc?.raw.message?.messageContextInfo?.messageSecret;
|
|
@@ -614,8 +799,8 @@ export class WhatsApp {
|
|
|
614
799
|
// Own HMAC identity depends on chat addressing (LID on @lid, PN on
|
|
615
800
|
// @s.whatsapp.net), so fromMe positions try both candidates:
|
|
616
801
|
// AES-GCM authenticates, a wrong key throws and the next is tried.
|
|
617
|
-
const self_id = this.
|
|
618
|
-
const self_lid = this.
|
|
802
|
+
const self_id = this.#internals.socket?.user?.id ?? '';
|
|
803
|
+
const self_lid = this.#internals.socket?.user?.lid ?? '';
|
|
619
804
|
const selves = [...new Set([self_lid, self_id].filter(Boolean))];
|
|
620
805
|
// Candidatos foráneos: todas las formas de identidad del key (LID,
|
|
621
806
|
// participant, alt, remoteJid); se prueban todas porque el addressing
|
|
@@ -649,9 +834,9 @@ export class WhatsApp {
|
|
|
649
834
|
vote: decrypted,
|
|
650
835
|
senderTimestampMs: Number(msg.messageTimestamp) || Date.now(),
|
|
651
836
|
});
|
|
652
|
-
await this.engine.set(`/chat/${resolved_cid}/message/${target_mid}`, serialize(poll_doc));
|
|
653
|
-
const msg_instance =
|
|
654
|
-
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);
|
|
655
840
|
}
|
|
656
841
|
}
|
|
657
842
|
catch {
|
|
@@ -672,28 +857,49 @@ export class WhatsApp {
|
|
|
672
857
|
doc) {
|
|
673
858
|
doc.raw.message = protocol.editedMessage;
|
|
674
859
|
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.
|
|
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);
|
|
679
864
|
}
|
|
680
865
|
else if (protocol.type === proto.Message.ProtocolMessage.Type.REVOKE) {
|
|
681
866
|
await this.engine.unset(`/chat/${target_cid}/message/${target_mid}`);
|
|
682
867
|
if (doc) {
|
|
683
|
-
const msg_instance =
|
|
684
|
-
this.
|
|
868
|
+
const msg_instance = message(this, doc);
|
|
869
|
+
this.emit('message:deleted', msg_instance, await msg_instance.chat(), this);
|
|
685
870
|
}
|
|
686
871
|
}
|
|
687
872
|
}
|
|
688
873
|
continue;
|
|
689
874
|
}
|
|
690
|
-
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
|
+
}
|
|
691
897
|
// Autocreación de contacto/chat desde pushName cuando baileys no emite upsert previo
|
|
692
|
-
if (!doc.me) {
|
|
898
|
+
if (!existing_doc && !doc.me) {
|
|
693
899
|
const push_name = msg.pushName ?? null;
|
|
694
900
|
const is_group = cid.endsWith('@g.us');
|
|
695
901
|
if (doc.author && !(await this.engine.get(`/contact/${doc.author}`))) {
|
|
696
|
-
await this
|
|
902
|
+
await this.#persist_contact({
|
|
697
903
|
id: doc.author,
|
|
698
904
|
lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : null,
|
|
699
905
|
name: null,
|
|
@@ -709,20 +915,17 @@ export class WhatsApp {
|
|
|
709
915
|
name: is_group ? null : push_name,
|
|
710
916
|
};
|
|
711
917
|
await this.engine.set(`/chat/${cid}`, serialize(chat_raw));
|
|
712
|
-
this.
|
|
918
|
+
this.emit('chat:created', new this.Chat(chat_raw), this);
|
|
713
919
|
}
|
|
714
920
|
}
|
|
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));
|
|
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.
|
|
724
924
|
let content_buf = Buffer.alloc(0);
|
|
725
|
-
if (
|
|
925
|
+
if (existing_doc) {
|
|
926
|
+
/* ya materializado / already materialized */
|
|
927
|
+
}
|
|
928
|
+
else if (doc.type === 'text') {
|
|
726
929
|
content_buf = Buffer.from(doc.caption, 'utf-8');
|
|
727
930
|
}
|
|
728
931
|
else if (doc.type === 'location') {
|
|
@@ -745,7 +948,7 @@ export class WhatsApp {
|
|
|
745
948
|
else if (doc.type === 'event') {
|
|
746
949
|
content_buf = Buffer.from(JSON.stringify(msg.message?.eventMessage ?? {}), 'utf-8');
|
|
747
950
|
}
|
|
748
|
-
else if (this.
|
|
951
|
+
else if (this.#internals.socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
|
|
749
952
|
try {
|
|
750
953
|
const buffer = await downloadMediaMessage(msg, 'buffer', {});
|
|
751
954
|
if (Buffer.isBuffer(buffer)) {
|
|
@@ -759,17 +962,17 @@ export class WhatsApp {
|
|
|
759
962
|
if (content_buf.length > 0) {
|
|
760
963
|
await this.engine.set(`/chat/${cid}/message/${mid}/content`, serialize({ data: content_buf.toString('base64') }));
|
|
761
964
|
}
|
|
762
|
-
const instance =
|
|
965
|
+
const instance = message(this, doc);
|
|
763
966
|
const chat_instance = await instance.chat();
|
|
764
|
-
this.
|
|
967
|
+
this.emit('message:created', instance, chat_instance, this);
|
|
765
968
|
if (doc.forwarded) {
|
|
766
|
-
this.
|
|
969
|
+
this.emit('message:forwarded', instance, chat_instance, this);
|
|
767
970
|
}
|
|
768
971
|
}
|
|
769
972
|
}
|
|
770
973
|
}
|
|
771
974
|
/** @internal */
|
|
772
|
-
async
|
|
975
|
+
async #handle_messages_update(updates) {
|
|
773
976
|
for (const { key, update: upd } of updates) {
|
|
774
977
|
if (key.remoteJid && key.id) {
|
|
775
978
|
// Updates sobre `status@broadcast` se descartan: el feed sólo se
|
|
@@ -791,42 +994,42 @@ export class WhatsApp {
|
|
|
791
994
|
raw.message = edited_message;
|
|
792
995
|
doc.raw = raw;
|
|
793
996
|
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.
|
|
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);
|
|
798
1001
|
}
|
|
799
1002
|
else if (content_update) {
|
|
800
1003
|
// Actualización de contenido (ej: live location). Mergea sobre el raw existente.
|
|
801
1004
|
raw.message = { ...raw.message, ...content_update };
|
|
802
1005
|
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.
|
|
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);
|
|
807
1010
|
}
|
|
808
1011
|
else if (starred_changed) {
|
|
809
1012
|
doc.starred = upd_any.starred === true;
|
|
810
1013
|
raw.starred = doc.starred;
|
|
811
1014
|
doc.raw = raw;
|
|
812
|
-
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc));
|
|
813
|
-
const msg_instance =
|
|
814
|
-
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);
|
|
815
1018
|
}
|
|
816
1019
|
else if (status !== undefined) {
|
|
817
1020
|
raw.status = status;
|
|
818
1021
|
doc.status = status;
|
|
819
1022
|
doc.raw = raw;
|
|
820
|
-
await this.engine.set(`/chat/${key.remoteJid}/message/${key.id}`, serialize(doc));
|
|
821
|
-
const msg_instance =
|
|
822
|
-
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);
|
|
823
1026
|
}
|
|
824
1027
|
}
|
|
825
1028
|
}
|
|
826
1029
|
}
|
|
827
1030
|
}
|
|
828
1031
|
/** @internal */
|
|
829
|
-
async
|
|
1032
|
+
async #handle_messages_reaction(reactions) {
|
|
830
1033
|
for (const { key, reaction } of reactions) {
|
|
831
1034
|
if (key.remoteJid && key.id) {
|
|
832
1035
|
// Reacciones sobre status@broadcast → feed:updated (no message:reacted).
|
|
@@ -834,14 +1037,21 @@ export class WhatsApp {
|
|
|
834
1037
|
if (key.remoteJid === 'status@broadcast') {
|
|
835
1038
|
const feed_raw = deserialize(await this.engine.get(`/status/${key.id}`));
|
|
836
1039
|
if (feed_raw) {
|
|
837
|
-
this.
|
|
1040
|
+
this.emit('feed:updated', new Feed(this, feed_raw), this);
|
|
838
1041
|
}
|
|
839
1042
|
continue;
|
|
840
1043
|
}
|
|
841
1044
|
const doc = deserialize(await this.engine.get(`/chat/${key.remoteJid}/message/${key.id}`));
|
|
842
1045
|
if (doc) {
|
|
843
|
-
const
|
|
844
|
-
|
|
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);
|
|
845
1055
|
}
|
|
846
1056
|
}
|
|
847
1057
|
}
|