import { Pool } from "pg"; import type { OmniInboundEnvelopeV1 } from "./types"; const TELEGRAM_FILE_MARKER = "tg-file:"; const TELEGRAM_API_BASE_DEFAULT = "https://api.telegram.org"; type AvatarStateRow = { avatar_fingerprint: string | null; }; function asString(value: unknown) { if (typeof value !== "string") return null; const normalized = value.trim(); return normalized || null; } function requiredEnv(name: string) { const value = asString(process.env[name]); if (!value) { throw new Error(`${name} is required`); } return value; } function parseTelegramFileId(avatarUrl: string | null) { const raw = asString(avatarUrl); if (!raw || !raw.startsWith(TELEGRAM_FILE_MARKER)) return null; const fileId = raw.slice(TELEGRAM_FILE_MARKER.length).trim(); return fileId || null; } function parseTelegramUserId(value: string | null) { const raw = asString(value); if (!raw || !/^\d+$/.test(raw)) return null; const userId = Number.parseInt(raw, 10); if (!Number.isFinite(userId) || userId <= 0) return null; return userId; } function telegramApiBase() { return asString(process.env.TELEGRAM_API_BASE) ?? TELEGRAM_API_BASE_DEFAULT; } type TelegramUserProfilePhotosResponse = { ok?: boolean; result?: { photos?: Array>; }; description?: string; }; function pickProfilePhotoFileId(payload: TelegramUserProfilePhotosResponse) { const groups = payload.result?.photos; if (!Array.isArray(groups) || !groups.length) return null; const firstGroup = groups[0]; if (!Array.isArray(firstGroup) || !firstGroup.length) return null; for (let index = firstGroup.length - 1; index >= 0; index -= 1) { const fileId = asString(firstGroup[index]?.file_id); if (fileId) return fileId; } return null; } async function resolveProfileAvatarFileId(contactExternalId: string | null) { const userId = parseTelegramUserId(contactExternalId); if (!userId) return null; const token = asString(process.env.TELEGRAM_BOT_TOKEN); if (!token) return null; const response = await fetch(`${telegramApiBase().replace(/\/+$/, "")}/bot${token}/getUserProfilePhotos`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ user_id: userId, offset: 0, limit: 1, }), }); if (!response.ok) return null; const payload = (await response.json()) as TelegramUserProfilePhotosResponse; if (!payload.ok) return null; return pickProfilePhotoFileId(payload); } const pool = new Pool({ connectionString: requiredEnv("TELEGRAM_PROFILE_STATE_DATABASE_URL"), }); let initPromise: Promise | null = null; async function ensureInitialized() { if (!initPromise) { initPromise = pool .query(` CREATE TABLE IF NOT EXISTS telegram_contact_profile_state ( contact_external_id TEXT PRIMARY KEY, avatar_fingerprint TEXT, avatar_file_id TEXT, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `) .then(() => undefined); } await initPromise; } async function detectAvatarChange(input: { contactExternalId: string | null; avatarFileId: string | null; avatarFingerprint: string | null; }) { const contactExternalId = asString(input.contactExternalId); const avatarFileId = asString(input.avatarFileId); const avatarFingerprint = asString(input.avatarFingerprint) ?? avatarFileId; if (!contactExternalId || !avatarFileId || !avatarFingerprint) { return { changed: false }; } await ensureInitialized(); const row = await pool.query( ` SELECT avatar_fingerprint FROM telegram_contact_profile_state WHERE contact_external_id = $1 `, [contactExternalId], ); const previousFingerprint = asString(row.rows[0]?.avatar_fingerprint); const changed = previousFingerprint !== avatarFingerprint; if (changed) { await pool.query( ` INSERT INTO telegram_contact_profile_state ( contact_external_id, avatar_fingerprint, avatar_file_id, updated_at ) VALUES ($1, $2, $3, NOW()) ON CONFLICT (contact_external_id) DO UPDATE SET avatar_fingerprint = EXCLUDED.avatar_fingerprint, avatar_file_id = EXCLUDED.avatar_file_id, updated_at = NOW() `, [contactExternalId, avatarFingerprint, avatarFileId], ); } return { changed }; } export async function applyAvatarProfileState(envelope: OmniInboundEnvelopeV1): Promise { const payload = envelope.payloadNormalized; const contactExternalId = asString(payload.contactExternalId); let contactAvatarUrl = asString(payload.contactAvatarUrl); let contactAvatarFingerprint = asString(payload.contactAvatarFingerprint); if (!contactAvatarUrl) { const profileFileId = await resolveProfileAvatarFileId(contactExternalId); if (profileFileId) { contactAvatarUrl = `${TELEGRAM_FILE_MARKER}${profileFileId}`; if (!contactAvatarFingerprint) { contactAvatarFingerprint = profileFileId; } } } const avatarFileId = parseTelegramFileId(contactAvatarUrl); const avatarState = await detectAvatarChange({ contactExternalId, avatarFileId, avatarFingerprint: contactAvatarFingerprint, }); return { ...envelope, payloadNormalized: { ...payload, contactAvatarChanged: avatarState.changed, contactAvatarUrl: avatarState.changed ? contactAvatarUrl : null, contactAvatarFingerprint, }, }; } export async function closeProfileState() { await pool.end(); }