Initial commit
Dieser Commit ist enthalten in:
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
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren