Files
AccountForger-neuerUpload/social_networks/tiktok/tiktok_registration.py
Claude Project Manager ea8fddce97 TikTok läuft wieder
2025-08-12 13:41:21 +02:00

2477 Zeilen
114 KiB
Python

# 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()
# 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_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