import express, { Express, Request, Response } from 'express'; import { Server } from 'http'; import { logger } from '../utils/logger'; import { config } from '../config'; import { runAuthTests, runSingleVariant, getVariantIds } from '../bot/authTestProcedure'; export interface HttpServerCallbacks { onJoinRequest: (sessionId: string, meetingUrl: string, botName?: string, instanceId?: string, gatewayWsUrl?: string, language?: string, botAccountEmail?: string, botAccountPassword?: string, transferMode?: string) => Promise; onLeaveRequest: (sessionId: string) => Promise; onStatusRequest: (sessionId: string) => { state: string; error?: string } | null; } /** * HTTP server for the Bot Launcher API. * Provides endpoints to deploy/control bots. */ export class HttpServer { private _app: Express; private _server: Server | null = null; private _callbacks: HttpServerCallbacks; constructor(callbacks: HttpServerCallbacks) { this._callbacks = callbacks; this._app = express(); this._setupMiddleware(); this._setupRoutes(); } /** * Start the HTTP server. */ async start(): Promise { return new Promise((resolve) => { this._server = this._app.listen(config.port, () => { logger.info(`HTTP server listening on port ${config.port}`); resolve(); }); }); } /** * Stop the HTTP server. */ async stop(): Promise { return new Promise((resolve, reject) => { if (!this._server) { resolve(); return; } this._server.close((err) => { if (err) { reject(err); } else { logger.info('HTTP server stopped'); resolve(); } }); }); } private _setupMiddleware(): void { this._app.use(express.json()); // Request logging this._app.use((req, res, next) => { logger.debug(`${req.method} ${req.path}`); next(); }); } private _setupRoutes(): void { // Health check this._app.get('/health', (req: Request, res: Response) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Deploy a new bot this._app.post('/api/bot', async (req: Request, res: Response) => { try { const { sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode } = req.body; if (!sessionId || !meetingUrl) { res.status(400).json({ error: 'Missing required fields: sessionId, meetingUrl' }); return; } await this._callbacks.onJoinRequest(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode); res.json({ success: true, sessionId, message: 'Bot deployment started', }); } catch (error) { logger.error('Error deploying bot:', error); res.status(500).json({ error: (error as Error).message }); } }); // Leave meeting this._app.post('/api/bot/:sessionId/leave', async (req: Request, res: Response) => { try { const { sessionId } = req.params; await this._callbacks.onLeaveRequest(sessionId); res.json({ success: true, sessionId, message: 'Bot leave initiated', }); } catch (error) { logger.error('Error leaving meeting:', error); res.status(500).json({ error: (error as Error).message }); } }); // Get bot status this._app.get('/api/bot/:sessionId/status', (req: Request, res: Response) => { const { sessionId } = req.params; const status = this._callbacks.onStatusRequest(sessionId); if (!status) { res.status(404).json({ error: 'Session not found' }); return; } res.json({ sessionId, ...status, }); }); // List all active bots this._app.get('/api/bots', (req: Request, res: Response) => { // This would need access to the session manager res.json({ message: 'Not implemented' }); }); // Get list of available test variants this._app.get('/api/bot/test-auth/variants', (_req: Request, res: Response) => { res.json(getVariantIds()); }); // Run a SINGLE test variant (stays within Azure 240s timeout) this._app.post('/api/bot/test-auth/variant', async (req: Request, res: Response) => { try { const { variantId, meetingUrl, botAccountEmail, botAccountPassword } = req.body; if (!meetingUrl || !variantId) { res.status(400).json({ error: 'Missing required fields: variantId, meetingUrl' }); return; } logger.info(`Running single auth test variant: ${variantId} for ${meetingUrl}`); const result = await runSingleVariant(variantId, meetingUrl, botAccountEmail, botAccountPassword); res.json(result); } catch (error) { logger.error('Error running single variant:', error); res.status(500).json({ error: (error as Error).message }); } }); // Run ALL auth detection tests (legacy — may timeout on Azure with many variants) this._app.post('/api/bot/test-auth', async (req: Request, res: Response) => { try { const { meetingUrl, botAccountEmail, botAccountPassword } = req.body; if (!meetingUrl) { res.status(400).json({ error: 'Missing required field: meetingUrl' }); return; } const hasEmail = !!botAccountEmail; const hasPassword = !!botAccountPassword; logger.info(`Starting auth detection tests for: ${meetingUrl} (credentials: email=${hasEmail}, password=${hasPassword})`); const results = await runAuthTests(meetingUrl, botAccountEmail, botAccountPassword); (results as any).credentialsReceived = { hasEmail, hasPassword }; res.json(results); } catch (error) { logger.error('Error running auth tests:', error); res.status(500).json({ error: (error as Error).message }); } }); } }