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>
97 lines
3.1 KiB
TypeScript
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" });
|
|
}
|
|
}
|
|
}
|
|
}
|