@arcaelas/whatsapp 6.2.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -15
- package/build/cjs/index.d.ts +9 -5
- package/build/cjs/index.js +8 -7
- package/build/cjs/lib/bot/decorator.d.ts +2 -9
- package/build/cjs/lib/bot/decorator.js +0 -1
- package/build/cjs/lib/bot/decorators.d.ts +48 -4
- package/build/cjs/lib/bot/decorators.js +2 -2
- package/build/cjs/lib/chat/index.d.ts +132 -152
- package/build/cjs/lib/chat/index.js +138 -219
- package/build/cjs/lib/contact/index.d.ts +114 -80
- package/build/cjs/lib/contact/index.js +187 -94
- package/build/cjs/lib/message/index.d.ts +400 -321
- package/build/cjs/lib/message/index.js +425 -913
- package/build/cjs/lib/status/index.d.ts +22 -33
- package/build/cjs/lib/status/index.js +52 -93
- package/build/cjs/lib/store/index.d.ts +16 -0
- package/build/cjs/lib/store/index.js +29 -3
- package/build/cjs/lib/whatsapp/index.d.ts +44 -212
- package/build/cjs/lib/whatsapp/index.js +483 -1099
- package/build/esm/index.d.ts +9 -5
- package/build/esm/index.js +6 -4
- package/build/esm/lib/bot/decorator.d.ts +2 -9
- package/build/esm/lib/bot/decorator.js +1 -1
- package/build/esm/lib/bot/decorators.d.ts +48 -4
- package/build/esm/lib/bot/decorators.js +2 -2
- package/build/esm/lib/chat/index.d.ts +132 -152
- package/build/esm/lib/chat/index.js +139 -219
- package/build/esm/lib/contact/index.d.ts +114 -80
- package/build/esm/lib/contact/index.js +186 -94
- package/build/esm/lib/message/index.d.ts +400 -321
- package/build/esm/lib/message/index.js +422 -913
- package/build/esm/lib/status/index.d.ts +22 -33
- package/build/esm/lib/status/index.js +49 -93
- package/build/esm/lib/store/index.d.ts +16 -0
- package/build/esm/lib/store/index.js +25 -0
- package/build/esm/lib/whatsapp/index.d.ts +44 -212
- package/build/esm/lib/whatsapp/index.js +486 -1101
- package/package.json +1 -1
- package/build/cjs/lib/internal.d.ts +0 -40
- package/build/cjs/lib/internal.js +0 -38
- package/build/esm/lib/internal.d.ts +0 -40
- package/build/esm/lib/internal.js +0 -34
|
@@ -1,245 +1,54 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file whatsapp/index.ts
|
|
3
|
-
* @description Orquestador principal del cliente WhatsApp v3.
|
|
4
|
-
* Main orchestrator of the WhatsApp v3 client.
|
|
5
|
-
*/
|
|
6
1
|
import { Browsers, decryptPollVote, DisconnectReason, downloadMediaMessage, fetchLatestBaileysVersion, getContentType, initAuthCreds, jidNormalizedUser, makeWASocket, proto, updateMessageWithPollUpdate, } from 'baileys';
|
|
7
2
|
import { EventEmitter } from 'node:events';
|
|
8
3
|
import pino from 'pino';
|
|
9
4
|
import * as QRCode from 'qrcode';
|
|
10
5
|
import { chat } from '../../lib/chat/index.js';
|
|
11
|
-
import { contact } from '../../lib/contact/index.js';
|
|
12
|
-
import {
|
|
13
|
-
import { Audio, Document, Event, Image, Location, message, Message, Poll, Sticker, Text, VCard, Video, } from '../../lib/message/index.js';
|
|
6
|
+
import { Account, contact } from '../../lib/contact/index.js';
|
|
7
|
+
import Message, { message } from '../../lib/message/index.js';
|
|
14
8
|
import { Feed, TTL_MS as FEED_TTL_MS } from '../../lib/status/index.js';
|
|
15
|
-
import { deserialize, serialize } from '../../lib/store/index.js';
|
|
16
|
-
|
|
17
|
-
const ERROR = 0;
|
|
18
|
-
/** Estados legibles del mensaje que el receipt puede avanzar. / Readable message states a receipt can advance to. */
|
|
19
|
-
const READ = 4;
|
|
20
|
-
const PLAYED = 5;
|
|
21
|
-
/**
|
|
22
|
-
* Deduce el MIME de un binario por su firma, acotado a lo que WhatsApp acepta en un estado.
|
|
23
|
-
* Infers a binary's MIME from its signature, limited to what WhatsApp accepts in a status.
|
|
24
|
-
*
|
|
25
|
-
* @param data - Binario a inspeccionar / Binary to inspect
|
|
26
|
-
* @returns MIME reconocido, o null / Recognized MIME, or null
|
|
27
|
-
*/
|
|
28
|
-
function sniff_media(data) {
|
|
29
|
-
if (data.subarray(0, 3).toString('hex') === 'ffd8ff')
|
|
30
|
-
return 'image/jpeg';
|
|
31
|
-
if (data.subarray(0, 8).toString('hex') === '89504e470d0a1a0a')
|
|
32
|
-
return 'image/png';
|
|
33
|
-
if (data.subarray(0, 4).toString() === 'RIFF' && data.subarray(8, 12).toString() === 'WEBP')
|
|
34
|
-
return 'image/webp';
|
|
35
|
-
if (data.subarray(4, 8).toString() === 'ftyp')
|
|
36
|
-
return 'video/mp4';
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Cliente principal de WhatsApp. No inicia la conexión al instanciar.
|
|
41
|
-
* Main WhatsApp client. Does not connect on instantiation.
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* const wa = new WhatsApp({ engine: new FileSystemEngine(__dirname), phone: 5491112345678 });
|
|
45
|
-
* wa.on('message:created', (msg) => console.log(msg.caption));
|
|
46
|
-
* await wa.connect((code) => console.log(code));
|
|
47
|
-
*/
|
|
48
|
-
export class WhatsApp {
|
|
49
|
-
/** @internal Emisor de los eventos del cliente. / Client event emitter. */
|
|
9
|
+
import { deserialize, jid_of, serialize } from '../../lib/store/index.js';
|
|
10
|
+
export default class WhatsApp {
|
|
50
11
|
#event = new EventEmitter();
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
#phone;
|
|
54
|
-
#method;
|
|
55
|
-
#autoclean;
|
|
56
|
-
#reconnect;
|
|
57
|
-
#sync;
|
|
58
|
-
#intentional_close = false;
|
|
59
|
-
#silent_close = false;
|
|
60
|
-
#has_connected = false;
|
|
61
|
-
#retry_timer = null;
|
|
62
|
-
#retry_count = 0;
|
|
63
|
-
/**
|
|
64
|
-
* @internal
|
|
65
|
-
* Cadena que serializa los handlers de eventos de baileys: dos eventos sobre el mismo
|
|
66
|
-
* documento ya no se intercalan (lost updates por read-modify-write concurrente).
|
|
67
|
-
* Chain serializing baileys event handlers: two events over the same document no longer
|
|
68
|
-
* interleave (lost updates from concurrent read-modify-write).
|
|
69
|
-
*/
|
|
70
|
-
#chain = Promise.resolve();
|
|
12
|
+
#options;
|
|
13
|
+
#close = null;
|
|
71
14
|
constructor(options) {
|
|
72
15
|
this.engine = options.engine;
|
|
73
|
-
this.#
|
|
74
|
-
this.#method = options.method;
|
|
75
|
-
this.#autoclean = options.autoclean ?? true;
|
|
76
|
-
this.#sync = options.sync ?? true;
|
|
77
|
-
this.#reconnect =
|
|
78
|
-
options.reconnect === false ? { max: 0, interval_ms: 60_000 }
|
|
79
|
-
: options.reconnect === undefined || options.reconnect === true ? { max: null, interval_ms: 60_000 }
|
|
80
|
-
: typeof options.reconnect === 'number' ? { max: options.reconnect, interval_ms: 60_000 }
|
|
81
|
-
: { max: options.reconnect.max ?? null, interval_ms: (options.reconnect.interval ?? 60) * 1_000 };
|
|
82
|
-
this.#internals = { socket: null, resolve_jid: (uid) => this.#resolve_jid(uid) };
|
|
83
|
-
bind(this, this.#internals);
|
|
84
|
-
this.Contact = contact(this);
|
|
85
|
-
this.Chat = chat(this);
|
|
86
|
-
this.Message = {
|
|
87
|
-
get: (cid, mid) => Message.get(this, cid, mid),
|
|
88
|
-
list: (cid, offset, limit) => Message.list(this, cid, offset, limit),
|
|
89
|
-
text: (cid, ...rest) => Message.text(this, cid, ...rest),
|
|
90
|
-
image: (cid, ...rest) => Message.image(this, cid, ...rest),
|
|
91
|
-
video: (cid, ...rest) => Message.video(this, cid, ...rest),
|
|
92
|
-
audio: (cid, ...rest) => Message.audio(this, cid, ...rest),
|
|
93
|
-
location: (cid, ...rest) => Message.location(this, cid, ...rest),
|
|
94
|
-
poll: (cid, ...rest) => Message.poll(this, cid, ...rest),
|
|
95
|
-
document: (cid, ...rest) => Message.document(this, cid, ...rest),
|
|
96
|
-
vcard: (cid, ...rest) => Message.vcard(this, cid, ...rest),
|
|
97
|
-
event: (cid, ...rest) => Message.event(this, cid, ...rest),
|
|
98
|
-
react: (cid, mid, emoji) => Message.react(this, cid, mid, emoji),
|
|
99
|
-
star: (cid, mid, value) => Message.star(this, cid, mid, value),
|
|
100
|
-
seen: (cid, mid) => Message.seen(this, cid, mid),
|
|
101
|
-
edit: (cid, mid, caption) => Message.edit(this, cid, mid, caption),
|
|
102
|
-
forward: (cid, mid, target) => Message.forward(this, cid, mid, target),
|
|
103
|
-
delete: (cid, mid, all) => Message.delete(this, cid, mid, all),
|
|
104
|
-
reactions: (cid, mid) => Message.reactions(this, cid, mid),
|
|
105
|
-
Text, Image, Video, Audio, Sticker, Document, Location, Poll, VCard, Event,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Contacto de la cuenta autenticada, o null mientras no hay sesión abierta.
|
|
110
|
-
* Authenticated account's contact, or null while there is no open session.
|
|
111
|
-
*/
|
|
112
|
-
get contact() {
|
|
113
|
-
const user = this.#internals.socket?.user;
|
|
114
|
-
if (user) {
|
|
115
|
-
const jid = jidNormalizedUser(user.id);
|
|
116
|
-
return new this.Contact({ id: jid, phone_number: jid, lid: user.lid ?? null, name: user.name ?? null });
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* @internal
|
|
122
|
-
* Persiste un binario: crudo cuando el driver lo soporta, JSON con base64 si no.
|
|
123
|
-
* Persists a binary: raw when the driver supports it, base64 JSON otherwise.
|
|
124
|
-
*/
|
|
125
|
-
async #write_content(path, data) {
|
|
126
|
-
if (this.engine.set_buffer) {
|
|
127
|
-
await this.engine.set_buffer(path, data);
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
await this.engine.set(path, serialize({ data: data.toString('base64') }));
|
|
131
|
-
}
|
|
16
|
+
this.#options = options;
|
|
132
17
|
}
|
|
133
|
-
/** @internal Encola una tarea en la cadena serial de handlers. / Queues a task on the serial handler chain. */
|
|
134
|
-
#enqueue(task) {
|
|
135
|
-
this.#chain = this.#chain.then(task).catch(() => { });
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Emite un evento del cliente. Lo usan las entidades de la librería para propagar los
|
|
139
|
-
* cambios que provocan; el consumidor puede emitir los suyos para pruebas.
|
|
140
|
-
* Emits a client event. Library entities use it to propagate the changes they cause;
|
|
141
|
-
* consumers may emit their own for testing.
|
|
142
|
-
*
|
|
143
|
-
* @param event - Nombre del evento / Event name
|
|
144
|
-
* @param args - Argumentos del evento / Event arguments
|
|
145
|
-
* @returns true si había listeners / true when listeners were present
|
|
146
|
-
*/
|
|
147
18
|
emit(event, ...args) {
|
|
148
19
|
return this.#event.emit(event, ...args);
|
|
149
20
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Registra un listener de evento. Retorna función para desuscribirse.
|
|
152
|
-
* Registers an event listener. Returns an unsubscribe function.
|
|
153
|
-
*/
|
|
154
21
|
on(event, handler) {
|
|
155
22
|
this.#event.on(event, handler);
|
|
156
23
|
return () => { this.#event.off(event, handler); };
|
|
157
24
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Quita un listener previamente registrado.
|
|
160
|
-
* Removes a previously registered listener.
|
|
161
|
-
*/
|
|
162
|
-
off(event, handler) {
|
|
163
|
-
this.#event.off(event, handler);
|
|
164
|
-
return this;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Registra un listener one-shot. Retorna función para desuscribirse antes de que dispare.
|
|
168
|
-
* Registers a one-shot listener. Returns an unsubscribe function.
|
|
169
|
-
*/
|
|
170
25
|
once(event, handler) {
|
|
171
26
|
this.#event.once(event, handler);
|
|
172
27
|
return () => { this.#event.off(event, handler); };
|
|
173
28
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* entidades lo alcanzan por el canal interno, no por la instancia.
|
|
178
|
-
* Normalizes any identifier (JID, LID, number…) into a canonical JID. Entities reach it
|
|
179
|
-
* through the internal channel, not through the instance.
|
|
180
|
-
*/
|
|
181
|
-
async #resolve_jid(uid) {
|
|
182
|
-
let result = null;
|
|
183
|
-
if (uid.endsWith('@g.us') || uid.endsWith('@s.whatsapp.net')) {
|
|
184
|
-
result = uid;
|
|
185
|
-
}
|
|
186
|
-
else if (uid.endsWith('@lid')) {
|
|
187
|
-
// Los receipts direccionan por dispositivo (`…:9@lid`); el índice se guarda sin él.
|
|
188
|
-
// Receipts address per device (`…:9@lid`); the index is stored without it.
|
|
189
|
-
const lid = jidNormalizedUser(uid);
|
|
190
|
-
const direct = deserialize(await this.engine.get(`/lid/${lid}`));
|
|
191
|
-
if (direct) {
|
|
192
|
-
result = direct.includes('@') ? direct : `${direct}@s.whatsapp.net`;
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
const reverse = deserialize(await this.engine.get(`/lid/${lid.split('@')[0]}_reverse`));
|
|
196
|
-
if (reverse != null) {
|
|
197
|
-
result = `${reverse}@s.whatsapp.net`;
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
// El store local puede no tener el mapping (sesión sin upsert del contacto);
|
|
201
|
-
// baileys lo conoce vía su lidMapping. Sin esto, un chat referenciado por @lid
|
|
202
|
-
// (p.ej. el pollCreationMessageKey de un voto entrante) no resuelve al PN donde
|
|
203
|
-
// realmente está guardado, y el mensaje/poll no se encuentra.
|
|
204
|
-
const pn = await this.#internals.socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null);
|
|
205
|
-
if (pn) {
|
|
206
|
-
// getPNForLID puede traer sufijo de dispositivo (`:0`); se normaliza para
|
|
207
|
-
// que el JID coincida con el que usa el store (sin device).
|
|
208
|
-
result = jidNormalizedUser(pn.includes('@') ? pn : `${pn}@s.whatsapp.net`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
const cleaned = uid.replace(/\D/g, '');
|
|
215
|
-
if (cleaned) {
|
|
216
|
-
result = `${cleaned}@s.whatsapp.net`;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return result;
|
|
29
|
+
off(event, handler) {
|
|
30
|
+
this.#event.off(event, handler);
|
|
31
|
+
return this;
|
|
220
32
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Inicia la conexión. El callback recibe el PIN (string) si se configuró `phone`, o el QR (Buffer PNG) si no.
|
|
223
|
-
* Resuelve cuando la sesión sincroniza; reintenta automáticamente en cierres no-loggedOut.
|
|
224
|
-
*
|
|
225
|
-
* Starts the connection. Callback receives the PIN (string) when `phone` is configured, or the QR (PNG Buffer) otherwise.
|
|
226
|
-
* Resolves once the session is synced; retries on non-loggedOut disconnects.
|
|
227
|
-
*/
|
|
228
33
|
async connect(callback) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
34
|
+
const { engine } = this;
|
|
35
|
+
const { phone, method, autoclean = true, sync = true, reconnect = true } = this.#options;
|
|
36
|
+
const digits = phone !== undefined ? String(phone).replace(/\D+/g, '') : '';
|
|
37
|
+
const budget = reconnect === false ? 0 : reconnect === true ? null : typeof reconnect === 'number' ? reconnect : reconnect.max ?? null;
|
|
38
|
+
const wait = typeof reconnect === 'object' ? (reconnect.interval ?? 60) * 1_000 : 60_000;
|
|
39
|
+
await this.#close?.(true);
|
|
232
40
|
const { version } = await fetchLatestBaileysVersion();
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
41
|
+
let connected = false;
|
|
42
|
+
let retries = 0;
|
|
43
|
+
let intentional = false;
|
|
44
|
+
let silent = false;
|
|
45
|
+
let alive = null;
|
|
46
|
+
let timer = null;
|
|
47
|
+
let chain = Promise.resolve();
|
|
236
48
|
return new Promise((resolve, reject) => {
|
|
237
49
|
const start = async () => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const stored = await this.engine.get('/session/creds');
|
|
241
|
-
const creds = deserialize(stored) ?? initAuthCreds();
|
|
242
|
-
this.#internals.socket = makeWASocket({
|
|
50
|
+
const creds = deserialize(await engine.get('/session/creds')) ?? initAuthCreds();
|
|
51
|
+
const socket = makeWASocket({
|
|
243
52
|
version,
|
|
244
53
|
auth: {
|
|
245
54
|
creds,
|
|
@@ -247,949 +56,525 @@ export class WhatsApp {
|
|
|
247
56
|
get: async (type, ids) => {
|
|
248
57
|
const data = {};
|
|
249
58
|
await Promise.all(ids.map(async (id) => {
|
|
250
|
-
const value = deserialize(await
|
|
59
|
+
const value = deserialize(await engine.get(`/session/${type}/${id}`));
|
|
251
60
|
if (value) {
|
|
252
|
-
data[id] =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
: value;
|
|
61
|
+
data[id] = type === 'app-state-sync-key'
|
|
62
|
+
? proto.Message.AppStateSyncKeyData.create(value)
|
|
63
|
+
: value;
|
|
256
64
|
}
|
|
257
65
|
}));
|
|
258
66
|
return data;
|
|
259
67
|
},
|
|
260
68
|
set: async (data) => {
|
|
261
69
|
await Promise.all(Object.entries(data).flatMap(([category, entries]) => Object.entries(entries).map(([id, value]) => value != null
|
|
262
|
-
?
|
|
263
|
-
:
|
|
70
|
+
? engine.set(`/session/${category}/${id}`, serialize(value))
|
|
71
|
+
: engine.unset(`/session/${category}/${id}`))));
|
|
264
72
|
},
|
|
265
73
|
},
|
|
266
74
|
},
|
|
267
75
|
browser: Browsers.windows('Chrome'),
|
|
268
76
|
logger: pino({ level: 'silent' }),
|
|
269
|
-
syncFullHistory:
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
77
|
+
syncFullHistory: sync,
|
|
78
|
+
shouldSyncHistoryMessage: ({ syncType }) => sync || syncType !== proto.HistorySync.HistorySyncType.FULL,
|
|
79
|
+
// Cuando el receptor no puede descifrar pide un retry; el cache interno de
|
|
80
|
+
// baileys indexa por JID pero el retry llega por LID y no lo encuentra: sin
|
|
81
|
+
// este fallback el mensaje muere en un solo check.
|
|
82
|
+
// When the receiver cannot decrypt it asks for a retry; the internal baileys
|
|
83
|
+
// cache indexes by JID but the retry arrives by LID and misses: without this
|
|
84
|
+
// fallback the message dies at a single check.
|
|
85
|
+
getMessage: async (key) => {
|
|
86
|
+
const found = key.remoteJid && key.id ? await locate(key.remoteJid, key.id) : null;
|
|
87
|
+
return found?.doc.raw.message ?? undefined;
|
|
88
|
+
},
|
|
279
89
|
markOnlineOnConnect: false,
|
|
280
90
|
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
91
|
+
alive = socket;
|
|
92
|
+
const init = { wa: this, engine, socket };
|
|
93
|
+
this.Contact = contact(init);
|
|
94
|
+
this.Chat = chat(init);
|
|
95
|
+
this.Message = message(init);
|
|
96
|
+
this.account = async () => {
|
|
97
|
+
const user = socket.user;
|
|
98
|
+
if (!user)
|
|
99
|
+
return null;
|
|
100
|
+
const id = jidNormalizedUser(user.id);
|
|
101
|
+
const card = deserialize(await engine.get(`/contact/${id}`));
|
|
102
|
+
return new Account(init, {
|
|
103
|
+
id,
|
|
104
|
+
phone_number: id,
|
|
105
|
+
lid: user.lid ?? card?.lid ?? null,
|
|
106
|
+
name: user.name ?? card?.name ?? null,
|
|
107
|
+
notify: card?.notify ?? null,
|
|
108
|
+
verified_name: card?.verified_name ?? null,
|
|
109
|
+
img_url: (await socket.profilePictureUrl(id, 'image').catch(() => null)) ?? card?.img_url ?? null,
|
|
110
|
+
status: card?.status ?? null,
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
const locate = async (cid, mid) => {
|
|
114
|
+
const lid = cid.endsWith('@lid') ? jidNormalizedUser(cid) : '';
|
|
115
|
+
const resolved = await jid_of(engine, cid, socket);
|
|
116
|
+
for (const candidate of new Set([resolved, cid, lid].filter(Boolean))) {
|
|
117
|
+
const path = `/chat/${candidate}/message/${mid}`;
|
|
118
|
+
const doc = deserialize(await engine.get(path));
|
|
119
|
+
if (doc) {
|
|
120
|
+
return { path, doc };
|
|
298
121
|
}
|
|
299
122
|
}
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
125
|
+
socket.ev.on('creds.update', () => engine.set('/session/creds', serialize(creds)));
|
|
126
|
+
socket.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
|
|
127
|
+
if (qr && !creds.registered) {
|
|
128
|
+
await callback(digits && (method ?? 'otp') === 'otp'
|
|
129
|
+
? await socket.requestPairingCode(digits)
|
|
130
|
+
: await QRCode.toBuffer(qr, { type: 'png', margin: 2 }));
|
|
131
|
+
}
|
|
300
132
|
if (connection === 'open') {
|
|
301
|
-
|
|
302
|
-
|
|
133
|
+
connected = true;
|
|
134
|
+
retries = 0;
|
|
303
135
|
this.emit('connected', this);
|
|
304
136
|
resolve();
|
|
305
137
|
}
|
|
306
138
|
else if (connection === 'close') {
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const is_transient = status_code === DisconnectReason.restartRequired;
|
|
312
|
-
// Limpieza del engine ANTES de emitir `disconnected` para que los
|
|
313
|
-
// listeners vean el estado final (engine vaciado o creds borradas).
|
|
314
|
-
if (status_code === DisconnectReason.loggedOut) {
|
|
315
|
-
if (this.#autoclean) {
|
|
316
|
-
await this.engine.clear();
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
await this.engine.unset('/session/creds');
|
|
320
|
-
}
|
|
139
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
140
|
+
const transient = code === DisconnectReason.restartRequired;
|
|
141
|
+
if (code === DisconnectReason.loggedOut) {
|
|
142
|
+
await (autoclean ? engine.clear() : engine.unset('/session/creds'));
|
|
321
143
|
}
|
|
322
|
-
|
|
323
|
-
// `disconnect({ silent: true })` mutes this specific close's event.
|
|
324
|
-
if (this.#has_connected && !is_transient && !this.#silent_close) {
|
|
144
|
+
if (connected && !transient && !silent) {
|
|
325
145
|
this.emit('disconnected', this);
|
|
326
146
|
}
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
147
|
+
if (intentional) {
|
|
148
|
+
/* cierre pedido por disconnect(): sin reintentos / close requested by disconnect(): no retries */
|
|
149
|
+
}
|
|
150
|
+
else if (code === DisconnectReason.loggedOut) {
|
|
151
|
+
reject(new Error('Logged out'));
|
|
152
|
+
}
|
|
153
|
+
else if (!transient && budget !== null && retries >= budget) {
|
|
154
|
+
reject(new Error(`Reconnect attempts exhausted (${budget})`));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
retries += transient ? 0 : 1;
|
|
158
|
+
timer = setTimeout(() => {
|
|
159
|
+
timer = null;
|
|
160
|
+
start().catch(reject);
|
|
161
|
+
}, transient ? 0 : wait);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
socket.ev.on('messaging-history.set', ({ chats, contacts, messages }) => {
|
|
166
|
+
socket.ev.emit('contacts.upsert', contacts);
|
|
167
|
+
socket.ev.emit('chats.upsert', chats);
|
|
168
|
+
socket.ev.emit('messages.upsert', { messages, type: 'append' });
|
|
169
|
+
});
|
|
170
|
+
socket.ev.on('contacts.upsert', (rows) => {
|
|
171
|
+
chain = chain.then(async () => {
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
if (row.id) {
|
|
174
|
+
const current = deserialize(await engine.get(`/contact/${row.id}`));
|
|
175
|
+
const doc = {
|
|
176
|
+
id: row.id,
|
|
177
|
+
lid: row.lid ?? current?.lid ?? null,
|
|
178
|
+
name: row.name ?? current?.name ?? null,
|
|
179
|
+
notify: row.notify ?? current?.notify ?? null,
|
|
180
|
+
verified_name: row.verifiedName ?? current?.verified_name ?? null,
|
|
181
|
+
img_url: (typeof row.imgUrl === 'string' ? row.imgUrl : null) ?? current?.img_url ?? null,
|
|
182
|
+
status: row.status ?? current?.status ?? null,
|
|
183
|
+
};
|
|
184
|
+
if (!current || JSON.stringify(current) !== JSON.stringify(doc)) {
|
|
185
|
+
await engine.set(`/contact/${row.id}`, serialize(doc));
|
|
186
|
+
if (doc.lid) {
|
|
187
|
+
await engine.set(`/lid/${doc.lid}`, serialize(doc.id));
|
|
342
188
|
}
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
start().catch(reject);
|
|
347
|
-
}, delay);
|
|
189
|
+
const person = new this.Contact(doc);
|
|
190
|
+
const owner = deserialize(await engine.get(`/chat/${doc.id}`));
|
|
191
|
+
this.emit(current ? 'contact:updated' : 'contact:created', person, new this.Chat(owner ?? { id: doc.id, name: person.name }), this);
|
|
348
192
|
}
|
|
349
193
|
}
|
|
350
194
|
}
|
|
351
|
-
}
|
|
195
|
+
}).catch(() => { });
|
|
352
196
|
});
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
await socket.updateProfileStatus(patch.content);
|
|
378
|
-
}
|
|
379
|
-
if (patch.photo === null) {
|
|
380
|
-
await socket.removeProfilePicture(self);
|
|
381
|
-
}
|
|
382
|
-
else if (patch.photo !== undefined) {
|
|
383
|
-
// baileys redimensiona la foto con `sharp` o `jimp`; ninguna es dependencia
|
|
384
|
-
// nuestra, así que su ausencia se traduce a un error accionable.
|
|
385
|
-
// baileys resizes the picture with `sharp` or `jimp`; neither is a dependency
|
|
386
|
-
// of ours, so their absence is translated into an actionable error.
|
|
387
|
-
await socket
|
|
388
|
-
.updateProfilePicture(self, typeof patch.photo === 'string' ? { url: patch.photo } : patch.photo)
|
|
389
|
-
.catch((error) => {
|
|
390
|
-
if (/image processing library/i.test(error.message)) {
|
|
391
|
-
throw new Error('ERR_PROFILE_PICTURE_LIB');
|
|
392
|
-
}
|
|
393
|
-
throw error;
|
|
197
|
+
socket.ev.on('contacts.update', (rows) => {
|
|
198
|
+
chain = chain.then(async () => {
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
const current = row.id ? deserialize(await engine.get(`/contact/${row.id}`)) : null;
|
|
201
|
+
const patch = {
|
|
202
|
+
...(row.notify && { notify: row.notify }),
|
|
203
|
+
...(row.name && { name: row.name }),
|
|
204
|
+
...(row.verifiedName && { verified_name: row.verifiedName }),
|
|
205
|
+
...(typeof row.imgUrl === 'string' && { img_url: row.imgUrl }),
|
|
206
|
+
...(row.status && { status: row.status }),
|
|
207
|
+
...(row.lid && { lid: row.lid }),
|
|
208
|
+
};
|
|
209
|
+
if (current && row.id && Object.keys(patch).length > 0) {
|
|
210
|
+
const doc = { ...current, ...patch };
|
|
211
|
+
await engine.set(`/contact/${row.id}`, serialize(doc));
|
|
212
|
+
if (patch.lid) {
|
|
213
|
+
await engine.set(`/lid/${patch.lid}`, serialize(row.id));
|
|
214
|
+
}
|
|
215
|
+
const person = new this.Contact(doc);
|
|
216
|
+
const owner = deserialize(await engine.get(`/chat/${row.id}`));
|
|
217
|
+
this.emit('contact:updated', person, new this.Chat(owner ?? { id: row.id, name: person.name }), this);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}).catch(() => { });
|
|
394
221
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Publica un estado (status broadcast). Con sólo `caption` publica texto; con `content`
|
|
402
|
-
* publica imagen o video (el tipo se deduce del binario) usando `caption` como pie.
|
|
403
|
-
* `contacts` es la audiencia: WhatsApp no reparte el estado a quien no esté en la lista.
|
|
404
|
-
* Publishes a status broadcast. With only `caption` it posts text; with `content` it
|
|
405
|
-
* posts an image or video (type inferred from the binary) using `caption` as its footer.
|
|
406
|
-
* `contacts` is the audience: WhatsApp does not deliver the status to anyone outside it.
|
|
407
|
-
*
|
|
408
|
-
* @param post - Contenido, pie y audiencia / Content, caption and audience
|
|
409
|
-
* @returns Publicación creada, o null si no hay sesión / Created post, or null without a session
|
|
410
|
-
* @throws ERR_FEED_EMPTY sin `content` ni `caption` / when neither `content` nor `caption` is given
|
|
411
|
-
* @throws ERR_FEED_MEDIA si el binario no es imagen ni video / when the binary is neither image nor video
|
|
412
|
-
*/
|
|
413
|
-
async feed(post) {
|
|
414
|
-
const socket = this.#internals.socket;
|
|
415
|
-
let result = null;
|
|
416
|
-
if (socket) {
|
|
417
|
-
const audience = (await Promise.all(post.contacts.map((uid) => this.#resolve_jid(String(uid))))).filter((jid) => jid !== null);
|
|
418
|
-
const mime = post.content ? sniff_media(post.content) : null;
|
|
419
|
-
if (post.content && !mime) {
|
|
420
|
-
throw new Error('ERR_FEED_MEDIA');
|
|
421
|
-
}
|
|
422
|
-
if (!post.content && !post.caption) {
|
|
423
|
-
throw new Error('ERR_FEED_EMPTY');
|
|
424
|
-
}
|
|
425
|
-
const kind = mime?.startsWith('video/') ? 'video' : mime ? 'image' : 'text';
|
|
426
|
-
const sent = await socket.sendMessage('status@broadcast', (post.content
|
|
427
|
-
? { [kind]: post.content, caption: post.caption }
|
|
428
|
-
: { text: post.caption }), { statusJidList: audience });
|
|
429
|
-
if (sent?.key?.id) {
|
|
430
|
-
const created_at = (Number(sent.messageTimestamp) || Math.floor(Date.now() / 1_000)) * 1_000;
|
|
431
|
-
const doc = {
|
|
432
|
-
id: sent.key.id,
|
|
433
|
-
author_jid: jidNormalizedUser(socket.user?.id ?? ''),
|
|
434
|
-
type: kind,
|
|
435
|
-
caption: post.caption ?? '',
|
|
436
|
-
mime: mime ?? 'text/plain',
|
|
437
|
-
created_at,
|
|
438
|
-
expires_at: created_at + FEED_TTL_MS,
|
|
439
|
-
viewed: true,
|
|
440
|
-
raw: sent,
|
|
441
|
-
};
|
|
442
|
-
await this.engine.set(`/status/${doc.id}`, serialize(doc), created_at);
|
|
443
|
-
if (post.content) {
|
|
444
|
-
await this.#write_content(`/status/${doc.id}/content`, post.content);
|
|
445
|
-
}
|
|
446
|
-
result = new Feed(this, doc);
|
|
447
|
-
this.emit('feed:created', result, this);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return result;
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Cierra la conexión. Con `destroy: true` vacía el engine completo.
|
|
454
|
-
* Closes the connection. With `destroy: true` clears the engine entirely.
|
|
455
|
-
*/
|
|
456
|
-
async disconnect(options = {}) {
|
|
457
|
-
this.#intentional_close = true;
|
|
458
|
-
this.#silent_close = options.silent === true;
|
|
459
|
-
// Cancela cualquier retry programado por un close anterior, para no resucitar
|
|
460
|
-
// el socket después de una desconexión manual.
|
|
461
|
-
if (this.#retry_timer) {
|
|
462
|
-
clearTimeout(this.#retry_timer);
|
|
463
|
-
this.#retry_timer = null;
|
|
464
|
-
}
|
|
465
|
-
if (this.#internals.socket) {
|
|
466
|
-
try {
|
|
467
|
-
// Pasa un error Boom-like con statusCode=connectionClosed (428) para que
|
|
468
|
-
// `lastDisconnect.error.output.statusCode` quede explícito en el close
|
|
469
|
-
// en lugar de `undefined`.
|
|
470
|
-
const intentional = Object.assign(new Error('intentional close'), {
|
|
471
|
-
output: { statusCode: DisconnectReason.connectionClosed },
|
|
222
|
+
socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
|
|
223
|
+
chain = chain.then(async () => {
|
|
224
|
+
await engine.set(`/lid/${lid}`, serialize(pn));
|
|
225
|
+
await engine.set(`/lid/${pn}`, serialize(lid));
|
|
226
|
+
}).catch(() => { });
|
|
472
227
|
});
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
await this.#handle_messages_upsert(messages);
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
socket.ev.on('contacts.upsert', (contacts) => {
|
|
499
|
-
this.#enqueue(() => this.#handle_contacts_upsert(contacts));
|
|
500
|
-
});
|
|
501
|
-
socket.ev.on('contacts.update', (contacts) => {
|
|
502
|
-
this.#enqueue(() => this.#handle_contacts_update(contacts));
|
|
503
|
-
});
|
|
504
|
-
socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
|
|
505
|
-
this.#enqueue(() => this.#handle_lid_mapping(lid, pn));
|
|
506
|
-
});
|
|
507
|
-
socket.ev.on('chats.upsert', (chats) => {
|
|
508
|
-
this.#enqueue(() => this.#handle_chats_upsert(chats));
|
|
509
|
-
});
|
|
510
|
-
socket.ev.on('chats.update', (chats) => {
|
|
511
|
-
this.#enqueue(() => this.#handle_chats_update(chats));
|
|
512
|
-
});
|
|
513
|
-
socket.ev.on('chats.delete', (ids) => {
|
|
514
|
-
this.#enqueue(() => this.#handle_chats_delete(ids));
|
|
515
|
-
});
|
|
516
|
-
socket.ev.on('messages.upsert', ({ messages }) => {
|
|
517
|
-
this.#enqueue(() => this.#handle_messages_upsert(messages));
|
|
518
|
-
});
|
|
519
|
-
socket.ev.on('messages.update', (updates) => {
|
|
520
|
-
this.#enqueue(() => this.#handle_messages_update(updates));
|
|
521
|
-
});
|
|
522
|
-
socket.ev.on('message-receipt.update', (updates) => {
|
|
523
|
-
this.#enqueue(() => this.#handle_message_receipt(updates));
|
|
524
|
-
});
|
|
525
|
-
// Las reacciones llegan duplicadas por `messages.reaction` Y `messages.upsert`
|
|
526
|
-
// (como `reactionMessage`). Se usa solo `messages.upsert` para evitar el doble disparo.
|
|
527
|
-
// socket.ev.on('messages.reaction', (reactions) => {
|
|
528
|
-
// void this.#handle_messages_reaction(reactions);
|
|
529
|
-
// });
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Persiste un contacto y su índice LID; emite `contact:created` solo si es nuevo.
|
|
533
|
-
* Persists a contact and its LID index; emits `contact:created` only when new.
|
|
534
|
-
*
|
|
535
|
-
* @param raw - Documento del contacto a persistir / Contact document to persist
|
|
536
|
-
* @internal
|
|
537
|
-
*/
|
|
538
|
-
async #persist_contact(raw) {
|
|
539
|
-
const current = deserialize(await this.engine.get(`/contact/${raw.id}`));
|
|
540
|
-
// Los upserts del re-sync llegan con los campos vacíos: sin fusionar borran el nombre
|
|
541
|
-
// que ya se conocía y el chat pasa a mostrar el número pelado.
|
|
542
|
-
// Re-sync upserts arrive with empty fields: without merging they wipe the name already
|
|
543
|
-
// known and the chat falls back to showing the bare number.
|
|
544
|
-
const doc = current
|
|
545
|
-
? {
|
|
546
|
-
id: raw.id,
|
|
547
|
-
lid: raw.lid ?? current.lid,
|
|
548
|
-
name: raw.name ?? current.name,
|
|
549
|
-
notify: raw.notify ?? current.notify,
|
|
550
|
-
verified_name: raw.verified_name ?? current.verified_name,
|
|
551
|
-
img_url: raw.img_url ?? current.img_url,
|
|
552
|
-
status: raw.status ?? current.status,
|
|
553
|
-
}
|
|
554
|
-
: raw;
|
|
555
|
-
await this.engine.set(`/contact/${raw.id}`, serialize(doc));
|
|
556
|
-
if (doc.lid) {
|
|
557
|
-
await this.engine.set(`/lid/${doc.lid}`, serialize(doc.id));
|
|
558
|
-
}
|
|
559
|
-
// Una ficha que existía vacía y ahora tiene nombre es un cambio que el consumidor
|
|
560
|
-
// necesita: sin avisar, quien memorice el contacto sigue mostrando el número.
|
|
561
|
-
// A card that existed empty and now has a name is a change the consumer needs: without
|
|
562
|
-
// notifying, whoever memoized the contact keeps showing the bare number.
|
|
563
|
-
const changed = current && ['lid', 'name', 'notify', 'verified_name', 'img_url', 'status'].some((key) => current[key] !== doc[key]);
|
|
564
|
-
if (!current || changed) {
|
|
565
|
-
const person = new this.Contact(doc);
|
|
566
|
-
const cached_chat = deserialize(await this.engine.get(`/chat/${doc.id}`));
|
|
567
|
-
const chat = new this.Chat(cached_chat ?? { id: doc.id, name: person.name });
|
|
568
|
-
this.emit(current ? 'contact:updated' : 'contact:created', person, chat, this);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
/** @internal */
|
|
572
|
-
async #handle_contacts_upsert(contacts) {
|
|
573
|
-
for (const c of contacts) {
|
|
574
|
-
if (c.id) {
|
|
575
|
-
await this.#persist_contact({
|
|
576
|
-
id: c.id,
|
|
577
|
-
lid: c.lid ?? null,
|
|
578
|
-
name: c.name ?? null,
|
|
579
|
-
notify: c.notify ?? null,
|
|
580
|
-
verified_name: c.verifiedName ?? null,
|
|
581
|
-
img_url: c.imgUrl ?? null,
|
|
582
|
-
status: c.status ?? null,
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
/** @internal */
|
|
588
|
-
async #handle_contacts_update(contacts) {
|
|
589
|
-
for (const c of contacts) {
|
|
590
|
-
if (c.id) {
|
|
591
|
-
const current = deserialize(await this.engine.get(`/contact/${c.id}`));
|
|
592
|
-
if (current) {
|
|
593
|
-
const patch = {
|
|
594
|
-
...(c.notify && { notify: c.notify }),
|
|
595
|
-
...(c.name && { name: c.name }),
|
|
596
|
-
...(c.verifiedName && { verified_name: c.verifiedName }),
|
|
597
|
-
...(c.imgUrl && { img_url: c.imgUrl }),
|
|
598
|
-
...(c.status && { status: c.status }),
|
|
599
|
-
...(c.lid && { lid: c.lid }),
|
|
600
|
-
};
|
|
601
|
-
if (Object.keys(patch).length > 0) {
|
|
602
|
-
const merged = { ...current, ...patch };
|
|
603
|
-
await this.engine.set(`/contact/${c.id}`, serialize(merged));
|
|
604
|
-
if (patch.lid) {
|
|
605
|
-
await this.engine.set(`/lid/${patch.lid}`, serialize(c.id));
|
|
606
|
-
}
|
|
607
|
-
const person = new this.Contact(merged);
|
|
608
|
-
const cached_chat = deserialize(await this.engine.get(`/chat/${c.id}`));
|
|
609
|
-
this.emit('contact:updated', person, new this.Chat(cached_chat ?? { id: c.id, name: person.name }), this);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
/** @internal */
|
|
616
|
-
async #handle_lid_mapping(lid, pn) {
|
|
617
|
-
await this.engine.set(`/lid/${lid}`, serialize(pn));
|
|
618
|
-
await this.engine.set(`/lid/${pn}`, serialize(lid));
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Actividad del chat según su último mensaje persistido. Es el respaldo para los
|
|
622
|
-
* documentos que se guardaron antes de que el chat llevara su propia marca.
|
|
623
|
-
* Chat activity from its last persisted message. It is the fallback for documents stored
|
|
624
|
-
* before the chat carried its own stamp.
|
|
625
|
-
*
|
|
626
|
-
* @param cid - Identificador del chat / Chat identifier
|
|
627
|
-
* @returns Epoch ms del último mensaje, o 0 si el chat no tiene ninguno / Last message epoch ms, or 0 when the chat has none
|
|
628
|
-
* @internal
|
|
629
|
-
*/
|
|
630
|
-
async #activity(cid) {
|
|
631
|
-
const [raw] = await this.engine.list(`/chat/${cid}/message`, 0, 1);
|
|
632
|
-
return deserialize(raw ?? null)?.created_at ?? 0;
|
|
633
|
-
}
|
|
634
|
-
/** @internal */
|
|
635
|
-
async #handle_chats_upsert(chats) {
|
|
636
|
-
for (const ch of chats) {
|
|
637
|
-
if (ch.id) {
|
|
638
|
-
const current = deserialize(await this.engine.get(`/chat/${ch.id}`));
|
|
639
|
-
const raw = current ?? {
|
|
640
|
-
id: ch.id,
|
|
641
|
-
name: ch.name ?? null,
|
|
642
|
-
archived: ch.archived ?? null,
|
|
643
|
-
pinned: ch.pinned ?? null,
|
|
644
|
-
mute_end_time: ch.muteEndTime != null ? Number(ch.muteEndTime) : null,
|
|
645
|
-
unread_count: ch.unreadCount ?? null,
|
|
646
|
-
};
|
|
647
|
-
if (ch.name) {
|
|
648
|
-
raw.name = ch.name;
|
|
649
|
-
}
|
|
650
|
-
// El sync trae la última actividad del chat; sin ella la lista quedaría ordenada
|
|
651
|
-
// por el momento en que se escribió cada documento.
|
|
652
|
-
// The sync carries the chat's last activity; without it the list would be ordered
|
|
653
|
-
// by the moment each document happened to be written.
|
|
654
|
-
const stamp = ch.conversationTimestamp != null ? Number(ch.conversationTimestamp) * 1_000 : null;
|
|
655
|
-
raw.activity = Math.max(stamp ?? 0, raw.activity ?? 0, await this.#activity(ch.id)) || null;
|
|
656
|
-
await this.engine.set(`/chat/${ch.id}`, serialize(raw), raw.activity ?? 0);
|
|
657
|
-
if (current === null) {
|
|
658
|
-
this.emit('chat:created', new this.Chat(raw), this);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
/** @internal */
|
|
664
|
-
async #handle_chats_update(chats) {
|
|
665
|
-
for (const ch of chats) {
|
|
666
|
-
if (ch.id && ch.id !== 'status@broadcast') {
|
|
667
|
-
const current = deserialize(await this.engine.get(`/chat/${ch.id}`)) ?? {
|
|
668
|
-
id: ch.id,
|
|
669
|
-
name: ch.name ?? null,
|
|
670
|
-
};
|
|
671
|
-
const patch = {};
|
|
672
|
-
const pinned_changed = 'pinned' in ch;
|
|
673
|
-
const archived_changed = ch.archived !== undefined;
|
|
674
|
-
const mute_changed = 'muteEndTime' in ch;
|
|
675
|
-
if (ch.name) {
|
|
676
|
-
patch.name = ch.name;
|
|
677
|
-
}
|
|
678
|
-
if (pinned_changed) {
|
|
679
|
-
patch.pinned = ch.pinned ?? null;
|
|
680
|
-
}
|
|
681
|
-
if (archived_changed) {
|
|
682
|
-
patch.archived = ch.archived ?? false;
|
|
683
|
-
}
|
|
684
|
-
if (mute_changed) {
|
|
685
|
-
patch.mute_end_time = ch.muteEndTime != null ? Number(ch.muteEndTime) : null;
|
|
686
|
-
}
|
|
687
|
-
if (ch.unreadCount != null) {
|
|
688
|
-
patch.unread_count = ch.unreadCount;
|
|
689
|
-
}
|
|
690
|
-
if (Object.keys(patch).length > 0) {
|
|
691
|
-
const merged = { ...current, ...patch };
|
|
692
|
-
// Fijar, archivar o silenciar no es actividad: el chat conserva su posición.
|
|
693
|
-
// Pinning, archiving or muting is not activity: the chat keeps its position.
|
|
694
|
-
await this.engine.set(`/chat/${ch.id}`, serialize(merged), merged.activity ?? undefined);
|
|
695
|
-
if (pinned_changed) {
|
|
696
|
-
this.emit(ch.pinned != null ? 'chat:pinned' : 'chat:unpinned', new this.Chat(merged), this);
|
|
697
|
-
}
|
|
698
|
-
if (archived_changed) {
|
|
699
|
-
this.emit(ch.archived ? 'chat:archived' : 'chat:unarchived', new this.Chat(merged), this);
|
|
700
|
-
}
|
|
701
|
-
if (mute_changed) {
|
|
702
|
-
const is_muted = patch.mute_end_time != null && patch.mute_end_time > Date.now();
|
|
703
|
-
this.emit(is_muted ? 'chat:muted' : 'chat:unmuted', new this.Chat(merged), this);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
/** @internal */
|
|
710
|
-
async #handle_chats_delete(ids) {
|
|
711
|
-
for (const cid of ids) {
|
|
712
|
-
const raw = deserialize(await this.engine.get(`/chat/${cid}`)) ?? { id: cid };
|
|
713
|
-
await this.engine.unset(`/chat/${cid}`);
|
|
714
|
-
this.emit('chat:deleted', new this.Chat(raw), this);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
/** @internal */
|
|
718
|
-
async #handle_message_receipt(updates) {
|
|
719
|
-
for (const { key, receipt } of updates) {
|
|
720
|
-
// Receipt sobre status@broadcast → marca el feed como visto y emite feed:updated.
|
|
721
|
-
// Receipt on status@broadcast → marks feed viewed and emits feed:updated.
|
|
722
|
-
if (key.remoteJid === 'status@broadcast' && key.id) {
|
|
723
|
-
const feed_raw = deserialize(await this.engine.get(`/status/${key.id}`));
|
|
724
|
-
if (feed_raw && !feed_raw.viewed) {
|
|
725
|
-
feed_raw.viewed = true;
|
|
726
|
-
await this.engine.set(`/status/${key.id}`, serialize(feed_raw));
|
|
727
|
-
this.emit('feed:updated', new Feed(this, feed_raw), this);
|
|
728
|
-
}
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
if (key.remoteJid && key.id && (receipt.readTimestamp != null || receipt.playedTimestamp != null)) {
|
|
732
|
-
const found = await this.#locate(key.remoteJid, key.id);
|
|
733
|
-
if (found) {
|
|
734
|
-
const { path, doc } = found;
|
|
735
|
-
const next = receipt.playedTimestamp != null ? PLAYED : READ;
|
|
736
|
-
if (doc.status < next) {
|
|
737
|
-
doc.status = next;
|
|
738
|
-
doc.raw.status = next;
|
|
739
|
-
await this.engine.set(path, serialize(doc), doc.created_at);
|
|
740
|
-
}
|
|
741
|
-
const msg_instance = message(this, doc);
|
|
742
|
-
this.emit('message:seen', msg_instance, await msg_instance.chat(), this);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
/** @internal */
|
|
748
|
-
async #handle_messages_upsert(messages) {
|
|
749
|
-
for (const msg of messages) {
|
|
750
|
-
if (msg.key?.remoteJid && msg.key.id) {
|
|
751
|
-
const cid = msg.key.remoteJidAlt ?? msg.key.remoteJid;
|
|
752
|
-
const mid = msg.key.id;
|
|
753
|
-
const content_type = getContentType(msg.message ?? {});
|
|
754
|
-
if (content_type === 'reactionMessage') {
|
|
755
|
-
// Canal único para reacciones: se procesa aquí y se ignora `messages.reaction`.
|
|
756
|
-
// Single channel for reactions: handled here; `messages.reaction` is disabled.
|
|
757
|
-
const reaction = msg.message?.reactionMessage;
|
|
758
|
-
if (reaction?.key?.id && reaction.key.remoteJid) {
|
|
759
|
-
const target_cid = (await this.#resolve_jid(reaction.key.remoteJid)) ?? reaction.key.remoteJid;
|
|
760
|
-
await this.#handle_messages_reaction([{
|
|
761
|
-
key: {
|
|
762
|
-
remoteJid: target_cid,
|
|
763
|
-
id: reaction.key.id,
|
|
764
|
-
participant: msg.key.fromMe ? (this.#internals.socket?.user?.id ?? null) : (msg.key.participant ?? cid),
|
|
765
|
-
},
|
|
766
|
-
reaction: { text: reaction.text ?? '' },
|
|
767
|
-
}]);
|
|
768
|
-
}
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
// Status broadcast — flujo dedicado. Nunca emite `message:*`.
|
|
772
|
-
// Status broadcast — dedicated flow. Never emits `message:*`.
|
|
773
|
-
if (msg.key.remoteJid === 'status@broadcast') {
|
|
774
|
-
if (content_type === 'protocolMessage') {
|
|
775
|
-
const protocol = msg.message?.protocolMessage;
|
|
776
|
-
if (protocol?.type === proto.Message.ProtocolMessage.Type.REVOKE &&
|
|
777
|
-
protocol.key?.id) {
|
|
778
|
-
const feed_raw = deserialize(await this.engine.get(`/status/${protocol.key.id}`));
|
|
779
|
-
if (feed_raw) {
|
|
780
|
-
await this.engine.unset(`/status/${protocol.key.id}`);
|
|
781
|
-
this.emit('feed:deleted', new Feed(this, feed_raw), this);
|
|
228
|
+
socket.ev.on('chats.upsert', (rows) => {
|
|
229
|
+
chain = chain.then(async () => {
|
|
230
|
+
for (const row of rows) {
|
|
231
|
+
if (row.id) {
|
|
232
|
+
const current = deserialize(await engine.get(`/chat/${row.id}`));
|
|
233
|
+
const doc = current ?? {
|
|
234
|
+
id: row.id,
|
|
235
|
+
name: row.name ?? null,
|
|
236
|
+
archived: row.archived ?? null,
|
|
237
|
+
pinned: row.pinned ?? null,
|
|
238
|
+
mute_end_time: row.muteEndTime != null ? Number(row.muteEndTime) : null,
|
|
239
|
+
unread_count: row.unreadCount ?? null,
|
|
240
|
+
};
|
|
241
|
+
if (row.name) {
|
|
242
|
+
doc.name = row.name;
|
|
243
|
+
}
|
|
244
|
+
const [newest] = await engine.list(`/chat/${row.id}/message`, 0, 1);
|
|
245
|
+
doc.activity = Math.max(row.conversationTimestamp != null ? Number(row.conversationTimestamp) * 1_000 : 0, doc.activity ?? 0, deserialize(newest ?? null)?.created_at ?? 0) || null;
|
|
246
|
+
await engine.set(`/chat/${row.id}`, serialize(doc), doc.activity ?? 0);
|
|
247
|
+
if (!current) {
|
|
248
|
+
this.emit('chat:created', new this.Chat(doc), this);
|
|
249
|
+
}
|
|
782
250
|
}
|
|
783
251
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
caption,
|
|
819
|
-
mime,
|
|
820
|
-
created_at,
|
|
821
|
-
expires_at: created_at + FEED_TTL_MS,
|
|
822
|
-
viewed: false,
|
|
823
|
-
raw: msg,
|
|
824
|
-
};
|
|
825
|
-
let content_buf = Buffer.alloc(0);
|
|
826
|
-
if (feed_type === 'text') {
|
|
827
|
-
content_buf = Buffer.from(caption, 'utf-8');
|
|
828
|
-
}
|
|
829
|
-
else if (this.#internals.socket) {
|
|
830
|
-
try {
|
|
831
|
-
const buf = await downloadMediaMessage(msg, 'buffer', {});
|
|
832
|
-
if (Buffer.isBuffer(buf)) {
|
|
833
|
-
content_buf = buf;
|
|
252
|
+
}).catch(() => { });
|
|
253
|
+
});
|
|
254
|
+
socket.ev.on('chats.update', (rows) => {
|
|
255
|
+
chain = chain.then(async () => {
|
|
256
|
+
for (const row of rows) {
|
|
257
|
+
if (row.id && row.id !== 'status@broadcast') {
|
|
258
|
+
const current = deserialize(await engine.get(`/chat/${row.id}`)) ?? { id: row.id, name: row.name ?? null };
|
|
259
|
+
const patch = {};
|
|
260
|
+
const events = [];
|
|
261
|
+
if (row.name) {
|
|
262
|
+
patch.name = row.name;
|
|
263
|
+
}
|
|
264
|
+
if (row.unreadCount != null) {
|
|
265
|
+
patch.unread_count = row.unreadCount;
|
|
266
|
+
}
|
|
267
|
+
if ('pinned' in row) {
|
|
268
|
+
patch.pinned = row.pinned ?? null;
|
|
269
|
+
events.push(row.pinned != null ? 'chat:pinned' : 'chat:unpinned');
|
|
270
|
+
}
|
|
271
|
+
if (row.archived !== undefined) {
|
|
272
|
+
patch.archived = row.archived ?? false;
|
|
273
|
+
events.push(row.archived ? 'chat:archived' : 'chat:unarchived');
|
|
274
|
+
}
|
|
275
|
+
if ('muteEndTime' in row) {
|
|
276
|
+
patch.mute_end_time = row.muteEndTime != null ? Number(row.muteEndTime) : null;
|
|
277
|
+
events.push(patch.mute_end_time != null && patch.mute_end_time > Date.now() ? 'chat:muted' : 'chat:unmuted');
|
|
278
|
+
}
|
|
279
|
+
if (Object.keys(patch).length > 0) {
|
|
280
|
+
const doc = { ...current, ...patch };
|
|
281
|
+
await engine.set(`/chat/${row.id}`, serialize(doc), doc.activity ?? undefined);
|
|
282
|
+
for (const event of events) {
|
|
283
|
+
this.emit(event, new this.Chat(doc), this);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
834
286
|
}
|
|
835
287
|
}
|
|
836
|
-
|
|
837
|
-
|
|
288
|
+
}).catch(() => { });
|
|
289
|
+
});
|
|
290
|
+
socket.ev.on('chats.delete', (ids) => {
|
|
291
|
+
chain = chain.then(async () => {
|
|
292
|
+
for (const cid of ids) {
|
|
293
|
+
const doc = deserialize(await engine.get(`/chat/${cid}`)) ?? { id: cid };
|
|
294
|
+
await engine.unset(`/chat/${cid}`);
|
|
295
|
+
this.emit('chat:deleted', new this.Chat(doc), this);
|
|
838
296
|
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
297
|
+
}).catch(() => { });
|
|
298
|
+
});
|
|
299
|
+
socket.ev.on('messages.upsert', ({ messages }) => {
|
|
300
|
+
chain = chain.then(async () => {
|
|
301
|
+
for (const msg of messages) {
|
|
302
|
+
const cid = msg.key?.remoteJidAlt ?? msg.key?.remoteJid;
|
|
303
|
+
const mid = msg.key?.id;
|
|
304
|
+
if (!cid || !mid) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const kind = getContentType(msg.message ?? {});
|
|
308
|
+
if (kind === 'reactionMessage') {
|
|
309
|
+
const target = msg.message?.reactionMessage;
|
|
310
|
+
const found = target?.key?.id && target.key.remoteJid ? await locate(target.key.remoteJid, target.key.id) : null;
|
|
311
|
+
if (found && target) {
|
|
312
|
+
const author = jidNormalizedUser((msg.key.fromMe ? socket.user?.id : msg.key.participant ?? cid) ?? cid);
|
|
313
|
+
const emoji = target.text ?? '';
|
|
314
|
+
found.doc.reactions = [
|
|
315
|
+
...(found.doc.reactions ?? []).filter((entry) => entry.author !== author),
|
|
316
|
+
...(emoji ? [{ author, emoji, at: Date.now() }] : []),
|
|
317
|
+
];
|
|
318
|
+
await engine.set(found.path, serialize(found.doc), found.doc.created_at);
|
|
319
|
+
const instance = new Message(init, found.doc);
|
|
320
|
+
this.emit('message:reacted', instance, await instance.chat(), emoji, this);
|
|
321
|
+
}
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (msg.key.remoteJid === 'status@broadcast') {
|
|
325
|
+
const revoked = kind === 'protocolMessage' && msg.message?.protocolMessage?.type === proto.Message.ProtocolMessage.Type.REVOKE
|
|
326
|
+
? msg.message.protocolMessage.key?.id
|
|
327
|
+
: null;
|
|
328
|
+
if (revoked) {
|
|
329
|
+
const gone = deserialize(await engine.get(`/status/${revoked}`));
|
|
330
|
+
if (gone) {
|
|
331
|
+
await engine.unset(`/status/${revoked}`);
|
|
332
|
+
this.emit('feed:deleted', new Feed(init, gone), this);
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const type = { conversation: 'text', extendedTextMessage: 'text', imageMessage: 'image', videoMessage: 'video', audioMessage: 'audio' }[kind ?? ''];
|
|
337
|
+
const author = msg.key.participant ?? '';
|
|
338
|
+
if (!type || !author) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const body = msg.message?.[kind];
|
|
342
|
+
const caption = typeof body === 'string' ? body : (body?.caption ?? body?.text ?? '');
|
|
343
|
+
const created_at = (Number(msg.messageTimestamp) || Math.floor(Date.now() / 1_000)) * 1_000;
|
|
344
|
+
const doc = {
|
|
345
|
+
id: mid,
|
|
346
|
+
author_jid: author,
|
|
347
|
+
type,
|
|
348
|
+
caption,
|
|
349
|
+
mime: type === 'text' ? 'text/plain' : ((typeof body === 'object' && body?.mimetype) || 'application/octet-stream'),
|
|
350
|
+
created_at,
|
|
351
|
+
expires_at: created_at + FEED_TTL_MS,
|
|
352
|
+
viewed: false,
|
|
353
|
+
raw: msg,
|
|
354
|
+
};
|
|
355
|
+
const binary = type === 'text'
|
|
356
|
+
? Buffer.from(caption, 'utf-8')
|
|
357
|
+
: await downloadMediaMessage(msg, 'buffer', {}).catch(() => Buffer.alloc(0));
|
|
358
|
+
await engine.set(`/status/${mid}`, serialize(doc));
|
|
359
|
+
if (binary.length > 0) {
|
|
360
|
+
await (engine.set_buffer?.(`/status/${mid}/content`, binary) ?? engine.set(`/status/${mid}/content`, serialize({ data: binary.toString('base64') })));
|
|
361
|
+
}
|
|
362
|
+
this.emit('feed:created', new Feed(init, doc), this);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (kind === 'pollUpdateMessage') {
|
|
366
|
+
const key = msg.message?.pollUpdateMessage?.pollCreationMessageKey;
|
|
367
|
+
const vote = msg.message?.pollUpdateMessage?.vote;
|
|
368
|
+
const found = key?.id && key.remoteJid ? await locate(key.remoteJid, key.id) : null;
|
|
369
|
+
const raw_secret = found?.doc.raw.message?.messageContextInfo?.messageSecret;
|
|
370
|
+
const secret = typeof raw_secret === 'string' ? Buffer.from(raw_secret, 'base64') : raw_secret;
|
|
371
|
+
if (found && secret && vote?.encPayload && vote.encIv) {
|
|
372
|
+
const mine = [socket.user?.lid, socket.user?.id];
|
|
373
|
+
const theirs = (from) => [from.remoteJid, from.participant, from.remoteJidAlt];
|
|
374
|
+
const voters = (msg.key.fromMe ? mine : theirs(msg.key)).filter((id) => Boolean(id));
|
|
375
|
+
const creators = (found.doc.raw.key?.fromMe ? mine : theirs(found.doc.raw.key ?? {})).filter((id) => Boolean(id));
|
|
376
|
+
for (const pair of voters.flatMap((who) => creators.map((creator) => [who, creator]))) {
|
|
881
377
|
try {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
378
|
+
updateMessageWithPollUpdate(found.doc.raw, {
|
|
379
|
+
pollUpdateMessageKey: msg.key,
|
|
380
|
+
vote: decryptPollVote({ encPayload: vote.encPayload, encIv: vote.encIv }, {
|
|
381
|
+
pollCreatorJid: jidNormalizedUser(pair[1]),
|
|
382
|
+
pollMsgId: found.doc.id,
|
|
383
|
+
pollEncKey: secret,
|
|
384
|
+
voterJid: jidNormalizedUser(pair[0]),
|
|
385
|
+
}),
|
|
386
|
+
senderTimestampMs: Number(msg.messageTimestamp) || Date.now(),
|
|
887
387
|
});
|
|
388
|
+
await engine.set(found.path, serialize(found.doc), found.doc.created_at);
|
|
389
|
+
const instance = new Message(init, found.doc);
|
|
390
|
+
this.emit('message:updated', instance, await instance.chat(), this);
|
|
888
391
|
break;
|
|
889
392
|
}
|
|
890
393
|
catch {
|
|
891
|
-
/* identidad equivocada
|
|
394
|
+
/* identidad equivocada / wrong identity */
|
|
892
395
|
}
|
|
893
396
|
}
|
|
894
|
-
if (decrypted) {
|
|
895
|
-
break;
|
|
896
|
-
}
|
|
897
397
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (kind === 'protocolMessage') {
|
|
401
|
+
const protocol = msg.message?.protocolMessage;
|
|
402
|
+
const found = protocol?.key?.id ? await locate(protocol.key.remoteJid ?? cid, protocol.key.id) : null;
|
|
403
|
+
if (found && protocol?.type === proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
|
|
404
|
+
found.doc.raw.message = protocol.editedMessage;
|
|
405
|
+
found.doc.edited = true;
|
|
406
|
+
found.doc.caption = new Message(init, found.doc.raw).caption;
|
|
407
|
+
await engine.set(found.path, serialize(found.doc), found.doc.created_at);
|
|
408
|
+
const instance = new Message(init, found.doc);
|
|
409
|
+
this.emit('message:updated', instance, await instance.chat(), this);
|
|
410
|
+
}
|
|
411
|
+
else if (found && protocol?.type === proto.Message.ProtocolMessage.Type.REVOKE) {
|
|
412
|
+
await engine.unset(found.path);
|
|
413
|
+
const instance = new Message(init, found.doc);
|
|
414
|
+
this.emit('message:deleted', instance, await instance.chat(), this);
|
|
907
415
|
}
|
|
416
|
+
continue;
|
|
908
417
|
}
|
|
909
|
-
|
|
910
|
-
|
|
418
|
+
const doc = new Message(init, msg)._raw;
|
|
419
|
+
const stored = deserialize(await engine.get(`/chat/${cid}/message/${mid}`));
|
|
420
|
+
if (stored) {
|
|
421
|
+
doc.multiple = typeof stored.multiple === 'boolean' ? stored.multiple : doc.multiple;
|
|
422
|
+
doc.reactions = stored.reactions ?? doc.reactions;
|
|
423
|
+
const advanced = doc.status > stored.status;
|
|
424
|
+
doc.status = Math.max(stored.status, doc.status);
|
|
425
|
+
if (!advanced && stored.caption === doc.caption && stored.edited === doc.edited && stored.starred === doc.starred) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
911
428
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
await
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
if (
|
|
935
|
-
const
|
|
936
|
-
|
|
429
|
+
if (!stored && !doc.me) {
|
|
430
|
+
const known = deserialize(await engine.get(`/contact/${doc.author}`));
|
|
431
|
+
if (doc.author && !(known?.name ?? known?.notify ?? known?.verified_name)) {
|
|
432
|
+
socket.ev.emit('contacts.upsert', [{
|
|
433
|
+
id: doc.author,
|
|
434
|
+
lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : undefined,
|
|
435
|
+
notify: msg.pushName ?? undefined,
|
|
436
|
+
verifiedName: msg.verifiedBizName ?? undefined,
|
|
437
|
+
}]);
|
|
438
|
+
}
|
|
439
|
+
if (!(await engine.get(`/chat/${cid}`))) {
|
|
440
|
+
const owner = { id: cid, name: cid.endsWith('@g.us') ? null : msg.pushName ?? null, activity: doc.created_at };
|
|
441
|
+
await engine.set(`/chat/${cid}`, serialize(owner), doc.created_at);
|
|
442
|
+
this.emit('chat:created', new this.Chat(owner), this);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
await engine.set(`/chat/${cid}/message/${mid}`, serialize(doc), doc.created_at);
|
|
446
|
+
const owner = deserialize(await engine.get(`/chat/${cid}`));
|
|
447
|
+
if (owner && doc.created_at > (owner.activity ?? 0)) {
|
|
448
|
+
owner.activity = doc.created_at;
|
|
449
|
+
await engine.set(`/chat/${cid}`, serialize(owner), doc.created_at);
|
|
450
|
+
}
|
|
451
|
+
if (!stored) {
|
|
452
|
+
const place = msg.message?.locationMessage ?? msg.message?.liveLocationMessage;
|
|
453
|
+
const poll = msg.message?.pollCreationMessage ?? msg.message?.pollCreationMessageV2 ?? msg.message?.pollCreationMessageV3;
|
|
454
|
+
const cards = msg.message?.contactsArrayMessage?.contacts ?? (msg.message?.contactMessage ? [msg.message.contactMessage] : []);
|
|
455
|
+
const body = doc.type === 'text' ? doc.caption
|
|
456
|
+
: doc.type === 'location' ? JSON.stringify({ lat: place?.degreesLatitude, lng: place?.degreesLongitude })
|
|
457
|
+
: doc.type === 'poll' ? JSON.stringify({ content: poll?.name ?? '', options: poll?.options?.map((option) => ({ content: option.optionName })) ?? [] })
|
|
458
|
+
: doc.type === 'vcard' ? cards.map((card) => card.vcard ?? '').join('\n')
|
|
459
|
+
: doc.type === 'event' ? JSON.stringify(msg.message?.eventMessage ?? {})
|
|
460
|
+
: null;
|
|
461
|
+
const binary = body !== null
|
|
462
|
+
? Buffer.from(body, 'utf-8')
|
|
463
|
+
: ['image', 'video', 'audio', 'document'].includes(doc.type)
|
|
464
|
+
? await downloadMediaMessage(msg, 'buffer', {}).catch(() => Buffer.alloc(0))
|
|
465
|
+
: Buffer.alloc(0);
|
|
466
|
+
if (binary.length > 0) {
|
|
467
|
+
await (engine.set_buffer?.(`/chat/${cid}/message/${mid}/content`, binary) ?? engine.set(`/chat/${cid}/message/${mid}/content`, serialize({ data: binary.toString('base64') })));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const instance = new Message(init, doc);
|
|
471
|
+
const owner_chat = await instance.chat();
|
|
472
|
+
this.emit('message:created', instance, owner_chat, this);
|
|
473
|
+
if (doc.forwarded) {
|
|
474
|
+
this.emit('message:forwarded', instance, owner_chat, this);
|
|
937
475
|
}
|
|
938
476
|
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
// all: the message carries the pushName and the verified business name, the only
|
|
980
|
-
// thing left when an earlier re-sync blanked the card.
|
|
981
|
-
if (doc.author) {
|
|
982
|
-
const known = deserialize(await this.engine.get(`/contact/${doc.author}`));
|
|
983
|
-
if (!known || !(known.name ?? known.notify ?? known.verified_name)) {
|
|
984
|
-
await this.#persist_contact({
|
|
985
|
-
id: doc.author,
|
|
986
|
-
lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : null,
|
|
987
|
-
name: null,
|
|
988
|
-
notify: push_name,
|
|
989
|
-
verified_name: msg.verifiedBizName ?? null,
|
|
990
|
-
img_url: null,
|
|
991
|
-
status: null,
|
|
992
|
-
});
|
|
477
|
+
}).catch(() => { });
|
|
478
|
+
});
|
|
479
|
+
socket.ev.on('messages.update', (updates) => {
|
|
480
|
+
chain = chain.then(async () => {
|
|
481
|
+
for (const { key, update } of updates) {
|
|
482
|
+
const found = key.remoteJid && key.id && key.remoteJid !== 'status@broadcast'
|
|
483
|
+
? await locate(key.remoteJid, key.id)
|
|
484
|
+
: null;
|
|
485
|
+
if (found) {
|
|
486
|
+
const { path, doc } = found;
|
|
487
|
+
const patch = update;
|
|
488
|
+
const raw = doc.raw ?? { key };
|
|
489
|
+
if (patch.message) {
|
|
490
|
+
const edited = patch.message.editedMessage?.message;
|
|
491
|
+
raw.message = edited ?? { ...raw.message, ...patch.message };
|
|
492
|
+
doc.edited = doc.edited || Boolean(edited);
|
|
493
|
+
doc.caption = new Message(init, raw).caption;
|
|
494
|
+
doc.raw = raw;
|
|
495
|
+
await engine.set(path, serialize(doc), doc.created_at);
|
|
496
|
+
const instance = new Message(init, doc);
|
|
497
|
+
this.emit('message:updated', instance, await instance.chat(), this);
|
|
498
|
+
}
|
|
499
|
+
else if (patch.starred !== undefined) {
|
|
500
|
+
doc.starred = patch.starred === true;
|
|
501
|
+
raw.starred = doc.starred;
|
|
502
|
+
doc.raw = raw;
|
|
503
|
+
await engine.set(path, serialize(doc), doc.created_at);
|
|
504
|
+
const instance = new Message(init, doc);
|
|
505
|
+
this.emit(doc.starred ? 'message:starred' : 'message:unstarred', instance, await instance.chat(), this);
|
|
506
|
+
}
|
|
507
|
+
else if (patch.status !== undefined && (patch.status > doc.status || patch.status === proto.WebMessageInfo.Status.ERROR)) {
|
|
508
|
+
raw.status = patch.status;
|
|
509
|
+
doc.status = patch.status;
|
|
510
|
+
raw.messageStubParameters = patch.messageStubParameters ?? raw.messageStubParameters;
|
|
511
|
+
doc.raw = raw;
|
|
512
|
+
await engine.set(path, serialize(doc), doc.created_at);
|
|
513
|
+
const instance = new Message(init, doc);
|
|
514
|
+
this.emit('message:updated', instance, await instance.chat(), this);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
993
517
|
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
content_buf = Buffer.from(doc.caption, 'utf-8');
|
|
1023
|
-
}
|
|
1024
|
-
else if (doc.type === 'location') {
|
|
1025
|
-
const loc = msg.message?.locationMessage ?? msg.message?.liveLocationMessage;
|
|
1026
|
-
content_buf = Buffer.from(JSON.stringify({ lat: loc?.degreesLatitude, lng: loc?.degreesLongitude }), 'utf-8');
|
|
1027
|
-
}
|
|
1028
|
-
else if (doc.type === 'poll') {
|
|
1029
|
-
const poll = msg.message?.pollCreationMessage ??
|
|
1030
|
-
msg.message?.pollCreationMessageV2 ??
|
|
1031
|
-
msg.message?.pollCreationMessageV3;
|
|
1032
|
-
content_buf = Buffer.from(JSON.stringify({
|
|
1033
|
-
content: poll?.name ?? '',
|
|
1034
|
-
options: poll?.options?.map((o) => ({ content: o.optionName })) ?? [],
|
|
1035
|
-
}), 'utf-8');
|
|
1036
|
-
}
|
|
1037
|
-
else if (doc.type === 'vcard') {
|
|
1038
|
-
const cards = msg.message?.contactsArrayMessage?.contacts ?? (msg.message?.contactMessage ? [msg.message.contactMessage] : []);
|
|
1039
|
-
content_buf = Buffer.from(cards.map((c) => c.vcard ?? '').join('\n'), 'utf-8');
|
|
1040
|
-
}
|
|
1041
|
-
else if (doc.type === 'event') {
|
|
1042
|
-
content_buf = Buffer.from(JSON.stringify(msg.message?.eventMessage ?? {}), 'utf-8');
|
|
1043
|
-
}
|
|
1044
|
-
else if (this.#internals.socket && ['image', 'video', 'audio', 'document'].includes(doc.type)) {
|
|
1045
|
-
try {
|
|
1046
|
-
const buffer = await downloadMediaMessage(msg, 'buffer', {});
|
|
1047
|
-
if (Buffer.isBuffer(buffer)) {
|
|
1048
|
-
content_buf = buffer;
|
|
518
|
+
}).catch(() => { });
|
|
519
|
+
});
|
|
520
|
+
socket.ev.on('message-receipt.update', (updates) => {
|
|
521
|
+
chain = chain.then(async () => {
|
|
522
|
+
for (const { key, receipt } of updates) {
|
|
523
|
+
if (key.remoteJid === 'status@broadcast' && key.id) {
|
|
524
|
+
const doc = deserialize(await engine.get(`/status/${key.id}`));
|
|
525
|
+
if (doc && !doc.viewed) {
|
|
526
|
+
doc.viewed = true;
|
|
527
|
+
await engine.set(`/status/${key.id}`, serialize(doc));
|
|
528
|
+
this.emit('feed:updated', new Feed(init, doc), this);
|
|
529
|
+
}
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const played = receipt.playedTimestamp != null;
|
|
533
|
+
const found = (played || receipt.readTimestamp != null) && key.remoteJid && key.id
|
|
534
|
+
? await locate(key.remoteJid, key.id)
|
|
535
|
+
: null;
|
|
536
|
+
if (found) {
|
|
537
|
+
const next = played ? proto.WebMessageInfo.Status.PLAYED : proto.WebMessageInfo.Status.READ;
|
|
538
|
+
if (found.doc.status < next) {
|
|
539
|
+
found.doc.status = next;
|
|
540
|
+
found.doc.raw.status = next;
|
|
541
|
+
await engine.set(found.path, serialize(found.doc), found.doc.created_at);
|
|
542
|
+
}
|
|
543
|
+
const instance = new Message(init, found.doc);
|
|
544
|
+
this.emit('message:seen', instance, await instance.chat(), this);
|
|
545
|
+
}
|
|
1049
546
|
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
547
|
+
}).catch(() => { });
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
this.#close = async (quiet) => {
|
|
551
|
+
intentional = true;
|
|
552
|
+
silent = quiet;
|
|
553
|
+
if (timer) {
|
|
554
|
+
clearTimeout(timer);
|
|
555
|
+
timer = null;
|
|
1054
556
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
557
|
+
try {
|
|
558
|
+
alive?.end(Object.assign(new Error('intentional close'), { output: { statusCode: DisconnectReason.connectionClosed } }));
|
|
1057
559
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
this.emit('message:created', instance, chat_instance, this);
|
|
1061
|
-
if (doc.forwarded) {
|
|
1062
|
-
this.emit('message:forwarded', instance, chat_instance, this);
|
|
560
|
+
catch {
|
|
561
|
+
/* el socket ya estaba cerrado / socket already closed */
|
|
1063
562
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
563
|
+
alive = null;
|
|
564
|
+
};
|
|
565
|
+
start().catch(reject);
|
|
566
|
+
});
|
|
1066
567
|
}
|
|
1067
568
|
/**
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
* vive bajo el JID con el que se guardó, así que se prueban ambas formas.
|
|
1071
|
-
* Locates a message document from the raw chat in the key. Updates and receipts arrive
|
|
1072
|
-
* LID-addressed —with or without device— while the document lives under the JID it was
|
|
1073
|
-
* stored with, so both forms are tried.
|
|
569
|
+
* Cierra la sesión: cancela el reintento pendiente y termina el socket.
|
|
570
|
+
* Closes the session: cancels the pending retry and ends the socket.
|
|
1074
571
|
*
|
|
1075
|
-
* @param
|
|
1076
|
-
* @param mid - Identificador del mensaje / Message identifier
|
|
1077
|
-
* @returns Ruta y documento, o null si no existe / Path and document, or null when missing
|
|
1078
|
-
* @internal
|
|
572
|
+
* @param options - `silent` calla el evento `disconnected`; `destroy` vacía el engine / `silent` mutes the `disconnected` event; `destroy` clears the engine
|
|
1079
573
|
*/
|
|
1080
|
-
async
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
tried.add(candidate);
|
|
1085
|
-
const path = `/chat/${candidate}/message/${mid}`;
|
|
1086
|
-
const doc = deserialize(await this.engine.get(path));
|
|
1087
|
-
if (doc) {
|
|
1088
|
-
return { path, doc };
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
return null;
|
|
1093
|
-
}
|
|
1094
|
-
/** @internal */
|
|
1095
|
-
async #handle_messages_update(updates) {
|
|
1096
|
-
for (const { key, update: upd } of updates) {
|
|
1097
|
-
if (key.remoteJid && key.id) {
|
|
1098
|
-
// Updates sobre `status@broadcast` se descartan: el feed sólo se
|
|
1099
|
-
// muta vía reacciones (`messages.reaction`), `Feed.view()` o REVOKE.
|
|
1100
|
-
// Updates on `status@broadcast` are discarded: feed mutates only via
|
|
1101
|
-
// reactions, `Feed.view()` or REVOKE.
|
|
1102
|
-
if (key.remoteJid === 'status@broadcast') {
|
|
1103
|
-
continue;
|
|
1104
|
-
}
|
|
1105
|
-
const found = await this.#locate(key.remoteJid, key.id);
|
|
1106
|
-
if (found) {
|
|
1107
|
-
const { path, doc } = found;
|
|
1108
|
-
const raw = doc.raw ?? { key };
|
|
1109
|
-
const upd_any = upd;
|
|
1110
|
-
const edited_message = upd_any.message?.editedMessage?.message;
|
|
1111
|
-
const content_update = upd_any.message;
|
|
1112
|
-
const status = upd_any.status;
|
|
1113
|
-
const starred_changed = upd_any.starred !== undefined;
|
|
1114
|
-
if (edited_message) {
|
|
1115
|
-
raw.message = edited_message;
|
|
1116
|
-
doc.raw = raw;
|
|
1117
|
-
doc.edited = true;
|
|
1118
|
-
doc.caption = message(this, raw).caption;
|
|
1119
|
-
await this.engine.set(path, serialize(doc), doc.created_at);
|
|
1120
|
-
const msg_instance = message(this, doc);
|
|
1121
|
-
this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
|
|
1122
|
-
}
|
|
1123
|
-
else if (content_update) {
|
|
1124
|
-
// Actualización de contenido (ej: live location). Mergea sobre el raw existente.
|
|
1125
|
-
raw.message = { ...raw.message, ...content_update };
|
|
1126
|
-
doc.raw = raw;
|
|
1127
|
-
doc.caption = message(this, raw).caption;
|
|
1128
|
-
await this.engine.set(path, serialize(doc), doc.created_at);
|
|
1129
|
-
const msg_instance = message(this, doc);
|
|
1130
|
-
this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
|
|
1131
|
-
}
|
|
1132
|
-
else if (starred_changed) {
|
|
1133
|
-
doc.starred = upd_any.starred === true;
|
|
1134
|
-
raw.starred = doc.starred;
|
|
1135
|
-
doc.raw = raw;
|
|
1136
|
-
await this.engine.set(path, serialize(doc), doc.created_at);
|
|
1137
|
-
const msg_instance = message(this, doc);
|
|
1138
|
-
this.emit(doc.starred ? 'message:starred' : 'message:unstarred', msg_instance, await msg_instance.chat(), this);
|
|
1139
|
-
// WhatsApp reemite los acks desordenados al reconectar (un `sent` después
|
|
1140
|
-
// de un `delivered`), así que el estado solo avanza; el rechazo (`error`)
|
|
1141
|
-
// es terminal y sí puede pisar lo que hubiera.
|
|
1142
|
-
// WhatsApp re-emits acks out of order on reconnect (a `sent` after a
|
|
1143
|
-
// `delivered`), so the state only moves forward; a rejection (`error`) is
|
|
1144
|
-
// terminal and may override whatever was there.
|
|
1145
|
-
}
|
|
1146
|
-
else if (status !== undefined && (status > doc.status || status === ERROR)) {
|
|
1147
|
-
raw.status = status;
|
|
1148
|
-
doc.status = status;
|
|
1149
|
-
// El rechazo del servidor viaja como stub del update; sin persistirlo el
|
|
1150
|
-
// mensaje queda en error sin decir por qué.
|
|
1151
|
-
// The server rejection travels as an update stub; without persisting it the
|
|
1152
|
-
// message stays in error without saying why.
|
|
1153
|
-
if (upd_any.messageStubParameters) {
|
|
1154
|
-
raw.messageStubParameters = upd_any.messageStubParameters;
|
|
1155
|
-
}
|
|
1156
|
-
doc.raw = raw;
|
|
1157
|
-
await this.engine.set(path, serialize(doc), doc.created_at);
|
|
1158
|
-
const msg_instance = message(this, doc);
|
|
1159
|
-
this.emit('message:updated', msg_instance, await msg_instance.chat(), this);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
/** @internal */
|
|
1166
|
-
async #handle_messages_reaction(reactions) {
|
|
1167
|
-
for (const { key, reaction } of reactions) {
|
|
1168
|
-
if (key.remoteJid && key.id) {
|
|
1169
|
-
// Reacciones sobre status@broadcast → feed:updated (no message:reacted).
|
|
1170
|
-
// Reactions on status@broadcast → feed:updated (not message:reacted).
|
|
1171
|
-
if (key.remoteJid === 'status@broadcast') {
|
|
1172
|
-
const feed_raw = deserialize(await this.engine.get(`/status/${key.id}`));
|
|
1173
|
-
if (feed_raw) {
|
|
1174
|
-
this.emit('feed:updated', new Feed(this, feed_raw), this);
|
|
1175
|
-
}
|
|
1176
|
-
continue;
|
|
1177
|
-
}
|
|
1178
|
-
const found = await this.#locate(key.remoteJid, key.id);
|
|
1179
|
-
if (found) {
|
|
1180
|
-
const { path, doc } = found;
|
|
1181
|
-
const reactor = jidNormalizedUser(key.participant ?? key.remoteJid);
|
|
1182
|
-
const emoji = reaction.text ?? '';
|
|
1183
|
-
doc.reactions = [
|
|
1184
|
-
...(doc.reactions ?? []).filter((r) => r.author !== reactor),
|
|
1185
|
-
...(emoji ? [{ author: reactor, emoji, at: Date.now() }] : []),
|
|
1186
|
-
];
|
|
1187
|
-
await this.engine.set(path, serialize(doc), doc.created_at);
|
|
1188
|
-
const msg_instance = message(this, doc);
|
|
1189
|
-
this.emit('message:reacted', msg_instance, await msg_instance.chat(), reaction.text ?? '', this);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
574
|
+
async disconnect(options = {}) {
|
|
575
|
+
await this.#close?.(options.silent === true);
|
|
576
|
+
if (options.destroy) {
|
|
577
|
+
await this.engine.clear();
|
|
1192
578
|
}
|
|
1193
579
|
}
|
|
1194
580
|
}
|
|
1195
|
-
export default WhatsApp;
|