diff --git a/src/teampulse/resolver.py b/src/teampulse/resolver.py index 82c6aa9..3fbc1f9 100644 --- a/src/teampulse/resolver.py +++ b/src/teampulse/resolver.py @@ -45,54 +45,83 @@ class Resolver: encoding="utf-8", ) - def _hover_sender(self, display_name: str) -> str: - """Find the sender's avatar/name and hover to open the profile card. - Returns the strategy used, or '' if not found.""" + def _hover_sender(self, display_name: str) -> tuple[str, dict]: + """Find the sender element and hover to open the profile card. + Returns (strategy, bbox) or ('', {}) if not found.""" - # Strategy 1: reply-message-header (channel meetings) — avatar next to name + def _mouse_move_to(el) -> dict | None: + """Scroll element into view, get its bbox, move mouse there.""" + el.scroll_into_view_if_needed() + bbox = el.bounding_box() + if not bbox or bbox['width'] == 0: + return None + x = bbox['x'] + bbox['width'] / 2 + y = bbox['y'] + bbox['height'] / 2 + self._page.mouse.move(x, y) + return bbox + + # Strategy 1: reply-message-header (channel meetings) for header in self._page.query_selector_all("[data-tid='reply-message-header']"): try: if display_name not in header.inner_text(): continue avatar = header.query_selector("[data-tid='reply-message-header-avatar']") if avatar: - avatar.scroll_into_view_if_needed() - avatar.hover() - return "reply-avatar" + bbox = _mouse_move_to(avatar) + if bbox: + return "reply-avatar", bbox for span in header.query_selector_all("span"): if span.inner_text().strip() == display_name: - span.scroll_into_view_if_needed() - span.hover() - return "reply-span" + bbox = _mouse_move_to(span) + if bbox: + return "reply-span", bbox except Exception: continue - # Strategy 2: aria-label containing the name (Fluent UI persona elements) + # Strategy 2: aria-label safe_name = display_name.replace("'", "\\'") for el in self._page.query_selector_all(f"[aria-label*='{safe_name}']"): try: if el.is_visible(): - el.scroll_into_view_if_needed() - el.hover() - return "aria-label" + bbox = _mouse_move_to(el) + if bbox: + return "aria-label", bbox except Exception: continue - # Strategy 3: message-author-name (private meeting chat) + # Strategy 3: message-author-name — also try avatar to the left 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.hover() - return "author-name" + if el.inner_text().strip() != display_name: + continue + # Look for avatar in parent tree (usually 3-4 levels up) + avatar_bbox = self._page.evaluate("""(nameEl) => { + let p = nameEl.parentElement; + for (let i = 0; i < 6; i++) { + if (!p) break; + const av = p.querySelector('[data-tid*="avatar"], [class*="avatar"] img, [class*="Avatar"] img'); + if (av) { + const r = av.getBoundingClientRect(); + if (r.width > 0) return {x: r.x + r.width/2, y: r.y + r.height/2, w: r.width, h: r.height}; + } + p = p.parentElement; + } + return null; + }""", el) + if avatar_bbox: + self._page.mouse.move(avatar_bbox['x'], avatar_bbox['y']) + return "nearby-avatar", avatar_bbox + bbox = _mouse_move_to(el) + if bbox: + return "author-name", bbox except Exception: continue - return "" + return "", {} def _extract_email_from_profile(self, display_name: str) -> str | None: try: - strategy = self._hover_sender(display_name) + strategy, bbox = self._hover_sender(display_name) except Exception as e: print(f" Hover auf '{display_name}' fehlgeschlagen: {e}") return None @@ -100,7 +129,18 @@ class Resolver: if not strategy: print(f" '{display_name}' nicht im sichtbaren Chat gefunden.") return None - print(f" '{display_name}' via '{strategy}' gehovered...") + print(f" '{display_name}' via '{strategy}' bei {bbox} — warte auf Karte...") + + # Screenshot right after hover for debugging + try: + from pathlib import Path as _Path + debug_dir = _Path("debug"); debug_dir.mkdir(exist_ok=True) + safe = display_name.replace(" ", "_") + self._page.screenshot(path=str(debug_dir / f"{safe}_after_hover.png")) + except Exception: + pass + + time.sleep(0.5) # Give Teams time to register the hover try: # Stage 1: wait for card container to appear @@ -136,12 +176,22 @@ class Resolver: time.sleep(0.3) return email or None except Exception: - # Diagnose: show shadow root text if available + # Screenshot + shadow root text for diagnosis + try: + from pathlib import Path as _Path + debug_dir = _Path("debug"); debug_dir.mkdir(exist_ok=True) + safe = display_name.replace(" ", "_") + self._page.screenshot(path=str(debug_dir / f"{safe}_card_state.png")) + except Exception: + pass card_text = self._page.evaluate("""() => { const lpcCard = document.querySelector('.lpc_ip_root_class lpc-card'); - if (!lpcCard || !lpcCard.shadowRoot) return '(kein Shadow Root)'; - return lpcCard.shadowRoot.textContent.trim().substring(0, 200); + if (!lpcCard) return '(kein lpc-card Element)'; + if (!lpcCard.shadowRoot) return '(kein Shadow Root)'; + return lpcCard.shadowRoot.textContent.trim().substring(0, 300); }""") or "(leer)" - print(f" Karte für '{display_name}' offen, E-Mail nicht im Shadow Root. Inhalt: {card_text!r}") + print(f" Karte für '{display_name}' offen, E-Mail nicht gefunden.") + print(f" Shadow-Root-Inhalt: {card_text!r}") + print(f" Screenshots in debug/{safe}_*.png") self._page.mouse.move(0, 0) return None