# social_networks/x/x_login.py """ X (Twitter) Login - Klasse für die Anmeldung bei X-Konten """ import time import random from typing import Dict, Any, Optional from .x_selectors import XSelectors from .x_workflow import XWorkflow from utils.logger import setup_logger # Konfiguriere Logger logger = setup_logger("x_login") class XLogin: """ Klasse für die Anmeldung bei X-Konten. Behandelt den kompletten Login-Prozess inklusive möglicher Sicherheitsabfragen. """ def __init__(self, automation): """ Initialisiert die X-Login-Klasse. Args: automation: Referenz auf die Hauptautomatisierungsklasse """ self.automation = automation self.selectors = XSelectors() self.workflow = XWorkflow.get_login_workflow() logger.debug("X-Login initialisiert") def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: """ Führt den Login-Prozess für einen X-Account 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 """ logger.info(f"Starte X-Login für '{username_or_email}'") try: # 1. Zur Startseite navigieren self.automation._emit_customer_log("🌐 Mit X verbinden...") if not self._navigate_to_homepage(): return { "success": False, "error": "Konnte nicht zur X-Startseite navigieren", "stage": "navigation" } # 2. Cookie-Banner behandeln self._handle_cookie_banner() # 3. Login-Button klicken (überspringen, da wir direkt zur Login-Seite navigieren) # self.automation._emit_customer_log("🔓 Login-Formular wird geöffnet...") # Der Login-Button ist nicht mehr nötig, da wir direkt auf der Login-Seite sind # 4. Benutzername/E-Mail eingeben self.automation._emit_customer_log("📧 Anmeldedaten werden eingegeben...") if not self._enter_username(username_or_email): return { "success": False, "error": "Fehler beim Eingeben des Benutzernamens/E-Mail", "stage": "username_input" } # 5. Weiter klicken if not self._click_next(): return { "success": False, "error": "Fehler beim Fortfahren nach Benutzername", "stage": "next_button" } # 6. Eventuell nach Telefonnummer/E-Mail fragen (Sicherheitsabfrage) if self._is_additional_info_required(): logger.info("Zusätzliche Informationen erforderlich") if not self._handle_additional_info_request(kwargs.get("phone_number"), kwargs.get("email")): return { "success": False, "error": "Konnte zusätzliche Sicherheitsinformationen nicht bereitstellen", "stage": "additional_info" } # 7. Passwort eingeben self.automation._emit_customer_log("🔐 Passwort wird eingegeben...") if not self._enter_password(password): return { "success": False, "error": "Fehler beim Eingeben des Passworts", "stage": "password_input" } # 8. Login abschicken if not self._submit_login(): return { "success": False, "error": "Fehler beim Abschicken des Login-Formulars", "stage": "login_submit" } # 9. Auf eventuelle Challenges/Captchas prüfen self.automation._emit_customer_log("🔍 Überprüfe Login-Status...") challenge_result = self._handle_login_challenges() if not challenge_result["success"]: return { "success": False, "error": challenge_result.get("error", "Login-Challenge fehlgeschlagen"), "stage": "login_challenge" } # 10. Erfolgreichen Login verifizieren if not self._verify_login_success(): return { "success": False, "error": "Login scheinbar fehlgeschlagen - keine Erfolgsindikatoren gefunden", "stage": "verification" } # Login erfolgreich logger.info(f"X-Login für '{username_or_email}' erfolgreich") self.automation._emit_customer_log("✅ Login erfolgreich!") return { "success": True, "stage": "completed", "username": username_or_email } except Exception as e: error_msg = f"Unerwarteter Fehler beim X-Login: {str(e)}" logger.error(error_msg, exc_info=True) return { "success": False, "error": error_msg, "stage": "exception" } def _navigate_to_homepage(self) -> bool: """ Navigiert zur X-Login-Seite. Returns: bool: True bei Erfolg, False bei Fehler """ try: page = self.automation.browser.page logger.info("Navigiere zur X-Login-Seite") page.goto("https://x.com/i/flow/login?lang=de", wait_until="domcontentloaded", timeout=30000) # Warte auf Seitenladung self.automation.human_behavior.random_delay(2, 4) # Screenshot self.automation._take_screenshot("x_login_page") return True except Exception as e: logger.error(f"Fehler beim Navigieren zur X-Login-Seite: {e}") return False def _handle_cookie_banner(self): """ Behandelt eventuelle Cookie-Banner. """ try: page = self.automation.browser.page for selector in self.selectors.COOKIE_ACCEPT_BUTTONS: try: if page.is_visible(selector): logger.info(f"Cookie-Banner gefunden: {selector}") page.wait_for_selector(selector, timeout=2000).click() self.automation.human_behavior.random_delay(1, 2) break except: continue except Exception as e: logger.debug(f"Kein Cookie-Banner gefunden oder Fehler: {e}") def _click_login_button(self) -> bool: """ Klickt auf den Login-Button. Returns: bool: True bei Erfolg, False bei Fehler """ try: page = self.automation.browser.page # Versuche verschiedene Login-Button-Selektoren login_selectors = [ self.selectors.LOGIN["login_button"], self.selectors.LOGIN["login_button_alt"], 'a[href="/login"]', 'div:has-text("Anmelden")', 'div:has-text("Log in")' ] for selector in login_selectors: try: if page.is_visible(selector, timeout=3000): logger.info(f"Login-Button gefunden: {selector}") self.automation.human_behavior.random_delay(0.5, 1.5) page.click(selector) self.automation.human_behavior.random_delay(1, 2) return True except: continue logger.error("Keinen Login-Button gefunden") return False except Exception as e: logger.error(f"Fehler beim Klicken auf Login-Button: {e}") return False def _enter_username(self, username_or_email: str) -> bool: """ Gibt den Benutzernamen oder die E-Mail-Adresse ein. Args: username_or_email: Benutzername oder E-Mail Returns: bool: True bei Erfolg, False bei Fehler """ try: page = self.automation.browser.page # Warte kurz, bis die Seite vollständig geladen ist self.automation.human_behavior.random_delay(1, 2) # Warte auf Eingabefeld - verwende den spezifischen Selektor aus der HTML-Struktur input_selectors = [ 'input[name="text"][autocomplete="username"]', # Spezifischer Selektor 'input[name="text"]', # Fallback self.selectors.LOGIN["email_or_username_input"], 'input[autocomplete="username"]', 'input[type="text"][name="text"]' ] for selector in input_selectors: try: # Warte auf sichtbares Eingabefeld input_field = page.wait_for_selector(selector, state="visible", timeout=5000) if input_field: logger.info(f"Benutzername-Eingabefeld gefunden: {selector}") # Klicke zuerst auf das Feld, um es zu fokussieren input_field.click() self.automation.human_behavior.random_delay(0.3, 0.5) # Lösche eventuell vorhandenen Text input_field.fill("") # Tippe den Text menschlich ein - simuliere Buchstabe für Buchstabe for char in username_or_email: input_field.type(char) # Zufällige Verzögerung zwischen Zeichen (50-150ms) delay = random.uniform(0.05, 0.15) time.sleep(delay) self.automation.human_behavior.random_delay(0.5, 1) # Screenshot nach Eingabe self.automation._take_screenshot("after_username_input") return True except Exception as e: logger.debug(f"Selektor {selector} nicht gefunden: {e}") continue # Wenn nichts gefunden wurde, Screenshot für Debugging self.automation._take_screenshot("username_input_not_found") logger.error("Kein Benutzername-Eingabefeld gefunden") return False except Exception as e: logger.error(f"Fehler beim Eingeben des Benutzernamens: {e}") return False def _click_next(self) -> bool: """ Klickt auf den Weiter-Button nach Benutzername-Eingabe. Returns: bool: True bei Erfolg, False bei Fehler """ try: page = self.automation.browser.page # Warte kurz self.automation.human_behavior.random_delay(0.5, 1) # Weiter-Button Selektoren - erweitert um spezifische Selektoren next_selectors = [ 'button[role="button"]:has-text("Weiter")', # Spezifisch für button mit role 'button:has-text("Weiter")', # Generischer button 'div[role="button"] span:has-text("Weiter")', # Span innerhalb div '//button[contains(., "Weiter")]', # XPath Alternative self.selectors.LOGIN["next_button"], self.selectors.LOGIN["next_button_en"], 'div[role="button"]:has-text("Weiter")', 'div[role="button"]:has-text("Next")', 'button:has-text("Next")', '[role="button"][type="button"]' # Generisch basierend auf Attributen ] for selector in next_selectors: try: # Versuche verschiedene Methoden if selector.startswith('//'): # XPath element = page.locator(selector).first if element.is_visible(timeout=2000): logger.info(f"Weiter-Button gefunden (XPath): {selector}") element.click() self.automation.human_behavior.random_delay(1, 2) return True else: # CSS Selector if page.is_visible(selector, timeout=2000): logger.info(f"Weiter-Button gefunden: {selector}") page.click(selector) self.automation.human_behavior.random_delay(1, 2) return True except Exception as e: logger.debug(f"Selektor {selector} nicht gefunden: {e}") continue # Fallback: Suche nach Button mit Text "Weiter" try: button = page.get_by_role("button").filter(has_text="Weiter").first if button.is_visible(): logger.info("Weiter-Button über get_by_role gefunden") button.click() self.automation.human_behavior.random_delay(1, 2) return True except: pass # Screenshot für Debugging self.automation._take_screenshot("next_button_not_found") logger.error("Keinen Weiter-Button gefunden") return False except Exception as e: logger.error(f"Fehler beim Klicken auf Weiter: {e}") return False def _is_additional_info_required(self) -> bool: """ Prüft, ob zusätzliche Informationen (Telefonnummer/E-Mail) angefordert werden. Returns: bool: True wenn zusätzliche Info benötigt wird """ try: page = self.automation.browser.page # Prüfe auf Sicherheitsabfrage security_indicators = [ 'text="Gib deine Telefonnummer oder E-Mail-Adresse ein"', 'text="Enter your phone number or email address"', 'input[name="text"][placeholder*="Telefon"]', 'input[name="text"][placeholder*="phone"]' ] for indicator in security_indicators: if page.is_visible(indicator, timeout=2000): logger.info("Zusätzliche Sicherheitsinformationen erforderlich") return True return False except Exception as e: logger.debug(f"Keine zusätzlichen Informationen erforderlich: {e}") return False def _handle_additional_info_request(self, phone_number: Optional[str], email: Optional[str]) -> bool: """ Behandelt die Anfrage nach zusätzlichen Sicherheitsinformationen. Args: phone_number: Optionale Telefonnummer email: Optionale E-Mail-Adresse Returns: bool: True bei Erfolg, False bei Fehler """ try: if not phone_number and not email: logger.error("Keine zusätzlichen Informationen verfügbar") return False page = self.automation.browser.page # Eingabefeld finden input_field = page.wait_for_selector('input[name="text"]', timeout=5000) if not input_field: return False # Bevorzuge Telefonnummer, dann E-Mail info_to_enter = phone_number if phone_number else email logger.info(f"Gebe zusätzliche Information ein: {info_to_enter[:3]}...") self.automation.human_behavior.type_text(input_field, info_to_enter) self.automation.human_behavior.random_delay(0.5, 1) # Weiter klicken return self._click_next() except Exception as e: logger.error(f"Fehler bei zusätzlichen Informationen: {e}") return False def _enter_password(self, password: str) -> bool: """ Gibt das Passwort ein. Args: password: Passwort Returns: bool: True bei Erfolg, False bei Fehler """ try: page = self.automation.browser.page # Warte kurz, bis die Seite vollständig geladen ist self.automation.human_behavior.random_delay(1, 2) # Passwort-Eingabefeld Selektoren - erweitert um spezifische Selektoren password_selectors = [ 'input[name="password"][autocomplete="current-password"]', # Spezifischer Selektor 'input[type="password"][name="password"]', # Spezifisch mit beiden Attributen 'input[name="password"]', # Name-basiert 'input[type="password"]', # Type-basiert self.selectors.LOGIN["password_input"], self.selectors.LOGIN["password_input_alt"], 'input[autocomplete="current-password"]' # Autocomplete-basiert ] for selector in password_selectors: try: # Warte auf sichtbares Passwortfeld password_field = page.wait_for_selector(selector, state="visible", timeout=5000) if password_field: logger.info(f"Passwort-Eingabefeld gefunden: {selector}") # Klicke zuerst auf das Feld, um es zu fokussieren password_field.click() self.automation.human_behavior.random_delay(0.3, 0.5) # Lösche eventuell vorhandenen Text password_field.fill("") # Tippe das Passwort menschlich ein - Buchstabe für Buchstabe for char in password: password_field.type(char) # Zufällige Verzögerung zwischen Zeichen (30-100ms für Passwörter) delay = random.uniform(0.03, 0.10) time.sleep(delay) self.automation.human_behavior.random_delay(0.5, 1) # Screenshot nach Eingabe self.automation._take_screenshot("after_password_input") return True except Exception as e: logger.debug(f"Selektor {selector} nicht gefunden: {e}") continue # Wenn nichts gefunden wurde, Screenshot für Debugging self.automation._take_screenshot("password_input_not_found") logger.error("Kein Passwort-Eingabefeld gefunden") return False except Exception as e: logger.error(f"Fehler beim Eingeben des Passworts: {e}") return False def _submit_login(self) -> bool: """ Schickt das Login-Formular ab. Returns: bool: True bei Erfolg, False bei Fehler """ try: page = self.automation.browser.page # Warte kurz self.automation.human_behavior.random_delay(0.5, 1) # Login-Submit-Button Selektoren - erweitert submit_selectors = [ 'button[role="button"]:has-text("Anmelden")', # Spezifisch für button 'button:has-text("Anmelden")', # Generischer button 'div[role="button"] span:has-text("Anmelden")', # Span innerhalb div '//button[contains(., "Anmelden")]', # XPath Alternative self.selectors.LOGIN["login_submit"], self.selectors.LOGIN["login_submit_en"], 'div[role="button"]:has-text("Anmelden")', 'div[role="button"]:has-text("Log in")', 'button:has-text("Log in")', '[role="button"][type="button"]' # Generisch basierend auf Attributen ] for selector in submit_selectors: try: # Versuche verschiedene Methoden if selector.startswith('//'): # XPath element = page.locator(selector).first if element.is_visible(timeout=2000): logger.info(f"Login-Submit-Button gefunden (XPath): {selector}") element.click() self.automation.human_behavior.random_delay(2, 3) self.automation._take_screenshot("after_login_button_click") return True else: # CSS Selector if page.is_visible(selector, timeout=2000): logger.info(f"Login-Submit-Button gefunden: {selector}") page.click(selector) self.automation.human_behavior.random_delay(2, 3) self.automation._take_screenshot("after_login_button_click") return True except Exception as e: logger.debug(f"Selektor {selector} nicht gefunden: {e}") continue # Fallback: Suche nach Button mit Text "Anmelden" try: button = page.get_by_role("button").filter(has_text="Anmelden").first if button.is_visible(): logger.info("Login-Submit-Button über get_by_role gefunden") button.click() self.automation.human_behavior.random_delay(2, 3) self.automation._take_screenshot("after_login_button_click") return True except: pass # Screenshot für Debugging self.automation._take_screenshot("login_submit_button_not_found") logger.error("Keinen Login-Submit-Button gefunden") return False except Exception as e: logger.error(f"Fehler beim Abschicken des Logins: {e}") return False def _handle_login_challenges(self) -> Dict[str, Any]: """ Behandelt eventuelle Login-Challenges (Captcha, Verifizierung, etc.). Returns: Dict[str, Any]: Ergebnis der Challenge-Behandlung """ try: page = self.automation.browser.page # Warte kurz auf eventuelle Challenges self.automation.human_behavior.random_delay(2, 3) # Prüfe auf Captcha if page.is_visible(self.selectors.VERIFICATION["captcha_frame"], timeout=2000): logger.warning("Captcha erkannt - manuelle Lösung erforderlich") return { "success": False, "error": "Captcha erkannt - manuelle Intervention erforderlich" } # Prüfe auf Arkose Challenge if page.is_visible(self.selectors.VERIFICATION["challenge_frame"], timeout=2000): logger.warning("Arkose Challenge erkannt") return { "success": False, "error": "Arkose Challenge erkannt - manuelle Intervention erforderlich" } # Prüfe auf Fehlermeldungen error_selectors = [ self.selectors.ERRORS["invalid_credentials"], self.selectors.ERRORS["error_message"], self.selectors.ERRORS["error_alert"] ] for error_selector in error_selectors: if page.is_visible(error_selector, timeout=1000): error_text = page.text_content(error_selector) logger.error(f"Login-Fehler: {error_text}") return { "success": False, "error": f"Login fehlgeschlagen: {error_text}" } # Keine Challenges erkannt return {"success": True} except Exception as e: logger.error(f"Fehler bei Challenge-Behandlung: {e}") return { "success": False, "error": f"Fehler bei Challenge-Behandlung: {str(e)}" } def _verify_login_success(self) -> bool: """ Verifiziert, ob der Login erfolgreich war. Returns: bool: True bei Erfolg, False bei Fehler """ try: page = self.automation.browser.page # Warte auf Weiterleitung self.automation.human_behavior.random_delay(2, 3) # Erfolgsindikatoren success_indicators = [ self.selectors.NAVIGATION["home_link"], self.selectors.NAVIGATION["tweet_button"], self.selectors.NAVIGATION["primary_nav"], 'a[href="/home"]', 'nav[aria-label="Primary"]', 'div[data-testid="primaryColumn"]' ] for indicator in success_indicators: if page.is_visible(indicator, timeout=5000): logger.info(f"Login erfolgreich - Indikator gefunden: {indicator}") # Finaler Screenshot self.automation._take_screenshot("login_success") return True # Prüfe URL als letzten Check current_url = page.url if "/home" in current_url: logger.info("Login erfolgreich - Home-URL erreicht") return True logger.error("Keine Login-Erfolgsindikatoren gefunden") return False except Exception as e: logger.error(f"Fehler bei Login-Verifizierung: {e}") return False