fix: click avatar instead of hover to open profile dialog
This commit is contained in:
parent
9580a94e25
commit
539e1a916b
@ -4,17 +4,6 @@ from pathlib import Path
|
||||
|
||||
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):
|
||||
@ -45,175 +34,153 @@ class Resolver:
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
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."""
|
||||
def _click_avatar(self, display_name: str) -> str:
|
||||
"""Find the sender's avatar and click it to open the profile dialog.
|
||||
Returns strategy name or '' if not found."""
|
||||
|
||||
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
|
||||
def _click_el(el) -> bool:
|
||||
try:
|
||||
el.scroll_into_view_if_needed()
|
||||
bbox = el.bounding_box()
|
||||
if not bbox or bbox['width'] == 0:
|
||||
return False
|
||||
el.click()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Strategy 1: reply-message-header (channel meetings)
|
||||
# Strategy 1: reply-message-header (channel meetings) — click avatar
|
||||
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:
|
||||
bbox = _mouse_move_to(avatar)
|
||||
if bbox:
|
||||
return "reply-avatar", bbox
|
||||
if avatar and _click_el(avatar):
|
||||
return "reply-avatar"
|
||||
for span in header.query_selector_all("span"):
|
||||
if span.inner_text().strip() == display_name:
|
||||
bbox = _mouse_move_to(span)
|
||||
if bbox:
|
||||
return "reply-span", bbox
|
||||
if span.inner_text().strip() == display_name and _click_el(span):
|
||||
return "reply-span"
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 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():
|
||||
bbox = _mouse_move_to(el)
|
||||
if bbox:
|
||||
return "aria-label", bbox
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Strategy 3: message-author-name — also try avatar to the left
|
||||
# Strategy 2: message-author-name — find avatar in parent tree and click
|
||||
for el in self._page.query_selector_all("[data-tid='message-author-name']"):
|
||||
try:
|
||||
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) => {
|
||||
# Find avatar div in parent tree via JS and click it
|
||||
clicked = 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');
|
||||
const av = p.querySelector('[class*="ChatMessage__avatar"], [class*="avatar"]');
|
||||
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};
|
||||
const inner = av.querySelector('img, [role="img"], button') || av;
|
||||
const r = inner.getBoundingClientRect();
|
||||
if (r.width > 0) { inner.click(); return true; }
|
||||
}
|
||||
p = p.parentElement;
|
||||
}
|
||||
return null;
|
||||
return false;
|
||||
}""", 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
|
||||
if clicked:
|
||||
return "nearby-avatar"
|
||||
# Fallback: click the name span
|
||||
if _click_el(el):
|
||||
return "author-name"
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return "", {}
|
||||
return ""
|
||||
|
||||
def _extract_email_from_profile(self, display_name: str) -> str | None:
|
||||
try:
|
||||
strategy, bbox = self._hover_sender(display_name)
|
||||
strategy = self._click_avatar(display_name)
|
||||
except Exception as e:
|
||||
print(f" Hover auf '{display_name}' fehlgeschlagen: {e}")
|
||||
print(f" Klick auf '{display_name}' fehlgeschlagen: {e}")
|
||||
return None
|
||||
|
||||
if not strategy:
|
||||
print(f" '{display_name}' nicht im sichtbaren Chat gefunden.")
|
||||
return None
|
||||
x = bbox.get('x', 0) + bbox.get('w', bbox.get('width', 32)) / 2 if isinstance(bbox, dict) else 0
|
||||
y = bbox.get('y', 0) + bbox.get('h', bbox.get('height', 32)) / 2 if isinstance(bbox, dict) else 0
|
||||
print(f" '{display_name}' via '{strategy}' → Viewport ({x:.0f}, {y:.0f}) — warte auf Karte...")
|
||||
|
||||
# Approach 1: gradual mouse movement (simulate human)
|
||||
self._page.mouse.move(x - 50, y, steps=5)
|
||||
time.sleep(0.1)
|
||||
self._page.mouse.move(x, y, steps=10)
|
||||
time.sleep(0.3)
|
||||
|
||||
# Approach 2: dispatch pointer/mouse events directly on element at that position
|
||||
hit_info = self._page.evaluate(f"""() => {{
|
||||
const el = document.elementFromPoint({x}, {y});
|
||||
if (!el) return 'no element at position';
|
||||
['pointerover','pointerenter','mouseover','mouseenter','pointermove','mousemove'].forEach(t => {{
|
||||
el.dispatchEvent(new MouseEvent(t, {{
|
||||
bubbles: true, cancelable: true, view: window,
|
||||
clientX: {x}, clientY: {y}
|
||||
}}));
|
||||
}});
|
||||
return el.tagName + '[' + (el.getAttribute('data-tid')||'') + '] ' + el.className.substring(0,60);
|
||||
}}""")
|
||||
print(f" Element bei ({x:.0f},{y:.0f}): {hit_info}")
|
||||
|
||||
# Screenshot after hover attempt
|
||||
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(1.5) # Wait for hover card to fully appear
|
||||
print(f" '{display_name}' via '{strategy}' geklickt — warte auf Dialog...")
|
||||
|
||||
try:
|
||||
# Stage 1: wait for card container to appear
|
||||
self._page.wait_for_selector(".lpc_ip_root_class", timeout=5_000)
|
||||
# Wait for the profile dialog to open
|
||||
# Could be lpc_ip_root_class (hover card) or a role="dialog" modal
|
||||
self._page.wait_for_selector(
|
||||
".lpc_ip_root_class, [role='dialog']",
|
||||
timeout=6_000,
|
||||
)
|
||||
except Exception:
|
||||
print(f" Hover-Card für '{display_name}' erscheint nicht.")
|
||||
self._page.mouse.move(0, 0)
|
||||
print(f" Profil-Dialog für '{display_name}' erscheint nicht.")
|
||||
self._page.keyboard.press("Escape")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Move mouse onto the card itself to keep it open while it loads
|
||||
card_el = self._page.query_selector(".lpc_ip_root_class")
|
||||
if card_el:
|
||||
card_el.hover()
|
||||
|
||||
# Stage 2: card content is inside a Shadow DOM (<lpc-card shadowrootmode="open">)
|
||||
# Wait for the mailto link to appear inside the shadow root
|
||||
self._page.wait_for_function(
|
||||
"""() => {
|
||||
const lpcCard = document.querySelector('.lpc_ip_root_class lpc-card');
|
||||
if (!lpcCard || !lpcCard.shadowRoot) return false;
|
||||
return !!lpcCard.shadowRoot.querySelector('a[href*="mailto:"]');
|
||||
}""",
|
||||
timeout=10_000,
|
||||
)
|
||||
# Check which dialog appeared and extract email
|
||||
# Try shadow DOM of lpc-card first
|
||||
email = self._page.evaluate("""() => {
|
||||
// Check lpc-card shadow DOM
|
||||
const lpcCard = document.querySelector('.lpc_ip_root_class lpc-card');
|
||||
if (!lpcCard || !lpcCard.shadowRoot) return null;
|
||||
const link = lpcCard.shadowRoot.querySelector('a[href*="mailto:"]');
|
||||
return link ? link.href.replace('mailto:', '').trim() : null;
|
||||
if (lpcCard && lpcCard.shadowRoot) {
|
||||
const link = lpcCard.shadowRoot.querySelector('a[href*="mailto:"]');
|
||||
if (link) return link.href.replace('mailto:', '').trim();
|
||||
// Also check text content for email pattern
|
||||
const text = lpcCard.shadowRoot.textContent;
|
||||
const match = text.match(/[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}/);
|
||||
if (match) return match[0];
|
||||
}
|
||||
// Check regular dialog
|
||||
const dialog = document.querySelector("[role='dialog']");
|
||||
if (dialog) {
|
||||
const link = dialog.querySelector('a[href*="mailto:"]');
|
||||
if (link) return link.href.replace('mailto:', '').trim();
|
||||
const text = dialog.textContent;
|
||||
const match = text.match(/[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}/);
|
||||
if (match) return match[0];
|
||||
}
|
||||
return null;
|
||||
}""")
|
||||
self._page.mouse.move(0, 0)
|
||||
|
||||
if not email:
|
||||
# Wait a bit more for the dialog to fully load (two-stage)
|
||||
time.sleep(2)
|
||||
email = self._page.evaluate("""() => {
|
||||
function findEmail(root) {
|
||||
const link = root.querySelector('a[href*="mailto:"]');
|
||||
if (link) return link.href.replace('mailto:', '').trim();
|
||||
const match = root.textContent.match(/[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}/);
|
||||
if (match) return match[0];
|
||||
// Search shadow roots recursively
|
||||
for (const el of root.querySelectorAll('*')) {
|
||||
if (el.shadowRoot) {
|
||||
const found = findEmail(el.shadowRoot);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return findEmail(document.body);
|
||||
}""")
|
||||
|
||||
if not email:
|
||||
# Diagnostic: show what's in the dialog
|
||||
content = self._page.evaluate("""() => {
|
||||
const d = document.querySelector('.lpc_ip_root_class lpc-card');
|
||||
if (d && d.shadowRoot) return 'shadow: ' + d.shadowRoot.textContent.trim().substring(0, 200);
|
||||
const r = document.querySelector("[role='dialog']");
|
||||
if (r) return 'dialog: ' + r.textContent.trim().substring(0, 200);
|
||||
return '(nichts gefunden)';
|
||||
}""")
|
||||
print(f" Dialog offen, E-Mail nicht gefunden. Inhalt: {content!r}")
|
||||
|
||||
self._page.keyboard.press("Escape")
|
||||
time.sleep(0.3)
|
||||
return email or None
|
||||
except Exception:
|
||||
# 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) 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 gefunden.")
|
||||
print(f" Shadow-Root-Inhalt: {card_text!r}")
|
||||
print(f" Screenshots in debug/{safe}_*.png")
|
||||
self._page.mouse.move(0, 0)
|
||||
|
||||
except Exception as e:
|
||||
print(f" Fehler beim Lesen des Profil-Dialogs: {e}")
|
||||
self._page.keyboard.press("Escape")
|
||||
return None
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user