service-teams-browser-bot/src/bot/backgroundProcedure.ts
ValueOn AG 79027f190b feat: authenticated bot join, virtual background, language selectors
- 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>
2026-02-15 11:56:04 +01:00

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
}
}
}