From de0e23063207ee0ecc1a55bbd4e7e35321616ce0 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 8 May 2026 19:37:10 +0700 Subject: [PATCH] Proxy Telegram bot user avatars --- src/auth/telegram-bot-login.ts | 49 +++++++++++++++++++++++++++++++--- src/config.ts | 1 + src/server.ts | 13 ++++++++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/auth/telegram-bot-login.ts b/src/auth/telegram-bot-login.ts index 34a5ca9..60551f1 100644 --- a/src/auth/telegram-bot-login.ts +++ b/src/auth/telegram-bot-login.ts @@ -25,6 +25,14 @@ type TelegramBotUser = { language_code?: string; }; +type TelegramProfilePhotos = { + photos: { file_id: string }[][]; +}; + +type TelegramFile = { + file_path: string; +}; + const loginPrefix = 'login_'; function randomToken() { @@ -43,7 +51,7 @@ function botApiUrl(method: string) { return `https://api.telegram.org/bot${config.telegramMiniAppBotToken}/${method}`; } -async function callTelegram(method: string, body: Record) { +async function callTelegram(method: string, body: Record) { const response = await fetch(botApiUrl(method), { method: 'POST', headers: { 'content-type': 'application/json' }, @@ -54,10 +62,16 @@ async function callTelegram(method: string, body: Record) { throw new Error(`Telegram ${method} failed with ${response.status}.`); } - const payload = (await response.json()) as { ok: boolean; description?: string }; + const payload = (await response.json()) as { + ok: boolean; + description?: string; + result?: T; + }; if (!payload.ok) { throw new Error(payload.description ?? `Telegram ${method} failed.`); } + + return payload.result as T; } function userPayload(from: TelegramBotUser): TelegramUserPayload { @@ -70,6 +84,32 @@ function userPayload(from: TelegramBotUser): TelegramUserPayload { }; } +async function telegramUserPhotoUrl(userId: number) { + const photos = await callTelegram('getUserProfilePhotos', { + user_id: userId, + limit: 1, + }); + const variants = photos.photos[0]; + const fileId = variants?.[variants.length - 1]?.file_id; + return fileId ? `${config.publicApiUrl}/telegram/photo/${fileId}` : undefined; +} + +export async function fetchTelegramPhoto(fileId: string) { + const file = await callTelegram('getFile', { file_id: fileId }); + const response = await fetch( + `https://api.telegram.org/file/bot${config.telegramMiniAppBotToken}/${file.file_path}`, + ); + + if (!response.ok) { + throw new Error(`Telegram file fetch failed with ${response.status}.`); + } + + return { + contentType: response.headers.get('content-type') ?? 'image/jpeg', + bytes: Buffer.from(await response.arrayBuffer()), + }; +} + async function sendLoginMessage(chatId: number, text: string, token?: string) { const replyMarkup = token ? { @@ -166,7 +206,10 @@ export async function handleTelegramBotWebhook( return; } - const user = await upsertTelegramUser(userPayload(from)); + const user = await upsertTelegramUser({ + ...userPayload(from), + photoUrl: await telegramUserPhotoUrl(from.id), + }); const sessionToken = randomToken(); await prisma.userSession.create({ data: { diff --git a/src/config.ts b/src/config.ts index 1d542ca..bfc6ad1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,6 +13,7 @@ export const config = { telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME ?? 'carfteebot', telegramWebhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET ?? '', webAppUrl: process.env.WEB_APP_URL ?? 'https://map.craftee.vn', + publicApiUrl: process.env.PUBLIC_API_URL ?? 'https://api.map.craftee.vn', telegramAuthMaxAgeSeconds: Number( process.env.TELEGRAM_AUTH_MAX_AGE_SECONDS ?? '86400', ), diff --git a/src/server.ts b/src/server.ts index 9305dac..c8f5dcd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,10 @@ import mercurius from 'mercurius'; import { config } from './config.js'; import { prisma } from './prisma.js'; import { resolvers, schema } from './graphql/schema.js'; -import { handleTelegramBotWebhook } from './auth/telegram-bot-login.js'; +import { + fetchTelegramPhoto, + handleTelegramBotWebhook, +} from './auth/telegram-bot-login.js'; const app = Fastify({ logger: true }); @@ -33,6 +36,14 @@ app.post('/telegram/webhook', async (request, reply) => { return reply.send({ ok: true }); }); +app.get('/telegram/photo/:fileId', async (request, reply) => { + const params = request.params as { fileId: string }; + const photo = await fetchTelegramPhoto(params.fileId); + reply.header('content-type', photo.contentType); + reply.header('cache-control', 'public, max-age=86400'); + return reply.send(photo.bytes); +}); + app.addHook('onClose', async () => { await prisma.$disconnect(); });