import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { resolveStateDir } from "../config/paths.js"; const STORE_VERSION = 1; const TELEGRAM_UPDATE_ID_MAX = 2_147_483_647; // Telegram Bot API: Integer (32-bit signed) type TelegramUpdateOffsetState = { version: number; lastUpdateId: number | null; }; function isValidTelegramUpdateId(value: unknown): value is number { return ( typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= TELEGRAM_UPDATE_ID_MAX ); } function formatBackupSuffix() { return new Date().toISOString().replaceAll(":", "").replaceAll(".", ""); } function normalizeAccountId(accountId?: string) { const trimmed = accountId?.trim(); if (!trimmed) return "default"; return trimmed.replace(/[^a-z0-9._-]+/gi, "_"); } function resolveTelegramUpdateOffsetPath( accountId?: string, env: NodeJS.ProcessEnv = process.env, ): string { const stateDir = resolveStateDir(env, os.homedir); const normalized = normalizeAccountId(accountId); return path.join(stateDir, "telegram", `update-offset-${normalized}.json`); } function safeParseState(raw: string): TelegramUpdateOffsetState | null { try { const parsed = JSON.parse(raw) as TelegramUpdateOffsetState; if (parsed?.version !== STORE_VERSION) return null; if (parsed.lastUpdateId !== null && !isValidTelegramUpdateId(parsed.lastUpdateId)) { return null; } return parsed; } catch { return null; } } export async function readTelegramUpdateOffset(params: { accountId?: string; env?: NodeJS.ProcessEnv; onInvalid?: (info: { path: string; backupPath: string }) => void; }): Promise { const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env); try { const raw = await fs.readFile(filePath, "utf-8"); const parsed = safeParseState(raw); if (!parsed) { const backupPath = `${filePath}.bak.invalid.${formatBackupSuffix()}`; try { await fs.rename(filePath, backupPath); params.onInvalid?.({ path: filePath, backupPath }); } catch { // Ignore backup failures; treat as missing offset. } return null; } return parsed.lastUpdateId ?? null; } catch (err) { const code = (err as { code?: string }).code; if (code === "ENOENT") return null; return null; } } export async function writeTelegramUpdateOffset(params: { accountId?: string; updateId: number; env?: NodeJS.ProcessEnv; }): Promise { const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env); const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true, mode: 0o700 }); const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`); const payload: TelegramUpdateOffsetState = { version: STORE_VERSION, lastUpdateId: params.updateId, }; await fs.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, { encoding: "utf-8", }); await fs.chmod(tmp, 0o600); await fs.rename(tmp, filePath); }