feat(teamsbot): dedicated bot account support with authenticated join

- New config fields: botAccountEmail, botAccountPassword for dedicated MSFT account
- BrowserBotConnector passes credentials + backgroundImageUrl to bot service
- Service passes config credentials to connector in joinMeeting
- Enables: full language settings, virtual background, no lobby wait
- Fallback: anonymous join when no bot account configured

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
patrick-motsch 2026-02-15 11:56:04 +01:00
parent 91425809c3
commit ad5c9d10cd
3 changed files with 28 additions and 6 deletions

View file

@ -36,21 +36,26 @@ class BrowserBotConnector:
instanceId: str, instanceId: str,
gatewayWsUrl: str, gatewayWsUrl: str,
language: str = "de-DE", language: str = "de-DE",
botAccountEmail: Optional[str] = None,
botAccountPassword: Optional[str] = None,
backgroundImageUrl: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Send join command to the Browser Bot service. Send join command to the Browser Bot service.
The bot will: The bot will:
1. Launch a headless browser 1. Launch a headless browser
2. Navigate to Teams web app 2. If botAccountEmail/Password provided: authenticate with Microsoft first
3. Join the meeting 3. Navigate to Teams web app and join the meeting
4. Enable captions and start scraping 4. Enable captions and start scraping
5. Connect back to Gateway via WebSocket using gatewayWsUrl 5. Connect back to Gateway via WebSocket using gatewayWsUrl
Args: Args:
gatewayWsUrl: Full WebSocket URL for the bot to connect back to gatewayWsUrl: Full WebSocket URL for the bot to connect back to
(e.g. wss://gateway-int.poweron-center.net/api/teamsbot/{instanceId}/bot/ws/{sessionId}) language: BCP-47 language code for captions spoken language
language: BCP-47 language code for captions spoken language (e.g. "de-DE", "en-US") botAccountEmail: Microsoft account email for authenticated join (None = anonymous)
botAccountPassword: Microsoft account password
backgroundImageUrl: URL to background image for virtual background
Returns: Returns:
Dict with 'success' bool and optional 'error' string. Dict with 'success' bool and optional 'error' string.
@ -67,10 +72,20 @@ class BrowserBotConnector:
"meetingUrl": meetingUrl, "meetingUrl": meetingUrl,
"botName": botName, "botName": botName,
"instanceId": instanceId, "instanceId": instanceId,
"gatewayWsUrl": gatewayWsUrl, # Full WebSocket URL for bot to connect back "gatewayWsUrl": gatewayWsUrl,
"language": language, # Spoken language for Teams captions "language": language,
} }
# Add authenticated join credentials if configured
if botAccountEmail and botAccountPassword:
payload["botAccountEmail"] = botAccountEmail
payload["botAccountPassword"] = botAccountPassword
logger.info(f"Bot will join authenticated as {botAccountEmail}")
# Add background image if configured
if backgroundImageUrl:
payload["backgroundImageUrl"] = backgroundImageUrl
try: try:
async with aiohttp.ClientSession(timeout=_BOT_TIMEOUT) as session: async with aiohttp.ClientSession(timeout=_BOT_TIMEOUT) as session:
async with session.post(f"{self.botUrl}/api/bot", json=payload) as resp: async with session.post(f"{self.botUrl}/api/bot", json=payload) as resp:

View file

@ -117,6 +117,8 @@ class TeamsbotConfig(BaseModel):
language: str = Field(default="de-DE", description="Primary language for STT/TTS") language: str = Field(default="de-DE", description="Primary language for STT/TTS")
voiceId: Optional[str] = Field(default=None, description="Google TTS voice ID (e.g., de-DE-Standard-A)") voiceId: Optional[str] = Field(default=None, description="Google TTS voice ID (e.g., de-DE-Standard-A)")
browserBotUrl: Optional[str] = Field(default=None, description="URL of the Browser Bot service. Falls back to TEAMSBOT_BROWSER_BOT_URL env variable if not set per-instance.") browserBotUrl: Optional[str] = Field(default=None, description="URL of the Browser Bot service. Falls back to TEAMSBOT_BROWSER_BOT_URL env variable if not set per-instance.")
botAccountEmail: Optional[str] = Field(default=None, description="Dedicated Microsoft account email for authenticated bot join. Leave empty for anonymous join.")
botAccountPassword: Optional[str] = Field(default=None, description="Dedicated Microsoft account password. MFA must be disabled for this account.")
triggerIntervalSeconds: int = Field(default=10, ge=3, le=60, description="Seconds between periodic AI analysis triggers") triggerIntervalSeconds: int = Field(default=10, ge=3, le=60, description="Seconds between periodic AI analysis triggers")
triggerCooldownSeconds: int = Field(default=3, ge=1, le=30, description="Minimum seconds between AI calls") triggerCooldownSeconds: int = Field(default=3, ge=1, le=30, description="Minimum seconds between AI calls")
contextWindowSegments: int = Field(default=20, ge=5, le=100, description="Number of transcript segments to include in AI context") contextWindowSegments: int = Field(default=20, ge=5, le=100, description="Number of transcript segments to include in AI context")
@ -157,6 +159,8 @@ class TeamsbotConfigUpdateRequest(BaseModel):
language: Optional[str] = None language: Optional[str] = None
voiceId: Optional[str] = None voiceId: Optional[str] = None
browserBotUrl: Optional[str] = None browserBotUrl: Optional[str] = None
botAccountEmail: Optional[str] = None
botAccountPassword: Optional[str] = None
triggerIntervalSeconds: Optional[int] = None triggerIntervalSeconds: Optional[int] = None
triggerCooldownSeconds: Optional[int] = None triggerCooldownSeconds: Optional[int] = None
contextWindowSegments: Optional[int] = None contextWindowSegments: Optional[int] = None

View file

@ -127,6 +127,9 @@ class TeamsbotService:
instanceId=self.instanceId, instanceId=self.instanceId,
gatewayWsUrl=fullGatewayWsUrl, gatewayWsUrl=fullGatewayWsUrl,
language=self.config.language, language=self.config.language,
botAccountEmail=self.config.botAccountEmail,
botAccountPassword=self.config.botAccountPassword,
backgroundImageUrl=session.get("backgroundImageUrl") or self.config.backgroundImageUrl,
) )
if result.get("success"): if result.get("success"):