Merge branch 'main' of https://github.com/valueonag/service-teams-browser-bot
This commit is contained in:
commit
48c5c33f63
4 changed files with 21 additions and 16 deletions
|
|
@ -33,6 +33,7 @@ export interface OrchestratorOptions {
|
||||||
botAccountEmail?: string;
|
botAccountEmail?: string;
|
||||||
botAccountPassword?: string;
|
botAccountPassword?: string;
|
||||||
transferMode?: string;
|
transferMode?: string;
|
||||||
|
debugMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,6 +70,7 @@ export class BotOrchestrator {
|
||||||
|
|
||||||
private _state: BotState = 'idle';
|
private _state: BotState = 'idle';
|
||||||
private _isShuttingDown: boolean = false;
|
private _isShuttingDown: boolean = false;
|
||||||
|
private _isDebugMode: boolean = false;
|
||||||
private _keepAliveInterval: NodeJS.Timeout | null = null;
|
private _keepAliveInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -83,6 +85,7 @@ export class BotOrchestrator {
|
||||||
this._botName = botName || config.botName;
|
this._botName = botName || config.botName;
|
||||||
this._callbacks = callbacks;
|
this._callbacks = callbacks;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
|
this._isDebugMode = !!options.debugMode;
|
||||||
this._logger = createSessionLogger(sessionId);
|
this._logger = createSessionLogger(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +236,7 @@ export class BotOrchestrator {
|
||||||
);
|
);
|
||||||
if (!emailInput) {
|
if (!emailInput) {
|
||||||
this._logger.warn(`No login page found, current URL: ${this._page!.url().substring(0, 150)}`);
|
this._logger.warn(`No login page found, current URL: ${this._page!.url().substring(0, 150)}`);
|
||||||
await this._takeScreenshot('step1-no-login-page', true);
|
await this._takeScreenshot('step1-no-login-page', this._isDebugMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 2: Microsoft Authentication
|
// STEP 2: Microsoft Authentication
|
||||||
|
|
@ -246,11 +249,11 @@ export class BotOrchestrator {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!authSuccess) {
|
if (!authSuccess) {
|
||||||
await this._takeScreenshot('step2-auth-failed', true);
|
await this._takeScreenshot('step2-auth-failed', this._isDebugMode);
|
||||||
throw new Error('Microsoft authentication failed');
|
throw new Error('Microsoft authentication failed');
|
||||||
}
|
}
|
||||||
this._logger.info('STEP 2: authentication successful');
|
this._logger.info('STEP 2: authentication successful');
|
||||||
await this._takeScreenshot('step2-auth-done', true);
|
await this._takeScreenshot('step2-auth-done', this._isDebugMode);
|
||||||
|
|
||||||
// STEP 3: Wait for Teams to load after auth
|
// STEP 3: Wait for Teams to load after auth
|
||||||
this._logger.info('STEP 3: waiting for Teams to load after auth...');
|
this._logger.info('STEP 3: waiting for Teams to load after auth...');
|
||||||
|
|
@ -262,7 +265,7 @@ export class BotOrchestrator {
|
||||||
} catch {
|
} catch {
|
||||||
this._logger.warn(`Unexpected URL after auth: ${this._page!.url().substring(0, 150)}`);
|
this._logger.warn(`Unexpected URL after auth: ${this._page!.url().substring(0, 150)}`);
|
||||||
}
|
}
|
||||||
await this._takeScreenshot('step3-teams-loaded', true);
|
await this._takeScreenshot('step3-teams-loaded', this._isDebugMode);
|
||||||
|
|
||||||
// STEP 4: Navigate to the meeting URL with proper launch params.
|
// STEP 4: Navigate to the meeting URL with proper launch params.
|
||||||
// CRITICAL: The suppress params (msLaunch, suppressPrompt, directDl) must
|
// CRITICAL: The suppress params (msLaunch, suppressPrompt, directDl) must
|
||||||
|
|
@ -291,7 +294,7 @@ export class BotOrchestrator {
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
this._logger.info(`STEP 4: URL after navigation: ${this._page!.url().substring(0, 150)}`);
|
this._logger.info(`STEP 4: URL after navigation: ${this._page!.url().substring(0, 150)}`);
|
||||||
await this._takeScreenshot('step4-meeting-url-loaded', true);
|
await this._takeScreenshot('step4-meeting-url-loaded', this._isDebugMode);
|
||||||
|
|
||||||
// STEP 4a: Poll for first actionable button (interstitial OR pre-join)
|
// STEP 4a: Poll for first actionable button (interstitial OR pre-join)
|
||||||
const interstitialSelectors = [
|
const interstitialSelectors = [
|
||||||
|
|
@ -327,32 +330,32 @@ export class BotOrchestrator {
|
||||||
if (!isPreJoin) {
|
if (!isPreJoin) {
|
||||||
await firstBtn.click();
|
await firstBtn.click();
|
||||||
this._logger.info(`STEP 4a: clicked interstitial: "${btnText}" (data-tid="${btnTid}")`);
|
this._logger.info(`STEP 4a: clicked interstitial: "${btnText}" (data-tid="${btnTid}")`);
|
||||||
await this._takeScreenshot('step4a-after-interstitial', true);
|
await this._takeScreenshot('step4a-after-interstitial', this._isDebugMode);
|
||||||
} else {
|
} else {
|
||||||
this._logger.info(`STEP 4a: pre-join button already visible: "${btnText}"`);
|
this._logger.info(`STEP 4a: pre-join button already visible: "${btnText}"`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this._takeScreenshot('step4a-no-buttons-found', true);
|
await this._takeScreenshot('step4a-no-buttons-found', this._isDebugMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 5: Poll for "Join now" on the pre-join screen (mic is NOT touched)
|
// STEP 5: Poll for "Join now" on the pre-join screen (mic is NOT touched)
|
||||||
await this._takeScreenshot('step5-before-join-now', true);
|
await this._takeScreenshot('step5-before-join-now', this._isDebugMode);
|
||||||
|
|
||||||
const joinNowBtn = await this._pollForElement(preJoinSelectors, 30000, 'Join now button');
|
const joinNowBtn = await this._pollForElement(preJoinSelectors, 30000, 'Join now button');
|
||||||
if (!joinNowBtn) {
|
if (!joinNowBtn) {
|
||||||
await this._takeScreenshot('step5-no-join-now', true);
|
await this._takeScreenshot('step5-no-join-now', this._isDebugMode);
|
||||||
throw new Error('"Join now" button not found on pre-join screen');
|
throw new Error('"Join now" button not found on pre-join screen');
|
||||||
}
|
}
|
||||||
await joinNowBtn.click();
|
await joinNowBtn.click();
|
||||||
this._logger.info('STEP 5: clicked "Join now", waiting for meeting');
|
this._logger.info('STEP 5: clicked "Join now", waiting for meeting');
|
||||||
await this._takeScreenshot('step5-join-now-clicked', true);
|
await this._takeScreenshot('step5-join-now-clicked', this._isDebugMode);
|
||||||
|
|
||||||
// STEP 6: Wait for meeting admission (hangup button = in meeting)
|
// STEP 6: Wait for meeting admission (hangup button = in meeting)
|
||||||
await this._waitForMeetingAdmission();
|
await this._waitForMeetingAdmission();
|
||||||
|
|
||||||
this._setState('in_meeting');
|
this._setState('in_meeting');
|
||||||
this._logger.info(`STEP 6: bot joined the meeting (authenticated as ${this._options.botAccountEmail})`);
|
this._logger.info(`STEP 6: bot joined the meeting (authenticated as ${this._options.botAccountEmail})`);
|
||||||
await this._takeScreenshot('step6-in-meeting', true);
|
await this._takeScreenshot('step6-in-meeting', this._isDebugMode);
|
||||||
|
|
||||||
this._startKeepAlive();
|
this._startKeepAlive();
|
||||||
await this._audioProcedure!.initialize();
|
await this._audioProcedure!.initialize();
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ async function main(): Promise<void> {
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
httpServer = new HttpServer({
|
httpServer = new HttpServer({
|
||||||
onJoinRequest: async (sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode) => {
|
onJoinRequest: async (sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode, debugMode) => {
|
||||||
await sessionManager.createSession(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode);
|
await sessionManager.createSession(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode, debugMode);
|
||||||
},
|
},
|
||||||
onLeaveRequest: async (sessionId) => {
|
onLeaveRequest: async (sessionId) => {
|
||||||
await sessionManager.endSession(sessionId);
|
await sessionManager.endSession(sessionId);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { config } from '../config';
|
||||||
import { runAuthTests, runSingleVariant, getVariantIds } from '../bot/authTestProcedure';
|
import { runAuthTests, runSingleVariant, getVariantIds } from '../bot/authTestProcedure';
|
||||||
|
|
||||||
export interface HttpServerCallbacks {
|
export interface HttpServerCallbacks {
|
||||||
onJoinRequest: (sessionId: string, meetingUrl: string, botName?: string, instanceId?: string, gatewayWsUrl?: string, language?: string, botAccountEmail?: string, botAccountPassword?: string, transferMode?: string) => Promise<void>;
|
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>;
|
onLeaveRequest: (sessionId: string) => Promise<void>;
|
||||||
onStatusRequest: (sessionId: string) => { state: string; error?: string } | null;
|
onStatusRequest: (sessionId: string) => { state: string; error?: string } | null;
|
||||||
}
|
}
|
||||||
|
|
@ -80,14 +80,14 @@ export class HttpServer {
|
||||||
// Deploy a new bot
|
// Deploy a new bot
|
||||||
this._app.post('/api/bot', async (req: Request, res: Response) => {
|
this._app.post('/api/bot', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode } = req.body;
|
const { sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode, debugMode } = req.body;
|
||||||
|
|
||||||
if (!sessionId || !meetingUrl) {
|
if (!sessionId || !meetingUrl) {
|
||||||
res.status(400).json({ error: 'Missing required fields: sessionId, meetingUrl' });
|
res.status(400).json({ error: 'Missing required fields: sessionId, meetingUrl' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._callbacks.onJoinRequest(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode);
|
await this._callbacks.onJoinRequest(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode, debugMode);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export class SessionManager {
|
||||||
botAccountEmail?: string,
|
botAccountEmail?: string,
|
||||||
botAccountPassword?: string,
|
botAccountPassword?: string,
|
||||||
transferMode?: string,
|
transferMode?: string,
|
||||||
|
debugMode?: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this._sessions.has(sessionId)) {
|
if (this._sessions.has(sessionId)) {
|
||||||
logger.warn(`Session ${sessionId} already exists`);
|
logger.warn(`Session ${sessionId} already exists`);
|
||||||
|
|
@ -76,6 +77,7 @@ export class SessionManager {
|
||||||
botAccountEmail: botAccountEmail,
|
botAccountEmail: botAccountEmail,
|
||||||
botAccountPassword: botAccountPassword,
|
botAccountPassword: botAccountPassword,
|
||||||
transferMode: transferMode,
|
transferMode: transferMode,
|
||||||
|
debugMode: debugMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
const orchestrator = new BotOrchestrator(
|
const orchestrator = new BotOrchestrator(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue