diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index e3b1b80..0b2b84f 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -133,193 +133,104 @@ export class BotOrchestrator { // 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'); - } - - // CRITICAL: After auth, navigate to Teams web app first to establish - // a Teams session. Without this, Teams redirects to anonymous mode - // when navigating directly to the meeting URL. - this._logger.info('Establishing Teams session after auth...'); - try { - await this._page!.goto('https://teams.microsoft.com', { - waitUntil: 'domcontentloaded', - timeout: 30000, - }); - // Wait for Teams v2 to fully load and establish the auth session - // Teams v2 does multiple redirects (OAuth callback -> v2 app) which takes time - try { - await this._page!.waitForSelector('[data-tid="app-layout"], [data-tid="left-rail"], [class*="teams-"]', { - timeout: 15000 - }); - this._logger.info('Teams v2 app loaded (UI elements detected)'); - } catch { - // Timeout waiting for UI elements, but auth session may still be established - await this._page!.waitForTimeout(5000); - } - const teamsUrl = this._page!.url(); - this._logger.info(`Teams session established at: ${teamsUrl.substring(0, 80)}...`); - } catch (teamsNavError) { - this._logger.warn(`Teams session establishment failed (non-fatal): ${teamsNavError}`); - } - } - // Update JoinProcedure with correct auth state this._joinProcedure = new JoinProcedure(this._page!, this._logger, this._botName, authenticate); this._setState('navigating'); + // STEP 1: Navigate to meeting URL and click "Continue on this browser" + // This is the same for both authenticated and anonymous joins. + await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); + + // STEP 2: For authenticated joins, click "Sign in" on the pre-join page + // instead of entering a name. The "Sign in" link is at the bottom of the + // anonymous pre-join page. Clicking it triggers the Microsoft login flow, + // which redirects back to an authenticated pre-join page within Teams v2. if (authenticate) { - // AUTHENTICATED JOIN: Use the Teams v2 "Join a meeting" form. - // The Teams v2 app (already loaded after auth) has a "Join a meeting" page - // with Meeting ID and Passcode fields. This keeps the user authenticated - // and avoids the external launcher which always redirects to anonymous mode. - this._logger.info(`Authenticated: joining meeting via Teams v2 "Join a meeting" form...`); + this._logger.info('Authenticated join: looking for "Sign in" link on pre-join page...'); - // Parse the meeting URL to extract meeting code and passcode - const { parseMeetingUrl } = await import('./meetingUrlParser'); - const parsed = parseMeetingUrl(this._meetingUrl); - const meetingId = parsed.meetingId || ''; - const passcode = parsed.passcode || ''; + // Wait for the pre-join page to load + await this._page!.waitForTimeout(3000); - this._logger.info(`Meeting ID: ${meetingId}, Passcode: ${passcode ? '***' : '(none)'}`); + // Dismiss mic/camera permission overlay if present + // The "Continue without audio or video" modal may appear here + await this._joinProcedure.dismissBrowserPermissionModals(); - let joinedViaForm = false; + // Find and click the "Sign in" link at the bottom of the pre-join page + const signInSelectors = [ + 'a:has-text("Sign in")', + 'button:has-text("Sign in")', + 'a:has-text("Anmelden")', + 'button:has-text("Anmelden")', + 'a[href*="login"]', + ]; - try { - // Navigate to the Teams v2 "Join a meeting" page - // This page is available within the authenticated Teams v2 app - await this._page!.goto('https://teams.microsoft.com/v2/', { - waitUntil: 'domcontentloaded', - timeout: 20000, + let signInClicked = false; + for (const sel of signInSelectors) { + try { + const link = await this._page!.$(sel); + if (link) { + await link.click(); + this._logger.info(`Clicked "Sign in" link: ${sel}`); + signInClicked = true; + break; + } + } catch { /* continue */ } + } + + if (!signInClicked) { + // Fallback: try to find "Sign in" by evaluating text content + signInClicked = await this._page!.evaluate(() => { + const links = document.querySelectorAll('a, button'); + for (let i = 0; i < links.length; i++) { + const el = links[i] as HTMLElement; + const text = el.innerText?.trim().toLowerCase() || ''; + if (text === 'sign in' || text === 'anmelden') { + el.click(); + return true; + } + } + return false; }); + if (signInClicked) { + this._logger.info('Clicked "Sign in" via DOM evaluation'); + } + } + + if (signInClicked) { + // Wait for Microsoft login page to load await this._page!.waitForTimeout(3000); - // Look for "Join a meeting" link/button in the Teams v2 sidebar or header - const joinMeetingSelectors = [ - 'button:has-text("Join a meeting")', - 'a:has-text("Join a meeting")', - 'button:has-text("Join with an ID")', - 'a:has-text("Join with an ID")', - '[data-tid="join-meeting-button"]', - 'button:has-text("An Besprechung teilnehmen")', - ]; + // Perform Microsoft login (email, password, stay signed in) + const { AuthProcedure } = await import('./authProcedure'); + const authProcedure = new AuthProcedure(this._page!, this._logger); + const authSuccess = await authProcedure.authenticateWithMicrosoft( + this._options.botAccountEmail!, + this._options.botAccountPassword! + ); - let foundJoinPage = false; - for (const sel of joinMeetingSelectors) { - try { - const btn = await this._page!.$(sel); - if (btn) { - await btn.click(); - this._logger.info(`Clicked: ${sel}`); - await this._page!.waitForTimeout(2000); - foundJoinPage = true; - break; - } - } catch { /* continue */ } + if (authSuccess) { + this._logger.info('Authentication via "Sign in" link succeeded'); + // After auth, Teams redirects back to the authenticated pre-join page + // within Teams v2 (/v2/) -- wait for it to load + await this._page!.waitForTimeout(5000); + + const postAuthUrl = this._page!.url(); + this._logger.info(`Post-auth URL: ${postAuthUrl.substring(0, 80)}`); + + // Verify we're on the authenticated pre-join page + const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || ''); + if (pageText.includes('Join now')) { + this._logger.info('On authenticated pre-join page with "Join now" button'); + } else { + this._logger.warn(`Post-auth page content: ${pageText.substring(0, 200)}`); + } + } else { + this._logger.warn('Authentication via "Sign in" failed - continuing as anonymous'); } - - // Check if we're on the "Join a meeting" form page - const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || ''); - - if (pageText.includes('Meeting ID') || pageText.includes('Join a meeting') || pageText.includes('Besprechungs-ID')) { - this._logger.info('On "Join a meeting" form page - filling in meeting details'); - - // Fill in the Meeting ID field - const meetingIdSelectors = [ - 'input[placeholder*="Meeting ID" i]', - 'input[placeholder*="Besprechungs-ID" i]', - 'input[aria-label*="Meeting ID" i]', - 'input[aria-label*="Besprechungs-ID" i]', - 'input[name*="meetingId" i]', - 'input[type="text"]:first-of-type', - ]; - - for (const sel of meetingIdSelectors) { - try { - const input = await this._page!.$(sel); - if (input) { - await input.click(); - await input.fill(meetingId); - this._logger.info(`Filled Meeting ID: ${meetingId}`); - break; - } - } catch { /* continue */ } - } - - // Fill in the Passcode field (if we have one) - if (passcode) { - const passcodeSelectors = [ - 'input[placeholder*="passcode" i]', - 'input[placeholder*="Passcode" i]', - 'input[placeholder*="Kennung" i]', - 'input[aria-label*="passcode" i]', - 'input[aria-label*="Kennung" i]', - 'input[name*="passcode" i]', - 'input[type="text"]:nth-of-type(2)', - 'input[type="password"]', - ]; - - for (const sel of passcodeSelectors) { - try { - const input = await this._page!.$(sel); - if (input) { - await input.click(); - await input.fill(passcode); - this._logger.info('Filled Passcode'); - break; - } - } catch { /* continue */ } - } - } - - // Click "Join meeting" button - await this._page!.waitForTimeout(500); - const joinBtnSelectors = [ - 'button:has-text("Join meeting")', - 'button:has-text("An Besprechung teilnehmen")', - 'button:has-text("Beitreten")', - 'button[data-tid="join-meeting-submit"]', - ]; - - for (const sel of joinBtnSelectors) { - try { - const btn = await this._page!.$(sel); - if (btn) { - await btn.click(); - this._logger.info(`Clicked: ${sel}`); - joinedViaForm = true; - break; - } - } catch { /* continue */ } - } - - if (joinedViaForm) { - // Wait for the pre-join/meeting page to load - await this._page!.waitForTimeout(5000); - this._logger.info('Submitted meeting join form - waiting for pre-join page'); - } - } - } catch (formError) { - this._logger.warn(`Teams v2 form join failed: ${formError}`); + } else { + this._logger.warn('Could not find "Sign in" link - continuing as anonymous'); } - - if (!joinedViaForm) { - this._logger.warn('Teams v2 form join did not work - falling back to launcher flow'); - await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); - } - - } else { - // ANONYMOUS JOIN: Use the launcher flow (resolves URL, adds anon params) - await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); } // Set virtual background if configured (must be done on pre-join screen, before "Join now")