Proxy Telegram bot user avatars
All checks were successful
Build and deploy Backend / build (push) Successful in 31s
All checks were successful
Build and deploy Backend / build (push) Successful in 31s
This commit is contained in:
@@ -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: {
|
||||||
|
|||||||
@@ -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',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user