fix: auth join via 'Sign in' link on pre-join page (correct Teams flow: launcher -> pre-join -> sign in -> auth pre-join -> join)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1972f698b4
commit
b07910410e
1 changed files with 82 additions and 171 deletions
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue