Files
IdP/apps/api-gateway/src/controllers/telegram-bot-api.controller.ts
2026-06-26 13:01:52 +03:00

245 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { All, Body, Controller, Get, HttpCode, Param, Post, Query, Req, Res } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { firstValueFrom, timeout } from 'rxjs';
import { CoreGrpcService } from '../core-grpc.service';
type HttpRequest = {
method: string;
query: Record<string, unknown>;
};
type HttpResponse = {
status(code: number): HttpResponse;
setHeader(name: string, value: string): void;
};
const botPathParam = {
name: 'botPath',
description: 'Путь токена в формате bot<token>, совместимо с Telegram Bot API.',
example: 'bot123456:ABC-DEF'
};
const chatIdProperty = {
oneOf: [{ type: 'string' }, { type: 'number' }],
description: 'ID пользователя/чата в приложении или numeric chat_id из ответа Bot API.'
};
@ApiTags('Telegram Bot API')
@Controller()
export class TelegramBotApiController {
constructor(private readonly core: CoreGrpcService) {}
@Get(':botPath/getMe')
@ApiOperation({
summary: 'Bot API: getMe',
description: 'Возвращает профиль бота в формате Telegram Bot API.'
})
@ApiParam(botPathParam)
getMe(@Res({ passthrough: true }) response: HttpResponse, @Param('botPath') botPath: string) {
return this.executeTelegramMethod(response, botPath, 'getMe', {});
}
@Post(':botPath/getChat')
@HttpCode(200)
@ApiOperation({
summary: 'Bot API: getChat',
description: 'Возвращает приватный чат приложения и pinned_message, если сообщение закреплено.'
})
@ApiParam(botPathParam)
@ApiBody({
schema: {
type: 'object',
required: ['chat_id'],
properties: {
chat_id: chatIdProperty
}
}
})
getChatPost(
@Res({ passthrough: true }) response: HttpResponse,
@Param('botPath') botPath: string,
@Body() body: Record<string, unknown>
) {
return this.executeTelegramMethod(response, botPath, 'getChat', body ?? {});
}
@Get(':botPath/getChat')
@ApiOperation({
summary: 'Bot API: getChat (GET)',
description: 'GET-вариант метода getChat для клиентов, которые передают параметры в query.'
})
@ApiParam(botPathParam)
getChatGet(
@Res({ passthrough: true }) response: HttpResponse,
@Param('botPath') botPath: string,
@Query() query: Record<string, unknown>
) {
return this.executeTelegramMethod(response, botPath, 'getChat', query ?? {});
}
@Post(':botPath/sendMessage')
@HttpCode(200)
@ApiOperation({
summary: 'Bot API: sendMessage',
description: 'Отправляет сообщение пользователю внутри приложения. Поддерживаются text, parse_mode и reply_markup.'
})
@ApiParam(botPathParam)
@ApiBody({
schema: {
type: 'object',
required: ['chat_id', 'text'],
properties: {
chat_id: chatIdProperty,
text: { type: 'string' },
parse_mode: { type: 'string', example: 'HTML' },
reply_markup: { type: 'object', additionalProperties: true }
}
}
})
sendMessage(
@Res({ passthrough: true }) response: HttpResponse,
@Param('botPath') botPath: string,
@Body() body: Record<string, unknown>
) {
return this.executeTelegramMethod(response, botPath, 'sendMessage', body ?? {});
}
@Post(':botPath/editMessageText')
@HttpCode(200)
@ApiOperation({
summary: 'Bot API: editMessageText',
description: 'Редактирует текст ранее отправленного ботом сообщения.'
})
@ApiParam(botPathParam)
@ApiBody({
schema: {
type: 'object',
required: ['chat_id', 'message_id', 'text'],
properties: {
chat_id: chatIdProperty,
message_id: { type: 'integer' },
text: { type: 'string' },
parse_mode: { type: 'string', example: 'HTML' },
reply_markup: { type: 'object', additionalProperties: true }
}
}
})
editMessageText(
@Res({ passthrough: true }) response: HttpResponse,
@Param('botPath') botPath: string,
@Body() body: Record<string, unknown>
) {
return this.executeTelegramMethod(response, botPath, 'editMessageText', body ?? {});
}
@Post(':botPath/pinChatMessage')
@HttpCode(200)
@ApiOperation({
summary: 'Bot API: pinChatMessage',
description: 'Закрепляет сообщение бота в чате приложения.'
})
@ApiParam(botPathParam)
@ApiBody({
schema: {
type: 'object',
required: ['chat_id', 'message_id'],
properties: {
chat_id: chatIdProperty,
message_id: { type: 'integer' },
disable_notification: { type: 'boolean', default: false }
}
}
})
pinChatMessage(
@Res({ passthrough: true }) response: HttpResponse,
@Param('botPath') botPath: string,
@Body() body: Record<string, unknown>
) {
return this.executeTelegramMethod(response, botPath, 'pinChatMessage', body ?? {});
}
@Post(':botPath/unpinChatMessage')
@HttpCode(200)
@ApiOperation({
summary: 'Bot API: unpinChatMessage',
description: 'Открепляет указанное сообщение. Если message_id не передан, открепляет последнее закреплённое сообщение.'
})
@ApiParam(botPathParam)
@ApiBody({
schema: {
type: 'object',
required: ['chat_id'],
properties: {
chat_id: chatIdProperty,
message_id: { type: 'integer' }
}
}
})
unpinChatMessage(
@Res({ passthrough: true }) response: HttpResponse,
@Param('botPath') botPath: string,
@Body() body: Record<string, unknown>
) {
return this.executeTelegramMethod(response, botPath, 'unpinChatMessage', body ?? {});
}
@All(':botPath/:method')
@HttpCode(200)
@ApiOperation({
summary: 'Bot API: универсальный метод',
description:
'Совместимый endpoint /bot<TOKEN>/<method>. Поддерживаются getMe, getChat, sendMessage, editMessageText, editMessageReplyMarkup, sendPhoto, sendDocument, pinChatMessage, unpinChatMessage, answerCallbackQuery, setWebhook, deleteWebhook, getWebhookInfo, getUpdates и setChatMenuButton.'
})
@ApiParam(botPathParam)
@ApiParam({
name: 'method',
description: 'Название метода Telegram Bot API.',
example: 'sendMessage'
})
async handleTelegramMethod(
@Req() request: HttpRequest,
@Res({ passthrough: true }) response: HttpResponse,
@Param('botPath') botPath: string,
@Param('method') method: string,
@Body() body: Record<string, unknown>
) {
const payload = request.method === 'GET' ? { ...request.query } : body ?? {};
return this.executeTelegramMethod(response, botPath, method, payload);
}
private async executeTelegramMethod(
response: HttpResponse,
botPath: string,
method: string,
payload: Record<string, unknown>
) {
if (!botPath.startsWith('bot')) {
response.status(404);
return { ok: false, error_code: 404, description: 'Not Found' };
}
const token = decodeURIComponent(botPath.slice(3));
if (!token) {
response.status(401);
return { ok: false, error_code: 401, description: 'Unauthorized' };
}
const grpcCall = this.core.bot.ExecuteBotMethod({
token,
method,
payloadJson: JSON.stringify(payload)
});
const waitSeconds = method === 'getUpdates' ? Number(payload.timeout ?? 0) : 0;
const grpcTimeoutMs = method === 'getUpdates' ? Math.min(Math.max(waitSeconds, 0), 50) * 1000 + 10_000 : 30_000;
const result = (await firstValueFrom(
grpcCall.pipe(timeout(grpcTimeoutMs)) as typeof grpcCall
)) as { responseJson: string; httpStatus: number };
response.status(result.httpStatus ?? 200);
response.setHeader('Content-Type', 'application/json');
return JSON.parse(result.responseJson);
}
}