2615 Zeilen
120 KiB
Python
2615 Zeilen
120 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()
|
|
|
|
# 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
|
|
# WICHTIG: TikTok ändert CSS-Klassen dynamisch, daher viele Fallbacks
|
|
month_selectors = [
|
|
# Neue TikTok CSS-Klassen (2025)
|
|
"div[class*='DivSelectLabel']:has-text('Monat')", # Partial class match
|
|
"div.eoyer411:has-text('Monat')", # Neue spezifische Klasse
|
|
|
|
# Generische Selektoren (stabiler)
|
|
"div:has-text('Monat'):has(svg)", # Dropdown mit SVG-Pfeil
|
|
"div:text-is('Monat')", # Exakter Text
|
|
"div:has-text('Monat')", # Text-basiert
|
|
|
|
# Alte Selektoren (Fallback)
|
|
"div.css-1fi2hzv-DivSelectLabel:has-text('Monat')", # Alt TikTok-Klasse
|
|
"div.e1phcp2x1:has-text('Monat')", # Alt TikTok-Klasse
|
|
|
|
# Alternative Ansätze
|
|
self.selectors.BIRTHDAY_MONTH_DROPDOWN, # select[name='month']
|
|
"div[data-e2e='date-picker-month']", # TikTok-spezifisch
|
|
"div:has-text('Month')", # Englisch
|
|
"[aria-label*='Monat']", # Aria-Label
|
|
"div[role='combobox']:has-text('Monat')" # Combobox
|
|
]
|
|
|
|
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 = [
|
|
# Neue TikTok Selektoren (2025) mit ID-Pattern
|
|
f"#Month-options-item-{birthday['month']-1}", # ID-basiert (0-indexed)
|
|
f"div#Month-options-item-{birthday['month']-1}", # Mit div prefix
|
|
|
|
# Neue CSS-Klassen
|
|
f"div[class*='DivOption']:has-text('{month_name}')", # Partial class match
|
|
f"div.eoyer415:has-text('{month_name}')", # Neue spezifische Klasse
|
|
|
|
# Role-basierte Selektoren (stabiler)
|
|
f"[role='option']:has-text('{month_name}')", # Role + Monatsname
|
|
f"div[role='option']:has-text('{month_name}')", # Div mit Role
|
|
|
|
# Text-basierte Selektoren
|
|
f"div:text-is('{month_name}')", # Exakter Text
|
|
f"div:has-text('{month_name}')", # Einfach Monatsname
|
|
|
|
# Alte Selektoren (Fallback)
|
|
f"div.css-vz5m7n-DivOption:has-text('{month_name}')", # Alt TikTok-Klasse
|
|
f"div.e1phcp2x5:has-text('{month_name}')", # Alt TikTok-Klasse
|
|
|
|
# Alternative Fallbacks
|
|
f"option[value='{birthday['month']}']", # Standard HTML
|
|
f"div[data-value='{birthday['month']}']" # Custom Dropdown
|
|
]
|
|
|
|
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 = [
|
|
# Neue TikTok CSS-Klassen (2025)
|
|
"div[class*='DivSelectLabel']:has-text('Tag')", # Partial class match
|
|
"div.eoyer411:has-text('Tag')", # Neue spezifische Klasse
|
|
|
|
# Generische Selektoren (stabiler)
|
|
"div:has-text('Tag'):has(svg)", # Dropdown mit SVG-Pfeil
|
|
"div:text-is('Tag')", # Exakter Text
|
|
"div:has-text('Tag')", # Text-basiert
|
|
|
|
# Alte Selektoren (Fallback)
|
|
"div.css-1fi2hzv-DivSelectLabel:has-text('Tag')", # Alt TikTok-Klasse
|
|
"div.e1phcp2x1:has-text('Tag')", # Alt TikTok-Klasse
|
|
|
|
# Alternative Ansätze
|
|
self.selectors.BIRTHDAY_DAY_DROPDOWN, # select[name='day']
|
|
"div[data-e2e='date-picker-day']", # TikTok-spezifisch
|
|
"div:has-text('Day')", # Englisch
|
|
"[aria-label*='Tag']", # Aria-Label
|
|
"div[role='combobox']:has-text('Tag')" # Combobox
|
|
]
|
|
|
|
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 = [
|
|
# Neue TikTok Selektoren (2025) mit ID-Pattern
|
|
f"#Day-options-item-{birthday['day']-1}", # ID-basiert (0-indexed)
|
|
f"div#Day-options-item-{birthday['day']-1}", # Mit div prefix
|
|
|
|
# Neue CSS-Klassen
|
|
f"div[class*='DivOption']:has-text('{birthday['day']}')", # Partial class match
|
|
f"div.eoyer415:has-text('{birthday['day']}')", # Neue spezifische Klasse
|
|
|
|
# Role-basierte Selektoren (stabiler)
|
|
f"[role='option']:has-text('{birthday['day']}')", # Role + Tag
|
|
f"div[role='option']:has-text('{birthday['day']}')", # Div mit Role
|
|
|
|
# Text-basierte Selektoren
|
|
f"div:text-is('{birthday['day']}')", # Exakter Text
|
|
f"div:has-text('{birthday['day']}')", # Einfach Tag
|
|
|
|
# Alte Selektoren (Fallback)
|
|
f"div.css-vz5m7n-DivOption:has-text('{birthday['day']}')", # Alt TikTok-Klasse
|
|
f"div.e1phcp2x5:has-text('{birthday['day']}')" # Alt TikTok-Klasse
|
|
]
|
|
|
|
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 = [
|
|
# Neue TikTok CSS-Klassen (2025)
|
|
"div[class*='DivSelectLabel']:has-text('Jahr')", # Partial class match
|
|
"div.eoyer411:has-text('Jahr')", # Neue spezifische Klasse
|
|
|
|
# Generische Selektoren (stabiler)
|
|
"div:has-text('Jahr'):has(svg)", # Dropdown mit SVG-Pfeil
|
|
"div:text-is('Jahr')", # Exakter Text
|
|
"div:has-text('Jahr')", # Text-basiert
|
|
|
|
# Alte Selektoren (Fallback)
|
|
"div.css-1fi2hzv-DivSelectLabel:has-text('Jahr')", # Alt TikTok-Klasse
|
|
"div.e1phcp2x1:has-text('Jahr')", # Alt TikTok-Klasse
|
|
|
|
# Alternative Ansätze
|
|
self.selectors.BIRTHDAY_YEAR_DROPDOWN, # select[name='year']
|
|
"div[data-e2e='date-picker-year']", # TikTok-spezifisch
|
|
"div:has-text('Year')", # Englisch
|
|
"[aria-label*='Jahr']", # Aria-Label
|
|
"div[role='combobox']:has-text('Jahr')" # Combobox
|
|
]
|
|
|
|
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
|
|
# Berechne den Index für das Jahr (normalerweise absteigend sortiert)
|
|
# Annahme: Jahre von aktuellem Jahr bis 1900, also Index = aktuelles_jahr - gewähltes_jahr
|
|
current_year = 2025 # oder datetime.now().year
|
|
year_index = current_year - birthday['year']
|
|
|
|
year_option_selectors = [
|
|
# Neue TikTok Selektoren (2025) mit ID-Pattern
|
|
f"#Year-options-item-{year_index}", # ID-basiert (Index berechnet)
|
|
f"div#Year-options-item-{year_index}", # Mit div prefix
|
|
|
|
# Neue CSS-Klassen
|
|
f"div[class*='DivOption']:has-text('{birthday['year']}')", # Partial class match
|
|
f"div.eoyer415:has-text('{birthday['year']}')", # Neue spezifische Klasse
|
|
|
|
# Role-basierte Selektoren (stabiler)
|
|
f"[role='option']:has-text('{birthday['year']}')", # Role + Jahr
|
|
f"div[role='option']:has-text('{birthday['year']}')", # Div mit Role
|
|
|
|
# Text-basierte Selektoren
|
|
f"div:text-is('{birthday['year']}')", # Exakter Text
|
|
f"div:has-text('{birthday['year']}')", # Einfach Jahr
|
|
|
|
# Alte Selektoren (Fallback)
|
|
f"div.css-vz5m7n-DivOption:has-text('{birthday['year']}')", # Alt TikTok-Klasse
|
|
f"div.e1phcp2x5:has-text('{birthday['year']}')", # Alt TikTok-Klasse
|
|
|
|
# Alternative Fallbacks
|
|
f"option[value='{birthday['year']}']", # Standard HTML
|
|
f"div[data-value='{birthday['year']}']" # Custom Dropdown
|
|
]
|
|
|
|
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 |