diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index dd8deec..a704bb7 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -91,7 +91,7 @@ export class BotOrchestrator { throw new Error(`Invalid meeting URL: ${this._meetingUrl}`); } - const isAuthenticated = !!(this._options.botAccountEmail && this._options.botAccountPassword); + let useAuthentication = !!(this._options.botAccountEmail && this._options.botAccountPassword); try { this._setState('launching'); @@ -99,61 +99,24 @@ export class BotOrchestrator { // Connect to Gateway WebSocket first await this._connectToGateway(); - // Launch browser - await this._launchBrowser(); - - // Authenticate with Microsoft if bot account is configured - if (isAuthenticated) { - const { AuthProcedure } = await import('./authProcedure'); - const authProcedure = new AuthProcedure(this._page!, this._logger); - const authSuccess = await authProcedure.authenticateWithMicrosoft( - this._options.botAccountEmail!, - this._options.botAccountPassword! - ); - if (!authSuccess) { - this._logger.warn('Microsoft authentication failed - falling back to anonymous join'); - } - } - - this._setState('navigating'); - - // Navigate to meeting and handle launcher - await this._joinProcedure!.startMeetingLauncherFlow(this._meetingUrl); - - // Set virtual background if configured (must be done on pre-join screen, before "Join now") - if (this._options.backgroundImageUrl && this._page) { - try { - const { BackgroundProcedure } = await import('./backgroundProcedure'); - const bgProcedure = new BackgroundProcedure(this._page, this._logger); - await bgProcedure.setBackgroundFromUrl(this._options.backgroundImageUrl); - } catch (error) { - this._logger.warn(`Background image setup failed (non-fatal): ${error}`); - } - } - - // Join the meeting (enter lobby for anonymous, direct join for authenticated) - await this._joinProcedure!.joinMeetingLobbyFlow(); - - // Check if we're in lobby - const inLobby = await this._joinProcedure!.isInMeetingLobby({ waitForSeconds: 10 }); - if (inLobby) { - this._setState('in_lobby'); - this._logger.info('Bot is in lobby, waiting to be admitted...'); - } - - // Wait to be admitted to the meeting - await this._waitForMeetingAdmission(); - - this._setState('in_meeting'); - this._logger.info('Bot joined the meeting!'); - - // Initialize audio - await this._audioProcedure!.initialize(); - - // Enable and subscribe to captions - await this._enableCaptions(); + // Try joining (authenticated first, then anonymous fallback) + await this._attemptJoin(useAuthentication); } catch (error) { + // If authenticated join failed, retry as anonymous + if (useAuthentication) { + this._logger.warn(`Authenticated join failed: ${(error as Error).message}. Retrying as anonymous guest...`); + try { + await this._cleanup(); + await this._attemptJoin(false); + return; + } catch (retryError) { + this._logger.error('Anonymous fallback also failed:', retryError); + this._setState('error', (retryError as Error).message); + await this._takeScreenshot('error-fallback'); + throw retryError; + } + } this._logger.error('Error starting bot:', error); this._setState('error', (error as Error).message); await this._takeScreenshot('error'); @@ -161,6 +124,87 @@ export class BotOrchestrator { } } + /** + * Attempt to join a meeting (authenticated or anonymous). + */ + private async _attemptJoin(authenticate: boolean): Promise { + // Launch browser + await this._launchBrowser(); + + // Authenticate with Microsoft if requested + if (authenticate) { + const { AuthProcedure } = await import('./authProcedure'); + const authProcedure = new AuthProcedure(this._page!, this._logger); + const authSuccess = await authProcedure.authenticateWithMicrosoft( + this._options.botAccountEmail!, + this._options.botAccountPassword! + ); + if (!authSuccess) { + throw new Error('Microsoft authentication failed'); + } + } + + // Update JoinProcedure with correct auth state + this._joinProcedure = new JoinProcedure(this._page!, this._logger, this._botName, authenticate); + + this._setState('navigating'); + + // Navigate to meeting and handle launcher + await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); + + // Set virtual background if configured (must be done on pre-join screen, before "Join now") + if (this._options.backgroundImageUrl && this._page && authenticate) { + try { + const { BackgroundProcedure } = await import('./backgroundProcedure'); + const bgProcedure = new BackgroundProcedure(this._page, this._logger); + await bgProcedure.setBackgroundFromUrl(this._options.backgroundImageUrl); + } catch (error) { + this._logger.warn(`Background image setup failed (non-fatal): ${error}`); + } + } + + // Join the meeting + await this._joinProcedure.joinMeetingLobbyFlow(); + + // Check if we're in lobby + const inLobby = await this._joinProcedure.isInMeetingLobby({ waitForSeconds: 10 }); + if (inLobby) { + this._setState('in_lobby'); + this._logger.info('Bot is in lobby, waiting to be admitted...'); + } + + // Wait to be admitted to the meeting + await this._waitForMeetingAdmission(); + + this._setState('in_meeting'); + this._logger.info(`Bot joined the meeting! (authenticated: ${authenticate})`); + + // Initialize audio + await this._audioProcedure!.initialize(); + + // Enable and subscribe to captions + await this._enableCaptions(); + } + + /** + * Clean up browser for retry (close browser without full shutdown). + */ + private async _cleanup(): Promise { + try { + if (this._page) await this._page.close().catch(() => {}); + if (this._context) await this._context.close().catch(() => {}); + if (this._browser) await this._browser.close().catch(() => {}); + this._page = null; + this._context = null; + this._browser = null; + this._joinProcedure = null; + this._captionsProcedure = null; + this._audioProcedure = null; + } catch { + // Ignore cleanup errors + } + } + /** * Connect to the Gateway WebSocket for this session. */