From 00bbd196bf152ba5e61b74a9b74dd791878bf485 Mon Sep 17 00:00:00 2001 From: Oliver Hofmann Date: Sun, 17 May 2026 15:03:16 +0200 Subject: [PATCH] fix: click avatar/author via element handles, prevent wild scrolling --- src/teampulse/resolver.py | 68 +++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/src/teampulse/resolver.py b/src/teampulse/resolver.py index f56c84b..63c86ec 100644 --- a/src/teampulse/resolver.py +++ b/src/teampulse/resolver.py @@ -6,6 +6,15 @@ from playwright.sync_api import Page from teampulse.monitor import _PROFILE_EMAIL_SELECTOR +# Selectors for the clickable sender element (avatar or name) that opens the profile card +_AUTHOR_SELECTORS = [ + # Channel meeting: avatar next to reply-message-header name + "[data-tid='reply-message-header-avatar']", + "[data-tid='post-message-header-avatar']", + # Meeting chat: author name element + "[data-tid='message-author-name']", +] + class Resolver: def __init__(self, cache_path: Path, page: Page): @@ -36,40 +45,57 @@ class Resolver: encoding="utf-8", ) - def _extract_email_from_profile(self, display_name: str) -> str | None: - # Use Playwright's native click (real mouse events) to trigger the Teams profile card. - # Try multiple selectors that contain the sender's name. - selectors = [ - f"[data-tid='message-author-name']:has-text('{display_name}')", - f"[data-tid='reply-message-header'] span:has-text('{display_name}')", - f"span:has-text('{display_name}')", - ] + def _find_and_click_sender(self, display_name: str) -> bool: + """Find the sender in the visible chat and click to open their profile card.""" - clicked = False - for selector in selectors: + # Strategy 1: find reply-message-header containing the name, click its avatar + for header in self._page.query_selector_all("[data-tid='reply-message-header']"): try: - loc = self._page.locator(selector).first - if loc.is_visible(timeout=1000): - loc.click() - clicked = True - break + name_spans = header.query_selector_all("span") + for span in name_spans: + if span.inner_text().strip() == display_name: + avatar = header.query_selector( + "[data-tid='reply-message-header-avatar']" + ) + target = avatar or span + target.scroll_into_view_if_needed() + target.click() + return True except Exception: continue + # Strategy 2: message-author-name (private meeting chat) + for el in self._page.query_selector_all("[data-tid='message-author-name']"): + try: + if el.inner_text().strip() == display_name: + el.scroll_into_view_if_needed() + el.click() + return True + except Exception: + continue + + return False + + def _extract_email_from_profile(self, display_name: str) -> str | None: + try: + clicked = self._find_and_click_sender(display_name) + except Exception as e: + print(f" Klick auf '{display_name}' fehlgeschlagen: {e}") + return None + if not clicked: - print(f" Sender '{display_name}' nicht im Chat gefunden.") + print(f" '{display_name}' nicht im sichtbaren Chat gefunden.") return None try: - # Step 1: wait for the profile card container to appear + # Wait for profile card container self._page.wait_for_selector(".lpc_ip_root_class", timeout=5000) except Exception: - print(f" Profilkarte für '{display_name}' öffnet sich nicht (Klick hat nicht gewirkt).") + print(f" Profilkarte für '{display_name}' öffnet sich nicht.") self._page.keyboard.press("Escape") return None try: - # Step 2: extract email — try mailto link first, then plain text fallbacks email = None for selector in [ ".lpc_ip_root_class a[href*='mailto:']", @@ -86,14 +112,14 @@ class Resolver: break if not email: - # Diagnose: show card text content card = self._page.query_selector(".lpc_ip_root_class") card_text = card.inner_text().strip()[:200] if card else "(leer)" - print(f" Karte geöffnet, E-Mail nicht gefunden. Karteninhalt: {card_text!r}") + print(f" Karte geöffnet, E-Mail nicht gefunden. Inhalt: {card_text!r}") self._page.keyboard.press("Escape") time.sleep(0.5) return email + except Exception as e: print(f" Fehler beim Lesen der Profilkarte: {e}") self._page.keyboard.press("Escape")