multica/apps/gateway/telegram/telegram.controller.ts
Jiayuan Zhang d75a24714d feat(gateway): add Telegram QR deep link connection flow
Add short code store, bot commands (/start, /status, /help), and
POST /telegram/connect-code endpoint for Desktop to create QR codes.
Users scan a QR → Telegram opens → /start {code} → auto-connects.

- ShortCodeStore: in-memory Map with TTL for connection info
- Bot commands registered via setMyCommands
- Refactor handleConnectionLink into shared connectUser method
- Fetch bot username via getMe() for deep link URL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 00:30:01 +08:00

97 lines
3.1 KiB
TypeScript

/**
* Telegram controller.
*
* - POST /telegram/webhook — Receives webhook requests from Telegram Bot API
* - POST /telegram/connect-code — Creates a short code for QR deep link flow
*/
import { Body, Controller, HttpException, HttpStatus, Inject, Logger, Post, Req, Res, Headers } from "@nestjs/common";
import { TelegramService } from "./telegram.service.js";
import type { ConnectionInfo } from "@multica/store/connection";
// Minimal Express types for webhook handling
interface ExpressRequest {
body: unknown;
header: (name: string) => string | undefined;
}
interface ExpressResponse {
status: (code: number) => ExpressResponse;
json: (data: unknown) => void;
headersSent: boolean;
}
@Controller("telegram")
export class TelegramController {
private readonly logger = new Logger(TelegramController.name);
constructor(@Inject(TelegramService) private readonly telegramService: TelegramService) {}
@Post("connect-code")
async createConnectCode(
@Body() body: { gateway: string; hubId: string; agentId: string; token: string; expires: number },
): Promise<{ code: string; botUsername: string }> {
if (!this.telegramService.isConfigured()) {
throw new HttpException("Telegram bot not configured", HttpStatus.SERVICE_UNAVAILABLE);
}
const botUsername = this.telegramService.getBotUsername();
if (!botUsername) {
throw new HttpException("Bot username not available", HttpStatus.INTERNAL_SERVER_ERROR);
}
const connectionInfo: ConnectionInfo = {
type: "multica-connect",
gateway: body.gateway,
hubId: body.hubId,
agentId: body.agentId,
token: body.token,
expires: body.expires,
};
const code = this.telegramService.createConnectCode(connectionInfo);
this.logger.debug(`Created connect code: ${code}`);
return { code, botUsername };
}
@Post("webhook")
async handleWebhook(
@Req() req: ExpressRequest,
@Res() res: ExpressResponse,
@Headers("x-telegram-bot-api-secret-token") secretToken?: string
): Promise<void> {
// Check if Telegram is configured
if (!this.telegramService.isConfigured()) {
this.logger.warn("Telegram webhook called but bot not configured");
res.status(503).json({ error: "Telegram not configured" });
return;
}
// Validate secret token if configured
const expectedToken = process.env["TELEGRAM_WEBHOOK_SECRET_TOKEN"];
if (expectedToken && secretToken !== expectedToken) {
this.logger.warn("Invalid Telegram webhook secret token");
res.status(401).json({ error: "Unauthorized" });
return;
}
// Get grammY webhook callback
const callback = this.telegramService.getWebhookCallback();
if (!callback) {
res.status(503).json({ error: "Telegram not configured" });
return;
}
// Let grammY handle the request
try {
await callback(req, res);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Telegram webhook error: ${message}`);
if (!res.headersSent) {
res.status(500).json({ error: "Internal server error" });
}
}
}
}