- New authProcedure.ts: Microsoft login flow (email/password, MFA detection, stay signed in) - New backgroundProcedure.ts: download image URL, upload as Teams virtual background - Orchestrator: authenticate before join when botAccountEmail provided - JoinProcedure: skip name input for authenticated joins - meetingUrlParser: anon=true only for anonymous joins - SessionManager/HttpServer: pass new fields through the chain - Updated Teams caption language selectors for current UI Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
5.6 KiB
TypeScript
182 lines
5.6 KiB
TypeScript
import { Page } from 'playwright';
|
|
import { Logger } from 'winston';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
|
|
/**
|
|
* BackgroundProcedure - Handles setting a virtual background in Teams pre-join screen.
|
|
*
|
|
* Must be called AFTER the bot is on the pre-join screen but BEFORE clicking "Join now".
|
|
* Only works for authenticated joins (anonymous guests may not have background options).
|
|
*/
|
|
export class BackgroundProcedure {
|
|
private _page: Page;
|
|
private _logger: Logger;
|
|
|
|
constructor(page: Page, logger: Logger) {
|
|
this._page = page;
|
|
this._logger = logger;
|
|
}
|
|
|
|
/**
|
|
* Set a virtual background from a URL on the Teams pre-join screen.
|
|
*
|
|
* @param imageUrl - URL of the background image to download and apply
|
|
* @returns true if background was set successfully
|
|
*/
|
|
async setBackgroundFromUrl(imageUrl: string): Promise<boolean> {
|
|
try {
|
|
this._logger.info(`Setting virtual background from: ${imageUrl}`);
|
|
|
|
// Download the image to a temp file
|
|
const tempDir = os.tmpdir();
|
|
const tempFile = path.join(tempDir, `poweron-bg-${Date.now()}.jpg`);
|
|
|
|
const response = await fetch(imageUrl);
|
|
if (!response.ok) {
|
|
this._logger.error(`Failed to download background image: ${response.status} ${response.statusText}`);
|
|
return false;
|
|
}
|
|
|
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
fs.writeFileSync(tempFile, buffer);
|
|
this._logger.info(`Background image downloaded: ${buffer.length} bytes -> ${tempFile}`);
|
|
|
|
// Open background effects panel
|
|
const panelOpened = await this._openBackgroundEffectsPanel();
|
|
if (!panelOpened) {
|
|
this._logger.warn('Could not open background effects panel - skipping background');
|
|
this._cleanup(tempFile);
|
|
return false;
|
|
}
|
|
|
|
// Upload the image
|
|
const uploaded = await this._uploadBackgroundImage(tempFile);
|
|
this._cleanup(tempFile);
|
|
|
|
if (uploaded) {
|
|
this._logger.info('Virtual background set successfully');
|
|
} else {
|
|
this._logger.warn('Could not upload background image');
|
|
}
|
|
|
|
return uploaded;
|
|
|
|
} catch (error) {
|
|
this._logger.error(`Background setup failed: ${error}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open the background effects panel on the pre-join screen.
|
|
*/
|
|
private async _openBackgroundEffectsPanel(): Promise<boolean> {
|
|
const backgroundButtonSelectors = [
|
|
'button[data-tid="toggle-background-effect"]',
|
|
'button[aria-label*="Background" i]',
|
|
'button[aria-label*="Hintergrund" i]',
|
|
'button[aria-label*="background filters" i]',
|
|
'button[aria-label*="Hintergrundeffekte" i]',
|
|
'#video-background-effects-button',
|
|
];
|
|
|
|
for (const selector of backgroundButtonSelectors) {
|
|
try {
|
|
const button = await this._page.$(selector);
|
|
if (button) {
|
|
await button.click();
|
|
this._logger.info(`Clicked background effects button: ${selector}`);
|
|
await this._page.waitForTimeout(2000);
|
|
return true;
|
|
}
|
|
} catch {
|
|
// Continue trying
|
|
}
|
|
}
|
|
|
|
this._logger.warn('Background effects button not found');
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Upload a background image via the file input in the background effects panel.
|
|
*/
|
|
private async _uploadBackgroundImage(filePath: string): Promise<boolean> {
|
|
try {
|
|
// Look for "Add new" or "+" button to upload custom image
|
|
const addButtonSelectors = [
|
|
'button[aria-label*="Add new" i]',
|
|
'button[aria-label*="Neu hinzufügen" i]',
|
|
'button[aria-label*="add image" i]',
|
|
'button[aria-label*="Bild hinzufügen" i]',
|
|
'button[data-tid="add-background-image"]',
|
|
'.add-image-button',
|
|
];
|
|
|
|
let addButtonClicked = false;
|
|
for (const selector of addButtonSelectors) {
|
|
try {
|
|
const button = await this._page.$(selector);
|
|
if (button) {
|
|
await button.click();
|
|
this._logger.info(`Clicked add image button: ${selector}`);
|
|
addButtonClicked = true;
|
|
await this._page.waitForTimeout(1000);
|
|
break;
|
|
}
|
|
} catch {
|
|
// Continue
|
|
}
|
|
}
|
|
|
|
// Try to find file input (may appear after clicking add, or may already exist)
|
|
const fileInput = await this._page.$('input[type="file"]');
|
|
if (fileInput) {
|
|
await fileInput.setInputFiles(filePath);
|
|
this._logger.info('Background image uploaded via file input');
|
|
await this._page.waitForTimeout(2000);
|
|
|
|
// The uploaded image should be auto-selected, but click it to be sure
|
|
// Look for the last image in the background gallery (newly uploaded)
|
|
try {
|
|
const images = await this._page.$$('[data-tid="background-image"], .background-image-item');
|
|
if (images.length > 0) {
|
|
const lastImage = images[images.length - 1];
|
|
await lastImage.click();
|
|
this._logger.info('Selected uploaded background image');
|
|
await this._page.waitForTimeout(1000);
|
|
}
|
|
} catch {
|
|
this._logger.debug('Could not click uploaded image - may be auto-selected');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (!addButtonClicked) {
|
|
this._logger.warn('No add-image button or file input found');
|
|
}
|
|
|
|
return false;
|
|
|
|
} catch (error) {
|
|
this._logger.error(`Background image upload failed: ${error}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up temp file.
|
|
*/
|
|
private _cleanup(filePath: string): void {
|
|
try {
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
}
|