From d0cbcbcb29df56ccf9d0166e96d75b8fc1d8744c Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 16 Feb 2026 11:24:19 +0100 Subject: [PATCH] fix: auth join - try multiple Teams v2 internal URL formats, skip launcher dialog, better session establishment Co-authored-by: Cursor --- src/bot/orchestrator.ts | 118 ++++++++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 23 deletions(-) diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index 468e1d7..0f9c89b 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -154,10 +154,19 @@ export class BotOrchestrator { waitUntil: 'domcontentloaded', timeout: 30000, }); - // Wait for Teams v2 to load (look for any Teams UI element) - await this._page!.waitForTimeout(5000); + // 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}`); + this._logger.info(`Teams session established at: ${teamsUrl.substring(0, 80)}...`); } catch (teamsNavError) { this._logger.warn(`Teams session establishment failed (non-fatal): ${teamsNavError}`); } @@ -169,30 +178,93 @@ export class BotOrchestrator { this._setState('navigating'); if (authenticate) { - // AUTHENTICATED JOIN: Navigate directly to the original meeting URL - // within the Teams v2 web app context. The launcher (/dl/launcher/) always - // redirects to the "light-meetings" anonymous experience regardless of auth. - // Instead, navigate directly to the original meeting URL -- Teams v2 will - // recognize the auth session and show the authenticated pre-join screen. - this._logger.info(`Authenticated: navigating directly to meeting URL: ${this._meetingUrl}`); - await this._page!.goto(this._meetingUrl, { - waitUntil: 'domcontentloaded', - timeout: 30000, - }); + // AUTHENTICATED JOIN: Navigate directly to the meeting URL within the + // Teams v2 web app context. The external launcher (/dl/launcher/) and its + // "Continue on this browser" button always redirect to the anonymous + // light-meetings experience, even with an active auth session. + // + // Strategy: Use the Teams v2 internal URL format that keeps the user + // within the authenticated Teams web app. The key is to NOT go through + // the launcher dialog at all. + this._logger.info(`Authenticated: navigating to meeting within Teams v2 app...`); - // Teams may show the launcher dialog even for direct URLs -- handle it - await this._joinProcedure.handleLauncherIfPresent(); + // Parse the meeting URL to extract meeting code and passcode + const { parseMeetingUrl } = await import('./meetingUrlParser'); + const parsed = parseMeetingUrl(this._meetingUrl); - // Wait for the pre-join page to stabilize - await this._page!.waitForTimeout(3000); + // Try multiple URL formats to find one that works within Teams v2 + const meetingUrlFormats = [ + // Format 1: Teams v2 internal pre-join (most likely to keep auth) + `https://teams.microsoft.com/v2/#/meeting-join/${parsed.meetingId || ''}${parsed.passcode ? '?p=' + parsed.passcode : ''}`, + // Format 2: Direct /meet/ URL (original meeting URL) + this._meetingUrl, + // Format 3: Teams v2 hash-based deep link + `https://teams.microsoft.com/_#/meet/${parsed.meetingId || ''}${parsed.passcode ? '?p=' + parsed.passcode : ''}`, + ]; - // Verify we're on an authenticated page (no "Type your name" input) - const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || ''); - if (pageText.includes('Enter the name') || pageText.includes('Type your name')) { - this._logger.warn('Still on anonymous page after auth navigation - auth session may not have transferred'); - } else { - this._logger.info('On authenticated pre-join page (no name input required)'); + let joinedSuccessfully = false; + + for (const url of meetingUrlFormats) { + this._logger.info(`Trying auth join URL: ${url}`); + + try { + await this._page!.goto(url, { + waitUntil: 'domcontentloaded', + timeout: 20000, + }); + await this._page!.waitForTimeout(3000); + + // Check if we landed on the authenticated pre-join page + const currentUrl = this._page!.url(); + const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || ''); + + this._logger.info(`After navigation - URL: ${currentUrl.substring(0, 100)}`); + + // If we see the launcher, DON'T click "Continue on this browser" -- that leads to anon + if (pageText.includes('Continue on this browser') || pageText.includes('Join on the Teams app')) { + this._logger.info('Launcher dialog detected - skipping it (would redirect to anonymous mode)'); + // Instead, try to find a "Use the web app" or direct join link + const useWebAppButton = await this._page!.$('a:has-text("Use the web app"), button:has-text("Use the web app")'); + if (useWebAppButton) { + await useWebAppButton.click(); + await this._page!.waitForTimeout(3000); + } + continue; // Try next URL format + } + + // Check if we're on the authenticated pre-join (no name input, has "Join now") + if (!pageText.includes('Enter the name') && !pageText.includes('Type your name') && + (pageText.includes('Join now') || pageText.includes('Join'))) { + this._logger.info('On authenticated pre-join page (no name input required)'); + joinedSuccessfully = true; + break; + } + + // Check if we ended up on anon page (light-meetings) + if (currentUrl.includes('anon=true') || currentUrl.includes('light-meetings')) { + this._logger.warn(`URL redirected to anonymous mode: ${currentUrl.substring(0, 80)}`); + continue; // Try next URL format + } + + // Check if we're already in the meeting (Teams v2 loaded meeting directly) + if (pageText.includes('Leave') && pageText.includes('Mute')) { + this._logger.info('Already in meeting after navigation (Teams v2 joined directly)'); + joinedSuccessfully = true; + break; + } + + } catch (navError) { + this._logger.warn(`Navigation failed for URL ${url.substring(0, 60)}: ${navError}`); + continue; + } } + + if (!joinedSuccessfully) { + this._logger.warn('All authenticated URL formats failed - falling back to launcher flow'); + // Last resort: use the launcher flow (will end up anonymous, but at least in the meeting) + await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); + } + } else { // ANONYMOUS JOIN: Use the launcher flow (resolves URL, adds anon params) await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);