Proxy Telegram bot user avatars
All checks were successful
Build and deploy Backend / build (push) Successful in 31s

This commit is contained in:
Ruslan Bakiev
2026-05-08 19:37:10 +07:00
parent 71561724a5
commit de0e230632
3 changed files with 59 additions and 4 deletions

View File

@@ -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<string, unknown>) {
async function callTelegram<T>(method: string, body: Record<string, unknown>) {
const response = await fetch(botApiUrl(method), {
method: 'POST',
headers: { 'content-type': 'application/json' },
@@ -54,10 +62,16 @@ async function callTelegram(method: string, body: Record<string, unknown>) {
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<TelegramProfilePhotos>('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<TelegramFile>('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: {

View File

@@ -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',
),

View File

@@ -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();
});