diff --git a/main.py b/main.py index 9034a5d..596aa6f 100644 --- a/main.py +++ b/main.py @@ -108,23 +108,25 @@ def main(): print("\nPoste '!start Name' im Chat um zu beginnen.\n") - window = monitor.run() - resolver = Resolver(cache_path=CACHE_PATH, page=page) - print(f"\nLöse {len(window.entries)} E-Mail-Adresse(n) auf...") - resolved_entries = [] - for entry in window.entries: - email = resolver.resolve(entry.display_name) - print(f" {entry.display_name} → {email}") - resolved_entries.append(AuditEntry(display_name=entry.display_name, email=email)) - window.entries = resolved_entries + while True: + window = monitor.run_once() - memo_content = generate_memo(window) - path = save_memo(memo_content) + print(f"\nLöse {len(window.entries)} E-Mail-Adresse(n) auf...") + resolved_entries = [] + for entry in window.entries: + email = resolver.resolve(entry.display_name) + print(f" {entry.display_name} → {email}") + resolved_entries.append(AuditEntry(display_name=entry.display_name, email=email)) + window.entries = resolved_entries - print(f"\nMemo gespeichert: {path}") - print("\n" + memo_content) + memo_content = generate_memo(window) + path = save_memo(memo_content) + + print(f"\nMemo gespeichert: {path}\n") + print(memo_content) + print("\n" + "─" * 60 + "\n") context.close() diff --git a/src/teampulse/monitor.py b/src/teampulse/monitor.py index 4806ed3..6a4423e 100644 --- a/src/teampulse/monitor.py +++ b/src/teampulse/monitor.py @@ -1,27 +1,24 @@ import re +import sys from datetime import datetime from playwright.sync_api import Page from teampulse.models import AuditEntry, AuditWindow, ChatMessage -# Selectors verified against Teams Web DOM (2026-05-17) -# Supports both channel meetings (channel-pane-message) and meeting chat (chat-pane-message) _MSG_SELECTOR = "[data-tid='channel-pane-message'], [data-tid='chat-pane-message']" _PROFILE_EMAIL_SELECTOR = ".lpc_ip_root_class a[href*='mailto:']" _START_RE = re.compile(r'^!start(?:\s+(.+))?$', re.IGNORECASE) _STOP_RE = re.compile(r'^!stop$', re.IGNORECASE) _QUOTE_CHARS = '"“”' # straight, left and right double quote +_SPINNER = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' -# JavaScript that extracts (sender, text, id) from both channel and meeting chat DOM. -# Use raw string so \s, \d etc. pass through to JavaScript unchanged. _POLL_JS = r"""() => { const msgs = []; - - // Channel meeting: channel-pane-message contains reply-message-header + message-body siblings - // Filter to visible elements only using bounding rect (offsetParent fails with fixed layouts) const isVisible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; }; + + // Channel meeting: reply-message-header + message-body are siblings inside channel-pane-message const channelPanes = Array.from( document.querySelectorAll("[data-tid='channel-pane-message']") ).filter(isVisible); @@ -40,21 +37,18 @@ _POLL_JS = r"""() => { senderIdx++; } else if (tid === 'message-body' && currentSender) { const text = el.innerText.trim(); - if (text) { - msgs.push({ - sender: currentSender, - text: text, - id: paneIdx + '_' + senderIdx + '_' + elIdx + '_' + text.substring(0, 20) - }); - } + if (text) msgs.push({ + sender: currentSender, + text: text, + id: paneIdx + '_' + senderIdx + '_' + elIdx + '_' + text.substring(0, 20) + }); } }); }); return msgs; } - // Meeting chat: message-author-name is a SIBLING of chat-pane-message, not a child. - // Walk both element types in document order, tracking current sender. + // Private meeting chat: message-author-name is a sibling of chat-pane-message const chatEls = Array.from(document.querySelectorAll( "[data-tid='message-author-name'], [data-tid='chat-pane-message']" )); @@ -70,9 +64,7 @@ _POLL_JS = r"""() => { text = Array.from(pEls).map(p => p.innerText.trim()).filter(t => t).join('\n'); } else { let raw = el.innerText.trim(); - if (currentSender && raw.startsWith(currentSender)) { - raw = raw.slice(currentSender.length); - } + if (currentSender && raw.startsWith(currentSender)) raw = raw.slice(currentSender.length); raw = raw.replace(/^\s*\d{1,2}:\d{2}(?:\s*(?:AM|PM))?\s*/i, '').trim(); text = raw; } @@ -83,22 +75,29 @@ _POLL_JS = r"""() => { return msgs; }""" +_CHAT_TITLE_JS = """() => { + const t = document.querySelector( + "[data-tid='chat-title'], [data-tid='channelTitle-text'], [data-tid='entity-header'] h1" + ); + return t ? t.innerText.trim() : ''; +}""" + + +def _clear_line() -> None: + sys.stdout.write('\r\033[K') + sys.stdout.flush() + def parse_trigger(message: ChatMessage, current_user: str) -> tuple[str, str] | None: if message.sender != current_user: return None - text = message.text.strip() - m = _START_RE.match(text) if m: raw = (m.group(1) or "").strip(_QUOTE_CHARS).strip() - name = raw or "Unbekannter Vortragender" - return ("start", name) - + return ("start", raw or "Unbekannter Vortragender") if _STOP_RE.match(text): return ("stop", "") - return None @@ -109,7 +108,7 @@ class Monitor: self._seen_message_ids: set[str] = set() def wait_for_chat(self) -> None: - print("Warte auf Teams-Chat-Seite... (bitte zur Meeting-Chat-Seite navigieren)") + print("Warte auf Teams-Chat-Seite... (bitte zum Meeting-Chat navigieren)") self._page.wait_for_selector(_MSG_SELECTOR, timeout=300_000) print("Chat erkannt.") @@ -132,36 +131,13 @@ class Monitor: except Exception: pass self._page.wait_for_timeout(1000) - - name = input("Benutzername nicht erkannt. Bitte deinen Teams-Anzeigenamen eingeben: ").strip() + name = input("Benutzername nicht erkannt. Bitte Teams-Anzeigenamen eingeben: ").strip() if not name: raise RuntimeError("Kein Benutzername angegeben.") return name def poll_new_messages(self) -> list[ChatMessage]: - result = self._page.evaluate(r"""() => { - const isVisible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; }; - const chatMsgs = Array.from(document.querySelectorAll("[data-tid='chat-pane-message']")).filter(isVisible); - const authorNames = Array.from(document.querySelectorAll("[data-tid='message-author-name']")); - const sample = chatMsgs.slice(0, 3).map(msg => ({ - hasAuthorInside: !!msg.querySelector("[data-tid='message-author-name']"), - text: msg.innerText.trim().substring(0, 40), - })); - return { - channelAll: document.querySelectorAll("[data-tid='channel-pane-message']").length, - chatAll: document.querySelectorAll("[data-tid='chat-pane-message']").length, - channelVisible: Array.from(document.querySelectorAll("[data-tid='channel-pane-message']")).filter(isVisible).length, - chatVisible: chatMsgs.length, - authorNamesTotal: authorNames.length, - sample, - }; - }""") - print(f" [DOM] channel={result['channelAll']}(vis:{result['channelVisible']}) chat={result['chatAll']}(vis:{result['chatVisible']}) authors={result['authorNamesTotal']}") - for s in result['sample']: - print(f" msg hasAuthor={s['hasAuthorInside']} text={s['text']!r}") raw = self._page.evaluate(_POLL_JS) - if raw: - print(f" [POLL] {len(raw)} Elemente, davon {sum(1 for r in raw if r['id'] not in self._seen_message_ids)} neu") new_messages = [] for item in raw: if item["id"] in self._seen_message_ids: @@ -175,7 +151,7 @@ class Monitor: )) return new_messages - def run(self) -> AuditWindow: + def run_once(self) -> AuditWindow: import time self.wait_for_chat() @@ -183,27 +159,33 @@ class Monitor: presenter: str | None = None start_time: datetime | None = None collected: list[ChatMessage] = [] - last_url: str = "" + last_chat: str = "" + spin_idx: int = 0 - print("Monitoring aktiv. Poste '!start Name' im Chat um ein Zeitfenster zu starten.") + print("Bereit — poste '!start Name' im Chat um zu beginnen.") while True: - current_chat = self._page.evaluate("""() => { - const t = document.querySelector( - "[data-tid='chat-title'], [data-tid='channelTitle-text'], [data-tid='entity-header'] h1" - ); - return t ? t.innerText.trim() : document.title; - }""") - if current_chat != last_url: - print(f" [CHAT] {current_chat}") - last_url = current_chat + # Chat-Titel prüfen + try: + current_chat = self._page.evaluate(_CHAT_TITLE_JS) + if current_chat and current_chat != last_chat: + _clear_line() + print(f"Chat: {current_chat}") + last_chat = current_chat + except Exception: + pass + + # Spinner anzeigen + spin = _SPINNER[spin_idx % len(_SPINNER)] + spin_idx += 1 + sys.stdout.write(f'\r {spin}') + sys.stdout.flush() try: new_msgs = self.poll_new_messages() - for m in new_msgs: - print(f" [MSG] von='{m.sender}' text={m.text[:60]!r}") except Exception as e: - print(f"Verbindung verloren ({type(e).__name__}: {e!s:.120}), warte auf Chat...") + _clear_line() + print(f"Verbindung unterbrochen, reconnecting...") time.sleep(2) try: self.wait_for_chat() @@ -216,23 +198,26 @@ class Monitor: if trigger is None: if start_time is not None: + _clear_line() + print(f" {msg.sender}: {msg.text[:80]}") collected.append(msg) continue action, value = trigger + _clear_line() if action == "start": presenter = value start_time = datetime.now() collected = [] - print(f"Zeitfenster gestartet — Vortragender: {presenter}") + print(f"▶ Zeitfenster gestartet — Vortragender: {presenter}") elif action == "stop": if start_time is None: print("!stop ohne vorheriges !start — ignoriert.") continue end_time = datetime.now() - print("Zeitfenster beendet.") + print("■ Zeitfenster beendet.") seen_senders = list(dict.fromkeys(m.sender for m in collected)) entries = [AuditEntry(display_name=s, email="") for s in seen_senders] return AuditWindow(