fix: handle Teams inline login modal - broad selectors, no navigation, simplified post-auth
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f27233f308
commit
8597f0eb5a
2 changed files with 206 additions and 204 deletions
|
|
@ -4,18 +4,19 @@ import { Logger } from 'winston';
|
|||
/**
|
||||
* AuthProcedure - Handles Microsoft account authentication in the browser.
|
||||
*
|
||||
* Used for dedicated bot accounts (e.g. bot@valueon.ch) to join Teams meetings
|
||||
* as an authenticated user instead of an anonymous guest.
|
||||
* Supports TWO login flows:
|
||||
*
|
||||
* Benefits of authenticated join:
|
||||
* - Full access to Teams features (language settings, background effects)
|
||||
* - No lobby wait (if user is member of the meeting org)
|
||||
* - Bot name is the account display name
|
||||
* - Can set spoken language for captions
|
||||
* 1. **Inline modal flow** (when clicking "Sign in" on Teams pre-join page):
|
||||
* - An inline modal appears on the same Teams page (no URL change)
|
||||
* - Email input → Next → Password input → Sign in → Stay signed in? → Yes
|
||||
* - Then redirect chain to teams.microsoft.com/v2/ with "Join now"
|
||||
*
|
||||
* 2. **Direct navigation flow** (standalone login):
|
||||
* - Navigate to login.microsoftonline.com
|
||||
* - Standard Microsoft login form
|
||||
*/
|
||||
|
||||
const _LOGIN_URL = 'https://login.microsoftonline.com';
|
||||
const _TEAMS_URL = 'https://teams.microsoft.com';
|
||||
|
||||
export class AuthProcedure {
|
||||
private _page: Page;
|
||||
|
|
@ -28,91 +29,67 @@ export class AuthProcedure {
|
|||
|
||||
/**
|
||||
* Authenticate with Microsoft using email + password.
|
||||
* Navigates to Microsoft login, enters credentials, and waits for successful sign-in.
|
||||
*
|
||||
* @param skipNavigation - When true, assumes the login form is already loading
|
||||
* (e.g. after clicking "Sign in" on Teams pre-join page). Does NOT navigate.
|
||||
* @returns true if authentication was successful, false otherwise
|
||||
*/
|
||||
async authenticateWithMicrosoft(email: string, password: string, skipNavigation = false): Promise<boolean> {
|
||||
try {
|
||||
this._logger.info(`Authenticating as ${email}...`);
|
||||
|
||||
if (skipNavigation) {
|
||||
// When called after clicking "Sign in" on Teams pre-join page,
|
||||
// the browser is navigating to login.microsoftonline.com WITH a return URL
|
||||
// embedded by Teams. We must NOT navigate ourselves - just wait for the
|
||||
// email input to appear on whatever page we're redirected to.
|
||||
const currentUrl = this._page.url();
|
||||
this._logger.info(`Skipping navigation (preserving return URL). Current URL: ${currentUrl.substring(0, 80)}`);
|
||||
} else {
|
||||
if (!skipNavigation) {
|
||||
this._logger.info('Navigating to Microsoft login...');
|
||||
await this._page.goto(_LOGIN_URL, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000,
|
||||
});
|
||||
} else {
|
||||
this._logger.info('Inline auth flow: waiting for login form to appear on current page...');
|
||||
}
|
||||
|
||||
// Wait for email input (may take time if page is still redirecting)
|
||||
const emailInput = await this._page.waitForSelector(
|
||||
'input[type="email"], input[name="loginfmt"]',
|
||||
{ timeout: 20000 }
|
||||
);
|
||||
// Step 1: Wait for email input field.
|
||||
// Works for both the Teams inline modal and the Microsoft login portal.
|
||||
const emailInput = await this._waitForEmailInput();
|
||||
if (!emailInput) {
|
||||
this._logger.error('Could not find email input field');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enter email
|
||||
// Step 2: Enter email and click Next
|
||||
await emailInput.fill(email);
|
||||
this._logger.info('Email entered');
|
||||
|
||||
// Click Next
|
||||
const nextButton = await this._page.$('input[type="submit"], button[type="submit"]');
|
||||
if (nextButton) {
|
||||
await nextButton.click();
|
||||
} else {
|
||||
const nextClicked = await this._clickNextButton();
|
||||
if (!nextClicked) {
|
||||
this._logger.warn('Could not find Next button, pressing Enter');
|
||||
await this._page.keyboard.press('Enter');
|
||||
}
|
||||
await this._page.waitForTimeout(2000);
|
||||
|
||||
// Check for "account not found" error
|
||||
const errorElement = await this._page.$('#usernameError, [data-tid="error"]');
|
||||
if (errorElement) {
|
||||
const errorText = await errorElement.textContent();
|
||||
this._logger.error(`Login error after email: ${errorText}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for password input
|
||||
const passwordInput = await this._page.waitForSelector(
|
||||
'input[type="password"], input[name="passwd"]',
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
// Step 3: Wait for password input
|
||||
const passwordInput = await this._waitForPasswordInput();
|
||||
if (!passwordInput) {
|
||||
this._logger.error('Could not find password input field');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enter password
|
||||
// Step 4: Enter password and click Sign in
|
||||
await passwordInput.fill(password);
|
||||
this._logger.info('Password entered');
|
||||
|
||||
// Click Sign in
|
||||
const signInButton = await this._page.$('input[type="submit"], button[type="submit"]');
|
||||
if (signInButton) {
|
||||
await signInButton.click();
|
||||
} else {
|
||||
const signInClicked = await this._clickSignInButton();
|
||||
if (!signInClicked) {
|
||||
this._logger.warn('Could not find Sign in button, pressing Enter');
|
||||
await this._page.keyboard.press('Enter');
|
||||
}
|
||||
await this._page.waitForTimeout(3000);
|
||||
|
||||
// Check for MFA prompt
|
||||
// Step 5: Check for MFA
|
||||
const mfaDetected = await this._detectMfa();
|
||||
if (mfaDetected) {
|
||||
this._logger.error('MFA prompt detected - cannot authenticate automatically. Please disable MFA for the bot account.');
|
||||
this._logger.error('MFA prompt detected - cannot authenticate automatically.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for password error
|
||||
// Step 6: Check for password error
|
||||
const pwdError = await this._page.$('#passwordError, [data-tid="error"]');
|
||||
if (pwdError) {
|
||||
const errorText = await pwdError.textContent();
|
||||
|
|
@ -120,38 +97,165 @@ export class AuthProcedure {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Handle "Stay signed in?" prompt
|
||||
// Step 7: Handle "Stay signed in?" prompt
|
||||
await this._handleStaySignedIn();
|
||||
|
||||
// Verify authentication succeeded by checking for Teams or Microsoft landing
|
||||
const isAuthenticated = await this._verifyAuthentication();
|
||||
if (isAuthenticated) {
|
||||
// Auth succeeded - the redirect chain will be handled by the caller
|
||||
this._logger.info(`Successfully authenticated as ${email}`);
|
||||
} else {
|
||||
this._logger.error('Authentication verification failed - may not be signed in');
|
||||
}
|
||||
|
||||
return isAuthenticated;
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = String(error);
|
||||
// "Execution context was destroyed" means page navigated (login redirect)
|
||||
if (errorMessage.includes('Execution context was destroyed') ||
|
||||
errorMessage.includes('execution context')) {
|
||||
this._logger.info('Page navigated during auth (execution context destroyed) - treating as success');
|
||||
return true;
|
||||
}
|
||||
this._logger.error(`Authentication failed: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an email/username input field to appear.
|
||||
* Uses broad selectors that work on both the Teams inline modal
|
||||
* and the Microsoft login portal.
|
||||
*/
|
||||
private async _waitForEmailInput() {
|
||||
const selectors = [
|
||||
'input[type="email"]',
|
||||
'input[name="loginfmt"]',
|
||||
'input[name="login"]',
|
||||
'input[name="email"]',
|
||||
'input[autocomplete="username"]',
|
||||
];
|
||||
const combinedSelector = selectors.join(', ');
|
||||
|
||||
this._logger.info('Waiting for email input field...');
|
||||
try {
|
||||
const element = await this._page.waitForSelector(combinedSelector, {
|
||||
timeout: 30000,
|
||||
state: 'visible',
|
||||
});
|
||||
const url = this._page.url();
|
||||
const matchedTag = await element?.evaluate(el => `${el.tagName}[type=${el.getAttribute('type')}, name=${el.getAttribute('name')}]`);
|
||||
this._logger.info(`Found email input: ${matchedTag} on URL: ${url.substring(0, 80)}`);
|
||||
return element;
|
||||
} catch {
|
||||
// Fallback: look for any visible text input that could be an email field
|
||||
this._logger.info('Primary email selectors failed, trying fallback (any visible text input)...');
|
||||
try {
|
||||
const fallback = await this._page.waitForSelector(
|
||||
'input[type="text"]:visible, input:not([type]):visible',
|
||||
{ timeout: 10000, state: 'visible' }
|
||||
);
|
||||
if (fallback) {
|
||||
const url = this._page.url();
|
||||
this._logger.info(`Found fallback text input on URL: ${url.substring(0, 80)}`);
|
||||
return fallback;
|
||||
}
|
||||
} catch {
|
||||
// Log page state for debugging
|
||||
await this._logPageState('email input not found');
|
||||
}
|
||||
}
|
||||
this._logger.error('Could not find email input field');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the password input field to appear.
|
||||
*/
|
||||
private async _waitForPasswordInput() {
|
||||
this._logger.info('Waiting for password input field...');
|
||||
try {
|
||||
const element = await this._page.waitForSelector(
|
||||
'input[type="password"], input[name="passwd"], input[name="password"]',
|
||||
{ timeout: 15000, state: 'visible' }
|
||||
);
|
||||
const url = this._page.url();
|
||||
this._logger.info(`Found password input on URL: ${url.substring(0, 80)}`);
|
||||
return element;
|
||||
} catch {
|
||||
await this._logPageState('password input not found');
|
||||
}
|
||||
this._logger.error('Could not find password input field');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "Next" button after entering email.
|
||||
* Works on both Teams inline modal and Microsoft login portal.
|
||||
*/
|
||||
private async _clickNextButton(): Promise<boolean> {
|
||||
const selectors = [
|
||||
'input[type="submit"]',
|
||||
'button[type="submit"]',
|
||||
'button:has-text("Next")',
|
||||
'button:has-text("Weiter")',
|
||||
'input[value="Next"]',
|
||||
'input[value="Weiter"]',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
try {
|
||||
const button = await this._page.$(selector);
|
||||
if (button && await button.isVisible()) {
|
||||
await button.click();
|
||||
this._logger.info(`Clicked Next: ${selector}`);
|
||||
await this._page.waitForTimeout(2000);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "Sign in" button after entering password.
|
||||
* Works on both Teams inline modal and Microsoft login portal.
|
||||
*/
|
||||
private async _clickSignInButton(): Promise<boolean> {
|
||||
const selectors = [
|
||||
'input[type="submit"]',
|
||||
'button[type="submit"]',
|
||||
'button:has-text("Sign in")',
|
||||
'button:has-text("Anmelden")',
|
||||
'input[value="Sign in"]',
|
||||
'input[value="Anmelden"]',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
try {
|
||||
const button = await this._page.$(selector);
|
||||
if (button && await button.isVisible()) {
|
||||
await button.click();
|
||||
this._logger.info(`Clicked Sign in: ${selector}`);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MFA prompts (authenticator app, SMS, phone call).
|
||||
*/
|
||||
private async _detectMfa(): Promise<boolean> {
|
||||
const mfaSelectors = [
|
||||
'#idDiv_SAOTCAS_Description', // Authenticator app prompt
|
||||
'#idDiv_SAOTCC_Description', // Code entry
|
||||
'#idDiv_SAASDS_Description', // SMS verification
|
||||
'[data-tid="phoneVerification"]', // Phone verification
|
||||
'text=Approve sign in request', // Authenticator approval
|
||||
'text=Enter code', // Code entry
|
||||
'text=Verify your identity', // Generic MFA
|
||||
'text=Approve sign-in request', // Authenticator
|
||||
'#idDiv_SAOTCAS_Description',
|
||||
'#idDiv_SAOTCC_Description',
|
||||
'#idDiv_SAASDS_Description',
|
||||
'[data-tid="phoneVerification"]',
|
||||
'text=Approve sign in request',
|
||||
'text=Enter code',
|
||||
'text=Verify your identity',
|
||||
'text=Approve sign-in request',
|
||||
];
|
||||
|
||||
for (const selector of mfaSelectors) {
|
||||
|
|
@ -172,10 +276,9 @@ export class AuthProcedure {
|
|||
*/
|
||||
private async _handleStaySignedIn(): Promise<void> {
|
||||
try {
|
||||
// Look for "Stay signed in?" or "Angemeldet bleiben?" prompt
|
||||
const staySignedInSelectors = [
|
||||
'input#idSIButton9', // "Yes" button (Stay signed in)
|
||||
'button#idSIButton9', // Alternative
|
||||
'input#idSIButton9',
|
||||
'button#idSIButton9',
|
||||
'input[value="Yes"]',
|
||||
'input[value="Ja"]',
|
||||
'button:has-text("Yes")',
|
||||
|
|
@ -196,7 +299,6 @@ export class AuthProcedure {
|
|||
}
|
||||
}
|
||||
|
||||
// No prompt found - that's OK, some tenants don't show it
|
||||
this._logger.debug('No "Stay signed in" prompt detected');
|
||||
} catch (error) {
|
||||
this._logger.debug(`Stay signed in handling: ${error}`);
|
||||
|
|
@ -204,91 +306,26 @@ export class AuthProcedure {
|
|||
}
|
||||
|
||||
/**
|
||||
* Verify that authentication was successful.
|
||||
* Checks if we're on a Microsoft/Teams page with an authenticated session.
|
||||
*
|
||||
* Note: After successful login, Microsoft often triggers navigation/redirects
|
||||
* which can destroy the execution context. An "Execution context was destroyed"
|
||||
* error is treated as a successful login (navigation = login worked).
|
||||
* Log current page state for debugging when a selector is not found.
|
||||
*/
|
||||
private async _verifyAuthentication(): Promise<boolean> {
|
||||
private async _logPageState(context: string): Promise<void> {
|
||||
try {
|
||||
// Wait for navigation that indicates login succeeded (redirect to Teams/Office)
|
||||
try {
|
||||
await this._page.waitForNavigation({
|
||||
url: (url) =>
|
||||
url.href.includes('teams.microsoft.com') ||
|
||||
url.href.includes('office.com') ||
|
||||
url.href.includes('myapps.microsoft.com') ||
|
||||
url.href.includes('microsoftonline.com/common/oauth2'),
|
||||
timeout: 15000,
|
||||
});
|
||||
this._logger.info('Navigation detected after login - authentication succeeded');
|
||||
return true;
|
||||
} catch (navError) {
|
||||
const errorMessage = String(navError);
|
||||
|
||||
// "Execution context was destroyed" means the page navigated away
|
||||
// from the login page, which indicates a successful login redirect
|
||||
if (errorMessage.includes('Execution context was destroyed') ||
|
||||
errorMessage.includes('execution context') ||
|
||||
errorMessage.includes('navigation')) {
|
||||
this._logger.info('Execution context destroyed during verification - treating as successful login (page navigated)');
|
||||
// Give the page a moment to settle after navigation
|
||||
await this._page.waitForTimeout(2000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Timeout - check where we ended up
|
||||
this._logger.debug(`waitForNavigation did not match expected URL: ${navError}`);
|
||||
}
|
||||
|
||||
const url = this._page.url();
|
||||
|
||||
// If we're on Teams or Microsoft portal, we're authenticated
|
||||
if (url.includes('teams.microsoft.com') ||
|
||||
url.includes('office.com') ||
|
||||
url.includes('microsoftonline.com/common/oauth2') ||
|
||||
url.includes('myapps.microsoft.com')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wait a bit and check again (redirects may be in progress)
|
||||
await this._page.waitForTimeout(3000);
|
||||
const finalUrl = this._page.url();
|
||||
|
||||
if (finalUrl.includes('teams.microsoft.com') ||
|
||||
finalUrl.includes('office.com') ||
|
||||
finalUrl.includes('portal.office.com')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for any error states
|
||||
const loginPage = finalUrl.includes('login.microsoftonline.com');
|
||||
if (loginPage) {
|
||||
// Still on login page - authentication may have failed
|
||||
const hasError = await this._page.$('[data-tid="error"], #passwordError, #usernameError');
|
||||
if (hasError) {
|
||||
return false;
|
||||
}
|
||||
// Might still be processing
|
||||
await this._page.waitForTimeout(5000);
|
||||
const afterWaitUrl = this._page.url();
|
||||
return !afterWaitUrl.includes('login.microsoftonline.com');
|
||||
}
|
||||
|
||||
// If we're somewhere else entirely, assume authenticated
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMessage = String(error);
|
||||
// Catch "Execution context was destroyed" at the top level too
|
||||
if (errorMessage.includes('Execution context was destroyed') ||
|
||||
errorMessage.includes('execution context')) {
|
||||
this._logger.info('Execution context destroyed during verification (top-level) - treating as successful login');
|
||||
return true;
|
||||
}
|
||||
this._logger.error(`Authentication verification error: ${error}`);
|
||||
return false;
|
||||
const title = await this._page.title();
|
||||
const bodyText = await this._page.evaluate(() =>
|
||||
document.body?.innerText?.substring(0, 300) || '(empty)'
|
||||
);
|
||||
const inputs = await this._page.evaluate(() => {
|
||||
const allInputs = document.querySelectorAll('input');
|
||||
return Array.from(allInputs).map(i =>
|
||||
`${i.tagName}[type=${i.type}, name=${i.name}, placeholder=${i.placeholder}]`
|
||||
).join(', ');
|
||||
});
|
||||
this._logger.warn(`Page state (${context}): URL=${url.substring(0, 80)}, Title=${title}`);
|
||||
this._logger.warn(`Inputs on page: ${inputs.substring(0, 300)}`);
|
||||
this._logger.warn(`Page text: ${bodyText.substring(0, 200)}`);
|
||||
} catch (err) {
|
||||
this._logger.warn(`Could not log page state: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,11 +200,11 @@ export class BotOrchestrator {
|
|||
}
|
||||
|
||||
if (signInClicked) {
|
||||
// Clicking "Sign in" on the Teams pre-join page triggers a redirect chain
|
||||
// to login.microsoftonline.com WITH a return URL embedded by Teams.
|
||||
// We must NOT navigate to login.microsoftonline.com ourselves - that would
|
||||
// destroy the return URL. Instead, pass skipNavigation=true and let
|
||||
// authenticateWithMicrosoft wait for the email input to appear.
|
||||
// Clicking "Sign in" on the Teams pre-join page opens an INLINE LOGIN MODAL
|
||||
// directly on the same page (no URL change). The modal shows an email input,
|
||||
// then password, then "Stay signed in?" — all on the light-meetings page.
|
||||
// After completing login, Teams redirects to /v2/ with the "Join now" button.
|
||||
// We pass skipNavigation=true so authProcedure does NOT navigate away.
|
||||
const { AuthProcedure } = await import('./authProcedure');
|
||||
const authProcedure = new AuthProcedure(this._page!, this._logger);
|
||||
const authSuccess = await authProcedure.authenticateWithMicrosoft(
|
||||
|
|
@ -216,57 +216,22 @@ export class BotOrchestrator {
|
|||
if (authSuccess) {
|
||||
this._logger.info('Authentication via "Sign in" link succeeded');
|
||||
|
||||
// After login, the redirect chain goes automatically:
|
||||
// login.microsoftonline.com → teams.microsoft.com/v2/?meetingjoin=true#/meet/...
|
||||
// The return URL is embedded by Teams when clicking "Sign in".
|
||||
// We just wait for the redirect to complete.
|
||||
this._logger.info('Waiting for redirect chain to leave login.microsoftonline.com...');
|
||||
// After the inline login flow completes (email → password → stay signed in),
|
||||
// Teams triggers a redirect chain that ends at teams.microsoft.com/v2/
|
||||
// with the authenticated pre-join page containing the "Join now" button.
|
||||
// Simply wait for that button to appear — no manual navigation needed.
|
||||
this._logger.info('Waiting for authenticated pre-join page (Join now button)...');
|
||||
|
||||
// Step 1: Wait for URL to leave login.microsoftonline.com (poll up to 45s)
|
||||
const maxWaitMs = 45000;
|
||||
const pollIntervalMs = 1000;
|
||||
const startTime = Date.now();
|
||||
let leftLoginPage = false;
|
||||
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
const currentUrl = this._page!.url();
|
||||
if (!currentUrl.includes('login.microsoftonline.com')) {
|
||||
this._logger.info(`Left login page after ${Date.now() - startTime}ms. URL: ${currentUrl.substring(0, 100)}`);
|
||||
leftLoginPage = true;
|
||||
break;
|
||||
}
|
||||
await this._page!.waitForTimeout(pollIntervalMs);
|
||||
}
|
||||
|
||||
if (!leftLoginPage) {
|
||||
const stuckUrl = this._page!.url();
|
||||
this._logger.warn(`Still on login page after ${maxWaitMs}ms. URL: ${stuckUrl.substring(0, 100)}`);
|
||||
// Try navigating to meeting URL as fallback (auth cookies should be set)
|
||||
this._logger.info('Fallback: navigating to meeting URL with auth cookies...');
|
||||
await this._page!.goto(this._meetingUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
await this._joinProcedure!.handleLauncherIfPresent();
|
||||
}
|
||||
|
||||
// Step 2: Wait for the pre-join page to fully load
|
||||
// The #prejoin-join-button appears on the Teams v2 authenticated pre-join page
|
||||
const joinButtonSelector = '#prejoin-join-button, button[data-tid="prejoin-join-button"]';
|
||||
try {
|
||||
await this._page!.waitForSelector(joinButtonSelector, { timeout: 30000, state: 'visible' });
|
||||
await this._page!.waitForSelector(joinButtonSelector, { timeout: 60000, state: 'visible' });
|
||||
const finalUrl = this._page!.url();
|
||||
this._logger.info(`On authenticated pre-join page: ${finalUrl.substring(0, 100)}`);
|
||||
} catch {
|
||||
const finalUrl = this._page!.url();
|
||||
const pageContent = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 300) || '');
|
||||
this._logger.warn(`Join button not found. URL: ${finalUrl.substring(0, 100)}`);
|
||||
this._logger.warn(`Join button not found after 60s. URL: ${finalUrl.substring(0, 100)}`);
|
||||
this._logger.warn(`Page content: ${pageContent.substring(0, 200)}`);
|
||||
|
||||
// If we ended up somewhere unexpected, try meeting URL as last resort
|
||||
if (!finalUrl.includes('teams.microsoft.com')) {
|
||||
this._logger.info('Not on Teams - navigating to meeting URL as last resort...');
|
||||
await this._page!.goto(this._meetingUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
await this._joinProcedure!.handleLauncherIfPresent();
|
||||
await this._page!.waitForTimeout(5000);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._logger.warn('Authentication via "Sign in" failed - continuing as anonymous');
|
||||
|
|
|
|||
Loading…
Reference in a new issue