diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts index dc3ea54..4be8097 100644 --- a/src/bot/chatProcedure.ts +++ b/src/bot/chatProcedure.ts @@ -58,33 +58,64 @@ export class ChatProcedure { this._logger.info('Chat panel opened'); } + /** + * Check if the chat panel is currently visible by probing for known + * UI elements (chat input, message list, or aria-pressed toggle). + */ + private async _isChatPanelOpen(): Promise { + return this._page.evaluate(() => { + // 1. Chat button toggle state + const chatBtn = document.querySelector('button[id="chat-button"], button[data-tid="chat-button"]') as HTMLElement | null; + if (chatBtn?.getAttribute('aria-pressed') === 'true') return true; + + // 2. Chat input visible (multiple Teams UI variants) + const inputSelectors = [ + '[data-tid="ckeditor-replyConversation"]', + 'div[role="textbox"][data-tid*="chat"]', + 'div[role="textbox"][aria-label*="message" i]', + 'div[role="textbox"][aria-label*="Nachricht" i]', + '[contenteditable="true"][aria-label*="message" i]', + '[contenteditable="true"][aria-label*="Nachricht" i]', + '[placeholder*="message" i]', + '[placeholder*="Nachricht" i]', + 'div[aria-label="Type a message"]', + ]; + for (const sel of inputSelectors) { + const el = document.querySelector(sel) as HTMLElement | null; + if (el && el.offsetHeight > 0) return true; + } + + // 3. Message list container visible + const listSelectors = [ + '[data-tid="message-pane-list"]', + '[data-tid="chat-pane-list"]', + '[data-tid="chat-pane"]', + '.ts-message-list-container', + '[role="log"]', + ]; + for (const sel of listSelectors) { + const el = document.querySelector(sel) as HTMLElement | null; + if (el && el.offsetHeight > 50) return true; + } + + // 4. "Meeting chat" heading visible + const headings = document.querySelectorAll('h2, h3, [role="heading"]'); + for (const h of Array.from(headings)) { + const txt = (h as HTMLElement).innerText?.toLowerCase() || ''; + if (txt.includes('meeting chat') || txt.includes('besprechungschat')) return true; + } + + return false; + }); + } + /** * Open the chat panel by clicking the chat button. * In authenticated Teams, the chat panel may already be open (meeting loads * from a chat thread). Clicking again would TOGGLE it closed. */ private async _openChatPanel(): Promise { - // Check if chat panel is already open before clicking (avoid toggle-off) - const alreadyOpen = await this._page.evaluate(() => { - const chatBtn = document.querySelector('button[id="chat-button"], button[data-tid="chat-button"]') as HTMLElement | null; - if (chatBtn) { - // Teams toggle buttons use aria-pressed="true" when active - if (chatBtn.getAttribute('aria-pressed') === 'true') return true; - } - // Also check if chat input or message list is visible (panel is open) - const chatInput = document.querySelector( - '[data-tid="ckeditor-replyConversation"], div[role="textbox"][data-tid*="chat"], div[role="textbox"][aria-label*="message" i]' - ) as HTMLElement | null; - if (chatInput && chatInput.offsetHeight > 0) return true; - // Check if message list container is visible - const messageList = document.querySelector( - '[data-tid="message-pane-list"], [data-tid="chat-pane-list"], [data-tid="chat-pane"]' - ) as HTMLElement | null; - if (messageList && messageList.offsetHeight > 50) return true; - return false; - }); - - if (alreadyOpen) { + if (await this._isChatPanelOpen()) { this._logger.info('Chat panel already open - skipping toggle'); return; } @@ -369,27 +400,23 @@ export class ChatProcedure { */ private _startPeriodicChatScan(): void { if (this._scanIntervalId) return; - const intervalMs = 2500; + const intervalMs = 5000; this._scanIntervalId = setInterval(async () => { if (!this._isSubscribed || !this._page) return; try { - const chatPanelOpen = await this._page.evaluate(() => { - const chatBtn = document.querySelector('button[id="chat-button"], button[data-tid="chat-button"]') as HTMLElement | null; - if (chatBtn?.getAttribute('aria-pressed') === 'true') return true; - const messageList = document.querySelector('[data-tid="message-pane-list"], [data-tid="chat-pane-list"], [data-tid="chat-pane"]') as HTMLElement | null; - return !!(messageList && messageList.offsetHeight > 50); - }); - if (!chatPanelOpen) { + if (!(await this._isChatPanelOpen())) { this._logger.info('Chat panel closed, reopening'); await this._openChatPanel(); + await this._page.waitForTimeout(1000); } const knownKeys = Array.from(this._recentMessageKeyTimestamps.keys()); - const messages = await this._page.evaluate((knownKeysArr: string[]) => { + const scanResult = await this._page.evaluate((knownKeysArr: string[]) => { const known = new Set(knownKeysArr); const noisePatterns = [ 'meeting ended', 'meeting started', 'was invited', 'left the chat', 'joined the meeting', 'left the meeting', 'doesn\'t have a teams account', + 'verify their identity', ]; function isNoise(t: string) { const l = t.toLowerCase(); @@ -397,15 +424,45 @@ export class ChatProcedure { } const results: Array<{ speaker: string; text: string; timestamp: string; teamsTimestamp?: string; messageKey: string }> = []; const seenThisScan = new Set(); - const container = document.querySelector('[data-tid="message-pane-list"], [data-tid="chat-pane-list"], [data-tid="chat-pane"]') || document.body; - const candidates = container.querySelectorAll('[data-tid="chat-message"], .fui-ChatMessage, [data-tid*="chat-pane-message"]'); + + // Strategy 1: known selectors + const containerSelectors = [ + '[data-tid="message-pane-list"]', '[data-tid="chat-pane-list"]', + '[data-tid="chat-pane"]', '[role="log"]', '.ts-message-list-container', + ]; + let container: Element | null = null; + for (const sel of containerSelectors) { + container = document.querySelector(sel); + if (container) break; + } + + const messageSelectors = [ + '[data-tid="chat-message"]', '.fui-ChatMessage', + '[data-tid*="chat-pane-message"]', '[data-tid*="message-body"]', + ]; + const target = container || document.body; + const candidates = target.querySelectorAll(messageSelectors.join(', ')); + for (const el of Array.from(candidates) as HTMLElement[]) { const messageEl = el.closest?.('[data-tid="chat-message"], .fui-ChatMessage') || el; let author = 'Unknown'; - const authorEl = messageEl.querySelector('[data-tid="message-author"], [data-tid="message-author-name"], .fui-ChatMessage__author, [data-tid*="author"]'); - if (authorEl?.textContent) author = authorEl.textContent.trim(); - const bodyEl = messageEl.querySelector('[data-tid="message-body"], .fui-ChatMessage__body, [data-tid*="message-body"]'); - const text = (bodyEl as HTMLElement)?.innerText?.trim() || ''; + const authorSels = [ + '[data-tid="message-author"]', '[data-tid="message-author-name"]', + '.fui-ChatMessage__author', '[data-tid*="author"]', + ]; + for (const sel of authorSels) { + const authorEl = messageEl.querySelector(sel) || el.querySelector(sel); + if (authorEl?.textContent) { author = authorEl.textContent.trim(); break; } + } + const bodySels = [ + '[data-tid="message-body"]', '.fui-ChatMessage__body', + '[data-tid="chat-message-text"]', '[data-tid*="message-body"]', + ]; + let text = ''; + for (const sel of bodySels) { + const bodyEl = messageEl.querySelector(sel) || el.querySelector(sel); + if (bodyEl) { text = (bodyEl as HTMLElement).innerText?.trim() || ''; break; } + } if (!text || text.length < 2 || isNoise(text)) continue; const key = `${author}::${text}`; if (known.has(key) || seenThisScan.has(key)) continue; @@ -414,9 +471,35 @@ export class ChatProcedure { const ts = timeEl?.getAttribute?.('datetime') || new Date().toISOString(); results.push({ speaker: author, text, timestamp: ts, teamsTimestamp: ts, messageKey: key }); } - return results; + + // Diagnostics (once per 20s, controlled by caller) + let diag: string | undefined; + if ((window as any).__chatScanDiagCounter === undefined) (window as any).__chatScanDiagCounter = 0; + (window as any).__chatScanDiagCounter++; + if ((window as any).__chatScanDiagCounter % 4 === 1) { + const info: string[] = []; + info.push(`container=${container?.tagName || 'body'}[${container?.getAttribute?.('data-tid') || ''}]`); + info.push(`candidates=${candidates.length}`); + // Dump all chat-ish elements for debugging + const allChat = document.querySelectorAll('[data-tid*="chat"], [data-tid*="message"], [role="log"], .fui-Chat'); + const tags: string[] = []; + for (const c of Array.from(allChat).slice(0, 15)) { + const tid = c.getAttribute('data-tid') || ''; + const cls = c.className?.toString?.()?.substring(0, 40) || ''; + const h = (c as HTMLElement).offsetHeight || 0; + tags.push(`<${c.tagName} tid="${tid}" cls="${cls}" h=${h}>`); + } + info.push(`domElements=[${tags.join(', ')}]`); + diag = info.join(' | '); + } + + return { messages: results, diag }; }, knownKeys); - for (const msg of messages) { + + if (scanResult.diag) { + this._logger.info(`[ChatScan] ${scanResult.diag}`); + } + for (const msg of scanResult.messages) { this._handleChatMessage(msg); } } catch { @@ -490,7 +573,11 @@ export class ChatProcedure { 'div[contenteditable="true"][data-tid*="chat"]', 'div[contenteditable="true"][aria-label*="message" i]', '[aria-label*="Type a new message" i]', + '[aria-label="Type a message"]', '[aria-label*="Neue Nachricht eingeben" i]', + '[placeholder*="Type a message" i]', + '[placeholder*="Nachricht" i]', + 'div[contenteditable="true"][role="textbox"]', ]; const maxAttempts = 5; diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index d4dcc53..b44cdd5 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -589,12 +589,17 @@ export class BotOrchestrator { * a WebSocket ping. Prevents Teams from detecting the bot as idle * and kicking it from the meeting. */ + private _keepAliveCounter: number = 0; + private _startKeepAlive(): void { if (this._keepAliveInterval) return; + this._keepAliveCounter = 0; this._keepAliveInterval = setInterval(async () => { if (this._isShuttingDown || !this._page) return; + this._keepAliveCounter++; + try { // Small random mouse movement to simulate user activity const x = 640 + Math.floor(Math.random() * 20 - 10); @@ -612,6 +617,11 @@ export class BotOrchestrator { // Connection might be closing } } + + // Periodic debug screenshot every ~45s (every 3rd keepalive cycle) + if (this._isDebugMode && this._keepAliveCounter % 3 === 0) { + await this._takeScreenshot(`debug-periodic-${this._keepAliveCounter}`); + } }, 15000); this._logger.info('Keepalive started (15s interval)');