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