@arcaelas/whatsapp 7.0.1 → 7.2.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/lib/message/index.d.ts +36 -2
- package/build/cjs/lib/message/index.js +75 -6
- package/build/cjs/lib/store/index.js +12 -3
- package/build/cjs/lib/whatsapp/index.d.ts +30 -3
- package/build/cjs/lib/whatsapp/index.js +238 -41
- package/build/esm/lib/message/index.d.ts +36 -2
- package/build/esm/lib/message/index.js +76 -7
- package/build/esm/lib/store/index.js +12 -3
- package/build/esm/lib/whatsapp/index.d.ts +30 -3
- package/build/esm/lib/whatsapp/index.js +238 -41
- package/package.json +1 -1
|
@@ -38,6 +38,15 @@ export function deserialize(raw) {
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Un mapeo que lleva a otro LID no traduce nada, y al ser truthy corta la cadena de respaldos
|
|
43
|
+
* de `jid_of` justo antes de llegar al mapping de baileys —el único que sí conoce el teléfono—.
|
|
44
|
+
* Descartarlo es lo que permite que la cadena siga buscando.
|
|
45
|
+
* A mapping leading to another LID translates nothing, and being truthy it cuts `jid_of`'s
|
|
46
|
+
* fallback chain right before reaching the baileys mapping —the only one that does know the
|
|
47
|
+
* phone—. Discarding it is what lets the chain keep looking.
|
|
48
|
+
*/
|
|
49
|
+
const phone_like = (value) => (value != null && !String(value).endsWith('@lid') ? String(value) : null);
|
|
41
50
|
/**
|
|
42
51
|
* JID canónico de un teléfono, JID o LID: los grupos y JIDs pasan tal cual, el LID se mapea
|
|
43
52
|
* contra los índices `/lid` del engine (o contra baileys cuando hay socket) y el resto se
|
|
@@ -56,9 +65,9 @@ export const jid_of = async (engine, uid, socket) => {
|
|
|
56
65
|
return uid;
|
|
57
66
|
const lid = uid.endsWith('@lid') ? jidNormalizedUser(uid) : '';
|
|
58
67
|
const mapped = lid
|
|
59
|
-
? deserialize(await engine.get(`/lid/${lid}`))
|
|
60
|
-
?? deserialize(await engine.get(`/lid/${lid.split('@')[0]}_reverse`))
|
|
61
|
-
?? await socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null)
|
|
68
|
+
? phone_like(deserialize(await engine.get(`/lid/${lid}`)))
|
|
69
|
+
?? phone_like(deserialize(await engine.get(`/lid/${lid.split('@')[0]}_reverse`)))
|
|
70
|
+
?? phone_like(await socket?.signalRepository?.lidMapping?.getPNForLID(lid).catch(() => null))
|
|
62
71
|
: uid.replace(/\D/g, '');
|
|
63
72
|
return mapped ? (String(mapped).includes('@') ? jidNormalizedUser(String(mapped)) : `${mapped}@s.whatsapp.net`) : null;
|
|
64
73
|
};
|
|
@@ -21,6 +21,15 @@ interface Options {
|
|
|
21
21
|
};
|
|
22
22
|
/** Descargar el historial de mensajes al vincular; contactos, credenciales, LID mappings y tctokens se sincronizan siempre. / Download the message history on link; contacts, credentials, LID mappings and tctokens always sync. */
|
|
23
23
|
sync?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Nombre con el que esta sesión aparece en «Dispositivos vinculados» del teléfono. Cuando
|
|
26
|
+
* una cuenta tiene varias sesiones, es lo ÚNICO que permite distinguirlas para cerrar la
|
|
27
|
+
* correcta: sin él todas se ven iguales y no hay forma de saber cuál sobra.
|
|
28
|
+
* Name this session shows under the phone's «Linked devices». When an account holds several
|
|
29
|
+
* sessions it is the ONLY thing telling them apart to close the right one: without it they
|
|
30
|
+
* all look alike and there is no way to know which one is spare.
|
|
31
|
+
*/
|
|
32
|
+
device?: string;
|
|
24
33
|
}
|
|
25
34
|
interface EventMap {
|
|
26
35
|
connected: [WhatsApp];
|
|
@@ -63,10 +72,28 @@ export default class WhatsApp {
|
|
|
63
72
|
off<E extends keyof EventMap>(event: E, handler: (...args: EventMap[E]) => void): this;
|
|
64
73
|
connect(callback: (auth: string | Buffer) => void | Promise<void>): Promise<void>;
|
|
65
74
|
/**
|
|
66
|
-
* Cierra la sesión:
|
|
67
|
-
*
|
|
75
|
+
* Cierra la sesión de verdad: desvincula el dispositivo del teléfono y termina el socket.
|
|
76
|
+
* La promesa no resuelve hasta que todo eso ocurrió.
|
|
77
|
+
*
|
|
78
|
+
* Los dos flags modulan efectos secundarios, nunca si la sesión muere: `silent` sólo calla
|
|
79
|
+
* el evento `disconnected` local, y `destroy` decide si el engine se vacía o conserva
|
|
80
|
+
* chats, mensajes y contactos para estudiarlos después. Las credenciales se borran en los
|
|
81
|
+
* dos casos: el dispositivo ya no existe, así que reconectar con ellas sólo devolvería un
|
|
82
|
+
* `loggedOut`.
|
|
83
|
+
*
|
|
84
|
+
* Closes the session for real: unlinks the device from the phone and ends the socket. The
|
|
85
|
+
* promise does not settle until all of that happened.
|
|
86
|
+
*
|
|
87
|
+
* Both flags modulate side effects, never whether the session dies: `silent` only mutes the
|
|
88
|
+
* local `disconnected` event, and `destroy` decides whether the engine is wiped or keeps
|
|
89
|
+
* chats, messages and contacts for later study. Credentials go in both cases: the device no
|
|
90
|
+
* longer exists, so reconnecting with them would only return a `loggedOut`.
|
|
91
|
+
*
|
|
92
|
+
* @param options - `silent` calla el evento; `destroy` vacía el engine entero / `silent` mutes the event; `destroy` wipes the whole engine
|
|
68
93
|
*
|
|
69
|
-
* @
|
|
94
|
+
* @example
|
|
95
|
+
* await wa.disconnect(); // desvincula y conserva el historial
|
|
96
|
+
* await wa.disconnect({ destroy: true }); // desvincula y no queda nada
|
|
70
97
|
*/
|
|
71
98
|
disconnect(options?: {
|
|
72
99
|
silent?: boolean;
|
|
@@ -7,10 +7,26 @@ import { Account, contact } from '../../lib/contact/index.js';
|
|
|
7
7
|
import Message, { message } from '../../lib/message/index.js';
|
|
8
8
|
import { Feed, TTL_MS as FEED_TTL_MS } from '../../lib/status/index.js';
|
|
9
9
|
import { deserialize, jid_of, serialize } from '../../lib/store/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* WhatsApp devuelve el nombre de la propia cuenta enmascarado —«+58∙∙∙∙∙∙∙∙40»— cuando el perfil
|
|
12
|
+
* no viajó completo. Eso no es un nombre: aceptarlo tapa al verdadero, que sí está guardado en
|
|
13
|
+
* la ficha del contacto propio.
|
|
14
|
+
* WhatsApp returns the own account name masked —«+58∙∙∙∙∙∙∙∙40»— when the profile did not travel
|
|
15
|
+
* whole. That is not a name: taking it hides the real one, which is stored on the own contact
|
|
16
|
+
* card.
|
|
17
|
+
*/
|
|
18
|
+
const readable = (value) => (value && !/^\+?[\d\s·•∙⋅]+$/.test(value) ? value : null);
|
|
10
19
|
export default class WhatsApp {
|
|
11
20
|
#event = new EventEmitter();
|
|
12
21
|
#options;
|
|
13
22
|
#close = null;
|
|
23
|
+
/**
|
|
24
|
+
* Cierre completo: desvincula del teléfono y termina el socket. Es distinto de `#close`,
|
|
25
|
+
* que sólo cuelga —lo que hace falta al reconectar, donde desvincular sería absurdo—.
|
|
26
|
+
* Full close: unlinks from the phone and ends the socket. Distinct from `#close`, which
|
|
27
|
+
* merely hangs up —what reconnecting needs, where unlinking would be absurd—.
|
|
28
|
+
*/
|
|
29
|
+
#unlink = null;
|
|
14
30
|
constructor(options) {
|
|
15
31
|
this.engine = options.engine;
|
|
16
32
|
this.#options = options;
|
|
@@ -32,7 +48,7 @@ export default class WhatsApp {
|
|
|
32
48
|
}
|
|
33
49
|
async connect(callback) {
|
|
34
50
|
const { engine } = this;
|
|
35
|
-
const { phone, method, autoclean = true, sync = true, reconnect = true } = this.#options;
|
|
51
|
+
const { phone, method, autoclean = true, sync = true, reconnect = true, device } = this.#options;
|
|
36
52
|
const digits = phone !== undefined ? String(phone).replace(/\D+/g, '') : '';
|
|
37
53
|
const budget = reconnect === false ? 0 : reconnect === true ? null : typeof reconnect === 'number' ? reconnect : reconnect.max ?? null;
|
|
38
54
|
const wait = typeof reconnect === 'object' ? (reconnect.interval ?? 60) * 1_000 : 60_000;
|
|
@@ -72,7 +88,7 @@ export default class WhatsApp {
|
|
|
72
88
|
},
|
|
73
89
|
},
|
|
74
90
|
},
|
|
75
|
-
browser: Browsers.
|
|
91
|
+
browser: Browsers.appropriate(device ?? 'Orchestrator'),
|
|
76
92
|
logger: pino({ level: 'silent' }),
|
|
77
93
|
syncFullHistory: sync,
|
|
78
94
|
shouldSyncHistoryMessage: ({ syncType }) => sync || syncType !== proto.HistorySync.HistorySyncType.FULL,
|
|
@@ -98,16 +114,29 @@ export default class WhatsApp {
|
|
|
98
114
|
if (!user)
|
|
99
115
|
return null;
|
|
100
116
|
const id = jidNormalizedUser(user.id);
|
|
117
|
+
// La cuenta propia también se guarda por LID cuando el teléfono se anuncia
|
|
118
|
+
// así, y entonces la ficha del JID viene vacía: se leen las dos y gana la
|
|
119
|
+
// que tenga el dato.
|
|
120
|
+
// The own account is stored by LID too when the phone announces itself that
|
|
121
|
+
// way, and then the JID card comes back empty: both are read and whichever
|
|
122
|
+
// holds the data wins.
|
|
101
123
|
const card = deserialize(await engine.get(`/contact/${id}`));
|
|
124
|
+
const alias = user.lid ? deserialize(await engine.get(`/contact/${jidNormalizedUser(user.lid)}`)) : null;
|
|
102
125
|
return new Account(init, {
|
|
103
126
|
id,
|
|
104
127
|
phone_number: id,
|
|
105
|
-
lid: user.lid ?? card?.lid ?? null,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
lid: user.lid ?? card?.lid ?? alias?.lid ?? null,
|
|
129
|
+
// `verified_name` es el nombre de una cuenta de empresa y `notify` el
|
|
130
|
+
// que la propia línea difunde en sus mensajes: cualquiera de los dos es
|
|
131
|
+
// el nombre real de la cuenta cuando el perfil no viajó en el login.
|
|
132
|
+
// `verified_name` is a business account's name and `notify` the one the
|
|
133
|
+
// line itself broadcasts in its messages: either is the account's real
|
|
134
|
+
// name when the profile did not travel in the login.
|
|
135
|
+
name: readable(user.name) ?? readable(card?.name) ?? readable(alias?.name) ?? card?.verified_name ?? alias?.verified_name ?? card?.notify ?? alias?.notify ?? null,
|
|
136
|
+
notify: card?.notify ?? alias?.notify ?? null,
|
|
137
|
+
verified_name: card?.verified_name ?? alias?.verified_name ?? null,
|
|
138
|
+
img_url: (await socket.profilePictureUrl(id, 'image').catch(() => null)) ?? card?.img_url ?? alias?.img_url ?? null,
|
|
139
|
+
status: card?.status ?? alias?.status ?? null,
|
|
111
140
|
});
|
|
112
141
|
};
|
|
113
142
|
const locate = async (cid, mid) => {
|
|
@@ -122,6 +151,89 @@ export default class WhatsApp {
|
|
|
122
151
|
}
|
|
123
152
|
return null;
|
|
124
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* Identidad con la que se guarda a alguien. El mismo contacto llega unas veces
|
|
156
|
+
* por teléfono y otras por LID, y tratar ambos como distintos le abre dos fichas
|
|
157
|
+
* y dos chats. El teléfono manda; el LID sólo se conserva cuando aún no hay
|
|
158
|
+
* forma de traducirlo.
|
|
159
|
+
* The identity someone is stored under. The same contact arrives sometimes by
|
|
160
|
+
* phone and sometimes by LID, and treating both as distinct opens two cards and
|
|
161
|
+
* two chats for them. The phone wins; the LID is only kept while there is still
|
|
162
|
+
* no way to translate it.
|
|
163
|
+
*/
|
|
164
|
+
const canonical = async (uid) => (uid.endsWith('@lid') ? await jid_of(engine, uid, socket).catch(() => null) : null) ?? uid;
|
|
165
|
+
/** Índice LID↔teléfono, sólo cuando traduce de verdad. / LID↔phone index, only when it actually translates. */
|
|
166
|
+
const remember = async (lid, jid) => {
|
|
167
|
+
if (lid && !jid.endsWith('@lid')) {
|
|
168
|
+
await engine.set(`/lid/${lid}`, serialize(jid));
|
|
169
|
+
await engine.set(`/lid/${jid}`, serialize(lid));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Vuelca sobre el teléfono lo que se había guardado bajo el LID —ficha, chat y
|
|
174
|
+
* mensajes— y borra el duplicado. Los campos ya presentes en el destino ganan:
|
|
175
|
+
* son los que la cuenta viene usando.
|
|
176
|
+
* Pours whatever was stored under the LID —card, chat and messages— onto the
|
|
177
|
+
* phone and drops the duplicate. Fields already present on the target win: those
|
|
178
|
+
* are the ones the account has been using.
|
|
179
|
+
*/
|
|
180
|
+
const absorb = async (lid, pn) => {
|
|
181
|
+
const [from, to] = [jidNormalizedUser(lid), jidNormalizedUser(pn)];
|
|
182
|
+
if (from !== to) {
|
|
183
|
+
const stale = deserialize(await engine.get(`/contact/${from}`));
|
|
184
|
+
if (stale) {
|
|
185
|
+
const target = deserialize(await engine.get(`/contact/${to}`));
|
|
186
|
+
await engine.set(`/contact/${to}`, serialize({ ...stale, ...target, id: to, lid: from }));
|
|
187
|
+
await engine.unset(`/contact/${from}`);
|
|
188
|
+
}
|
|
189
|
+
const orphan = deserialize(await engine.get(`/chat/${from}`));
|
|
190
|
+
if (orphan) {
|
|
191
|
+
const target = deserialize(await engine.get(`/chat/${to}`));
|
|
192
|
+
for (const raw of await engine.list(`/chat/${from}/message`, 0, 10_000)) {
|
|
193
|
+
const msg = deserialize(raw);
|
|
194
|
+
if (msg) {
|
|
195
|
+
await engine.set(`/chat/${to}/message/${msg.id}`, serialize({ ...msg, cid: to }), msg.created_at);
|
|
196
|
+
await engine.unset(`/chat/${from}/message/${msg.id}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const doc = { ...orphan, ...target, id: to, activity: Math.max(orphan.activity ?? 0, target?.activity ?? 0) || null };
|
|
200
|
+
await engine.set(`/chat/${to}`, serialize(doc), doc.activity ?? 0);
|
|
201
|
+
await engine.unset(`/chat/${from}`);
|
|
202
|
+
// Para quien escucha, el duplicado desaparece y el bueno aparece: es
|
|
203
|
+
// literalmente lo que pasó, y deja la lista sin la fila fantasma.
|
|
204
|
+
// To a listener the duplicate goes away and the good one shows up:
|
|
205
|
+
// that is literally what happened, and it leaves the list without
|
|
206
|
+
// the ghost row.
|
|
207
|
+
this.emit('chat:deleted', new this.Chat(orphan), this);
|
|
208
|
+
if (!target) {
|
|
209
|
+
this.emit('chat:created', new this.Chat(doc), this);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Pasa por todo lo guardado bajo un LID y lo une a su teléfono. Cubre lo que se
|
|
216
|
+
* escribió antes de que el mapeo existiera —o antes de que la librería supiera
|
|
217
|
+
* unirlo—, que es lo que deja la lista con el mismo contacto dos veces: una con
|
|
218
|
+
* su nombre y otra como un número largo sin sentido.
|
|
219
|
+
* Walks everything stored under a LID and joins it to its phone. It covers what
|
|
220
|
+
* was written before the mapping existed —or before the library knew how to join
|
|
221
|
+
* it—, which is what leaves the same contact twice in the list: once with a name
|
|
222
|
+
* and once as a long meaningless number.
|
|
223
|
+
*/
|
|
224
|
+
const reconcile = async () => {
|
|
225
|
+
for (const path of ['/contact', '/chat']) {
|
|
226
|
+
for (const raw of await engine.list(path, 0, 10_000)) {
|
|
227
|
+
const id = deserialize(raw)?.id;
|
|
228
|
+
if (id?.endsWith('@lid')) {
|
|
229
|
+
const jid = await jid_of(engine, id, socket).catch(() => null);
|
|
230
|
+
if (jid) {
|
|
231
|
+
await absorb(id, jid);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
125
237
|
socket.ev.on('creds.update', () => engine.set('/session/creds', serialize(creds)));
|
|
126
238
|
socket.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
|
|
127
239
|
if (qr && !creds.registered) {
|
|
@@ -132,6 +244,13 @@ export default class WhatsApp {
|
|
|
132
244
|
if (connection === 'open') {
|
|
133
245
|
connected = true;
|
|
134
246
|
retries = 0;
|
|
247
|
+
// Los mapeos que faltaban ya viajaron en el handshake: recién ahora se
|
|
248
|
+
// puede unir lo que quedó partido en sesiones anteriores, cuando esos
|
|
249
|
+
// LID todavía eran intraducibles.
|
|
250
|
+
// The missing mappings already travelled in the handshake: only now can
|
|
251
|
+
// whatever stayed split in earlier sessions be joined, back when those
|
|
252
|
+
// LIDs were still untranslatable.
|
|
253
|
+
chain = chain.then(reconcile).catch(() => { });
|
|
135
254
|
this.emit('connected', this);
|
|
136
255
|
resolve();
|
|
137
256
|
}
|
|
@@ -171,10 +290,11 @@ export default class WhatsApp {
|
|
|
171
290
|
chain = chain.then(async () => {
|
|
172
291
|
for (const row of rows) {
|
|
173
292
|
if (row.id) {
|
|
174
|
-
const
|
|
293
|
+
const id = await canonical(row.id);
|
|
294
|
+
const current = deserialize(await engine.get(`/contact/${id}`));
|
|
175
295
|
const doc = {
|
|
176
|
-
id
|
|
177
|
-
lid: row.lid ?? current?.lid ?? null,
|
|
296
|
+
id,
|
|
297
|
+
lid: row.lid ?? (row.id.endsWith('@lid') ? row.id : null) ?? current?.lid ?? null,
|
|
178
298
|
name: row.name ?? current?.name ?? null,
|
|
179
299
|
notify: row.notify ?? current?.notify ?? null,
|
|
180
300
|
verified_name: row.verifiedName ?? current?.verified_name ?? null,
|
|
@@ -182,13 +302,11 @@ export default class WhatsApp {
|
|
|
182
302
|
status: row.status ?? current?.status ?? null,
|
|
183
303
|
};
|
|
184
304
|
if (!current || JSON.stringify(current) !== JSON.stringify(doc)) {
|
|
185
|
-
await engine.set(`/contact/${
|
|
186
|
-
|
|
187
|
-
await engine.set(`/lid/${doc.lid}`, serialize(doc.id));
|
|
188
|
-
}
|
|
305
|
+
await engine.set(`/contact/${id}`, serialize(doc));
|
|
306
|
+
await remember(doc.lid, id);
|
|
189
307
|
const person = new this.Contact(doc);
|
|
190
|
-
const owner = deserialize(await engine.get(`/chat/${
|
|
191
|
-
this.emit(current ? 'contact:updated' : 'contact:created', person, new this.Chat(owner ?? { id
|
|
308
|
+
const owner = deserialize(await engine.get(`/chat/${id}`));
|
|
309
|
+
this.emit(current ? 'contact:updated' : 'contact:created', person, new this.Chat(owner ?? { id, name: person.name }), this);
|
|
192
310
|
}
|
|
193
311
|
}
|
|
194
312
|
}
|
|
@@ -197,38 +315,44 @@ export default class WhatsApp {
|
|
|
197
315
|
socket.ev.on('contacts.update', (rows) => {
|
|
198
316
|
chain = chain.then(async () => {
|
|
199
317
|
for (const row of rows) {
|
|
200
|
-
const
|
|
318
|
+
const id = row.id ? await canonical(row.id) : '';
|
|
319
|
+
const current = id ? deserialize(await engine.get(`/contact/${id}`)) : null;
|
|
201
320
|
const patch = {
|
|
202
321
|
...(row.notify && { notify: row.notify }),
|
|
203
322
|
...(row.name && { name: row.name }),
|
|
204
323
|
...(row.verifiedName && { verified_name: row.verifiedName }),
|
|
205
324
|
...(typeof row.imgUrl === 'string' && { img_url: row.imgUrl }),
|
|
206
325
|
...(row.status && { status: row.status }),
|
|
207
|
-
...(row.lid && { lid: row.lid }),
|
|
326
|
+
...((row.lid ?? (row.id?.endsWith('@lid') ? row.id : null)) && { lid: row.lid ?? row.id }),
|
|
208
327
|
};
|
|
209
|
-
if (current &&
|
|
328
|
+
if (current && Object.keys(patch).length > 0) {
|
|
210
329
|
const doc = { ...current, ...patch };
|
|
211
|
-
await engine.set(`/contact/${
|
|
212
|
-
|
|
213
|
-
await engine.set(`/lid/${patch.lid}`, serialize(row.id));
|
|
214
|
-
}
|
|
330
|
+
await engine.set(`/contact/${id}`, serialize(doc));
|
|
331
|
+
await remember(patch.lid, id);
|
|
215
332
|
const person = new this.Contact(doc);
|
|
216
|
-
const owner = deserialize(await engine.get(`/chat/${
|
|
217
|
-
this.emit('contact:updated', person, new this.Chat(owner ?? { id
|
|
333
|
+
const owner = deserialize(await engine.get(`/chat/${id}`));
|
|
334
|
+
this.emit('contact:updated', person, new this.Chat(owner ?? { id, name: person.name }), this);
|
|
218
335
|
}
|
|
219
336
|
}
|
|
220
337
|
}).catch(() => { });
|
|
221
338
|
});
|
|
222
339
|
socket.ev.on('lid-mapping.update', ({ lid, pn }) => {
|
|
223
340
|
chain = chain.then(async () => {
|
|
224
|
-
await
|
|
225
|
-
|
|
341
|
+
await remember(lid, pn);
|
|
342
|
+
// El mapeo recién llega: lo que se guardó bajo el LID mientras era
|
|
343
|
+
// irresoluble se une ahora a su teléfono, o el contacto queda partido
|
|
344
|
+
// en dos fichas y dos chats que nunca se vuelven a encontrar.
|
|
345
|
+
// The mapping just arrived: whatever was stored under the LID while it
|
|
346
|
+
// was unresolvable now joins its phone, or the contact stays split into
|
|
347
|
+
// two cards and two chats that never meet again.
|
|
348
|
+
await absorb(lid, pn);
|
|
226
349
|
}).catch(() => { });
|
|
227
350
|
});
|
|
228
351
|
socket.ev.on('chats.upsert', (rows) => {
|
|
229
352
|
chain = chain.then(async () => {
|
|
230
|
-
for (const
|
|
231
|
-
if (
|
|
353
|
+
for (const raw of rows) {
|
|
354
|
+
if (raw.id) {
|
|
355
|
+
const row = { ...raw, id: await canonical(raw.id) };
|
|
232
356
|
const current = deserialize(await engine.get(`/chat/${row.id}`));
|
|
233
357
|
const doc = current ?? {
|
|
234
358
|
id: row.id,
|
|
@@ -399,7 +523,13 @@ export default class WhatsApp {
|
|
|
399
523
|
}
|
|
400
524
|
if (kind === 'protocolMessage') {
|
|
401
525
|
const protocol = msg.message?.protocolMessage;
|
|
402
|
-
|
|
526
|
+
// El aviso puede venir direccionado por LID y el documento estar bajo el JID
|
|
527
|
+
// (o al revés): se busca por el chat que nombra el protocolo y por el del sobre.
|
|
528
|
+
// The notice may be LID-addressed while the document lives under the JID (or the
|
|
529
|
+
// other way around): it is looked up by the protocol's chat and by the envelope's.
|
|
530
|
+
const found = protocol?.key?.id
|
|
531
|
+
? (await locate(protocol.key.remoteJid ?? cid, protocol.key.id)) ?? (await locate(cid, protocol.key.id))
|
|
532
|
+
: null;
|
|
403
533
|
if (found && protocol?.type === proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
|
|
404
534
|
found.doc.raw.message = protocol.editedMessage;
|
|
405
535
|
found.doc.edited = true;
|
|
@@ -409,7 +539,12 @@ export default class WhatsApp {
|
|
|
409
539
|
this.emit('message:updated', instance, await instance.chat(), this);
|
|
410
540
|
}
|
|
411
541
|
else if (found && protocol?.type === proto.Message.ProtocolMessage.Type.REVOKE) {
|
|
412
|
-
|
|
542
|
+
// El mensaje retirado no se borra: se marca, y así la interfaz puede
|
|
543
|
+
// mostrar «se eliminó este mensaje» donde estaba en vez de un hueco.
|
|
544
|
+
// A revoked message is not removed: it gets flagged, so the interface can
|
|
545
|
+
// show "this message was deleted" in its place instead of a gap.
|
|
546
|
+
found.doc.revoked_at = Date.now();
|
|
547
|
+
await engine.set(found.path, serialize(found.doc), found.doc.created_at);
|
|
413
548
|
const instance = new Message(init, found.doc);
|
|
414
549
|
this.emit('message:deleted', instance, await instance.chat(), this);
|
|
415
550
|
}
|
|
@@ -420,24 +555,40 @@ export default class WhatsApp {
|
|
|
420
555
|
if (stored) {
|
|
421
556
|
doc.multiple = typeof stored.multiple === 'boolean' ? stored.multiple : doc.multiple;
|
|
422
557
|
doc.reactions = stored.reactions ?? doc.reactions;
|
|
558
|
+
doc.revoked_at = stored.revoked_at ?? doc.revoked_at;
|
|
423
559
|
const advanced = doc.status > stored.status;
|
|
424
560
|
doc.status = Math.max(stored.status, doc.status);
|
|
425
561
|
if (!advanced && stored.caption === doc.caption && stored.edited === doc.edited && stored.starred === doc.starred) {
|
|
426
562
|
continue;
|
|
427
563
|
}
|
|
428
564
|
}
|
|
565
|
+
if (!stored && doc.me && msg.pushName && socket.user) {
|
|
566
|
+
// Un mensaje propio lleva el nombre con el que la cuenta se
|
|
567
|
+
// anuncia al mundo. Cuando el perfil no viajó en el login —una
|
|
568
|
+
// reconexión, por ejemplo— esta es la única vía para conocerlo,
|
|
569
|
+
// y sin ella la línea se ve a sí misma como un número.
|
|
570
|
+
// An own message carries the name the account announces itself
|
|
571
|
+
// with. When the profile did not travel in the login —a
|
|
572
|
+
// reconnection, say— this is the only way to learn it, and
|
|
573
|
+
// without it the line sees itself as a number.
|
|
574
|
+
const own = jidNormalizedUser(socket.user.id);
|
|
575
|
+
const known = deserialize(await engine.get(`/contact/${own}`));
|
|
576
|
+
if (!(known?.name ?? known?.notify ?? known?.verified_name)) {
|
|
577
|
+
socket.ev.emit('contacts.upsert', [{ id: own, lid: socket.user.lid, notify: readable(msg.pushName) ?? undefined }]);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
429
580
|
if (!stored && !doc.me) {
|
|
430
581
|
const known = deserialize(await engine.get(`/contact/${doc.author}`));
|
|
431
582
|
if (doc.author && !(known?.name ?? known?.notify ?? known?.verified_name)) {
|
|
432
583
|
socket.ev.emit('contacts.upsert', [{
|
|
433
584
|
id: doc.author,
|
|
434
585
|
lid: msg.key.remoteJid?.endsWith('@lid') ? msg.key.remoteJid : undefined,
|
|
435
|
-
notify: msg.pushName ?? undefined,
|
|
586
|
+
notify: readable(msg.pushName) ?? undefined,
|
|
436
587
|
verifiedName: msg.verifiedBizName ?? undefined,
|
|
437
588
|
}]);
|
|
438
589
|
}
|
|
439
590
|
if (!(await engine.get(`/chat/${cid}`))) {
|
|
440
|
-
const owner = { id: cid, name: cid.endsWith('@g.us') ? null : msg.pushName
|
|
591
|
+
const owner = { id: cid, name: cid.endsWith('@g.us') ? null : readable(msg.pushName), activity: doc.created_at };
|
|
441
592
|
await engine.set(`/chat/${cid}`, serialize(owner), doc.created_at);
|
|
442
593
|
this.emit('chat:created', new this.Chat(owner), this);
|
|
443
594
|
}
|
|
@@ -547,6 +698,34 @@ export default class WhatsApp {
|
|
|
547
698
|
}).catch(() => { });
|
|
548
699
|
});
|
|
549
700
|
};
|
|
701
|
+
this.#unlink = async (quiet) => {
|
|
702
|
+
intentional = true;
|
|
703
|
+
silent = quiet;
|
|
704
|
+
if (timer) {
|
|
705
|
+
clearTimeout(timer);
|
|
706
|
+
timer = null;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
// `logout` avisa al teléfono y termina el socket; la promesa no resuelve
|
|
710
|
+
// hasta que el teléfono acusa, que es cuando el dispositivo ya no existe.
|
|
711
|
+
// `logout` notifies the phone and ends the socket; the promise does not
|
|
712
|
+
// settle until the phone acknowledges, which is when the device is gone.
|
|
713
|
+
await alive?.logout();
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
// Sin red o con el socket ya muerto no hay a quién avisar: se cierra de
|
|
717
|
+
// este lado para no dejar el proceso colgado de una sesión que no existe.
|
|
718
|
+
// With no network or an already dead socket there is nobody to notify: it
|
|
719
|
+
// closes on this side so the process is not left hanging on a dead session.
|
|
720
|
+
try {
|
|
721
|
+
alive?.end(Object.assign(new Error('intentional close'), { output: { statusCode: DisconnectReason.connectionClosed } }));
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
/* el socket ya estaba cerrado / socket already closed */
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
alive = null;
|
|
728
|
+
};
|
|
550
729
|
this.#close = async (quiet) => {
|
|
551
730
|
intentional = true;
|
|
552
731
|
silent = quiet;
|
|
@@ -566,15 +745,33 @@ export default class WhatsApp {
|
|
|
566
745
|
});
|
|
567
746
|
}
|
|
568
747
|
/**
|
|
569
|
-
* Cierra la sesión:
|
|
570
|
-
*
|
|
748
|
+
* Cierra la sesión de verdad: desvincula el dispositivo del teléfono y termina el socket.
|
|
749
|
+
* La promesa no resuelve hasta que todo eso ocurrió.
|
|
750
|
+
*
|
|
751
|
+
* Los dos flags modulan efectos secundarios, nunca si la sesión muere: `silent` sólo calla
|
|
752
|
+
* el evento `disconnected` local, y `destroy` decide si el engine se vacía o conserva
|
|
753
|
+
* chats, mensajes y contactos para estudiarlos después. Las credenciales se borran en los
|
|
754
|
+
* dos casos: el dispositivo ya no existe, así que reconectar con ellas sólo devolvería un
|
|
755
|
+
* `loggedOut`.
|
|
756
|
+
*
|
|
757
|
+
* Closes the session for real: unlinks the device from the phone and ends the socket. The
|
|
758
|
+
* promise does not settle until all of that happened.
|
|
759
|
+
*
|
|
760
|
+
* Both flags modulate side effects, never whether the session dies: `silent` only mutes the
|
|
761
|
+
* local `disconnected` event, and `destroy` decides whether the engine is wiped or keeps
|
|
762
|
+
* chats, messages and contacts for later study. Credentials go in both cases: the device no
|
|
763
|
+
* longer exists, so reconnecting with them would only return a `loggedOut`.
|
|
764
|
+
*
|
|
765
|
+
* @param options - `silent` calla el evento; `destroy` vacía el engine entero / `silent` mutes the event; `destroy` wipes the whole engine
|
|
571
766
|
*
|
|
572
|
-
* @
|
|
767
|
+
* @example
|
|
768
|
+
* await wa.disconnect(); // desvincula y conserva el historial
|
|
769
|
+
* await wa.disconnect({ destroy: true }); // desvincula y no queda nada
|
|
573
770
|
*/
|
|
574
771
|
async disconnect(options = {}) {
|
|
575
|
-
await this.#
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
772
|
+
await this.#unlink?.(options.silent === true);
|
|
773
|
+
this.#unlink = null;
|
|
774
|
+
this.#close = null;
|
|
775
|
+
await (options.destroy ? this.engine.clear() : this.engine.unset('/session'));
|
|
579
776
|
}
|
|
580
777
|
}
|