service-teams-browser-bot/src/server/httpServer.ts
2026-02-17 18:43:30 +01:00

188 lines
6 KiB
TypeScript

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<void>;
onLeaveRequest: (sessionId: string) => Promise<void>;
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<void> {
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<void> {
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 });
}
});
}
}