341 Zeilen
12 KiB
Python
341 Zeilen
12 KiB
Python
# social_networks/facebook/facebook_ui_helper.py
|
|
|
|
"""
|
|
Facebook UI Helper - Hilfsklasse für UI-Interaktionen bei Facebook
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
import random
|
|
from typing import List, Union, Optional
|
|
|
|
from .facebook_selectors import FacebookSelectors
|
|
from utils.text_similarity import TextSimilarity
|
|
from utils.logger import setup_logger
|
|
|
|
logger = setup_logger("facebook_ui_helper")
|
|
|
|
class FacebookUIHelper:
|
|
"""
|
|
Hilfsklasse für UI-Interaktionen bei Facebook.
|
|
Bietet Methoden für menschenähnliche Interaktionen.
|
|
"""
|
|
|
|
def __init__(self, automation):
|
|
"""
|
|
Initialisiert den UI Helper.
|
|
|
|
Args:
|
|
automation: Referenz auf die Hauptautomatisierungsklasse
|
|
"""
|
|
self.automation = automation
|
|
self.selectors = FacebookSelectors()
|
|
self.text_similarity = TextSimilarity(default_threshold=0.7)
|
|
|
|
logger.debug("Facebook UI Helper initialisiert")
|
|
|
|
def _ensure_browser(self) -> bool:
|
|
"""
|
|
Stellt sicher, dass der Browser verfügbar ist.
|
|
|
|
Returns:
|
|
bool: True wenn Browser verfügbar
|
|
"""
|
|
if not self.automation.browser or not hasattr(self.automation.browser, 'page'):
|
|
logger.error("Browser nicht verfügbar")
|
|
return False
|
|
return True
|
|
|
|
def type_text_human_like(self, selector: str, text: str, delay_range: tuple = (50, 150)) -> bool:
|
|
"""
|
|
Tippt Text menschenähnlich mit zufälligen Verzögerungen.
|
|
|
|
Args:
|
|
selector: CSS-Selektor des Eingabefelds
|
|
text: Einzugebender Text
|
|
delay_range: Bereich für Verzögerung zwischen Tastenanschlägen in ms
|
|
|
|
Returns:
|
|
bool: True bei Erfolg
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
# Element fokussieren
|
|
if not self.automation.browser.click_element(selector):
|
|
logger.error(f"Konnte Element nicht fokussieren: {selector}")
|
|
return False
|
|
|
|
# Kurze Pause nach Fokus
|
|
self.automation.human_behavior.random_delay(0.2, 0.5)
|
|
|
|
# Feld leeren (dreifach-klick und löschen)
|
|
self.automation.browser.page.click(selector, click_count=3)
|
|
self.automation.browser.page.keyboard.press("Delete")
|
|
|
|
# Text Zeichen für Zeichen eingeben
|
|
for char in text:
|
|
self.automation.browser.page.keyboard.type(char)
|
|
|
|
# Zufällige Verzögerung zwischen Zeichen
|
|
delay_ms = random.randint(*delay_range)
|
|
time.sleep(delay_ms / 1000)
|
|
|
|
# Gelegentlich längere Pause (Denken)
|
|
if random.random() < 0.1:
|
|
self.automation.human_behavior.random_delay(0.3, 0.8)
|
|
|
|
# Sehr selten Tippfehler simulieren und korrigieren
|
|
if random.random() < 0.02 and len(text) > 5:
|
|
# Falschen Buchstaben tippen
|
|
wrong_char = random.choice('abcdefghijklmnopqrstuvwxyz')
|
|
self.automation.browser.page.keyboard.type(wrong_char)
|
|
time.sleep(random.randint(100, 300) / 1000)
|
|
|
|
# Korrigieren
|
|
self.automation.browser.page.keyboard.press("Backspace")
|
|
time.sleep(random.randint(50, 150) / 1000)
|
|
|
|
logger.debug(f"Text erfolgreich eingegeben in {selector}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim menschenähnlichen Tippen: {e}")
|
|
return False
|
|
|
|
def click_button_fuzzy(self, button_texts: Union[str, List[str]],
|
|
fallback_selector: str = None,
|
|
threshold: float = 0.7,
|
|
timeout: int = 5000) -> bool:
|
|
"""
|
|
Klickt einen Button mit Fuzzy-Text-Matching.
|
|
|
|
Args:
|
|
button_texts: Text oder Liste von Texten des Buttons
|
|
fallback_selector: CSS-Selektor für Fallback
|
|
threshold: Schwellenwert für Textähnlichkeit (0-1)
|
|
timeout: Zeitlimit für die Suche in Millisekunden
|
|
|
|
Returns:
|
|
bool: True bei Erfolg
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
# Normalisiere button_texts zu einer Liste
|
|
if isinstance(button_texts, str):
|
|
button_texts = [button_texts]
|
|
|
|
logger.info(f"Suche nach Button mit Texten: {button_texts}")
|
|
|
|
# Versuche jeden Text
|
|
for target_text in button_texts:
|
|
# Methode 1: Exakter Text-Match mit has-text
|
|
selector = f"button:has-text('{target_text}')"
|
|
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
|
if self.automation.browser.click_element(selector):
|
|
logger.info(f"Button geklickt (exakter Match): {target_text}")
|
|
return True
|
|
|
|
# Methode 2: Link als Button
|
|
selector = f"a:has-text('{target_text}')"
|
|
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
|
if self.automation.browser.click_element(selector):
|
|
logger.info(f"Link-Button geklickt: {target_text}")
|
|
return True
|
|
|
|
# Methode 3: Div mit role="button"
|
|
selector = f"div[role='button']:has-text('{target_text}')"
|
|
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
|
if self.automation.browser.click_element(selector):
|
|
logger.info(f"Div-Button geklickt: {target_text}")
|
|
return True
|
|
|
|
# Methode 4: Fuzzy-Matching mit allen Buttons
|
|
try:
|
|
all_buttons = self.automation.browser.page.query_selector_all("button, a[role='button'], div[role='button']")
|
|
|
|
for button in all_buttons:
|
|
button_text = button.inner_text().strip()
|
|
|
|
# Prüfe Ähnlichkeit mit jedem Zieltext
|
|
for target_text in button_texts:
|
|
similarity = self.text_similarity.calculate_similarity(button_text.lower(), target_text.lower())
|
|
|
|
if similarity >= threshold:
|
|
logger.info(f"Fuzzy-Match gefunden: '{button_text}' (Ähnlichkeit: {similarity:.2f})")
|
|
button.click()
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Fuzzy-Matching fehlgeschlagen: {e}")
|
|
|
|
# Fallback-Selektor verwenden
|
|
if fallback_selector:
|
|
if self.automation.browser.click_element(fallback_selector, timeout=timeout):
|
|
logger.info(f"Button geklickt mit Fallback-Selektor: {fallback_selector}")
|
|
return True
|
|
|
|
logger.warning(f"Konnte keinen Button mit Texten finden: {button_texts}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Fuzzy-Button-Click: {e}")
|
|
return False
|
|
|
|
def scroll_to_element(self, selector: str) -> bool:
|
|
"""
|
|
Scrollt zu einem Element.
|
|
|
|
Args:
|
|
selector: CSS-Selektor des Elements
|
|
|
|
Returns:
|
|
bool: True bei Erfolg
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
self.automation.browser.page.eval_on_selector(
|
|
selector,
|
|
"element => element.scrollIntoView({behavior: 'smooth', block: 'center'})"
|
|
)
|
|
|
|
# Warte auf Scroll-Animation
|
|
self.automation.human_behavior.random_delay(0.5, 1.0)
|
|
|
|
logger.debug(f"Zu Element gescrollt: {selector}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Scrollen zu Element: {e}")
|
|
return False
|
|
|
|
def wait_for_element(self, selector: str, timeout: int = 10000, state: str = "visible") -> bool:
|
|
"""
|
|
Wartet auf ein Element.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
timeout: Maximale Wartezeit in ms
|
|
state: Erwarteter Zustand ("visible", "hidden", "attached", "detached")
|
|
|
|
Returns:
|
|
bool: True wenn Element im erwarteten Zustand
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
self.automation.browser.page.wait_for_selector(selector, timeout=timeout, state=state)
|
|
logger.debug(f"Element gefunden: {selector} (Zustand: {state})")
|
|
return True
|
|
except Exception as e:
|
|
logger.debug(f"Element nicht gefunden: {selector} - {e}")
|
|
return False
|
|
|
|
def handle_dialog(self, action: str = "accept") -> bool:
|
|
"""
|
|
Behandelt JavaScript-Dialoge (alert, confirm, prompt).
|
|
|
|
Args:
|
|
action: "accept" oder "dismiss"
|
|
|
|
Returns:
|
|
bool: True bei Erfolg
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
# Dialog-Handler setzen
|
|
def handle_dialog(dialog):
|
|
logger.info(f"Dialog erkannt: {dialog.message}")
|
|
if action == "accept":
|
|
dialog.accept()
|
|
else:
|
|
dialog.dismiss()
|
|
|
|
self.automation.browser.page.on("dialog", handle_dialog)
|
|
|
|
# Kurz warten
|
|
time.sleep(0.5)
|
|
|
|
# Handler wieder entfernen
|
|
self.automation.browser.page.remove_listener("dialog", handle_dialog)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Dialog-Behandlung: {e}")
|
|
return False
|
|
|
|
def get_element_text(self, selector: str) -> Optional[str]:
|
|
"""
|
|
Holt den Text eines Elements.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
|
|
Returns:
|
|
Optional[str]: Text des Elements oder None
|
|
"""
|
|
if not self._ensure_browser():
|
|
return None
|
|
|
|
try:
|
|
element = self.automation.browser.page.query_selector(selector)
|
|
if element:
|
|
return element.inner_text().strip()
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen des Element-Texts: {e}")
|
|
return None
|
|
|
|
def fill_select_option(self, selector: str, value: str) -> bool:
|
|
"""
|
|
Wählt eine Option in einem Select-Element aus.
|
|
|
|
Args:
|
|
selector: CSS-Selektor des Select-Elements
|
|
value: Wert der zu wählenden Option
|
|
|
|
Returns:
|
|
bool: True bei Erfolg
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
try:
|
|
# Menschenähnliche Interaktion: Erst klicken, dann wählen
|
|
self.automation.browser.click_element(selector)
|
|
self.automation.human_behavior.random_delay(0.2, 0.5)
|
|
|
|
# Option auswählen
|
|
self.automation.browser.page.select_option(selector, value=value)
|
|
|
|
logger.debug(f"Option '{value}' ausgewählt in {selector}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Auswählen der Option: {e}")
|
|
return False
|
|
|
|
def check_element_exists(self, selector: str, timeout: int = 1000) -> bool:
|
|
"""
|
|
Prüft ob ein Element existiert.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
timeout: Maximale Wartezeit in ms
|
|
|
|
Returns:
|
|
bool: True wenn Element existiert
|
|
"""
|
|
if not self._ensure_browser():
|
|
return False
|
|
|
|
return self.automation.browser.is_element_visible(selector, timeout=timeout) |