import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { logVerbose, shouldLogVerbose } from "./globals.js"; export async function ensureDir(dir: string) { await fs.promises.mkdir(dir, { recursive: true }); } export type Provider = "web"; export function assertProvider(input: string): asserts input is Provider { if (input !== "web") { throw new Error("Provider must be 'web'"); } } export function normalizePath(p: string): string { if (!p.startsWith("/")) return `/${p}`; return p; } export function withWhatsAppPrefix(number: string): string { return number.startsWith("whatsapp:") ? number : `whatsapp:${number}`; } export function normalizeE164(number: string): string { const withoutPrefix = number.replace(/^whatsapp:/, "").trim(); const digits = withoutPrefix.replace(/[^\d+]/g, ""); if (digits.startsWith("+")) return `+${digits.slice(1)}`; return `+${digits}`; } /** * "Self-chat mode" heuristic (single phone): the gateway is logged in as the owner's own WhatsApp account, * and `whatsapp.allowFrom` includes that same number. Used to avoid side-effects that make no sense when the * "bot" and the human are the same WhatsApp identity (e.g. auto read receipts, @mention JID triggers). */ export function isSelfChatMode( selfE164: string | null | undefined, allowFrom?: Array | null, ): boolean { if (!selfE164) return false; if (!Array.isArray(allowFrom) || allowFrom.length === 0) return false; const normalizedSelf = normalizeE164(selfE164); return allowFrom.some((n) => { if (n === "*") return false; try { return normalizeE164(String(n)) === normalizedSelf; } catch { return false; } }); } export function toWhatsappJid(number: string): string { const withoutPrefix = number.replace(/^whatsapp:/, "").trim(); if (withoutPrefix.includes("@")) return withoutPrefix; const e164 = normalizeE164(withoutPrefix); const digits = e164.replace(/\D/g, ""); return `${digits}@s.whatsapp.net`; } export function jidToE164(jid: string): string | null { // Convert a WhatsApp JID (with optional device suffix, e.g. 1234:1@s.whatsapp.net) back to +1234. const match = jid.match(/^(\d+)(?::\d+)?@s\.whatsapp\.net$/); if (match) { const digits = match[1]; return `+${digits}`; } // Support @lid format (WhatsApp Linked ID) - look up reverse mapping const lidMatch = jid.match(/^(\d+)(?::\d+)?@lid$/); if (lidMatch) { const lid = lidMatch[1]; try { const mappingPath = `${CONFIG_DIR}/credentials/lid-mapping-${lid}_reverse.json`; const data = fs.readFileSync(mappingPath, "utf8"); const phone = JSON.parse(data); if (phone) return `+${phone}`; } catch { if (shouldLogVerbose()) { logVerbose( `LID mapping not found for ${lid}; skipping inbound message`, ); } // Mapping not found, fall through } } return null; } export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } export function resolveUserPath(input: string): string { const trimmed = input.trim(); if (!trimmed) return trimmed; if (trimmed.startsWith("~")) { return path.resolve(trimmed.replace("~", os.homedir())); } return path.resolve(trimmed); } export function resolveHomeDir(): string | undefined { const envHome = process.env.HOME?.trim(); if (envHome) return envHome; const envProfile = process.env.USERPROFILE?.trim(); if (envProfile) return envProfile; try { const home = os.homedir(); return home?.trim() ? home : undefined; } catch { return undefined; } } export function shortenHomePath(input: string): string { if (!input) return input; const home = resolveHomeDir(); if (!home) return input; if (input === home) return "~"; if (input.startsWith(`${home}/`)) return `~${input.slice(home.length)}`; return input; } export function shortenHomeInString(input: string): string { if (!input) return input; const home = resolveHomeDir(); if (!home) return input; return input.split(home).join("~"); } // Fixed configuration root; legacy ~/.clawdis is no longer used. export const CONFIG_DIR = path.join(os.homedir(), ".clawdis");