""" TikTok-UI-Helper - Hilfsmethoden für die Interaktion mit der TikTok-UI """ import time import re from typing import Dict, List, Any, Optional, Tuple, Union, Callable from .tiktok_selectors import TikTokSelectors from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button from utils.logger import setup_logger # Konfiguriere Logger logger = setup_logger("tiktok_ui_helper") class TikTokUIHelper: """ Hilfsmethoden für die Interaktion mit der TikTok-Benutzeroberfläche. Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen. """ def __init__(self, automation): """ Initialisiert den TikTok-UI-Helper. Args: automation: Referenz auf die Hauptautomatisierungsklasse """ self.automation = automation # Browser wird direkt von automation verwendet self.selectors = TikTokSelectors() # Initialisiere TextSimilarity für Fuzzy-Matching self.text_similarity = TextSimilarity(default_threshold=0.7) logger.debug("TikTok-UI-Helper initialisiert") def _ensure_browser(self) -> bool: """ Stellt sicher, dass die Browser-Referenz verfügbar ist. Returns: bool: True wenn Browser verfügbar, False sonst """ if self.automation.browser is None: logger.error("Browser-Referenz nicht verfügbar") return False return True def fill_field_fuzzy(self, field_labels: Union[str, List[str]], value: str, fallback_selector: str = None, threshold: float = 0.7, timeout: int = 5000) -> bool: """ Füllt ein Formularfeld mit Fuzzy-Text-Matching aus. Args: field_labels: Bezeichner oder Liste von Bezeichnern des Feldes value: Einzugebender Wert fallback_selector: CSS-Selektor für Fallback threshold: Schwellenwert für die Textähnlichkeit (0-1) timeout: Zeitlimit für die Suche in Millisekunden Returns: bool: True bei Erfolg, False bei Fehler """ if not self._ensure_browser(): return False try: # Normalisiere field_labels zu einer Liste if isinstance(field_labels, str): field_labels = [field_labels] # Versuche, das Feld mit Fuzzy-Matching zu finden element = fuzzy_find_element( self.automation.browser.page, field_labels, selector_type="input", threshold=threshold, wait_time=timeout ) if element: # Versuche, das Feld zu fokussieren und den Wert einzugeben element.focus() time.sleep(0.1) element.fill("") # Leere das Feld zuerst time.sleep(0.2) # Text menschenähnlich eingeben for char in value: element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000) time.sleep(0.01) logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}") return True # Fuzzy-Matching fehlgeschlagen, versuche über Attribute if fallback_selector: field_success = self.automation.browser.fill_form_field(fallback_selector, value) if field_success: logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}") return True # Versuche noch alternative Selektoren basierend auf field_labels for label in field_labels: # Versuche aria-label Attribut aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']" if self.automation.browser.is_element_visible(aria_selector, timeout=1000): if self.automation.browser.fill_form_field(aria_selector, value): logger.info(f"Feld über aria-label gefüllt: {label}") return True # Versuche placeholder Attribut placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']" if self.automation.browser.is_element_visible(placeholder_selector, timeout=1000): if self.automation.browser.fill_form_field(placeholder_selector, value): logger.info(f"Feld über placeholder gefüllt: {label}") return True # Versuche name Attribut name_selector = f"input[name='{label.lower().replace(' ', '')}']" if self.automation.browser.is_element_visible(name_selector, timeout=1000): if self.automation.browser.fill_form_field(name_selector, value): logger.info(f"Feld über name-Attribut gefüllt: {label}") return True logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen") return False except Exception as e: logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {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 die Textähnlichkeit (0-1) timeout: Zeitlimit für die Suche in Millisekunden Returns: bool: True bei Erfolg, False bei Fehler """ if not self._ensure_browser(): return False try: # Normalisiere button_texts zu einer Liste if isinstance(button_texts, str): button_texts = [button_texts] # Logging der Suche logger.info(f"Suche nach Button mit Texten: {button_texts}") if not button_texts or button_texts == [[]]: logger.warning("Leere Button-Text-Liste angegeben!") return False # TikTok-spezifische Selektoren zuerst prüfen # Diese Selektoren sind häufig in TikTok's UI zu finden tiktok_button_selectors = [ "button[type='submit']", "button[data-e2e='send-code-button']", "button.e1w6iovg0", "button.css-10nhlj9-Button-StyledButton" ] for selector in tiktok_button_selectors: if self.automation.browser.is_element_visible(selector, timeout=1000): button_element = self.automation.browser.wait_for_selector(selector, timeout=1000) if button_element: button_text = button_element.inner_text().strip() # Überprüfe, ob der Button-Text mit einem der gesuchten Texte übereinstimmt for text in button_texts: if self.text_similarity.is_similar(text, button_text, threshold=threshold): logger.info(f"Button mit passendem Text gefunden: '{button_text}'") button_element.click() return True # Die allgemeine fuzzy_click_button-Funktion verwenden result = click_fuzzy_button( self.automation.browser.page, button_texts, threshold=threshold, timeout=timeout ) if result: logger.info(f"Button mit Fuzzy-Matching geklickt") return True # Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector if fallback_selector: logger.info(f"Versuche Fallback-Selektor: {fallback_selector}") if self.automation.browser.click_element(fallback_selector): logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}") return True # Versuche alternative Methoden # 1. Versuche über aria-label for text in button_texts: if not text: continue aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']" if self.automation.browser.is_element_visible(aria_selector, timeout=1000): if self.automation.browser.click_element(aria_selector): logger.info(f"Button über aria-label geklickt: {text}") return True # 2. Versuche über role='button' mit Text for text in button_texts: if not text: continue xpath_selector = f"//div[@role='button' and contains(., '{text}')]" if self.automation.browser.is_element_visible(xpath_selector, timeout=1000): if self.automation.browser.click_element(xpath_selector): logger.info(f"Button über role+text geklickt: {text}") return True # 3. Versuche über Link-Text for text in button_texts: if not text: continue link_selector = f"//a[contains(text(), '{text}')]" if self.automation.browser.is_element_visible(link_selector, timeout=1000): if self.automation.browser.click_element(link_selector): logger.info(f"Link mit passendem Text geklickt: {text}") return True # 4. Als letzten Versuch, klicke auf einen beliebigen Button logger.warning("Kein spezifischer Button gefunden, versuche beliebigen Button zu klicken") buttons = self.automation.browser.page.query_selector_all("button") if buttons and len(buttons) > 0: for button in buttons: visible = button.is_visible() if visible: logger.info("Klicke auf beliebigen sichtbaren Button") button.click() return True logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken") return False except Exception as e: logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}") return False def select_dropdown_option(self, dropdown_selector: str, option_value: str, option_type: str = "text", timeout: int = 5000) -> bool: """ Wählt eine Option aus einer Dropdown-Liste aus. Args: dropdown_selector: Selektor für das Dropdown-Element option_value: Wert oder Text der auszuwählenden Option option_type: "text" für Text-Matching, "value" für Wert-Matching timeout: Zeitlimit in Millisekunden Returns: bool: True bei Erfolg, False bei Fehler """ if not self._ensure_browser(): return False try: # Auf Dropdown-Element klicken, um die Optionen anzuzeigen dropdown_element = self.automation.browser.wait_for_selector(dropdown_selector, timeout=timeout) if not dropdown_element: logger.warning(f"Dropdown-Element nicht gefunden: {dropdown_selector}") return False # Dropdown öffnen dropdown_element.click() time.sleep(0.5) # Kurz warten, damit die Optionen angezeigt werden # Optionen suchen option_selector = "div[role='option']" options = self.automation.browser.page.query_selector_all(option_selector) if not options or len(options) == 0: logger.warning(f"Keine Optionen gefunden für Dropdown: {dropdown_selector}") return False # Option nach Text oder Wert suchen selected = False for option in options: option_text = option.inner_text().strip() if option_type == "text": if option_text == option_value or self.text_similarity.is_similar(option_text, option_value, threshold=0.9): option.click() selected = True break elif option_type == "value": option_val = option.get_attribute("value") or "" if option_val == option_value: option.click() selected = True break if not selected: logger.warning(f"Keine passende Option für '{option_value}' gefunden") return False logger.info(f"Option '{option_value}' im Dropdown ausgewählt") return True except Exception as e: logger.error(f"Fehler bei der Auswahl der Dropdown-Option: {e}") return False def check_for_error(self, error_selectors: List[str] = None, error_texts: List[str] = None) -> Optional[str]: """ Überprüft, ob Fehlermeldungen angezeigt werden. Args: error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen error_texts: Liste mit typischen Fehlertexten Returns: Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden """ if not self._ensure_browser(): return None try: # Standardselektoren verwenden, wenn keine angegeben sind if error_selectors is None: error_selectors = [ "div[role='alert']", "p[class*='error']", "span[class*='error']", ".error-message" ] # Standardfehlertexte verwenden, wenn keine angegeben sind if error_texts is None: error_texts = TikTokSelectors.get_error_indicators() # 1. Nach Fehlerselektoren suchen for selector in error_selectors: element = self.automation.browser.wait_for_selector(selector, timeout=2000) if element: error_text = element.text_content() if error_text and len(error_text.strip()) > 0: logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}") return error_text.strip() # 2. Alle Texte auf der Seite durchsuchen page_content = self.automation.browser.page.content() for error_text in error_texts: if error_text.lower() in page_content.lower(): # Versuche, den genauen Fehlertext zu extrahieren matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower()) if matches: full_error = matches[0].strip() logger.info(f"Fehlermeldung gefunden (Text): {full_error}") return full_error else: logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}") return error_text # 3. Nach weiteren Fehlerelementen suchen elements = self.automation.browser.page.query_selector_all("p, div, span") for element in elements: element_text = element.inner_text() if not element_text: continue element_text = element_text.strip() # Prüfe Textähnlichkeit mit Fehlertexten for error_text in error_texts: if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \ self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7): logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}") return element_text return None except Exception as e: logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}") return None def check_for_captcha(self) -> bool: """ Überprüft, ob ein Captcha angezeigt wird. Returns: bool: True wenn Captcha erkannt, False sonst """ if not self._ensure_browser(): return False try: # Selektoren für Captcha-Erkennung captcha_selectors = [ "div[data-testid='captcha']", "iframe[src*='captcha']", "iframe[title*='captcha']", "iframe[title*='reCAPTCHA']" ] # Captcha-Texte für textbasierte Erkennung captcha_texts = [ "captcha", "recaptcha", "sicherheitsüberprüfung", "security check", "i'm not a robot", "ich bin kein roboter", "verify you're human", "bestätige, dass du ein mensch bist" ] # Nach Selektoren suchen for selector in captcha_selectors: if self.automation.browser.is_element_visible(selector, timeout=2000): logger.warning(f"Captcha erkannt (Selektor): {selector}") return True # Nach Texten suchen page_content = self.automation.browser.page.content().lower() for text in captcha_texts: if text in page_content: logger.warning(f"Captcha erkannt (Text): {text}") return True return False except Exception as e: logger.error(f"Fehler bei der Captcha-Erkennung: {e}") return False def wait_for_element(self, selectors: Union[str, List[str]], timeout: int = 10000, check_interval: int = 500) -> Optional[Any]: """ Wartet auf das Erscheinen eines Elements. Args: selectors: CSS-Selektor oder Liste von Selektoren timeout: Zeitlimit in Millisekunden check_interval: Intervall zwischen den Prüfungen in Millisekunden Returns: Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist """ if not self._ensure_browser(): return None try: # Normalisiere selectors zu einer Liste if isinstance(selectors, str): selectors = [selectors] start_time = time.time() end_time = start_time + (timeout / 1000) while time.time() < end_time: for selector in selectors: element = self.automation.browser.wait_for_selector(selector, timeout=check_interval) if element: logger.info(f"Element mit Selektor '{selector}' gefunden") return element # Kurze Pause vor der nächsten Prüfung time.sleep(check_interval / 1000) logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'") return None except Exception as e: logger.error(f"Fehler beim Warten auf Element: {e}") return None def is_registration_successful(self) -> bool: """ Überprüft, ob die Registrierung erfolgreich war. Returns: bool: True wenn erfolgreich, False sonst """ try: # Erfolgsindikatoren überprüfen success_indicators = TikTokSelectors.SUCCESS_INDICATORS for selector in success_indicators: if self.automation.browser.is_element_visible(selector, timeout=2000): logger.info(f"Registrierung erfolgreich (Indikator gefunden: {selector})") return True # URL überprüfen current_url = self.automation.browser.page.url if "/foryou" in current_url or "tiktok.com/explore" in current_url: logger.info("Registrierung erfolgreich (Erfolgreiche Navigation erkannt)") return True # Überprüfen, ob Fehler angezeigt werden error_message = self.check_for_error() if error_message: logger.warning(f"Registrierung nicht erfolgreich: {error_message}") return False logger.warning("Konnte Registrierungserfolg nicht bestätigen") return False except Exception as e: logger.error(f"Fehler bei der Überprüfung des Registrierungserfolgs: {e}") return False