246 lines
8.1 KiB
TypeScript
246 lines
8.1 KiB
TypeScript
import express, { Express, Request, Response } from 'express';
|
|
import { Server } from 'http';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
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, debugMode?: boolean) => 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, debugMode } = 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, debugMode);
|
|
|
|
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 });
|
|
}
|
|
});
|
|
|
|
// List screenshots for a session
|
|
// Filename format: {sessionId}-{step}-{timestamp}.png
|
|
this._app.get('/api/bot/screenshots/:sessionId', (req: Request, res: Response) => {
|
|
try {
|
|
const { sessionId } = req.params;
|
|
const screenshotDir = config.screenshotDir;
|
|
|
|
if (!fs.existsSync(screenshotDir)) {
|
|
res.json({ screenshots: [] });
|
|
return;
|
|
}
|
|
|
|
const files = fs.readdirSync(screenshotDir)
|
|
.filter(f => f.startsWith(`${sessionId}-`) && f.endsWith('.png'))
|
|
.sort();
|
|
|
|
const screenshots = files.map(name => {
|
|
const stats = fs.statSync(path.join(screenshotDir, name));
|
|
const withoutExt = name.replace('.png', '');
|
|
const parts = withoutExt.split('-');
|
|
// sessionId is a UUID (5 parts joined by -), step follows, timestamp is last
|
|
const timestamp = parseInt(parts[parts.length - 1], 10) || 0;
|
|
const step = parts.slice(5, -1).join('-');
|
|
return { name, step, timestamp, sizeBytes: stats.size };
|
|
});
|
|
|
|
res.json({ screenshots });
|
|
} catch (error) {
|
|
logger.error('Error listing screenshots:', error);
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
// Serve a single screenshot file
|
|
this._app.get('/api/bot/screenshots/file/:filename', (req: Request, res: Response) => {
|
|
try {
|
|
const { filename } = req.params;
|
|
|
|
if (!filename.endsWith('.png') || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
res.status(400).json({ error: 'Invalid filename' });
|
|
return;
|
|
}
|
|
|
|
const filepath = path.join(config.screenshotDir, filename);
|
|
if (!fs.existsSync(filepath)) {
|
|
res.status(404).json({ error: 'Screenshot not found' });
|
|
return;
|
|
}
|
|
|
|
res.type('image/png').sendFile(path.resolve(filepath));
|
|
} catch (error) {
|
|
logger.error('Error serving screenshot:', error);
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
}
|
|
}
|