From b552ccd547f265091934ba67b3c3d1be8e53b40a Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Fri, 6 Mar 2026 12:40:50 +0100 Subject: [PATCH] chat: periodischer Scan fuer Teilnehmer-Nachrichten, Chat-Panel-Reopen wenn geschlossen Made-with: Cursor --- src/bot/chatProcedure.ts | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts index 4302eba..dc3ea54 100644 --- a/src/bot/chatProcedure.ts +++ b/src/bot/chatProcedure.ts @@ -23,6 +23,7 @@ export class ChatProcedure { private _lastMessageText: string = ''; private _botJoinedAtMs: number = 0; private _recentMessageKeyTimestamps: Map = new Map(); + private _scanIntervalId: ReturnType | null = null; constructor( page: Page, @@ -357,6 +358,72 @@ export class ChatProcedure { }); this._logger.info(`Chat MutationObserver set up (target: ${chatObserverTarget})`); + + this._startPeriodicChatScan(); + } + + /** + * Periodic scan as fallback: participant messages may load async and miss + * the MutationObserver (addedNode with empty placeholder, then content update). + * Also reopens the chat panel if it was closed (e.g. by user or Teams). + */ + private _startPeriodicChatScan(): void { + if (this._scanIntervalId) return; + const intervalMs = 2500; + 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) { + this._logger.info('Chat panel closed, reopening'); + await this._openChatPanel(); + } + + const knownKeys = Array.from(this._recentMessageKeyTimestamps.keys()); + const messages = 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', + ]; + function isNoise(t: string) { + const l = t.toLowerCase(); + return noisePatterns.some(p => l.includes(p)); + } + 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"]'); + 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() || ''; + if (!text || text.length < 2 || isNoise(text)) continue; + const key = `${author}::${text}`; + if (known.has(key) || seenThisScan.has(key)) continue; + seenThisScan.add(key); + const timeEl = messageEl.querySelector('time[datetime], [data-tid*="timestamp"] time'); + const ts = timeEl?.getAttribute?.('datetime') || new Date().toISOString(); + results.push({ speaker: author, text, timestamp: ts, teamsTimestamp: ts, messageKey: key }); + } + return results; + }, knownKeys); + for (const msg of messages) { + this._handleChatMessage(msg); + } + } catch { + // Page may be closed + } + }, intervalMs); + this._logger.info(`Chat periodic scan started (interval: ${intervalMs}ms)`); } /** @@ -477,6 +544,11 @@ export class ChatProcedure { async unsubscribe(): Promise { this._isSubscribed = false; + if (this._scanIntervalId) { + clearInterval(this._scanIntervalId); + this._scanIntervalId = null; + } + try { await this._page.evaluate(() => { if ((window as any).__chatObserver) {