import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { makeWASocket, useMultiFileAuthState, fetchLatestBaileysVersion, type WASocket, type ConnectionState, DisconnectReason, Browsers, } from "@whiskeysockets/baileys"; import { logger } from "../logger.js"; import { env } from "../env.js"; import { syncGroupsForAccount } from "./group-sync.js"; export type SessionEvent = | { type: "qr"; payload: string } | { type: "open"; phoneNumber: string | undefined } // `restartRequired` is set when Baileys closes the socket with status // 515 — the normal post-pair handshake reconnect, NOT a failure. Both // pair-handler and session-manager use it to skip the "pairing failed" // path and re-open the socket so the account finishes linking. | { type: "close"; reason: number; loggedOut: boolean; restartRequired: boolean }; export type SessionEventHandler = (event: SessionEvent) => void | Promise; export type Session = { accountId: string; socket: WASocket; close: () => Promise; }; export async function startSession(params: { accountId: string; onEvent: SessionEventHandler; }): Promise { const { accountId, onEvent } = params; const sessionDir = join(env.SESSIONS_DIR, accountId); await mkdir(sessionDir, { recursive: true }); const { state, saveCreds } = await useMultiFileAuthState(sessionDir); // Fetch the WhatsApp Web version Baileys should announce. Without this, // the noise handshake fails because WA's server-side rejects stale versions. const { version, isLatest } = await fetchLatestBaileysVersion(); logger.info({ accountId, version, isLatest }, "session: using WA Web version"); const socket = makeWASocket({ version, auth: state, browser: Browsers.macOS("Safari"), syncFullHistory: false, // Use Baileys' default QR cadence (60 s for the first ref, ~20 s for // each subsequent ref) — that's the native WhatsApp Web cadence and // each rotation just refreshes the displayed QR. The earlier "QR // refresh every 5 s" bug was the session-manager reconnect loop, // not the cadence. logger: logger.child({ accountId, component: "baileys" }) as never, }); socket.ev.on("creds.update", () => void saveCreds()); // Keep `whatsapp_groups` in sync as Baileys discovers new groups or updates. // Debounced so a flurry of upsert events from the initial sync collapses // into a single DB write. let groupsSyncTimer: NodeJS.Timeout | null = null; const scheduleGroupsSync = (): void => { if (groupsSyncTimer) return; groupsSyncTimer = setTimeout(() => { groupsSyncTimer = null; void syncGroupsForAccount(accountId, socket).catch((err) => logger.warn({ err, accountId }, "auto group sync failed"), ); }, 1500); }; socket.ev.on("groups.upsert", scheduleGroupsSync); socket.ev.on("groups.update", scheduleGroupsSync); socket.ev.on("connection.update", (update: Partial) => { if (update.qr) { void onEvent({ type: "qr", payload: update.qr }); } if (update.connection === "open") { const phoneNumber = socket.user?.id?.split(":")[0]; void onEvent({ type: "open", phoneNumber }); } if (update.connection === "close") { const reason = (update.lastDisconnect?.error as { output?: { statusCode?: number } } | undefined)?.output?.statusCode ?? 0; const loggedOut = reason === DisconnectReason.loggedOut; const restartRequired = reason === DisconnectReason.restartRequired; void onEvent({ type: "close", reason, loggedOut, restartRequired }); } }); return { accountId, socket, close: async () => { try { socket.end(undefined); } catch (err) { logger.warn({ err, accountId }, "session.close: error closing socket"); } }, }; }