@usebridger/transport-baileys 0.0.0-stage → 0.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/LICENSE +202 -0
- package/dist/index.d.mts +27 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1184 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -3
- package/README.md +0 -3
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
import { Browsers, BufferJSON, DisconnectReason, WAMessageStubType, decryptPollVote, downloadMediaMessage, fetchLatestBaileysVersion, initAuthCreds, isJidGroup, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, makeCacheableSignalKeyStore, makeWASocket, proto, toNumber } from "baileys";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { TypedEmitter, createMessage } from "@usebridger/core/adapter";
|
|
4
|
+
import { UnsupportedError } from "@usebridger/core";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
//#region src/auth-state.ts
|
|
7
|
+
/**
|
|
8
|
+
* Monta o auth state de uma tentativa de conexão. As chaves passam por um cache em memória
|
|
9
|
+
* (o do Baileys, 5 min por chave): o Signal lê a sessão do remetente a cada mensagem, e sem ele
|
|
10
|
+
* cada leitura iria ao storage e repetiria a conversão do `BufferJSON`. O cache vive só nesta
|
|
11
|
+
* tentativa: depois de um `clear()` do bot, a próxima começa vazia e não ressuscita chave
|
|
12
|
+
* apagada. Ele não vê gravações de outro processo, o que não acontece porque a mesma sessão
|
|
13
|
+
* não roda em dois (ADR 0036).
|
|
14
|
+
*/
|
|
15
|
+
async function loadAuthState(store, logger) {
|
|
16
|
+
const stored = await store.getCreds();
|
|
17
|
+
const creds = stored === void 0 ? initAuthCreds() : fromJson(stored);
|
|
18
|
+
const cached = makeCacheableSignalKeyStore({
|
|
19
|
+
get: async (type, ids) => {
|
|
20
|
+
const raw = await store.getKeys(type, ids);
|
|
21
|
+
const keys = {};
|
|
22
|
+
for (const [id, value] of Object.entries(raw)) keys[id] = type === "app-state-sync-key" ? toSyncKeyInstance(fromJson(value)) : fromJson(value);
|
|
23
|
+
return keys;
|
|
24
|
+
},
|
|
25
|
+
set: (data) => store.setKeys(toKeyData(data))
|
|
26
|
+
}, logger);
|
|
27
|
+
return {
|
|
28
|
+
state: {
|
|
29
|
+
creds,
|
|
30
|
+
keys: {
|
|
31
|
+
get: async (type, ids) => {
|
|
32
|
+
const found = await cached.get(type, ids);
|
|
33
|
+
for (const id of Object.keys(found)) if (found[id] === null || found[id] === void 0) delete found[id];
|
|
34
|
+
return found;
|
|
35
|
+
},
|
|
36
|
+
set: (data) => cached.set(withSyncKeyInstances(data))
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
saveCreds: () => store.setCreds(toJson(creds))
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function toSyncKeyInstance(value) {
|
|
43
|
+
return value instanceof proto.Message.AppStateSyncKeyData ? value : proto.Message.AppStateSyncKeyData.fromObject(value);
|
|
44
|
+
}
|
|
45
|
+
function withSyncKeyInstances(data) {
|
|
46
|
+
const syncKeys = data["app-state-sync-key"];
|
|
47
|
+
if (syncKeys === void 0) return data;
|
|
48
|
+
const converted = {};
|
|
49
|
+
for (const [id, value] of Object.entries(syncKeys)) converted[id] = value ? toSyncKeyInstance(value) : value;
|
|
50
|
+
return {
|
|
51
|
+
...data,
|
|
52
|
+
"app-state-sync-key": converted
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function toKeyData(data) {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const [type, entries] of Object.entries(data)) {
|
|
58
|
+
const converted = {};
|
|
59
|
+
for (const [id, value] of Object.entries(entries ?? {})) converted[id] = value === null || value === void 0 ? null : toJson(value);
|
|
60
|
+
out[type] = converted;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function toJson(value) {
|
|
65
|
+
return JSON.parse(JSON.stringify(value, BufferJSON.replacer));
|
|
66
|
+
}
|
|
67
|
+
function fromJson(value) {
|
|
68
|
+
return JSON.parse(JSON.stringify(value), BufferJSON.reviver);
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/disconnect-reason.ts
|
|
72
|
+
function toDisconnectReason(error, { pairing }) {
|
|
73
|
+
switch (statusCode(error)) {
|
|
74
|
+
case DisconnectReason.loggedOut: return "logged-out";
|
|
75
|
+
case DisconnectReason.forbidden:
|
|
76
|
+
case DisconnectReason.multideviceMismatch: return "auth-failed";
|
|
77
|
+
case DisconnectReason.connectionReplaced: return "replaced";
|
|
78
|
+
case DisconnectReason.timedOut: return pairing ? "qr-timeout" : "connection-lost";
|
|
79
|
+
case DisconnectReason.connectionClosed:
|
|
80
|
+
case DisconnectReason.restartRequired: return "connection-lost";
|
|
81
|
+
case DisconnectReason.badSession:
|
|
82
|
+
case DisconnectReason.unavailableService:
|
|
83
|
+
case 405: return "server-error";
|
|
84
|
+
default: return "unknown";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** O Baileys encerra com um `Boom`, que traz o código em `output.statusCode`. */
|
|
88
|
+
function statusCode(error) {
|
|
89
|
+
if (typeof error !== "object" || error === null || !("output" in error)) return void 0;
|
|
90
|
+
const { output } = error;
|
|
91
|
+
if (typeof output !== "object" || output === null || !("statusCode" in output)) return void 0;
|
|
92
|
+
return typeof output.statusCode === "number" ? output.statusCode : void 0;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/normalize.ts
|
|
96
|
+
/** Envelopes que só embrulham o conteúdo de verdade. */
|
|
97
|
+
const ENVELOPES = [
|
|
98
|
+
"ephemeralMessage",
|
|
99
|
+
"viewOnceMessage",
|
|
100
|
+
"viewOnceMessageV2",
|
|
101
|
+
"viewOnceMessageV2Extension",
|
|
102
|
+
"documentWithCaptionMessage",
|
|
103
|
+
"pollCreationMessageV4"
|
|
104
|
+
];
|
|
105
|
+
const VIEW_ONCE = /* @__PURE__ */ new Set([
|
|
106
|
+
"viewOnceMessage",
|
|
107
|
+
"viewOnceMessageV2",
|
|
108
|
+
"viewOnceMessageV2Extension"
|
|
109
|
+
]);
|
|
110
|
+
/**
|
|
111
|
+
* Campos que não são o conteúdo da mensagem. Edição, apagamento e reação chegam por eventos
|
|
112
|
+
* próprios do Baileys (`events.ts`); o resto é metadado do protocolo.
|
|
113
|
+
*/
|
|
114
|
+
const CONTROL = /* @__PURE__ */ new Set([
|
|
115
|
+
"protocolMessage",
|
|
116
|
+
"reactionMessage",
|
|
117
|
+
"encReactionMessage",
|
|
118
|
+
"pollUpdateMessage",
|
|
119
|
+
"keepInChatMessage",
|
|
120
|
+
"pinInChatMessage",
|
|
121
|
+
"senderKeyDistributionMessage",
|
|
122
|
+
"fastRatchetKeySenderKeyDistributionMessage",
|
|
123
|
+
"messageContextInfo"
|
|
124
|
+
]);
|
|
125
|
+
/**
|
|
126
|
+
* Tira os envelopes, em qualquer ordem e aninhados (ephemeral com viewOnce dentro, por exemplo).
|
|
127
|
+
* O limite só protege de um proto malformado.
|
|
128
|
+
*/
|
|
129
|
+
function unwrap(content) {
|
|
130
|
+
let current = content;
|
|
131
|
+
let viewOnce = false;
|
|
132
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
133
|
+
const envelope = ENVELOPES.find((name) => current[name]?.message);
|
|
134
|
+
if (envelope === void 0) break;
|
|
135
|
+
if (VIEW_ONCE.has(envelope)) viewOnce = true;
|
|
136
|
+
current = current[envelope]?.message ?? current;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
content: current,
|
|
140
|
+
viewOnce
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Campo que define o tipo: o primeiro preenchido que não é de controle. */
|
|
144
|
+
function contentKey(content) {
|
|
145
|
+
return Object.keys(content).find((key) => content[key] != null && !CONTROL.has(key));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Converte uma mensagem do Baileys. `null` quando não há o que entregar: sem conteúdo (aviso de
|
|
149
|
+
* grupo, mensagem que não deu para decifrar) ou só controle (edição, reação, apagamento).
|
|
150
|
+
*/
|
|
151
|
+
function toMessage(raw, env) {
|
|
152
|
+
return build(raw, env, toNumber(raw.messageTimestamp) * 1e3, false);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Nova versão de uma mensagem editada, com `isEdited: true`. `raw` é a chave da original com o
|
|
156
|
+
* conteúdo novo; o Baileys não manda o `pushName` na edição, então o nome vem de quem chama.
|
|
157
|
+
*/
|
|
158
|
+
function toEditedMessage(raw, env) {
|
|
159
|
+
return build(raw, env, toNumber(raw.messageTimestamp) * 1e3, true);
|
|
160
|
+
}
|
|
161
|
+
async function build(raw, env, timestamp, isEdited) {
|
|
162
|
+
const chatId = raw.key.remoteJid;
|
|
163
|
+
const id = raw.key.id;
|
|
164
|
+
if (!raw.message || !chatId || !id) return null;
|
|
165
|
+
const { content, viewOnce } = unwrap(raw.message);
|
|
166
|
+
const key = contentKey(content);
|
|
167
|
+
if (key === void 0) return null;
|
|
168
|
+
const isGroup = isJidGroup(chatId) === true;
|
|
169
|
+
const fromMe = raw.key.fromMe === true;
|
|
170
|
+
const node = content[key];
|
|
171
|
+
const context = contextOf(node);
|
|
172
|
+
const base = {
|
|
173
|
+
id,
|
|
174
|
+
chat: {
|
|
175
|
+
id: chatId,
|
|
176
|
+
isGroup
|
|
177
|
+
},
|
|
178
|
+
sender: await authorOf(raw.key, chatId, env, raw.pushName ?? null),
|
|
179
|
+
timestamp,
|
|
180
|
+
fromMe,
|
|
181
|
+
quoted: context ? await quoted(context, chatId, env, timestamp) : null,
|
|
182
|
+
mentions: await Promise.all((context?.mentionedJid ?? []).map((jid) => resolveContact(env, jid, void 0, null))),
|
|
183
|
+
isForwarded: context?.isForwarded === true,
|
|
184
|
+
isViewOnce: viewOnce || raw.key.isViewOnce === true || flag(node, "viewOnce"),
|
|
185
|
+
isEdited
|
|
186
|
+
};
|
|
187
|
+
const unwrapped = {
|
|
188
|
+
...raw,
|
|
189
|
+
message: content
|
|
190
|
+
};
|
|
191
|
+
const media = (m) => ({
|
|
192
|
+
mimetype: m.mimetype ?? "application/octet-stream",
|
|
193
|
+
size: m.fileLength == null ? null : toNumber(m.fileLength) || null,
|
|
194
|
+
download: () => env.download(unwrapped),
|
|
195
|
+
stream: () => env.stream(unwrapped)
|
|
196
|
+
});
|
|
197
|
+
const message = createMessage(init(key, content, base, media));
|
|
198
|
+
natives.set(message, raw);
|
|
199
|
+
return message;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Mensagem do Baileys de onde cada `Message` saiu. Citar no envio precisa do proto original
|
|
203
|
+
* (`quoted` do `sendMessage`); o `WeakMap` some com a `Message`, sem cache para limpar.
|
|
204
|
+
*/
|
|
205
|
+
const natives = /* @__PURE__ */ new WeakMap();
|
|
206
|
+
/** O `WAMessage` que originou a mensagem, se ela foi normalizada por este transport. */
|
|
207
|
+
function nativeOf(message) {
|
|
208
|
+
return natives.get(message);
|
|
209
|
+
}
|
|
210
|
+
function init(key, c, base, media) {
|
|
211
|
+
switch (key) {
|
|
212
|
+
case "conversation": return {
|
|
213
|
+
...base,
|
|
214
|
+
type: "text",
|
|
215
|
+
text: c.conversation ?? ""
|
|
216
|
+
};
|
|
217
|
+
case "extendedTextMessage": return {
|
|
218
|
+
...base,
|
|
219
|
+
type: "text",
|
|
220
|
+
text: c.extendedTextMessage?.text ?? ""
|
|
221
|
+
};
|
|
222
|
+
case "imageMessage": {
|
|
223
|
+
const m = c.imageMessage ?? {};
|
|
224
|
+
return {
|
|
225
|
+
...base,
|
|
226
|
+
type: "image",
|
|
227
|
+
text: m.caption ?? null,
|
|
228
|
+
media: media(m)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
case "videoMessage":
|
|
232
|
+
case "ptvMessage": {
|
|
233
|
+
const m = c[key] ?? {};
|
|
234
|
+
return {
|
|
235
|
+
...base,
|
|
236
|
+
type: "video",
|
|
237
|
+
text: m.caption ?? null,
|
|
238
|
+
media: media(m)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
case "audioMessage": {
|
|
242
|
+
const m = c.audioMessage ?? {};
|
|
243
|
+
return {
|
|
244
|
+
...base,
|
|
245
|
+
type: m.ptt ? "voice" : "audio",
|
|
246
|
+
text: null,
|
|
247
|
+
media: media(m)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
case "stickerMessage": return {
|
|
251
|
+
...base,
|
|
252
|
+
type: "sticker",
|
|
253
|
+
text: null,
|
|
254
|
+
media: media(c.stickerMessage ?? {})
|
|
255
|
+
};
|
|
256
|
+
case "documentMessage": {
|
|
257
|
+
const m = c.documentMessage ?? {};
|
|
258
|
+
return {
|
|
259
|
+
...base,
|
|
260
|
+
type: "document",
|
|
261
|
+
text: m.caption ?? null,
|
|
262
|
+
fileName: m.fileName ?? null,
|
|
263
|
+
media: media(m)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case "locationMessage": {
|
|
267
|
+
const m = c.locationMessage ?? {};
|
|
268
|
+
return {
|
|
269
|
+
...base,
|
|
270
|
+
type: "location",
|
|
271
|
+
text: m.comment ?? null,
|
|
272
|
+
location: {
|
|
273
|
+
latitude: m.degreesLatitude ?? 0,
|
|
274
|
+
longitude: m.degreesLongitude ?? 0,
|
|
275
|
+
name: m.name ?? m.address ?? null,
|
|
276
|
+
...m.address ? { address: m.address } : {}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
case "liveLocationMessage": {
|
|
281
|
+
const m = c.liveLocationMessage ?? {};
|
|
282
|
+
return {
|
|
283
|
+
...base,
|
|
284
|
+
type: "location",
|
|
285
|
+
text: m.caption ?? null,
|
|
286
|
+
location: {
|
|
287
|
+
latitude: m.degreesLatitude ?? 0,
|
|
288
|
+
longitude: m.degreesLongitude ?? 0,
|
|
289
|
+
name: null
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
case "contactMessage":
|
|
294
|
+
case "contactsArrayMessage": {
|
|
295
|
+
const list = key === "contactMessage" ? [c.contactMessage ?? {}] : c.contactsArrayMessage?.contacts ?? [];
|
|
296
|
+
return {
|
|
297
|
+
...base,
|
|
298
|
+
type: "contact",
|
|
299
|
+
text: null,
|
|
300
|
+
contacts: list.map((m) => ({
|
|
301
|
+
name: m.displayName ?? "",
|
|
302
|
+
vcard: m.vcard ?? ""
|
|
303
|
+
}))
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
case "pollCreationMessage":
|
|
307
|
+
case "pollCreationMessageV2":
|
|
308
|
+
case "pollCreationMessageV3":
|
|
309
|
+
case "pollCreationMessageV5": {
|
|
310
|
+
const m = c[key] ?? {};
|
|
311
|
+
return {
|
|
312
|
+
...base,
|
|
313
|
+
type: "poll",
|
|
314
|
+
text: null,
|
|
315
|
+
poll: {
|
|
316
|
+
name: m.name ?? "",
|
|
317
|
+
options: (m.options ?? []).map((o) => o.optionName ?? "")
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
default: return {
|
|
322
|
+
...base,
|
|
323
|
+
type: "unknown",
|
|
324
|
+
text: null
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function contextOf(node) {
|
|
329
|
+
if (typeof node !== "object" || node === null || !("contextInfo" in node)) return null;
|
|
330
|
+
return node.contextInfo ?? null;
|
|
331
|
+
}
|
|
332
|
+
function flag(node, name) {
|
|
333
|
+
return typeof node === "object" && node !== null && name in node && Reflect.get(node, name) === true;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* A citada vem só com id, autor e conteúdo: sem horário (vale o da mensagem que cita) e sem nome
|
|
337
|
+
* do autor.
|
|
338
|
+
*/
|
|
339
|
+
function quoted(context, chatId, env, timestamp) {
|
|
340
|
+
if (!context.quotedMessage || !context.stanzaId) return Promise.resolve(null);
|
|
341
|
+
const author = context.participant ? jidNormalizedUser(context.participant) : void 0;
|
|
342
|
+
return build({
|
|
343
|
+
key: {
|
|
344
|
+
remoteJid: context.remoteJid ?? chatId,
|
|
345
|
+
id: context.stanzaId,
|
|
346
|
+
fromMe: author !== void 0 && env.selfIds.includes(author),
|
|
347
|
+
participant: author
|
|
348
|
+
},
|
|
349
|
+
message: context.quotedMessage
|
|
350
|
+
}, env, timestamp, false);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Autor de uma mensagem pela chave. Grupo, status e citada o trazem em `participant`. Sem ele
|
|
354
|
+
* (conversa privada), o autor é o próprio chat, ou a sessão quando foi ela que mandou.
|
|
355
|
+
*/
|
|
356
|
+
function authorOf(key, chatId, env, name) {
|
|
357
|
+
const alt = key.participant ? key.participantAlt : key.fromMe ? void 0 : key.remoteJidAlt;
|
|
358
|
+
return resolveContact(env, authorJid(key, chatId, env.selfIds), alt, name);
|
|
359
|
+
}
|
|
360
|
+
/** O JID do autor pela chave, sem resolver o telefone (ver `authorOf`). */
|
|
361
|
+
function authorJid(key, chatId, selfIds) {
|
|
362
|
+
if (key.participant) return key.participant;
|
|
363
|
+
return key.fromMe ? selfIds[0] ?? chatId : chatId;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Contato com o telefone resolvido: do próprio JID de telefone, do JID alternativo que o
|
|
367
|
+
* Baileys manda junto com um LID ou do mapeamento LID ↔ telefone da sessão.
|
|
368
|
+
*/
|
|
369
|
+
async function resolveContact(env, jid, alt, name) {
|
|
370
|
+
const id = jidNormalizedUser(jid) || jid;
|
|
371
|
+
return {
|
|
372
|
+
id,
|
|
373
|
+
name,
|
|
374
|
+
phone: await phoneOf(env, id, alt)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async function phoneOf(env, id, alt) {
|
|
378
|
+
if (isPnUser(id)) return jidDecode(id)?.user ?? null;
|
|
379
|
+
if (alt && isPnUser(alt)) return jidDecode(alt)?.user ?? null;
|
|
380
|
+
if (!isLidUser(id)) return null;
|
|
381
|
+
const pn = await env.pnForLid(id);
|
|
382
|
+
return pn ? jidDecode(pn)?.user ?? null : null;
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/events.ts
|
|
386
|
+
/**
|
|
387
|
+
* Nome e telefone já vistos de cada contato. Decide quando sai o `contact.updated` e dá nome a
|
|
388
|
+
* quem aparece sem `pushName` (reação, edição, apagamento, grupo). Vive enquanto o transport
|
|
389
|
+
* vive: depois de reiniciar o processo, cada contato conta de novo como visto pela primeira vez.
|
|
390
|
+
*/
|
|
391
|
+
var ContactBook = class {
|
|
392
|
+
#known = /* @__PURE__ */ new Map();
|
|
393
|
+
/**
|
|
394
|
+
* Registra um remetente. Devolve o `contact.updated` quando há o que contar: na primeira vez
|
|
395
|
+
* que o contato aparece (ADR 0049) ou quando o nome ou o telefone mudou. Campo desconhecido
|
|
396
|
+
* (`null`) não apaga o que já se sabia.
|
|
397
|
+
*/
|
|
398
|
+
observe(contact) {
|
|
399
|
+
const known = this.#known.get(contact.id);
|
|
400
|
+
const name = contact.name ?? known?.name ?? null;
|
|
401
|
+
const phone = contact.phone ?? known?.phone ?? null;
|
|
402
|
+
this.#known.set(contact.id, {
|
|
403
|
+
name,
|
|
404
|
+
phone
|
|
405
|
+
});
|
|
406
|
+
const update = { id: contact.id };
|
|
407
|
+
if (name !== null && name !== known?.name) update.name = name;
|
|
408
|
+
if (phone !== null && phone !== known?.phone) update.phone = phone;
|
|
409
|
+
return update.name === void 0 && update.phone === void 0 ? null : update;
|
|
410
|
+
}
|
|
411
|
+
nameOf(id) {
|
|
412
|
+
return this.#known.get(id)?.name ?? null;
|
|
413
|
+
}
|
|
414
|
+
/** O contato com o nome já visto, quando o evento não trouxe nenhum. */
|
|
415
|
+
named(contact) {
|
|
416
|
+
return contact.name === null ? {
|
|
417
|
+
...contact,
|
|
418
|
+
name: this.nameOf(contact.id)
|
|
419
|
+
} : contact;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
/** O `Chat` de um JID; os eventos de grupo também o usam (ADR 0059). */
|
|
423
|
+
function chatOf(id) {
|
|
424
|
+
return {
|
|
425
|
+
id,
|
|
426
|
+
isGroup: isJidGroup(id) === true
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* `reaction` a partir do `messages.reaction`: `key` é a mensagem reagida e `reaction.key` a
|
|
431
|
+
* mensagem de reação, que diz quem reagiu. Texto vazio é reação removida.
|
|
432
|
+
*/
|
|
433
|
+
async function toReaction({ key, reaction }, env, contacts) {
|
|
434
|
+
const from = reaction.key;
|
|
435
|
+
const chatId = from?.remoteJid ?? key.remoteJid;
|
|
436
|
+
if (!from || !chatId || !key.id || isJidStatusBroadcast(chatId)) return null;
|
|
437
|
+
return {
|
|
438
|
+
chat: chatOf(chatId),
|
|
439
|
+
messageId: key.id,
|
|
440
|
+
sender: contacts.named(await authorOf(from, chatId, env, null)),
|
|
441
|
+
emoji: reaction.text || null,
|
|
442
|
+
fromMe: from.fromMe === true
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* `message.edited` a partir do `messages.update` de edição. A chave é a da original com o autor
|
|
447
|
+
* da edição, e o conteúdo novo vem em `editedMessage`.
|
|
448
|
+
*/
|
|
449
|
+
function toEdited({ key, update }, env, contacts) {
|
|
450
|
+
const content = update.message?.editedMessage?.message;
|
|
451
|
+
const chatId = key.remoteJid;
|
|
452
|
+
if (!content || !chatId || isJidStatusBroadcast(chatId)) return Promise.resolve(null);
|
|
453
|
+
return toEditedMessage({
|
|
454
|
+
key,
|
|
455
|
+
message: content,
|
|
456
|
+
messageTimestamp: update.messageTimestamp ?? Math.floor(Date.now() / 1e3),
|
|
457
|
+
pushName: contacts.nameOf(jidNormalizedUser(authorJid(key, chatId, env.selfIds)))
|
|
458
|
+
}, env);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* `message.deleted` a partir do `messages.update` de apagamento (`REVOKE`). `key` é a apagada;
|
|
462
|
+
* `update.key` é a mensagem que apagou, que diz quem apagou (o autor ou um admin do grupo).
|
|
463
|
+
*/
|
|
464
|
+
async function toDeleted({ key, update }, env, contacts) {
|
|
465
|
+
const chatId = key.remoteJid;
|
|
466
|
+
if (!chatId || !key.id || isJidStatusBroadcast(chatId)) return null;
|
|
467
|
+
const by = update.key;
|
|
468
|
+
return {
|
|
469
|
+
chat: chatOf(chatId),
|
|
470
|
+
messageId: key.id,
|
|
471
|
+
deletedBy: by ? contacts.named(await authorOf(by, chatId, env, null)) : null,
|
|
472
|
+
fromMe: (by ?? key).fromMe === true
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Converte a alteração de participantes. Entrada e saída da própria sessão viram
|
|
477
|
+
* `group.joined`/`group.left` e saem da lista; promovida ou rebaixada, ela segue em
|
|
478
|
+
* `group.participants`. Troca de número (`modify`) não tem evento no core.
|
|
479
|
+
*/
|
|
480
|
+
async function toParticipantEvents({ id, author, authorPn, participants, action }, env, contacts) {
|
|
481
|
+
if (action === "modify") return {
|
|
482
|
+
self: null,
|
|
483
|
+
others: null
|
|
484
|
+
};
|
|
485
|
+
const isSelf = (p) => [
|
|
486
|
+
p.id,
|
|
487
|
+
p.phoneNumber,
|
|
488
|
+
p.lid
|
|
489
|
+
].some((jid) => jid && env.selfIds.includes(jidNormalizedUser(jid)));
|
|
490
|
+
const self = !participants.some(isSelf) ? null : action === "add" ? "group.joined" : action === "remove" ? "group.left" : null;
|
|
491
|
+
const rest = self === null ? participants : participants.filter((p) => !isSelf(p));
|
|
492
|
+
if (rest.length === 0) return {
|
|
493
|
+
self,
|
|
494
|
+
others: null
|
|
495
|
+
};
|
|
496
|
+
const contact = async (jid, alt, name) => contacts.named(await resolveContact(env, jid, alt, name));
|
|
497
|
+
return {
|
|
498
|
+
self,
|
|
499
|
+
others: {
|
|
500
|
+
chat: chatOf(id),
|
|
501
|
+
action,
|
|
502
|
+
participants: await Promise.all(rest.map((p) => contact(p.id, p.phoneNumber, p.notify ?? p.name ?? null))),
|
|
503
|
+
actor: author ? await contact(author, authorPn, null) : null
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* `group.updated` com só os campos que mudaram. O Baileys também emite `groups.update` com os
|
|
509
|
+
* metadados completos de cada grupo ao sincronizar (`groupFetchAllParticipating`); esse traz
|
|
510
|
+
* `participants` e não é uma alteração, então fica de fora.
|
|
511
|
+
*/
|
|
512
|
+
function toGroupUpdated(update) {
|
|
513
|
+
if (!update.id || update.participants) return null;
|
|
514
|
+
const changed = { chat: chatOf(update.id) };
|
|
515
|
+
if (update.subject !== void 0) changed.title = update.subject;
|
|
516
|
+
if ("desc" in update) changed.description = update.desc || null;
|
|
517
|
+
if (update.announce !== void 0) changed.announce = update.announce;
|
|
518
|
+
if (update.restrict !== void 0) changed.restrict = update.restrict;
|
|
519
|
+
return Object.keys(changed).length > 1 ? changed : null;
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/format.ts
|
|
523
|
+
/**
|
|
524
|
+
* Renderiza a árvore. O WhatsApp não tem escape: um `*` literal do texto pode casar com outro e
|
|
525
|
+
* virar negrito, como já acontece no app. Por isso o texto literal vai como está.
|
|
526
|
+
*/
|
|
527
|
+
function renderWhatsApp(text) {
|
|
528
|
+
const mentions = /* @__PURE__ */ new Set();
|
|
529
|
+
const render = (nodes) => {
|
|
530
|
+
let out = "";
|
|
531
|
+
for (const node of nodes) {
|
|
532
|
+
if (typeof node === "string") {
|
|
533
|
+
out += node;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
switch (node.type) {
|
|
537
|
+
case "bold":
|
|
538
|
+
case "italic":
|
|
539
|
+
out += wrap(node.type === "bold" ? "*" : "_", render(node.children));
|
|
540
|
+
break;
|
|
541
|
+
case "code":
|
|
542
|
+
out += wrap("`", node.text);
|
|
543
|
+
break;
|
|
544
|
+
case "link": {
|
|
545
|
+
const label = render(node.children);
|
|
546
|
+
out += label === node.url ? node.url : `${label} (${node.url})`;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
case "mention":
|
|
550
|
+
mentions.add(node.contact.id);
|
|
551
|
+
out += `@${jidDecode(node.contact.id)?.user ?? node.contact.id}`;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
};
|
|
556
|
+
return {
|
|
557
|
+
text: render(text.nodes),
|
|
558
|
+
mentions: [...mentions]
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* O marcador só vale colado no texto (`* a *` sai literal): o espaço das pontas fica do lado de
|
|
563
|
+
* fora.
|
|
564
|
+
*/
|
|
565
|
+
function wrap(marker, inner) {
|
|
566
|
+
const [, lead = "", core = "", trail = ""] = /^(\s*)([\s\S]*?)(\s*)$/.exec(inner) ?? [];
|
|
567
|
+
if (core === "") return inner;
|
|
568
|
+
return `${lead}${marker}${core}${marker}${trail}`;
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/logger.ts
|
|
572
|
+
function toBaileysLogger(log) {
|
|
573
|
+
const write = (method) => (obj, msg) => {
|
|
574
|
+
if (typeof obj === "string") log[method](obj);
|
|
575
|
+
else log[method](msg ?? "", toFields(obj));
|
|
576
|
+
};
|
|
577
|
+
return {
|
|
578
|
+
get level() {
|
|
579
|
+
return log.level;
|
|
580
|
+
},
|
|
581
|
+
child: (bindings) => toBaileysLogger(log.child(bindings)),
|
|
582
|
+
trace: write("trace"),
|
|
583
|
+
debug: write("debug"),
|
|
584
|
+
info: write("info"),
|
|
585
|
+
warn: write("warn"),
|
|
586
|
+
error: write("error")
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function toFields(obj) {
|
|
590
|
+
if (obj instanceof Error) return { err: obj };
|
|
591
|
+
if (typeof obj === "object" && obj !== null) return obj;
|
|
592
|
+
return obj === void 0 ? void 0 : { value: obj };
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/outgoing.ts
|
|
596
|
+
/**
|
|
597
|
+
* Conteúdo do `sendMessage`, com as menções que o Baileys põe no `contextInfo` de qualquer tipo.
|
|
598
|
+
* A árvore neutra (`formatted`, `formattedCaption`) vira a marcação do WhatsApp, e as menções
|
|
599
|
+
* dela se juntam às de `options.mentions` (ADR 0061).
|
|
600
|
+
*/
|
|
601
|
+
function toContent(content, options) {
|
|
602
|
+
const ids = new Set(options?.mentions);
|
|
603
|
+
let text;
|
|
604
|
+
let caption;
|
|
605
|
+
if (content.type === "text") {
|
|
606
|
+
text = content.text;
|
|
607
|
+
if (content.formatted) {
|
|
608
|
+
const rendered = renderWhatsApp(content.formatted);
|
|
609
|
+
text = rendered.text;
|
|
610
|
+
for (const id of rendered.mentions) ids.add(id);
|
|
611
|
+
}
|
|
612
|
+
} else if ("caption" in content || "formattedCaption" in content) {
|
|
613
|
+
caption = content.caption;
|
|
614
|
+
if (content.formattedCaption) {
|
|
615
|
+
const rendered = renderWhatsApp(content.formattedCaption);
|
|
616
|
+
caption = rendered.text;
|
|
617
|
+
for (const id of rendered.mentions) ids.add(id);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const mentions = ids.size > 0 ? { mentions: [...ids] } : {};
|
|
621
|
+
switch (content.type) {
|
|
622
|
+
case "text": return {
|
|
623
|
+
text: text ?? content.text,
|
|
624
|
+
...mentions
|
|
625
|
+
};
|
|
626
|
+
case "image": return {
|
|
627
|
+
image: content.media,
|
|
628
|
+
caption,
|
|
629
|
+
mimetype: content.mimetype,
|
|
630
|
+
...mentions
|
|
631
|
+
};
|
|
632
|
+
case "video": return {
|
|
633
|
+
video: content.media,
|
|
634
|
+
caption,
|
|
635
|
+
mimetype: content.mimetype,
|
|
636
|
+
...mentions
|
|
637
|
+
};
|
|
638
|
+
case "audio":
|
|
639
|
+
case "voice": return {
|
|
640
|
+
audio: content.media,
|
|
641
|
+
ptt: content.type === "voice",
|
|
642
|
+
mimetype: content.mimetype,
|
|
643
|
+
...mentions
|
|
644
|
+
};
|
|
645
|
+
case "sticker": return {
|
|
646
|
+
sticker: content.media,
|
|
647
|
+
...mentions
|
|
648
|
+
};
|
|
649
|
+
case "document": return {
|
|
650
|
+
document: content.media,
|
|
651
|
+
fileName: content.fileName,
|
|
652
|
+
mimetype: content.mimetype,
|
|
653
|
+
caption,
|
|
654
|
+
...mentions
|
|
655
|
+
};
|
|
656
|
+
case "album": throw new UnsupportedError("send.album", "baileys");
|
|
657
|
+
case "poll": return {
|
|
658
|
+
poll: {
|
|
659
|
+
name: content.name,
|
|
660
|
+
values: [...content.options],
|
|
661
|
+
selectableCount: content.multiple === true ? 0 : 1
|
|
662
|
+
},
|
|
663
|
+
...mentions
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function toWAKey(key) {
|
|
668
|
+
return {
|
|
669
|
+
remoteJid: key.chatId,
|
|
670
|
+
id: key.id,
|
|
671
|
+
fromMe: key.fromMe,
|
|
672
|
+
participant: key.senderId ?? void 0
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* A mensagem a citar como o Baileys a quer. Recebida por este transport, vai o proto original
|
|
677
|
+
* (a citada mostra mídia e legenda); montada fora dele (testes, storage), vai só o texto.
|
|
678
|
+
*/
|
|
679
|
+
function toQuoted(message) {
|
|
680
|
+
return nativeOf(message) ?? {
|
|
681
|
+
key: toWAKey(message.key),
|
|
682
|
+
message: { conversation: message.text ?? "" }
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
/** Metadados do grupo com o telefone de cada participante resolvido, inclusive com LID. */
|
|
686
|
+
async function toGroupMetadata(native, env) {
|
|
687
|
+
const participants = await Promise.all(native.participants.map(async (p) => {
|
|
688
|
+
return {
|
|
689
|
+
...await resolveContact(env, p.id, p.phoneNumber, p.name ?? p.notify ?? null),
|
|
690
|
+
isAdmin: p.admin === "admin" || p.admin === "superadmin",
|
|
691
|
+
isSuperAdmin: p.admin === "superadmin"
|
|
692
|
+
};
|
|
693
|
+
}));
|
|
694
|
+
return {
|
|
695
|
+
id: native.id,
|
|
696
|
+
title: native.subject,
|
|
697
|
+
description: native.desc ?? null,
|
|
698
|
+
ownerId: native.owner ? jidNormalizedUser(native.owner) || native.owner : null,
|
|
699
|
+
participants
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/polls.ts
|
|
704
|
+
/** Enquetes guardadas por padrão; a mais antiga sai primeiro. */
|
|
705
|
+
const DEFAULT_MAX_POLLS = 1e3;
|
|
706
|
+
/**
|
|
707
|
+
* Enquetes que a sessão enviou ou recebeu enquanto conectada, pelo ID da mensagem. O ID basta: o
|
|
708
|
+
* mesmo chat privado aparece ora como LID, ora como telefone, e o ID do WhatsApp é aleatório. Vive
|
|
709
|
+
* até o `disconnect()`: voto de enquete anterior não se decifra e é descartado.
|
|
710
|
+
*/
|
|
711
|
+
var PollBook = class {
|
|
712
|
+
#polls = /* @__PURE__ */ new Map();
|
|
713
|
+
#max;
|
|
714
|
+
constructor(max = DEFAULT_MAX_POLLS) {
|
|
715
|
+
this.#max = max;
|
|
716
|
+
}
|
|
717
|
+
/** Guarda a enquete, se a mensagem for uma e trouxer o segredo; o resto é ignorado. */
|
|
718
|
+
remember(raw) {
|
|
719
|
+
const id = raw.key.id;
|
|
720
|
+
if (!id || !raw.message) return;
|
|
721
|
+
const { content } = unwrap(raw.message);
|
|
722
|
+
const poll = content.pollCreationMessage ?? content.pollCreationMessageV2 ?? content.pollCreationMessageV3 ?? content.pollCreationMessageV5;
|
|
723
|
+
const secret = raw.message.messageContextInfo?.messageSecret ?? content.messageContextInfo?.messageSecret;
|
|
724
|
+
if (!poll || !secret) return;
|
|
725
|
+
this.#polls.delete(id);
|
|
726
|
+
this.#polls.set(id, {
|
|
727
|
+
key: raw.key,
|
|
728
|
+
secret,
|
|
729
|
+
hashes: (poll.options ?? []).map((o) => sha256Hex(o.optionName ?? ""))
|
|
730
|
+
});
|
|
731
|
+
if (this.#polls.size > this.#max) {
|
|
732
|
+
const oldest = this.#polls.keys().next().value;
|
|
733
|
+
if (oldest !== void 0) this.#polls.delete(oldest);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
get(id) {
|
|
737
|
+
return this.#polls.get(id);
|
|
738
|
+
}
|
|
739
|
+
clear() {
|
|
740
|
+
this.#polls.clear();
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
/** O `pollUpdateMessage` da mensagem, se ela for um voto. */
|
|
744
|
+
function pollUpdateOf(raw) {
|
|
745
|
+
return raw.message ? unwrap(raw.message).content.pollUpdateMessage ?? null : null;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* `poll.vote` a partir do `pollUpdateMessage`. Devolve `null` quando a enquete não está no
|
|
749
|
+
* `PollBook` (enviada antes da conexão atual, por exemplo) e lança quando ela está, mas o voto não
|
|
750
|
+
* decifra.
|
|
751
|
+
*/
|
|
752
|
+
async function toPollVote(raw, polls, env, contacts) {
|
|
753
|
+
const update = pollUpdateOf(raw);
|
|
754
|
+
const pollKey = update?.pollCreationMessageKey;
|
|
755
|
+
const chatId = raw.key.remoteJid;
|
|
756
|
+
if (!update?.vote || !pollKey?.id || !chatId) return null;
|
|
757
|
+
const poll = polls.get(pollKey.id);
|
|
758
|
+
if (poll === void 0) return null;
|
|
759
|
+
const vote = decrypt(update.vote, poll, pollKey, raw.key, env.selfIds);
|
|
760
|
+
if (vote === null) throw new Error("baileys: o voto da enquete não decifrou");
|
|
761
|
+
const options = /* @__PURE__ */ new Set();
|
|
762
|
+
for (const hash of vote.selectedOptions ?? []) {
|
|
763
|
+
const index = poll.hashes.indexOf(Buffer.from(hash).toString("hex"));
|
|
764
|
+
if (index >= 0) options.add(index);
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
chat: chatOf(chatId),
|
|
768
|
+
messageId: pollKey.id,
|
|
769
|
+
sender: contacts.named(await authorOf(raw.key, chatId, env, raw.pushName ?? null)),
|
|
770
|
+
options: [...options].sort((a, b) => a - b),
|
|
771
|
+
fromMe: raw.key.fromMe === true
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Decifra o voto. A chave sai do JID de quem criou e de quem votou, como o aparelho do votante os
|
|
776
|
+
* viu, e com o LID ele pode ter usado o telefone ou o LID: tenta cada par conhecido.
|
|
777
|
+
*/
|
|
778
|
+
function decrypt(vote, poll, pollKey, voteKey, selfIds) {
|
|
779
|
+
const creators = unique([...jidsOf(poll.key, selfIds), ...jidsOf(pollKey, selfIds)]);
|
|
780
|
+
const voters = unique(jidsOf(voteKey, selfIds));
|
|
781
|
+
const pollMsgId = pollKey.id ?? "";
|
|
782
|
+
for (const pollCreatorJid of creators) for (const voterJid of voters) {
|
|
783
|
+
const decrypted = tryDecrypt(vote, {
|
|
784
|
+
pollCreatorJid,
|
|
785
|
+
pollMsgId,
|
|
786
|
+
pollEncKey: poll.secret,
|
|
787
|
+
voterJid
|
|
788
|
+
});
|
|
789
|
+
if (decrypted !== null) return decrypted;
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
function tryDecrypt(vote, context) {
|
|
794
|
+
try {
|
|
795
|
+
return decryptPollVote(vote, context);
|
|
796
|
+
} catch {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/** JIDs possíveis do autor de uma chave: o da sessão, o do participante ou o do chat privado. */
|
|
801
|
+
function jidsOf(key, selfIds) {
|
|
802
|
+
if (key.fromMe) return [...selfIds];
|
|
803
|
+
return (key.participant ? [key.participant, key.participantAlt] : [key.remoteJid, key.remoteJidAlt]).flatMap((jid) => jid ? [jidNormalizedUser(jid) || jid] : []);
|
|
804
|
+
}
|
|
805
|
+
function unique(values) {
|
|
806
|
+
return [...new Set(values)];
|
|
807
|
+
}
|
|
808
|
+
function sha256Hex(text) {
|
|
809
|
+
return createHash("sha256").update(text).digest("hex");
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/transport.ts
|
|
813
|
+
var BaileysTransport = class {
|
|
814
|
+
name = "baileys";
|
|
815
|
+
/**
|
|
816
|
+
* Todas as capabilities do core, menos `actions`: o WhatsApp pelo Baileys não tem botões
|
|
817
|
+
* confiáveis, e o kernel envia o menu em texto numerado (ADR 0062).
|
|
818
|
+
*/
|
|
819
|
+
capabilities = /* @__PURE__ */ new Set([
|
|
820
|
+
"groups",
|
|
821
|
+
"groups.add",
|
|
822
|
+
"groups.remove",
|
|
823
|
+
"groups.promote",
|
|
824
|
+
"mentions",
|
|
825
|
+
"reactions",
|
|
826
|
+
"typing",
|
|
827
|
+
"send.text",
|
|
828
|
+
"send.image",
|
|
829
|
+
"send.video",
|
|
830
|
+
"send.audio",
|
|
831
|
+
"send.voice",
|
|
832
|
+
"send.sticker",
|
|
833
|
+
"send.document",
|
|
834
|
+
"media.download",
|
|
835
|
+
"message.edit",
|
|
836
|
+
"message.delete",
|
|
837
|
+
"polls",
|
|
838
|
+
"quoted",
|
|
839
|
+
"pairing"
|
|
840
|
+
]);
|
|
841
|
+
#pairing;
|
|
842
|
+
#driver;
|
|
843
|
+
#deps;
|
|
844
|
+
#events;
|
|
845
|
+
#socket = null;
|
|
846
|
+
#self = null;
|
|
847
|
+
/** Sobe a cada `connect()`/`disconnect()`: invalida a tentativa que ainda estava começando. */
|
|
848
|
+
#attempt = 0;
|
|
849
|
+
/**
|
|
850
|
+
* Metadados por grupo (ADR 0046): o kernel os pede a cada comando `group-admin`. Invalidado
|
|
851
|
+
* pelos eventos de grupo do Baileys e a cada conexão, porque a queda pode ter perdido algum.
|
|
852
|
+
*/
|
|
853
|
+
#groups = /* @__PURE__ */ new Map();
|
|
854
|
+
/** Contatos já vistos, entre conexões: o `contact.updated` sai só no que é novo. */
|
|
855
|
+
#contacts = new ContactBook();
|
|
856
|
+
/** Enquetes vistas, para decifrar os votos (ADR 0071); o `disconnect()` as esquece. */
|
|
857
|
+
#polls = new PollBook();
|
|
858
|
+
constructor(options, deps) {
|
|
859
|
+
this.#pairing = options.pairing;
|
|
860
|
+
this.#driver = options.driver;
|
|
861
|
+
this.#deps = deps;
|
|
862
|
+
this.#events = new TypedEmitter((error, event) => this.#log.error("handler de evento do transport falhou", {
|
|
863
|
+
err: error,
|
|
864
|
+
event
|
|
865
|
+
}));
|
|
866
|
+
}
|
|
867
|
+
get self() {
|
|
868
|
+
return this.#self;
|
|
869
|
+
}
|
|
870
|
+
/** O socket do Baileys (`WASocket`) da tentativa atual, ou `null` sem conexão. */
|
|
871
|
+
get native() {
|
|
872
|
+
return this.#socket;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* O `WAMessage` de onde a mensagem saiu (ADR 0066), o mesmo que o envio usa para citar. A citada
|
|
876
|
+
* devolve o proto montado do `contextInfo`, sem `pushName` nem horário. O Baileys não emite
|
|
877
|
+
* `interaction`: para ela, e para mensagem que não veio daqui, `undefined`.
|
|
878
|
+
*/
|
|
879
|
+
raw(source) {
|
|
880
|
+
return "key" in source ? nativeOf(source) : void 0;
|
|
881
|
+
}
|
|
882
|
+
get #log() {
|
|
883
|
+
return this.#deps.log;
|
|
884
|
+
}
|
|
885
|
+
on(event, handler) {
|
|
886
|
+
return this.#events.on(event, handler);
|
|
887
|
+
}
|
|
888
|
+
async connect() {
|
|
889
|
+
const attempt = ++this.#attempt;
|
|
890
|
+
this.#endSocket();
|
|
891
|
+
const version = await this.#driver.version();
|
|
892
|
+
const logger = toBaileysLogger(this.#log.child({ lib: "baileys" }));
|
|
893
|
+
const { state, saveCreds } = await loadAuthState(this.#deps.auth, logger);
|
|
894
|
+
if (attempt !== this.#attempt) return;
|
|
895
|
+
this.#groups.clear();
|
|
896
|
+
const socket = this.#driver.makeSocket({
|
|
897
|
+
auth: state,
|
|
898
|
+
logger,
|
|
899
|
+
version,
|
|
900
|
+
cachedGroupMetadata: (jid) => this.#cachedGroup(jid)
|
|
901
|
+
});
|
|
902
|
+
this.#socket = socket;
|
|
903
|
+
const current = () => this.#socket === socket;
|
|
904
|
+
let saving = Promise.resolve();
|
|
905
|
+
let pairing = false;
|
|
906
|
+
let codeRequested = false;
|
|
907
|
+
socket.ev.on("creds.update", () => {
|
|
908
|
+
if (!current()) return;
|
|
909
|
+
saving = saving.then(saveCreds).catch((error) => {
|
|
910
|
+
this.#log.error("falha ao gravar as credenciais da sessão", { err: error });
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
let inbound = Promise.resolve();
|
|
914
|
+
const enqueue = (work, failure, messageId) => {
|
|
915
|
+
inbound = inbound.then(work).catch((error) => {
|
|
916
|
+
this.#log.error(failure, {
|
|
917
|
+
err: error,
|
|
918
|
+
messageId
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
const env = this.#normalizeEnv(socket, state, logger);
|
|
923
|
+
const contacts = this.#contacts;
|
|
924
|
+
const polls = this.#polls;
|
|
925
|
+
socket.ev.on("messages.upsert", ({ messages, type }) => {
|
|
926
|
+
if (!current()) return;
|
|
927
|
+
if (type !== "notify") return;
|
|
928
|
+
for (const raw of messages) {
|
|
929
|
+
if (raw.key.remoteJid && isJidStatusBroadcast(raw.key.remoteJid)) continue;
|
|
930
|
+
enqueue(async () => {
|
|
931
|
+
polls.remember(raw);
|
|
932
|
+
if (pollUpdateOf(raw) !== null) {
|
|
933
|
+
const vote = await toPollVote(raw, polls, env, contacts);
|
|
934
|
+
if (vote === null) this.#log.debug("voto de enquete desconhecida; descartado", { messageId: raw.key.id });
|
|
935
|
+
else if (current()) this.#events.emit("poll.vote", vote);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const message = await toMessage(raw, env);
|
|
939
|
+
if (message === null || !current()) return;
|
|
940
|
+
const contact = message.fromMe ? null : contacts.observe(message.sender);
|
|
941
|
+
if (contact !== null) this.#events.emit("contact.updated", contact);
|
|
942
|
+
this.#events.emit("message", message);
|
|
943
|
+
}, "falha ao normalizar mensagem do Baileys; descartada", raw.key.id);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
socket.ev.on("messages.reaction", (reactions) => {
|
|
947
|
+
if (!current()) return;
|
|
948
|
+
for (const item of reactions) enqueue(async () => {
|
|
949
|
+
const reaction = await toReaction(item, env, contacts);
|
|
950
|
+
if (reaction !== null && current()) this.#events.emit("reaction", reaction);
|
|
951
|
+
}, "falha ao converter reação do Baileys; descartada", item.key.id);
|
|
952
|
+
});
|
|
953
|
+
socket.ev.on("messages.update", (updates) => {
|
|
954
|
+
if (!current()) return;
|
|
955
|
+
for (const item of updates) if (item.update.message?.editedMessage) enqueue(async () => {
|
|
956
|
+
const message = await toEdited(item, env, contacts);
|
|
957
|
+
if (message !== null && current()) this.#events.emit("message.edited", message);
|
|
958
|
+
}, "falha ao converter edição do Baileys; descartada", item.key.id);
|
|
959
|
+
else if (item.update.messageStubType === WAMessageStubType.REVOKE) enqueue(async () => {
|
|
960
|
+
const deleted = await toDeleted(item, env, contacts);
|
|
961
|
+
if (deleted !== null && current()) this.#events.emit("message.deleted", deleted);
|
|
962
|
+
}, "falha ao converter apagamento do Baileys; descartado", item.key.id);
|
|
963
|
+
});
|
|
964
|
+
socket.ev.on("groups.upsert", (groups) => {
|
|
965
|
+
if (!current()) return;
|
|
966
|
+
for (const { id } of groups) enqueue(async () => {
|
|
967
|
+
if (current()) this.#events.emit("group.joined", { chat: chatOf(id) });
|
|
968
|
+
}, "falha ao repassar grupo novo");
|
|
969
|
+
});
|
|
970
|
+
socket.ev.on("groups.update", (updates) => {
|
|
971
|
+
if (!current()) return;
|
|
972
|
+
for (const update of updates) {
|
|
973
|
+
if (update.id) this.#groups.delete(update.id);
|
|
974
|
+
const changed = toGroupUpdated(update);
|
|
975
|
+
if (changed === null) continue;
|
|
976
|
+
enqueue(async () => {
|
|
977
|
+
if (current()) this.#events.emit("group.updated", changed);
|
|
978
|
+
}, "falha ao repassar alteração de grupo");
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
socket.ev.on("group-participants.update", (update) => {
|
|
982
|
+
if (!current()) return;
|
|
983
|
+
this.#groups.delete(update.id);
|
|
984
|
+
enqueue(async () => {
|
|
985
|
+
const { self, others } = await toParticipantEvents(update, env, contacts);
|
|
986
|
+
if (!current()) return;
|
|
987
|
+
if (self !== null) this.#events.emit(self, { chat: chatOf(update.id) });
|
|
988
|
+
if (others !== null) this.#events.emit("group.participants", others);
|
|
989
|
+
}, "falha ao converter alteração de participantes do Baileys; descartada");
|
|
990
|
+
});
|
|
991
|
+
socket.ev.on("connection.update", ({ connection, lastDisconnect, qr }) => {
|
|
992
|
+
if (!current()) return;
|
|
993
|
+
if (qr !== void 0) {
|
|
994
|
+
pairing = true;
|
|
995
|
+
if (this.#pairing === "qr") this.#events.emit("connection.qr", { qr });
|
|
996
|
+
else if (!codeRequested && !state.creds.registered) {
|
|
997
|
+
codeRequested = true;
|
|
998
|
+
this.#requestPairingCode(socket, this.#pairing.phone);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
switch (connection) {
|
|
1002
|
+
case "connecting":
|
|
1003
|
+
this.#events.emit("connection.status", { status: "connecting" });
|
|
1004
|
+
return;
|
|
1005
|
+
case "open":
|
|
1006
|
+
pairing = false;
|
|
1007
|
+
this.#self = socket.user ? toContact(socket.user) : null;
|
|
1008
|
+
this.#events.emit("connection.status", { status: "open" });
|
|
1009
|
+
return;
|
|
1010
|
+
case "close": {
|
|
1011
|
+
this.#socket = null;
|
|
1012
|
+
const error = lastDisconnect?.error;
|
|
1013
|
+
const reason = toDisconnectReason(error, { pairing });
|
|
1014
|
+
this.#events.emit("connection.status", {
|
|
1015
|
+
status: "closed",
|
|
1016
|
+
reason,
|
|
1017
|
+
error
|
|
1018
|
+
});
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
async disconnect() {
|
|
1025
|
+
this.#attempt++;
|
|
1026
|
+
this.#endSocket();
|
|
1027
|
+
this.#polls.clear();
|
|
1028
|
+
}
|
|
1029
|
+
/** Encerra o socket atual sem emitir nada: quem chamou já sabe que a conexão acabou. */
|
|
1030
|
+
#endSocket() {
|
|
1031
|
+
const socket = this.#socket;
|
|
1032
|
+
if (socket === null) return;
|
|
1033
|
+
this.#socket = null;
|
|
1034
|
+
socket.end(void 0).catch((error) => {
|
|
1035
|
+
this.#log.warn("falha ao encerrar o socket do Baileys", { err: error });
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
#normalizeEnv(socket, state, logger) {
|
|
1039
|
+
const media = {
|
|
1040
|
+
reuploadRequest: (m) => socket.updateMediaMessage(m),
|
|
1041
|
+
logger
|
|
1042
|
+
};
|
|
1043
|
+
return {
|
|
1044
|
+
get selfIds() {
|
|
1045
|
+
const me = state.creds.me;
|
|
1046
|
+
return [me?.id, me?.lid].flatMap((id) => id ? [jidNormalizedUser(id)] : []);
|
|
1047
|
+
},
|
|
1048
|
+
pnForLid: (lid) => this.#pnForLid(socket, lid),
|
|
1049
|
+
download: (raw) => downloadMediaMessage(raw, "buffer", {}, media),
|
|
1050
|
+
stream: async (raw) => Readable.toWeb(await downloadMediaMessage(raw, "stream", {}, media))
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
#pnForLid(socket, lid) {
|
|
1054
|
+
return socket.signalRepository.lidMapping.getPNForLID(lid).catch((error) => {
|
|
1055
|
+
this.#log.warn("falha ao resolver o telefone de um LID", {
|
|
1056
|
+
err: error,
|
|
1057
|
+
lid
|
|
1058
|
+
});
|
|
1059
|
+
return null;
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
#requestPairingCode(socket, phone) {
|
|
1063
|
+
socket.requestPairingCode(phone).then((code) => {
|
|
1064
|
+
if (this.#socket === socket) this.#events.emit("connection.pairing-code", { code });
|
|
1065
|
+
}, (error) => {
|
|
1066
|
+
this.#log.error("falha ao pedir o código de pareamento", { err: error });
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
async send(chatId, content, options) {
|
|
1070
|
+
const sent = await this.#connected().sendMessage(chatId, toContent(content, options), options?.quoted ? { quoted: toQuoted(options.quoted) } : void 0);
|
|
1071
|
+
const id = sent?.key.id;
|
|
1072
|
+
if (!id) throw new Error("baileys: o envio não devolveu a chave da mensagem");
|
|
1073
|
+
if (content.type === "poll" && sent) this.#polls.remember(sent);
|
|
1074
|
+
return {
|
|
1075
|
+
chatId,
|
|
1076
|
+
id,
|
|
1077
|
+
fromMe: true,
|
|
1078
|
+
senderId: isJidGroup(chatId) ? this.#self?.id ?? null : null
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
async react(key, emoji) {
|
|
1082
|
+
await this.#connected().sendMessage(key.chatId, { react: {
|
|
1083
|
+
text: emoji ?? "",
|
|
1084
|
+
key: toWAKey(key)
|
|
1085
|
+
} });
|
|
1086
|
+
}
|
|
1087
|
+
async edit(key, text, formatted) {
|
|
1088
|
+
if (!formatted) {
|
|
1089
|
+
await this.#connected().sendMessage(key.chatId, {
|
|
1090
|
+
text,
|
|
1091
|
+
edit: toWAKey(key)
|
|
1092
|
+
});
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const rendered = renderWhatsApp(formatted);
|
|
1096
|
+
await this.#connected().sendMessage(key.chatId, {
|
|
1097
|
+
text: rendered.text,
|
|
1098
|
+
edit: toWAKey(key),
|
|
1099
|
+
...rendered.mentions.length > 0 && { mentions: [...rendered.mentions] }
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
async delete(key) {
|
|
1103
|
+
await this.#connected().sendMessage(key.chatId, { delete: toWAKey(key) });
|
|
1104
|
+
}
|
|
1105
|
+
async sendTyping(chatId, kind) {
|
|
1106
|
+
await this.#connected().sendPresenceUpdate(kind === "voice" ? "recording" : "composing", chatId);
|
|
1107
|
+
}
|
|
1108
|
+
async getGroupMetadata(groupId) {
|
|
1109
|
+
const socket = this.#connected();
|
|
1110
|
+
let native = this.#groups.get(groupId);
|
|
1111
|
+
if (native === void 0) {
|
|
1112
|
+
const fetching = socket.groupMetadata(groupId);
|
|
1113
|
+
native = fetching;
|
|
1114
|
+
this.#groups.set(groupId, fetching);
|
|
1115
|
+
fetching.catch(() => {
|
|
1116
|
+
if (this.#groups.get(groupId) === fetching) this.#groups.delete(groupId);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
return toGroupMetadata(await native, { pnForLid: (lid) => this.#pnForLid(socket, lid) });
|
|
1120
|
+
}
|
|
1121
|
+
async updateGroupParticipants(groupId, participantIds, action) {
|
|
1122
|
+
const results = await this.#connected().groupParticipantsUpdate(groupId, [...participantIds], action);
|
|
1123
|
+
this.#groups.delete(groupId);
|
|
1124
|
+
const failed = results.filter((r) => r.status !== "200");
|
|
1125
|
+
if (failed.length > 0) {
|
|
1126
|
+
const list = failed.map((r) => `${r.jid ?? "?"} (${r.status})`).join(", ");
|
|
1127
|
+
throw new Error(`baileys: ${action} recusado para ${list}`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/** Socket da conexão atual; sem ele, a ação falha na hora em vez de esperar. */
|
|
1131
|
+
#connected() {
|
|
1132
|
+
if (this.#socket === null) throw new Error("baileys: sem conexão");
|
|
1133
|
+
return this.#socket;
|
|
1134
|
+
}
|
|
1135
|
+
/** Para o envio em grupo: usa a consulta que já existe, sem disparar outra. */
|
|
1136
|
+
async #cachedGroup(jid) {
|
|
1137
|
+
return this.#groups.get(jid)?.catch(() => void 0);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
/** A própria sessão: o `id` do Baileys traz o aparelho (`5511…:12@s.whatsapp.net`). */
|
|
1141
|
+
function toContact(user) {
|
|
1142
|
+
const id = jidNormalizedUser(user.id);
|
|
1143
|
+
const jid = jidDecode(id);
|
|
1144
|
+
const phone = jid?.server === "s.whatsapp.net" ? jid.user : jidDecode(user.phoneNumber)?.user ?? null;
|
|
1145
|
+
return {
|
|
1146
|
+
id,
|
|
1147
|
+
name: user.name ?? user.notify ?? null,
|
|
1148
|
+
phone
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region src/index.ts
|
|
1153
|
+
function baileys(options = {}) {
|
|
1154
|
+
const pairing = options.pairing ?? "qr";
|
|
1155
|
+
if (pairing !== "qr" && !/^\d{8,15}$/.test(pairing.phone)) throw new TypeError(`baileys: pairing.phone deve ter só dígitos, com DDI (ex.: 5511999999999); recebido "${pairing.phone}"`);
|
|
1156
|
+
return (deps) => new BaileysTransport({
|
|
1157
|
+
pairing,
|
|
1158
|
+
driver: driver(options.version, deps)
|
|
1159
|
+
}, deps);
|
|
1160
|
+
}
|
|
1161
|
+
function driver(fixed, deps) {
|
|
1162
|
+
let latest;
|
|
1163
|
+
return {
|
|
1164
|
+
makeSocket: ({ auth, logger, version, cachedGroupMetadata }) => makeWASocket({
|
|
1165
|
+
auth,
|
|
1166
|
+
logger,
|
|
1167
|
+
version,
|
|
1168
|
+
cachedGroupMetadata,
|
|
1169
|
+
browser: Browsers.ubuntu("Chrome")
|
|
1170
|
+
}),
|
|
1171
|
+
version: () => {
|
|
1172
|
+
if (fixed) return Promise.resolve(fixed);
|
|
1173
|
+
latest ??= fetchLatestBaileysVersion().then(({ version, error }) => {
|
|
1174
|
+
if (error !== void 0) deps.log.warn("não deu para buscar a versão do WhatsApp Web; usando a do Baileys", { err: error });
|
|
1175
|
+
return version;
|
|
1176
|
+
});
|
|
1177
|
+
return latest;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
//#endregion
|
|
1182
|
+
export { baileys };
|
|
1183
|
+
|
|
1184
|
+
//# sourceMappingURL=index.mjs.map
|