From 6bae0300c8e3fdc4e5b97e11c81c1f09bf7fd952 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:59:44 +0700 Subject: [PATCH] Cache Telegram avatars locally and refresh avatar file ids --- .../server/api/omni/telegram/avatar.get.ts | 26 +++++++++++++++++++ omni_chat/src/worker.ts | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/frontend/server/api/omni/telegram/avatar.get.ts b/frontend/server/api/omni/telegram/avatar.get.ts index 4a3d78f..85b3676 100644 --- a/frontend/server/api/omni/telegram/avatar.get.ts +++ b/frontend/server/api/omni/telegram/avatar.get.ts @@ -1,9 +1,13 @@ 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; @@ -17,6 +21,10 @@ function parseTelegramFileId(avatarUrl: string | null | undefined) { return fileId || null; } +function sanitizeCacheKey(input: string) { + return input.replace(/[^a-zA-Z0-9._-]/g, "_"); +} + export default defineEventHandler(async (event) => { const auth = await getAuthContext(event); const query = getQuery(event); @@ -45,6 +53,20 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 404, statusMessage: "telegram avatar is missing" }); } + 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) { @@ -62,6 +84,10 @@ export default defineEventHandler(async (event) => { 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) { diff --git a/omni_chat/src/worker.ts b/omni_chat/src/worker.ts index 001f429..dc56f1d 100644 --- a/omni_chat/src/worker.ts +++ b/omni_chat/src/worker.ts @@ -248,7 +248,7 @@ async function maybeHydrateContact(contactId: string, profile: ContactProfile) { } const currentAvatar = asString(current.avatarUrl); - if (profile.avatarUrl && !currentAvatar) { + if (profile.avatarUrl && profile.avatarUrl !== currentAvatar) { updates.avatarUrl = profile.avatarUrl; }