Initial commit
Dieser Commit ist enthalten in:
0
social_networks/tiktok/__init__.py
Normale Datei
0
social_networks/tiktok/__init__.py
Normale Datei
392
social_networks/tiktok/tiktok_automation.py
Normale Datei
392
social_networks/tiktok/tiktok_automation.py
Normale Datei
@ -0,0 +1,392 @@
|
||||
"""
|
||||
TikTok-Automatisierung - Hauptklasse für TikTok-Automatisierungsfunktionalität
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from browser.playwright_manager import PlaywrightManager
|
||||
from browser.playwright_extensions import PlaywrightExtensions
|
||||
from social_networks.base_automation import BaseAutomation
|
||||
from utils.password_generator import PasswordGenerator
|
||||
from utils.username_generator import UsernameGenerator
|
||||
from utils.birthday_generator import BirthdayGenerator
|
||||
from utils.human_behavior import HumanBehavior
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Importiere Helferklassen
|
||||
from .tiktok_registration import TikTokRegistration
|
||||
from .tiktok_login import TikTokLogin
|
||||
from .tiktok_verification import TikTokVerification
|
||||
from .tiktok_ui_helper import TikTokUIHelper
|
||||
from .tiktok_utils import TikTokUtils
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_automation")
|
||||
|
||||
class TikTokAutomation(BaseAutomation):
|
||||
"""
|
||||
Hauptklasse für die TikTok-Automatisierung.
|
||||
Implementiert die Registrierung und Anmeldung bei TikTok.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
headless: bool = False,
|
||||
use_proxy: bool = False,
|
||||
proxy_type: str = None,
|
||||
save_screenshots: bool = True,
|
||||
screenshots_dir: str = None,
|
||||
slowmo: int = 0,
|
||||
debug: bool = False,
|
||||
email_domain: str = "z5m7q9dk3ah2v1plx6ju.com",
|
||||
enhanced_stealth: bool = True,
|
||||
fingerprint_noise: float = 0.5,
|
||||
window_position = None,
|
||||
fingerprint = None,
|
||||
auto_close_browser: bool = False):
|
||||
"""
|
||||
Initialisiert die TikTok-Automatisierung.
|
||||
|
||||
Args:
|
||||
headless: Ob der Browser im Headless-Modus ausgeführt werden soll
|
||||
use_proxy: Ob ein Proxy verwendet werden soll
|
||||
proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ
|
||||
save_screenshots: Ob Screenshots gespeichert werden sollen
|
||||
screenshots_dir: Verzeichnis für Screenshots
|
||||
slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging)
|
||||
debug: Ob Debug-Informationen angezeigt werden sollen
|
||||
email_domain: Domain für generierte E-Mail-Adressen
|
||||
enhanced_stealth: Ob erweiterter Stealth-Modus aktiviert werden soll
|
||||
fingerprint_noise: Menge an Rauschen für Fingerprint-Verschleierung (0.0-1.0)
|
||||
window_position: Optional - Fensterposition als Tuple (x, y)
|
||||
fingerprint: Optional - Vordefinierter Browser-Fingerprint
|
||||
auto_close_browser: Ob Browser automatisch geschlossen werden soll (Standard: False)
|
||||
"""
|
||||
# Initialisiere die Basisklasse
|
||||
super().__init__(
|
||||
headless=headless,
|
||||
use_proxy=use_proxy,
|
||||
proxy_type=proxy_type,
|
||||
save_screenshots=save_screenshots,
|
||||
screenshots_dir=screenshots_dir,
|
||||
slowmo=slowmo,
|
||||
debug=debug,
|
||||
email_domain=email_domain,
|
||||
window_position=window_position,
|
||||
auto_close_browser=auto_close_browser
|
||||
)
|
||||
|
||||
# Stealth-Modus-Einstellungen
|
||||
self.enhanced_stealth = enhanced_stealth
|
||||
self.fingerprint_noise = max(0.0, min(1.0, fingerprint_noise))
|
||||
|
||||
# Initialisiere Helferklassen
|
||||
self.registration = TikTokRegistration(self)
|
||||
self.login = TikTokLogin(self)
|
||||
self.verification = TikTokVerification(self)
|
||||
self.ui_helper = TikTokUIHelper(self)
|
||||
self.utils = TikTokUtils(self)
|
||||
|
||||
# Zusätzliche Hilfsklassen
|
||||
self.password_generator = PasswordGenerator()
|
||||
self.username_generator = UsernameGenerator()
|
||||
self.birthday_generator = BirthdayGenerator()
|
||||
self.human_behavior = HumanBehavior(speed_factor=0.8, randomness=0.6)
|
||||
|
||||
# Nutze übergebenen Fingerprint wenn vorhanden
|
||||
self.provided_fingerprint = fingerprint
|
||||
|
||||
logger.info("TikTok-Automatisierung initialisiert")
|
||||
|
||||
def _initialize_browser(self) -> bool:
|
||||
"""
|
||||
Initialisiert den Browser mit den entsprechenden Einstellungen.
|
||||
Diese Methode überschreibt die Methode der Basisklasse, um den erweiterten
|
||||
Fingerprint-Schutz zu aktivieren.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Proxy-Konfiguration, falls aktiviert
|
||||
proxy_config = None
|
||||
if self.use_proxy:
|
||||
proxy_config = self.proxy_rotator.get_proxy(self.proxy_type)
|
||||
if not proxy_config:
|
||||
logger.warning(f"Kein Proxy vom Typ '{self.proxy_type}' verfügbar, verwende direkten Zugriff")
|
||||
|
||||
# Browser initialisieren
|
||||
self.browser = PlaywrightManager(
|
||||
headless=self.headless,
|
||||
proxy=proxy_config,
|
||||
browser_type="chromium",
|
||||
screenshots_dir=self.screenshots_dir,
|
||||
slowmo=self.slowmo
|
||||
)
|
||||
|
||||
# Browser starten
|
||||
self.browser.start()
|
||||
|
||||
# Erweiterten Fingerprint-Schutz aktivieren, wenn gewünscht
|
||||
if self.enhanced_stealth:
|
||||
# Erstelle Extensions-Objekt
|
||||
extensions = PlaywrightExtensions(self.browser)
|
||||
|
||||
# Methoden anhängen
|
||||
extensions.hook_into_playwright_manager()
|
||||
|
||||
# Fingerprint-Schutz aktivieren mit angepasster Konfiguration
|
||||
if self.provided_fingerprint:
|
||||
# Nutze den bereitgestellten Fingerprint
|
||||
logger.info("Verwende bereitgestellten Fingerprint für Account-Erstellung")
|
||||
# Konvertiere Dict zu BrowserFingerprint wenn nötig
|
||||
if isinstance(self.provided_fingerprint, dict):
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
fingerprint_obj = BrowserFingerprint.from_dict(self.provided_fingerprint)
|
||||
else:
|
||||
fingerprint_obj = self.provided_fingerprint
|
||||
|
||||
# Wende Fingerprint über FingerprintProtection an
|
||||
from browser.fingerprint_protection import FingerprintProtection
|
||||
protection = FingerprintProtection(
|
||||
context=self.browser.context,
|
||||
fingerprint_config=fingerprint_obj
|
||||
)
|
||||
protection.apply_to_context(self.browser.context)
|
||||
logger.info(f"Fingerprint {fingerprint_obj.fingerprint_id} angewendet")
|
||||
else:
|
||||
# Fallback: Zufällige Fingerprint-Konfiguration
|
||||
fingerprint_config = {
|
||||
"noise_level": self.fingerprint_noise,
|
||||
"canvas_noise": True,
|
||||
"audio_noise": True,
|
||||
"webgl_noise": True,
|
||||
"hardware_concurrency": random.choice([4, 6, 8]),
|
||||
"device_memory": random.choice([4, 8]),
|
||||
"timezone_id": "Europe/Berlin"
|
||||
}
|
||||
|
||||
success = self.browser.enable_enhanced_fingerprint_protection(fingerprint_config)
|
||||
if success:
|
||||
logger.info("Erweiterter Fingerprint-Schutz erfolgreich aktiviert")
|
||||
else:
|
||||
logger.warning("Erweiterter Fingerprint-Schutz konnte nicht aktiviert werden")
|
||||
|
||||
logger.info("Browser erfolgreich initialisiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Browser-Initialisierung: {e}")
|
||||
self.status["error"] = f"Browser-Initialisierungsfehler: {str(e)}"
|
||||
return False
|
||||
|
||||
def register_account(self, full_name: str, age: int, registration_method: str = "email",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Registriert einen neuen TikTok-Account.
|
||||
|
||||
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
|
||||
"""
|
||||
logger.info(f"Starte TikTok-Account-Registrierung für '{full_name}' via {registration_method}")
|
||||
self._emit_customer_log(f"🎵 TikTok-Account wird erstellt für: {full_name}")
|
||||
|
||||
try:
|
||||
# Initialisiere Browser, falls noch nicht geschehen
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
if not self._initialize_browser():
|
||||
return {"success": False, "error": "Browser konnte nicht initialisiert werden"}
|
||||
|
||||
# Rotiere Fingerprint vor der Hauptaktivität, um Erkennung weiter zu erschweren
|
||||
if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'):
|
||||
self.browser.rotate_fingerprint()
|
||||
logger.info("Browser-Fingerprint vor der Registrierung rotiert")
|
||||
|
||||
# Delegiere die Hauptregistrierungslogik an die Registration-Klasse
|
||||
result = self.registration.register_account(full_name, age, registration_method, phone_number, **kwargs)
|
||||
|
||||
# Nehme Abschlussfoto auf
|
||||
self._take_screenshot(f"registration_finished_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update(result)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der Account-Registrierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Fehler-Screenshot
|
||||
self._take_screenshot(f"registration_error_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update({
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "error"
|
||||
})
|
||||
|
||||
return self.status
|
||||
finally:
|
||||
# Browser schließen
|
||||
self._close_browser()
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Meldet sich bei einem bestehenden TikTok-Account an.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Anmeldung mit Status
|
||||
"""
|
||||
logger.info(f"Starte TikTok-Login für '{username_or_email}'")
|
||||
|
||||
try:
|
||||
# Initialisiere Browser, falls noch nicht geschehen
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
if not self._initialize_browser():
|
||||
return {"success": False, "error": "Browser konnte nicht initialisiert werden"}
|
||||
|
||||
# Rotiere Fingerprint vor dem Login
|
||||
if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'):
|
||||
self.browser.rotate_fingerprint()
|
||||
logger.info("Browser-Fingerprint vor dem Login rotiert")
|
||||
|
||||
# Delegiere die Hauptlogin-Logik an die Login-Klasse
|
||||
result = self.login.login_account(username_or_email, password, **kwargs)
|
||||
|
||||
# Nehme Abschlussfoto auf
|
||||
self._take_screenshot(f"login_finished_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update(result)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler beim Login: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Fehler-Screenshot
|
||||
self._take_screenshot(f"login_error_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update({
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "error"
|
||||
})
|
||||
|
||||
return self.status
|
||||
finally:
|
||||
# Browser schließen
|
||||
self._close_browser()
|
||||
|
||||
def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Verifiziert einen TikTok-Account mit einem Bestätigungscode.
|
||||
|
||||
Args:
|
||||
verification_code: Der Bestätigungscode
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung mit Status
|
||||
"""
|
||||
logger.info(f"Starte TikTok-Account-Verifizierung mit Code: {verification_code}")
|
||||
|
||||
try:
|
||||
# Initialisiere Browser, falls noch nicht geschehen
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
if not self._initialize_browser():
|
||||
return {"success": False, "error": "Browser konnte nicht initialisiert werden"}
|
||||
|
||||
# Delegiere die Hauptverifizierungslogik an die Verification-Klasse
|
||||
result = self.verification.verify_account(verification_code, **kwargs)
|
||||
|
||||
# Nehme Abschlussfoto auf
|
||||
self._take_screenshot(f"verification_finished_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update(result)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der Account-Verifizierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Fehler-Screenshot
|
||||
self._take_screenshot(f"verification_error_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update({
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "error"
|
||||
})
|
||||
|
||||
return self.status
|
||||
finally:
|
||||
# Browser schließen
|
||||
self._close_browser()
|
||||
|
||||
def get_fingerprint_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den aktuellen Status des Fingerprint-Schutzes zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Status des Fingerprint-Schutzes
|
||||
"""
|
||||
if not self.enhanced_stealth or not hasattr(self.browser, 'get_fingerprint_status'):
|
||||
return {
|
||||
"active": False,
|
||||
"message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert"
|
||||
}
|
||||
|
||||
return self.browser.get_fingerprint_status()
|
||||
|
||||
def get_current_page(self):
|
||||
"""
|
||||
Gibt die aktuelle Playwright Page-Instanz zurück.
|
||||
|
||||
Returns:
|
||||
Page: Die aktuelle Seite oder None
|
||||
"""
|
||||
if self.browser and hasattr(self.browser, 'page'):
|
||||
return self.browser.page
|
||||
return None
|
||||
|
||||
def get_session_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Extrahiert Session-Daten (Cookies, LocalStorage, etc.) aus dem aktuellen Browser.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Session-Daten
|
||||
"""
|
||||
if not self.is_browser_open():
|
||||
return {}
|
||||
|
||||
try:
|
||||
return {
|
||||
"cookies": self.browser.page.context.cookies(),
|
||||
"local_storage": self.browser.page.evaluate("() => Object.assign({}, window.localStorage)"),
|
||||
"session_storage": self.browser.page.evaluate("() => Object.assign({}, window.sessionStorage)"),
|
||||
"url": self.browser.page.url
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren der Session-Daten: {e}")
|
||||
return {}
|
||||
825
social_networks/tiktok/tiktok_login.py
Normale Datei
825
social_networks/tiktok/tiktok_login.py
Normale Datei
@ -0,0 +1,825 @@
|
||||
"""
|
||||
TikTok-Login - Klasse für die Anmeldefunktionalität bei TikTok
|
||||
"""
|
||||
|
||||
import time
|
||||
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_login")
|
||||
|
||||
class TikTokLogin:
|
||||
"""
|
||||
Klasse für die Anmeldung bei TikTok-Konten.
|
||||
Enthält alle Methoden für den Login-Prozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Login-Funktionalität.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = TikTokSelectors()
|
||||
self.workflow = TikTokWorkflow.get_login_workflow()
|
||||
|
||||
logger.debug("TikTok-Login initialisiert")
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Login-Prozess für ein TikTok-Konto durch.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis des Logins mit Status
|
||||
"""
|
||||
# Browser wird direkt von automation verwendet
|
||||
|
||||
# Validiere die Eingaben
|
||||
if not self._validate_login_inputs(username_or_email, password):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültige Login-Eingaben",
|
||||
"stage": "input_validation"
|
||||
}
|
||||
|
||||
# Account-Daten für die Anmeldung
|
||||
account_data = {
|
||||
"username": username_or_email,
|
||||
"password": password,
|
||||
"handle_2fa": kwargs.get("handle_2fa", False),
|
||||
"two_factor_code": kwargs.get("two_factor_code"),
|
||||
"skip_save_login": kwargs.get("skip_save_login", True)
|
||||
}
|
||||
|
||||
logger.info(f"Starte TikTok-Login für {username_or_email}")
|
||||
|
||||
try:
|
||||
# 1. Zur Login-Seite navigieren
|
||||
if not self._navigate_to_login_page():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht zur Login-Seite navigieren",
|
||||
"stage": "navigation"
|
||||
}
|
||||
|
||||
# 2. Cookie-Banner behandeln
|
||||
self._handle_cookie_banner()
|
||||
|
||||
# 3. Login-Formular ausfüllen
|
||||
if not self._fill_login_form(account_data):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Ausfüllen des Login-Formulars",
|
||||
"stage": "login_form"
|
||||
}
|
||||
|
||||
# 4. Auf 2FA prüfen und behandeln, falls nötig
|
||||
needs_2fa, two_fa_error = self._check_needs_two_factor_auth()
|
||||
|
||||
if needs_2fa:
|
||||
if not account_data["handle_2fa"]:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Zwei-Faktor-Authentifizierung erforderlich, aber nicht aktiviert",
|
||||
"stage": "two_factor_required"
|
||||
}
|
||||
|
||||
# 2FA behandeln
|
||||
if not self._handle_two_factor_auth(account_data["two_factor_code"]):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler bei der Zwei-Faktor-Authentifizierung",
|
||||
"stage": "two_factor_auth"
|
||||
}
|
||||
|
||||
# 5. Benachrichtigungserlaubnis-Dialog behandeln
|
||||
self._handle_notifications_prompt()
|
||||
|
||||
# 6. Erfolgreichen Login überprüfen
|
||||
if not self._check_login_success():
|
||||
error_message = self._get_login_error()
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Login fehlgeschlagen: {error_message or 'Unbekannter Fehler'}",
|
||||
"stage": "login_check"
|
||||
}
|
||||
|
||||
# Login erfolgreich
|
||||
logger.info(f"TikTok-Login für {username_or_email} erfolgreich")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "completed",
|
||||
"username": username_or_email
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler beim TikTok-Login: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception"
|
||||
}
|
||||
|
||||
def _validate_login_inputs(self, username_or_email: str, password: str) -> bool:
|
||||
"""
|
||||
Validiert die Eingaben für den Login.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
|
||||
Returns:
|
||||
bool: True wenn alle Eingaben gültig sind, False sonst
|
||||
"""
|
||||
if not username_or_email or len(username_or_email) < 3:
|
||||
logger.error("Ungültiger Benutzername oder E-Mail")
|
||||
return False
|
||||
|
||||
if not password or len(password) < 6:
|
||||
logger.error("Ungültiges Passwort")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _navigate_to_login_page(self) -> bool:
|
||||
"""
|
||||
Navigiert zur TikTok-Login-Seite über die Explore-Seite.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Zur Explore-Seite navigieren
|
||||
logger.info("Navigiere zur TikTok Explore-Seite")
|
||||
self.automation.browser.navigate_to(TikTokSelectors.EXPLORE_URL)
|
||||
|
||||
# Warten, bis die Seite geladen ist
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("tiktok_explore_page")
|
||||
|
||||
# Login-Button auf der Explore-Seite suchen und klicken
|
||||
logger.info("Suche Anmelden-Button auf Explore-Seite")
|
||||
login_button_selectors = [
|
||||
"button#header-login-button",
|
||||
"button.TUXButton--primary",
|
||||
"button:has-text('Anmelden')",
|
||||
TikTokSelectors.LOGIN_BUTTON_HEADER,
|
||||
"button[class*='StyledLeftSidePrimaryButton']"
|
||||
]
|
||||
|
||||
button_clicked = False
|
||||
for selector in login_button_selectors:
|
||||
logger.debug(f"Versuche Login-Button: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Kurz warten vor dem Klick
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
if self.automation.browser.click_element(selector):
|
||||
button_clicked = True
|
||||
logger.info(f"Anmelden-Button erfolgreich geklickt: {selector}")
|
||||
break
|
||||
|
||||
if not button_clicked:
|
||||
logger.error("Konnte keinen Anmelden-Button auf der Explore-Seite finden")
|
||||
self.automation._take_screenshot("no_login_button_found")
|
||||
return False
|
||||
|
||||
# Warten, bis der Login-Dialog erscheint
|
||||
logger.info("Warte auf Login-Dialog")
|
||||
self.automation.human_behavior.random_delay(2.0, 3.0)
|
||||
|
||||
# Prüfen, ob der Login-Dialog sichtbar ist
|
||||
dialog_visible = False
|
||||
dialog_selectors = [
|
||||
"div[role='dialog']",
|
||||
TikTokSelectors.LOGIN_DIALOG,
|
||||
"div[class*='login-modal']",
|
||||
"div[class*='DivLoginContainer']"
|
||||
]
|
||||
|
||||
for dialog_selector in dialog_selectors:
|
||||
if self.automation.browser.is_element_visible(dialog_selector, timeout=5000):
|
||||
dialog_visible = True
|
||||
logger.info(f"Login-Dialog erschienen: {dialog_selector}")
|
||||
break
|
||||
|
||||
if not dialog_visible:
|
||||
logger.error("Login-Dialog ist nach 5 Sekunden nicht erschienen")
|
||||
self.automation._take_screenshot("no_login_dialog")
|
||||
return False
|
||||
|
||||
# Screenshot vom geöffneten Dialog
|
||||
self.automation._take_screenshot("login_dialog_opened")
|
||||
|
||||
logger.info("Erfolgreich zum Login-Dialog navigiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Navigieren zur Login-Seite: {e}")
|
||||
return False
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
Akzeptiert IMMER Cookies für vollständiges Session-Management beim Login.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
# Cookie-Dialog-Erkennung
|
||||
if self.automation.browser.is_element_visible(TikTokSelectors.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 Login)
|
||||
accept_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
TikTokSelectors.get_button_texts("accept_cookies"),
|
||||
TikTokSelectors.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 _fill_login_form(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Füllt das Login-Formular aus und sendet es ab.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten für den Login
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# 1. E-Mail/Telefon-Login-Option auswählen
|
||||
logger.info("Klicke auf 'Telefon-Nr./E-Mail/Anmeldename nutzen'")
|
||||
|
||||
# Warte kurz, damit Dialog vollständig geladen ist
|
||||
self.automation.human_behavior.random_delay(1.0, 1.5)
|
||||
|
||||
# Selektoren für die Option - spezifischer für E-Mail/Telefon
|
||||
phone_email_selectors = [
|
||||
"div[data-e2e='channel-item']:has(p:has-text('Telefon-Nr./E-Mail/Anmeldename nutzen'))",
|
||||
"div[role='link']:has-text('Telefon-Nr./E-Mail/Anmeldename nutzen')",
|
||||
"//div[@data-e2e='channel-item'][.//p[contains(text(), 'Telefon-Nr./E-Mail/Anmeldename nutzen')]]",
|
||||
"div.css-17hparj-DivBoxContainer:has-text('Telefon-Nr./E-Mail/Anmeldename nutzen')",
|
||||
"//div[contains(@class, 'DivBoxContainer') and contains(., 'Telefon-Nr./E-Mail/Anmeldename nutzen')]"
|
||||
]
|
||||
|
||||
email_phone_clicked = False
|
||||
for selector in phone_email_selectors:
|
||||
logger.debug(f"Versuche E-Mail/Telefon-Selektor: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Kurz warten vor dem Klick
|
||||
self.automation.human_behavior.random_delay(0.3, 0.5)
|
||||
if self.automation.browser.click_element(selector):
|
||||
email_phone_clicked = True
|
||||
logger.info(f"E-Mail/Telefon-Option erfolgreich geklickt: {selector}")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Klick fehlgeschlagen für Selektor: {selector}")
|
||||
|
||||
if not email_phone_clicked:
|
||||
logger.error("Konnte 'Telefon-Nr./E-Mail/Anmeldename nutzen' nicht klicken")
|
||||
self.automation._take_screenshot("phone_email_option_not_found")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# 2. "Mit E-Mail-Adresse oder Benutzernamen anmelden" Link klicken
|
||||
logger.info("Klicke auf 'Mit E-Mail-Adresse oder Benutzernamen anmelden'")
|
||||
|
||||
email_link_selectors = [
|
||||
"a[href='/login/phone-or-email/email']",
|
||||
"a.css-1mgli76-ALink-StyledLink",
|
||||
"a:has-text('Mit E-Mail-Adresse oder Benutzernamen anmelden')",
|
||||
"//a[contains(text(), 'Mit E-Mail-Adresse oder Benutzernamen anmelden')]"
|
||||
]
|
||||
|
||||
email_login_clicked = False
|
||||
for selector in email_link_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
email_login_clicked = True
|
||||
logger.info(f"E-Mail-Login-Link geklickt: {selector}")
|
||||
break
|
||||
|
||||
if not email_login_clicked:
|
||||
logger.error("Konnte E-Mail-Login-Link nicht klicken")
|
||||
self.automation._take_screenshot("email_login_link_not_found")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.5, 2.5)
|
||||
|
||||
# 3. E-Mail/Benutzername eingeben (Character-by-Character)
|
||||
logger.info(f"Gebe E-Mail/Benutzername ein: {account_data['username']}")
|
||||
|
||||
# Warte bis Formular geladen ist
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
|
||||
username_selectors = [
|
||||
"input[name='username']",
|
||||
"input[placeholder='E-Mail-Adresse oder Benutzername']",
|
||||
"input.css-11to27l-InputContainer[name='username']",
|
||||
"input[type='text'][autocomplete='webauthn']"
|
||||
]
|
||||
|
||||
username_success = False
|
||||
for selector in username_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
username_success = self._fill_username_field_character_by_character(selector, account_data["username"])
|
||||
if username_success:
|
||||
logger.info(f"Benutzername erfolgreich eingegeben mit Selektor: {selector}")
|
||||
break
|
||||
|
||||
if not username_success:
|
||||
logger.error("Konnte Benutzername-Feld nicht ausfüllen")
|
||||
self.automation._take_screenshot("username_field_not_found")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
|
||||
# 4. Passwort eingeben (mit Character-by-Character für bessere Kompatibilität)
|
||||
logger.info("Gebe Passwort ein")
|
||||
password_selectors = [
|
||||
"input[type='password']",
|
||||
"input[placeholder='Passwort']",
|
||||
"input.css-wv3bkt-InputContainer[type='password']",
|
||||
"input[autocomplete='new-password']"
|
||||
]
|
||||
|
||||
password_success = False
|
||||
for selector in password_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Verwende character-by-character Eingabe
|
||||
password_success = self._fill_password_field_character_by_character(selector, account_data["password"])
|
||||
if password_success:
|
||||
logger.info(f"Passwort erfolgreich eingegeben mit Selektor: {selector}")
|
||||
break
|
||||
|
||||
if not password_success:
|
||||
logger.error("Konnte Passwort-Feld nicht ausfüllen")
|
||||
self.automation._take_screenshot("password_field_not_found")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot vorm Absenden
|
||||
self.automation._take_screenshot("login_form_filled")
|
||||
|
||||
# 5. Prüfe ob Login-Button aktiviert ist
|
||||
logger.info("Prüfe Login-Button Status")
|
||||
login_button_selectors = [
|
||||
"button[data-e2e='login-button']",
|
||||
"button[type='submit'][data-e2e='login-button']",
|
||||
"button.css-11sviba-Button-StyledButton",
|
||||
"button:has-text('Anmelden')"
|
||||
]
|
||||
|
||||
button_ready = False
|
||||
active_selector = None
|
||||
for selector in login_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
element = self.automation.browser.page.locator(selector).first
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
if not is_disabled:
|
||||
button_ready = True
|
||||
active_selector = selector
|
||||
logger.info(f"Login-Button ist aktiviert: {selector}")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Login-Button ist disabled: {selector}")
|
||||
|
||||
if not button_ready:
|
||||
logger.warning("Login-Button ist nicht bereit, warte zusätzlich")
|
||||
self.automation.human_behavior.random_delay(2.0, 3.0)
|
||||
# Nochmal prüfen
|
||||
for selector in login_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
element = self.automation.browser.page.locator(selector).first
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
if not is_disabled:
|
||||
button_ready = True
|
||||
active_selector = selector
|
||||
break
|
||||
|
||||
# 6. Login-Button klicken
|
||||
logger.info("Klicke auf Anmelden-Button")
|
||||
if button_ready and active_selector:
|
||||
if self.automation.browser.click_element(active_selector):
|
||||
logger.info(f"Login-Button erfolgreich geklickt: {active_selector}")
|
||||
else:
|
||||
logger.error("Klick auf Login-Button fehlgeschlagen")
|
||||
return False
|
||||
else:
|
||||
logger.error("Konnte keinen aktivierten Login-Button finden")
|
||||
self.automation._take_screenshot("no_active_login_button")
|
||||
return False
|
||||
|
||||
# Nach dem Absenden warten
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=2.0)
|
||||
|
||||
# Überprüfen, ob es eine Fehlermeldung gab
|
||||
error_message = self._get_login_error()
|
||||
if error_message:
|
||||
logger.error(f"Login-Fehler erkannt: {error_message}")
|
||||
return False
|
||||
|
||||
logger.info("Login-Formular erfolgreich ausgefüllt und abgesendet")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Login-Formulars: {e}")
|
||||
return False
|
||||
|
||||
def _fill_username_field_character_by_character(self, selector: str, username: str) -> bool:
|
||||
"""
|
||||
Füllt das Benutzername-Feld Zeichen für Zeichen aus.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor für das Username-Feld
|
||||
username: Der einzugebende Benutzername
|
||||
|
||||
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 Benutzername-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
|
||||
import random
|
||||
for i, char in enumerate(username):
|
||||
element.type(char, delay=random.randint(50, 150)) # Zufällige Tippgeschwindigkeit
|
||||
|
||||
# Nach jedem 4. Zeichen eine kleine Pause (simuliert echtes Tippen)
|
||||
if (i + 1) % 4 == 0:
|
||||
self.automation.human_behavior.random_delay(0.1, 0.3)
|
||||
|
||||
# Fokus verlassen
|
||||
self.automation.human_behavior.random_delay(0.2, 0.4)
|
||||
element.press("Tab")
|
||||
|
||||
logger.info(f"Benutzername character-by-character eingegeben: {len(username)} Zeichen")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Character-by-Character Benutzername-Eingabe: {e}")
|
||||
return False
|
||||
|
||||
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
|
||||
import random
|
||||
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_login_error(self) -> Optional[str]:
|
||||
"""
|
||||
Überprüft, ob eine Login-Fehlermeldung angezeigt wird.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Fehlermeldung oder None, wenn keine gefunden wurde
|
||||
"""
|
||||
try:
|
||||
# Auf Fehlermeldungen prüfen
|
||||
error_selectors = [
|
||||
TikTokSelectors.ERROR_MESSAGE,
|
||||
"p[class*='error']",
|
||||
"div[role='alert']",
|
||||
"div[class*='error']",
|
||||
"div[class*='Error']"
|
||||
]
|
||||
|
||||
for selector in error_selectors:
|
||||
error_element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if error_element:
|
||||
error_text = error_element.text_content()
|
||||
if error_text and len(error_text.strip()) > 0:
|
||||
return error_text.strip()
|
||||
|
||||
# Wenn keine spezifische Fehlermeldung gefunden wurde, nach bekannten Fehlermustern suchen
|
||||
error_texts = [
|
||||
"Falsches Passwort",
|
||||
"Benutzername nicht gefunden",
|
||||
"incorrect password",
|
||||
"username you entered doesn't belong",
|
||||
"please wait a few minutes",
|
||||
"try again later",
|
||||
"Bitte warte einige Minuten",
|
||||
"versuche es später noch einmal"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content()
|
||||
for error_text in error_texts:
|
||||
if error_text.lower() in page_content.lower():
|
||||
return f"Erkannter Fehler: {error_text}"
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Login-Fehler: {e}")
|
||||
return None
|
||||
|
||||
def _check_needs_two_factor_auth(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Überprüft, ob eine Zwei-Faktor-Authentifizierung erforderlich ist.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (2FA erforderlich, Fehlermeldung falls vorhanden)
|
||||
"""
|
||||
try:
|
||||
# Nach 2FA-Indikatoren suchen
|
||||
two_fa_selectors = [
|
||||
"input[name='verificationCode']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder*='Code']",
|
||||
"div[class*='verification-code']",
|
||||
"div[class*='two-factor']"
|
||||
]
|
||||
|
||||
for selector in two_fa_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Zwei-Faktor-Authentifizierung erforderlich")
|
||||
return True, None
|
||||
|
||||
# Texte, die auf 2FA hinweisen
|
||||
two_fa_indicators = [
|
||||
"Verifizierungscode",
|
||||
"Verification code",
|
||||
"Sicherheitscode",
|
||||
"Security code",
|
||||
"zwei-faktor",
|
||||
"two-factor",
|
||||
"2FA"
|
||||
]
|
||||
|
||||
# Seiteninhalt durchsuchen
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for indicator in two_fa_indicators:
|
||||
if indicator.lower() in page_content:
|
||||
logger.info(f"Zwei-Faktor-Authentifizierung erkannt durch Text: {indicator}")
|
||||
return True, None
|
||||
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf 2FA: {e}")
|
||||
return False, f"Fehler bei der 2FA-Erkennung: {str(e)}"
|
||||
|
||||
def _handle_two_factor_auth(self, two_factor_code: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Behandelt die Zwei-Faktor-Authentifizierung.
|
||||
|
||||
Args:
|
||||
two_factor_code: Optional vorhandener 2FA-Code
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("two_factor_auth")
|
||||
|
||||
# 2FA-Eingabefeld finden
|
||||
two_fa_selectors = [
|
||||
"input[name='verificationCode']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder*='Code']"
|
||||
]
|
||||
|
||||
two_fa_field = None
|
||||
for selector in two_fa_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
two_fa_field = selector
|
||||
break
|
||||
|
||||
if not two_fa_field:
|
||||
logger.error("Konnte 2FA-Eingabefeld nicht finden")
|
||||
return False
|
||||
|
||||
# Wenn kein Code bereitgestellt wurde, Benutzer auffordern
|
||||
if not two_factor_code:
|
||||
logger.warning("Kein 2FA-Code bereitgestellt, kann nicht fortfahren")
|
||||
return False
|
||||
|
||||
# 2FA-Code eingeben
|
||||
code_success = self.automation.browser.fill_form_field(two_fa_field, two_factor_code)
|
||||
|
||||
if not code_success:
|
||||
logger.error("Konnte 2FA-Code nicht eingeben")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Bestätigen-Button finden und klicken
|
||||
confirm_button_selectors = [
|
||||
"button[type='submit']",
|
||||
"//button[contains(text(), 'Bestätigen')]",
|
||||
"//button[contains(text(), 'Confirm')]",
|
||||
"//button[contains(text(), 'Verify')]"
|
||||
]
|
||||
|
||||
confirm_clicked = False
|
||||
for selector in confirm_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
confirm_clicked = True
|
||||
break
|
||||
|
||||
if not confirm_clicked:
|
||||
# Alternative: Mit Tastendruck bestätigen
|
||||
self.automation.browser.page.keyboard.press("Enter")
|
||||
logger.info("Enter-Taste gedrückt, um 2FA zu bestätigen")
|
||||
|
||||
# Warten nach der Bestätigung
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Überprüfen, ob 2FA erfolgreich war
|
||||
still_on_2fa = self._check_needs_two_factor_auth()[0]
|
||||
|
||||
if still_on_2fa:
|
||||
# Prüfen, ob Fehlermeldung angezeigt wird
|
||||
error_message = self._get_login_error()
|
||||
if error_message:
|
||||
logger.error(f"2FA-Fehler: {error_message}")
|
||||
else:
|
||||
logger.error("2FA fehlgeschlagen, immer noch auf 2FA-Seite")
|
||||
return False
|
||||
|
||||
logger.info("Zwei-Faktor-Authentifizierung erfolgreich")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Zwei-Faktor-Authentifizierung: {e}")
|
||||
return False
|
||||
|
||||
def _handle_notifications_prompt(self) -> bool:
|
||||
"""
|
||||
Behandelt den Benachrichtigungen-Dialog.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Nach "Nicht jetzt"-Button suchen
|
||||
not_now_selectors = [
|
||||
"//button[contains(text(), 'Nicht jetzt')]",
|
||||
"//button[contains(text(), 'Not now')]",
|
||||
"//button[contains(text(), 'Skip')]",
|
||||
"//button[contains(text(), 'Später')]",
|
||||
"//button[contains(text(), 'Later')]"
|
||||
]
|
||||
|
||||
for selector in not_now_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Benachrichtigungen-Dialog übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
# Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden
|
||||
logger.debug("Kein Benachrichtigungen-Dialog erkannt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Behandeln des Benachrichtigungen-Dialogs: {e}")
|
||||
# Dies ist nicht kritisch, daher geben wir trotzdem True zurück
|
||||
return True
|
||||
|
||||
def _check_login_success(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob der Login erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich, False sonst
|
||||
"""
|
||||
try:
|
||||
# Warten nach dem Login
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("login_final")
|
||||
|
||||
# Erfolg anhand verschiedener Indikatoren prüfen
|
||||
success_indicators = TikTokSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self.automation.browser.is_element_visible(indicator, timeout=2000):
|
||||
logger.info(f"Login-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 "/login" not in current_url:
|
||||
logger.info(f"Login-Erfolg basierend auf URL: {current_url}")
|
||||
return True
|
||||
|
||||
# Prüfen, ob immer noch auf der Login-Seite
|
||||
if "/login" in current_url or self.automation.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=1000):
|
||||
logger.warning("Immer noch auf der Login-Seite, Login fehlgeschlagen")
|
||||
return False
|
||||
|
||||
logger.warning("Keine Login-Erfolgsindikatoren gefunden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen des Login-Erfolgs: {e}")
|
||||
return False
|
||||
2455
social_networks/tiktok/tiktok_registration.py
Normale Datei
2455
social_networks/tiktok/tiktok_registration.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
2203
social_networks/tiktok/tiktok_registration_backup.py
Normale Datei
2203
social_networks/tiktok/tiktok_registration_backup.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
115
social_networks/tiktok/tiktok_registration_final.py
Normale Datei
115
social_networks/tiktok/tiktok_registration_final.py
Normale Datei
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
801
social_networks/tiktok/tiktok_registration_new.py
Normale Datei
801
social_networks/tiktok/tiktok_registration_new.py
Normale Datei
@ -0,0 +1,801 @@
|
||||
# social_networks/tiktok/tiktok_registration_new.py
|
||||
|
||||
"""
|
||||
TikTok-Registrierung - Optimierte Klasse für die Kontoerstellung bei TikTok
|
||||
NEUE IMPLEMENTIERUNG mit korrekter Workflow-Reihenfolge für maximale Stabilität.
|
||||
|
||||
OPTIMIERTER WORKFLOW:
|
||||
1. E-Mail eingeben
|
||||
2. Passwort eingeben
|
||||
3. Code senden Button klicken
|
||||
4. Code empfangen und eingeben
|
||||
5. Weiter Button wird automatisch aktiviert
|
||||
"""
|
||||
|
||||
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:
|
||||
"""
|
||||
Optimierte Klasse für die Registrierung von TikTok-Konten.
|
||||
Implementiert einen robusten, zukunftssicheren Workflow.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Registrierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
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
|
||||
"""
|
||||
# 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 optimierten TikTok-Registrierungsprozess 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 self._create_error_result("Konnte nicht zur TikTok-Startseite navigieren", "navigation", 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 self._create_error_result("Konnte nicht auf Anmelden-Button klicken", "login_button", account_data)
|
||||
|
||||
# 4. Registrieren-Link klicken
|
||||
if not self._click_register_link():
|
||||
return self._create_error_result("Konnte nicht auf Registrieren-Link klicken", "register_link", account_data)
|
||||
|
||||
# 5. Telefon/E-Mail-Option auswählen
|
||||
if not self._click_phone_email_option():
|
||||
return self._create_error_result("Konnte nicht auf Telefon/E-Mail-Option klicken", "phone_email_option", account_data)
|
||||
|
||||
# 6. E-Mail oder Telefon als Registrierungsmethode wählen
|
||||
if not self._select_registration_method(registration_method):
|
||||
return self._create_error_result(f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen", "registration_method", account_data)
|
||||
|
||||
# 7. Geburtsdatum eingeben
|
||||
self.automation._emit_customer_log("🎂 Geburtsdatum wird festgelegt...")
|
||||
if not self._enter_birthday(account_data["birthday"]):
|
||||
return self._create_error_result("Fehler beim Eingeben des Geburtsdatums", "birthday", account_data)
|
||||
|
||||
# 8. OPTIMIERTER REGISTRIERUNGSWORKFLOW
|
||||
self.automation._emit_customer_log("📝 Persönliche Daten werden übertragen...")
|
||||
if not self._execute_optimized_registration_workflow(account_data, registration_method):
|
||||
return self._create_error_result("Fehler im Registrierungsworkflow", "registration_workflow", account_data)
|
||||
|
||||
# 9. Benutzernamen erstellen
|
||||
self.automation._emit_customer_log("👤 Benutzername wird erstellt...")
|
||||
if not self._create_username(account_data):
|
||||
return self._create_error_result("Fehler beim Erstellen des Benutzernamens", "username", account_data)
|
||||
|
||||
# 10. Erfolgreiche Registrierung überprüfen
|
||||
self.automation._emit_customer_log("🔍 Account wird finalisiert...")
|
||||
if not self._check_registration_success():
|
||||
return self._create_error_result("Registrierung fehlgeschlagen oder konnte nicht verifiziert werden", "final_check", 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 _execute_optimized_registration_workflow(self, account_data: Dict[str, Any], registration_method: str) -> bool:
|
||||
"""
|
||||
Führt den optimierten Registrierungsworkflow aus.
|
||||
|
||||
KORRIGIERTE REIHENFOLGE für E-Mail-Registrierung:
|
||||
1. E-Mail eingeben
|
||||
2. Code senden Button klicken
|
||||
3. Code empfangen und eingeben
|
||||
4. Passwort eingeben
|
||||
5. Dummy-Input-Trick anwenden
|
||||
6. Weiter Button klicken
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
if registration_method == "email":
|
||||
return self._execute_email_workflow(account_data)
|
||||
elif registration_method == "phone":
|
||||
return self._execute_phone_workflow(account_data)
|
||||
else:
|
||||
logger.error(f"Unbekannte Registrierungsmethode: {registration_method}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im optimierten Registrierungsworkflow: {e}")
|
||||
return False
|
||||
|
||||
def _execute_email_workflow(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Führt den optimierten E-Mail-Registrierungsworkflow aus.
|
||||
|
||||
KORRIGIERTER WORKFLOW: E-Mail → Code senden → Code eingeben → Passwort → Dummy-Trick → Weiter
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("=== STARTE OPTIMIERTEN E-MAIL-WORKFLOW ===")
|
||||
|
||||
# SCHRITT 1: E-Mail-Feld ausfüllen
|
||||
logger.info("SCHRITT 1/6: E-Mail-Adresse eingeben")
|
||||
if not self._fill_email_field(account_data["email"]):
|
||||
logger.error("Fehler beim Ausfüllen des E-Mail-Feldes")
|
||||
return False
|
||||
|
||||
# SCHRITT 2: Code senden Button klicken (VOR Passwort!)
|
||||
logger.info("SCHRITT 2/6: Code senden Button klicken")
|
||||
if not self._click_send_code_button():
|
||||
logger.error("Fehler beim Klicken des Code-senden-Buttons")
|
||||
return False
|
||||
|
||||
# SCHRITT 3: Verifizierungscode empfangen und eingeben
|
||||
logger.info("SCHRITT 3/6: Auf Code warten und eingeben")
|
||||
if not self._handle_email_verification(account_data["email"]):
|
||||
logger.error("Fehler bei der E-Mail-Verifizierung")
|
||||
return False
|
||||
|
||||
# SCHRITT 4: Passwort-Feld ausfüllen (NACH Code-Eingabe!)
|
||||
logger.info("SCHRITT 4/6: Passwort eingeben (nach Code-Verifizierung)")
|
||||
if not self._fill_password_field(account_data["password"]):
|
||||
logger.error("Fehler beim Ausfüllen des Passwort-Feldes")
|
||||
return False
|
||||
|
||||
# SCHRITT 5: Dummy-Input-Trick anwenden
|
||||
logger.info("SCHRITT 5/6: Dummy-Input-Trick anwenden")
|
||||
if not self._apply_dummy_input_trick():
|
||||
logger.error("Fehler beim Dummy-Input-Trick")
|
||||
return False
|
||||
|
||||
# SCHRITT 6: Weiter Button klicken
|
||||
logger.info("SCHRITT 6/6: Weiter Button klicken")
|
||||
if not self._click_continue_button():
|
||||
logger.error("Fehler beim Klicken des Weiter-Buttons")
|
||||
return False
|
||||
|
||||
logger.info("=== E-MAIL-WORKFLOW ERFOLGREICH ABGESCHLOSSEN ===")
|
||||
|
||||
# Kurze Pause für UI-Updates - das Weiter-Button sollte jetzt aktiviert sein
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im E-Mail-Workflow: {e}")
|
||||
return False
|
||||
|
||||
def _fill_email_field(self, email: str) -> bool:
|
||||
"""
|
||||
Füllt das E-Mail-Feld mit robusten Selektoren aus.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste E-Mail-Feld-Selektoren (in Prioritätsreihenfolge)
|
||||
email_selectors = [
|
||||
"input[placeholder*='E-Mail']",
|
||||
"input[placeholder*='Email']",
|
||||
"input[type='email']",
|
||||
"input[name='email']",
|
||||
"input[aria-label*='Email']",
|
||||
"input[aria-label*='E-Mail']",
|
||||
self.selectors.EMAIL_FIELD,
|
||||
self.selectors.EMAIL_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(email_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
success = self.automation.browser.fill_form_field(selector, email, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"E-Mail-Feld erfolgreich ausgefüllt mit Selektor {i+1}: {email}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"E-Mail-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["E-Mail-Adresse", "Email", "E-Mail"],
|
||||
email,
|
||||
email_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"E-Mail-Feld über Fuzzy-Matching ausgefüllt: {email}")
|
||||
return True
|
||||
|
||||
logger.error("Konnte E-Mail-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des E-Mail-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _fill_password_field(self, password: str) -> bool:
|
||||
"""
|
||||
Füllt das Passwort-Feld mit robusten Selektoren aus.
|
||||
|
||||
Args:
|
||||
password: Passwort
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste Passwort-Feld-Selektoren (in Prioritätsreihenfolge)
|
||||
password_selectors = [
|
||||
"input[type='password'][placeholder*='Passwort']",
|
||||
"input[type='password'][placeholder*='Password']",
|
||||
"input[type='password']",
|
||||
"input[name='password']",
|
||||
"input[aria-label*='Password']",
|
||||
"input[aria-label*='Passwort']",
|
||||
self.selectors.PASSWORD_FIELD,
|
||||
self.selectors.PASSWORD_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(password_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
success = self.automation.browser.fill_form_field(selector, password, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"Passwort-Feld erfolgreich ausgefüllt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Passwort-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Passwort", "Password"],
|
||||
password,
|
||||
password_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Passwort-Feld über Fuzzy-Matching ausgefüllt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte Passwort-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Passwort-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _click_send_code_button(self) -> bool:
|
||||
"""
|
||||
Klickt den 'Code senden'-Button mit robusten Selektoren.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Kurze Pause vor dem Klicken
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
|
||||
# Robuste Send-Code-Button-Selektoren
|
||||
send_code_selectors = [
|
||||
"button[data-e2e='send-code-button']",
|
||||
"button:has-text('Code senden')",
|
||||
"button:has-text('Send code')",
|
||||
"button[type='submit']",
|
||||
"button.css-10nhlj9-Button-StyledButton",
|
||||
self.selectors.SEND_CODE_BUTTON
|
||||
]
|
||||
|
||||
for i, selector in enumerate(send_code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Prüfe, ob Button enabled ist
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
||||
if element:
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
if is_disabled:
|
||||
logger.debug(f"Send-Code-Button {i+1} ist disabled, versuche nächsten")
|
||||
continue
|
||||
|
||||
success = self.automation.browser.click_element(selector)
|
||||
if success:
|
||||
logger.info(f"'Code senden'-Button erfolgreich geklickt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Send-Code-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Button-Matching
|
||||
success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Code senden", "Send code", "Senden"],
|
||||
send_code_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("'Code senden'-Button über Fuzzy-Matching geklickt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte 'Code senden'-Button mit keinem Selektor klicken")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken des 'Code senden'-Buttons: {e}")
|
||||
return False
|
||||
|
||||
def _handle_email_verification(self, email: str) -> bool:
|
||||
"""
|
||||
Behandelt die E-Mail-Verifizierung mit verbessertem Timing.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("Warte auf E-Mail-Verifizierungscode...")
|
||||
|
||||
# Warte auf den Code mit exponential backoff
|
||||
verification_code = self._get_email_verification_code_with_retry(email)
|
||||
|
||||
if not verification_code:
|
||||
logger.error("Konnte keinen Verifizierungscode empfangen")
|
||||
return False
|
||||
|
||||
logger.info(f"Verifizierungscode empfangen: {verification_code}")
|
||||
|
||||
# Code-Feld ausfüllen
|
||||
if not self._fill_verification_code_field(verification_code):
|
||||
logger.error("Konnte Verifizierungscode-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
logger.info("Verifizierungscode erfolgreich eingegeben")
|
||||
|
||||
# Kurze Pause nach Code-Eingabe
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der E-Mail-Verifizierung: {e}")
|
||||
return False
|
||||
|
||||
def _get_email_verification_code_with_retry(self, email: str, max_attempts: int = 30) -> Optional[str]:
|
||||
"""
|
||||
Ruft den E-Mail-Verifizierungscode mit Retry-Logik ab.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
max_attempts: Maximale Anzahl Versuche
|
||||
|
||||
Returns:
|
||||
Optional[str]: Verifizierungscode oder None
|
||||
"""
|
||||
try:
|
||||
for attempt in range(max_attempts):
|
||||
# Exponential backoff: 2s, 3s, 4.5s, 6.75s, ... (max 30s)
|
||||
delay = min(2 * (1.5 ** attempt), 30)
|
||||
|
||||
logger.debug(f"E-Mail-Abruf Versuch {attempt + 1}/{max_attempts} (Wartezeit: {delay:.1f}s)")
|
||||
|
||||
# Versuche Code abzurufen
|
||||
code = self.automation.email_handler.get_verification_code(
|
||||
target_email=email,
|
||||
platform="tiktok",
|
||||
max_attempts=1, # Nur ein Versuch pro Iteration
|
||||
delay_seconds=1
|
||||
)
|
||||
|
||||
if code:
|
||||
logger.info(f"E-Mail-Code nach {attempt + 1} Versuchen empfangen")
|
||||
return code
|
||||
|
||||
# Warte vor nächstem Versuch
|
||||
time.sleep(delay)
|
||||
|
||||
logger.warning(f"Kein E-Mail-Code nach {max_attempts} Versuchen empfangen")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim E-Mail-Code-Abruf: {e}")
|
||||
return None
|
||||
|
||||
def _fill_verification_code_field(self, code: str) -> bool:
|
||||
"""
|
||||
Füllt das Verifizierungscode-Feld aus.
|
||||
|
||||
Args:
|
||||
code: Verifizierungscode
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste Verifizierungscode-Feld-Selektoren
|
||||
code_selectors = [
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[placeholder*='verification code']",
|
||||
"input[placeholder*='Code']",
|
||||
"input[name='verificationCode']",
|
||||
"input[type='text'][maxlength='6']",
|
||||
self.selectors.VERIFICATION_CODE_FIELD,
|
||||
self.selectors.VERIFICATION_CODE_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
# Normale Code-Eingabe (Dummy-Trick wird separat angewendet)
|
||||
success = self.automation.browser.fill_form_field(selector, code, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"Verifizierungscode-Feld erfolgreich ausgefüllt mit Selektor {i+1}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Code-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Gib den sechsstelligen Code ein", "Enter verification code", "Verification code"],
|
||||
code,
|
||||
code_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Verifizierungscode-Feld über Fuzzy-Matching ausgefüllt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte Verifizierungscode-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Verifizierungscode-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _execute_phone_workflow(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Führt den Telefon-Registrierungsworkflow aus.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("=== STARTE TELEFON-WORKFLOW ===")
|
||||
|
||||
# Telefonnummer aufbereiten
|
||||
phone_number = account_data["phone"]
|
||||
if phone_number.startswith("+"):
|
||||
parts = phone_number.split(" ", 1)
|
||||
if len(parts) > 1:
|
||||
phone_number = parts[1]
|
||||
|
||||
# Telefonnummer eingeben
|
||||
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
|
||||
if not self._click_send_code_button():
|
||||
return False
|
||||
|
||||
# SMS-Code behandeln (Platzhalter)
|
||||
logger.warning("SMS-Verifizierung ist noch nicht vollständig implementiert")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im Telefon-Workflow: {e}")
|
||||
return False
|
||||
|
||||
# Hilfsmethoden für die Basis-Funktionalität
|
||||
def _validate_registration_inputs(self, full_name: str, age: int,
|
||||
registration_method: str, phone_number: str) -> bool:
|
||||
"""Validiert die Eingaben für die Registrierung."""
|
||||
if not full_name or len(full_name) < 3:
|
||||
logger.error("Ungültiger vollständiger Name")
|
||||
return False
|
||||
|
||||
if age < 13:
|
||||
logger.error("Benutzer muss mindestens 13 Jahre alt sein")
|
||||
return False
|
||||
|
||||
if registration_method not in ["email", "phone"]:
|
||||
logger.error(f"Ungültige Registrierungsmethode: {registration_method}")
|
||||
return False
|
||||
|
||||
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."""
|
||||
# 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 _create_error_result(self, error_msg: str, stage: str, account_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Erstellt ein standardisiertes Fehler-Result."""
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": stage,
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# Platzhalter für weitere Methoden (Navigation, etc.)
|
||||
def _navigate_to_homepage(self) -> bool:
|
||||
"""Navigiert zur TikTok-Startseite."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""Behandelt den Cookie-Banner."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_login_button(self) -> bool:
|
||||
"""Klickt auf den Anmelden-Button."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_register_link(self) -> bool:
|
||||
"""Klickt auf den Registrieren-Link."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_phone_email_option(self) -> bool:
|
||||
"""Klickt auf die Telefon/E-Mail-Option."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _select_registration_method(self, method: str) -> bool:
|
||||
"""Wählt die Registrierungsmethode aus."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _enter_birthday(self, birthday: Dict[str, Any]) -> bool:
|
||||
"""Gibt das Geburtsdatum ein."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _create_username(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""Erstellt einen Benutzernamen."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _check_registration_success(self) -> bool:
|
||||
"""Überprüft, ob die Registrierung erfolgreich war."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _apply_dummy_input_trick(self) -> bool:
|
||||
"""
|
||||
Wendet den Dummy-Input-Trick auf das Code-Feld an.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.debug("Wende Dummy-Input-Trick an")
|
||||
|
||||
# Code-Feld-Selektoren
|
||||
code_selectors = [
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[placeholder*='verification code']",
|
||||
"input[placeholder*='Code']",
|
||||
"input[name='verificationCode']",
|
||||
"input[type='text'][maxlength='6']",
|
||||
self.selectors.VERIFICATION_CODE_FIELD,
|
||||
self.selectors.VERIFICATION_CODE_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Dummy-Input-Trick anwenden
|
||||
success = self.automation.browser.fill_form_field_with_dummy_trick(
|
||||
selector, "123456", timeout=3000
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"Dummy-Input-Trick erfolgreich angewendet mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Dummy-Input-Trick Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
logger.warning("Dummy-Input-Trick konnte nicht angewendet werden")
|
||||
return True # Nicht kritisch - fortfahren
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Kritischer Fehler beim Dummy-Input-Trick: {e}")
|
||||
return True # Nicht kritisch - fortfahren
|
||||
|
||||
def _click_continue_button(self) -> bool:
|
||||
"""
|
||||
Klickt den Weiter/Continue-Button mit robusten Selektoren.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.debug("Klicke Weiter-Button")
|
||||
|
||||
# Robuste Continue-Button-Selektoren
|
||||
continue_selectors = [
|
||||
"button[data-e2e='continue-button']",
|
||||
"button:has-text('Weiter')",
|
||||
"button:has-text('Continue')",
|
||||
"button:has-text('Fortfahren')",
|
||||
"button[type='submit']",
|
||||
"button.css-10nhlj9-Button-StyledButton:not([disabled])"
|
||||
]
|
||||
|
||||
for i, selector in enumerate(continue_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
# Prüfe, ob Button enabled ist
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
||||
if element:
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
aria_disabled = element.get_attribute("aria-disabled")
|
||||
|
||||
if is_disabled or aria_disabled == "true":
|
||||
logger.debug(f"Continue-Button {i+1} ist disabled, versuche nächsten")
|
||||
continue
|
||||
|
||||
# Button klicken
|
||||
success = self.automation.browser.click_element(selector)
|
||||
if success:
|
||||
logger.info(f"Weiter-Button erfolgreich geklickt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Continue-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Button-Matching
|
||||
try:
|
||||
success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Weiter", "Continue", "Fortfahren", "Next"],
|
||||
continue_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Weiter-Button über Fuzzy-Matching geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Continue Fuzzy-Matching fehlgeschlagen: {e}")
|
||||
|
||||
logger.error("Weiter-Button konnte nicht geklickt werden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Kritischer Fehler beim Weiter-Button: {e}")
|
||||
return False
|
||||
225
social_networks/tiktok/tiktok_selectors.py
Normale Datei
225
social_networks/tiktok/tiktok_selectors.py
Normale Datei
@ -0,0 +1,225 @@
|
||||
"""
|
||||
TikTok-Selektoren - CSS-Selektoren und XPath-Ausdrücke für die TikTok-Automatisierung
|
||||
Mit Text-Matching-Funktionen für robuste Element-Erkennung
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
class TikTokSelectors:
|
||||
"""
|
||||
Zentrale Sammlung aller Selektoren für die TikTok-Automatisierung.
|
||||
Bei Änderungen der TikTok-Webseite müssen nur hier Anpassungen vorgenommen werden.
|
||||
Enthält auch Fuzzy-Text-Matching-Daten für robustere Element-Erkennung.
|
||||
"""
|
||||
|
||||
# URL-Konstanten
|
||||
BASE_URL = "https://www.tiktok.com"
|
||||
SIGNUP_URL = "https://www.tiktok.com/signup"
|
||||
LOGIN_URL = "https://www.tiktok.com/login"
|
||||
EXPLORE_URL = "https://www.tiktok.com/explore"
|
||||
|
||||
# Anmelden/Registrieren-Buttons Hauptseite
|
||||
LOGIN_BUTTON = "button#header-login-button"
|
||||
LOGIN_BUTTON_HEADER = "button#header-login-button"
|
||||
LOGIN_BUTTON_CLASS = "button.TUXButton:has-text('Anmelden')"
|
||||
LOGIN_BUTTON_LEFT = "button#header-login-button"
|
||||
LOGIN_BUTTON_RIGHT = "button#top-right-action-bar-login-button"
|
||||
LOGIN_BUTTON_TOP = "button#header-login-button"
|
||||
LOGIN_BUTTON_TOP_RIGHT = "button#top-right-action-bar-login-button"
|
||||
LOGIN_BUTTON_SIDEBAR = "button[data-e2e='login-button-sidebar']"
|
||||
LOGIN_BUTTON_FALLBACK = "//button[contains(text(), 'Anmelden')]"
|
||||
SIGNUP_LINK = "span[data-e2e='bottom-sign-up']"
|
||||
SIGNUP_LINK_FALLBACK = "a[href*='/signup']"
|
||||
|
||||
# Login-Dialog Optionen
|
||||
LOGIN_DIALOG = "div[role='dialog']"
|
||||
LOGIN_EMAIL_PHONE_OPTION = "div[data-e2e='channel-item']"
|
||||
LOGIN_EMAIL_USERNAME_LINK = "a[href='/login/phone-or-email/email']"
|
||||
|
||||
# Cookie-Dialog
|
||||
COOKIE_DIALOG = "div[role='dialog'][data-testid='cookie-banner']"
|
||||
COOKIE_ACCEPT_BUTTON = "button[data-testid='accept-all-cookies']"
|
||||
|
||||
# Registrierungsdialog - Methoden
|
||||
REGISTER_DIALOG_TITLE = "h1:contains('Registrieren')"
|
||||
REGISTER_LINK = "a:contains('Registrieren')"
|
||||
REGISTER_LINK_FALLBACK = "//a[contains(text(), 'Registrieren')]"
|
||||
REGISTRATION_DIALOG = "div[role='dialog']"
|
||||
PHONE_EMAIL_BUTTON = "div[data-e2e='channel-item']"
|
||||
PHONE_EMAIL_OPTION = "div[data-e2e='channel-item']"
|
||||
PHONE_EMAIL_OPTION_FALLBACK = "//div[contains(text(), 'Telefonnummer oder E-Mail')]"
|
||||
EMAIL_OPTION = "a[href*='/signup/phone-or-email/email']"
|
||||
EMAIL_OPTION_FALLBACK = "//a[contains(text(), 'E-Mail')]"
|
||||
PHONE_OPTION = "a[href*='/signup/phone-or-email/phone']"
|
||||
PHONE_OPTION_FALLBACK = "//a[contains(text(), 'Telefon')]"
|
||||
REGISTER_WITH_EMAIL = "a[href*='/signup/phone-or-email/email']"
|
||||
REGISTER_WITH_PHONE = "a[href*='/signup/phone-or-email/phone']"
|
||||
|
||||
# Geburtsdatum-Selektoren
|
||||
BIRTHDAY_MONTH_DROPDOWN = "select[name='month']"
|
||||
BIRTHDAY_DAY_DROPDOWN = "select[name='day']"
|
||||
BIRTHDAY_YEAR_DROPDOWN = "select[name='year']"
|
||||
BIRTHDAY_MONTH_SELECT = "div.css-1leicpq-DivSelectLabel:contains('Monat')"
|
||||
BIRTHDAY_DAY_SELECT = "div.css-1leicpq-DivSelectLabel:contains('Tag')"
|
||||
BIRTHDAY_YEAR_SELECT = "div.css-1leicpq-DivSelectLabel:contains('Jahr')"
|
||||
BIRTHDAY_DROPDOWN_OPTION = "div[role='option']"
|
||||
BIRTHDAY_DROPDOWN_CONTAINER = "div.css-1leicpq-DivSelectLabel"
|
||||
BIRTHDAY_ARROW = "svg.css-gz151e-StyledArrowTriangleDownLargeFill"
|
||||
|
||||
# Formularfelder - E-Mail-Registrierung
|
||||
EMAIL_FIELD = "input[placeholder='E-Mail-Adresse']"
|
||||
EMAIL_FIELD_ALT = "input[name='email']"
|
||||
PASSWORD_FIELD = "input[placeholder='Passwort']"
|
||||
PASSWORD_FIELD_ALT = "input[type='password']"
|
||||
VERIFICATION_CODE_FIELD = "input[placeholder*='sechsstelligen Code']"
|
||||
VERIFICATION_CODE_FIELD_ALT = "input[placeholder='Gib den sechsstelligen Code ein']"
|
||||
USERNAME_FIELD = "input[placeholder='Benutzername']"
|
||||
USERNAME_FIELD_ALT = "input[name='new-username']"
|
||||
|
||||
# Formularfelder - Telefon-Registrierung
|
||||
COUNTRY_CODE_SELECT = "div[role='combobox']"
|
||||
PHONE_FIELD = "input[placeholder='Telefonnummer']"
|
||||
|
||||
# Buttons
|
||||
SEND_CODE_BUTTON = "button[data-e2e='send-code-button']"
|
||||
RESEND_CODE_BUTTON = "button:contains('Code erneut senden')"
|
||||
CONTINUE_BUTTON = "button[type='submit']"
|
||||
CONTINUE_BUTTON_ALT = "button.e1w6iovg0"
|
||||
REGISTER_BUTTON = "button:contains('Registrieren')"
|
||||
REGISTER_BUTTON_ALT = "button[type='submit']:contains('Registrieren')"
|
||||
SKIP_BUTTON = "div:contains('Überspringen')"
|
||||
SKIP_BUTTON_ALT = "div.css-4y1w75-DivTextContainer"
|
||||
SKIP_USERNAME_BUTTON = "button:contains('Überspringen')"
|
||||
|
||||
# Checkbox
|
||||
NEWSLETTER_CHECKBOX = "input[type='checkbox']"
|
||||
|
||||
# Login-Formularfelder
|
||||
LOGIN_EMAIL_FIELD = "input[name='username'][placeholder='E-Mail-Adresse oder Benutzername']"
|
||||
LOGIN_EMAIL_FIELD_ALT = "input.tiktok-11to27l-InputContainer[name='username']"
|
||||
LOGIN_PASSWORD_FIELD = "input[type='password'][placeholder='Passwort']"
|
||||
LOGIN_PASSWORD_FIELD_ALT = "input.tiktok-wv3bkt-InputContainer[type='password']"
|
||||
LOGIN_SUBMIT_BUTTON = "button[type='submit'][data-e2e='login-button']"
|
||||
LOGIN_SUBMIT_BUTTON_ALT = "button.tiktok-11sviba-Button-StyledButton[data-e2e='login-button']"
|
||||
|
||||
# Erfolgs-Indikatoren für Registrierung
|
||||
SUCCESS_INDICATORS = [
|
||||
"a[href='/foryou']",
|
||||
"a[href='/explore']",
|
||||
"button[data-e2e='profile-icon']",
|
||||
"svg[data-e2e='profile-icon']"
|
||||
]
|
||||
|
||||
# Links für Nutzungsbedingungen und Datenschutz
|
||||
TERMS_LINK = "a:contains('Nutzungsbedingungen')"
|
||||
PRIVACY_LINK = "a:contains('Datenschutzerklärung')"
|
||||
|
||||
# Login-Fehler
|
||||
ERROR_MESSAGE = "span.error-message"
|
||||
LOGIN_ERROR_CONTAINER = "div[class*='error']"
|
||||
|
||||
# Text-Matching-Parameter für Fuzzy-Matching
|
||||
TEXT_MATCH = {
|
||||
# Formularfelder
|
||||
"form_fields": {
|
||||
"email": ["E-Mail-Adresse", "E-Mail", "Email", "Mail"],
|
||||
"phone": ["Telefonnummer", "Telefon", "Phone", "Mobile"],
|
||||
"password": ["Passwort", "Password"],
|
||||
"verification_code": ["Bestätigungscode", "Code", "Verifizierungscode", "Sicherheitscode"],
|
||||
"username": ["Benutzername", "Username", "Name"]
|
||||
},
|
||||
|
||||
# Buttons
|
||||
"buttons": {
|
||||
"send_code": ["Code senden", "Senden", "Send code", "Verification code", "Send"],
|
||||
"continue": ["Weiter", "Continue", "Next", "Fortfahren"],
|
||||
"register": ["Registrieren", "Register", "Sign up", "Konto erstellen"],
|
||||
"skip": ["Überspringen", "Skip", "Later", "Später", "Nicht jetzt"],
|
||||
},
|
||||
|
||||
# Fehler-Indikatoren
|
||||
"error_indicators": [
|
||||
"Fehler", "Error", "Leider", "Ungültig", "Invalid", "Nicht verfügbar",
|
||||
"Fehlgeschlagen", "Problem", "Failed", "Nicht möglich", "Bereits verwendet",
|
||||
"Too many attempts", "Zu viele Versuche", "Rate limit", "Bitte warte"
|
||||
],
|
||||
|
||||
# Bestätigungscode-Texte in E-Mails
|
||||
"email_verification_patterns": [
|
||||
"ist dein Bestätigungscode",
|
||||
"ist dein TikTok-Code",
|
||||
"is your TikTok code",
|
||||
"is your verification code",
|
||||
"Dein Bestätigungscode lautet",
|
||||
"Your verification code is"
|
||||
],
|
||||
|
||||
# E-Mail-Betreff-Muster für TikTok
|
||||
"email_subject_patterns": [
|
||||
"ist dein Bestätigungscode",
|
||||
"is your confirmation code",
|
||||
"TikTok verification code",
|
||||
"TikTok Bestätigungscode"
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_field_labels(cls, field_type: str) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Bezeichnungen für ein Formularfeld zurück.
|
||||
|
||||
Args:
|
||||
field_type: Typ des Formularfelds (z.B. "email", "phone")
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Bezeichnungen
|
||||
"""
|
||||
return cls.TEXT_MATCH["form_fields"].get(field_type, [])
|
||||
|
||||
@classmethod
|
||||
def get_button_texts(cls, button_type: str) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für einen Button zurück.
|
||||
|
||||
Args:
|
||||
button_type: Typ des Buttons (z.B. "send_code", "continue")
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Button-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["buttons"].get(button_type, [])
|
||||
|
||||
@classmethod
|
||||
def get_error_indicators(cls) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für Fehlerindikatoren zurück.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Fehlerindikator-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["error_indicators"]
|
||||
|
||||
@classmethod
|
||||
def get_email_verification_patterns(cls) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für Bestätigungscodes in E-Mails zurück.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen E-Mail-Bestätigungscode-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["email_verification_patterns"]
|
||||
|
||||
@classmethod
|
||||
def get_month_option_selector(cls, month: int) -> str:
|
||||
"""Returns selector for month option."""
|
||||
return f"option[value='{month}']"
|
||||
|
||||
@classmethod
|
||||
def get_day_option_selector(cls, day: int) -> str:
|
||||
"""Returns selector for day option."""
|
||||
return f"option[value='{day}']"
|
||||
|
||||
@classmethod
|
||||
def get_year_option_selector(cls, year: int) -> str:
|
||||
"""Returns selector for year option."""
|
||||
return f"option[value='{year}']"
|
||||
520
social_networks/tiktok/tiktok_ui_helper.py
Normale Datei
520
social_networks/tiktok/tiktok_ui_helper.py
Normale Datei
@ -0,0 +1,520 @@
|
||||
"""
|
||||
TikTok-UI-Helper - Hilfsmethoden für die Interaktion mit der TikTok-UI
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union, Callable
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_ui_helper")
|
||||
|
||||
class TikTokUIHelper:
|
||||
"""
|
||||
Hilfsmethoden für die Interaktion mit der TikTok-Benutzeroberfläche.
|
||||
Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert den TikTok-UI-Helper.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = TikTokSelectors()
|
||||
|
||||
# Initialisiere TextSimilarity für Fuzzy-Matching
|
||||
self.text_similarity = TextSimilarity(default_threshold=0.7)
|
||||
|
||||
logger.debug("TikTok-UI-Helper initialisiert")
|
||||
|
||||
def _ensure_browser(self) -> bool:
|
||||
"""
|
||||
Stellt sicher, dass die Browser-Referenz verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Browser verfügbar, False sonst
|
||||
"""
|
||||
if self.automation.browser is None:
|
||||
logger.error("Browser-Referenz nicht verfügbar")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def fill_field_fuzzy(self, field_labels: Union[str, List[str]],
|
||||
value: str, fallback_selector: str = None,
|
||||
threshold: float = 0.7, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Füllt ein Formularfeld mit Fuzzy-Text-Matching aus.
|
||||
|
||||
Args:
|
||||
field_labels: Bezeichner oder Liste von Bezeichnern des Feldes
|
||||
value: Einzugebender Wert
|
||||
fallback_selector: CSS-Selektor für Fallback
|
||||
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
||||
timeout: Zeitlimit für die Suche in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Normalisiere field_labels zu einer Liste
|
||||
if isinstance(field_labels, str):
|
||||
field_labels = [field_labels]
|
||||
|
||||
# Versuche, das Feld mit Fuzzy-Matching zu finden
|
||||
element = fuzzy_find_element(
|
||||
self.automation.browser.page,
|
||||
field_labels,
|
||||
selector_type="input",
|
||||
threshold=threshold,
|
||||
wait_time=timeout
|
||||
)
|
||||
|
||||
if element:
|
||||
# Versuche, das Feld zu fokussieren und den Wert einzugeben
|
||||
element.focus()
|
||||
time.sleep(0.1)
|
||||
element.fill("") # Leere das Feld zuerst
|
||||
time.sleep(0.2)
|
||||
|
||||
# Text menschenähnlich eingeben
|
||||
for char in value:
|
||||
element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000)
|
||||
time.sleep(0.01)
|
||||
|
||||
logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}")
|
||||
return True
|
||||
|
||||
# Fuzzy-Matching fehlgeschlagen, versuche über Attribute
|
||||
if fallback_selector:
|
||||
field_success = self.automation.browser.fill_form_field(fallback_selector, value)
|
||||
if field_success:
|
||||
logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}")
|
||||
return True
|
||||
|
||||
# Versuche noch alternative Selektoren basierend auf field_labels
|
||||
for label in field_labels:
|
||||
# Versuche aria-label Attribut
|
||||
aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']"
|
||||
if self.automation.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(aria_selector, value):
|
||||
logger.info(f"Feld über aria-label gefüllt: {label}")
|
||||
return True
|
||||
|
||||
# Versuche placeholder Attribut
|
||||
placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']"
|
||||
if self.automation.browser.is_element_visible(placeholder_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(placeholder_selector, value):
|
||||
logger.info(f"Feld über placeholder gefüllt: {label}")
|
||||
return True
|
||||
|
||||
# Versuche name Attribut
|
||||
name_selector = f"input[name='{label.lower().replace(' ', '')}']"
|
||||
if self.automation.browser.is_element_visible(name_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(name_selector, value):
|
||||
logger.info(f"Feld über name-Attribut gefüllt: {label}")
|
||||
return True
|
||||
|
||||
logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {e}")
|
||||
return False
|
||||
|
||||
def click_button_fuzzy(self, button_texts: Union[str, List[str]],
|
||||
fallback_selector: str = None, threshold: float = 0.7,
|
||||
timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Klickt einen Button mit Fuzzy-Text-Matching.
|
||||
|
||||
Args:
|
||||
button_texts: Text oder Liste von Texten des Buttons
|
||||
fallback_selector: CSS-Selektor für Fallback
|
||||
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
||||
timeout: Zeitlimit für die Suche in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Normalisiere button_texts zu einer Liste
|
||||
if isinstance(button_texts, str):
|
||||
button_texts = [button_texts]
|
||||
|
||||
# Logging der Suche
|
||||
logger.info(f"Suche nach Button mit Texten: {button_texts}")
|
||||
|
||||
if not button_texts or button_texts == [[]]:
|
||||
logger.warning("Leere Button-Text-Liste angegeben!")
|
||||
return False
|
||||
|
||||
# TikTok-spezifische Selektoren zuerst prüfen
|
||||
# Diese Selektoren sind häufig in TikTok's UI zu finden
|
||||
tiktok_button_selectors = [
|
||||
"button[type='submit']",
|
||||
"button[data-e2e='send-code-button']",
|
||||
"button.e1w6iovg0",
|
||||
"button.css-10nhlj9-Button-StyledButton"
|
||||
]
|
||||
|
||||
for selector in tiktok_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
button_element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
||||
if button_element:
|
||||
button_text = button_element.inner_text().strip()
|
||||
|
||||
# Überprüfe, ob der Button-Text mit einem der gesuchten Texte übereinstimmt
|
||||
for text in button_texts:
|
||||
if self.text_similarity.is_similar(text, button_text, threshold=threshold):
|
||||
logger.info(f"Button mit passendem Text gefunden: '{button_text}'")
|
||||
button_element.click()
|
||||
return True
|
||||
|
||||
# Die allgemeine fuzzy_click_button-Funktion verwenden
|
||||
result = click_fuzzy_button(
|
||||
self.automation.browser.page,
|
||||
button_texts,
|
||||
threshold=threshold,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info(f"Button mit Fuzzy-Matching geklickt")
|
||||
return True
|
||||
|
||||
# Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector
|
||||
if fallback_selector:
|
||||
logger.info(f"Versuche Fallback-Selektor: {fallback_selector}")
|
||||
if self.automation.browser.click_element(fallback_selector):
|
||||
logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}")
|
||||
return True
|
||||
|
||||
# Versuche alternative Methoden
|
||||
|
||||
# 1. Versuche über aria-label
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']"
|
||||
if self.automation.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.automation.browser.click_element(aria_selector):
|
||||
logger.info(f"Button über aria-label geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 2. Versuche über role='button' mit Text
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
xpath_selector = f"//div[@role='button' and contains(., '{text}')]"
|
||||
if self.automation.browser.is_element_visible(xpath_selector, timeout=1000):
|
||||
if self.automation.browser.click_element(xpath_selector):
|
||||
logger.info(f"Button über role+text geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 3. Versuche über Link-Text
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
link_selector = f"//a[contains(text(), '{text}')]"
|
||||
if self.automation.browser.is_element_visible(link_selector, timeout=1000):
|
||||
if self.automation.browser.click_element(link_selector):
|
||||
logger.info(f"Link mit passendem Text geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 4. Als letzten Versuch, klicke auf einen beliebigen Button
|
||||
logger.warning("Kein spezifischer Button gefunden, versuche beliebigen Button zu klicken")
|
||||
buttons = self.automation.browser.page.query_selector_all("button")
|
||||
if buttons and len(buttons) > 0:
|
||||
for button in buttons:
|
||||
visible = button.is_visible()
|
||||
if visible:
|
||||
logger.info("Klicke auf beliebigen sichtbaren Button")
|
||||
button.click()
|
||||
return True
|
||||
|
||||
logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}")
|
||||
return False
|
||||
|
||||
def select_dropdown_option(self, dropdown_selector: str, option_value: str,
|
||||
option_type: str = "text", timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Wählt eine Option aus einer Dropdown-Liste aus.
|
||||
|
||||
Args:
|
||||
dropdown_selector: Selektor für das Dropdown-Element
|
||||
option_value: Wert oder Text der auszuwählenden Option
|
||||
option_type: "text" für Text-Matching, "value" für Wert-Matching
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Auf Dropdown-Element klicken, um die Optionen anzuzeigen
|
||||
dropdown_element = self.automation.browser.wait_for_selector(dropdown_selector, timeout=timeout)
|
||||
if not dropdown_element:
|
||||
logger.warning(f"Dropdown-Element nicht gefunden: {dropdown_selector}")
|
||||
return False
|
||||
|
||||
# Dropdown öffnen
|
||||
dropdown_element.click()
|
||||
time.sleep(0.5) # Kurz warten, damit die Optionen angezeigt werden
|
||||
|
||||
# Optionen suchen
|
||||
option_selector = "div[role='option']"
|
||||
options = self.automation.browser.page.query_selector_all(option_selector)
|
||||
|
||||
if not options or len(options) == 0:
|
||||
logger.warning(f"Keine Optionen gefunden für Dropdown: {dropdown_selector}")
|
||||
return False
|
||||
|
||||
# Option nach Text oder Wert suchen
|
||||
selected = False
|
||||
for option in options:
|
||||
option_text = option.inner_text().strip()
|
||||
|
||||
if option_type == "text":
|
||||
if option_text == option_value or self.text_similarity.is_similar(option_text, option_value, threshold=0.9):
|
||||
option.click()
|
||||
selected = True
|
||||
break
|
||||
elif option_type == "value":
|
||||
option_val = option.get_attribute("value") or ""
|
||||
if option_val == option_value:
|
||||
option.click()
|
||||
selected = True
|
||||
break
|
||||
|
||||
if not selected:
|
||||
logger.warning(f"Keine passende Option für '{option_value}' gefunden")
|
||||
return False
|
||||
|
||||
logger.info(f"Option '{option_value}' im Dropdown ausgewählt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Auswahl der Dropdown-Option: {e}")
|
||||
return False
|
||||
|
||||
def check_for_error(self, error_selectors: List[str] = None,
|
||||
error_texts: List[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Überprüft, ob Fehlermeldungen angezeigt werden.
|
||||
|
||||
Args:
|
||||
error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen
|
||||
error_texts: Liste mit typischen Fehlertexten
|
||||
|
||||
Returns:
|
||||
Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Standardselektoren verwenden, wenn keine angegeben sind
|
||||
if error_selectors is None:
|
||||
error_selectors = [
|
||||
"div[role='alert']",
|
||||
"p[class*='error']",
|
||||
"span[class*='error']",
|
||||
".error-message"
|
||||
]
|
||||
|
||||
# Standardfehlertexte verwenden, wenn keine angegeben sind
|
||||
if error_texts is None:
|
||||
error_texts = TikTokSelectors.get_error_indicators()
|
||||
|
||||
# 1. Nach Fehlerselektoren suchen
|
||||
for selector in error_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
error_text = element.text_content()
|
||||
if error_text and len(error_text.strip()) > 0:
|
||||
logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}")
|
||||
return error_text.strip()
|
||||
|
||||
# 2. Alle Texte auf der Seite durchsuchen
|
||||
page_content = self.automation.browser.page.content()
|
||||
|
||||
for error_text in error_texts:
|
||||
if error_text.lower() in page_content.lower():
|
||||
# Versuche, den genauen Fehlertext zu extrahieren
|
||||
matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower())
|
||||
if matches:
|
||||
full_error = matches[0].strip()
|
||||
logger.info(f"Fehlermeldung gefunden (Text): {full_error}")
|
||||
return full_error
|
||||
else:
|
||||
logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}")
|
||||
return error_text
|
||||
|
||||
# 3. Nach weiteren Fehlerelementen suchen
|
||||
elements = self.automation.browser.page.query_selector_all("p, div, span")
|
||||
|
||||
for element in elements:
|
||||
element_text = element.inner_text()
|
||||
if not element_text:
|
||||
continue
|
||||
|
||||
element_text = element_text.strip()
|
||||
|
||||
# Prüfe Textähnlichkeit mit Fehlertexten
|
||||
for error_text in error_texts:
|
||||
if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \
|
||||
self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7):
|
||||
logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}")
|
||||
return element_text
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}")
|
||||
return None
|
||||
|
||||
def check_for_captcha(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob ein Captcha angezeigt wird.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Captcha erkannt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Selektoren für Captcha-Erkennung
|
||||
captcha_selectors = [
|
||||
"div[data-testid='captcha']",
|
||||
"iframe[src*='captcha']",
|
||||
"iframe[title*='captcha']",
|
||||
"iframe[title*='reCAPTCHA']"
|
||||
]
|
||||
|
||||
# Captcha-Texte für textbasierte Erkennung
|
||||
captcha_texts = [
|
||||
"captcha", "recaptcha", "sicherheitsüberprüfung", "security check",
|
||||
"i'm not a robot", "ich bin kein roboter", "verify you're human",
|
||||
"bestätige, dass du ein mensch bist"
|
||||
]
|
||||
|
||||
# Nach Selektoren suchen
|
||||
for selector in captcha_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.warning(f"Captcha erkannt (Selektor): {selector}")
|
||||
return True
|
||||
|
||||
# Nach Texten suchen
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for text in captcha_texts:
|
||||
if text in page_content:
|
||||
logger.warning(f"Captcha erkannt (Text): {text}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Captcha-Erkennung: {e}")
|
||||
return False
|
||||
|
||||
def wait_for_element(self, selectors: Union[str, List[str]],
|
||||
timeout: int = 10000, check_interval: int = 500) -> Optional[Any]:
|
||||
"""
|
||||
Wartet auf das Erscheinen eines Elements.
|
||||
|
||||
Args:
|
||||
selectors: CSS-Selektor oder Liste von Selektoren
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
||||
|
||||
Returns:
|
||||
Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Normalisiere selectors zu einer Liste
|
||||
if isinstance(selectors, str):
|
||||
selectors = [selectors]
|
||||
|
||||
start_time = time.time()
|
||||
end_time = start_time + (timeout / 1000)
|
||||
|
||||
while time.time() < end_time:
|
||||
for selector in selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=check_interval)
|
||||
if element:
|
||||
logger.info(f"Element mit Selektor '{selector}' gefunden")
|
||||
return element
|
||||
|
||||
# Kurze Pause vor der nächsten Prüfung
|
||||
time.sleep(check_interval / 1000)
|
||||
|
||||
logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Warten auf Element: {e}")
|
||||
return None
|
||||
|
||||
def is_registration_successful(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob die Registrierung erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich, False sonst
|
||||
"""
|
||||
try:
|
||||
# Erfolgsindikatoren überprüfen
|
||||
success_indicators = TikTokSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for selector in success_indicators:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Registrierung erfolgreich (Indikator gefunden: {selector})")
|
||||
return True
|
||||
|
||||
# URL überprüfen
|
||||
current_url = self.automation.browser.page.url
|
||||
if "/foryou" in current_url or "tiktok.com/explore" in current_url:
|
||||
logger.info("Registrierung erfolgreich (Erfolgreiche Navigation erkannt)")
|
||||
return True
|
||||
|
||||
# Überprüfen, ob Fehler angezeigt werden
|
||||
error_message = self.check_for_error()
|
||||
if error_message:
|
||||
logger.warning(f"Registrierung nicht erfolgreich: {error_message}")
|
||||
return False
|
||||
|
||||
logger.warning("Konnte Registrierungserfolg nicht bestätigen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung des Registrierungserfolgs: {e}")
|
||||
return False
|
||||
492
social_networks/tiktok/tiktok_utils.py
Normale Datei
492
social_networks/tiktok/tiktok_utils.py
Normale Datei
@ -0,0 +1,492 @@
|
||||
"""
|
||||
TikTok-Utils - Hilfsfunktionen für die TikTok-Automatisierung.
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_utils")
|
||||
|
||||
class TikTokUtils:
|
||||
"""
|
||||
Hilfsfunktionen für die TikTok-Automatisierung.
|
||||
Enthält allgemeine Hilfsmethoden und kleinere Funktionen.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Utils.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = TikTokSelectors()
|
||||
|
||||
logger.debug("TikTok-Utils initialisiert")
|
||||
|
||||
def _ensure_browser(self) -> bool:
|
||||
"""
|
||||
Stellt sicher, dass die Browser-Referenz verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Browser verfügbar, False sonst
|
||||
"""
|
||||
if self.automation.browser is None:
|
||||
logger.error("Browser-Referenz nicht verfügbar")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Cookie-Dialoge in TikTok prüfen
|
||||
cookie_selectors = [
|
||||
"button[data-e2e='cookie-banner-reject']",
|
||||
"button:contains('Ablehnen')",
|
||||
"button:contains('Nur erforderliche')",
|
||||
"button:contains('Reject')",
|
||||
"button[data-e2e='cookie-banner-accept']"
|
||||
]
|
||||
|
||||
for selector in cookie_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Cookie-Banner erkannt: {selector}")
|
||||
|
||||
# Versuche, den Ablehnen-Button zu klicken
|
||||
if "reject" in selector.lower() or "ablehnen" in selector.lower() or "erforderliche" in selector.lower():
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Cookie-Banner erfolgreich abgelehnt")
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
return True
|
||||
|
||||
# Fallback: Akzeptieren-Button klicken, wenn Ablehnen nicht funktioniert
|
||||
else:
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Cookie-Banner erfolgreich akzeptiert")
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
return True
|
||||
|
||||
# Wenn kein Cookie-Banner gefunden wurde
|
||||
logger.debug("Kein Cookie-Banner erkannt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Behandeln des Cookie-Banners: {e}")
|
||||
return False
|
||||
|
||||
def extract_username_from_url(self, url: str) -> Optional[str]:
|
||||
"""
|
||||
Extrahiert den Benutzernamen aus einer TikTok-URL.
|
||||
|
||||
Args:
|
||||
url: Die TikTok-URL
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der extrahierte Benutzername oder None
|
||||
"""
|
||||
try:
|
||||
# Muster für Profil-URLs
|
||||
patterns = [
|
||||
r'tiktok\.com/@([a-zA-Z0-9._]+)/?(?:$|\?|#)',
|
||||
r'tiktok\.com/user/([a-zA-Z0-9._]+)/?',
|
||||
r'tiktok\.com/video/[^/]+/by/([a-zA-Z0-9._]+)/?'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
username = match.group(1)
|
||||
# Einige Ausnahmen filtern
|
||||
if username not in ["explore", "accounts", "video", "foryou", "trending"]:
|
||||
return username
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren des Benutzernamens aus der URL: {e}")
|
||||
return None
|
||||
|
||||
def get_current_username(self) -> Optional[str]:
|
||||
"""
|
||||
Versucht, den Benutzernamen des aktuell angemeldeten Kontos zu ermitteln.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der Benutzername oder None, wenn nicht gefunden
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Verschiedene Methoden zur Erkennung des Benutzernamens
|
||||
|
||||
# 1. Benutzername aus URL des Profils
|
||||
profile_link_selectors = [
|
||||
"a[href*='/@']",
|
||||
"a[href*='/user/']"
|
||||
]
|
||||
|
||||
for selector in profile_link_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
href = element.get_attribute("href")
|
||||
if href:
|
||||
username = self.extract_username_from_url(href)
|
||||
if username:
|
||||
logger.info(f"Benutzername aus Profil-Link ermittelt: {username}")
|
||||
return username
|
||||
|
||||
# 2. Profilicon prüfen auf data-e2e-Attribut
|
||||
profile_icon_selectors = [
|
||||
"button[data-e2e='profile-icon']",
|
||||
"svg[data-e2e='profile-icon']"
|
||||
]
|
||||
|
||||
for selector in profile_icon_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
# Prüfen, ob ein Elternelement möglicherweise ein data-e2e-Attribut mit dem Benutzernamen hat
|
||||
parent = element.evaluate("node => node.parentElement")
|
||||
if parent:
|
||||
data_e2e = parent.get_attribute("data-e2e")
|
||||
if data_e2e and "profile" in data_e2e:
|
||||
username_match = re.search(r'profile-([a-zA-Z0-9._]+)', data_e2e)
|
||||
if username_match:
|
||||
username = username_match.group(1)
|
||||
logger.info(f"Benutzername aus data-e2e-Attribut ermittelt: {username}")
|
||||
return username
|
||||
|
||||
# 3. TikTok-spezifisches Element mit Benutzername suchen
|
||||
username_element = self.automation.browser.wait_for_selector("h1[data-e2e='user-title']", timeout=2000)
|
||||
if username_element:
|
||||
username = username_element.inner_text().strip()
|
||||
if username:
|
||||
logger.info(f"Benutzername aus user-title-Element ermittelt: {username}")
|
||||
return username
|
||||
|
||||
logger.warning("Konnte Benutzernamen nicht ermitteln")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Ermittlung des Benutzernamens: {e}")
|
||||
return None
|
||||
|
||||
def wait_for_navigation(self, expected_url_pattern: str = None,
|
||||
timeout: int = 30000, check_interval: int = 500) -> bool:
|
||||
"""
|
||||
Wartet, bis die Seite zu einer URL mit einem bestimmten Muster navigiert.
|
||||
|
||||
Args:
|
||||
expected_url_pattern: Erwartetes Muster der URL (Regex)
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn die Navigation erfolgreich war, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
end_time = start_time + (timeout / 1000)
|
||||
|
||||
while time.time() < end_time:
|
||||
current_url = self.automation.browser.page.url
|
||||
|
||||
if expected_url_pattern and re.search(expected_url_pattern, current_url):
|
||||
logger.info(f"Navigation zu URL mit Muster '{expected_url_pattern}' erfolgreich")
|
||||
return True
|
||||
|
||||
# Kurze Pause vor der nächsten Prüfung
|
||||
time.sleep(check_interval / 1000)
|
||||
|
||||
logger.warning(f"Zeitüberschreitung bei Navigation zu URL mit Muster '{expected_url_pattern}'")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Warten auf Navigation: {e}")
|
||||
return False
|
||||
|
||||
def handle_dialog_or_popup(self, expected_text: Union[str, List[str]] = None,
|
||||
action: str = "close", timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Behandelt einen Dialog oder Popup.
|
||||
|
||||
Args:
|
||||
expected_text: Erwarteter Text im Dialog oder Liste von Texten
|
||||
action: Aktion ("close", "confirm", "cancel")
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn der Dialog erfolgreich behandelt wurde, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Dialog-Element suchen
|
||||
dialog_selector = "div[role='dialog']"
|
||||
dialog_element = self.automation.browser.wait_for_selector(dialog_selector, timeout=timeout)
|
||||
|
||||
if not dialog_element:
|
||||
logger.debug("Kein Dialog gefunden")
|
||||
return False
|
||||
|
||||
logger.info("Dialog gefunden")
|
||||
|
||||
# Text im Dialog prüfen, falls angegeben
|
||||
if expected_text:
|
||||
if isinstance(expected_text, str):
|
||||
expected_text = [expected_text]
|
||||
|
||||
dialog_text = dialog_element.inner_text()
|
||||
text_found = False
|
||||
|
||||
for text in expected_text:
|
||||
if text in dialog_text:
|
||||
logger.info(f"Erwarteter Text im Dialog gefunden: '{text}'")
|
||||
text_found = True
|
||||
break
|
||||
|
||||
if not text_found:
|
||||
logger.warning(f"Erwarteter Text nicht im Dialog gefunden: {expected_text}")
|
||||
return False
|
||||
|
||||
# Aktion ausführen
|
||||
if action == "close":
|
||||
# Schließen-Button suchen und klicken
|
||||
close_button_selectors = [
|
||||
"button[data-e2e='modal-close']",
|
||||
"svg[data-e2e='modal-close']",
|
||||
"button.css-1afoydx-StyledCloseButton",
|
||||
"div[role='dialog'] button:first-child"
|
||||
]
|
||||
|
||||
for selector in close_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Dialog geschlossen")
|
||||
return True
|
||||
|
||||
# Wenn kein Schließen-Button gefunden wurde, Escape-Taste drücken
|
||||
self.automation.browser.page.keyboard.press("Escape")
|
||||
logger.info("Dialog mit Escape-Taste geschlossen")
|
||||
|
||||
elif action == "confirm":
|
||||
# Bestätigen-Button suchen und klicken
|
||||
confirm_button_selectors = [
|
||||
"button[type='submit']",
|
||||
"button:contains('OK')",
|
||||
"button:contains('Ja')",
|
||||
"button:contains('Yes')",
|
||||
"button:contains('Bestätigen')",
|
||||
"button:contains('Confirm')"
|
||||
]
|
||||
|
||||
for selector in confirm_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Dialog bestätigt")
|
||||
return True
|
||||
|
||||
elif action == "cancel":
|
||||
# Abbrechen-Button suchen und klicken
|
||||
cancel_button_selectors = [
|
||||
"button:contains('Abbrechen')",
|
||||
"button:contains('Cancel')",
|
||||
"button:contains('Nein')",
|
||||
"button:contains('No')"
|
||||
]
|
||||
|
||||
for selector in cancel_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Dialog abgebrochen")
|
||||
return True
|
||||
|
||||
logger.warning(f"Konnte keine {action}-Aktion für den Dialog ausführen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Dialog-Behandlung: {e}")
|
||||
return False
|
||||
|
||||
def handle_rate_limiting(self, rotate_proxy: bool = True) -> bool:
|
||||
"""
|
||||
Behandelt eine Rate-Limiting-Situation.
|
||||
|
||||
Args:
|
||||
rotate_proxy: Ob der Proxy rotiert werden soll
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich behandelt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.warning("Rate-Limiting erkannt, warte und versuche es erneut")
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("rate_limit_detected")
|
||||
|
||||
# Proxy rotieren, falls gewünscht
|
||||
if rotate_proxy and self.automation.use_proxy:
|
||||
success = self.automation._rotate_proxy()
|
||||
if not success:
|
||||
logger.warning("Konnte Proxy nicht rotieren")
|
||||
|
||||
# Längere Wartezeit
|
||||
wait_time = random.uniform(120, 300) # 2-5 Minuten
|
||||
logger.info(f"Warte {wait_time:.1f} Sekunden vor dem nächsten Versuch")
|
||||
time.sleep(wait_time)
|
||||
|
||||
# Seite neuladen
|
||||
self.automation.browser.page.reload()
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Prüfen, ob Rate-Limiting noch aktiv ist
|
||||
rate_limit_texts = [
|
||||
"bitte warte einige minuten",
|
||||
"please wait a few minutes",
|
||||
"try again later",
|
||||
"versuche es später erneut",
|
||||
"zu viele anfragen",
|
||||
"too many requests"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
still_rate_limited = False
|
||||
for text in rate_limit_texts:
|
||||
if text in page_content:
|
||||
still_rate_limited = True
|
||||
break
|
||||
|
||||
if still_rate_limited:
|
||||
logger.warning("Immer noch Rate-Limited nach dem Warten")
|
||||
return False
|
||||
else:
|
||||
logger.info("Rate-Limiting scheint aufgehoben zu sein")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Behandlung des Rate-Limitings: {e}")
|
||||
return False
|
||||
|
||||
def is_logged_in(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob der Benutzer bei TikTok angemeldet ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn angemeldet, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Erfolgsindikatoren überprüfen
|
||||
success_indicators = TikTokSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for selector in success_indicators:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Benutzer ist angemeldet (Indikator: {selector})")
|
||||
return True
|
||||
|
||||
# URL überprüfen
|
||||
current_url = self.automation.browser.page.url
|
||||
if "/foryou" in current_url or "tiktok.com/explore" in current_url:
|
||||
logger.info("Benutzer ist angemeldet (URL-Check)")
|
||||
return True
|
||||
|
||||
# Anmelden-Button prüfen - wenn sichtbar, dann nicht angemeldet
|
||||
login_button_selectors = [
|
||||
TikTokSelectors.LOGIN_BUTTON_LEFT,
|
||||
TikTokSelectors.LOGIN_BUTTON_RIGHT
|
||||
]
|
||||
|
||||
for selector in login_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Benutzer ist nicht angemeldet (Anmelde-Button sichtbar)")
|
||||
return False
|
||||
|
||||
# Profilicon checken - wenn sichtbar, dann angemeldet
|
||||
profile_selectors = [
|
||||
"button[data-e2e='profile-icon']",
|
||||
"svg[data-e2e='profile-icon']"
|
||||
]
|
||||
|
||||
for selector in profile_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Benutzer ist angemeldet (Profilicon sichtbar)")
|
||||
return True
|
||||
|
||||
logger.warning("Konnte Login-Status nicht eindeutig bestimmen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung des Login-Status: {e}")
|
||||
return False
|
||||
|
||||
def extract_verification_code_from_email(self, email_body: str) -> Optional[str]:
|
||||
"""
|
||||
Extrahiert den Verifizierungscode aus einer E-Mail.
|
||||
|
||||
Args:
|
||||
email_body: Der E-Mail-Text
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der Verifizierungscode oder None, wenn nicht gefunden
|
||||
"""
|
||||
try:
|
||||
# Muster für TikTok-Verifizierungscodes
|
||||
patterns = [
|
||||
r'(\d{6}) ist dein Bestätigungscode',
|
||||
r'(\d{6}) ist dein TikTok-Code',
|
||||
r'(\d{6}) is your TikTok code',
|
||||
r'(\d{6}) is your verification code',
|
||||
r'Dein Bestätigungscode lautet (\d{6})',
|
||||
r'Your verification code is (\d{6})',
|
||||
r'Verification code: (\d{6})',
|
||||
r'Bestätigungscode: (\d{6})',
|
||||
r'TikTok code: (\d{6})',
|
||||
r'TikTok-Code: (\d{6})'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, email_body)
|
||||
if match:
|
||||
code = match.group(1)
|
||||
logger.info(f"Verifizierungscode aus E-Mail extrahiert: {code}")
|
||||
return code
|
||||
|
||||
# Allgemeine Suche nach 6-stelligen Zahlen, wenn keine spezifischen Muster passen
|
||||
general_match = re.search(r'[^\d](\d{6})[^\d]', email_body)
|
||||
if general_match:
|
||||
code = general_match.group(1)
|
||||
logger.info(f"6-stelliger Code aus E-Mail extrahiert: {code}")
|
||||
return code
|
||||
|
||||
logger.warning("Kein Verifizierungscode in der E-Mail gefunden")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren des Verifizierungscodes aus der E-Mail: {e}")
|
||||
return None
|
||||
458
social_networks/tiktok/tiktok_verification.py
Normale Datei
458
social_networks/tiktok/tiktok_verification.py
Normale Datei
@ -0,0 +1,458 @@
|
||||
# social_networks/tiktok/tiktok_verification.py
|
||||
|
||||
"""
|
||||
TikTok-Verifizierung - Klasse für die Verifizierungsfunktionalität bei TikTok
|
||||
"""
|
||||
|
||||
import time
|
||||
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_verification")
|
||||
|
||||
class TikTokVerification:
|
||||
"""
|
||||
Klasse für die Verifizierung von TikTok-Konten.
|
||||
Enthält alle Methoden für den Verifizierungsprozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Verifizierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = TikTokSelectors()
|
||||
self.workflow = TikTokWorkflow.get_verification_workflow()
|
||||
|
||||
logger.debug("TikTok-Verifizierung initialisiert")
|
||||
|
||||
def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Verifizierungsprozess für ein TikTok-Konto durch.
|
||||
|
||||
Args:
|
||||
verification_code: Der Bestätigungscode
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung mit Status
|
||||
"""
|
||||
# Browser wird direkt von automation verwendet
|
||||
|
||||
# Validiere den Verifizierungscode
|
||||
if not self._validate_verification_code(verification_code):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültiger Verifizierungscode",
|
||||
"stage": "code_validation"
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. Überprüfen, ob wir auf der Verifizierungsseite sind
|
||||
if not self._is_on_verification_page():
|
||||
# Versuche, zur Verifizierungsseite zu navigieren, falls möglich
|
||||
# Direktnavigation ist jedoch normalerweise nicht möglich
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Nicht auf der Verifizierungsseite",
|
||||
"stage": "page_check"
|
||||
}
|
||||
|
||||
# 2. Verifizierungscode eingeben und absenden
|
||||
if not self.enter_and_submit_verification_code(verification_code):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Eingeben oder Absenden des Verifizierungscodes",
|
||||
"stage": "code_entry"
|
||||
}
|
||||
|
||||
# 3. Überprüfen, ob die Verifizierung erfolgreich war
|
||||
success, error_message = self._check_verification_success()
|
||||
|
||||
if not success:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Verifizierung fehlgeschlagen: {error_message or 'Unbekannter Fehler'}",
|
||||
"stage": "verification_check"
|
||||
}
|
||||
|
||||
# 4. Zusätzliche Dialoge behandeln
|
||||
self._handle_post_verification_dialogs()
|
||||
|
||||
# Verifizierung erfolgreich
|
||||
logger.info("TikTok-Verifizierung erfolgreich abgeschlossen")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "completed"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der TikTok-Verifizierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception"
|
||||
}
|
||||
|
||||
def _validate_verification_code(self, verification_code: str) -> bool:
|
||||
"""
|
||||
Validiert den Verifizierungscode.
|
||||
|
||||
Args:
|
||||
verification_code: Der zu validierende Code
|
||||
|
||||
Returns:
|
||||
bool: True wenn der Code gültig ist, False sonst
|
||||
"""
|
||||
# Leerer Code
|
||||
if not verification_code:
|
||||
logger.error("Verifizierungscode ist leer")
|
||||
return False
|
||||
|
||||
# Code-Format prüfen (normalerweise 6-stellige Zahl)
|
||||
if not re.match(r"^\d{6}$", verification_code):
|
||||
logger.warning(f"Verifizierungscode hat unerwartetes Format: {verification_code}")
|
||||
# Wir geben trotzdem True zurück, da einige Codes andere Formate haben könnten
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def _is_on_verification_page(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob wir auf der Verifizierungsseite sind.
|
||||
|
||||
Returns:
|
||||
bool: True wenn auf der Verifizierungsseite, False sonst
|
||||
"""
|
||||
try:
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_page_check")
|
||||
|
||||
# Nach Verifizierungsfeld suchen
|
||||
verification_selectors = [
|
||||
TikTokSelectors.VERIFICATION_CODE_FIELD,
|
||||
"input[placeholder*='Code']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder*='sechsstelligen']",
|
||||
"input[data-e2e='verification-code-input']"
|
||||
]
|
||||
|
||||
for selector in verification_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
logger.info("Auf Verifizierungsseite")
|
||||
return True
|
||||
|
||||
# Textbasierte Erkennung
|
||||
verification_texts = [
|
||||
"Bestätigungscode",
|
||||
"Verification code",
|
||||
"sechsstelligen Code",
|
||||
"6-digit code",
|
||||
"Code senden",
|
||||
"Send code"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for text in verification_texts:
|
||||
if text.lower() in page_content:
|
||||
logger.info(f"Auf Verifizierungsseite (erkannt durch Text: {text})")
|
||||
return True
|
||||
|
||||
logger.warning("Nicht auf der Verifizierungsseite")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen der Verifizierungsseite: {e}")
|
||||
return False
|
||||
|
||||
def enter_and_submit_verification_code(self, verification_code: str) -> bool:
|
||||
"""
|
||||
Gibt den Verifizierungscode ein und sendet ihn ab.
|
||||
|
||||
Args:
|
||||
verification_code: Der einzugebende Verifizierungscode
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Versuche Verifizierungscode einzugeben: {verification_code}")
|
||||
|
||||
# Mögliche Selektoren für das Verifizierungscode-Feld
|
||||
code_field_selectors = [
|
||||
TikTokSelectors.VERIFICATION_CODE_FIELD,
|
||||
TikTokSelectors.VERIFICATION_CODE_FIELD_ALT,
|
||||
"input[placeholder*='Code']",
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[data-e2e='verification-code-input']"
|
||||
]
|
||||
|
||||
# Versuche, das Feld zu finden und auszufüllen
|
||||
code_field_found = False
|
||||
|
||||
for selector in code_field_selectors:
|
||||
logger.debug(f"Versuche Selektor: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Codefeld gefunden mit Selektor: {selector}")
|
||||
if self.automation.browser.fill_form_field(selector, verification_code):
|
||||
code_field_found = True
|
||||
logger.info(f"Verifizierungscode eingegeben: {verification_code}")
|
||||
break
|
||||
|
||||
# Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen
|
||||
if not code_field_found:
|
||||
logger.info("Versuche Fuzzy-Matching für Codefeld")
|
||||
code_field_found = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Bestätigungscode", "Code eingeben", "Verification code", "6-digit code"],
|
||||
verification_code
|
||||
)
|
||||
|
||||
if not code_field_found:
|
||||
logger.error("Konnte Verifizierungscode-Feld nicht finden oder ausfüllen")
|
||||
|
||||
# Erstelle einen Screenshot zum Debuggen
|
||||
self.automation._take_screenshot("code_field_not_found")
|
||||
return False
|
||||
|
||||
# Menschliche Verzögerung vor dem Absenden
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_code_entered")
|
||||
|
||||
# "Weiter"-Button finden und klicken
|
||||
weiter_button_selectors = [
|
||||
TikTokSelectors.CONTINUE_BUTTON,
|
||||
TikTokSelectors.CONTINUE_BUTTON_ALT,
|
||||
"button[type='submit']",
|
||||
"button.e1w6iovg0",
|
||||
"button[data-e2e='next-button']",
|
||||
"//button[contains(text(), 'Weiter')]"
|
||||
]
|
||||
|
||||
weiter_button_found = False
|
||||
|
||||
logger.info("Suche nach Weiter-Button")
|
||||
for selector in weiter_button_selectors:
|
||||
logger.debug(f"Versuche Weiter-Button-Selektor: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Weiter-Button gefunden mit Selektor: {selector}")
|
||||
if self.automation.browser.click_element(selector):
|
||||
weiter_button_found = True
|
||||
logger.info("Verifizierungscode-Formular abgesendet")
|
||||
break
|
||||
|
||||
# Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen
|
||||
if not weiter_button_found:
|
||||
logger.info("Versuche Fuzzy-Matching für Weiter-Button")
|
||||
weiter_buttons = ["Weiter", "Next", "Continue", "Fertig", "Submit", "Verify", "Senden"]
|
||||
weiter_button_found = self.automation.ui_helper.click_button_fuzzy(
|
||||
weiter_buttons
|
||||
)
|
||||
|
||||
if not weiter_button_found:
|
||||
# Erstelle einen Screenshot zum Debuggen
|
||||
self.automation._take_screenshot("weiter_button_not_found")
|
||||
|
||||
# Versuche es mit Enter-Taste als letzten Ausweg
|
||||
logger.info("Konnte Weiter-Button nicht finden, versuche Enter-Taste")
|
||||
self.automation.browser.page.keyboard.press("Enter")
|
||||
logger.info("Enter-Taste zur Bestätigung des Verifizierungscodes gedrückt")
|
||||
weiter_button_found = True
|
||||
|
||||
# Warten nach dem Absenden
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
return weiter_button_found
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Eingeben und Absenden des Verifizierungscodes: {e}")
|
||||
return False
|
||||
|
||||
def _check_verification_success(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Überprüft, ob die Verifizierung erfolgreich war.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (Erfolg, Fehlermeldung falls vorhanden)
|
||||
"""
|
||||
try:
|
||||
# Warten nach der Verifizierung
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_result")
|
||||
|
||||
# Immer noch auf der Verifizierungsseite?
|
||||
still_on_verification = self._is_on_verification_page()
|
||||
|
||||
if still_on_verification:
|
||||
# Fehlermeldung suchen
|
||||
error_message = self.automation.ui_helper.check_for_error()
|
||||
|
||||
if error_message:
|
||||
logger.error(f"Verifizierungsfehler: {error_message}")
|
||||
return False, error_message
|
||||
else:
|
||||
logger.error("Verifizierung fehlgeschlagen, immer noch auf der Verifizierungsseite")
|
||||
return False, "Immer noch auf der Verifizierungsseite"
|
||||
|
||||
# Prüfe, ob wir zur Benutzernamen-Erstellung weitergeleitet wurden
|
||||
username_selectors = [
|
||||
"input[placeholder='Benutzername']",
|
||||
"input[name='new-username']",
|
||||
"//input[@placeholder='Benutzername']"
|
||||
]
|
||||
|
||||
for selector in username_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Verifizierung erfolgreich, zur Benutzernamenauswahl weitergeleitet")
|
||||
return True, None
|
||||
|
||||
# Prüfe auf TikTok-Startseite
|
||||
current_url = self.automation.browser.page.url
|
||||
if "tiktok.com" in current_url and "/login" not in current_url and "/signup" not in current_url:
|
||||
logger.info("Verifizierung erfolgreich, jetzt auf der Startseite")
|
||||
return True, None
|
||||
|
||||
# Wenn keine eindeutigen Indikatoren gefunden wurden, aber auch keine Fehler
|
||||
logger.warning("Keine eindeutigen Erfolgsindikatoren für die Verifizierung gefunden")
|
||||
return True, None # Wir gehen davon aus, dass es erfolgreich war
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen des Verifizierungserfolgs: {e}")
|
||||
return False, f"Fehler bei der Erfolgsprüfung: {str(e)}"
|
||||
|
||||
def _handle_post_verification_dialogs(self) -> None:
|
||||
"""
|
||||
Behandelt Dialoge, die nach erfolgreicher Verifizierung erscheinen können.
|
||||
"""
|
||||
try:
|
||||
# Liste der möglichen Dialoge und wie man sie überspringt
|
||||
dialogs_to_handle = [
|
||||
{
|
||||
"name": "username_setup",
|
||||
"skip_texts": ["Überspringen", "Skip"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"]
|
||||
},
|
||||
{
|
||||
"name": "interests",
|
||||
"skip_texts": ["Überspringen", "Skip"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"]
|
||||
},
|
||||
{
|
||||
"name": "follow_accounts",
|
||||
"skip_texts": ["Überspringen", "Skip"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"]
|
||||
},
|
||||
{
|
||||
"name": "notifications",
|
||||
"skip_texts": ["Später", "Nein", "Nicht jetzt", "Later", "No", "Not now"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Später')]", "//button[contains(text(), 'Not now')]"]
|
||||
}
|
||||
]
|
||||
|
||||
# Versuche, jeden möglichen Dialog zu behandeln
|
||||
for dialog in dialogs_to_handle:
|
||||
self._try_skip_dialog(dialog)
|
||||
|
||||
logger.info("Nachverifizierungs-Dialoge behandelt")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Behandeln der Nachverifizierungs-Dialoge: {e}")
|
||||
# Nicht kritisch, daher keine Fehlerbehandlung
|
||||
|
||||
def _try_skip_dialog(self, dialog: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Versucht, einen bestimmten Dialog zu überspringen.
|
||||
|
||||
Args:
|
||||
dialog: Informationen zum Dialog
|
||||
|
||||
Returns:
|
||||
bool: True wenn Dialog gefunden und übersprungen, False sonst
|
||||
"""
|
||||
try:
|
||||
# Zuerst mit Fuzzy-Matching versuchen
|
||||
skip_clicked = self.automation.ui_helper.click_button_fuzzy(
|
||||
dialog["skip_texts"],
|
||||
threshold=0.7,
|
||||
timeout=3000
|
||||
)
|
||||
|
||||
if skip_clicked:
|
||||
logger.info(f"Dialog '{dialog['name']}' mit Fuzzy-Matching übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
# Wenn Fuzzy-Matching fehlschlägt, direkte Selektoren versuchen
|
||||
for selector in dialog["skip_selectors"]:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info(f"Dialog '{dialog['name']}' mit direktem Selektor übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Versuch, Dialog '{dialog['name']}' zu überspringen: {e}")
|
||||
return False
|
||||
|
||||
def resend_verification_code(self) -> bool:
|
||||
"""
|
||||
Versucht, den Verifizierungscode erneut senden zu lassen.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Resend-Button suchen und klicken
|
||||
resend_selectors = [
|
||||
"button[data-e2e='send-code-button']",
|
||||
"//button[contains(text(), 'Code senden')]",
|
||||
"//button[contains(text(), 'Code erneut senden')]",
|
||||
"//button[contains(text(), 'Erneut senden')]",
|
||||
"a[data-e2e='resend-code-link']"
|
||||
]
|
||||
|
||||
for selector in resend_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Code erneut angefordert")
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
return True
|
||||
|
||||
# Fuzzy-Matching versuchen
|
||||
resend_texts = ["Code senden", "Code erneut senden", "Erneut senden", "Resend code", "Send again"]
|
||||
|
||||
resend_clicked = self.automation.ui_helper.click_button_fuzzy(
|
||||
resend_texts,
|
||||
threshold=0.7,
|
||||
timeout=3000
|
||||
)
|
||||
|
||||
if resend_clicked:
|
||||
logger.info("Code erneut angefordert (über Fuzzy-Matching)")
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
return True
|
||||
|
||||
logger.warning("Konnte keinen 'Code erneut senden'-Button finden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim erneuten Anfordern des Codes: {e}")
|
||||
return False
|
||||
427
social_networks/tiktok/tiktok_workflow.py
Normale Datei
427
social_networks/tiktok/tiktok_workflow.py
Normale Datei
@ -0,0 +1,427 @@
|
||||
"""
|
||||
TikTok-Workflow - Definiert die Schritte für die TikTok-Anmeldung und -Registrierung
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import re
|
||||
|
||||
from utils.text_similarity import TextSimilarity
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_workflow")
|
||||
|
||||
class TikTokWorkflow:
|
||||
"""
|
||||
Definiert die Workflow-Schritte für verschiedene TikTok-Aktionen
|
||||
wie Registrierung, Anmeldung und Verifizierung.
|
||||
"""
|
||||
|
||||
# Text-Ähnlichkeits-Threshold für Fuzzy-Matching
|
||||
SIMILARITY_THRESHOLD = 0.7
|
||||
|
||||
# Initialisiere TextSimilarity für Matching
|
||||
text_similarity = TextSimilarity(default_threshold=SIMILARITY_THRESHOLD)
|
||||
|
||||
# Mögliche alternative Texte für verschiedene UI-Elemente
|
||||
TEXT_ALTERNATIVES = {
|
||||
"email": ["E-Mail", "Email", "E-mail", "Mail", "email"],
|
||||
"phone": ["Telefon", "Telefonnummer", "Phone", "Mobile", "mobile"],
|
||||
"password": ["Passwort", "Password", "pass"],
|
||||
"code": ["Code", "Bestätigungscode", "Verification code", "Sicherheitscode"],
|
||||
"username": ["Benutzername", "Username", "user name"],
|
||||
"submit": ["Registrieren", "Sign up", "Anmelden", "Login", "Log in", "Submit"],
|
||||
"next": ["Weiter", "Next", "Continue", "Fortfahren"],
|
||||
"skip": ["Überspringen", "Skip", "Later", "Später", "Not now", "Nicht jetzt"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_registration_workflow(registration_method: str = "email") -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die TikTok-Registrierung zurück.
|
||||
|
||||
Args:
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
# Basisschritte für beide Methoden
|
||||
common_steps = [
|
||||
{
|
||||
"name": "navigate_to_signup",
|
||||
"description": "Zur TikTok-Startseite navigieren",
|
||||
"url": "https://www.tiktok.com",
|
||||
"wait_for": ["button#header-login-button", "button#top-right-action-bar-login-button"],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "click_login_button",
|
||||
"description": "Anmelden-Button klicken",
|
||||
"action": "click",
|
||||
"target": "button#top-right-action-bar-login-button",
|
||||
"wait_for": ["a[href*='/signup']", "div[role='dialog']"],
|
||||
"fuzzy_match": ["Anmelden", "Sign in", "Log in"]
|
||||
},
|
||||
{
|
||||
"name": "click_register_link",
|
||||
"description": "Registrieren-Link klicken",
|
||||
"action": "click",
|
||||
"target": "a[href*='/signup']",
|
||||
"wait_for": ["div[data-e2e='channel-item']"],
|
||||
"fuzzy_match": ["Registrieren", "Sign up", "Register"]
|
||||
},
|
||||
{
|
||||
"name": "click_phone_email_button",
|
||||
"description": "Telefon/E-Mail-Option auswählen",
|
||||
"action": "click",
|
||||
"target": "div[data-e2e='channel-item']",
|
||||
"wait_for": ["a[href*='/signup/phone-or-email/email']", "a[href*='/signup/phone-or-email/phone']"],
|
||||
"fuzzy_match": ["Telefonnummer oder E-Mail-Adresse", "Phone or Email"]
|
||||
}
|
||||
]
|
||||
|
||||
# Spezifische Schritte je nach Registrierungsmethode
|
||||
method_steps = []
|
||||
if registration_method == "email":
|
||||
method_steps.append({
|
||||
"name": "click_email_registration",
|
||||
"description": "Mit E-Mail registrieren auswählen",
|
||||
"action": "click",
|
||||
"target": "a[href*='/signup/phone-or-email/email']",
|
||||
"wait_for": ["input[placeholder='E-Mail-Adresse']"],
|
||||
"fuzzy_match": ["Mit E-Mail-Adresse registrieren", "Email", "E-Mail"]
|
||||
})
|
||||
else: # phone
|
||||
method_steps.append({
|
||||
"name": "click_phone_registration",
|
||||
"description": "Mit Telefonnummer registrieren auswählen",
|
||||
"action": "click",
|
||||
"target": "a[href*='/signup/phone-or-email/phone']",
|
||||
"wait_for": ["input[placeholder='Telefonnummer']"],
|
||||
"fuzzy_match": ["Mit Telefonnummer registrieren", "Phone", "Telefon"]
|
||||
})
|
||||
|
||||
# Geburtsdatum-Schritte
|
||||
birthday_steps = [
|
||||
{
|
||||
"name": "select_birth_month",
|
||||
"description": "Geburtsmonat auswählen",
|
||||
"action": "click",
|
||||
"target": "div.css-1fi2hzv-DivSelectLabel:contains('Monat')",
|
||||
"wait_for": ["div[role='option']"],
|
||||
"fuzzy_match": ["Monat", "Month"]
|
||||
},
|
||||
{
|
||||
"name": "select_month_option",
|
||||
"description": "Monats-Option auswählen",
|
||||
"action": "select_option",
|
||||
"target": "div[role='option']",
|
||||
"value": "{MONTH_NAME}",
|
||||
"wait_for": [],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "select_birth_day",
|
||||
"description": "Geburtstag auswählen",
|
||||
"action": "click",
|
||||
"target": "div.css-1fi2hzv-DivSelectLabel:contains('Tag')",
|
||||
"wait_for": ["div[role='option']"],
|
||||
"fuzzy_match": ["Tag", "Day"]
|
||||
},
|
||||
{
|
||||
"name": "select_day_option",
|
||||
"description": "Tags-Option auswählen",
|
||||
"action": "select_option",
|
||||
"target": "div[role='option']",
|
||||
"value": "{DAY}",
|
||||
"wait_for": [],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "select_birth_year",
|
||||
"description": "Geburtsjahr auswählen",
|
||||
"action": "click",
|
||||
"target": "div.css-1fi2hzv-DivSelectLabel:contains('Jahr')",
|
||||
"wait_for": ["div[role='option']"],
|
||||
"fuzzy_match": ["Jahr", "Year"]
|
||||
},
|
||||
{
|
||||
"name": "select_year_option",
|
||||
"description": "Jahres-Option auswählen",
|
||||
"action": "select_option",
|
||||
"target": "div[role='option']",
|
||||
"value": "{YEAR}",
|
||||
"wait_for": [],
|
||||
"fuzzy_match": None
|
||||
}
|
||||
]
|
||||
|
||||
# Formularschritte für E-Mail
|
||||
email_form_steps = [
|
||||
{
|
||||
"name": "fill_email",
|
||||
"description": "E-Mail-Adresse eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder='E-Mail-Adresse']",
|
||||
"value": "{EMAIL}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["email"]
|
||||
},
|
||||
{
|
||||
"name": "fill_password",
|
||||
"description": "Passwort eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder='Passwort']",
|
||||
"value": "{PASSWORD}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["password"]
|
||||
},
|
||||
{
|
||||
"name": "click_send_code",
|
||||
"description": "Code senden klicken",
|
||||
"action": "click",
|
||||
"target": "button[data-e2e='send-code-button']",
|
||||
"wait_for": ["input[placeholder*='sechsstelligen Code']"],
|
||||
"fuzzy_match": ["Code senden", "Send code", "Senden"]
|
||||
},
|
||||
{
|
||||
"name": "fill_verification_code",
|
||||
"description": "Bestätigungscode eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder*='sechsstelligen Code']",
|
||||
"value": "{VERIFICATION_CODE}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"]
|
||||
},
|
||||
{
|
||||
"name": "click_continue",
|
||||
"description": "Weiter klicken",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["input[placeholder='Benutzername']"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"]
|
||||
}
|
||||
]
|
||||
|
||||
# Formularschritte für Telefon
|
||||
phone_form_steps = [
|
||||
{
|
||||
"name": "select_country_code",
|
||||
"description": "Ländervorwahl auswählen",
|
||||
"action": "click",
|
||||
"target": "div[role='combobox']",
|
||||
"wait_for": ["div[role='option']"],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "select_country_option",
|
||||
"description": "Land auswählen",
|
||||
"action": "select_option",
|
||||
"target": "div[role='option']",
|
||||
"value": "{COUNTRY_NAME}",
|
||||
"wait_for": [],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "fill_phone",
|
||||
"description": "Telefonnummer eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder='Telefonnummer']",
|
||||
"value": "{PHONE}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["phone"]
|
||||
},
|
||||
{
|
||||
"name": "click_send_code",
|
||||
"description": "Code senden klicken",
|
||||
"action": "click",
|
||||
"target": "button[data-e2e='send-code-button']",
|
||||
"wait_for": ["input[placeholder*='sechsstelligen Code']"],
|
||||
"fuzzy_match": ["Code senden", "Send code", "Senden"]
|
||||
},
|
||||
{
|
||||
"name": "fill_verification_code",
|
||||
"description": "Bestätigungscode eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder*='sechsstelligen Code']",
|
||||
"value": "{VERIFICATION_CODE}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"]
|
||||
},
|
||||
{
|
||||
"name": "click_continue",
|
||||
"description": "Weiter klicken",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["input[placeholder='Benutzername']"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"]
|
||||
}
|
||||
]
|
||||
|
||||
# Benutzername-Schritte
|
||||
username_steps = [
|
||||
{
|
||||
"name": "fill_username",
|
||||
"description": "Benutzernamen eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder='Benutzername']",
|
||||
"value": "{USERNAME}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["username"]
|
||||
},
|
||||
{
|
||||
"name": "click_register",
|
||||
"description": "Registrieren klicken",
|
||||
"action": "click",
|
||||
"target": "button:contains('Registrieren')",
|
||||
"wait_for": ["a[href='/foryou']", "button:contains('Überspringen')"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["submit"]
|
||||
},
|
||||
{
|
||||
"name": "handle_skip_option",
|
||||
"description": "Optional: Überspringen klicken",
|
||||
"action": "click",
|
||||
"target": "button:contains('Überspringen')",
|
||||
"optional": True,
|
||||
"wait_for": ["a[href='/foryou']"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["skip"]
|
||||
}
|
||||
]
|
||||
|
||||
# Vollständigen Workflow zusammenstellen
|
||||
if registration_method == "email":
|
||||
workflow = common_steps + method_steps + birthday_steps + email_form_steps + username_steps
|
||||
else: # phone
|
||||
workflow = common_steps + method_steps + birthday_steps + phone_form_steps + username_steps
|
||||
|
||||
return workflow
|
||||
|
||||
@staticmethod
|
||||
def get_login_workflow() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die TikTok-Anmeldung zurück.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
login_steps = [
|
||||
{
|
||||
"name": "navigate_to_login",
|
||||
"description": "Zur TikTok-Startseite navigieren",
|
||||
"url": "https://www.tiktok.com",
|
||||
"wait_for": ["button#header-login-button", "button#top-right-action-bar-login-button"],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "click_login_button",
|
||||
"description": "Anmelden-Button klicken",
|
||||
"action": "click",
|
||||
"target": "button#top-right-action-bar-login-button",
|
||||
"wait_for": ["div[role='dialog']"],
|
||||
"fuzzy_match": ["Anmelden", "Sign in", "Log in"]
|
||||
},
|
||||
{
|
||||
"name": "click_phone_email_button",
|
||||
"description": "Telefon/E-Mail-Option auswählen",
|
||||
"action": "click",
|
||||
"target": "div[data-e2e='channel-item']",
|
||||
"wait_for": ["input[type='text']"],
|
||||
"fuzzy_match": ["Telefon-Nr./E-Mail/Anmeldename", "Phone or Email"]
|
||||
},
|
||||
{
|
||||
"name": "fill_login_field",
|
||||
"description": "Benutzername/E-Mail/Telefon eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[type='text']",
|
||||
"value": "{USERNAME_OR_EMAIL}",
|
||||
"fuzzy_match": ["Email", "Benutzername", "Telefon"]
|
||||
},
|
||||
{
|
||||
"name": "fill_password",
|
||||
"description": "Passwort eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[type='password']",
|
||||
"value": "{PASSWORD}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["password"]
|
||||
},
|
||||
{
|
||||
"name": "click_login",
|
||||
"description": "Anmelden klicken",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["a[href='/foryou']"],
|
||||
"fuzzy_match": ["Anmelden", "Log in", "Login"]
|
||||
}
|
||||
]
|
||||
|
||||
return login_steps
|
||||
|
||||
@staticmethod
|
||||
def get_verification_workflow() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die TikTok-Verifizierung zurück.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
verification_steps = [
|
||||
{
|
||||
"name": "fill_verification_code",
|
||||
"description": "Bestätigungscode eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder*='sechsstelligen Code']",
|
||||
"value": "{VERIFICATION_CODE}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"]
|
||||
},
|
||||
{
|
||||
"name": "click_continue",
|
||||
"description": "Weiter klicken",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["input[placeholder='Benutzername']", "a[href='/foryou']"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"]
|
||||
}
|
||||
]
|
||||
|
||||
return verification_steps
|
||||
|
||||
@staticmethod
|
||||
def identify_current_step(page_title: str, page_url: str, visible_elements: List[str]) -> str:
|
||||
"""
|
||||
Identifiziert den aktuellen Schritt basierend auf dem Seitentitel, der URL und sichtbaren Elementen.
|
||||
|
||||
Args:
|
||||
page_title: Titel der Seite
|
||||
page_url: URL der Seite
|
||||
visible_elements: Liste sichtbarer Elemente (Selektoren)
|
||||
|
||||
Returns:
|
||||
str: Name des identifizierten Schritts
|
||||
"""
|
||||
# Auf der Startseite
|
||||
if "tiktok.com" in page_url and not "/signup" in page_url and not "/login" in page_url:
|
||||
return "navigate_to_signup"
|
||||
|
||||
# Anmelde-/Registrierungsauswahl
|
||||
if "signup" in page_url or "login" in page_url:
|
||||
if any("channel-item" in element for element in visible_elements):
|
||||
return "click_phone_email_button"
|
||||
|
||||
# Geburtsdatum
|
||||
if "Monat" in page_title or "Month" in page_title or any("Geburtsdatum" in element for element in visible_elements):
|
||||
return "select_birth_month"
|
||||
|
||||
# E-Mail-/Telefon-Eingabe
|
||||
if any("E-Mail-Adresse" in element for element in visible_elements):
|
||||
return "fill_email"
|
||||
if any("Telefonnummer" in element for element in visible_elements):
|
||||
return "fill_phone"
|
||||
|
||||
# Bestätigungscode
|
||||
if any("sechsstelligen Code" in element for element in visible_elements):
|
||||
return "fill_verification_code"
|
||||
|
||||
# Benutzernamen-Erstellung
|
||||
if any("Benutzername" in element for element in visible_elements):
|
||||
return "fill_username"
|
||||
|
||||
# Erfolgreiche Anmeldung
|
||||
if "foryou" in page_url or any("Für dich" in element for element in visible_elements):
|
||||
return "logged_in"
|
||||
|
||||
return "unknown"
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren