582 Zeilen
22 KiB
Python
582 Zeilen
22 KiB
Python
"""
|
|
TikTok-Login - Klasse für die Anmeldefunktionalität bei TikTok
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
import re
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
|
|
from .tiktok_selectors import TikTokSelectors
|
|
from .tiktok_workflow import TikTokWorkflow
|
|
|
|
# Konfiguriere Logger
|
|
logger = logging.getLogger("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
|
|
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
|
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
|
|
"""
|
|
# Hole Browser-Referenz von der Hauptklasse
|
|
self.browser = self.automation.browser
|
|
|
|
# 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.
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
try:
|
|
# Zur Login-Seite navigieren
|
|
self.browser.navigate_to(TikTokSelectors.LOGIN_URL)
|
|
|
|
# Warten, bis die Seite geladen ist
|
|
self.automation.human_behavior.wait_for_page_load()
|
|
|
|
# Screenshot erstellen
|
|
self.automation._take_screenshot("login_page")
|
|
|
|
# Prüfen, ob Login-Dialog sichtbar ist
|
|
if not self.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=5000):
|
|
logger.warning("Login-Dialog nicht sichtbar, versuche Login-Button zu klicken")
|
|
|
|
# Versuche, den Login-Button zu klicken, um das Login-Modal zu öffnen
|
|
login_buttons = [
|
|
TikTokSelectors.LOGIN_BUTTON_TOP,
|
|
TikTokSelectors.LOGIN_BUTTON_SIDEBAR
|
|
]
|
|
|
|
button_clicked = False
|
|
for button in login_buttons:
|
|
if self.browser.is_element_visible(button, timeout=2000):
|
|
self.browser.click_element(button)
|
|
button_clicked = True
|
|
break
|
|
|
|
if not button_clicked:
|
|
logger.warning("Keine Login-Buttons gefunden")
|
|
return False
|
|
|
|
# Warten, bis der Login-Dialog erscheint
|
|
self.automation.human_behavior.wait_between_actions("decision", 1.5)
|
|
|
|
# Erneut prüfen, ob der Login-Dialog sichtbar ist
|
|
if not self.browser.is_element_visible(TikTokSelectors.LOGIN_DIALOG, timeout=5000):
|
|
logger.warning("Login-Dialog nach dem Klicken auf den Login-Button nicht sichtbar")
|
|
return False
|
|
|
|
logger.info("Erfolgreich zur Login-Seite 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.
|
|
|
|
Returns:
|
|
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
|
"""
|
|
# Cookie-Dialog-Erkennung
|
|
if self.browser.is_element_visible(TikTokSelectors.COOKIE_DIALOG, timeout=2000):
|
|
logger.info("Cookie-Banner erkannt")
|
|
|
|
# Ablehnen-Button suchen und klicken
|
|
reject_success = self.automation.ui_helper.click_button_fuzzy(
|
|
TikTokSelectors.get_button_texts("reject_cookies"),
|
|
TikTokSelectors.COOKIE_REJECT_BUTTON
|
|
)
|
|
|
|
if reject_success:
|
|
logger.info("Cookie-Banner erfolgreich abgelehnt")
|
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
|
return True
|
|
else:
|
|
logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren")
|
|
|
|
# Akzeptieren-Button als Fallback
|
|
accept_success = self.browser.click_element(TikTokSelectors.COOKIE_ACCEPT_BUTTON)
|
|
|
|
if accept_success:
|
|
logger.info("Cookie-Banner erfolgreich akzeptiert")
|
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
|
return True
|
|
else:
|
|
logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren")
|
|
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:
|
|
# E-Mail/Telefon-Login-Option auswählen
|
|
email_phone_option = self.automation.ui_helper.click_button_fuzzy(
|
|
["Telefon-Nr./E-Mail/Anmeldename nutzen", "Use phone / email / username"],
|
|
TikTokSelectors.LOGIN_EMAIL_PHONE_OPTION
|
|
)
|
|
|
|
if not email_phone_option:
|
|
logger.warning("Konnte die E-Mail/Telefon-Option nicht auswählen, versuche direkt das Formular auszufüllen")
|
|
|
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
|
|
|
# E-Mail/Benutzername eingeben
|
|
username_success = self.automation.ui_helper.fill_field_fuzzy(
|
|
TikTokSelectors.get_field_labels("email_username"),
|
|
account_data["username"],
|
|
TikTokSelectors.LOGIN_EMAIL_FIELD
|
|
)
|
|
|
|
if not username_success:
|
|
logger.error("Konnte Benutzername-Feld nicht ausfüllen")
|
|
return False
|
|
|
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
|
|
|
# Passwort eingeben
|
|
password_success = self.automation.ui_helper.fill_field_fuzzy(
|
|
TikTokSelectors.get_field_labels("password"),
|
|
account_data["password"],
|
|
TikTokSelectors.LOGIN_PASSWORD_FIELD
|
|
)
|
|
|
|
if not password_success:
|
|
logger.error("Konnte Passwort-Feld nicht ausfüllen")
|
|
return False
|
|
|
|
self.automation.human_behavior.random_delay(1.0, 2.0)
|
|
|
|
# Screenshot vorm Absenden
|
|
self.automation._take_screenshot("login_form_filled")
|
|
|
|
# Formular absenden
|
|
submit_success = self.automation.ui_helper.click_button_fuzzy(
|
|
["Anmelden", "Log in", "Login"],
|
|
TikTokSelectors.LOGIN_SUBMIT_BUTTON
|
|
)
|
|
|
|
if not submit_success:
|
|
logger.error("Konnte Login-Formular nicht absenden")
|
|
return False
|
|
|
|
# Nach dem Absenden warten
|
|
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
|
|
|
# Ü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 _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.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.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.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.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.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.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.browser.is_element_visible(selector, timeout=1000):
|
|
if self.browser.click_element(selector):
|
|
confirm_clicked = True
|
|
break
|
|
|
|
if not confirm_clicked:
|
|
# Alternative: Mit Tastendruck bestätigen
|
|
self.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.browser.is_element_visible(selector, timeout=3000):
|
|
if self.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.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.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.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 |