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.
@@ -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 };