diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index 0f9c89b..e3b1b80 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -178,90 +178,142 @@ export class BotOrchestrator { this._setState('navigating'); if (authenticate) { - // 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...`); + // 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...`); // 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 || ''; - // 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 : ''}`, - ]; + this._logger.info(`Meeting ID: ${meetingId}, Passcode: ${passcode ? '***' : '(none)'}`); - let joinedSuccessfully = false; + let joinedViaForm = 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); + 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, + }); + 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)}`); + // 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")', + ]; - // 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); + 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; } - 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; + } catch { /* continue */ } } + + // 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}`); } - 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) + if (!joinedViaForm) { + this._logger.warn('Teams v2 form join did not work - falling back to launcher flow'); await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); }