import { rm } from "node:fs/promises"; import { join } from "node:path"; import { db } from "../db.js"; import { env } from "../env.js"; import { sessionManager } from "../whatsapp/session-manager.js"; import { writeAuditLog } from "../audit.js"; import { pgNotifyWeb } from "./notify.js"; import { logger } from "../logger.js"; /** * Unpair handler: stop the live Baileys session and remove the on-disk * session files. The web action keeps the account row alive (status = * 'unpaired') so the operator can re-pair without retyping the label; * the {intentional: true} stop tells the session manager not to race * the web's status write with its own "disconnected" update or * schedule a reconnect for a session we just chose to tear down. * * For the delete-account flow the row IS gone by the time we run; * the audit log lookup tolerates that. */ export async function handleUnpair(accountId: string): Promise { await sessionManager.stop(accountId, { intentional: true }); await rm(join(env.SESSIONS_DIR, accountId), { recursive: true, force: true }); try { const row = await db.query.whatsappAccounts.findFirst({ where: (a, { eq }) => eq(a.id, accountId), columns: { operatorId: true }, }); await writeAuditLog(db, { operatorId: row?.operatorId ?? null, source: "web", action: "account.unpaired", targetType: "whatsapp_account", targetId: accountId, payload: {}, }); } catch (err) { logger.warn({ err, accountId }, "unpair: audit log failed (non-fatal)"); } await pgNotifyWeb({ type: "session.disconnected", accountId }); } /** * Delete-account flow on the bot side. Distinct from unpair because * we want WhatsApp to drop this device from the user's linked-devices * list — otherwise the phone keeps showing a phantom entry that has * to be manually removed from WhatsApp's UI. * * Order is important: * 1. socket.logout() over the still-connected socket → WhatsApp * removes the linked device on the server side. * 2. close() the local Baileys session. * 3. rm() the on-disk auth blob so the next pairing starts clean. * * Step 1 is best-effort — if the socket is already torn down or the * RPC fails the delete still proceeds. The web action then deletes * the row (cascade FKs handle groups/reminders/runs). */ export async function handleDelete(accountId: string): Promise { await sessionManager.logoutAndStop(accountId); await rm(join(env.SESSIONS_DIR, accountId), { recursive: true, force: true }); try { const row = await db.query.whatsappAccounts.findFirst({ where: (a, { eq }) => eq(a.id, accountId), columns: { operatorId: true, label: true }, }); await writeAuditLog(db, { operatorId: row?.operatorId ?? null, source: "web", action: "account.deleted", targetType: "whatsapp_account", targetId: accountId, payload: { label: row?.label ?? null }, }); } catch (err) { logger.warn({ err, accountId }, "delete: audit log failed (non-fatal)"); } await pgNotifyWeb({ type: "session.disconnected", accountId }); }