From ad5c9d10cde742bed111b89f3b9087b4f04f3f5f Mon Sep 17 00:00:00 2001
From: patrick-motsch
Date: Sun, 15 Feb 2026 11:56:04 +0100
Subject: [PATCH] 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
---
.../features/teamsbot/browserBotConnector.py | 27 ++++++++++++++-----
.../features/teamsbot/datamodelTeamsbot.py | 4 +++
modules/features/teamsbot/service.py | 3 +++
3 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/modules/features/teamsbot/browserBotConnector.py b/modules/features/teamsbot/browserBotConnector.py
index 64acc891..b48280cd 100644
--- a/modules/features/teamsbot/browserBotConnector.py
+++ b/modules/features/teamsbot/browserBotConnector.py
@@ -36,21 +36,26 @@ class BrowserBotConnector:
instanceId: str,
gatewayWsUrl: str,
language: str = "de-DE",
+ botAccountEmail: Optional[str] = None,
+ botAccountPassword: Optional[str] = None,
+ backgroundImageUrl: Optional[str] = None,
) -> Dict[str, Any]:
"""
Send join command to the Browser Bot service.
The bot will:
1. Launch a headless browser
- 2. Navigate to Teams web app
- 3. Join the meeting
+ 2. If botAccountEmail/Password provided: authenticate with Microsoft first
+ 3. Navigate to Teams web app and join the meeting
4. Enable captions and start scraping
5. Connect back to Gateway via WebSocket using gatewayWsUrl
Args:
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 (e.g. "de-DE", "en-US")
+ language: BCP-47 language code for captions spoken language
+ botAccountEmail: Microsoft account email for authenticated join (None = anonymous)
+ botAccountPassword: Microsoft account password
+ backgroundImageUrl: URL to background image for virtual background
Returns:
Dict with 'success' bool and optional 'error' string.
@@ -67,10 +72,20 @@ class BrowserBotConnector:
"meetingUrl": meetingUrl,
"botName": botName,
"instanceId": instanceId,
- "gatewayWsUrl": gatewayWsUrl, # Full WebSocket URL for bot to connect back
- "language": language, # Spoken language for Teams captions
+ "gatewayWsUrl": gatewayWsUrl,
+ "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:
async with aiohttp.ClientSession(timeout=_BOT_TIMEOUT) as session:
async with session.post(f"{self.botUrl}/api/bot", json=payload) as resp:
diff --git a/modules/features/teamsbot/datamodelTeamsbot.py b/modules/features/teamsbot/datamodelTeamsbot.py
index d85e6e2f..3dee59d6 100644
--- a/modules/features/teamsbot/datamodelTeamsbot.py
+++ b/modules/features/teamsbot/datamodelTeamsbot.py
@@ -117,6 +117,8 @@ class TeamsbotConfig(BaseModel):
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)")
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")
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")
@@ -157,6 +159,8 @@ class TeamsbotConfigUpdateRequest(BaseModel):
language: Optional[str] = None
voiceId: Optional[str] = None
browserBotUrl: Optional[str] = None
+ botAccountEmail: Optional[str] = None
+ botAccountPassword: Optional[str] = None
triggerIntervalSeconds: Optional[int] = None
triggerCooldownSeconds: Optional[int] = None
contextWindowSegments: Optional[int] = None
diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py
index 8c31df3c..2441400e 100644
--- a/modules/features/teamsbot/service.py
+++ b/modules/features/teamsbot/service.py
@@ -127,6 +127,9 @@ class TeamsbotService:
instanceId=self.instanceId,
gatewayWsUrl=fullGatewayWsUrl,
language=self.config.language,
+ botAccountEmail=self.config.botAccountEmail,
+ botAccountPassword=self.config.botAccountPassword,
+ backgroundImageUrl=session.get("backgroundImageUrl") or self.config.backgroundImageUrl,
)
if result.get("success"):