whatsappd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/bin/whatsappd.js +18 -0
- package/dist/adapter-BES4PRA9.mjs +1778 -0
- package/dist/adapters/eve.d.mts +83 -0
- package/dist/adapters/eve.mjs +181 -0
- package/dist/index.d.mts +283 -0
- package/dist/index.mjs +15 -0
- package/dist/ports-DeTIMztA.d.mts +60 -0
- package/dist/sidecar/index.d.mts +47 -0
- package/dist/sidecar/index.mjs +326 -0
- package/dist/stores/libsql.d.mts +16 -0
- package/dist/stores/libsql.mjs +57 -0
- package/dist/stores/memory.d.mts +6 -0
- package/dist/stores/memory.mjs +18 -0
- package/dist/tools/index.d.mts +99 -0
- package/dist/tools/index.mjs +117 -0
- package/dist/types-B8d1OyHV.d.mts +71 -0
- package/dist/update-Bi5ZPUjP.d.mts +341 -0
- package/dist/wire-Bx6OmkxC.d.mts +135 -0
- package/dist/wire-CsVVkLhn.mjs +122 -0
- package/package.json +98 -0
|
@@ -0,0 +1,1778 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
import makeWASocket, { Browsers, BufferJSON, DisconnectReason, downloadMediaMessage, fetchLatestBaileysVersion, getContentType, initAuthCreds, isJidGroup, makeCacheableSignalKeyStore, normalizeMessageContent, proto } from "baileys";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
//#region src/errors.ts
|
|
7
|
+
const DISPOSITION = {
|
|
8
|
+
restart_required: "retryable",
|
|
9
|
+
connection_lost: "retryable",
|
|
10
|
+
timed_out: "retryable",
|
|
11
|
+
service_unavailable: "retryable",
|
|
12
|
+
unknown: "retryable",
|
|
13
|
+
logged_out_remote: "logged_out",
|
|
14
|
+
connection_replaced: "logged_out",
|
|
15
|
+
pairing_rejected: "logged_out",
|
|
16
|
+
credentials_invalid: "suspended",
|
|
17
|
+
multidevice_mismatch: "suspended",
|
|
18
|
+
bad_session: "suspended",
|
|
19
|
+
intentional: "retryable"
|
|
20
|
+
};
|
|
21
|
+
/** Which sink this reason lands in. */
|
|
22
|
+
function dispositionFor(reason) {
|
|
23
|
+
return DISPOSITION[reason];
|
|
24
|
+
}
|
|
25
|
+
/** Reconnect only what is genuinely retryable. No fix-by-retry. */
|
|
26
|
+
function isRetryable(reason) {
|
|
27
|
+
if (reason === "intentional") return false;
|
|
28
|
+
return dispositionFor(reason) === "retryable";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Classify a disconnect into a sanitized domain {@link WhatsAppFault}.
|
|
32
|
+
*
|
|
33
|
+
* @param error - The transport error that closed the connection, or a falsy
|
|
34
|
+
* value for an intentional teardown. The raw payload is never surfaced on the
|
|
35
|
+
* result — only its mapped status code.
|
|
36
|
+
* @param intentional - `true` when the caller closed the connection on purpose;
|
|
37
|
+
* such a close is never treated as a fault.
|
|
38
|
+
* @returns The classified fault, including its {@link Disposition}.
|
|
39
|
+
*/
|
|
40
|
+
function classifyDisconnect(error, intentional) {
|
|
41
|
+
if (intentional) return {
|
|
42
|
+
reason: "intentional",
|
|
43
|
+
retryable: false,
|
|
44
|
+
disposition: "retryable"
|
|
45
|
+
};
|
|
46
|
+
const statusCode = error?.output?.statusCode;
|
|
47
|
+
const reason = reasonFromStatus(statusCode);
|
|
48
|
+
return {
|
|
49
|
+
reason,
|
|
50
|
+
statusCode,
|
|
51
|
+
retryable: isRetryable(reason),
|
|
52
|
+
disposition: dispositionFor(reason)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function reasonFromStatus(statusCode) {
|
|
56
|
+
switch (statusCode) {
|
|
57
|
+
case DisconnectReason.restartRequired: return "restart_required";
|
|
58
|
+
case DisconnectReason.connectionClosed:
|
|
59
|
+
case DisconnectReason.timedOut: return "connection_lost";
|
|
60
|
+
case DisconnectReason.unavailableService: return "service_unavailable";
|
|
61
|
+
case DisconnectReason.loggedOut: return "logged_out_remote";
|
|
62
|
+
case DisconnectReason.connectionReplaced: return "connection_replaced";
|
|
63
|
+
case DisconnectReason.forbidden: return "credentials_invalid";
|
|
64
|
+
case DisconnectReason.multideviceMismatch: return "multidevice_mismatch";
|
|
65
|
+
case DisconnectReason.badSession: return "bad_session";
|
|
66
|
+
default: return "unknown";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A tagged error for the pairing flow. Carries only safe metadata (the reason
|
|
71
|
+
* and an optional status code); the underlying error is logged, never attached.
|
|
72
|
+
*/
|
|
73
|
+
var PairingError = class extends Error {
|
|
74
|
+
_tag = "PairingError";
|
|
75
|
+
reason;
|
|
76
|
+
statusCode;
|
|
77
|
+
constructor(reason, statusCode) {
|
|
78
|
+
super(`PairingError(${reason}${statusCode ? `, ${statusCode}` : ""})`);
|
|
79
|
+
this.name = "PairingError";
|
|
80
|
+
this.reason = reason;
|
|
81
|
+
this.statusCode = statusCode;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/** Validate E.164 at the edge so bad input fails loudly before reaching Baileys. */
|
|
85
|
+
function assertE164(input) {
|
|
86
|
+
if (!/^\+[1-9]\d{7,14}$/.test(input)) throw new PairingError("invalid_phone");
|
|
87
|
+
return input;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/pacer.ts
|
|
91
|
+
/**
|
|
92
|
+
* Send pacing. WhatsApp can flag accounts that emit bursts of messages, so
|
|
93
|
+
* outbound sends are funnelled through a FIFO queue that guarantees a minimum
|
|
94
|
+
* gap between them. Serializing also gives a caller firing many sends
|
|
95
|
+
* concurrently a predictable order rather than a thundering herd.
|
|
96
|
+
*
|
|
97
|
+
* Pure timing mechanism. A `minGapMs` of `0` or less disables the wait (each
|
|
98
|
+
* call still chains in order).
|
|
99
|
+
*/
|
|
100
|
+
const delay$1 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
101
|
+
function createPacer(minGapMs, now = Date.now) {
|
|
102
|
+
let last = -Infinity;
|
|
103
|
+
let tail = Promise.resolve();
|
|
104
|
+
return { run(fn) {
|
|
105
|
+
const result = tail.then(async () => {
|
|
106
|
+
const wait = minGapMs > 0 ? last + minGapMs - now() : 0;
|
|
107
|
+
if (wait > 0) await delay$1(wait);
|
|
108
|
+
last = now();
|
|
109
|
+
return fn();
|
|
110
|
+
});
|
|
111
|
+
tail = result.catch(() => {});
|
|
112
|
+
return result;
|
|
113
|
+
} };
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/machine.ts
|
|
117
|
+
const initialState = { phase: "disconnected" };
|
|
118
|
+
/** Exponential backoff with cap. Pure: caller supplies `now`. */
|
|
119
|
+
function backoffDelay(attempt, ctx) {
|
|
120
|
+
const base = ctx.reconnectBaseMs ?? 1e3;
|
|
121
|
+
const max = ctx.reconnectMaxMs ?? 3e4;
|
|
122
|
+
return Math.min(max, base * 2 ** attempt);
|
|
123
|
+
}
|
|
124
|
+
/** Route a terminal/retry close into the next state. */
|
|
125
|
+
function onClose(state, fault, ctx, now, attempt) {
|
|
126
|
+
if (fault.reason === "intentional") return { phase: "disconnected" };
|
|
127
|
+
switch (dispositionFor(fault.reason)) {
|
|
128
|
+
case "logged_out": return {
|
|
129
|
+
phase: "logged_out",
|
|
130
|
+
reason: fault.reason
|
|
131
|
+
};
|
|
132
|
+
case "suspended": return {
|
|
133
|
+
phase: "suspended",
|
|
134
|
+
reason: fault.reason
|
|
135
|
+
};
|
|
136
|
+
case "retryable": return enterBackoff(fault.reason, ctx, now, attempt);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function enterBackoff(reason, ctx, now, attempt) {
|
|
140
|
+
return {
|
|
141
|
+
phase: "backing_off",
|
|
142
|
+
reason,
|
|
143
|
+
retryAttempt: attempt,
|
|
144
|
+
nextRetryAt: now + backoffDelay(attempt, ctx)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The reducer. Unhandled (state, input) pairs are no-ops — the machine simply
|
|
149
|
+
* stays put, so out-of-order or duplicate inputs can never push it into an
|
|
150
|
+
* invalid state.
|
|
151
|
+
*/
|
|
152
|
+
function transition(state, input, ctx, now) {
|
|
153
|
+
if (input.t === "stop") return state.phase === "logged_out" || state.phase === "suspended" ? state : { phase: "disconnected" };
|
|
154
|
+
if (input.t === "close") {
|
|
155
|
+
if (state.phase === "logged_out" || state.phase === "suspended") return state;
|
|
156
|
+
if (state.phase === "pairing" && state.pairing.step === "restart_pending") return dispositionFor(input.fault.reason) === "retryable" ? {
|
|
157
|
+
phase: "connecting",
|
|
158
|
+
retryAttempt: 0
|
|
159
|
+
} : onClose(state, input.fault, ctx, now, 0);
|
|
160
|
+
const attempt = state.phase === "connecting" ? state.retryAttempt ?? 0 : 0;
|
|
161
|
+
return onClose(state, input.fault, ctx, now, attempt);
|
|
162
|
+
}
|
|
163
|
+
switch (state.phase) {
|
|
164
|
+
case "disconnected":
|
|
165
|
+
if (input.t === "start") return {
|
|
166
|
+
phase: "connecting",
|
|
167
|
+
retryAttempt: 0
|
|
168
|
+
};
|
|
169
|
+
return state;
|
|
170
|
+
case "connecting":
|
|
171
|
+
if (input.t === "open") return {
|
|
172
|
+
phase: "authenticated",
|
|
173
|
+
sync: { step: "draining" }
|
|
174
|
+
};
|
|
175
|
+
if (input.t === "code_ready" && ctx.method === "pairing_code") return {
|
|
176
|
+
phase: "pairing",
|
|
177
|
+
pairing: {
|
|
178
|
+
step: "challenge_live",
|
|
179
|
+
method: "pairing_code",
|
|
180
|
+
code: input.code,
|
|
181
|
+
expiresAt: input.expiresAt
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
if (input.t === "ready") return ctx.method === "qr" ? {
|
|
185
|
+
phase: "pairing",
|
|
186
|
+
pairing: {
|
|
187
|
+
step: "challenge_live",
|
|
188
|
+
method: "qr",
|
|
189
|
+
qr: input.qr,
|
|
190
|
+
expiresAt: input.expiresAt
|
|
191
|
+
}
|
|
192
|
+
} : {
|
|
193
|
+
phase: "pairing",
|
|
194
|
+
pairing: { step: "awaiting_ready" }
|
|
195
|
+
};
|
|
196
|
+
return state;
|
|
197
|
+
case "pairing": {
|
|
198
|
+
const p = state.pairing;
|
|
199
|
+
if (input.t === "paired") return {
|
|
200
|
+
phase: "pairing",
|
|
201
|
+
pairing: { step: "restart_pending" }
|
|
202
|
+
};
|
|
203
|
+
if (p.step === "awaiting_ready") {
|
|
204
|
+
if (input.t === "code_ready") return {
|
|
205
|
+
phase: "pairing",
|
|
206
|
+
pairing: {
|
|
207
|
+
step: "challenge_live",
|
|
208
|
+
method: "pairing_code",
|
|
209
|
+
code: input.code,
|
|
210
|
+
expiresAt: input.expiresAt
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
return state;
|
|
214
|
+
}
|
|
215
|
+
if (p.step === "challenge_live") {
|
|
216
|
+
if (input.t === "pairing_rejected") return {
|
|
217
|
+
phase: "logged_out",
|
|
218
|
+
reason: "pairing_rejected"
|
|
219
|
+
};
|
|
220
|
+
if (input.t === "qr_refresh" && p.method === "qr") return {
|
|
221
|
+
phase: "pairing",
|
|
222
|
+
pairing: {
|
|
223
|
+
...p,
|
|
224
|
+
qr: input.qr,
|
|
225
|
+
expiresAt: input.expiresAt
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
if (input.t === "open") return {
|
|
231
|
+
phase: "authenticated",
|
|
232
|
+
sync: { step: "draining" }
|
|
233
|
+
};
|
|
234
|
+
return state;
|
|
235
|
+
}
|
|
236
|
+
case "authenticated": {
|
|
237
|
+
const s = state.sync;
|
|
238
|
+
if (input.t === "synced") return { phase: "online" };
|
|
239
|
+
if (s.step === "draining" && input.t === "pending_drained") return {
|
|
240
|
+
phase: "authenticated",
|
|
241
|
+
sync: { step: "syncing" }
|
|
242
|
+
};
|
|
243
|
+
if (input.t === "sync_progress") {
|
|
244
|
+
if (s.step === "syncing" && s.progress === input.progress) return state;
|
|
245
|
+
return {
|
|
246
|
+
phase: "authenticated",
|
|
247
|
+
sync: {
|
|
248
|
+
step: "syncing",
|
|
249
|
+
progress: input.progress
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return state;
|
|
254
|
+
}
|
|
255
|
+
case "online": return state;
|
|
256
|
+
case "backing_off":
|
|
257
|
+
if (input.t === "retry_due") return {
|
|
258
|
+
phase: "connecting",
|
|
259
|
+
retryAttempt: state.retryAttempt + 1
|
|
260
|
+
};
|
|
261
|
+
return state;
|
|
262
|
+
case "logged_out":
|
|
263
|
+
case "suspended": return state;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/model/status.ts
|
|
268
|
+
/** True once a status is terminal (the `connection` stream ends here). */
|
|
269
|
+
function isTerminal(status) {
|
|
270
|
+
return status.phase === "logged_out" || status.phase === "suspended";
|
|
271
|
+
}
|
|
272
|
+
/** True once the device is genuinely sendable. */
|
|
273
|
+
function isOnline(status) {
|
|
274
|
+
return status.phase === "online";
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/baileys/authState.ts
|
|
278
|
+
/**
|
|
279
|
+
* Builds a Baileys auth state (creds + signal key store) backed by the opaque
|
|
280
|
+
* `SessionStore` KV port. This is the ONLY place that knows both Baileys' auth
|
|
281
|
+
* shapes AND how they serialize — so no Baileys type ever crosses the port. A
|
|
282
|
+
* direct re-implementation of `useMultiFileAuthState` over `read/write` instead
|
|
283
|
+
* of files; values are BufferJSON strings, keys are `"creds"` / `"type:id"`.
|
|
284
|
+
*/
|
|
285
|
+
const keyOf = (type, id) => `${type}:${id}`;
|
|
286
|
+
async function loadAuth(store) {
|
|
287
|
+
const rawCreds = await store.read("creds");
|
|
288
|
+
const creds = rawCreds ? JSON.parse(rawCreds, BufferJSON.reviver) : initAuthCreds();
|
|
289
|
+
return {
|
|
290
|
+
creds,
|
|
291
|
+
keys: {
|
|
292
|
+
get: async (type, ids) => {
|
|
293
|
+
const out = {};
|
|
294
|
+
await Promise.all(ids.map(async (id) => {
|
|
295
|
+
const raw = await store.read(keyOf(type, id));
|
|
296
|
+
let value = raw ? JSON.parse(raw, BufferJSON.reviver) : null;
|
|
297
|
+
if (type === "app-state-sync-key" && value) value = proto.Message.AppStateSyncKeyData.fromObject(value);
|
|
298
|
+
out[id] = value;
|
|
299
|
+
}));
|
|
300
|
+
return out;
|
|
301
|
+
},
|
|
302
|
+
set: async (data) => {
|
|
303
|
+
const entries = {};
|
|
304
|
+
for (const type of Object.keys(data)) {
|
|
305
|
+
const bucket = data[type];
|
|
306
|
+
if (!bucket) continue;
|
|
307
|
+
for (const id of Object.keys(bucket)) {
|
|
308
|
+
const value = bucket[id];
|
|
309
|
+
entries[keyOf(type, id)] = value ? JSON.stringify(value, BufferJSON.replacer) : null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
await store.write(entries);
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
saveCreds: () => store.write({ creds: JSON.stringify(creds, BufferJSON.replacer) }),
|
|
316
|
+
registered: Boolean(creds.registered),
|
|
317
|
+
initialSyncComplete: Number(creds.accountSyncCounter ?? 0) > 0
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/baileys/contacts.ts
|
|
322
|
+
function text$1(value) {
|
|
323
|
+
const trimmed = value?.trim();
|
|
324
|
+
return trimmed ? trimmed : void 0;
|
|
325
|
+
}
|
|
326
|
+
function unique(values) {
|
|
327
|
+
const out = [];
|
|
328
|
+
for (const value of values) {
|
|
329
|
+
if (!value || out.includes(value)) continue;
|
|
330
|
+
out.push(value);
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
function mapContactUpdates(contacts, at = Date.now()) {
|
|
335
|
+
const out = [];
|
|
336
|
+
for (const contact of contacts) {
|
|
337
|
+
const nativeIds = unique([
|
|
338
|
+
text$1(contact.id),
|
|
339
|
+
text$1(contact.phoneNumber),
|
|
340
|
+
text$1(contact.lid)
|
|
341
|
+
]);
|
|
342
|
+
const id = nativeIds[0];
|
|
343
|
+
if (!id) continue;
|
|
344
|
+
out.push({
|
|
345
|
+
id,
|
|
346
|
+
nativeIds,
|
|
347
|
+
...text$1(contact.name) ? { displayName: text$1(contact.name) } : {},
|
|
348
|
+
...text$1(contact.notify) ? { profileName: text$1(contact.notify) } : {},
|
|
349
|
+
...text$1(contact.verifiedName) ? { verifiedName: text$1(contact.verifiedName) } : {},
|
|
350
|
+
...text$1(contact.username) ? { username: text$1(contact.username) } : {},
|
|
351
|
+
...contact.imgUrl !== void 0 ? { imgUrl: contact.imgUrl } : {},
|
|
352
|
+
...text$1(contact.status) ? { status: text$1(contact.status) } : {},
|
|
353
|
+
at
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/baileys/groups.ts
|
|
360
|
+
const participantActions = /* @__PURE__ */ new Set([
|
|
361
|
+
"add",
|
|
362
|
+
"remove",
|
|
363
|
+
"promote",
|
|
364
|
+
"demote",
|
|
365
|
+
"modify"
|
|
366
|
+
]);
|
|
367
|
+
function text(value) {
|
|
368
|
+
const trimmed = value?.trim();
|
|
369
|
+
return trimmed ? trimmed : void 0;
|
|
370
|
+
}
|
|
371
|
+
function roleOf(participant) {
|
|
372
|
+
const admin = text(participant.admin);
|
|
373
|
+
if (admin) return admin;
|
|
374
|
+
if (participant.isSuperAdmin) return "superadmin";
|
|
375
|
+
if (participant.isAdmin) return "admin";
|
|
376
|
+
}
|
|
377
|
+
function mapParticipant(participant) {
|
|
378
|
+
const id = text(participant.id);
|
|
379
|
+
if (!id) return void 0;
|
|
380
|
+
const role = roleOf(participant);
|
|
381
|
+
return {
|
|
382
|
+
id,
|
|
383
|
+
...role ? { role } : {}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function mapParticipants(participants) {
|
|
387
|
+
if (!participants) return void 0;
|
|
388
|
+
const out = [];
|
|
389
|
+
for (const participant of participants) {
|
|
390
|
+
const mapped = mapParticipant(participant);
|
|
391
|
+
if (mapped) out.push(mapped);
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
function isParticipantAction(value) {
|
|
396
|
+
return participantActions.has(value);
|
|
397
|
+
}
|
|
398
|
+
function mapGroupMetadataUpdates(groups, at = Date.now()) {
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const group of groups) {
|
|
401
|
+
const id = text(group.id);
|
|
402
|
+
if (!id) continue;
|
|
403
|
+
const participants = mapParticipants(group.participants);
|
|
404
|
+
out.push({
|
|
405
|
+
kind: "metadata",
|
|
406
|
+
id,
|
|
407
|
+
...text(group.subject) ? { subject: text(group.subject) } : {},
|
|
408
|
+
...participants ? { participants } : {},
|
|
409
|
+
at
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
function mapGroupParticipantsUpdate(update, at = Date.now()) {
|
|
415
|
+
const id = text(update.id);
|
|
416
|
+
const action = text(update.action);
|
|
417
|
+
if (!id || !action || !isParticipantAction(action)) return void 0;
|
|
418
|
+
const participants = mapParticipants(update.participants) ?? [];
|
|
419
|
+
if (participants.length === 0) return void 0;
|
|
420
|
+
return {
|
|
421
|
+
kind: "participants",
|
|
422
|
+
id,
|
|
423
|
+
action,
|
|
424
|
+
participants,
|
|
425
|
+
at
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/baileys/download.ts
|
|
430
|
+
/**
|
|
431
|
+
* Media download-handle factory. Bytes never sit in the event stream — each
|
|
432
|
+
* inbound media message carries a `download()` thunk that fetches and decrypts
|
|
433
|
+
* on demand. The thunk closes over the live socket so expired media is
|
|
434
|
+
* transparently re-uploaded via `updateMediaMessage` (the only way to recover a
|
|
435
|
+
* stale `directPath`). This is the one impure piece; the inbound mapper stays
|
|
436
|
+
* pure and just receives the factory.
|
|
437
|
+
*/
|
|
438
|
+
/** Given the live socket, produce a per-message download-thunk factory. */
|
|
439
|
+
function mediaDownloader(sock, logger) {
|
|
440
|
+
return (raw) => () => downloadMediaMessage(raw, "buffer", {}, {
|
|
441
|
+
logger,
|
|
442
|
+
reuploadRequest: sock.updateMediaMessage
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/** Default when no socket is wired (pure tests): the handle exists but won't fetch. */
|
|
446
|
+
const noDownloader = (_raw) => {
|
|
447
|
+
return () => Promise.reject(/* @__PURE__ */ new Error("no downloader bound to this message"));
|
|
448
|
+
};
|
|
449
|
+
//#endregion
|
|
450
|
+
//#region src/baileys/inbound.ts
|
|
451
|
+
/**
|
|
452
|
+
* WAMessage → InboundMessage. The ONLY file that reads the message proto.
|
|
453
|
+
*
|
|
454
|
+
* Detection follows Baileys exactly: `normalizeMessageContent` unwraps the
|
|
455
|
+
* wrapper layers (viewOnce / ephemeral / edited / documentWithCaption), then
|
|
456
|
+
* `getContentType` names the variant. We peel the wrappers ourselves first to
|
|
457
|
+
* keep the flags, then map the inner content. Anything we don't model becomes
|
|
458
|
+
* `unsupported` — a message is never dropped or thrown on.
|
|
459
|
+
*/
|
|
460
|
+
/** Long | number | null → number. */
|
|
461
|
+
function num$1(v) {
|
|
462
|
+
if (v == null) return void 0;
|
|
463
|
+
return typeof v === "number" ? v : v.toNumber();
|
|
464
|
+
}
|
|
465
|
+
function toMillis(ts) {
|
|
466
|
+
const s = num$1(ts);
|
|
467
|
+
return s == null ? 0 : s * 1e3;
|
|
468
|
+
}
|
|
469
|
+
/** Peel the known wrapper layers, recording flags as we go. */
|
|
470
|
+
function unwrapFlags(message) {
|
|
471
|
+
const flags = {};
|
|
472
|
+
let m = message;
|
|
473
|
+
for (let i = 0; m && i < 5; i++) if (m.ephemeralMessage) {
|
|
474
|
+
flags.ephemeral = true;
|
|
475
|
+
m = m.ephemeralMessage.message;
|
|
476
|
+
} else if (m.viewOnceMessage ?? m.viewOnceMessageV2 ?? m.viewOnceMessageV2Extension) {
|
|
477
|
+
flags.viewOnce = true;
|
|
478
|
+
m = (m.viewOnceMessage ?? m.viewOnceMessageV2 ?? m.viewOnceMessageV2Extension)?.message;
|
|
479
|
+
} else if (m.editedMessage) {
|
|
480
|
+
flags.edited = true;
|
|
481
|
+
m = m.editedMessage.message;
|
|
482
|
+
} else if (m.documentWithCaptionMessage) m = m.documentWithCaptionMessage.message;
|
|
483
|
+
else break;
|
|
484
|
+
return flags;
|
|
485
|
+
}
|
|
486
|
+
function context(content) {
|
|
487
|
+
const ci = content.imageMessage?.contextInfo ?? content.videoMessage?.contextInfo ?? content.audioMessage?.contextInfo ?? content.documentMessage?.contextInfo ?? content.stickerMessage?.contextInfo ?? content.extendedTextMessage?.contextInfo ?? content.locationMessage?.contextInfo ?? content.contactMessage?.contextInfo;
|
|
488
|
+
if (!ci) return void 0;
|
|
489
|
+
const quoted = ci.stanzaId ? {
|
|
490
|
+
id: ci.stanzaId,
|
|
491
|
+
from: ci.participant ?? ci.remoteJid ?? ""
|
|
492
|
+
} : void 0;
|
|
493
|
+
const mentions = ci.mentionedJid && ci.mentionedJid.length > 0 ? ci.mentionedJid : void 0;
|
|
494
|
+
if (!quoted && !mentions) return void 0;
|
|
495
|
+
return {
|
|
496
|
+
...quoted && { quoted },
|
|
497
|
+
...mentions && { mentions }
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function media(m) {
|
|
501
|
+
return {
|
|
502
|
+
...m.mimetype != null && { mimetype: m.mimetype },
|
|
503
|
+
...num$1(m.fileLength) != null && { fileLength: num$1(m.fileLength) },
|
|
504
|
+
...m.fileName != null && { fileName: m.fileName },
|
|
505
|
+
...m.seconds != null && { seconds: m.seconds },
|
|
506
|
+
...m.ptt != null && { ptt: m.ptt },
|
|
507
|
+
...m.width != null && { width: m.width },
|
|
508
|
+
...m.height != null && { height: m.height },
|
|
509
|
+
...m.caption != null && { caption: m.caption }
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** Resolve the sender's addressing mode and alternate identity. */
|
|
513
|
+
function addressing(raw) {
|
|
514
|
+
const key = raw.key;
|
|
515
|
+
const mode = key.addressingMode === "lid" ? "lid" : key.addressingMode === "pn" ? "pn" : void 0;
|
|
516
|
+
const alt = key.participantAlt || key.remoteJidAlt || void 0;
|
|
517
|
+
if (!mode && !alt) return void 0;
|
|
518
|
+
return {
|
|
519
|
+
mode: mode ?? "pn",
|
|
520
|
+
...alt && { alt }
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function toInbound(raw, live, makeDownload = noDownloader) {
|
|
524
|
+
/** Attach the on-demand byte fetcher to the media metadata. */
|
|
525
|
+
const handle = (meta) => ({
|
|
526
|
+
...meta,
|
|
527
|
+
download: makeDownload(raw)
|
|
528
|
+
});
|
|
529
|
+
const chatId = raw.key.remoteJid;
|
|
530
|
+
const id = raw.key.id;
|
|
531
|
+
if (!chatId || !id) return void 0;
|
|
532
|
+
const flags = unwrapFlags(raw.message);
|
|
533
|
+
const content = normalizeMessageContent(raw.message);
|
|
534
|
+
if (!content) return void 0;
|
|
535
|
+
const kind = getContentType(content);
|
|
536
|
+
const base = {
|
|
537
|
+
id,
|
|
538
|
+
chatId,
|
|
539
|
+
from: raw.key.participant || chatId,
|
|
540
|
+
...raw.pushName && { pushName: raw.pushName },
|
|
541
|
+
fromMe: raw.key.fromMe ?? false,
|
|
542
|
+
timestamp: toMillis(raw.messageTimestamp),
|
|
543
|
+
live,
|
|
544
|
+
isGroup: isJidGroup(chatId) ?? false,
|
|
545
|
+
...context(content) && { context: context(content) },
|
|
546
|
+
...addressing(raw) && { addressing: addressing(raw) },
|
|
547
|
+
...Object.keys(flags).length > 0 && { flags }
|
|
548
|
+
};
|
|
549
|
+
switch (kind) {
|
|
550
|
+
case "conversation": return {
|
|
551
|
+
...base,
|
|
552
|
+
kind: "text",
|
|
553
|
+
text: content.conversation ?? ""
|
|
554
|
+
};
|
|
555
|
+
case "extendedTextMessage": return {
|
|
556
|
+
...base,
|
|
557
|
+
kind: "text",
|
|
558
|
+
text: content.extendedTextMessage?.text ?? ""
|
|
559
|
+
};
|
|
560
|
+
case "imageMessage": return {
|
|
561
|
+
...base,
|
|
562
|
+
kind: "image",
|
|
563
|
+
media: handle(media(content.imageMessage)),
|
|
564
|
+
...textOf(content.imageMessage?.caption)
|
|
565
|
+
};
|
|
566
|
+
case "videoMessage": return {
|
|
567
|
+
...base,
|
|
568
|
+
kind: "video",
|
|
569
|
+
media: handle(media(content.videoMessage)),
|
|
570
|
+
...textOf(content.videoMessage?.caption)
|
|
571
|
+
};
|
|
572
|
+
case "audioMessage": return {
|
|
573
|
+
...base,
|
|
574
|
+
kind: "audio",
|
|
575
|
+
media: handle(media(content.audioMessage))
|
|
576
|
+
};
|
|
577
|
+
case "documentMessage": return {
|
|
578
|
+
...base,
|
|
579
|
+
kind: "document",
|
|
580
|
+
media: handle(media(content.documentMessage)),
|
|
581
|
+
...textOf(content.documentMessage?.caption)
|
|
582
|
+
};
|
|
583
|
+
case "stickerMessage": return {
|
|
584
|
+
...base,
|
|
585
|
+
kind: "sticker",
|
|
586
|
+
media: handle(media(content.stickerMessage))
|
|
587
|
+
};
|
|
588
|
+
case "locationMessage": {
|
|
589
|
+
const l = content.locationMessage;
|
|
590
|
+
return {
|
|
591
|
+
...base,
|
|
592
|
+
kind: "location",
|
|
593
|
+
lat: l.degreesLatitude ?? 0,
|
|
594
|
+
lng: l.degreesLongitude ?? 0,
|
|
595
|
+
...l.name != null && { name: l.name },
|
|
596
|
+
...l.address != null && { address: l.address }
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
case "contactMessage": {
|
|
600
|
+
const c = content.contactMessage;
|
|
601
|
+
return {
|
|
602
|
+
...base,
|
|
603
|
+
kind: "contacts",
|
|
604
|
+
contacts: [{
|
|
605
|
+
...c.displayName != null && { name: c.displayName },
|
|
606
|
+
vcard: c.vcard ?? ""
|
|
607
|
+
}]
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
case "contactsArrayMessage": {
|
|
611
|
+
const arr = content.contactsArrayMessage?.contacts ?? [];
|
|
612
|
+
return {
|
|
613
|
+
...base,
|
|
614
|
+
kind: "contacts",
|
|
615
|
+
contacts: arr.map((c) => ({
|
|
616
|
+
...c.displayName != null && { name: c.displayName },
|
|
617
|
+
vcard: c.vcard ?? ""
|
|
618
|
+
}))
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
case "pollCreationMessage":
|
|
622
|
+
case "pollCreationMessageV2":
|
|
623
|
+
case "pollCreationMessageV3": {
|
|
624
|
+
const p = content[kind];
|
|
625
|
+
return {
|
|
626
|
+
...base,
|
|
627
|
+
kind: "poll",
|
|
628
|
+
name: p.name ?? "",
|
|
629
|
+
options: (p.options ?? []).map((o) => o.optionName ?? ""),
|
|
630
|
+
selectableCount: p.selectableOptionsCount ?? 0
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
default: return {
|
|
634
|
+
...base,
|
|
635
|
+
kind: "unsupported",
|
|
636
|
+
rawType: kind ?? "unknown"
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const textOf = (caption) => caption != null && caption !== "" ? { text: caption } : {};
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/baileys/history.ts
|
|
643
|
+
function num(v) {
|
|
644
|
+
if (v == null) return void 0;
|
|
645
|
+
return typeof v === "number" ? v : v.toNumber();
|
|
646
|
+
}
|
|
647
|
+
function secondsToMillis(v) {
|
|
648
|
+
const seconds = num(v);
|
|
649
|
+
return seconds == null ? void 0 : seconds * 1e3;
|
|
650
|
+
}
|
|
651
|
+
function firstText(...values) {
|
|
652
|
+
return values.find((v) => typeof v === "string" && v.length > 0);
|
|
653
|
+
}
|
|
654
|
+
function toHistoryParticipants(participants) {
|
|
655
|
+
if (!participants) return void 0;
|
|
656
|
+
const out = [];
|
|
657
|
+
for (const participant of participants) {
|
|
658
|
+
if (!participant.id) continue;
|
|
659
|
+
out.push({
|
|
660
|
+
id: participant.id,
|
|
661
|
+
...participant.admin ? { role: participant.admin } : {}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
return out.length > 0 ? out : void 0;
|
|
665
|
+
}
|
|
666
|
+
function toHistoryChat(chat) {
|
|
667
|
+
if (!chat.id) return void 0;
|
|
668
|
+
const subject = firstText(chat.name, chat.displayName);
|
|
669
|
+
const lastMessageAt = secondsToMillis(chat.lastMsgTimestamp ?? chat.lastMessageRecvTimestamp ?? chat.conversationTimestamp);
|
|
670
|
+
const participants = toHistoryParticipants(chat.participants);
|
|
671
|
+
return {
|
|
672
|
+
id: chat.id,
|
|
673
|
+
...subject && { subject },
|
|
674
|
+
isGroup: isJidGroup(chat.id) ?? false,
|
|
675
|
+
...lastMessageAt != null && { lastMessageAt },
|
|
676
|
+
...participants ? { participants } : {}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function toHistoryContact(contact) {
|
|
680
|
+
if (!contact.id) return void 0;
|
|
681
|
+
const displayName = firstText(contact.name, contact.notify, contact.verifiedName, contact.username);
|
|
682
|
+
return {
|
|
683
|
+
id: contact.id,
|
|
684
|
+
...displayName && { displayName }
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function toConversationSyncBatch(payload, makeDownload = noDownloader) {
|
|
688
|
+
return {
|
|
689
|
+
chats: payload.chats.flatMap((chat) => {
|
|
690
|
+
const mapped = toHistoryChat(chat);
|
|
691
|
+
return mapped ? [mapped] : [];
|
|
692
|
+
}),
|
|
693
|
+
contacts: payload.contacts.flatMap((contact) => {
|
|
694
|
+
const mapped = toHistoryContact(contact);
|
|
695
|
+
return mapped ? [mapped] : [];
|
|
696
|
+
}),
|
|
697
|
+
messages: toConversationSyncMessages(payload.messages, makeDownload)
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function toConversationSyncMessages(messages, makeDownload = noDownloader) {
|
|
701
|
+
return messages.flatMap((raw) => {
|
|
702
|
+
const msg = toInbound(raw, false, makeDownload);
|
|
703
|
+
return msg ? [msg] : [];
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/baileys/outbound.ts
|
|
708
|
+
/**
|
|
709
|
+
* Outbound → Baileys. The pure mirror of `inbound.ts`: our `Outbound` union →
|
|
710
|
+
* `AnyMessageContent`, and `SendOptions` → `MiscMessageGenerationOptions`. The
|
|
711
|
+
* Baileys types stay sealed in this dir. Quoting needs the original `WAMessage`,
|
|
712
|
+
* which the surface must never see — so the caller passes a `MessageRef` and a
|
|
713
|
+
* `resolveQuoted` lookup (the socket's recent-message LRU) turns it back into a
|
|
714
|
+
* `WAMessage` inside this wall.
|
|
715
|
+
*/
|
|
716
|
+
const VOICE_NOTE_MIME = "audio/ogg; codecs=opus";
|
|
717
|
+
/** BinaryInput → WAMediaUpload (Buffer | {url} | {stream}). */
|
|
718
|
+
function upload(b) {
|
|
719
|
+
if (Buffer.isBuffer(b)) return b;
|
|
720
|
+
if ("url" in b) return { url: b.url };
|
|
721
|
+
return { stream: Readable.from(b.stream) };
|
|
722
|
+
}
|
|
723
|
+
/** MessageRef → WAMessageKey — plain reconstruction, no proto needed. */
|
|
724
|
+
function refToKey(ref) {
|
|
725
|
+
return {
|
|
726
|
+
remoteJid: ref.chatId,
|
|
727
|
+
id: ref.id,
|
|
728
|
+
fromMe: ref.fromMe,
|
|
729
|
+
...ref.participant && { participant: ref.participant }
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/** WAMessageKey → MessageRef — what `send` returns so the caller can edit/delete/react it. */
|
|
733
|
+
function keyToRef(key) {
|
|
734
|
+
return {
|
|
735
|
+
id: key.id ?? "",
|
|
736
|
+
chatId: key.remoteJid ?? "",
|
|
737
|
+
fromMe: key.fromMe ?? true,
|
|
738
|
+
...key.participant && { participant: key.participant }
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/** Our union → Baileys content. Pure. */
|
|
742
|
+
function toContent(out) {
|
|
743
|
+
if ("text" in out) return { text: out.text };
|
|
744
|
+
if ("image" in out) return {
|
|
745
|
+
image: upload(out.image),
|
|
746
|
+
...out.caption && { caption: out.caption }
|
|
747
|
+
};
|
|
748
|
+
if ("video" in out) return {
|
|
749
|
+
video: upload(out.video),
|
|
750
|
+
...out.caption && { caption: out.caption },
|
|
751
|
+
...out.gifPlayback && { gifPlayback: true }
|
|
752
|
+
};
|
|
753
|
+
if ("audio" in out) return {
|
|
754
|
+
audio: upload(out.audio),
|
|
755
|
+
...out.ptt && { ptt: true },
|
|
756
|
+
...out.seconds != null && { seconds: out.seconds },
|
|
757
|
+
mimetype: out.mimetype ?? (out.ptt ? VOICE_NOTE_MIME : "audio/mp4")
|
|
758
|
+
};
|
|
759
|
+
if ("document" in out) return {
|
|
760
|
+
document: upload(out.document),
|
|
761
|
+
fileName: out.fileName,
|
|
762
|
+
mimetype: out.mimetype,
|
|
763
|
+
...out.caption && { caption: out.caption }
|
|
764
|
+
};
|
|
765
|
+
if ("sticker" in out) return { sticker: upload(out.sticker) };
|
|
766
|
+
if ("location" in out) return { location: {
|
|
767
|
+
degreesLatitude: out.location.lat,
|
|
768
|
+
degreesLongitude: out.location.lng,
|
|
769
|
+
...out.location.name && { name: out.location.name },
|
|
770
|
+
...out.location.address && { address: out.location.address }
|
|
771
|
+
} };
|
|
772
|
+
if ("contacts" in out) return { contacts: {
|
|
773
|
+
...out.contacts.displayName && { displayName: out.contacts.displayName },
|
|
774
|
+
contacts: out.contacts.vcards.map((vcard) => ({ vcard }))
|
|
775
|
+
} };
|
|
776
|
+
if ("react" in out) return { react: {
|
|
777
|
+
text: out.react.emoji,
|
|
778
|
+
key: refToKey(out.react.to)
|
|
779
|
+
} };
|
|
780
|
+
if ("edit" in out) return {
|
|
781
|
+
text: out.edit.text,
|
|
782
|
+
edit: refToKey(out.edit.target)
|
|
783
|
+
};
|
|
784
|
+
return { delete: refToKey(out.delete) };
|
|
785
|
+
}
|
|
786
|
+
/** SendOptions → Baileys generation options; resolves a quote via the LRU. */
|
|
787
|
+
function toOptions(opts, resolveQuoted) {
|
|
788
|
+
if (!opts) return {};
|
|
789
|
+
const quoted = opts.quote ? resolveQuoted(opts.quote) : void 0;
|
|
790
|
+
return {
|
|
791
|
+
...quoted && { quoted },
|
|
792
|
+
...opts.mentions && opts.mentions.length > 0 && { mentions: [...opts.mentions] }
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/baileys/updates.ts
|
|
797
|
+
/**
|
|
798
|
+
* Update protos → `Update`. The companion to the inbound mapper, for the
|
|
799
|
+
* updates stream. Three socket events feed it (all confined here):
|
|
800
|
+
*
|
|
801
|
+
* - `messages.update` — multiplexed: per-message status (receipt),
|
|
802
|
+
* plus edits and revokes (both arrive rewritten
|
|
803
|
+
* into this event).
|
|
804
|
+
* - `message-receipt.update` — per-participant receipts (the group case).
|
|
805
|
+
* - `messages.reaction` — a reaction added or cleared.
|
|
806
|
+
*
|
|
807
|
+
* All pure: proto in, `Update` out. Edits reuse the inbound mapper so the new
|
|
808
|
+
* content lands in the exact same shape as anything on the inbound stream.
|
|
809
|
+
*/
|
|
810
|
+
const Status = proto.WebMessageInfo.Status;
|
|
811
|
+
/** Long | number | null → ms epoch (receipts are in seconds). */
|
|
812
|
+
function secsToMs(v) {
|
|
813
|
+
if (v == null) return void 0;
|
|
814
|
+
return (typeof v === "number" ? v : v.toNumber()) * 1e3;
|
|
815
|
+
}
|
|
816
|
+
/** WebMessageInfo.Status enum → our ladder. SERVER_ACK is "sent to server". */
|
|
817
|
+
function statusOf(s) {
|
|
818
|
+
switch (s) {
|
|
819
|
+
case Status.ERROR: return "error";
|
|
820
|
+
case Status.PENDING: return "pending";
|
|
821
|
+
case Status.SERVER_ACK: return "server_ack";
|
|
822
|
+
case Status.DELIVERY_ACK: return "delivered";
|
|
823
|
+
case Status.READ: return "read";
|
|
824
|
+
case Status.PLAYED: return "played";
|
|
825
|
+
default: return;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Map one `messages.update`. It carries three distinct things; we discriminate:
|
|
830
|
+
* revoke → update.messageStubType === REVOKE (Baileys nulls the message)
|
|
831
|
+
* edit → update.message.editedMessage present (the rewritten content)
|
|
832
|
+
* receipt → update.status is a known Status
|
|
833
|
+
* Returns undefined for updates we don't model (e.g. starred, label changes).
|
|
834
|
+
*/
|
|
835
|
+
function mapMessageUpdate(u, makeDownload = noDownloader) {
|
|
836
|
+
const ref = keyToRef(u.key);
|
|
837
|
+
const up = u.update;
|
|
838
|
+
if (up.messageStubType === proto.WebMessageInfo.StubType.REVOKE) {
|
|
839
|
+
const by = up.key?.participant ?? up.key?.remoteJid ?? void 0;
|
|
840
|
+
return {
|
|
841
|
+
kind: "revoke",
|
|
842
|
+
ref,
|
|
843
|
+
...by != null && { by }
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
const edited = up.message?.editedMessage?.message;
|
|
847
|
+
if (edited) {
|
|
848
|
+
const message = toInbound({
|
|
849
|
+
key: u.key,
|
|
850
|
+
message: edited,
|
|
851
|
+
messageTimestamp: up.messageTimestamp ?? void 0
|
|
852
|
+
}, true, makeDownload);
|
|
853
|
+
if (!message) return void 0;
|
|
854
|
+
return {
|
|
855
|
+
kind: "edit",
|
|
856
|
+
ref,
|
|
857
|
+
message
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const status = statusOf(up.status);
|
|
861
|
+
if (status) return {
|
|
862
|
+
kind: "receipt",
|
|
863
|
+
ref,
|
|
864
|
+
status
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/** Map a per-participant receipt (`message-receipt.update`, the group case). */
|
|
868
|
+
function mapReceiptUpdate(r) {
|
|
869
|
+
const ref = keyToRef(r.key);
|
|
870
|
+
const rc = r.receipt;
|
|
871
|
+
const at = secsToMs(rc.playedTimestamp) ?? secsToMs(rc.readTimestamp) ?? secsToMs(rc.receiptTimestamp);
|
|
872
|
+
const status = rc.playedTimestamp ? "played" : rc.readTimestamp ? "read" : "delivered";
|
|
873
|
+
const by = rc.userJid ?? void 0;
|
|
874
|
+
return {
|
|
875
|
+
kind: "receipt",
|
|
876
|
+
ref,
|
|
877
|
+
status,
|
|
878
|
+
...by != null && { by },
|
|
879
|
+
...at != null && { at }
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
/** Map a reaction (`messages.reaction`). Falsey `text` means it was cleared. */
|
|
883
|
+
function mapReaction(r) {
|
|
884
|
+
const ref = keyToRef(r.key);
|
|
885
|
+
const emoji = r.reaction.text || void 0;
|
|
886
|
+
const by = r.reaction.key?.participant ?? r.reaction.key?.remoteJid ?? void 0;
|
|
887
|
+
const ms = r.reaction.senderTimestampMs;
|
|
888
|
+
const at = ms == null ? void 0 : typeof ms === "number" ? ms : ms.toNumber();
|
|
889
|
+
return {
|
|
890
|
+
kind: "reaction",
|
|
891
|
+
ref,
|
|
892
|
+
removed: !emoji,
|
|
893
|
+
...emoji != null && { emoji },
|
|
894
|
+
...by != null && { by },
|
|
895
|
+
...at != null && { at }
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
//#endregion
|
|
899
|
+
//#region src/baileys/presence.ts
|
|
900
|
+
function kindOf(value) {
|
|
901
|
+
switch (value) {
|
|
902
|
+
case "composing": return "typing";
|
|
903
|
+
case "recording": return "recording";
|
|
904
|
+
case "available": return "available";
|
|
905
|
+
case "paused": return "idle";
|
|
906
|
+
case "unavailable": return "unavailable";
|
|
907
|
+
default: return;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function mapPresenceUpdate(update, at = Date.now()) {
|
|
911
|
+
const chatId = update.id ?? void 0;
|
|
912
|
+
if (!chatId || !update.presences) return [];
|
|
913
|
+
const out = [];
|
|
914
|
+
for (const [participant, presence] of Object.entries(update.presences)) {
|
|
915
|
+
const kind = kindOf(presence?.lastKnownPresence);
|
|
916
|
+
if (!kind) continue;
|
|
917
|
+
out.push({
|
|
918
|
+
chatId,
|
|
919
|
+
participant,
|
|
920
|
+
kind,
|
|
921
|
+
at
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
925
|
+
}
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/baileys/socket.ts
|
|
928
|
+
/**
|
|
929
|
+
* The single protocol-facing layer. Wraps the WhatsApp socket and turns the raw
|
|
930
|
+
* protocol into a typed `RawEvent` stream plus a few imperative verbs. Socket
|
|
931
|
+
* library types never escape this directory — that boundary is what keeps the
|
|
932
|
+
* rest of the codebase protocol-free. The session orchestrator consumes
|
|
933
|
+
* `RawEvent` and makes all decisions.
|
|
934
|
+
*/
|
|
935
|
+
/** How many recent raw messages to retain for quote resolution. */
|
|
936
|
+
const RECENT_CAP = 500;
|
|
937
|
+
/** Track recent raw messages in an LRU Map for quote/reply resolution. */
|
|
938
|
+
function rememberRecent(recent, messages) {
|
|
939
|
+
for (const m of messages) if (m.key.id) {
|
|
940
|
+
recent.set(m.key.id, m);
|
|
941
|
+
if (recent.size > RECENT_CAP) recent.delete(recent.keys().next().value);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function toMessagesUpsertEvents(payload, makeDownload = noDownloader) {
|
|
945
|
+
if (payload.type !== "notify") {
|
|
946
|
+
const sync = toConversationSyncBatch({
|
|
947
|
+
chats: [],
|
|
948
|
+
contacts: [],
|
|
949
|
+
messages: payload.messages
|
|
950
|
+
}, makeDownload);
|
|
951
|
+
return sync.messages.length > 0 ? [{
|
|
952
|
+
t: "conversation_sync",
|
|
953
|
+
sync
|
|
954
|
+
}] : [];
|
|
955
|
+
}
|
|
956
|
+
return payload.messages.flatMap((raw) => {
|
|
957
|
+
const msg = toInbound(raw, true, makeDownload);
|
|
958
|
+
return msg ? [{
|
|
959
|
+
t: "message",
|
|
960
|
+
msg
|
|
961
|
+
}] : [];
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
function toMessagingHistoryEvents(payload, makeDownload = noDownloader) {
|
|
965
|
+
const events = [];
|
|
966
|
+
const complete = payload.progress === 100;
|
|
967
|
+
if (!complete && typeof payload.progress === "number" && Number.isFinite(payload.progress)) events.push({
|
|
968
|
+
t: "conversation_sync_progress",
|
|
969
|
+
progress: payload.progress
|
|
970
|
+
});
|
|
971
|
+
const sync = toConversationSyncBatch(payload, makeDownload);
|
|
972
|
+
if (sync.chats.length > 0 || sync.contacts.length > 0 || sync.messages.length > 0) events.push({
|
|
973
|
+
t: "conversation_sync",
|
|
974
|
+
sync
|
|
975
|
+
});
|
|
976
|
+
if (complete) events.push({ t: "conversation_sync_complete" });
|
|
977
|
+
return events;
|
|
978
|
+
}
|
|
979
|
+
function toMessagingHistoryStatusEvents(payload) {
|
|
980
|
+
if (payload.syncType === proto.HistorySync.HistorySyncType.RECENT && (payload.status === "complete" || payload.status === "paused")) return [{ t: "conversation_sync_complete" }];
|
|
981
|
+
return [];
|
|
982
|
+
}
|
|
983
|
+
function historySetTelemetry(payload) {
|
|
984
|
+
const chatsWithInlineMessage = payload.chats.filter((chat) => (chat.messages?.length ?? 0) > 0).length;
|
|
985
|
+
return {
|
|
986
|
+
syncType: payload.syncType ?? null,
|
|
987
|
+
chunkOrder: payload.chunkOrder ?? null,
|
|
988
|
+
progress: payload.progress ?? null,
|
|
989
|
+
isLatest: payload.isLatest ?? null,
|
|
990
|
+
chats: payload.chats.length,
|
|
991
|
+
contacts: payload.contacts.length,
|
|
992
|
+
messages: payload.messages.length,
|
|
993
|
+
chatsWithInlineMessage,
|
|
994
|
+
chatsWithoutInlineMessage: payload.chats.length - chatsWithInlineMessage,
|
|
995
|
+
peerDataRequestSessionId: payload.peerDataRequestSessionId ?? null
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function shouldRequestFullHistoryOnOpen(auth) {
|
|
999
|
+
return auth.creds.registered === true;
|
|
1000
|
+
}
|
|
1001
|
+
const promiseWithResolvers = Promise;
|
|
1002
|
+
/** Minimal async queue: push events, await them one at a time, close to end. */
|
|
1003
|
+
var EventQueue = class {
|
|
1004
|
+
buffer = [];
|
|
1005
|
+
resolve;
|
|
1006
|
+
done = false;
|
|
1007
|
+
push(ev) {
|
|
1008
|
+
if (this.done) return;
|
|
1009
|
+
if (this.resolve) {
|
|
1010
|
+
this.resolve({
|
|
1011
|
+
value: ev,
|
|
1012
|
+
done: false
|
|
1013
|
+
});
|
|
1014
|
+
this.resolve = void 0;
|
|
1015
|
+
} else this.buffer.push(ev);
|
|
1016
|
+
}
|
|
1017
|
+
close() {
|
|
1018
|
+
this.done = true;
|
|
1019
|
+
if (this.resolve) {
|
|
1020
|
+
this.resolve({
|
|
1021
|
+
value: void 0,
|
|
1022
|
+
done: true
|
|
1023
|
+
});
|
|
1024
|
+
this.resolve = void 0;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
[Symbol.asyncIterator]() {
|
|
1028
|
+
return { next: () => {
|
|
1029
|
+
const queued = this.buffer.shift();
|
|
1030
|
+
if (queued) return Promise.resolve({
|
|
1031
|
+
value: queued,
|
|
1032
|
+
done: false
|
|
1033
|
+
});
|
|
1034
|
+
if (this.done) return Promise.resolve({
|
|
1035
|
+
value: void 0,
|
|
1036
|
+
done: true
|
|
1037
|
+
});
|
|
1038
|
+
const { promise, resolve } = promiseWithResolvers.withResolvers();
|
|
1039
|
+
this.resolve = resolve;
|
|
1040
|
+
return promise;
|
|
1041
|
+
} };
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
function summarizeLogValue(value, depth = 0) {
|
|
1045
|
+
if (value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
1046
|
+
if (typeof value === "bigint") return value.toString();
|
|
1047
|
+
if (Buffer.isBuffer(value)) return {
|
|
1048
|
+
type: "Buffer",
|
|
1049
|
+
bytes: value.length
|
|
1050
|
+
};
|
|
1051
|
+
if (Array.isArray(value)) {
|
|
1052
|
+
if (depth >= 2) return {
|
|
1053
|
+
type: "Array",
|
|
1054
|
+
length: value.length
|
|
1055
|
+
};
|
|
1056
|
+
return value.slice(0, 10).map((item) => summarizeLogValue(item, depth + 1));
|
|
1057
|
+
}
|
|
1058
|
+
if (typeof value !== "object") return String(value);
|
|
1059
|
+
if (depth >= 2) return { type: "Object" };
|
|
1060
|
+
const out = {};
|
|
1061
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1062
|
+
if (typeof child === "function") continue;
|
|
1063
|
+
out[key] = summarizeLogValue(child, depth + 1);
|
|
1064
|
+
}
|
|
1065
|
+
return out;
|
|
1066
|
+
}
|
|
1067
|
+
function disconnectTelemetry(error) {
|
|
1068
|
+
const err = error;
|
|
1069
|
+
return {
|
|
1070
|
+
name: err?.name,
|
|
1071
|
+
message: err?.message,
|
|
1072
|
+
statusCode: err?.output?.statusCode,
|
|
1073
|
+
payload: summarizeLogValue(err?.output?.payload),
|
|
1074
|
+
data: summarizeLogValue(err?.data)
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function connectionUpdateTelemetry(update) {
|
|
1078
|
+
return {
|
|
1079
|
+
connection: update.connection,
|
|
1080
|
+
hasQr: Boolean(update.qr),
|
|
1081
|
+
qrChars: typeof update.qr === "string" ? update.qr.length : void 0,
|
|
1082
|
+
isNewLogin: update.isNewLogin,
|
|
1083
|
+
receivedPendingNotifications: update.receivedPendingNotifications,
|
|
1084
|
+
lastDisconnect: update.lastDisconnect ? disconnectTelemetry(update.lastDisconnect.error) : void 0
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
async function openSocket(opts) {
|
|
1088
|
+
const { auth, saveCreds, logger } = opts;
|
|
1089
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
1090
|
+
const queue = new EventQueue();
|
|
1091
|
+
let intentional = false;
|
|
1092
|
+
const requestFullHistory = shouldRequestFullHistoryOnOpen(auth);
|
|
1093
|
+
logger.info({
|
|
1094
|
+
version,
|
|
1095
|
+
requestFullHistory,
|
|
1096
|
+
credsRegistered: auth.creds.registered === true,
|
|
1097
|
+
hasCredsMe: Boolean(auth.creds.me),
|
|
1098
|
+
browser: "macOS Desktop"
|
|
1099
|
+
}, "opening baileys socket");
|
|
1100
|
+
const sock = makeWASocket({
|
|
1101
|
+
version,
|
|
1102
|
+
logger,
|
|
1103
|
+
browser: Browsers.macOS("Desktop"),
|
|
1104
|
+
syncFullHistory: requestFullHistory,
|
|
1105
|
+
shouldSyncHistoryMessage: () => true,
|
|
1106
|
+
auth: {
|
|
1107
|
+
creds: auth.creds,
|
|
1108
|
+
keys: makeCacheableSignalKeyStore(auth.keys, logger)
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
sock.ev.on("creds.update", () => void saveCreds());
|
|
1112
|
+
const makeDownload = mediaDownloader(sock, logger);
|
|
1113
|
+
const recent = /* @__PURE__ */ new Map();
|
|
1114
|
+
const resolveQuoted = (ref) => recent.get(ref.id);
|
|
1115
|
+
sock.ev.on("messages.upsert", (payload) => {
|
|
1116
|
+
rememberRecent(recent, payload.messages);
|
|
1117
|
+
for (const event of toMessagesUpsertEvents(payload, makeDownload)) queue.push(event);
|
|
1118
|
+
});
|
|
1119
|
+
sock.ev.on("messages.update", (updates) => {
|
|
1120
|
+
for (const u of updates) {
|
|
1121
|
+
const update = mapMessageUpdate(u, makeDownload);
|
|
1122
|
+
if (update) queue.push({
|
|
1123
|
+
t: "update",
|
|
1124
|
+
update
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
sock.ev.on("message-receipt.update", (receipts) => {
|
|
1129
|
+
for (const r of receipts) queue.push({
|
|
1130
|
+
t: "update",
|
|
1131
|
+
update: mapReceiptUpdate(r)
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
sock.ev.on("messages.reaction", (reactions) => {
|
|
1135
|
+
for (const r of reactions) queue.push({
|
|
1136
|
+
t: "update",
|
|
1137
|
+
update: mapReaction(r)
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
sock.ev.on("contacts.upsert", (contacts) => {
|
|
1141
|
+
for (const contact of mapContactUpdates(contacts)) queue.push({
|
|
1142
|
+
t: "contact",
|
|
1143
|
+
contact
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
sock.ev.on("contacts.update", (contacts) => {
|
|
1147
|
+
for (const contact of mapContactUpdates(contacts)) queue.push({
|
|
1148
|
+
t: "contact",
|
|
1149
|
+
contact
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
sock.ev.on("groups.upsert", (groups) => {
|
|
1153
|
+
for (const group of mapGroupMetadataUpdates(groups)) queue.push({
|
|
1154
|
+
t: "group",
|
|
1155
|
+
group
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
sock.ev.on("groups.update", (groups) => {
|
|
1159
|
+
for (const group of mapGroupMetadataUpdates(groups)) queue.push({
|
|
1160
|
+
t: "group",
|
|
1161
|
+
group
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
sock.ev.on("group-participants.update", (update) => {
|
|
1165
|
+
const group = mapGroupParticipantsUpdate(update);
|
|
1166
|
+
if (group) queue.push({
|
|
1167
|
+
t: "group",
|
|
1168
|
+
group
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
sock.ev.on("presence.update", (update) => {
|
|
1172
|
+
for (const presence of mapPresenceUpdate(update)) queue.push({
|
|
1173
|
+
t: "presence",
|
|
1174
|
+
presence
|
|
1175
|
+
});
|
|
1176
|
+
});
|
|
1177
|
+
sock.ev.on("messaging-history.set", (payload) => {
|
|
1178
|
+
logger.info(historySetTelemetry(payload), "messaging history set");
|
|
1179
|
+
rememberRecent(recent, payload.messages);
|
|
1180
|
+
for (const event of toMessagingHistoryEvents(payload, makeDownload)) queue.push(event);
|
|
1181
|
+
});
|
|
1182
|
+
sock.ev.on("messaging-history.status", (payload) => {
|
|
1183
|
+
logger.info({
|
|
1184
|
+
syncType: payload.syncType,
|
|
1185
|
+
status: payload.status,
|
|
1186
|
+
explicit: payload.explicit
|
|
1187
|
+
}, "messaging history status");
|
|
1188
|
+
for (const event of toMessagingHistoryStatusEvents(payload)) queue.push(event);
|
|
1189
|
+
});
|
|
1190
|
+
sock.ev.on("connection.update", (u) => {
|
|
1191
|
+
logger.info(connectionUpdateTelemetry(u), "connection update");
|
|
1192
|
+
if (u.connection === "connecting") queue.push({ t: "connecting" });
|
|
1193
|
+
if (u.qr) queue.push({
|
|
1194
|
+
t: "qr",
|
|
1195
|
+
qr: u.qr
|
|
1196
|
+
});
|
|
1197
|
+
if (u.isNewLogin) queue.push({ t: "paired" });
|
|
1198
|
+
if (u.connection === "open") queue.push({ t: "open" });
|
|
1199
|
+
if (u.receivedPendingNotifications) queue.push({ t: "pending_drained" });
|
|
1200
|
+
if (u.connection === "close") {
|
|
1201
|
+
const fault = classifyDisconnect(u.lastDisconnect?.error, intentional);
|
|
1202
|
+
queue.push({
|
|
1203
|
+
t: "close",
|
|
1204
|
+
fault
|
|
1205
|
+
});
|
|
1206
|
+
queue.close();
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
return {
|
|
1210
|
+
events: queue,
|
|
1211
|
+
requestPairingCode: (digits) => sock.requestPairingCode(digits),
|
|
1212
|
+
send: async (to, out, opts) => {
|
|
1213
|
+
return keyToRef((await sock.sendMessage(to, toContent(out), toOptions(opts, resolveQuoted)))?.key ?? {
|
|
1214
|
+
remoteJid: to,
|
|
1215
|
+
fromMe: true
|
|
1216
|
+
});
|
|
1217
|
+
},
|
|
1218
|
+
markRead: (refs) => sock.readMessages(refs.map(refToKey)),
|
|
1219
|
+
setTyping: (chatId, on) => sock.sendPresenceUpdate(on ? "composing" : "paused", chatId),
|
|
1220
|
+
groupMetadata: async (chatId) => {
|
|
1221
|
+
const metadata = await sock.groupMetadata(chatId);
|
|
1222
|
+
return {
|
|
1223
|
+
id: metadata.id ?? chatId,
|
|
1224
|
+
...metadata.subject ? { subject: metadata.subject } : {},
|
|
1225
|
+
participants: metadata.participants.map((participant) => ({
|
|
1226
|
+
id: participant.id,
|
|
1227
|
+
...participant.admin ? { role: participant.admin } : {}
|
|
1228
|
+
}))
|
|
1229
|
+
};
|
|
1230
|
+
},
|
|
1231
|
+
profilePictureUrl: (jid, type) => sock.profilePictureUrl(jid, type),
|
|
1232
|
+
identity: () => {
|
|
1233
|
+
const u = sock.user;
|
|
1234
|
+
if (!u?.id) return void 0;
|
|
1235
|
+
const digits = u.id.split(/[:@]/)[0] ?? "";
|
|
1236
|
+
const phoneE164 = /^\d+$/.test(digits) ? `+${digits}` : void 0;
|
|
1237
|
+
return {
|
|
1238
|
+
jid: u.id,
|
|
1239
|
+
pushName: u.name ?? void 0,
|
|
1240
|
+
phoneE164
|
|
1241
|
+
};
|
|
1242
|
+
},
|
|
1243
|
+
end: () => {
|
|
1244
|
+
intentional = true;
|
|
1245
|
+
sock.end(void 0);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
//#endregion
|
|
1250
|
+
//#region src/session.ts
|
|
1251
|
+
/**
|
|
1252
|
+
* The session orchestrator. Feeds raw socket events through the pure connection
|
|
1253
|
+
* state machine, owns the timers the machine cannot (the pairing verdict window
|
|
1254
|
+
* and reconnect backoff), drives reconnection, and exposes the public surface:
|
|
1255
|
+
* a set of async-iterable streams plus `start`/`send`/`stop`. It makes only
|
|
1256
|
+
* timing decisions — never protocol ones.
|
|
1257
|
+
*/
|
|
1258
|
+
const QR_FIRST_MS = 6e4;
|
|
1259
|
+
const QR_REFRESH_MS = 2e4;
|
|
1260
|
+
/** Minimal single-consumer async stream. */
|
|
1261
|
+
var Stream = class {
|
|
1262
|
+
buffer = [];
|
|
1263
|
+
resolve;
|
|
1264
|
+
done = false;
|
|
1265
|
+
push(v) {
|
|
1266
|
+
if (this.done) return;
|
|
1267
|
+
if (this.resolve) {
|
|
1268
|
+
this.resolve({
|
|
1269
|
+
value: v,
|
|
1270
|
+
done: false
|
|
1271
|
+
});
|
|
1272
|
+
this.resolve = void 0;
|
|
1273
|
+
} else this.buffer.push(v);
|
|
1274
|
+
}
|
|
1275
|
+
close() {
|
|
1276
|
+
this.done = true;
|
|
1277
|
+
if (this.resolve) {
|
|
1278
|
+
this.resolve({
|
|
1279
|
+
value: void 0,
|
|
1280
|
+
done: true
|
|
1281
|
+
});
|
|
1282
|
+
this.resolve = void 0;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
[Symbol.asyncIterator]() {
|
|
1286
|
+
return { next: () => {
|
|
1287
|
+
const q = this.buffer.shift();
|
|
1288
|
+
if (q !== void 0) return Promise.resolve({
|
|
1289
|
+
value: q,
|
|
1290
|
+
done: false
|
|
1291
|
+
});
|
|
1292
|
+
if (this.done) return Promise.resolve({
|
|
1293
|
+
value: void 0,
|
|
1294
|
+
done: true
|
|
1295
|
+
});
|
|
1296
|
+
return new Promise((r) => this.resolve = r);
|
|
1297
|
+
} };
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1301
|
+
/**
|
|
1302
|
+
* Create a single WhatsApp account session.
|
|
1303
|
+
*
|
|
1304
|
+
* @remarks
|
|
1305
|
+
* The returned session is inert until {@link WhatsAppSession.start | start} is
|
|
1306
|
+
* called. Consume its streams to observe the connection lifecycle and incoming
|
|
1307
|
+
* messages; call its command methods to send and interact.
|
|
1308
|
+
*
|
|
1309
|
+
* @param config - Store, auth strategy, and optional tuning — see
|
|
1310
|
+
* {@link SessionConfig}.
|
|
1311
|
+
* @returns A not-yet-connected {@link WhatsAppSession}.
|
|
1312
|
+
*
|
|
1313
|
+
* @example
|
|
1314
|
+
* ```ts
|
|
1315
|
+
* import { createSession, qrAuth, fileStore, refOf } from "whatsappd";
|
|
1316
|
+
*
|
|
1317
|
+
* const session = createSession({
|
|
1318
|
+
* store: fileStore("./.wa-auth"),
|
|
1319
|
+
* auth: qrAuth(),
|
|
1320
|
+
* });
|
|
1321
|
+
*
|
|
1322
|
+
* for await (const status of session.connection) {
|
|
1323
|
+
* if (status.phase === "pairing" && status.pairing.step === "challenge_live") {
|
|
1324
|
+
* console.log("scan:", status.pairing.qr ?? status.pairing.code);
|
|
1325
|
+
* }
|
|
1326
|
+
* }
|
|
1327
|
+
*
|
|
1328
|
+
* await session.start();
|
|
1329
|
+
* ```
|
|
1330
|
+
*/
|
|
1331
|
+
function createSession(config) {
|
|
1332
|
+
const { store, auth } = config;
|
|
1333
|
+
const logger = config.logger ?? pino({ level: process.env.WA_LOG_LEVEL ?? "warn" });
|
|
1334
|
+
const receiveStatusBroadcast = config.receiveStatusBroadcast ?? false;
|
|
1335
|
+
const pacer = createPacer(config.sendMinGapMs ?? 1e3);
|
|
1336
|
+
const emit = (event) => {
|
|
1337
|
+
try {
|
|
1338
|
+
config.metrics?.(event);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
logger.warn({ err }, "metrics hook threw");
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
const ctx = {
|
|
1344
|
+
method: auth.method,
|
|
1345
|
+
reconnectBaseMs: config.reconnectBaseMs,
|
|
1346
|
+
reconnectMaxMs: config.reconnectMaxMs
|
|
1347
|
+
};
|
|
1348
|
+
const verdictWindowMs = config.verdictWindowMs ?? 3e4;
|
|
1349
|
+
const syncGraceMs = config.syncGraceMs ?? 3e3;
|
|
1350
|
+
const connection = new Stream();
|
|
1351
|
+
const inbound = new Stream();
|
|
1352
|
+
const conversationSync = new Stream();
|
|
1353
|
+
const updates = new Stream();
|
|
1354
|
+
const contacts = new Stream();
|
|
1355
|
+
const groups = new Stream();
|
|
1356
|
+
const presence = new Stream();
|
|
1357
|
+
let status = initialState;
|
|
1358
|
+
let stopped = false;
|
|
1359
|
+
let conn;
|
|
1360
|
+
let firstQrSeen = false;
|
|
1361
|
+
let verdictTimer;
|
|
1362
|
+
let verdictFired = false;
|
|
1363
|
+
let initialSyncComplete = false;
|
|
1364
|
+
let syncTimer;
|
|
1365
|
+
async function apply(input) {
|
|
1366
|
+
const next = transition(status, input, ctx, Date.now());
|
|
1367
|
+
if (next === status) return;
|
|
1368
|
+
emit({
|
|
1369
|
+
type: "transition",
|
|
1370
|
+
from: status.phase,
|
|
1371
|
+
to: next.phase
|
|
1372
|
+
});
|
|
1373
|
+
if (next.phase === "logged_out") await store.clear().catch(() => {});
|
|
1374
|
+
status = next;
|
|
1375
|
+
connection.push(status);
|
|
1376
|
+
if (isTerminal(status)) {
|
|
1377
|
+
connection.close();
|
|
1378
|
+
inbound.close();
|
|
1379
|
+
conversationSync.close();
|
|
1380
|
+
updates.close();
|
|
1381
|
+
contacts.close();
|
|
1382
|
+
groups.close();
|
|
1383
|
+
presence.close();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
function clearVerdict() {
|
|
1387
|
+
if (verdictTimer) clearTimeout(verdictTimer);
|
|
1388
|
+
verdictTimer = void 0;
|
|
1389
|
+
}
|
|
1390
|
+
function clearSync() {
|
|
1391
|
+
if (syncTimer) clearTimeout(syncTimer);
|
|
1392
|
+
syncTimer = void 0;
|
|
1393
|
+
}
|
|
1394
|
+
function armSync() {
|
|
1395
|
+
clearSync();
|
|
1396
|
+
syncTimer = setTimeout(() => void apply({ t: "synced" }), syncGraceMs);
|
|
1397
|
+
}
|
|
1398
|
+
function fireVerdict() {
|
|
1399
|
+
if (verdictFired) return;
|
|
1400
|
+
verdictFired = true;
|
|
1401
|
+
clearVerdict();
|
|
1402
|
+
apply({ t: "pairing_rejected" });
|
|
1403
|
+
}
|
|
1404
|
+
async function handle(ev) {
|
|
1405
|
+
switch (ev.t) {
|
|
1406
|
+
case "connecting": return;
|
|
1407
|
+
case "qr":
|
|
1408
|
+
if (!firstQrSeen) {
|
|
1409
|
+
firstQrSeen = true;
|
|
1410
|
+
await apply({
|
|
1411
|
+
t: "ready",
|
|
1412
|
+
qr: ev.qr,
|
|
1413
|
+
expiresAt: Date.now() + QR_FIRST_MS
|
|
1414
|
+
});
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (auth.method === "qr") await apply({
|
|
1418
|
+
t: "qr_refresh",
|
|
1419
|
+
qr: ev.qr,
|
|
1420
|
+
expiresAt: Date.now() + QR_REFRESH_MS
|
|
1421
|
+
});
|
|
1422
|
+
else fireVerdict();
|
|
1423
|
+
return;
|
|
1424
|
+
case "paired":
|
|
1425
|
+
clearVerdict();
|
|
1426
|
+
await apply({ t: "paired" });
|
|
1427
|
+
return;
|
|
1428
|
+
case "open":
|
|
1429
|
+
await apply({ t: "open" });
|
|
1430
|
+
if (initialSyncComplete) armSync();
|
|
1431
|
+
return;
|
|
1432
|
+
case "pending_drained":
|
|
1433
|
+
await apply({ t: "pending_drained" });
|
|
1434
|
+
if (initialSyncComplete) {
|
|
1435
|
+
clearSync();
|
|
1436
|
+
await apply({ t: "synced" });
|
|
1437
|
+
}
|
|
1438
|
+
return;
|
|
1439
|
+
case "conversation_sync_progress":
|
|
1440
|
+
await apply({
|
|
1441
|
+
t: "sync_progress",
|
|
1442
|
+
progress: ev.progress
|
|
1443
|
+
});
|
|
1444
|
+
return;
|
|
1445
|
+
case "conversation_sync_complete":
|
|
1446
|
+
clearSync();
|
|
1447
|
+
await apply({ t: "synced" });
|
|
1448
|
+
return;
|
|
1449
|
+
case "conversation_sync":
|
|
1450
|
+
conversationSync.push(ev.sync);
|
|
1451
|
+
return;
|
|
1452
|
+
case "message":
|
|
1453
|
+
if (!receiveStatusBroadcast && ev.msg.chatId === "status@broadcast") return;
|
|
1454
|
+
inbound.push(ev.msg);
|
|
1455
|
+
emit({
|
|
1456
|
+
type: "message_in",
|
|
1457
|
+
kind: ev.msg.kind,
|
|
1458
|
+
live: ev.msg.live
|
|
1459
|
+
});
|
|
1460
|
+
return;
|
|
1461
|
+
case "update":
|
|
1462
|
+
updates.push(ev.update);
|
|
1463
|
+
emit({
|
|
1464
|
+
type: "update_in",
|
|
1465
|
+
kind: ev.update.kind
|
|
1466
|
+
});
|
|
1467
|
+
return;
|
|
1468
|
+
case "contact":
|
|
1469
|
+
contacts.push(ev.contact);
|
|
1470
|
+
emit({
|
|
1471
|
+
type: "contact_in",
|
|
1472
|
+
hasDisplayName: Boolean(ev.contact.displayName),
|
|
1473
|
+
identityCount: ev.contact.nativeIds.length
|
|
1474
|
+
});
|
|
1475
|
+
return;
|
|
1476
|
+
case "group":
|
|
1477
|
+
groups.push(ev.group);
|
|
1478
|
+
emit({
|
|
1479
|
+
type: "group_in",
|
|
1480
|
+
kind: ev.group.kind
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
case "presence":
|
|
1484
|
+
presence.push(ev.presence);
|
|
1485
|
+
emit({
|
|
1486
|
+
type: "presence_in",
|
|
1487
|
+
kind: ev.presence.kind
|
|
1488
|
+
});
|
|
1489
|
+
return;
|
|
1490
|
+
case "close":
|
|
1491
|
+
clearVerdict();
|
|
1492
|
+
clearSync();
|
|
1493
|
+
await apply({
|
|
1494
|
+
t: "close",
|
|
1495
|
+
fault: ev.fault
|
|
1496
|
+
});
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async function runOnce() {
|
|
1501
|
+
firstQrSeen = false;
|
|
1502
|
+
verdictFired = false;
|
|
1503
|
+
clearVerdict();
|
|
1504
|
+
clearSync();
|
|
1505
|
+
const auth = await loadAuth(store);
|
|
1506
|
+
initialSyncComplete = auth.initialSyncComplete;
|
|
1507
|
+
conn = await openSocket({
|
|
1508
|
+
auth: {
|
|
1509
|
+
creds: auth.creds,
|
|
1510
|
+
keys: auth.keys
|
|
1511
|
+
},
|
|
1512
|
+
saveCreds: auth.saveCreds,
|
|
1513
|
+
logger
|
|
1514
|
+
});
|
|
1515
|
+
if (config.auth.method === "pairing_code" && !auth.creds.me) {
|
|
1516
|
+
await apply({
|
|
1517
|
+
t: "code_ready",
|
|
1518
|
+
code: await conn.requestPairingCode(assertE164(config.auth.phone).replace(/^\+/, "")),
|
|
1519
|
+
expiresAt: Date.now() + verdictWindowMs
|
|
1520
|
+
});
|
|
1521
|
+
verdictTimer = setTimeout(fireVerdict, verdictWindowMs);
|
|
1522
|
+
}
|
|
1523
|
+
for await (const ev of conn.events) await handle(ev);
|
|
1524
|
+
}
|
|
1525
|
+
async function supervise() {
|
|
1526
|
+
await apply({ t: "start" });
|
|
1527
|
+
while (!stopped) {
|
|
1528
|
+
await runOnce().catch(async (err) => {
|
|
1529
|
+
logger.error({ err }, "session run errored");
|
|
1530
|
+
await apply({
|
|
1531
|
+
t: "close",
|
|
1532
|
+
fault: {
|
|
1533
|
+
reason: "unknown",
|
|
1534
|
+
retryable: true,
|
|
1535
|
+
disposition: "retryable"
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
});
|
|
1539
|
+
if (isTerminal(status)) break;
|
|
1540
|
+
if (status.phase === "disconnected") break;
|
|
1541
|
+
if (status.phase === "backing_off") {
|
|
1542
|
+
const attempt = status.retryAttempt;
|
|
1543
|
+
await delay(Math.max(0, status.nextRetryAt - Date.now()));
|
|
1544
|
+
if (stopped) break;
|
|
1545
|
+
emit({
|
|
1546
|
+
type: "reconnect",
|
|
1547
|
+
attempt
|
|
1548
|
+
});
|
|
1549
|
+
await apply({ t: "retry_due" });
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
connection.close();
|
|
1553
|
+
inbound.close();
|
|
1554
|
+
conversationSync.close();
|
|
1555
|
+
updates.close();
|
|
1556
|
+
contacts.close();
|
|
1557
|
+
groups.close();
|
|
1558
|
+
presence.close();
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
get status() {
|
|
1562
|
+
return status;
|
|
1563
|
+
},
|
|
1564
|
+
connection,
|
|
1565
|
+
inbound,
|
|
1566
|
+
conversationSync,
|
|
1567
|
+
updates,
|
|
1568
|
+
contacts,
|
|
1569
|
+
groups,
|
|
1570
|
+
presence,
|
|
1571
|
+
start: supervise,
|
|
1572
|
+
async send(to, msg, opts) {
|
|
1573
|
+
if (status.phase !== "online" || !conn) throw new Error(`not online (phase: ${status.phase})`);
|
|
1574
|
+
const c = conn;
|
|
1575
|
+
const ref = await pacer.run(() => c.send(to, msg, opts));
|
|
1576
|
+
emit({ type: "message_out" });
|
|
1577
|
+
return ref;
|
|
1578
|
+
},
|
|
1579
|
+
async markRead(refs) {
|
|
1580
|
+
if (status.phase !== "online" || !conn) throw new Error(`not online (phase: ${status.phase})`);
|
|
1581
|
+
return conn.markRead(refs);
|
|
1582
|
+
},
|
|
1583
|
+
async setTyping(chatId, on) {
|
|
1584
|
+
if (status.phase !== "online" || !conn) throw new Error(`not online (phase: ${status.phase})`);
|
|
1585
|
+
return conn.setTyping(chatId, on);
|
|
1586
|
+
},
|
|
1587
|
+
async groupMetadata(chatId) {
|
|
1588
|
+
if (status.phase !== "online" || !conn) throw new Error(`not online (phase: ${status.phase})`);
|
|
1589
|
+
return conn.groupMetadata(chatId);
|
|
1590
|
+
},
|
|
1591
|
+
async profilePictureUrl(jid, type) {
|
|
1592
|
+
if (status.phase !== "online" || !conn) throw new Error(`not online (phase: ${status.phase})`);
|
|
1593
|
+
return conn.profilePictureUrl(jid, type);
|
|
1594
|
+
},
|
|
1595
|
+
identity: () => conn?.identity(),
|
|
1596
|
+
async stop() {
|
|
1597
|
+
stopped = true;
|
|
1598
|
+
clearVerdict();
|
|
1599
|
+
clearSync();
|
|
1600
|
+
conn?.end();
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
//#endregion
|
|
1605
|
+
//#region src/ports.ts
|
|
1606
|
+
/**
|
|
1607
|
+
* The two pluggable seams — where credentials live and how login happens. Both
|
|
1608
|
+
* are deliberately free of any WhatsApp-protocol types, so a custom store or a
|
|
1609
|
+
* host adapter needs no knowledge of the underlying socket library.
|
|
1610
|
+
*
|
|
1611
|
+
* @packageDocumentation
|
|
1612
|
+
*/
|
|
1613
|
+
/**
|
|
1614
|
+
* Log in by scanning a QR code shown to the user.
|
|
1615
|
+
*
|
|
1616
|
+
* @returns A QR {@link AuthStrategy}.
|
|
1617
|
+
*/
|
|
1618
|
+
function qrAuth() {
|
|
1619
|
+
return { method: "qr" };
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Log in with a pairing code delivered to a known phone number.
|
|
1623
|
+
*
|
|
1624
|
+
* @param phone - The phone number in E.164 format (e.g. `+15551234567`).
|
|
1625
|
+
* @returns A pairing-code {@link AuthStrategy}.
|
|
1626
|
+
* @throws {@link PairingError} with reason `"invalid_phone"` when `phone` is
|
|
1627
|
+
* not valid E.164 — validated here so bad input fails before any network call.
|
|
1628
|
+
*/
|
|
1629
|
+
function pairingAuth(phone) {
|
|
1630
|
+
return {
|
|
1631
|
+
method: "pairing_code",
|
|
1632
|
+
phone: assertE164(phone)
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
//#endregion
|
|
1636
|
+
//#region src/stores/file.ts
|
|
1637
|
+
/**
|
|
1638
|
+
* A file-backed {@link SessionStore} — one file per key under a directory.
|
|
1639
|
+
* Durable across restarts; a good default for a single sidecar process.
|
|
1640
|
+
*/
|
|
1641
|
+
/** Make a key safe to use as a filename. */
|
|
1642
|
+
const fileName = (key) => `${key.replace(/[^0-9A-Za-z._-]/g, "_")}.json`;
|
|
1643
|
+
function fileStore(dir) {
|
|
1644
|
+
const path = (key) => join(dir, fileName(key));
|
|
1645
|
+
let ensured = false;
|
|
1646
|
+
const ensureDir = async () => {
|
|
1647
|
+
if (ensured) return;
|
|
1648
|
+
await mkdir(dir, { recursive: true });
|
|
1649
|
+
ensured = true;
|
|
1650
|
+
};
|
|
1651
|
+
return {
|
|
1652
|
+
async read(key) {
|
|
1653
|
+
try {
|
|
1654
|
+
return await readFile(path(key), "utf-8");
|
|
1655
|
+
} catch {
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
},
|
|
1659
|
+
async write(entries) {
|
|
1660
|
+
await ensureDir();
|
|
1661
|
+
await Promise.all(Object.entries(entries).map(([key, value]) => value === null ? rm(path(key), { force: true }) : writeFile(path(key), value)));
|
|
1662
|
+
},
|
|
1663
|
+
async clear() {
|
|
1664
|
+
ensured = false;
|
|
1665
|
+
await rm(dir, {
|
|
1666
|
+
recursive: true,
|
|
1667
|
+
force: true
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
//#endregion
|
|
1673
|
+
//#region src/channel/adapter.ts
|
|
1674
|
+
/** Build a ConversationRef from an inbound message. */
|
|
1675
|
+
function refFromMessage(msg) {
|
|
1676
|
+
return {
|
|
1677
|
+
chatId: msg.chatId,
|
|
1678
|
+
isGroup: msg.isGroup,
|
|
1679
|
+
from: msg.from,
|
|
1680
|
+
pushName: msg.pushName
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
/** Build a ConversationRef from an update's ref. */
|
|
1684
|
+
function refFromUpdate(update) {
|
|
1685
|
+
const chatId = update.ref.chatId;
|
|
1686
|
+
if (!chatId) return void 0;
|
|
1687
|
+
return {
|
|
1688
|
+
chatId,
|
|
1689
|
+
isGroup: chatId.includes("@g.us"),
|
|
1690
|
+
from: update.ref.participant
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Create a `WhatsAppChannelAdapter` over one session.
|
|
1695
|
+
*
|
|
1696
|
+
* Construction does NOT connect — call `start()` to begin pumping the
|
|
1697
|
+
* session's streams and open the socket to WhatsApp.
|
|
1698
|
+
*/
|
|
1699
|
+
function createChannelAdapter(opts) {
|
|
1700
|
+
const { session } = opts;
|
|
1701
|
+
const accountId = opts.accountId ?? "default";
|
|
1702
|
+
const log = opts.logger;
|
|
1703
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
1704
|
+
let started = false;
|
|
1705
|
+
function emit(event) {
|
|
1706
|
+
for (const h of subscribers) if (h.onEvent(event) instanceof Promise);
|
|
1707
|
+
}
|
|
1708
|
+
/** Pump the session's streams into ChannelEvents (they end on stop/terminal). */
|
|
1709
|
+
function startPump() {
|
|
1710
|
+
const tasks = [
|
|
1711
|
+
(async () => {
|
|
1712
|
+
for await (const message of session.inbound) emit({
|
|
1713
|
+
type: "message",
|
|
1714
|
+
ref: refFromMessage(message),
|
|
1715
|
+
message
|
|
1716
|
+
});
|
|
1717
|
+
})(),
|
|
1718
|
+
(async () => {
|
|
1719
|
+
for await (const update of session.updates) {
|
|
1720
|
+
const ref = refFromUpdate(update);
|
|
1721
|
+
if (ref) emit({
|
|
1722
|
+
type: "update",
|
|
1723
|
+
ref,
|
|
1724
|
+
update
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
})(),
|
|
1728
|
+
(async () => {
|
|
1729
|
+
for await (const status of session.connection) emit({
|
|
1730
|
+
type: "status",
|
|
1731
|
+
accountId,
|
|
1732
|
+
status
|
|
1733
|
+
});
|
|
1734
|
+
})()
|
|
1735
|
+
];
|
|
1736
|
+
Promise.all(tasks).catch((err) => {
|
|
1737
|
+
log?.error({ err }, "channel adapter stream pump error");
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
function requireStarted() {
|
|
1741
|
+
if (!started) throw new Error(`adapter for account ${accountId} — call start() first`);
|
|
1742
|
+
return session;
|
|
1743
|
+
}
|
|
1744
|
+
return {
|
|
1745
|
+
accountId,
|
|
1746
|
+
async start() {
|
|
1747
|
+
if (started) return;
|
|
1748
|
+
started = true;
|
|
1749
|
+
startPump();
|
|
1750
|
+
await session.start();
|
|
1751
|
+
},
|
|
1752
|
+
async send(chatId, content, opts) {
|
|
1753
|
+
return requireStarted().send(chatId, content, opts);
|
|
1754
|
+
},
|
|
1755
|
+
async markRead(chatId) {
|
|
1756
|
+
await requireStarted().markRead([{
|
|
1757
|
+
id: "",
|
|
1758
|
+
chatId,
|
|
1759
|
+
fromMe: false
|
|
1760
|
+
}]);
|
|
1761
|
+
},
|
|
1762
|
+
async setTyping(chatId, kind) {
|
|
1763
|
+
const on = kind === "typing" || kind === "recording";
|
|
1764
|
+
await requireStarted().setTyping(chatId, on);
|
|
1765
|
+
},
|
|
1766
|
+
subscribe(handler) {
|
|
1767
|
+
subscribers.add(handler);
|
|
1768
|
+
return () => {
|
|
1769
|
+
subscribers.delete(handler);
|
|
1770
|
+
};
|
|
1771
|
+
},
|
|
1772
|
+
async stop() {
|
|
1773
|
+
await session.stop();
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
//#endregion
|
|
1778
|
+
export { createSession as a, PairingError as c, dispositionFor as d, isRetryable as f, qrAuth as i, assertE164 as l, fileStore as n, isOnline as o, pairingAuth as r, isTerminal as s, createChannelAdapter as t, classifyDisconnect as u };
|