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 = 2; type TelegramUpdateOffsetState = { version: number; lastUpdateId: number | null; botId: string | null; }; 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 extractBotIdFromToken(token?: string): string | null { const trimmed = token?.trim(); if (!trimmed) { return null; } const [rawBotId] = trimmed.split(":", 1); if (!rawBotId || !/^\d+$/.test(rawBotId)) { return null; } return rawBotId; } function safeParseState(raw: string): TelegramUpdateOffsetState | null { try { const parsed = JSON.parse(raw) as { version?: number; lastUpdateId?: number | null; botId?: string | null; }; if (parsed?.version !== STORE_VERSION && parsed?.version !== 1) { return null; } if (parsed.lastUpdateId !== null && typeof parsed.lastUpdateId !== "number") { return null; } if ( parsed.version === STORE_VERSION && parsed.botId !== null && typeof parsed.botId !== "string" ) { return null; } return { version: STORE_VERSION, lastUpdateId: parsed.lastUpdateId ?? null, botId: parsed.version === STORE_VERSION ? (parsed.botId ?? null) : null, }; } catch { return null; } } export async function readTelegramUpdateOffset(params: { accountId?: string; botToken?: string; env?: NodeJS.ProcessEnv; }): Promise { const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env); try { const raw = await fs.readFile(filePath, "utf-8"); const parsed = safeParseState(raw); const expectedBotId = extractBotIdFromToken(params.botToken); if (expectedBotId && parsed?.botId && parsed.botId !== expectedBotId) { return null; } if (expectedBotId && parsed?.botId === null) { 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; botToken?: string; 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, botId: extractBotIdFromToken(params.botToken), }; await fs.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, { encoding: "utf-8", }); await fs.chmod(tmp, 0o600); await fs.rename(tmp, filePath); } export async function deleteTelegramUpdateOffset(params: { accountId?: string; env?: NodeJS.ProcessEnv; }): Promise { const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env); try { await fs.unlink(filePath); } catch (err) { const code = (err as { code?: string }).code; if (code === "ENOENT") { return; } throw err; } }