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; language_code?: string;
}; };
type TelegramProfilePhotos = {
photos: { file_id: string }[][];
};
type TelegramFile = {
file_path: string;
};
const loginPrefix = 'login_'; const loginPrefix = 'login_';
function randomToken() { function randomToken() {
@@ -43,7 +51,7 @@ function botApiUrl(method: string) {
return `https://api.telegram.org/bot${config.telegramMiniAppBotToken}/${method}`; 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), { const response = await fetch(botApiUrl(method), {
method: 'POST', method: 'POST',
headers: { 'content-type': 'application/json' }, 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}.`); 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) { if (!payload.ok) {
throw new Error(payload.description ?? `Telegram ${method} failed.`); throw new Error(payload.description ?? `Telegram ${method} failed.`);
} }
return payload.result as T;
} }
function userPayload(from: TelegramBotUser): TelegramUserPayload { 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) { async function sendLoginMessage(chatId: number, text: string, token?: string) {
const replyMarkup = token const replyMarkup = token
? { ? {
@@ -166,7 +206,10 @@ export async function handleTelegramBotWebhook(
return; return;
} }
const user = await upsertTelegramUser(userPayload(from)); const user = await upsertTelegramUser({
...userPayload(from),
photoUrl: await telegramUserPhotoUrl(from.id),
});
const sessionToken = randomToken(); const sessionToken = randomToken();
await prisma.userSession.create({ await prisma.userSession.create({
data: { data: {

View File

@@ -13,6 +13,7 @@ export const config = {
telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME ?? 'carfteebot', telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME ?? 'carfteebot',
telegramWebhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET ?? '', telegramWebhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET ?? '',
webAppUrl: process.env.WEB_APP_URL ?? 'https://map.craftee.vn', webAppUrl: process.env.WEB_APP_URL ?? 'https://map.craftee.vn',
publicApiUrl: process.env.PUBLIC_API_URL ?? 'https://api.map.craftee.vn',
telegramAuthMaxAgeSeconds: Number( telegramAuthMaxAgeSeconds: Number(
process.env.TELEGRAM_AUTH_MAX_AGE_SECONDS ?? '86400', process.env.TELEGRAM_AUTH_MAX_AGE_SECONDS ?? '86400',
), ),

View File

@@ -4,7 +4,10 @@ import mercurius from 'mercurius';
import { config } from './config.js'; import { config } from './config.js';
import { prisma } from './prisma.js'; import { prisma } from './prisma.js';
import { resolvers, schema } from './graphql/schema.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 }); const app = Fastify({ logger: true });
@@ -33,6 +36,14 @@ app.post('/telegram/webhook', async (request, reply) => {
return reply.send({ ok: true }); 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 () => { app.addHook('onClose', async () => {
await prisma.$disconnect(); await prisma.$disconnect();
}); });