import { randomUUID } from "node:crypto"; import { DisconnectReason } from "@whiskeysockets/baileys"; import { loadConfig } from "../config/config.js"; import { danger, info, success } from "../globals.js"; import { logInfo } from "../logger.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { resolveWhatsAppAccount } from "./accounts.js"; import { renderQrPngBase64 } from "./qr-image.js"; import { createWaSocket, formatError, getStatusCode, logoutWeb, readWebSelfId, waitForWaConnection, webAuthExists, } from "./session.js"; type WaSocket = Awaited>; type ActiveLogin = { accountId: string; authDir: string; isLegacyAuthDir: boolean; id: string; sock: WaSocket; startedAt: number; qr?: string; qrDataUrl?: string; connected: boolean; error?: string; errorStatus?: number; waitPromise: Promise; restartAttempted: boolean; verbose: boolean; }; const ACTIVE_LOGIN_TTL_MS = 3 * 60_000; const activeLogins = new Map(); function closeSocket(sock: WaSocket) { try { sock.ws?.close(); } catch { // ignore } } async function resetActiveLogin(accountId: string, reason?: string) { const login = activeLogins.get(accountId); if (login) { closeSocket(login.sock); activeLogins.delete(accountId); } if (reason) { logInfo(reason); } } function isLoginFresh(login: ActiveLogin) { return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS; } function attachLoginWaiter(accountId: string, login: ActiveLogin) { login.waitPromise = waitForWaConnection(login.sock) .then(() => { const current = activeLogins.get(accountId); if (current?.id === login.id) { current.connected = true; } }) .catch((err) => { const current = activeLogins.get(accountId); if (current?.id !== login.id) { return; } current.error = formatError(err); current.errorStatus = getStatusCode(err); }); } async function restartLoginSocket(login: ActiveLogin, runtime: RuntimeEnv) { if (login.restartAttempted) { return false; } login.restartAttempted = true; runtime.log( info("WhatsApp asked for a restart after pairing (code 515); retrying connection once…"), ); closeSocket(login.sock); try { const sock = await createWaSocket(false, login.verbose, { authDir: login.authDir, }); login.sock = sock; login.connected = false; login.error = undefined; login.errorStatus = undefined; attachLoginWaiter(login.accountId, login); return true; } catch (err) { login.error = formatError(err); login.errorStatus = getStatusCode(err); return false; } } export async function startWebLoginWithQr( opts: { verbose?: boolean; timeoutMs?: number; force?: boolean; accountId?: string; runtime?: RuntimeEnv; } = {}, ): Promise<{ qrDataUrl?: string; message: string }> { const runtime = opts.runtime ?? defaultRuntime; const cfg = loadConfig(); const account = resolveWhatsAppAccount({ cfg, accountId: opts.accountId }); const hasWeb = await webAuthExists(account.authDir); const selfId = readWebSelfId(account.authDir); if (hasWeb && !opts.force) { const who = selfId.e164 ?? selfId.jid ?? "unknown"; return { message: `WhatsApp is already linked (${who}). Say “relink” if you want a fresh QR.`, }; } const existing = activeLogins.get(account.accountId); if (existing && isLoginFresh(existing) && existing.qrDataUrl) { return { qrDataUrl: existing.qrDataUrl, message: "QR already active. Scan it in WhatsApp → Linked Devices.", }; } await resetActiveLogin(account.accountId); let resolveQr: ((qr: string) => void) | null = null; let rejectQr: ((err: Error) => void) | null = null; const qrPromise = new Promise((resolve, reject) => { resolveQr = resolve; rejectQr = reject; }); const qrTimer = setTimeout( () => { rejectQr?.(new Error("Timed out waiting for WhatsApp QR")); }, Math.max(opts.timeoutMs ?? 30_000, 5000), ); let sock: WaSocket; let pendingQr: string | null = null; try { sock = await createWaSocket(false, Boolean(opts.verbose), { authDir: account.authDir, onQr: (qr: string) => { if (pendingQr) { return; } pendingQr = qr; const current = activeLogins.get(account.accountId); if (current && !current.qr) { current.qr = qr; } clearTimeout(qrTimer); runtime.log(info("WhatsApp QR received.")); resolveQr?.(qr); }, }); } catch (err) { clearTimeout(qrTimer); await resetActiveLogin(account.accountId); return { message: `Failed to start WhatsApp login: ${String(err)}`, }; } const login: ActiveLogin = { accountId: account.accountId, authDir: account.authDir, isLegacyAuthDir: account.isLegacyAuthDir, id: randomUUID(), sock, startedAt: Date.now(), connected: false, waitPromise: Promise.resolve(), restartAttempted: false, verbose: Boolean(opts.verbose), }; activeLogins.set(account.accountId, login); if (pendingQr && !login.qr) { login.qr = pendingQr; } attachLoginWaiter(account.accountId, login); let qr: string; try { qr = await qrPromise; } catch (err) { clearTimeout(qrTimer); await resetActiveLogin(account.accountId); return { message: `Failed to get QR: ${String(err)}`, }; } const base64 = await renderQrPngBase64(qr); login.qrDataUrl = `data:image/png;base64,${base64}`; return { qrDataUrl: login.qrDataUrl, message: "Scan this QR in WhatsApp → Linked Devices.", }; } export async function waitForWebLogin( opts: { timeoutMs?: number; runtime?: RuntimeEnv; accountId?: string } = {}, ): Promise<{ connected: boolean; message: string }> { const runtime = opts.runtime ?? defaultRuntime; const cfg = loadConfig(); const account = resolveWhatsAppAccount({ cfg, accountId: opts.accountId }); const activeLogin = activeLogins.get(account.accountId); if (!activeLogin) { return { connected: false, message: "No active WhatsApp login in progress.", }; } const login = activeLogin; if (!isLoginFresh(login)) { await resetActiveLogin(account.accountId); return { connected: false, message: "The login QR expired. Ask me to generate a new one.", }; } const timeoutMs = Math.max(opts.timeoutMs ?? 120_000, 1000); const deadline = Date.now() + timeoutMs; while (true) { const remaining = deadline - Date.now(); if (remaining <= 0) { return { connected: false, message: "Still waiting for the QR scan. Let me know when you’ve scanned it.", }; } const timeout = new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), remaining), ); const result = await Promise.race([login.waitPromise.then(() => "done"), timeout]); if (result === "timeout") { return { connected: false, message: "Still waiting for the QR scan. Let me know when you’ve scanned it.", }; } if (login.error) { if (login.errorStatus === DisconnectReason.loggedOut) { await logoutWeb({ authDir: login.authDir, isLegacyAuthDir: login.isLegacyAuthDir, runtime, }); const message = "WhatsApp reported the session is logged out. Cleared cached web session; please scan a new QR."; await resetActiveLogin(account.accountId, message); runtime.log(danger(message)); return { connected: false, message }; } if (login.errorStatus === 515) { const restarted = await restartLoginSocket(login, runtime); if (restarted && isLoginFresh(login)) { continue; } } const message = `WhatsApp login failed: ${login.error}`; await resetActiveLogin(account.accountId, message); runtime.log(danger(message)); return { connected: false, message }; } if (login.connected) { const message = "✅ Linked! WhatsApp is ready."; runtime.log(success(message)); await resetActiveLogin(account.accountId); return { connected: true, message }; } return { connected: false, message: "Login ended without a connection." }; } }