import { getQuery, setHeader } from "h3"; import { existsSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { getAuthContext } from "../../../utils/auth"; import { prisma } from "../../../utils/prisma"; import { requireTelegramBotToken, telegramApiBase, telegramBotApi } from "../../../utils/telegram"; const TELEGRAM_FILE_MARKER = "tg-file:"; const AVATAR_CACHE_DIR = path.join(process.cwd(), ".data", "telegram-avatars"); type TelegramFileMeta = { file_id: string; file_path?: string; }; type TelegramChatPhoto = { small_file_id?: string; big_file_id?: string; }; type TelegramGetChatResponse = { ok?: boolean; result?: { photo?: TelegramChatPhoto; }; }; type TelegramProfilePhotoSize = { file_id?: string; }; type TelegramGetUserProfilePhotosResponse = { ok?: boolean; result?: { photos?: TelegramProfilePhotoSize[][]; }; }; function parseTelegramFileId(avatarUrl: string | null | undefined) { const raw = String(avatarUrl ?? "").trim(); if (!raw.startsWith(TELEGRAM_FILE_MARKER)) return null; const fileId = raw.slice(TELEGRAM_FILE_MARKER.length).trim(); return fileId || null; } function sanitizeCacheKey(input: string) { return input.replace(/[^a-zA-Z0-9._-]/g, "_"); } async function fetchTelegramAvatarFileIdByExternalId(externalId: string) { const getChatRes = await telegramBotApi("getChat", { chat_id: externalId, }); const fromChat = String(getChatRes?.photo?.small_file_id ?? getChatRes?.photo?.big_file_id ?? "").trim(); if (fromChat) return fromChat; const getUserPhotosRes = await telegramBotApi("getUserProfilePhotos", { user_id: externalId, limit: 1, }); const firstPhotoSizes = Array.isArray(getUserPhotosRes?.photos?.[0]) ? getUserPhotosRes.photos[0] : []; const candidate = String(firstPhotoSizes.at(-1)?.file_id ?? firstPhotoSizes[0]?.file_id ?? "").trim(); return candidate || null; } export default defineEventHandler(async (event) => { const auth = await getAuthContext(event); const query = getQuery(event); const contactId = String(query.contactId ?? "").trim(); if (!contactId) { throw createError({ statusCode: 400, statusMessage: "contactId is required" }); } const contact = await prisma.contact.findFirst({ where: { id: contactId, teamId: auth.teamId, }, select: { avatarUrl: true, }, }); if (!contact) { throw createError({ statusCode: 404, statusMessage: "contact not found" }); } let fileId = parseTelegramFileId(contact.avatarUrl); if (!fileId) { const identity = await prisma.omniContactIdentity.findFirst({ where: { teamId: auth.teamId, contactId, channel: "TELEGRAM", }, select: { externalId: true, }, orderBy: { updatedAt: "desc", }, }); if (!identity?.externalId) { throw createError({ statusCode: 404, statusMessage: "telegram identity is missing" }); } const fetchedFileId = await fetchTelegramAvatarFileIdByExternalId(identity.externalId); if (!fetchedFileId) { throw createError({ statusCode: 404, statusMessage: "telegram avatar is missing" }); } fileId = fetchedFileId; await prisma.contact.update({ where: { id: contactId }, data: { avatarUrl: `${TELEGRAM_FILE_MARKER}${fileId}` }, }); } const cacheKey = sanitizeCacheKey(fileId); const cacheBodyPath = path.join(AVATAR_CACHE_DIR, `${cacheKey}.img`); const cacheMetaPath = path.join(AVATAR_CACHE_DIR, `${cacheKey}.meta`); if (existsSync(cacheBodyPath) && existsSync(cacheMetaPath)) { const cachedBody = await readFile(cacheBodyPath); const cachedContentType = (await readFile(cacheMetaPath, "utf8")).trim() || "image/jpeg"; setHeader(event, "content-type", cachedContentType); setHeader(event, "cache-control", "private, max-age=300"); setHeader(event, "content-length", cachedBody.length); return cachedBody; } const meta = await telegramBotApi("getFile", { file_id: fileId }); const filePath = String(meta?.file_path ?? "").trim(); if (!filePath) { throw createError({ statusCode: 502, statusMessage: "telegram file path is unavailable" }); } const apiBase = telegramApiBase().replace(/\/+$/, ""); const token = requireTelegramBotToken(); const remote = await fetch(`${apiBase}/file/bot${token}/${filePath}`); if (!remote.ok) { throw createError({ statusCode: 502, statusMessage: `telegram file fetch failed (${remote.status})` }); } const contentType = remote.headers.get("content-type") || "image/jpeg"; const contentLength = remote.headers.get("content-length"); const body = Buffer.from(await remote.arrayBuffer()); await mkdir(AVATAR_CACHE_DIR, { recursive: true }); await writeFile(cacheBodyPath, body); await writeFile(cacheMetaPath, contentType, "utf8"); setHeader(event, "content-type", contentType); setHeader(event, "cache-control", "private, max-age=300"); if (contentLength) { const parsedLength = Number(contentLength); if (Number.isFinite(parsedLength) && parsedLength >= 0) { setHeader(event, "content-length", parsedLength); } } return body; });