# social_networks/tiktok/tiktok_registration.py """ TikTok-Registrierung - Klasse für die Kontoerstellung bei TikTok """ import time import random import re from typing import Dict, List, Any, Optional, Tuple from .tiktok_selectors import TikTokSelectors from .tiktok_workflow import TikTokWorkflow from utils.logger import setup_logger # Konfiguriere Logger logger = setup_logger("tiktok_registration") class TikTokRegistration: """ Klasse für die Registrierung von TikTok-Konten. Enthält alle Methoden zur Kontoerstellung. """ def __init__(self, automation): """ Initialisiert die TikTok-Registrierung. Args: automation: Referenz auf die Hauptautomatisierungsklasse """ self.automation = automation # Browser wird direkt von automation verwendet self.selectors = TikTokSelectors() self.workflow = TikTokWorkflow.get_registration_workflow() logger.debug("TikTok-Registrierung initialisiert") def register_account(self, full_name: str, age: int, registration_method: str = "email", phone_number: str = None, **kwargs) -> Dict[str, Any]: """ Führt den vollständigen Registrierungsprozess für einen TikTok-Account durch. Args: full_name: Vollständiger Name für den Account age: Alter des Benutzers registration_method: "email" oder "phone" phone_number: Telefonnummer (nur bei registration_method="phone") **kwargs: Weitere optionale Parameter Returns: Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten """ # Browser wird direkt von automation verwendet # Validiere die Eingaben if not self._validate_registration_inputs(full_name, age, registration_method, phone_number): return { "success": False, "error": "Ungültige Eingabeparameter", "stage": "input_validation" } # Account-Daten generieren account_data = self._generate_account_data(full_name, age, registration_method, phone_number, **kwargs) # Starte den Registrierungsprozess logger.info(f"Starte TikTok-Registrierung für {account_data['username']} via {registration_method}") try: # 1. Zur Startseite navigieren self.automation._emit_customer_log("🌐 Mit TikTok verbinden...") if not self._navigate_to_homepage(): return { "success": False, "error": "Konnte nicht zur TikTok-Startseite navigieren", "stage": "navigation", "account_data": account_data } # 2. Cookie-Banner behandeln self.automation._emit_customer_log("⚙️ Einstellungen werden vorbereitet...") self._handle_cookie_banner() # 2b. Interessen-Dialog behandeln (falls vorhanden) self._handle_interests_dialog() # 3. Anmelden-Button klicken self.automation._emit_customer_log("📋 Registrierungsformular wird geöffnet...") if not self._click_login_button(): return { "success": False, "error": "Konnte nicht auf Anmelden-Button klicken", "stage": "login_button", "account_data": account_data } # 4. Registrieren-Link klicken if not self._click_register_link(): return { "success": False, "error": "Konnte nicht auf Registrieren-Link klicken", "stage": "register_link", "account_data": account_data } # 5. Telefon/E-Mail-Option auswählen if not self._click_phone_email_option(): return { "success": False, "error": "Konnte nicht auf Telefon/E-Mail-Option klicken", "stage": "phone_email_option", "account_data": account_data } # 6. E-Mail oder Telefon als Registrierungsmethode wählen if not self._select_registration_method(registration_method): return { "success": False, "error": f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen", "stage": "registration_method", "account_data": account_data } # 7. Geburtsdatum eingeben self.automation._emit_customer_log("🎂 Geburtsdatum wird festgelegt...") if not self._enter_birthday(account_data["birthday"]): return { "success": False, "error": "Fehler beim Eingeben des Geburtsdatums", "stage": "birthday", "account_data": account_data } # 8. Registrierungsformular ausfüllen self.automation._emit_customer_log("📝 Persönliche Daten werden übertragen...") if not self._fill_registration_form(account_data, registration_method): return { "success": False, "error": "Fehler beim Ausfüllen des Registrierungsformulars", "stage": "registration_form", "account_data": account_data } # 9. Bestätigungscode wurde bereits in _fill_registration_form() behandelt # (Code-Eingabe passiert jetzt BEVOR Passwort-Eingabe für bessere Stabilität) logger.debug("Verifizierung bereits in optimierter Reihenfolge abgeschlossen") # 10. Benutzernamen erstellen self.automation._emit_customer_log("👤 Benutzername wird erstellt...") if not self._create_username(account_data): return { "success": False, "error": "Fehler beim Erstellen des Benutzernamens", "stage": "username", "account_data": account_data } # 11. Erfolgreiche Registrierung überprüfen self.automation._emit_customer_log("🔍 Account wird finalisiert...") if not self._check_registration_success(): return { "success": False, "error": "Registrierung fehlgeschlagen oder konnte nicht verifiziert werden", "stage": "final_check", "account_data": account_data } # Registrierung erfolgreich abgeschlossen logger.info(f"TikTok-Account {account_data['username']} erfolgreich erstellt") self.automation._emit_customer_log("✅ Account erfolgreich erstellt!") return { "success": True, "stage": "completed", "account_data": account_data } except Exception as e: error_msg = f"Unerwarteter Fehler bei der TikTok-Registrierung: {str(e)}" logger.error(error_msg, exc_info=True) return { "success": False, "error": error_msg, "stage": "exception", "account_data": account_data } def _validate_registration_inputs(self, full_name: str, age: int, registration_method: str, phone_number: str) -> bool: """ Validiert die Eingaben für die Registrierung. Args: full_name: Vollständiger Name für den Account age: Alter des Benutzers registration_method: "email" oder "phone" phone_number: Telefonnummer (nur bei registration_method="phone") Returns: bool: True wenn alle Eingaben gültig sind, False sonst """ # Vollständiger Name prüfen if not full_name or len(full_name) < 3: logger.error("Ungültiger vollständiger Name") return False # Alter prüfen if age < 13: logger.error("Benutzer muss mindestens 13 Jahre alt sein") return False # Registrierungsmethode prüfen if registration_method not in ["email", "phone"]: logger.error(f"Ungültige Registrierungsmethode: {registration_method}") return False # Telefonnummer prüfen, falls erforderlich if registration_method == "phone" and not phone_number: logger.error("Telefonnummer erforderlich für Registrierung via Telefon") return False return True def _generate_account_data(self, full_name: str, age: int, registration_method: str, phone_number: str, **kwargs) -> Dict[str, Any]: """ Generiert Account-Daten für die Registrierung. Args: full_name: Vollständiger Name für den Account age: Alter des Benutzers registration_method: "email" oder "phone" phone_number: Telefonnummer (nur bei registration_method="phone") **kwargs: Weitere optionale Parameter Returns: Dict[str, Any]: Generierte Account-Daten """ # Benutzername generieren username = kwargs.get("username") if not username: username = self.automation.username_generator.generate_username("tiktok", full_name) # Passwort generieren password = kwargs.get("password") if not password: password = self.automation.password_generator.generate_password("tiktok") # E-Mail generieren (falls nötig) email = None if registration_method == "email": email_prefix = username.lower().replace(".", "").replace("_", "") email = f"{email_prefix}@{self.automation.email_domain}" # Geburtsdatum generieren birthday = self.automation.birthday_generator.generate_birthday_components("tiktok", age) # Account-Daten zusammenstellen account_data = { "username": username, "password": password, "full_name": full_name, "email": email, "phone": phone_number, "birthday": birthday, "age": age, "registration_method": registration_method } logger.debug(f"Account-Daten generiert: {account_data['username']}") return account_data def _navigate_to_homepage(self) -> bool: """ Navigiert zur TikTok-Startseite. Returns: bool: True bei Erfolg, False bei Fehler """ try: # Zur Startseite navigieren self.automation.browser.navigate_to(self.selectors.BASE_URL) # Warten, bis die Seite geladen ist self.automation.human_behavior.wait_for_page_load() # Screenshot erstellen self.automation._take_screenshot("tiktok_homepage") # Prüfen, ob die Seite korrekt geladen wurde - mehrere Selektoren versuchen page_loaded = False login_button_selectors = [ self.selectors.LOGIN_BUTTON, self.selectors.LOGIN_BUTTON_CLASS, "button.TUXButton:has-text('Anmelden')", "button:has(.TUXButton-label:text('Anmelden'))", "//button[contains(text(), 'Anmelden')]" ] for selector in login_button_selectors: if self.automation.browser.is_element_visible(selector, timeout=5000): logger.info(f"TikTok-Startseite erfolgreich geladen - Login-Button gefunden: {selector}") page_loaded = True break if not page_loaded: logger.warning("TikTok-Startseite nicht korrekt geladen - kein Login-Button gefunden") # Debug: Seiteninhalt loggen current_url = self.automation.browser.page.url logger.debug(f"Aktuelle URL: {current_url}") return False logger.info("Erfolgreich zur TikTok-Startseite navigiert") return True except Exception as e: logger.error(f"Fehler beim Navigieren zur TikTok-Startseite: {e}") return False def _handle_interests_dialog(self) -> bool: """ Behandelt den "Wähle deine Interessen aus" Dialog, falls angezeigt. Dieser Dialog erscheint oft beim ersten Besuch der TikTok-Seite. Returns: bool: True wenn Dialog behandelt wurde oder nicht existiert, False bei Fehler """ try: # Prüfe ob der Interessen-Dialog vorhanden ist # Suche nach verschiedenen möglichen Selektoren für den Dialog interests_dialog_selectors = [ "button:has-text('Überspringen')", "button:has-text('Skip')", "button.TUXButton--secondary:has-text('Überspringen')", "button.TUXButton--secondary:has-text('Skip')", "button[type='button']:has-text('Überspringen')", "button[type='button']:has-text('Skip')" ] # Warte kurz und prüfe ob der Dialog erscheint dialog_found = False for selector in interests_dialog_selectors: if self.automation.browser.is_element_visible(selector, timeout=2000): dialog_found = True logger.info(f"Interessen-Dialog erkannt mit Selector: {selector}") break if dialog_found: self.automation._emit_customer_log("⏭️ Überspringe Interessen-Auswahl...") # Versuche verschiedene Methoden den Überspringen-Button zu klicken skip_clicked = False # Methode 1: Direkter Click auf Button mit Text for selector in interests_dialog_selectors: try: if self.automation.browser.click_element(selector, timeout=1000): logger.info(f"Interessen-Dialog übersprungen mit: {selector}") skip_clicked = True break except: continue # Methode 2: Falls direkter Click nicht funktioniert, versuche mit JavaScript if not skip_clicked: try: # JavaScript-Click auf Button mit "Überspringen" Text js_code = """ const buttons = document.querySelectorAll('button'); for (let button of buttons) { if (button.textContent.includes('Überspringen') || button.textContent.includes('Skip')) { button.click(); return true; } } return false; """ result = self.automation.browser.page.evaluate(js_code) if result: logger.info("Interessen-Dialog mit JavaScript übersprungen") skip_clicked = True except Exception as e: logger.warning(f"JavaScript-Click fehlgeschlagen: {e}") if skip_clicked: # Warte kurz nach dem Überspringen self.automation.human_behavior.random_delay(1.0, 2.0) logger.info("Interessen-Dialog erfolgreich übersprungen") return True else: logger.warning("Konnte Interessen-Dialog nicht überspringen - versuche trotzdem fortzufahren") return True # Trotzdem fortfahren logger.debug("Kein Interessen-Dialog gefunden - fahre normal fort") return True except Exception as e: logger.error(f"Fehler beim Behandeln des Interessen-Dialogs: {e}") # Nicht als kritischer Fehler behandeln - versuche fortzufahren return True def _handle_cookie_banner(self) -> bool: """ Behandelt den Cookie-Banner, falls angezeigt. Akzeptiert IMMER Cookies für vollständiges Session-Management bei der Registrierung. Returns: bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler """ # Cookie-Dialog-Erkennung if self.automation.browser.is_element_visible(self.selectors.COOKIE_DIALOG, timeout=2000): logger.info("Cookie-Banner erkannt - akzeptiere alle Cookies für Session-Management") # Akzeptieren-Button suchen und klicken (PRIMÄR für Registrierung) accept_success = self.automation.ui_helper.click_button_fuzzy( self.selectors.get_button_texts("accept_cookies"), self.selectors.COOKIE_ACCEPT_BUTTON ) if accept_success: logger.info("Cookie-Banner erfolgreich akzeptiert - Session-Cookies werden gespeichert") self.automation.human_behavior.random_delay(0.5, 1.5) return True else: logger.warning("Konnte Cookie-Banner nicht akzeptieren, versuche alternativen Akzeptieren-Button") # Alternative Akzeptieren-Selektoren versuchen alternative_accept_selectors = [ "//button[contains(text(), 'Alle akzeptieren')]", "//button[contains(text(), 'Accept All')]", "//button[contains(text(), 'Zulassen')]", "//button[contains(text(), 'Allow All')]", "//button[contains(@aria-label, 'Accept')]", "[data-testid='accept-all-button']" ] for selector in alternative_accept_selectors: if self.automation.browser.is_element_visible(selector, timeout=1000): if self.automation.browser.click_element(selector): logger.info("Cookie-Banner mit alternativem Selector akzeptiert") self.automation.human_behavior.random_delay(0.5, 1.5) return True logger.error("Konnte Cookie-Banner nicht akzeptieren - Session-Management könnte beeinträchtigt sein") return False else: logger.debug("Kein Cookie-Banner erkannt") return True def _click_login_button(self) -> bool: """ Klickt auf den Anmelden-Button auf der Startseite. Returns: bool: True bei Erfolg, False bei Fehler """ try: # Liste aller Login-Button-Selektoren, die wir versuchen wollen login_selectors = [ self.selectors.LOGIN_BUTTON, # button#header-login-button self.selectors.LOGIN_BUTTON_CLASS, # button.TUXButton:has-text('Anmelden') self.selectors.LOGIN_BUTTON_TOP_RIGHT, # button#top-right-action-bar-login-button "button.TUXButton[id='header-login-button']", # Spezifischer Selektor "button.TUXButton--primary:has-text('Anmelden')", # CSS-Klassen-basiert "button[aria-label*='Anmelden']", # Aria-Label "button:has(.TUXButton-label:text('Anmelden'))" # Verschachtelte Struktur ] # Versuche jeden Selektor for i, selector in enumerate(login_selectors): logger.debug(f"Versuche Login-Selektor {i+1}: {selector}") if self.automation.browser.is_element_visible(selector, timeout=3000): result = self.automation.browser.click_element(selector) if result: logger.info(f"Anmelden-Button erfolgreich geklickt mit Selektor {i+1}") self.automation.human_behavior.random_delay(0.5, 1.5) return True # Versuche es mit Fuzzy-Button-Matching result = self.automation.ui_helper.click_button_fuzzy( ["Anmelden", "Log in", "Login"], self.selectors.LOGIN_BUTTON_FALLBACK ) if result: logger.info("Anmelden-Button über Fuzzy-Matching erfolgreich geklickt") self.automation.human_behavior.random_delay(0.5, 1.5) return True logger.error("Konnte keinen Anmelden-Button finden") return False except Exception as e: logger.error(f"Fehler beim Klicken auf den Anmelden-Button: {e}") return False def _click_register_link(self) -> bool: """ Klickt auf den Registrieren-Link im Login-Dialog. Returns: bool: True bei Erfolg, False bei Fehler """ try: # Warten, bis der Login-Dialog angezeigt wird self.automation.human_behavior.random_delay(2.0, 3.0) # Screenshot für Debugging self.automation._take_screenshot("after_login_button_click") # Verschiedene Registrieren-Selektoren versuchen register_selectors = [ "a:text('Registrieren')", # Direkter Text-Match "button:text('Registrieren')", # Button-Text "div:text('Registrieren')", # Div-Text "span:text('Registrieren')", # Span-Text "[data-e2e*='signup']", # Data-Attribute "[data-e2e*='register']", # Data-Attribute "a[href*='signup']", # Signup-Link "//a[contains(text(), 'Registrieren')]", # XPath "//button[contains(text(), 'Registrieren')]", # XPath Button "//span[contains(text(), 'Registrieren')]", # XPath Span "//div[contains(text(), 'Konto erstellen')]", # Alternative Text "//a[contains(text(), 'Sign up')]", # Englisch ".signup-link", # CSS-Klasse ".register-link" # CSS-Klasse ] # Versuche jeden Selektor for i, selector in enumerate(register_selectors): logger.debug(f"Versuche Registrieren-Selektor {i+1}: {selector}") try: if self.automation.browser.is_element_visible(selector, timeout=2000): result = self.automation.browser.click_element(selector) if result: logger.info(f"Registrieren-Link erfolgreich geklickt mit Selektor {i+1}: {selector}") self.automation.human_behavior.random_delay(0.5, 1.5) return True except Exception as e: logger.debug(f"Selektor {i+1} fehlgeschlagen: {e}") continue # Fallback: Fuzzy-Text-Suche try: page_content = self.automation.browser.page.content() if "Registrieren" in page_content or "Sign up" in page_content: logger.info("Registrieren-Text auf Seite gefunden, versuche Textklick") # Versuche verschiedene Text-Klick-Strategien text_selectors = [ "text=Registrieren", "text=Sign up", "text=Konto erstellen" ] for text_sel in text_selectors: try: element = self.automation.browser.page.locator(text_sel).first if element.is_visible(): element.click() logger.info(f"Auf Text geklickt: {text_sel}") self.automation.human_behavior.random_delay(0.5, 1.5) return True except Exception: continue except Exception as e: logger.debug(f"Fallback-Text-Suche fehlgeschlagen: {e}") logger.error("Konnte keinen Registrieren-Link finden") return False except Exception as e: logger.error(f"Fehler beim Klicken auf den Registrieren-Link: {e}") # Debug-Screenshot bei Fehler self.automation._take_screenshot("register_link_error") return False def _click_phone_email_option(self) -> bool: """ Klickt auf die Telefon/E-Mail-Option im Registrierungsdialog. Returns: bool: True bei Erfolg, False bei Fehler """ try: # Warten, bis der Registrierungsdialog angezeigt wird self.automation.human_behavior.random_delay(1.0, 2.0) # Prüfen, ob wir bereits die Optionen für Telefon/E-Mail sehen if self.automation.browser.is_element_visible(self.selectors.EMAIL_FIELD, timeout=2000) or \ self.automation.browser.is_element_visible(self.selectors.PHONE_FIELD, timeout=2000): logger.info("Bereits auf der Telefon/E-Mail-Registrierungsseite") return True # Versuche, die Telefon/E-Mail-Option zu finden und zu klicken if self.automation.browser.is_element_visible(self.selectors.PHONE_EMAIL_OPTION, timeout=2000): result = self.automation.browser.click_element(self.selectors.PHONE_EMAIL_OPTION) if result: logger.info("Telefon/E-Mail-Option erfolgreich geklickt") self.automation.human_behavior.random_delay(0.5, 1.5) return True # Versuche es mit Fuzzy-Button-Matching result = self.automation.ui_helper.click_button_fuzzy( ["Telefonnummer oder E-Mail-Adresse nutzen", "Use phone or email", "Phone or email"], self.selectors.PHONE_EMAIL_OPTION_FALLBACK ) if result: logger.info("Telefon/E-Mail-Option über Fuzzy-Matching erfolgreich geklickt") self.automation.human_behavior.random_delay(0.5, 1.5) return True logger.error("Konnte keine Telefon/E-Mail-Option finden") return False except Exception as e: logger.error(f"Fehler beim Klicken auf die Telefon/E-Mail-Option: {e}") return False def _select_registration_method(self, registration_method: str) -> bool: """ Wählt die Registrierungsmethode (E-Mail oder Telefon). Args: registration_method: "email" oder "phone" Returns: bool: True bei Erfolg, False bei Fehler """ try: # Warten, bis die Registrierungsmethoden-Seite geladen ist self.automation.human_behavior.random_delay(1.0, 2.0) if registration_method == "email": # Wenn bereits das E-Mail-Feld sichtbar ist, sind wir schon auf der richtigen Seite if self.automation.browser.is_element_visible(self.selectors.EMAIL_FIELD, timeout=1000): logger.info("Bereits auf der E-Mail-Registrierungsseite") return True # Suche nach dem "Mit E-Mail-Adresse registrieren" Link if self.automation.browser.is_element_visible(self.selectors.EMAIL_OPTION, timeout=2000): result = self.automation.browser.click_element(self.selectors.EMAIL_OPTION) if result: logger.info("E-Mail-Registrierungsmethode erfolgreich ausgewählt") self.automation.human_behavior.random_delay(0.5, 1.5) return True # Versuche es mit Fuzzy-Button-Matching result = self.automation.ui_helper.click_button_fuzzy( ["Mit E-Mail-Adresse registrieren", "Register with email", "E-Mail-Adresse"], self.selectors.EMAIL_OPTION_FALLBACK ) if result: logger.info("E-Mail-Option über Fuzzy-Matching erfolgreich geklickt") self.automation.human_behavior.random_delay(0.5, 1.5) return True elif registration_method == "phone": # Wenn bereits das Telefon-Feld sichtbar ist, sind wir schon auf der richtigen Seite if self.automation.browser.is_element_visible(self.selectors.PHONE_FIELD, timeout=1000): logger.info("Bereits auf der Telefon-Registrierungsseite") return True # Suche nach dem "Mit Telefonnummer registrieren" Link if self.automation.browser.is_element_visible(self.selectors.PHONE_OPTION, timeout=2000): result = self.automation.browser.click_element(self.selectors.PHONE_OPTION) if result: logger.info("Telefon-Registrierungsmethode erfolgreich ausgewählt") self.automation.human_behavior.random_delay(0.5, 1.5) return True # Versuche es mit Fuzzy-Button-Matching result = self.automation.ui_helper.click_button_fuzzy( ["Mit Telefonnummer registrieren", "Register with phone", "Telefonnummer"], self.selectors.PHONE_OPTION_FALLBACK ) if result: logger.info("Telefon-Option über Fuzzy-Matching erfolgreich geklickt") self.automation.human_behavior.random_delay(0.5, 1.5) return True logger.error(f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen") return False except Exception as e: logger.error(f"Fehler beim Auswählen der Registrierungsmethode: {e}") return False def _enter_birthday(self, birthday: Dict[str, int]) -> bool: """ Gibt das Geburtsdatum ein. Args: birthday: Dictionary mit 'year', 'month', 'day' Schlüsseln Returns: bool: True bei Erfolg, False bei Fehler """ try: # Warten, bis die Geburtstagsauswahl angezeigt wird self.automation.human_behavior.random_delay(1.0, 2.0) # Screenshot für Debugging self.automation._take_screenshot("birthday_page") # Verschiedene Monat-Dropdown-Selektoren versuchen month_selectors = [ "div.css-1fi2hzv-DivSelectLabel:has-text('Monat')", # Exakt TikTok-Klasse "div.e1phcp2x1:has-text('Monat')", # TikTok-Klasse alternative "div:has-text('Monat')", # Text-basiert (funktioniert!) self.selectors.BIRTHDAY_MONTH_DROPDOWN, # select[name='month'] "div[data-e2e='date-picker-month']", # TikTok-spezifisch "button[data-testid='month-selector']", # Test-ID "div:has-text('Month')", # Englisch "[aria-label*='Month']", # Aria-Label "[aria-label*='Monat']", # Deutsch "div[role='combobox']:has-text('Monat')", # Combobox ".month-selector", # CSS-Klasse ".date-picker-month", # CSS-Klasse "//div[contains(text(), 'Monat')]", # XPath "//button[contains(text(), 'Monat')]", # XPath Button "//select[@name='month']" # XPath Select ] month_dropdown = None for i, selector in enumerate(month_selectors): logger.debug(f"Versuche Monat-Selektor {i+1}: {selector}") try: if self.automation.browser.is_element_visible(selector, timeout=2000): month_dropdown = self.automation.browser.page.locator(selector).first logger.info(f"Monat-Dropdown gefunden mit Selektor {i+1}: {selector}") break except Exception as e: logger.debug(f"Monat-Selektor {i+1} fehlgeschlagen: {e}") continue if not month_dropdown: logger.error("Monat-Dropdown nicht gefunden - alle Selektoren fehlgeschlagen") return False month_dropdown.click() self.automation.human_behavior.random_delay(0.3, 0.8) # Monat-Option auswählen - TikTok verwendet Monatsnamen! month_names = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"] month_name = month_names[birthday['month'] - 1] # birthday['month'] ist 1-12 month_selected = False month_option_selectors = [ f"div.css-vz5m7n-DivOption:has-text('{month_name}')", # Exakte TikTok-Klasse + Monatsname f"div.e1phcp2x5:has-text('{month_name}')", # TikTok-Klasse alternative + Monatsname f"[role='option']:has-text('{month_name}')", # Role + Monatsname f"div:has-text('{month_name}')", # Einfach Monatsname f"//div[@role='option'][contains(text(), '{month_name}')]", # XPath + Monatsname f"option[value='{birthday['month']}']", # Standard HTML (Fallback) f"div[data-value='{birthday['month']}']", # Custom Dropdown (Fallback) f"li[data-value='{birthday['month']}']", # List-Item (Fallback) f"button:has-text('{birthday['month']:02d}')", # Button mit Monatszahl (Fallback) f"div:has-text('{birthday['month']:02d}')", # Div mit Monatszahl (Fallback) f"[role='option']:has-text('{birthday['month']:02d}')" # Role-based Zahl (Fallback) ] for i, option_selector in enumerate(month_option_selectors): logger.debug(f"Versuche Monat-Option-Selektor {i+1}: {option_selector}") try: if self.automation.browser.is_element_visible(option_selector, timeout=1000): self.automation.browser.click_element(option_selector) logger.info(f"Monat {birthday['month']} ausgewählt mit Selektor {i+1}") month_selected = True break except Exception as e: logger.debug(f"Monat-Option-Selektor {i+1} fehlgeschlagen: {e}") continue if not month_selected: logger.error(f"Konnte Monat {birthday['month']} nicht auswählen") return False self.automation.human_behavior.random_delay(0.3, 0.8) # Tag-Dropdown finden day_selectors = [ "div.css-1fi2hzv-DivSelectLabel:has-text('Tag')", # Exakt TikTok-Klasse "div.e1phcp2x1:has-text('Tag')", # TikTok-Klasse alternative "div:has-text('Tag')", # Text-basiert self.selectors.BIRTHDAY_DAY_DROPDOWN, # select[name='day'] "div[data-e2e='date-picker-day']", # TikTok-spezifisch "button[data-testid='day-selector']", # Test-ID "div:has-text('Day')", # Englisch "[aria-label*='Day']", # Aria-Label "[aria-label*='Tag']", # Deutsch "div[role='combobox']:has-text('Tag')", # Combobox ".day-selector", # CSS-Klasse ".date-picker-day", # CSS-Klasse "//div[contains(text(), 'Tag')]", # XPath "//button[contains(text(), 'Tag')]", # XPath Button "//select[@name='day']" # XPath Select ] day_dropdown = None for i, selector in enumerate(day_selectors): logger.debug(f"Versuche Tag-Selektor {i+1}: {selector}") try: if self.automation.browser.is_element_visible(selector, timeout=2000): day_dropdown = self.automation.browser.page.locator(selector).first logger.info(f"Tag-Dropdown gefunden mit Selektor {i+1}: {selector}") break except Exception as e: logger.debug(f"Tag-Selektor {i+1} fehlgeschlagen: {e}") continue if not day_dropdown: logger.error("Tag-Dropdown nicht gefunden") return False day_dropdown.click() self.automation.human_behavior.random_delay(0.3, 0.8) # Tag-Option auswählen - TikTok verwendet einfache Zahlen day_selected = False day_option_selectors = [ f"div.css-vz5m7n-DivOption:has-text('{birthday['day']}')", # Exakte TikTok-Klasse + Tag f"div.e1phcp2x5:has-text('{birthday['day']}')", # TikTok-Klasse alternative + Tag f"[role='option']:has-text('{birthday['day']}')", # Role + Tag f"div:has-text('{birthday['day']}')", # Einfach Tag f"//div[@role='option'][contains(text(), '{birthday['day']}')]", # XPath + Tag f"option[value='{birthday['day']}']", # Standard HTML (Fallback) f"div[data-value='{birthday['day']}']", # Custom Dropdown (Fallback) f"li[data-value='{birthday['day']}']", # List-Item (Fallback) f"button:has-text('{birthday['day']:02d}')", # Button mit führender Null (Fallback) f"div:has-text('{birthday['day']:02d}')" # Div mit führender Null (Fallback) ] for i, option_selector in enumerate(day_option_selectors): logger.debug(f"Versuche Tag-Option-Selektor {i+1}: {option_selector}") try: if self.automation.browser.is_element_visible(option_selector, timeout=1000): self.automation.browser.click_element(option_selector) logger.info(f"Tag {birthday['day']} ausgewählt mit Selektor {i+1}") day_selected = True break except Exception as e: logger.debug(f"Tag-Option-Selektor {i+1} fehlgeschlagen: {e}") continue if not day_selected: logger.error(f"Konnte Tag {birthday['day']} nicht auswählen") return False self.automation.human_behavior.random_delay(0.3, 0.8) # Jahr-Dropdown finden year_selectors = [ "div.css-1fi2hzv-DivSelectLabel:has-text('Jahr')", # Exakt TikTok-Klasse "div.e1phcp2x1:has-text('Jahr')", # TikTok-Klasse alternative "div:has-text('Jahr')", # Text-basiert self.selectors.BIRTHDAY_YEAR_DROPDOWN, # select[name='year'] "div[data-e2e='date-picker-year']", # TikTok-spezifisch "button[data-testid='year-selector']", # Test-ID "div:has-text('Year')", # Englisch "[aria-label*='Year']", # Aria-Label "[aria-label*='Jahr']", # Deutsch "div[role='combobox']:has-text('Jahr')", # Combobox ".year-selector", # CSS-Klasse ".date-picker-year", # CSS-Klasse "//div[contains(text(), 'Jahr')]", # XPath "//button[contains(text(), 'Jahr')]", # XPath Button "//select[@name='year']" # XPath Select ] year_dropdown = None for i, selector in enumerate(year_selectors): logger.debug(f"Versuche Jahr-Selektor {i+1}: {selector}") try: if self.automation.browser.is_element_visible(selector, timeout=2000): year_dropdown = self.automation.browser.page.locator(selector).first logger.info(f"Jahr-Dropdown gefunden mit Selektor {i+1}: {selector}") break except Exception as e: logger.debug(f"Jahr-Selektor {i+1} fehlgeschlagen: {e}") continue if not year_dropdown: logger.error("Jahr-Dropdown nicht gefunden") return False year_dropdown.click() self.automation.human_behavior.random_delay(0.3, 0.8) # Jahr-Option auswählen - TikTok verwendet vierstellige Jahreszahlen year_selected = False year_option_selectors = [ f"div.css-vz5m7n-DivOption:has-text('{birthday['year']}')", # Exakte TikTok-Klasse + Jahr f"div.e1phcp2x5:has-text('{birthday['year']}')", # TikTok-Klasse alternative + Jahr f"[role='option']:has-text('{birthday['year']}')", # Role + Jahr f"div:has-text('{birthday['year']}')", # Einfach Jahr f"//div[@role='option'][contains(text(), '{birthday['year']}')]", # XPath + Jahr f"option[value='{birthday['year']}']", # Standard HTML (Fallback) f"div[data-value='{birthday['year']}']", # Custom Dropdown (Fallback) f"li[data-value='{birthday['year']}']", # List-Item (Fallback) f"button:has-text('{birthday['year']}')", # Button (Fallback) f"span:has-text('{birthday['year']}')" # Span (Fallback) ] for i, option_selector in enumerate(year_option_selectors): logger.debug(f"Versuche Jahr-Option-Selektor {i+1}: {option_selector}") try: if self.automation.browser.is_element_visible(option_selector, timeout=1000): self.automation.browser.click_element(option_selector) logger.info(f"Jahr {birthday['year']} ausgewählt mit Selektor {i+1}") year_selected = True break except Exception as e: logger.debug(f"Jahr-Option-Selektor {i+1} fehlgeschlagen: {e}") continue if not year_selected: logger.error(f"Konnte Jahr {birthday['year']} nicht auswählen") return False logger.info(f"Geburtsdatum {birthday['month']}/{birthday['day']}/{birthday['year']} erfolgreich eingegeben") return True except Exception as e: logger.error(f"Fehler beim Eingeben des Geburtsdatums: {e}") return False def _fill_registration_form(self, account_data: Dict[str, Any], registration_method: str) -> bool: """ Füllt das Registrierungsformular aus. Args: account_data: Account-Daten für die Registrierung registration_method: "email" oder "phone" Returns: bool: True bei Erfolg, False bei Fehler """ try: # Je nach Registrierungsmethode das entsprechende Feld ausfüllen if registration_method == "email": # E-Mail-Feld ausfüllen email_success = self.automation.ui_helper.fill_field_fuzzy( ["E-Mail-Adresse", "Email", "E-Mail"], account_data["email"], self.selectors.EMAIL_FIELD ) if not email_success: logger.error("Konnte E-Mail-Feld nicht ausfüllen") return False logger.info(f"E-Mail-Feld ausgefüllt: {account_data['email']}") # NEUE REIHENFOLGE: Bei E-Mail sofort Code senden, dann Passwort self.automation.human_behavior.random_delay(0.5, 1.0) logger.info("NEUE STRATEGIE: Code senden DIREKT nach E-Mail-Eingabe") send_code_success = self._click_send_code_button_with_retry() if not send_code_success: logger.error("Konnte 'Code senden'-Button nicht klicken") return False logger.info("'Code senden'-Button erfolgreich geklickt - Code wird gesendet") # NEUE STRATEGIE: Code eingeben BEVOR Passwort (verhindert UI-Interferenz) logger.info("OPTIMIERTE REIHENFOLGE: Warte auf E-Mail und gebe Code ein BEVOR Passwort") # Warten auf Verification Code und eingeben verification_success = self._handle_verification_code_entry(account_data) if not verification_success: logger.error("Konnte Verifizierungscode nicht eingeben") return False logger.info("Verifizierungscode erfolgreich eingegeben") # Jetzt erst Passwort eingeben (nach Code-Verifikation) self.automation.human_behavior.random_delay(1.0, 2.0) logger.info("Gebe jetzt Passwort ein (nach Code-Verifikation)") # Nach Code-Eingabe erscheint ein neues Passwort-Feld # Verschiedene Selektoren für das Passwort-Feld nach Code-Eingabe password_selectors = [ # Aktueller Selektor basierend auf Console-Output "input[type='password'][placeholder='Passwort']", "input.css-wv3bkt-InputContainer.etcs7ny1", "input.css-wv3bkt-InputContainer", "input.etcs7ny1[type='password']", # Original Selektor self.selectors.PASSWORD_FIELD, # Fallback-Selektoren "input[type='password']", "input[placeholder*='Passwort']", "input[placeholder*='Password']", "input[name*='password']" ] password_success = False for selector in password_selectors: if self.automation.browser.is_element_visible(selector, timeout=2000): logger.info(f"Passwort-Feld gefunden: {selector}") # Verwende Character-by-Character Eingabe für Passwort-Feld password_success = self._fill_password_field_character_by_character(selector, account_data["password"]) if password_success: # VALIDATION: Prüfe ob Passwort tatsächlich im Feld steht self.automation.human_behavior.random_delay(0.5, 1.0) actual_value = self._get_input_field_value(selector) if actual_value == account_data["password"]: logger.info("Passwort erfolgreich eingegeben und validiert") break else: logger.warning(f"Passwort-Validierung fehlgeschlagen: erwartet='{account_data['password']}', erhalten='{actual_value}'") password_success = False else: logger.debug(f"Passwort-Eingabe mit Selektor {selector} fehlgeschlagen") if not password_success: logger.warning("Fallback 1: Versuche UI Helper für Passwort-Eingabe") password_success = self.automation.ui_helper.fill_field_fuzzy( ["Passwort", "Password"], account_data["password"], self.selectors.PASSWORD_FIELD ) # Validiere auch den Fallback if password_success: self.automation.human_behavior.random_delay(0.5, 1.0) for selector in password_selectors: if self.automation.browser.is_element_visible(selector, timeout=1000): actual_value = self._get_input_field_value(selector) if actual_value == account_data["password"]: logger.info("Passwort-Fallback erfolgreich validiert") break else: logger.warning(f"Passwort-Fallback fehlgeschlagen: erwartet='{account_data['password']}', erhalten='{actual_value}'") password_success = False # Wenn immer noch nicht erfolgreich, versuche direktes Playwright fill() if not password_success: logger.warning("Fallback 2: Versuche direktes Playwright fill()") for selector in password_selectors: if self.automation.browser.is_element_visible(selector, timeout=1000): try: element = self.automation.browser.page.locator(selector).first element.clear() element.fill(account_data["password"]) self.automation.human_behavior.random_delay(0.5, 1.0) actual_value = self._get_input_field_value(selector) if actual_value == account_data["password"]: logger.info("Passwort mit Playwright fill() erfolgreich") password_success = True break else: logger.debug(f"Playwright fill() für {selector} fehlgeschlagen") except Exception as e: logger.debug(f"Playwright fill() Fehler für {selector}: {e}") if not password_success: logger.error("Konnte Passwort-Feld nicht ausfüllen") return False logger.info("Passwort-Feld ausgefüllt (nach Code-Verifikation)") # 6. WORKAROUND: Code-Feld manipulieren (0 hinzufügen und löschen) self.automation.human_behavior.random_delay(0.5, 1.0) logger.info("Führe Workaround aus: Gehe zurück zum Code-Feld und füge 0 hinzu/lösche sie") # Finde das Code-Feld wieder code_field_selectors = [ "input[type='text'][placeholder='Gib den sechsstelligen Code ein']", "input.css-11to27l-InputContainer", "input.etcs7ny1", "input[placeholder*='sechsstelligen Code']", "input[placeholder*='Code']" ] workaround_success = False for selector in code_field_selectors: if self.automation.browser.is_element_visible(selector, timeout=1000): try: element = self.automation.browser.page.locator(selector).first # Fokussiere das Code-Feld element.focus() self.automation.human_behavior.random_delay(0.2, 0.3) # Hole aktuellen Wert (sollte der 6-stellige Code sein) current_code = element.input_value() logger.debug(f"Aktueller Code im Feld: {current_code}") # Füge eine 0 hinzu element.press("End") # Gehe ans Ende element.type("0") self.automation.human_behavior.random_delay(0.2, 0.3) logger.debug("0 hinzugefügt") # Lösche die 0 wieder element.press("Backspace") self.automation.human_behavior.random_delay(0.2, 0.3) logger.debug("0 wieder gelöscht") # Verlasse das Feld element.blur() logger.info("Workaround erfolgreich ausgeführt") workaround_success = True break except Exception as e: logger.debug(f"Workaround fehlgeschlagen für {selector}: {e}") if not workaround_success: logger.warning("Workaround konnte nicht ausgeführt werden - versuche trotzdem fortzufahren") # 7. Nach Workaround auf "Weiter" klicken self.automation.human_behavior.random_delay(1.0, 2.0) logger.info("Klicke auf 'Weiter'-Button nach Workaround...") weiter_selectors = [ "button[type='submit']:has-text('Weiter')", "button:has-text('Weiter')", "button:has-text('Continue')", "button:has-text('Next')", "button[type='submit']", self.selectors.CONTINUE_BUTTON, self.selectors.CONTINUE_BUTTON_ALT, "button[data-e2e='next-button']", "button.TUXButton.TUXButton--default.TUXButton--large.TUXButton--primary" ] weiter_clicked = False for selector in weiter_selectors: if self.automation.browser.is_element_visible(selector, timeout=2000): # Prüfe ob Button nicht disabled ist is_disabled = self.automation.browser.page.locator(selector).first.get_attribute("disabled") if not is_disabled: if self.automation.browser.click_element(selector): logger.info(f"'Weiter'-Button erfolgreich geklickt: {selector}") weiter_clicked = True break else: logger.debug(f"Button ist disabled: {selector}") if not weiter_clicked: logger.error("Konnte 'Weiter'-Button nicht klicken nach Passwort-Eingabe") return False return True elif registration_method == "phone": # Telefonnummer-Feld ausfüllen (ohne Ländervorwahl) phone_number = account_data["phone"] if phone_number.startswith("+"): # Entferne Ländervorwahl, wenn vorhanden parts = phone_number.split(" ", 1) if len(parts) > 1: phone_number = parts[1] phone_success = self.automation.ui_helper.fill_field_fuzzy( ["Telefonnummer", "Phone number", "Phone"], phone_number, self.selectors.PHONE_FIELD ) if not phone_success: logger.error("Konnte Telefonnummer-Feld nicht ausfüllen") return False logger.info(f"Telefonnummer-Feld ausgefüllt: {phone_number}") self.automation.human_behavior.random_delay(0.5, 1.5) # Code senden Button klicken - mit disabled-State-Prüfung send_code_success = self._click_send_code_button_with_retry() if not send_code_success: logger.error("Konnte 'Code senden'-Button nicht klicken") return False logger.info("'Code senden'-Button erfolgreich geklickt") return True except Exception as e: logger.error(f"Fehler beim Ausfüllen des Registrierungsformulars: {e}") return False def _handle_verification(self, account_data: Dict[str, Any], registration_method: str) -> bool: """ Behandelt den Verifizierungsprozess (E-Mail/SMS). Args: account_data: Account-Daten mit E-Mail/Telefon registration_method: "email" oder "phone" Returns: bool: True bei Erfolg, False bei Fehler """ try: # Warten, bis der Bestätigungscode gesendet wurde self.automation.human_behavior.wait_for_page_load() self.automation.human_behavior.random_delay(2.0, 4.0) # Verifizierungscode je nach Methode abrufen if registration_method == "email": # Verifizierungscode von E-Mail abrufen verification_code = self._get_email_confirmation_code(account_data["email"]) else: # Verifizierungscode von SMS abrufen verification_code = self._get_sms_confirmation_code(account_data["phone"]) if not verification_code: logger.error("Konnte keinen Verifizierungscode abrufen") return False logger.info(f"Verifizierungscode erhalten: {verification_code}") # Verifizierungscode-Feld ausfüllen code_success = self.automation.ui_helper.fill_field_fuzzy( ["Gib den sechsstelligen Code ein", "Enter verification code", "Verification code"], verification_code, self.selectors.VERIFICATION_CODE_FIELD ) if not code_success: logger.error("Konnte Verifizierungscode-Feld nicht ausfüllen") return False self.automation.human_behavior.random_delay(1.0, 2.0) # Weiter-Button klicken continue_success = self.automation.ui_helper.click_button_fuzzy( ["Weiter", "Continue", "Next", "Submit"], self.selectors.CONTINUE_BUTTON ) if not continue_success: logger.error("Konnte 'Weiter'-Button nicht klicken") return False logger.info("Verifizierungscode eingegeben und 'Weiter' geklickt") # Warten nach der Verifizierung self.automation.human_behavior.wait_for_page_load() self.automation.human_behavior.random_delay(1.0, 2.0) return True except Exception as e: logger.error(f"Fehler bei der Verifizierung: {e}") return False def _get_email_confirmation_code(self, email: str) -> Optional[str]: """ Ruft den Bestätigungscode von einer E-Mail ab. Args: email: E-Mail-Adresse, an die der Code gesendet wurde Returns: Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden """ try: # Warte auf die E-Mail verification_code = self.automation.email_handler.get_verification_code( target_email=email, # Verwende die vollständige E-Mail-Adresse platform="tiktok", max_attempts=60, # 60 Versuche * 2 Sekunden = 120 Sekunden delay_seconds=2 ) if verification_code: return verification_code # Wenn kein Code gefunden wurde, prüfen, ob der Code vielleicht direkt angezeigt wird verification_code = self._extract_code_from_page() if verification_code: logger.info(f"Verifizierungscode direkt von der Seite extrahiert: {verification_code}") return verification_code logger.warning(f"Konnte keinen Verifizierungscode für {email} finden") return None except Exception as e: logger.error(f"Fehler beim Abrufen des E-Mail-Bestätigungscodes: {e}") return None def _get_sms_confirmation_code(self, phone: str) -> Optional[str]: """ Ruft den Bestätigungscode aus einer SMS ab. Hier müsste ein SMS-Empfangs-Service eingebunden werden. Args: phone: Telefonnummer, an die der Code gesendet wurde Returns: Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden """ # Diese Implementierung ist ein Platzhalter # In einer echten Implementierung würde hier ein SMS-Empfangs-Service verwendet logger.warning("SMS-Verifizierung ist noch nicht implementiert") # Versuche, den Code trotzdem zu extrahieren, falls er auf der Seite angezeigt wird return self._extract_code_from_page() def _extract_code_from_page(self) -> Optional[str]: """ Versucht, einen Bestätigungscode direkt von der Seite zu extrahieren. Returns: Optional[str]: Der extrahierte Code oder None, wenn nicht gefunden """ try: # Gesamten Seiteninhalt abrufen page_content = self.automation.browser.page.content() # Mögliche Regex-Muster für Bestätigungscodes patterns = [ r"Dein Code ist (\d{6})", r"Your code is (\d{6})", r"Bestätigungscode: (\d{6})", r"Confirmation code: (\d{6})", r"(\d{6}) ist dein TikTok-Code", r"(\d{6}) is your TikTok code" ] for pattern in patterns: match = re.search(pattern, page_content) if match: return match.group(1) return None except Exception as e: logger.error(f"Fehler beim Extrahieren des Codes von der Seite: {e}") return None def _create_username(self, account_data: Dict[str, Any]) -> bool: """ Erstellt einen Benutzernamen. Args: account_data: Account-Daten Returns: bool: True bei Erfolg, False bei Fehler """ try: # Warten, bis die Benutzernamen-Seite geladen ist self.automation.human_behavior.wait_for_page_load() # Prüfen, ob wir auf der Benutzernamen-Seite sind if not self.automation.browser.is_element_visible(self.selectors.USERNAME_FIELD, timeout=5000): logger.warning("Benutzernamen-Feld nicht gefunden, möglicherweise ist dieser Schritt übersprungen worden") # Versuche, den "Überspringen"-Button zu klicken, falls vorhanden skip_visible = self.automation.browser.is_element_visible(self.selectors.SKIP_USERNAME_BUTTON, timeout=2000) if skip_visible: self.automation.browser.click_element(self.selectors.SKIP_USERNAME_BUTTON) logger.info("Benutzernamen-Schritt übersprungen") return True # Möglicherweise wurde der Benutzername automatisch erstellt logger.info("Benutzernamen-Schritt möglicherweise automatisch abgeschlossen") return True # Benutzernamen eingeben username_success = self.automation.ui_helper.fill_field_fuzzy( ["Benutzername", "Username"], account_data["username"], self.selectors.USERNAME_FIELD ) if not username_success: logger.error("Konnte Benutzernamen-Feld nicht ausfüllen") return False logger.info(f"Benutzernamen-Feld ausgefüllt: {account_data['username']}") self.automation.human_behavior.random_delay(1.0, 2.0) # Registrieren-Button klicken register_success = self.automation.ui_helper.click_button_fuzzy( ["Registrieren", "Register", "Sign up", "Submit"], self.selectors.REGISTER_BUTTON ) if not register_success: logger.error("Konnte 'Registrieren'-Button nicht klicken") return False logger.info("'Registrieren'-Button erfolgreich geklickt") # Warten nach der Registrierung self.automation.human_behavior.wait_for_page_load() return True except Exception as e: logger.error(f"Fehler beim Erstellen des Benutzernamens: {e}") return False def _click_send_code_button_with_retry(self) -> bool: """ Klickt den 'Code senden'-Button mit Prüfung auf disabled-State und Countdown. Returns: bool: True bei Erfolg, False bei Fehler """ try: import re import time max_wait_time = 70 # Maximal 70 Sekunden warten (60s Countdown + Puffer) check_interval = 2 # Alle 2 Sekunden prüfen start_time = time.time() logger.info("Prüfe 'Code senden'-Button Status...") # Liste aller möglichen Selektoren für den Button send_code_selectors = [ self.selectors.SEND_CODE_BUTTON, # Original data-e2e self.selectors.SEND_CODE_BUTTON_ALT, # Neue CSS-Klasse self.selectors.SEND_CODE_BUTTON_ALT2, # Wildcard CSS-Klasse self.selectors.SEND_CODE_BUTTON_TEXT, # Text-basiert "button.css-1jjb4td-ButtonSendCode", # Exakte neue Klasse "button:has-text('Code senden')", # Text-Selektor "button[type='button']:has-text('Code senden')" # Type + Text ] while time.time() - start_time < max_wait_time: # Button-Element mit verschiedenen Selektoren suchen button_element = None used_selector = None for selector in send_code_selectors: try: button_element = self.automation.browser.wait_for_selector( selector, timeout=1000 ) if button_element: used_selector = selector logger.debug(f"Button gefunden mit Selektor: {selector}") break except: continue if not button_element: logger.warning("'Code senden'-Button nicht gefunden mit keinem der Selektoren") time.sleep(check_interval) continue # Disabled-Attribut prüfen is_disabled = button_element.get_attribute("disabled") button_text = button_element.inner_text() or "" logger.debug(f"Button Status: disabled={is_disabled}, text='{button_text}'") # Wenn Button nicht disabled ist, versuche zu klicken if not is_disabled: if "Code senden" in button_text and "erneut" not in button_text: logger.info("Button ist bereit zum Klicken") # Mehrere Klick-Strategien versuchen click_success = False # 1. Direkter Klick auf das gefundene Element try: logger.info(f"Versuche direkten Klick auf Button-Element (Selektor: {used_selector})") button_element.click() click_success = True logger.info(f"Direkter Klick erfolgreich mit Selektor: {used_selector}") except Exception as e: logger.warning(f"Direkter Klick fehlgeschlagen: {e}") # 2. Fallback: Fuzzy-Matching Klick if not click_success: logger.info("Versuche Fuzzy-Matching Klick") click_success = self.automation.ui_helper.click_button_fuzzy( ["Code senden", "Send code", "Send verification code"], self.selectors.SEND_CODE_BUTTON ) if click_success: logger.info("Fuzzy-Matching Klick erfolgreich") # 3. Fallback: React-kompatibler Event-Dispatch if not click_success: try: logger.info("Versuche React-kompatiblen Event-Dispatch") click_success = self._dispatch_react_click_events(button_element) if click_success: logger.info("React-Event-Dispatch erfolgreich") except Exception as e: logger.warning(f"React-Event-Dispatch fehlgeschlagen: {e}") # 4. Fallback: Einfacher JavaScript-Klick if not click_success: try: logger.info("Versuche einfachen JavaScript-Klick") button_element.evaluate("element => element.click()") click_success = True logger.info("JavaScript-Klick erfolgreich") except Exception as e: logger.warning(f"JavaScript-Klick fehlgeschlagen: {e}") # 5. Klick-Erfolg validieren if click_success: # Umfassende Erfolgsvalidierung validation_success = self._validate_send_code_success() if validation_success: logger.info("'Code senden'-Button erfolgreich geklickt (validiert)") return True else: logger.error("Klick scheinbar erfolglos - keine Reaktion erkannt") click_success = False if not click_success: logger.warning("Alle Klick-Strategien fehlgeschlagen, versuche erneut...") else: logger.debug(f"Button-Text nicht bereit: '{button_text}'") # Wenn Button disabled ist, Countdown extrahieren elif "erneut senden" in button_text.lower(): countdown_match = re.search(r'(\d+)s', button_text) if countdown_match: countdown = int(countdown_match.group(1)) logger.info(f"Button ist disabled, warte {countdown} Sekunden...") # Effizienter warten - nicht länger als nötig if countdown > 5: time.sleep(countdown - 3) # 3 Sekunden vor Ende wieder prüfen else: time.sleep(check_interval) else: logger.info("Button ist disabled, warte...") time.sleep(check_interval) else: logger.info("Button ist disabled ohne Countdown-Info, warte...") time.sleep(check_interval) logger.error(f"Timeout nach {max_wait_time} Sekunden - Button konnte nicht geklickt werden") return False except Exception as e: logger.error(f"Fehler beim Klicken des 'Code senden'-Buttons: {e}") return False def _dispatch_react_click_events(self, element) -> bool: """ Dispatcht React-kompatible Events für moderne Web-Interfaces. Args: element: Das Button-Element auf das geklickt werden soll Returns: bool: True bei Erfolg, False bei Fehler """ try: # Erweiterte JavaScript-Funktion für TikTok React-Interface react_click_script = """ (element) => { console.log('Starting React click dispatch for TikTok button'); // 1. Element-Informationen sammeln console.log('Button element:', element); console.log('Button tagName:', element.tagName); console.log('Button type:', element.type); console.log('Button disabled:', element.disabled); console.log('Button innerHTML:', element.innerHTML); console.log('Button classList:', Array.from(element.classList)); // 2. React Fiber-Node finden (TikTok verwendet React Fiber) let reactFiber = null; const fiberKeys = Object.keys(element).filter(key => key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance') || key.startsWith('__reactEventHandlers') ); console.log('Found fiber keys:', fiberKeys); if (fiberKeys.length > 0) { reactFiber = element[fiberKeys[0]]; console.log('React fiber found:', reactFiber); } // 3. Alle Event Listener finden const listeners = getEventListeners ? getEventListeners(element) : {}; console.log('Event listeners:', listeners); // 4. React Event Handler suchen let clickHandler = null; if (reactFiber) { // Fiber-Baum durchsuchen let currentFiber = reactFiber; while (currentFiber && !clickHandler) { if (currentFiber.memoizedProps && currentFiber.memoizedProps.onClick) { clickHandler = currentFiber.memoizedProps.onClick; console.log('Found onClick handler in fiber props'); break; } if (currentFiber.pendingProps && currentFiber.pendingProps.onClick) { clickHandler = currentFiber.pendingProps.onClick; console.log('Found onClick handler in pending props'); break; } currentFiber = currentFiber.return || currentFiber.child; } } // 5. Backup: Element-Properties durchsuchen if (!clickHandler) { const propKeys = Object.getOwnPropertyNames(element); for (const key of propKeys) { if (key.includes('click') || key.includes('Click')) { const prop = element[key]; if (typeof prop === 'function') { clickHandler = prop; console.log('Found click handler in element properties:', key); break; } } } } try { // 6. React Synthetic Event erstellen const syntheticEvent = { type: 'click', target: element, currentTarget: element, bubbles: true, cancelable: true, preventDefault: function() { this.defaultPrevented = true; }, stopPropagation: function() { this.propagationStopped = true; }, nativeEvent: new MouseEvent('click', { bubbles: true, cancelable: true }), timeStamp: Date.now(), isTrusted: false }; // 7. Handler direkt aufrufen, falls gefunden if (clickHandler) { console.log('Calling React click handler directly'); clickHandler(syntheticEvent); return true; } // 8. Fallback: Umfassende Event-Sequenz console.log('Using fallback event sequence'); // Element fokussieren element.focus(); // Realistische Koordinaten berechnen const rect = element.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const eventOptions = { view: window, bubbles: true, cancelable: true, clientX: centerX, clientY: centerY, button: 0, buttons: 1, detail: 1 }; // Vollständige Event-Sequenz const events = [ new MouseEvent('mouseenter', eventOptions), new MouseEvent('mouseover', eventOptions), new MouseEvent('mousedown', eventOptions), new FocusEvent('focus', { bubbles: true }), new MouseEvent('mouseup', eventOptions), new MouseEvent('click', eventOptions), new Event('input', { bubbles: true }), new Event('change', { bubbles: true }) ]; for (const event of events) { element.dispatchEvent(event); } // 9. Form-Submit als letzter Ausweg const form = element.closest('form'); if (form && element.type === 'submit') { console.log('Triggering form submit as last resort'); form.dispatchEvent(new Event('submit', { bubbles: true })); } return true; } catch (error) { console.error('React click dispatch failed:', error); return false; } } """ # Event-Dispatch ausführen result = element.evaluate(react_click_script) if result: logger.info("Erweiterte React-Events erfolgreich dispatcht") return True else: logger.warning("React-Event-Dispatch meldet Fehler") return False except Exception as e: logger.error(f"Fehler beim React-Event-Dispatch: {e}") return False def _wait_for_password_validation(self) -> None: """ Wartet, bis TikToks Passwort-Validierung abgeschlossen ist und UI stabil wird. Das verhindert Interferenzen mit dem 'Code senden'-Button. """ try: import time # Häufige Passwort-Validation-Indikatoren bei TikTok validation_indicators = [ # Deutsche Texte "8-20 zeichen", "sonderzeichen", "buchstaben und zahlen", "mindestens 8 zeichen", "großbuchstaben", "kleinbuchstaben", # Englische Texte "8-20 characters", "special characters", "letters and numbers", "at least 8 characters", "uppercase", "lowercase", "password requirements", "password strength" ] # CSS-Selektoren für Validierungsmeldungen validation_selectors = [ "div[class*='password']", "div[class*='validation']", "div[class*='requirement']", "div[class*='error']", "div[class*='hint']", ".password-hint", ".validation-message", "[data-e2e*='password']" ] logger.info("Prüfe auf Passwort-Validierungsmeldungen...") max_wait_time = 8 # Maximal 8 Sekunden warten check_interval = 0.5 # Alle 500ms prüfen start_time = time.time() validation_found = False validation_disappeared = False while time.time() - start_time < max_wait_time: # 1. Prüfung: Sind Validierungsmeldungen sichtbar? current_validation = False # Text-basierte Suche try: page_content = self.automation.browser.page.content().lower() for indicator in validation_indicators: if indicator in page_content: current_validation = True validation_found = True logger.debug(f"Passwort-Validierung aktiv: '{indicator}'") break except: pass # Element-basierte Suche if not current_validation: for selector in validation_selectors: try: if self.automation.browser.is_element_visible(selector, timeout=500): element = self.automation.browser.wait_for_selector(selector, timeout=500) if element: element_text = element.inner_text() or "" if any(indicator in element_text.lower() for indicator in validation_indicators): current_validation = True validation_found = True logger.debug(f"Passwort-Validierung in Element: '{element_text[:50]}'") break except: continue # 2. Zustandsüberwachung if validation_found and not current_validation: # Validierung war da, ist jetzt weg validation_disappeared = True logger.info("Passwort-Validierung verschwunden - UI sollte stabil sein") break elif current_validation: logger.debug("Passwort-Validierung noch aktiv, warte...") time.sleep(check_interval) # Zusätzliche Stabilisierungszeit nach Validierung if validation_found: if validation_disappeared: logger.info("Extra-Wartezeit für UI-Stabilisierung nach Passwort-Validierung") time.sleep(2) # 2 Sekunden extra für Stabilität else: logger.warning("Passwort-Validierung immer noch aktiv - fahre trotzdem fort") time.sleep(1) # Kurze Wartezeit else: logger.debug("Keine Passwort-Validierungsmeldungen erkannt") time.sleep(1) # Standard-Wartezeit für UI-Stabilität except Exception as e: logger.warning(f"Fehler bei Passwort-Validierung-Überwachung: {e}") # Fallback: Einfach 2 Sekunden warten time.sleep(2) def _handle_verification_code_entry(self, account_data: dict) -> bool: """ Wartet auf E-Mail mit Verification Code und gibt ihn ein. Optimiert für störungsfreie Eingabe vor Passwort-Validierung. Args: account_data: Account-Daten mit E-Mail-Adresse Returns: bool: True bei Erfolg, False bei Fehler """ try: import time email_address = account_data.get("email") if not email_address: logger.error("Keine E-Mail-Adresse für Code-Abruf verfügbar") return False logger.info(f"Warte auf Verifizierungscode für {email_address}") # Warten auf das Erscheinen des Verification-Feldes logger.info("Warte auf Verifizierungsfeld...") verification_field_appeared = False max_field_wait = 10 # 10 Sekunden warten auf Feld for attempt in range(max_field_wait): verification_selectors = [ # Exakter Selektor basierend auf echtem TikTok HTML "input[type='text'][placeholder='Gib den sechsstelligen Code ein']", "input.css-11to27l-InputContainer", "input.etcs7ny1", # Fallback-Selektoren "input[placeholder*='sechsstelligen Code']", "input[placeholder*='Code']", "input[placeholder*='code']", "input[data-e2e='verification-code-input']", "input[name*='verif']", "input[name*='code']" ] for selector in verification_selectors: if self.automation.browser.is_element_visible(selector, timeout=1000): logger.info(f"Verifizierungsfeld erschienen: {selector}") verification_field_appeared = True break if verification_field_appeared: break time.sleep(1) if not verification_field_appeared: logger.error("Verifizierungsfeld ist nicht erschienen nach Code senden") return False # E-Mail-Handler: Warte auf Verifizierungscode logger.info("Rufe E-Mail ab und extrahiere Verifizierungscode...") verification_code = None max_email_attempts = 12 # 12 Versuche über 2 Minuten for attempt in range(max_email_attempts): logger.debug(f"E-Mail-Abruf Versuch {attempt + 1}/{max_email_attempts}") try: verification_code = self.automation.email_handler.get_verification_code( target_email=email_address, platform="tiktok", max_attempts=3, # Kurze Versuche pro E-Mail-Abruf delay_seconds=2 ) if verification_code: logger.info(f"Verifizierungscode erhalten: {verification_code}") break except Exception as e: logger.warning(f"Fehler beim E-Mail-Abruf (Versuch {attempt + 1}): {e}") # Kurz warten zwischen Versuchen time.sleep(10) # 10 Sekunden zwischen E-Mail-Abruf-Versuchen if not verification_code: logger.error("Kein Verifizierungscode verfügbar") return False # Code in das Feld eingeben (verschiedene Strategien) logger.info("Gebe Verifizierungscode ein...") code_entered = False # 1. Debug: Alle Input-Felder auf der Seite finden try: all_inputs = self.automation.browser.page.query_selector_all("input") logger.info(f"Debug: Gefundene Input-Felder auf der Seite: {len(all_inputs)}") for i, input_elem in enumerate(all_inputs): placeholder = input_elem.get_attribute("placeholder") or "" input_type = input_elem.get_attribute("type") or "" classes = input_elem.get_attribute("class") or "" logger.debug(f"Input {i+1}: type='{input_type}', placeholder='{placeholder}', class='{classes[:50]}...'") except Exception as e: logger.debug(f"Debug-Info fehlgeschlagen: {e}") # 2. Direkte Eingabe über Selektoren mit React-kompatiblem Input for i, selector in enumerate(verification_selectors): logger.debug(f"Teste Selektor {i+1}/{len(verification_selectors)}: {selector}") try: if self.automation.browser.is_element_visible(selector, timeout=2000): logger.info(f"✅ Selektor gefunden: {selector}") # React-kompatible Input-Eingabe OHNE Workaround für Code success = self._fill_input_field_react_compatible(selector, verification_code, use_workaround=False) if success: logger.info(f"Code erfolgreich eingegeben über: {selector}") code_entered = True break else: logger.warning(f"Code-Eingabe fehlgeschlagen für: {selector}") else: logger.debug(f"❌ Selektor nicht gefunden: {selector}") except Exception as e: logger.debug(f"❌ Selektor-Fehler {selector}: {e}") # 2. Fallback: Fuzzy Matching if not code_entered: code_entered = self.automation.ui_helper.fill_field_fuzzy( ["Code", "Bestätigungscode", "Verification", "Verifikation"], verification_code ) if code_entered: logger.info("Code erfolgreich eingegeben über Fuzzy-Matching") if not code_entered: logger.error("Konnte Verifizierungscode nicht eingeben") return False # Kurz warten nach Code-Eingabe self.automation.human_behavior.random_delay(1.0, 2.0) # Optional: "Weiter" oder "Submit" Button klicken (falls nötig) self._try_submit_verification_code() logger.info("Verifizierungscode-Eingabe abgeschlossen") return True except Exception as e: logger.error(f"Fehler bei Verifizierungscode-Behandlung: {e}") return False def _debug_form_state_after_password(self) -> None: """ Debug-Funktion: Analysiert den Formular-Zustand nach Passwort-Eingabe um herauszufinden, warum der Weiter-Button disabled ist. """ try: logger.info("=== DEBUG: Formular-Zustand nach Passwort-Eingabe ===") # 1. Prüfe alle Weiter-Buttons und deren Status weiter_selectors = [ "button:has-text('Weiter')", "button:has-text('Continue')", "button:has-text('Next')", "button[type='submit']", "button.TUXButton" ] for selector in weiter_selectors: try: if self.automation.browser.is_element_visible(selector, timeout=1000): element = self.automation.browser.page.locator(selector).first is_disabled = element.is_disabled() text = element.text_content() logger.info(f"Button gefunden: '{text}' - Disabled: {is_disabled} - Selektor: {selector}") except Exception as e: logger.debug(f"Button-Check fehlgeschlagen für {selector}: {e}") # 2. Prüfe auf Terms & Conditions Checkbox checkbox_selectors = [ "input[type='checkbox']", "input.css-1pewyex-InputCheckbox", "label:has-text('Nutzungsbedingungen')", "label:has-text('Terms')", "label:has-text('Ich stimme')", "label:has-text('I agree')" ] logger.info("Prüfe auf ungesetzte Checkboxen...") for selector in checkbox_selectors: try: if self.automation.browser.is_element_visible(selector, timeout=1000): element = self.automation.browser.page.locator(selector).first if selector.startswith("input"): is_checked = element.is_checked() logger.info(f"Checkbox gefunden - Checked: {is_checked} - Selektor: {selector}") if not is_checked: logger.warning(f"UNCHECKED CHECKBOX GEFUNDEN: {selector}") else: text = element.text_content() logger.info(f"Checkbox-Label gefunden: '{text}' - Selektor: {selector}") except Exception as e: logger.debug(f"Checkbox-Check fehlgeschlagen für {selector}: {e}") # 3. Prüfe auf Passwort-Validierungsfehler error_selectors = [ ".error-message", ".form-error", ".css-error", "div[class*='error']", "span[class*='error']", "div[style*='color: red']", "span[style*='color: red']" ] logger.info("Prüfe auf Passwort-Validierungsfehler...") for selector in error_selectors: try: if self.automation.browser.is_element_visible(selector, timeout=1000): element = self.automation.browser.page.locator(selector).first text = element.text_content() if text and len(text.strip()) > 0: logger.warning(f"VALIDIERUNGSFEHLER GEFUNDEN: '{text}' - Selektor: {selector}") except Exception as e: logger.debug(f"Error-Check fehlgeschlagen für {selector}: {e}") # 4. Prüfe alle Input-Felder und deren Werte logger.info("Prüfe alle Input-Felder...") try: inputs = self.automation.browser.page.locator("input").all() for i, input_element in enumerate(inputs): input_type = input_element.get_attribute("type") or "text" placeholder = input_element.get_attribute("placeholder") or "" value = input_element.input_value() if input_type != "checkbox" else str(input_element.is_checked()) name = input_element.get_attribute("name") or "" logger.info(f"Input {i+1}: type='{input_type}', placeholder='{placeholder}', value='{value}', name='{name}'") # Warne bei leeren required Feldern if input_type in ["text", "email", "password"] and not value and placeholder: logger.warning(f"LEERES FELD GEFUNDEN: {placeholder}") except Exception as e: logger.debug(f"Input-Field-Check fehlgeschlagen: {e}") logger.info("=== DEBUG: Formular-Zustand Ende ===") except Exception as e: logger.error(f"Debug-Funktion fehlgeschlagen: {e}") def _fill_password_field_character_by_character(self, selector: str, password: str) -> bool: """ Füllt das Passwort-Feld Zeichen für Zeichen aus, um React's State korrekt zu aktualisieren. Args: selector: CSS-Selektor für das Passwort-Feld password: Das einzugebende Passwort Returns: bool: True bei Erfolg, False bei Fehler """ try: element = self.automation.browser.page.locator(selector).first if not element.is_visible(): return False logger.info("Verwende Character-by-Character Eingabe für Passwort-Feld") # Fokussiere und lösche das Feld element.click() self.automation.human_behavior.random_delay(0.1, 0.2) # Lösche existierenden Inhalt element.select_text() element.press("Delete") self.automation.human_behavior.random_delay(0.1, 0.2) # Tippe jeden Buchstaben einzeln for i, char in enumerate(password): element.type(char, delay=random.randint(50, 150)) # Zufällige Tippgeschwindigkeit # Nach jedem 3. Zeichen eine kleine Pause (simuliert echtes Tippen) if (i + 1) % 3 == 0: self.automation.human_behavior.random_delay(0.1, 0.3) # Fokus verlassen, um Validierung zu triggern self.automation.human_behavior.random_delay(0.2, 0.4) element.press("Tab") logger.info(f"Passwort character-by-character eingegeben: {len(password)} Zeichen") return True except Exception as e: logger.error(f"Fehler bei Character-by-Character Passwort-Eingabe: {e}") return False def _get_input_field_value(self, selector: str) -> str: """ Liest den aktuellen Wert eines Input-Feldes aus. Args: selector: CSS-Selektor für das Input-Feld Returns: str: Der aktuelle Wert des Feldes oder leerer String bei Fehler """ try: element = self.automation.browser.page.locator(selector).first if element.is_visible(): return element.input_value() return "" except Exception as e: logger.debug(f"Fehler beim Lesen des Input-Werts für {selector}: {e}") return "" def _try_submit_verification_code(self) -> bool: """ Versucht, den Verifizierungscode zu bestätigen/submitten falls nötig. Returns: bool: True wenn Submit gefunden und geklickt, False wenn nicht nötig """ try: submit_selectors = [ "button[type='submit']", "button:has-text('Weiter')", "button:has-text('Continue')", "button:has-text('Bestätigen')", "button:has-text('Verify')", "button[data-e2e='next-button']" ] for selector in submit_selectors: if self.automation.browser.is_element_visible(selector, timeout=2000): if self.automation.browser.click_element(selector): logger.info(f"Verification Submit-Button geklickt: {selector}") return True logger.debug("Kein Submit-Button für Verification gefunden - wahrscheinlich nicht nötig") return False except Exception as e: logger.debug(f"Fehler beim Submit-Versuch: {e}") return False def _fill_input_field_react_compatible(self, selector: str, value: str, use_workaround: bool = False) -> bool: """ Füllt ein Input-Feld mit React-kompatiblen Events. Speziell für moderne TikTok-Interface optimiert. Args: selector: CSS-Selektor für das Input-Feld value: Wert der eingegeben werden soll use_workaround: True wenn der "0 hinzufügen/löschen" Workaround verwendet werden soll Returns: bool: True bei Erfolg, False bei Fehler """ try: # Element finden element = self.automation.browser.wait_for_selector(selector, timeout=2000) if not element: return False logger.debug(f"Verwende React-kompatible Input-Eingabe für: {selector}") if use_workaround: logger.info("Verwende speziellen Workaround (0 hinzufügen/löschen)") # React-kompatible Input-Eingabe mit JavaScript # SICHERE Übergabe des Wertes als Parameter (nicht String-Interpolation) react_input_script = """ (element, params) => { const value = params.value; const useWorkaround = params.useWorkaround; const isPassword = params.isPassword || element.type === 'password'; console.log('React input field injection for TikTok'); console.log('Element:', element); console.log('Value to input:', value); console.log('Use workaround:', useWorkaround); console.log('Is password field:', isPassword); try { // 1. Element fokussieren element.focus(); element.click(); // Zusätzlicher Click für React // Warte kurz nach Focus const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // 2. Aktuellen Wert löschen mit Select All + Delete element.select(); const deleteEvent = new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', keyCode: 46, bubbles: true }); element.dispatchEvent(deleteEvent); element.value = ''; element.dispatchEvent(new Event('input', { bubbles: true })); // 3. Für Passwort-Felder: Character-by-Character Input if (isPassword) { console.log('Using character-by-character input for password field'); // Simuliere echtes Tippen let currentValue = ''; for (let i = 0; i < value.length; i++) { const char = value[i]; currentValue += char; // Keydown Event const keydownEvent = new KeyboardEvent('keydown', { key: char, code: char.match(/[0-9]/) ? 'Digit' + char : 'Key' + char.toUpperCase(), keyCode: char.charCodeAt(0), charCode: char.charCodeAt(0), which: char.charCodeAt(0), bubbles: true, cancelable: true }); element.dispatchEvent(keydownEvent); // Setze den Wert schrittweise element.value = currentValue; // Input Event nach jedem Zeichen const inputEvent = new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: char }); element.dispatchEvent(inputEvent); // Keyup Event const keyupEvent = new KeyboardEvent('keyup', { key: char, code: char.match(/[0-9]/) ? 'Digit' + char : 'Key' + char.toUpperCase(), keyCode: char.charCodeAt(0), charCode: char.charCodeAt(0), which: char.charCodeAt(0), bubbles: true, cancelable: true }); element.dispatchEvent(keyupEvent); // Kleine Verzögerung zwischen Zeichen (simuliert Tippgeschwindigkeit) if (i < value.length - 1) { // Wir können hier kein await verwenden, also machen wir es synchron } } // Am Ende nochmal focus/blur für Validierung element.focus(); const changeEvent = new Event('change', { bubbles: true }); element.dispatchEvent(changeEvent); } else { // Für andere Felder: normale Eingabe element.value = value; // Events für React const inputEvent = new Event('input', { bubbles: true, cancelable: true }); const changeEvent = new Event('change', { bubbles: true, cancelable: true }); element.dispatchEvent(inputEvent); element.dispatchEvent(changeEvent); } // 8. SPEZIELLER WORKAROUND (0 HINZUFÜGEN/LÖSCHEN) if (useWorkaround) { console.log('Applying 0 add/remove workaround...'); // Warte kurz setTimeout(() => { // Füge eine 0 hinzu element.value = value + '0'; element.dispatchEvent(new Event('input', { bubbles: true })); // Warte kurz setTimeout(() => { // Lösche die 0 wieder element.value = value; element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); console.log('0 add/remove workaround completed'); }, 100); }, 100); } // 9. Finaler Input-Event element.dispatchEvent(new Event('input', { bubbles: true })); console.log('React input injection completed'); console.log('Final element value:', element.value); return element.value === value; } catch (error) { console.error('React input injection failed:', error); return false; } } """ # Script ausführen mit Wert als Parameter (als Dictionary für Playwright) # Erkenne ob es ein Passwort-Feld ist is_password = 'password' in selector.lower() or element.get_attribute('type') == 'password' result = element.evaluate(react_input_script, {'value': value, 'useWorkaround': use_workaround, 'isPassword': is_password}) if result: # Kurze Pause nach Input import time time.sleep(0.5) # Bei Workaround länger warten if use_workaround: time.sleep(0.5) # Validierung: Prüfen ob Wert wirklich gesetzt wurde current_value = element.input_value() if current_value == value: logger.info(f"React-Input erfolgreich: '{current_value}'") return True else: logger.warning(f"React-Input unvollständig: '{current_value}' != '{value}'") return False else: logger.warning("React-Input-Script meldet Fehler") return False except Exception as e: logger.error(f"Fehler bei React-kompatible Input-Eingabe: {e}") return False def _validate_send_code_success(self) -> bool: """ Umfassende Validierung, ob der 'Code senden'-Button erfolgreich geklickt wurde. Returns: bool: True wenn erfolgreich, False sonst """ try: import time logger.info("Führe umfassende Erfolgsvalidierung durch...") # Zuerst kurz warten time.sleep(1) # Vor-Validierung: Button-Status vor der Hauptprüfung original_button = self.automation.browser.wait_for_selector( self.selectors.SEND_CODE_BUTTON, timeout=2000 ) if original_button: pre_text = original_button.inner_text() or "" pre_disabled = original_button.get_attribute("disabled") logger.debug(f"Pre-validation - Button Text: '{pre_text}', Disabled: {pre_disabled}") # Hauptwartung für Reaktion time.sleep(3) # 1. STRENGE Prüfung: Verifizierungsfeld erschienen UND ist editierbar verification_field_selectors = [ "input[placeholder*='sechsstelligen Code']", "input[placeholder*='Code']", "input[placeholder*='code']", "input[data-e2e='verification-code-input']", "input[name*='verif']", "input[name*='code']" ] verification_field_found = False for selector in verification_field_selectors: if self.automation.browser.is_element_visible(selector, timeout=2000): # Zusätzliche Prüfung: Ist das Feld auch wirklich interaktiv? field_element = self.automation.browser.wait_for_selector(selector, timeout=1000) if field_element: is_disabled = field_element.get_attribute("disabled") is_readonly = field_element.get_attribute("readonly") if not is_disabled and not is_readonly: logger.info(f"VALIDES Verifizierungsfeld erschienen: {selector}") verification_field_found = True break else: logger.debug(f"Feld gefunden aber nicht editierbar: {selector}") if verification_field_found: return True # 2. STRENGE Prüfung: Button-Text MUSS sich geändert haben try: updated_element = self.automation.browser.wait_for_selector( self.selectors.SEND_CODE_BUTTON, timeout=2000 ) if updated_element: updated_text = updated_element.inner_text() or "" logger.debug(f"Aktueller Button-Text: '{updated_text}'") # Text MUSS sich geändert haben von "Code senden" if updated_text != "Code senden": # Countdown-Indikatoren (sehr spezifisch) countdown_indicators = [ "erneut senden", "code erneut senden", "wieder senden", "resend", "send again", ":" ] # Prüfung auf Countdown-Format (z.B. "55s", "1:23") import re if re.search(r'\d+s|\d+:\d+|\d+\s*sec', updated_text.lower()): logger.info(f"Button zeigt COUNTDOWN: '{updated_text}' - ECHTER Klick bestätigt") return True for indicator in countdown_indicators: if indicator in updated_text.lower(): logger.info(f"Button zeigt ERNEUT-Status: '{updated_text}' - ECHTER Klick bestätigt") return True else: logger.warning(f"Button-Text unverändert: '{updated_text}' - Klick war NICHT erfolgreich") except Exception as e: logger.debug(f"Button-Text-Prüfung fehlgeschlagen: {e}") # 3. Prüfung: Disabled-Status des Buttons try: button_element = self.automation.browser.wait_for_selector( self.selectors.SEND_CODE_BUTTON, timeout=2000 ) if button_element: is_disabled = button_element.get_attribute("disabled") if is_disabled: logger.info("Button ist jetzt disabled - Code wurde gesendet") return True except Exception as e: logger.debug(f"Button-Disabled-Prüfung fehlgeschlagen: {e}") # 4. Prüfung: Neue Textinhalte auf der Seite try: page_content = self.automation.browser.page.content().lower() success_indicators = [ "code gesendet", "code sent", "verification sent", "email gesendet", "email sent", "check your email", "prüfe deine", "überprüfe deine" ] for indicator in success_indicators: if indicator in page_content: logger.info(f"Erfolgsindikator im Seiteninhalt gefunden: '{indicator}'") return True except Exception as e: logger.debug(f"Seiteninhalt-Prüfung fehlgeschlagen: {e}") # 5. Prüfung: Neue Elemente oder Dialoge try: new_element_selectors = [ "div[role='alert']", "div[class*='notification']", "div[class*='message']", "div[class*='success']", ".toast", ".alert", ".notification" ] for selector in new_element_selectors: if self.automation.browser.is_element_visible(selector, timeout=1000): element = self.automation.browser.wait_for_selector(selector, timeout=1000) if element: element_text = element.inner_text() or "" if any(word in element_text.lower() for word in ["code", "sent", "gesendet", "email"]): logger.info(f"Erfolgs-Element gefunden: '{element_text}'") return True except Exception as e: logger.debug(f"Neue-Elemente-Prüfung fehlgeschlagen: {e}") # 6. Screenshot für Debugging erstellen self.automation._take_screenshot("validation_failed") # 7. Finale Button-Status-Ausgabe try: final_button = self.automation.browser.wait_for_selector( self.selectors.SEND_CODE_BUTTON, timeout=1000 ) if final_button: final_text = final_button.inner_text() or "" final_disabled = final_button.get_attribute("disabled") logger.error(f"VALIDATION FAILED - Finaler Button-Status: Text='{final_text}', Disabled={final_disabled}") except: pass logger.error("VALIDATION FAILED: 'Code senden'-Button wurde NICHT erfolgreich geklickt") return False except Exception as e: logger.error(f"Fehler bei der Erfolgsvalidierung: {e}") return False def _check_registration_success(self) -> bool: """ Überprüft, ob die Registrierung erfolgreich war. Returns: bool: True wenn erfolgreich, False sonst """ try: # Warten nach der Registrierung self.automation.human_behavior.wait_for_page_load(multiplier=2.0) # Screenshot erstellen self.automation._take_screenshot("registration_final") # Erfolg anhand verschiedener Indikatoren prüfen success_indicators = self.selectors.SUCCESS_INDICATORS for indicator in success_indicators: if self.automation.browser.is_element_visible(indicator, timeout=3000): logger.info(f"Erfolgsindikator gefunden: {indicator}") return True # Alternativ prüfen, ob wir auf der TikTok-Startseite sind current_url = self.automation.browser.page.url if "tiktok.com" in current_url and "/signup" not in current_url and "/login" not in current_url: logger.info(f"Erfolg basierend auf URL: {current_url}") return True logger.warning("Keine Erfolgsindikatoren gefunden") return False except Exception as e: logger.error(f"Fehler beim Überprüfen des Registrierungserfolgs: {e}") return False