Initial commit
Dieser Commit ist enthalten in:
25
social_networks/x/__init__.py
Normale Datei
25
social_networks/x/__init__.py
Normale Datei
@ -0,0 +1,25 @@
|
||||
# social_networks/x/__init__.py
|
||||
|
||||
"""
|
||||
X (Twitter) Automatisierungsmodul
|
||||
"""
|
||||
|
||||
from .x_automation import XAutomation
|
||||
from .x_registration import XRegistration
|
||||
from .x_login import XLogin
|
||||
from .x_verification import XVerification
|
||||
from .x_ui_helper import XUIHelper
|
||||
from .x_utils import XUtils
|
||||
from .x_selectors import XSelectors
|
||||
from .x_workflow import XWorkflow
|
||||
|
||||
__all__ = [
|
||||
'XAutomation',
|
||||
'XRegistration',
|
||||
'XLogin',
|
||||
'XVerification',
|
||||
'XUIHelper',
|
||||
'XUtils',
|
||||
'XSelectors',
|
||||
'XWorkflow'
|
||||
]
|
||||
392
social_networks/x/x_automation.py
Normale Datei
392
social_networks/x/x_automation.py
Normale Datei
@ -0,0 +1,392 @@
|
||||
"""
|
||||
X (Twitter) Automatisierung - Hauptklasse für X-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 .x_registration import XRegistration
|
||||
from .x_login import XLogin
|
||||
from .x_verification import XVerification
|
||||
from .x_ui_helper import XUIHelper
|
||||
from .x_utils import XUtils
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("x_automation")
|
||||
|
||||
class XAutomation(BaseAutomation):
|
||||
"""
|
||||
Hauptklasse für die X (Twitter) Automatisierung.
|
||||
Implementiert die Registrierung und Anmeldung bei X.
|
||||
"""
|
||||
|
||||
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 X-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 = XRegistration(self)
|
||||
self.login = XLogin(self)
|
||||
self.verification = XVerification(self)
|
||||
self.ui_helper = XUIHelper(self)
|
||||
self.utils = XUtils(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("X-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 X-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 X-Account-Registrierung für '{full_name}' via {registration_method}")
|
||||
self._emit_customer_log(f"🐦 X-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 X-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 X-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 X-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 X-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 {}
|
||||
669
social_networks/x/x_login.py
Normale Datei
669
social_networks/x/x_login.py
Normale Datei
@ -0,0 +1,669 @@
|
||||
# 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
|
||||
1096
social_networks/x/x_registration.py
Normale Datei
1096
social_networks/x/x_registration.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
178
social_networks/x/x_selectors.py
Normale Datei
178
social_networks/x/x_selectors.py
Normale Datei
@ -0,0 +1,178 @@
|
||||
# social_networks/x/x_selectors.py
|
||||
|
||||
"""
|
||||
X (Twitter) Selektoren - Zentrale Sammlung aller CSS-Selektoren für X
|
||||
"""
|
||||
|
||||
class XSelectors:
|
||||
"""
|
||||
Zentrale Klasse für alle X-spezifischen CSS-Selektoren.
|
||||
Organisiert nach Funktionsbereichen.
|
||||
"""
|
||||
|
||||
# === ALLGEMEINE SELEKTOREN ===
|
||||
COOKIE_ACCEPT_BUTTONS = [
|
||||
"button:has-text('Accept all')",
|
||||
"button:has-text('Alle akzeptieren')",
|
||||
"button:has-text('Accept')",
|
||||
"button:has-text('Akzeptieren')"
|
||||
]
|
||||
|
||||
# === REGISTRIERUNG ===
|
||||
REGISTRATION = {
|
||||
# Hauptseite
|
||||
"create_account_button": 'text="Account erstellen"', # Robustester Selektor
|
||||
"create_account_button_en": 'text="Create account"', # Englische Version
|
||||
"create_account_button_alt": 'span:has-text("Account erstellen")', # Alternative
|
||||
"create_account_button_div": 'div[dir="ltr"] >> text="Account erstellen"', # Spezifischer
|
||||
|
||||
# Formularfelder
|
||||
"name_input": 'input[name="name"]',
|
||||
"email_input": 'input[name="email"]',
|
||||
"phone_input": 'input[name="phone"]',
|
||||
|
||||
# Geburtsdatum Dropdowns
|
||||
"month_select": 'select#SELECTOR_1',
|
||||
"day_select": 'select#SELECTOR_2',
|
||||
"year_select": 'select#SELECTOR_3',
|
||||
|
||||
# Buttons
|
||||
"next_button_birthday": 'button[data-testid="ocfSignupNextLink"]',
|
||||
"next_button_settings": 'button[data-testid="ocfSettingsListNextButton"]',
|
||||
"register_button": 'button[data-testid="LoginForm_Login_Button"]',
|
||||
|
||||
# Verifizierung
|
||||
"verification_code_input": 'input[autocomplete="one-time-code"]',
|
||||
"verification_code_label": 'div:has-text("Verifizierungscode")',
|
||||
|
||||
# Passwort
|
||||
"password_input": 'input[type="password"]',
|
||||
"password_label": 'div:has-text("Passwort")',
|
||||
|
||||
# Skip-Buttons
|
||||
"skip_profile_picture": 'button[data-testid="ocfSelectAvatarSkipForNowButton"]',
|
||||
"skip_username": 'button[data-testid="ocfEnterUsernameSkipButton"]',
|
||||
"skip_notifications": 'button:has-text("Vorerst überspringen")',
|
||||
"skip_for_now": 'button:has-text("Nicht jetzt")'
|
||||
}
|
||||
|
||||
# === LOGIN ===
|
||||
LOGIN = {
|
||||
# Login-Buttons
|
||||
"login_button": 'a[href="/login"]',
|
||||
"login_button_alt": 'div:has-text("Anmelden")',
|
||||
|
||||
# Formularfelder
|
||||
"username_input": 'input[autocomplete="username"]',
|
||||
"email_or_username_input": 'input[name="text"]',
|
||||
"password_input": 'input[type="password"]',
|
||||
"password_input_alt": 'input[name="password"]',
|
||||
|
||||
# Submit-Buttons
|
||||
"next_button": 'div[role="button"]:has-text("Weiter")',
|
||||
"next_button_en": 'div[role="button"]:has-text("Next")',
|
||||
"login_submit": 'div[role="button"]:has-text("Anmelden")',
|
||||
"login_submit_en": 'div[role="button"]:has-text("Log in")'
|
||||
}
|
||||
|
||||
# === NAVIGATION ===
|
||||
NAVIGATION = {
|
||||
# Hauptnavigation
|
||||
"home_link": 'a[href="/home"]',
|
||||
"explore_link": 'a[href="/explore"]',
|
||||
"notifications_link": 'a[href="/notifications"]',
|
||||
"messages_link": 'a[href="/messages"]',
|
||||
"profile_link": 'a[data-testid="AppTabBar_Profile_Link"]',
|
||||
|
||||
# Tweet/Post-Buttons
|
||||
"tweet_button": 'a[data-testid="SideNav_NewTweet_Button"]',
|
||||
"tweet_button_inline": 'button[data-testid="tweetButtonInline"]',
|
||||
|
||||
# Navigation Container
|
||||
"primary_nav": 'nav[aria-label="Primary"]',
|
||||
"sidebar": 'div[data-testid="sidebarColumn"]'
|
||||
}
|
||||
|
||||
# === PROFIL ===
|
||||
PROFILE = {
|
||||
# Profilbearbeitung
|
||||
"edit_profile_button": 'button:has-text("Profil bearbeiten")',
|
||||
"edit_profile_button_en": 'button:has-text("Edit profile")',
|
||||
|
||||
# Formularfelder
|
||||
"display_name_input": 'input[name="displayName"]',
|
||||
"bio_textarea": 'textarea[name="description"]',
|
||||
"location_input": 'input[name="location"]',
|
||||
"website_input": 'input[name="url"]',
|
||||
|
||||
# Speichern
|
||||
"save_button": 'button:has-text("Speichern")',
|
||||
"save_button_en": 'button:has-text("Save")'
|
||||
}
|
||||
|
||||
# === VERIFIZIERUNG ===
|
||||
VERIFICATION = {
|
||||
# Challenge/Captcha
|
||||
"challenge_frame": 'iframe[title="arkose-challenge"]',
|
||||
"captcha_frame": 'iframe[src*="recaptcha"]',
|
||||
|
||||
# Telefonnummer-Verifizierung
|
||||
"phone_verification_input": 'input[name="phone_number"]',
|
||||
"send_code_button": 'button:has-text("Code senden")',
|
||||
"verification_code_input": 'input[name="verification_code"]'
|
||||
}
|
||||
|
||||
# === FEHLER UND WARNUNGEN ===
|
||||
ERRORS = {
|
||||
# Fehlermeldungen
|
||||
"error_message": 'div[data-testid="toast"]',
|
||||
"error_alert": 'div[role="alert"]',
|
||||
"rate_limit_message": 'span:has-text("versuchen Sie es später")',
|
||||
"suspended_message": 'span:has-text("gesperrt")',
|
||||
|
||||
# Spezifische Fehler
|
||||
"email_taken": 'span:has-text("E-Mail-Adresse wird bereits verwendet")',
|
||||
"invalid_credentials": 'span:has-text("Falscher Benutzername oder falsches Passwort")'
|
||||
}
|
||||
|
||||
# === MODALE DIALOGE ===
|
||||
MODALS = {
|
||||
# Allgemeine Modale
|
||||
"modal_container": 'div[role="dialog"]',
|
||||
"modal_close_button": 'div[aria-label="Schließen"]',
|
||||
"modal_close_button_en": 'div[aria-label="Close"]',
|
||||
|
||||
# Bestätigungsdialoge
|
||||
"confirm_button": 'button:has-text("Bestätigen")',
|
||||
"cancel_button": 'button:has-text("Abbrechen")'
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_selector(cls, category: str, key: str) -> str:
|
||||
"""
|
||||
Holt einen spezifischen Selektor.
|
||||
|
||||
Args:
|
||||
category: Kategorie (z.B. "REGISTRATION", "LOGIN")
|
||||
key: Schlüssel innerhalb der Kategorie
|
||||
|
||||
Returns:
|
||||
str: CSS-Selektor oder None
|
||||
"""
|
||||
category_dict = getattr(cls, category.upper(), {})
|
||||
if isinstance(category_dict, dict):
|
||||
return category_dict.get(key)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_all_selectors(cls, category: str) -> dict:
|
||||
"""
|
||||
Holt alle Selektoren einer Kategorie.
|
||||
|
||||
Args:
|
||||
category: Kategorie
|
||||
|
||||
Returns:
|
||||
dict: Alle Selektoren der Kategorie
|
||||
"""
|
||||
return getattr(cls, category.upper(), {})
|
||||
424
social_networks/x/x_ui_helper.py
Normale Datei
424
social_networks/x/x_ui_helper.py
Normale Datei
@ -0,0 +1,424 @@
|
||||
# social_networks/x/x_ui_helper.py
|
||||
|
||||
"""
|
||||
X (Twitter) UI Helper - Hilfsklasse für UI-Interaktionen bei X
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from playwright.sync_api import Page, ElementHandle
|
||||
|
||||
from .x_selectors import XSelectors
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("x_ui_helper")
|
||||
|
||||
class XUIHelper:
|
||||
"""
|
||||
Hilfsklasse für UI-Interaktionen mit X.
|
||||
Bietet wiederverwendbare Methoden für häufige UI-Operationen.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert den X UI Helper.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
self.selectors = XSelectors()
|
||||
|
||||
logger.debug("X UI Helper initialisiert")
|
||||
|
||||
def wait_for_element(self, selector: str, timeout: int = 10000,
|
||||
state: str = "visible") -> Optional[ElementHandle]:
|
||||
"""
|
||||
Wartet auf ein Element und gibt es zurück.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor
|
||||
timeout: Timeout in Millisekunden
|
||||
state: Gewünschter Zustand ("visible", "attached", "detached", "hidden")
|
||||
|
||||
Returns:
|
||||
Optional[ElementHandle]: Element oder None
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
element = page.wait_for_selector(selector, timeout=timeout, state=state)
|
||||
return element
|
||||
except Exception as e:
|
||||
logger.debug(f"Element nicht gefunden: {selector} - {e}")
|
||||
return None
|
||||
|
||||
def click_element_safely(self, selector: str, timeout: int = 10000,
|
||||
retry_count: int = 3) -> bool:
|
||||
"""
|
||||
Klickt sicher auf ein Element mit Retry-Logik.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor
|
||||
timeout: Timeout in Millisekunden
|
||||
retry_count: Anzahl der Wiederholungsversuche
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
page = self.automation.browser.page
|
||||
|
||||
for attempt in range(retry_count):
|
||||
try:
|
||||
# Warte auf Element
|
||||
element = self.wait_for_element(selector, timeout)
|
||||
if not element:
|
||||
logger.warning(f"Element nicht gefunden beim {attempt + 1}. Versuch: {selector}")
|
||||
continue
|
||||
|
||||
# Scrolle zum Element
|
||||
element.scroll_into_view_if_needed()
|
||||
|
||||
# Warte kurz
|
||||
self.automation.human_behavior.random_delay(0.3, 0.7)
|
||||
|
||||
# Klicke
|
||||
element.click()
|
||||
logger.info(f"Element erfolgreich geklickt: {selector}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Klicken (Versuch {attempt + 1}): {e}")
|
||||
if attempt < retry_count - 1:
|
||||
self.automation.human_behavior.random_delay(1, 2)
|
||||
continue
|
||||
|
||||
logger.error(f"Konnte Element nicht klicken nach {retry_count} Versuchen: {selector}")
|
||||
return False
|
||||
|
||||
def type_text_safely(self, selector: str, text: str, clear_first: bool = True,
|
||||
timeout: int = 10000) -> bool:
|
||||
"""
|
||||
Gibt Text sicher in ein Eingabefeld ein.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor
|
||||
text: Einzugebender Text
|
||||
clear_first: Ob das Feld zuerst geleert werden soll
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
element = self.wait_for_element(selector, timeout)
|
||||
if not element:
|
||||
logger.error(f"Eingabefeld nicht gefunden: {selector}")
|
||||
return False
|
||||
|
||||
# Fokussiere das Element
|
||||
element.focus()
|
||||
self.automation.human_behavior.random_delay(0.2, 0.5)
|
||||
|
||||
# Leere das Feld wenn gewünscht
|
||||
if clear_first:
|
||||
element.click(click_count=3) # Alles auswählen
|
||||
self.automation.human_behavior.random_delay(0.1, 0.3)
|
||||
element.press("Delete")
|
||||
self.automation.human_behavior.random_delay(0.2, 0.5)
|
||||
|
||||
# Tippe den Text
|
||||
self.automation.human_behavior.type_text(element, text)
|
||||
|
||||
logger.info(f"Text erfolgreich eingegeben in: {selector}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Texteingeben: {e}")
|
||||
return False
|
||||
|
||||
def select_dropdown_option(self, selector: str, value: str, timeout: int = 10000) -> bool:
|
||||
"""
|
||||
Wählt eine Option aus einem Dropdown-Menü.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor des Select-Elements
|
||||
value: Wert der zu wählenden Option
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Warte auf Select-Element
|
||||
select = self.wait_for_element(selector, timeout)
|
||||
if not select:
|
||||
logger.error(f"Dropdown nicht gefunden: {selector}")
|
||||
return False
|
||||
|
||||
# Wähle Option
|
||||
page.select_option(selector, value)
|
||||
|
||||
logger.info(f"Option '{value}' ausgewählt in: {selector}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Auswählen der Dropdown-Option: {e}")
|
||||
return False
|
||||
|
||||
def handle_modal(self, action: str = "accept", timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Behandelt modale Dialoge.
|
||||
|
||||
Args:
|
||||
action: "accept", "dismiss" oder "close"
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn Modal behandelt wurde, False sonst
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Prüfe ob Modal vorhanden
|
||||
modal = self.wait_for_element(self.selectors.MODALS["modal_container"], timeout)
|
||||
if not modal:
|
||||
logger.debug("Kein Modal gefunden")
|
||||
return False
|
||||
|
||||
if action == "accept":
|
||||
# Suche Bestätigen-Button
|
||||
if self.click_element_safely(self.selectors.MODALS["confirm_button"], timeout=3000):
|
||||
logger.info("Modal bestätigt")
|
||||
return True
|
||||
|
||||
elif action == "dismiss":
|
||||
# Suche Abbrechen-Button
|
||||
if self.click_element_safely(self.selectors.MODALS["cancel_button"], timeout=3000):
|
||||
logger.info("Modal abgebrochen")
|
||||
return True
|
||||
|
||||
elif action == "close":
|
||||
# Suche Schließen-Button
|
||||
close_selectors = [
|
||||
self.selectors.MODALS["modal_close_button"],
|
||||
self.selectors.MODALS["modal_close_button_en"]
|
||||
]
|
||||
for selector in close_selectors:
|
||||
if self.click_element_safely(selector, timeout=3000):
|
||||
logger.info("Modal geschlossen")
|
||||
return True
|
||||
|
||||
logger.warning(f"Konnte Modal nicht mit Aktion '{action}' behandeln")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Modal-Behandlung: {e}")
|
||||
return False
|
||||
|
||||
def check_for_errors(self, timeout: int = 2000) -> Optional[str]:
|
||||
"""
|
||||
Prüft auf Fehlermeldungen auf der Seite.
|
||||
|
||||
Args:
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
Optional[str]: Fehlermeldung wenn gefunden, sonst None
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Prüfe alle Fehler-Selektoren
|
||||
for category, selector in [
|
||||
("error_message", self.selectors.ERRORS["error_message"]),
|
||||
("error_alert", self.selectors.ERRORS["error_alert"]),
|
||||
("rate_limit", self.selectors.ERRORS["rate_limit_message"]),
|
||||
("suspended", self.selectors.ERRORS["suspended_message"]),
|
||||
("email_taken", self.selectors.ERRORS["email_taken"]),
|
||||
("invalid_credentials", self.selectors.ERRORS["invalid_credentials"])
|
||||
]:
|
||||
error_element = self.wait_for_element(selector, timeout=timeout)
|
||||
if error_element:
|
||||
error_text = error_element.text_content()
|
||||
logger.warning(f"Fehler gefunden ({category}): {error_text}")
|
||||
return error_text
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Fehler bei Fehlerprüfung: {e}")
|
||||
return None
|
||||
|
||||
def wait_for_navigation(self, timeout: int = 30000) -> bool:
|
||||
"""
|
||||
Wartet auf Navigation/Seitenwechsel.
|
||||
|
||||
Args:
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn Navigation erfolgt ist
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
with page.expect_navigation(timeout=timeout):
|
||||
pass
|
||||
|
||||
logger.info("Navigation abgeschlossen")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Keine Navigation erkannt: {e}")
|
||||
return False
|
||||
|
||||
def scroll_to_bottom(self, smooth: bool = True, pause_time: float = 1.0):
|
||||
"""
|
||||
Scrollt zum Ende der Seite.
|
||||
|
||||
Args:
|
||||
smooth: Ob sanft gescrollt werden soll
|
||||
pause_time: Pausenzeit nach dem Scrollen
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
if smooth:
|
||||
# Sanftes Scrollen in Schritten
|
||||
page.evaluate("""
|
||||
async () => {
|
||||
const distance = 100;
|
||||
const delay = 50;
|
||||
const timer = setInterval(() => {
|
||||
window.scrollBy(0, distance);
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
""")
|
||||
else:
|
||||
# Direktes Scrollen
|
||||
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
|
||||
time.sleep(pause_time)
|
||||
logger.debug("Zum Seitenende gescrollt")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Scrollen: {e}")
|
||||
|
||||
def take_element_screenshot(self, selector: str, filename: str,
|
||||
timeout: int = 10000) -> bool:
|
||||
"""
|
||||
Macht einen Screenshot von einem spezifischen Element.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor
|
||||
filename: Dateiname für den Screenshot
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
element = self.wait_for_element(selector, timeout)
|
||||
if not element:
|
||||
logger.error(f"Element für Screenshot nicht gefunden: {selector}")
|
||||
return False
|
||||
|
||||
# Screenshot vom Element
|
||||
element.screenshot(path=f"{self.automation.screenshots_dir}/{filename}")
|
||||
logger.info(f"Element-Screenshot gespeichert: {filename}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Element-Screenshot: {e}")
|
||||
return False
|
||||
|
||||
def get_element_text(self, selector: str, timeout: int = 10000) -> Optional[str]:
|
||||
"""
|
||||
Holt den Text eines Elements.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
Optional[str]: Text des Elements oder None
|
||||
"""
|
||||
try:
|
||||
element = self.wait_for_element(selector, timeout)
|
||||
if element:
|
||||
return element.text_content()
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen des Element-Texts: {e}")
|
||||
return None
|
||||
|
||||
def is_element_visible(self, selector: str, timeout: int = 1000) -> bool:
|
||||
"""
|
||||
Prüft ob ein Element sichtbar ist.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn sichtbar, False sonst
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
# is_visible hat kein timeout parameter in Playwright
|
||||
# Verwende wait_for_selector für timeout-Funktionalität
|
||||
try:
|
||||
element = page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
return element is not None
|
||||
except:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug(f"Element nicht sichtbar: {selector} - {e}")
|
||||
return False
|
||||
|
||||
def wait_for_any_selector(self, selectors: List[str], timeout: int = 10000) -> Optional[Tuple[str, ElementHandle]]:
|
||||
"""
|
||||
Wartet auf eines von mehreren Elementen.
|
||||
|
||||
Args:
|
||||
selectors: Liste von CSS-Selektoren
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
Optional[Tuple[str, ElementHandle]]: Tuple aus Selektor und Element oder None
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Erstelle Promise für jeden Selektor
|
||||
promises = []
|
||||
for selector in selectors:
|
||||
promises.append(page.wait_for_selector(selector, timeout=timeout))
|
||||
|
||||
# Warte auf das erste Element
|
||||
element = page.evaluate(f"""
|
||||
() => {{
|
||||
const selectors = {selectors};
|
||||
for (const selector of selectors) {{
|
||||
const element = document.querySelector(selector);
|
||||
if (element) return {{selector, found: true}};
|
||||
}}
|
||||
return {{found: false}};
|
||||
}}
|
||||
""")
|
||||
|
||||
if element["found"]:
|
||||
actual_element = page.query_selector(element["selector"])
|
||||
return (element["selector"], actual_element)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Keines der Elemente gefunden: {e}")
|
||||
return None
|
||||
379
social_networks/x/x_utils.py
Normale Datei
379
social_networks/x/x_utils.py
Normale Datei
@ -0,0 +1,379 @@
|
||||
# social_networks/x/x_utils.py
|
||||
|
||||
"""
|
||||
X (Twitter) Utils - Utility-Funktionen für X-Automatisierung
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("x_utils")
|
||||
|
||||
class XUtils:
|
||||
"""
|
||||
Utility-Klasse mit Hilfsfunktionen für X-Automatisierung.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert X Utils.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
|
||||
logger.debug("X Utils initialisiert")
|
||||
|
||||
@staticmethod
|
||||
def validate_username(username: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validiert einen X-Benutzernamen.
|
||||
|
||||
Args:
|
||||
username: Zu validierender Benutzername
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (Gültig, Fehlermeldung wenn ungültig)
|
||||
"""
|
||||
# Längenprüfung
|
||||
if len(username) < 1:
|
||||
return False, "Benutzername ist zu kurz (mindestens 1 Zeichen)"
|
||||
if len(username) > 15:
|
||||
return False, "Benutzername ist zu lang (maximal 15 Zeichen)"
|
||||
|
||||
# Zeichenprüfung (nur Buchstaben, Zahlen und Unterstrich)
|
||||
if not re.match(r'^[a-zA-Z0-9_]+$', username):
|
||||
return False, "Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten"
|
||||
|
||||
# Verbotene Muster
|
||||
forbidden_patterns = ["twitter", "admin", "x.com", "root", "system"]
|
||||
username_lower = username.lower()
|
||||
for pattern in forbidden_patterns:
|
||||
if pattern in username_lower:
|
||||
return False, f"Benutzername darf '{pattern}' nicht enthalten"
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def validate_email(email: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validiert eine E-Mail-Adresse für X.
|
||||
|
||||
Args:
|
||||
email: Zu validierende E-Mail
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (Gültig, Fehlermeldung wenn ungültig)
|
||||
"""
|
||||
# Grundlegendes E-Mail-Pattern
|
||||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
|
||||
if not re.match(email_pattern, email):
|
||||
return False, "Ungültiges E-Mail-Format"
|
||||
|
||||
# Verbotene Domains
|
||||
forbidden_domains = ["example.com", "test.com", "temp-mail.com"]
|
||||
domain = email.split('@')[1].lower()
|
||||
|
||||
for forbidden in forbidden_domains:
|
||||
if forbidden in domain:
|
||||
return False, f"E-Mail-Domain '{forbidden}' ist nicht erlaubt"
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def validate_password(password: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validiert ein Passwort für X.
|
||||
|
||||
Args:
|
||||
password: Zu validierendes Passwort
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (Gültig, Fehlermeldung wenn ungültig)
|
||||
"""
|
||||
# Längenprüfung
|
||||
if len(password) < 8:
|
||||
return False, "Passwort muss mindestens 8 Zeichen lang sein"
|
||||
if len(password) > 128:
|
||||
return False, "Passwort darf maximal 128 Zeichen lang sein"
|
||||
|
||||
# Mindestens ein Kleinbuchstabe
|
||||
if not re.search(r'[a-z]', password):
|
||||
return False, "Passwort muss mindestens einen Kleinbuchstaben enthalten"
|
||||
|
||||
# Mindestens eine Zahl
|
||||
if not re.search(r'\d', password):
|
||||
return False, "Passwort muss mindestens eine Zahl enthalten"
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def generate_device_info() -> Dict[str, Any]:
|
||||
"""
|
||||
Generiert realistische Geräteinformationen.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Geräteinformationen
|
||||
"""
|
||||
devices = [
|
||||
{
|
||||
"type": "desktop",
|
||||
"os": "Windows",
|
||||
"browser": "Chrome",
|
||||
"screen": {"width": 1920, "height": 1080},
|
||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
},
|
||||
{
|
||||
"type": "desktop",
|
||||
"os": "macOS",
|
||||
"browser": "Safari",
|
||||
"screen": {"width": 2560, "height": 1440},
|
||||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||
},
|
||||
{
|
||||
"type": "mobile",
|
||||
"os": "iOS",
|
||||
"browser": "Safari",
|
||||
"screen": {"width": 414, "height": 896},
|
||||
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X)"
|
||||
},
|
||||
{
|
||||
"type": "mobile",
|
||||
"os": "Android",
|
||||
"browser": "Chrome",
|
||||
"screen": {"width": 412, "height": 915},
|
||||
"user_agent": "Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36"
|
||||
}
|
||||
]
|
||||
|
||||
return random.choice(devices)
|
||||
|
||||
@staticmethod
|
||||
def generate_session_id() -> str:
|
||||
"""
|
||||
Generiert eine realistische Session-ID.
|
||||
|
||||
Returns:
|
||||
str: Session-ID
|
||||
"""
|
||||
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
return ''.join(random.choice(chars) for _ in range(32))
|
||||
|
||||
def detect_language(self) -> str:
|
||||
"""
|
||||
Erkennt die aktuelle Sprache der X-Oberfläche.
|
||||
|
||||
Returns:
|
||||
str: Sprachcode (z.B. "de", "en")
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Prüfe HTML lang-Attribut
|
||||
lang = page.evaluate("() => document.documentElement.lang")
|
||||
if lang:
|
||||
return lang.split('-')[0] # z.B. "de" aus "de-DE"
|
||||
|
||||
# Fallback: Prüfe bekannte Texte
|
||||
if page.is_visible('text="Anmelden"'):
|
||||
return "de"
|
||||
elif page.is_visible('text="Log in"'):
|
||||
return "en"
|
||||
|
||||
return "en" # Standard
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Spracherkennung: {e}")
|
||||
return "en"
|
||||
|
||||
@staticmethod
|
||||
def format_phone_number(phone: str, country_code: str = "+49") -> str:
|
||||
"""
|
||||
Formatiert eine Telefonnummer für X.
|
||||
|
||||
Args:
|
||||
phone: Rohe Telefonnummer
|
||||
country_code: Ländervorwahl
|
||||
|
||||
Returns:
|
||||
str: Formatierte Telefonnummer
|
||||
"""
|
||||
# Entferne alle Nicht-Ziffern
|
||||
digits = re.sub(r'\D', '', phone)
|
||||
|
||||
# Füge Ländervorwahl hinzu wenn nicht vorhanden
|
||||
if not phone.startswith('+'):
|
||||
return f"{country_code}{digits}"
|
||||
|
||||
return f"+{digits}"
|
||||
|
||||
@staticmethod
|
||||
def parse_error_message(error_text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Analysiert X-Fehlermeldungen.
|
||||
|
||||
Args:
|
||||
error_text: Fehlermeldungstext
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Analysierte Fehlerinformationen
|
||||
"""
|
||||
error_info = {
|
||||
"type": "unknown",
|
||||
"message": error_text,
|
||||
"recoverable": True,
|
||||
"action": "retry"
|
||||
}
|
||||
|
||||
# Rate Limit
|
||||
if "zu viele" in error_text.lower() or "too many" in error_text.lower():
|
||||
error_info.update({
|
||||
"type": "rate_limit",
|
||||
"recoverable": True,
|
||||
"action": "wait",
|
||||
"wait_time": 900 # 15 Minuten
|
||||
})
|
||||
|
||||
# Account gesperrt
|
||||
elif "gesperrt" in error_text.lower() or "suspended" in error_text.lower():
|
||||
error_info.update({
|
||||
"type": "suspended",
|
||||
"recoverable": False,
|
||||
"action": "abort"
|
||||
})
|
||||
|
||||
# Ungültige Anmeldedaten
|
||||
elif "passwort" in error_text.lower() or "password" in error_text.lower():
|
||||
error_info.update({
|
||||
"type": "invalid_credentials",
|
||||
"recoverable": True,
|
||||
"action": "check_credentials"
|
||||
})
|
||||
|
||||
# E-Mail bereits verwendet
|
||||
elif "bereits verwendet" in error_text.lower() or "already" in error_text.lower():
|
||||
error_info.update({
|
||||
"type": "duplicate",
|
||||
"recoverable": True,
|
||||
"action": "use_different_email"
|
||||
})
|
||||
|
||||
return error_info
|
||||
|
||||
def wait_for_rate_limit(self, wait_time: int = None):
|
||||
"""
|
||||
Wartet bei Rate Limiting mit visueller Anzeige.
|
||||
|
||||
Args:
|
||||
wait_time: Wartezeit in Sekunden (None für zufällige Zeit)
|
||||
"""
|
||||
if wait_time is None:
|
||||
wait_time = random.randint(300, 600) # 5-10 Minuten
|
||||
|
||||
logger.info(f"Rate Limit erkannt - warte {wait_time} Sekunden")
|
||||
self.automation._emit_customer_log(f"⏳ Rate Limit - warte {wait_time // 60} Minuten...")
|
||||
|
||||
# Warte in Intervallen mit Status-Updates
|
||||
intervals = min(10, wait_time // 10)
|
||||
for i in range(intervals):
|
||||
time.sleep(wait_time // intervals)
|
||||
remaining = wait_time - (i + 1) * (wait_time // intervals)
|
||||
if remaining > 60:
|
||||
self.automation._emit_customer_log(f"⏳ Noch {remaining // 60} Minuten...")
|
||||
|
||||
@staticmethod
|
||||
def generate_bio() -> str:
|
||||
"""
|
||||
Generiert eine realistische Bio für ein X-Profil.
|
||||
|
||||
Returns:
|
||||
str: Generierte Bio
|
||||
"""
|
||||
templates = [
|
||||
"✈️ Explorer | 📚 Book lover | ☕ Coffee enthusiast",
|
||||
"Life is a journey 🌟 | Making memories 📸",
|
||||
"Student 📖 | Dreamer 💭 | Music lover 🎵",
|
||||
"Tech enthusiast 💻 | Always learning 🎯",
|
||||
"Living life one day at a time ✨",
|
||||
"Passionate about {interest} | {city} 📍",
|
||||
"Just here to share thoughts 💭",
|
||||
"{hobby} in my free time | DM for collabs",
|
||||
"Spreading positivity 🌈 | {emoji} lover"
|
||||
]
|
||||
|
||||
interests = ["photography", "travel", "coding", "art", "fitness", "cooking"]
|
||||
cities = ["Berlin", "Munich", "Hamburg", "Frankfurt", "Cologne"]
|
||||
hobbies = ["Gaming", "Reading", "Hiking", "Painting", "Yoga"]
|
||||
emojis = ["🎨", "🎮", "📚", "🎯", "🌸", "⭐"]
|
||||
|
||||
bio = random.choice(templates)
|
||||
bio = bio.replace("{interest}", random.choice(interests))
|
||||
bio = bio.replace("{city}", random.choice(cities))
|
||||
bio = bio.replace("{hobby}", random.choice(hobbies))
|
||||
bio = bio.replace("{emoji}", random.choice(emojis))
|
||||
|
||||
return bio
|
||||
|
||||
@staticmethod
|
||||
def calculate_age_from_birthday(birthday: Dict[str, int]) -> int:
|
||||
"""
|
||||
Berechnet das Alter aus einem Geburtstagsdatum.
|
||||
|
||||
Args:
|
||||
birthday: Dictionary mit 'day', 'month', 'year'
|
||||
|
||||
Returns:
|
||||
int: Berechnetes Alter
|
||||
"""
|
||||
birth_date = datetime(birthday['year'], birthday['month'], birthday['day'])
|
||||
today = datetime.now()
|
||||
age = today.year - birth_date.year
|
||||
|
||||
# Prüfe ob Geburtstag dieses Jahr schon war
|
||||
if (today.month, today.day) < (birth_date.month, birth_date.day):
|
||||
age -= 1
|
||||
|
||||
return age
|
||||
|
||||
def check_account_restrictions(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Prüft auf Account-Einschränkungen.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Informationen über Einschränkungen
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
restrictions = {
|
||||
"limited": False,
|
||||
"locked": False,
|
||||
"suspended": False,
|
||||
"verification_required": False
|
||||
}
|
||||
|
||||
# Prüfe auf verschiedene Einschränkungen
|
||||
if page.is_visible('text="Dein Account ist eingeschränkt"', timeout=1000):
|
||||
restrictions["limited"] = True
|
||||
logger.warning("Account ist eingeschränkt")
|
||||
|
||||
if page.is_visible('text="Account gesperrt"', timeout=1000):
|
||||
restrictions["suspended"] = True
|
||||
logger.error("Account ist gesperrt")
|
||||
|
||||
if page.is_visible('text="Verifizierung erforderlich"', timeout=1000):
|
||||
restrictions["verification_required"] = True
|
||||
logger.warning("Verifizierung erforderlich")
|
||||
|
||||
return restrictions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Einschränkungsprüfung: {e}")
|
||||
return {"error": str(e)}
|
||||
511
social_networks/x/x_verification.py
Normale Datei
511
social_networks/x/x_verification.py
Normale Datei
@ -0,0 +1,511 @@
|
||||
# social_networks/x/x_verification.py
|
||||
|
||||
"""
|
||||
X (Twitter) Verification - Klasse für Account-Verifizierungsprozesse bei X
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from .x_selectors import XSelectors
|
||||
from .x_workflow import XWorkflow
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("x_verification")
|
||||
|
||||
class XVerification:
|
||||
"""
|
||||
Klasse für die Verifizierung von X-Konten.
|
||||
Behandelt verschiedene Verifizierungsmethoden und Sicherheitsabfragen.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die X-Verifizierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
self.selectors = XSelectors()
|
||||
self.workflow = XWorkflow.get_verification_workflow()
|
||||
|
||||
logger.debug("X-Verifizierung initialisiert")
|
||||
|
||||
def verify_account(self, verification_code: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Verifizierungsprozess für einen X-Account durch.
|
||||
|
||||
Args:
|
||||
verification_code: Optionaler Verifizierungscode
|
||||
**kwargs: Weitere Parameter (method, phone_number, email)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung
|
||||
"""
|
||||
logger.info("Starte X-Account-Verifizierung")
|
||||
|
||||
try:
|
||||
# Prüfe welche Art von Verifizierung benötigt wird
|
||||
verification_type = self._detect_verification_type()
|
||||
|
||||
if not verification_type:
|
||||
logger.info("Keine Verifizierung erforderlich")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Keine Verifizierung erforderlich"
|
||||
}
|
||||
|
||||
logger.info(f"Verifizierungstyp erkannt: {verification_type}")
|
||||
self.automation._emit_customer_log(f"🔐 Verifizierung erforderlich: {verification_type}")
|
||||
|
||||
# Führe entsprechende Verifizierung durch
|
||||
if verification_type == "email":
|
||||
return self._handle_email_verification(verification_code, **kwargs)
|
||||
elif verification_type == "phone":
|
||||
return self._handle_phone_verification(verification_code, **kwargs)
|
||||
elif verification_type == "captcha":
|
||||
return self._handle_captcha_verification()
|
||||
elif verification_type == "arkose":
|
||||
return self._handle_arkose_challenge()
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unbekannter Verifizierungstyp: {verification_type}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der Verifizierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception"
|
||||
}
|
||||
|
||||
def _detect_verification_type(self) -> Optional[str]:
|
||||
"""
|
||||
Erkennt welche Art von Verifizierung benötigt wird.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Verifizierungstyp oder None
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# E-Mail-Verifizierung
|
||||
if page.is_visible(self.selectors.REGISTRATION["verification_code_input"], timeout=2000):
|
||||
return "email"
|
||||
|
||||
# Telefon-Verifizierung
|
||||
if page.is_visible(self.selectors.VERIFICATION["phone_verification_input"], timeout=2000):
|
||||
return "phone"
|
||||
|
||||
# Captcha
|
||||
if page.is_visible(self.selectors.VERIFICATION["captcha_frame"], timeout=2000):
|
||||
return "captcha"
|
||||
|
||||
# Arkose Challenge
|
||||
if page.is_visible(self.selectors.VERIFICATION["challenge_frame"], timeout=2000):
|
||||
return "arkose"
|
||||
|
||||
# Prüfe auf Text-Hinweise
|
||||
if page.is_visible('text="Verifiziere deine E-Mail"', timeout=1000):
|
||||
return "email"
|
||||
if page.is_visible('text="Verifiziere deine Telefonnummer"', timeout=1000):
|
||||
return "phone"
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Verifizierungstyp-Erkennung: {e}")
|
||||
return None
|
||||
|
||||
def _handle_email_verification(self, verification_code: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Behandelt E-Mail-Verifizierung.
|
||||
|
||||
Args:
|
||||
verification_code: Verifizierungscode
|
||||
**kwargs: Weitere Parameter (email)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Wenn kein Code übergeben wurde, versuche ihn abzurufen
|
||||
if not verification_code:
|
||||
email = kwargs.get("email")
|
||||
if not email:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "E-Mail-Adresse für Verifizierung fehlt"
|
||||
}
|
||||
|
||||
self.automation._emit_customer_log("📧 Warte auf Verifizierungs-E-Mail...")
|
||||
verification_code = self._retrieve_email_code(email)
|
||||
|
||||
if not verification_code:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte keinen Verifizierungscode aus E-Mail abrufen"
|
||||
}
|
||||
|
||||
# Code eingeben
|
||||
self.automation._emit_customer_log(f"✍️ Gebe Verifizierungscode ein: {verification_code}")
|
||||
if not self._enter_verification_code(verification_code):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Eingeben des Verifizierungscodes"
|
||||
}
|
||||
|
||||
# Bestätigen
|
||||
if not self._submit_verification():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Bestätigen der Verifizierung"
|
||||
}
|
||||
|
||||
# Warte auf Erfolg
|
||||
if self._check_verification_success():
|
||||
logger.info("E-Mail-Verifizierung erfolgreich")
|
||||
self.automation._emit_customer_log("✅ E-Mail erfolgreich verifiziert!")
|
||||
return {
|
||||
"success": True,
|
||||
"method": "email",
|
||||
"code": verification_code
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Verifizierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei E-Mail-Verifizierung: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"E-Mail-Verifizierung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _handle_phone_verification(self, verification_code: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Behandelt Telefon-Verifizierung.
|
||||
|
||||
Args:
|
||||
verification_code: Verifizierungscode
|
||||
**kwargs: Weitere Parameter (phone_number)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Telefonnummer eingeben wenn erforderlich
|
||||
phone_number = kwargs.get("phone_number")
|
||||
if phone_number and page.is_visible(self.selectors.VERIFICATION["phone_verification_input"]):
|
||||
logger.info(f"Gebe Telefonnummer ein: {phone_number}")
|
||||
phone_input = page.wait_for_selector(self.selectors.VERIFICATION["phone_verification_input"])
|
||||
self.automation.human_behavior.type_text(phone_input, phone_number)
|
||||
|
||||
# Code senden
|
||||
if page.is_visible(self.selectors.VERIFICATION["send_code_button"]):
|
||||
page.click(self.selectors.VERIFICATION["send_code_button"])
|
||||
self.automation.human_behavior.random_delay(2, 3)
|
||||
|
||||
# Wenn kein Code übergeben wurde, warte auf manuelle Eingabe
|
||||
if not verification_code:
|
||||
self.automation._emit_customer_log("📱 Bitte gib den SMS-Code manuell ein...")
|
||||
# Warte bis Code eingegeben wurde (max 5 Minuten)
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 300: # 5 Minuten
|
||||
code_input = page.query_selector(self.selectors.VERIFICATION["verification_code_input"])
|
||||
if code_input:
|
||||
current_value = code_input.get_attribute("value")
|
||||
if current_value and len(current_value) >= 6:
|
||||
verification_code = current_value
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
if not verification_code:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Kein SMS-Code eingegeben (Timeout)"
|
||||
}
|
||||
else:
|
||||
# Code eingeben
|
||||
self.automation._emit_customer_log(f"✍️ Gebe SMS-Code ein: {verification_code}")
|
||||
if not self._enter_verification_code(verification_code):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Eingeben des SMS-Codes"
|
||||
}
|
||||
|
||||
# Bestätigen
|
||||
if not self._submit_verification():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Bestätigen der Telefon-Verifizierung"
|
||||
}
|
||||
|
||||
# Warte auf Erfolg
|
||||
if self._check_verification_success():
|
||||
logger.info("Telefon-Verifizierung erfolgreich")
|
||||
self.automation._emit_customer_log("✅ Telefonnummer erfolgreich verifiziert!")
|
||||
return {
|
||||
"success": True,
|
||||
"method": "phone",
|
||||
"phone_number": phone_number
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Telefon-Verifizierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Telefon-Verifizierung: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Telefon-Verifizierung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _handle_captcha_verification(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Behandelt Captcha-Verifizierung.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung
|
||||
"""
|
||||
try:
|
||||
logger.warning("Captcha-Verifizierung erkannt")
|
||||
self.automation._emit_customer_log("🤖 Captcha erkannt - manuelle Lösung erforderlich")
|
||||
|
||||
# Screenshot für Debugging
|
||||
self.automation._take_screenshot("captcha_challenge")
|
||||
|
||||
# Hier könnte Integration mit Captcha-Solving-Service erfolgen
|
||||
# Für jetzt: Warte auf manuelle Lösung
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Captcha-Lösung erforderlich - bitte manuell lösen",
|
||||
"type": "captcha",
|
||||
"manual_intervention_required": True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Captcha-Behandlung: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Captcha-Behandlung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _handle_arkose_challenge(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Behandelt Arkose Labs Challenge.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung
|
||||
"""
|
||||
try:
|
||||
logger.warning("Arkose Challenge erkannt")
|
||||
self.automation._emit_customer_log("🛡️ Arkose Challenge erkannt - erweiterte Verifizierung erforderlich")
|
||||
|
||||
# Screenshot für Debugging
|
||||
self.automation._take_screenshot("arkose_challenge")
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Arkose Challenge erkannt - manuelle Intervention erforderlich",
|
||||
"type": "arkose",
|
||||
"manual_intervention_required": True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Arkose Challenge: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Arkose Challenge fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _retrieve_email_code(self, email: str) -> Optional[str]:
|
||||
"""
|
||||
Ruft Verifizierungscode aus E-Mail ab.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
|
||||
Returns:
|
||||
Optional[str]: Verifizierungscode oder None
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Rufe Verifizierungscode für {email} ab")
|
||||
|
||||
# Warte auf E-Mail
|
||||
self.automation.human_behavior.random_delay(5, 10)
|
||||
|
||||
# Hole E-Mails
|
||||
emails = self.automation.email_handler.get_emails(
|
||||
email,
|
||||
subject_filter="X Verifizierungscode"
|
||||
)
|
||||
|
||||
if not emails:
|
||||
# Versuche alternative Betreff-Filter
|
||||
emails = self.automation.email_handler.get_emails(
|
||||
email,
|
||||
subject_filter="Twitter"
|
||||
)
|
||||
|
||||
if not emails:
|
||||
logger.error("Keine Verifizierungs-E-Mail gefunden")
|
||||
return None
|
||||
|
||||
# Extrahiere Code aus der neuesten E-Mail
|
||||
latest_email = emails[0]
|
||||
subject = latest_email.get('subject', '')
|
||||
body = latest_email.get('body', '')
|
||||
|
||||
# Suche nach 6-stelligem Code
|
||||
# Erst im Betreff
|
||||
code_match = re.search(r'(\d{6})', subject)
|
||||
if code_match:
|
||||
code = code_match.group(1)
|
||||
logger.info(f"Code aus Betreff extrahiert: {code}")
|
||||
return code
|
||||
|
||||
# Dann im Body
|
||||
code_match = re.search(r'(\d{6})', body)
|
||||
if code_match:
|
||||
code = code_match.group(1)
|
||||
logger.info(f"Code aus E-Mail-Body extrahiert: {code}")
|
||||
return code
|
||||
|
||||
logger.error("Konnte keinen Code aus E-Mail extrahieren")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen des E-Mail-Codes: {e}")
|
||||
return None
|
||||
|
||||
def _enter_verification_code(self, code: str) -> bool:
|
||||
"""
|
||||
Gibt einen Verifizierungscode ein.
|
||||
|
||||
Args:
|
||||
code: Verifizierungscode
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Finde Code-Eingabefeld
|
||||
code_selectors = [
|
||||
self.selectors.REGISTRATION["verification_code_input"],
|
||||
self.selectors.VERIFICATION["verification_code_input"],
|
||||
'input[autocomplete="one-time-code"]',
|
||||
'input[name="code"]'
|
||||
]
|
||||
|
||||
for selector in code_selectors:
|
||||
try:
|
||||
code_input = page.wait_for_selector(selector, timeout=3000)
|
||||
if code_input:
|
||||
logger.info(f"Code-Eingabefeld gefunden: {selector}")
|
||||
self.automation.human_behavior.type_text(code_input, code)
|
||||
self.automation.human_behavior.random_delay(0.5, 1)
|
||||
return True
|
||||
except:
|
||||
continue
|
||||
|
||||
logger.error("Kein Code-Eingabefeld gefunden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Eingeben des Codes: {e}")
|
||||
return False
|
||||
|
||||
def _submit_verification(self) -> bool:
|
||||
"""
|
||||
Bestätigt die Verifizierung.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Submit-Button Selektoren
|
||||
submit_selectors = [
|
||||
'button:has-text("Weiter")',
|
||||
'button:has-text("Bestätigen")',
|
||||
'button:has-text("Verifizieren")',
|
||||
'button:has-text("Next")',
|
||||
'button:has-text("Verify")',
|
||||
'button:has-text("Submit")'
|
||||
]
|
||||
|
||||
for selector in submit_selectors:
|
||||
if page.is_visible(selector, timeout=2000):
|
||||
logger.info(f"Submit-Button gefunden: {selector}")
|
||||
page.click(selector)
|
||||
self.automation.human_behavior.random_delay(2, 3)
|
||||
return True
|
||||
|
||||
# Alternativ: Enter drücken
|
||||
page.keyboard.press("Enter")
|
||||
logger.info("Enter gedrückt zur Bestätigung")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Bestätigen: {e}")
|
||||
return False
|
||||
|
||||
def _check_verification_success(self) -> bool:
|
||||
"""
|
||||
Prüft ob Verifizierung erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Warte auf Weiterleitung oder Erfolgsmeldung
|
||||
self.automation.human_behavior.random_delay(2, 3)
|
||||
|
||||
# Erfolgsindikatoren
|
||||
success_indicators = [
|
||||
# Fehlen von Verifizierungsfeldern
|
||||
lambda: not page.is_visible(self.selectors.REGISTRATION["verification_code_input"], timeout=2000),
|
||||
# Vorhandensein von Account-Elementen
|
||||
lambda: page.is_visible(self.selectors.NAVIGATION["home_link"], timeout=2000),
|
||||
# URL-Änderung
|
||||
lambda: "/home" in page.url or "/welcome" in page.url
|
||||
]
|
||||
|
||||
for indicator in success_indicators:
|
||||
if indicator():
|
||||
logger.info("Verifizierung erfolgreich")
|
||||
return True
|
||||
|
||||
# Prüfe auf Fehlermeldungen
|
||||
error_msg = self.automation.ui_helper.check_for_errors()
|
||||
if error_msg:
|
||||
logger.error(f"Verifizierungsfehler: {error_msg}")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Erfolgsprüfung: {e}")
|
||||
return False
|
||||
329
social_networks/x/x_workflow.py
Normale Datei
329
social_networks/x/x_workflow.py
Normale Datei
@ -0,0 +1,329 @@
|
||||
# social_networks/x/x_workflow.py
|
||||
|
||||
"""
|
||||
X (Twitter) Workflow - Definiert die Arbeitsabläufe für X-Automatisierung
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
|
||||
class XWorkflow:
|
||||
"""
|
||||
Definiert strukturierte Workflows für verschiedene X-Operationen.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_registration_workflow() -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den Workflow für die Account-Registrierung zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Workflow-Definition
|
||||
"""
|
||||
return {
|
||||
"name": "X Account Registration",
|
||||
"steps": [
|
||||
{
|
||||
"id": "navigate",
|
||||
"name": "Navigate to X",
|
||||
"description": "Zur X-Startseite navigieren",
|
||||
"required": True,
|
||||
"retry_count": 3
|
||||
},
|
||||
{
|
||||
"id": "cookie_banner",
|
||||
"name": "Handle Cookie Banner",
|
||||
"description": "Cookie-Banner akzeptieren falls vorhanden",
|
||||
"required": False,
|
||||
"retry_count": 1
|
||||
},
|
||||
{
|
||||
"id": "create_account",
|
||||
"name": "Click Create Account",
|
||||
"description": "Account erstellen Button klicken",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "fill_initial_form",
|
||||
"name": "Fill Initial Form",
|
||||
"description": "Name und E-Mail eingeben",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "enter_birthday",
|
||||
"name": "Enter Birthday",
|
||||
"description": "Geburtsdatum auswählen",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "next_birthday",
|
||||
"name": "Continue After Birthday",
|
||||
"description": "Weiter nach Geburtsdatum klicken",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "next_settings",
|
||||
"name": "Continue Settings",
|
||||
"description": "Weiter in Einstellungen klicken",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "email_verification",
|
||||
"name": "Email Verification",
|
||||
"description": "E-Mail-Verifizierungscode eingeben",
|
||||
"required": True,
|
||||
"retry_count": 3
|
||||
},
|
||||
{
|
||||
"id": "set_password",
|
||||
"name": "Set Password",
|
||||
"description": "Passwort festlegen",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "skip_profile_picture",
|
||||
"name": "Skip Profile Picture",
|
||||
"description": "Profilbild überspringen",
|
||||
"required": False,
|
||||
"retry_count": 1
|
||||
},
|
||||
{
|
||||
"id": "skip_username",
|
||||
"name": "Skip Username",
|
||||
"description": "Benutzername überspringen",
|
||||
"required": False,
|
||||
"retry_count": 1
|
||||
},
|
||||
{
|
||||
"id": "skip_notifications",
|
||||
"name": "Skip Notifications",
|
||||
"description": "Benachrichtigungen überspringen",
|
||||
"required": False,
|
||||
"retry_count": 1
|
||||
},
|
||||
{
|
||||
"id": "verify_success",
|
||||
"name": "Verify Success",
|
||||
"description": "Erfolgreiche Registrierung überprüfen",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
}
|
||||
],
|
||||
"timeout": 600, # 10 Minuten Gesamttimeout
|
||||
"checkpoints": ["fill_initial_form", "email_verification", "verify_success"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_login_workflow() -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den Workflow für den Account-Login zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Workflow-Definition
|
||||
"""
|
||||
return {
|
||||
"name": "X Account Login",
|
||||
"steps": [
|
||||
{
|
||||
"id": "navigate",
|
||||
"name": "Navigate to X",
|
||||
"description": "Zur X-Startseite navigieren",
|
||||
"required": True,
|
||||
"retry_count": 3
|
||||
},
|
||||
{
|
||||
"id": "cookie_banner",
|
||||
"name": "Handle Cookie Banner",
|
||||
"description": "Cookie-Banner akzeptieren falls vorhanden",
|
||||
"required": False,
|
||||
"retry_count": 1
|
||||
},
|
||||
{
|
||||
"id": "click_login",
|
||||
"name": "Click Login",
|
||||
"description": "Anmelden Button klicken",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "enter_username",
|
||||
"name": "Enter Username/Email",
|
||||
"description": "Benutzername oder E-Mail eingeben",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "click_next",
|
||||
"name": "Click Next",
|
||||
"description": "Weiter klicken nach Benutzername",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "enter_password",
|
||||
"name": "Enter Password",
|
||||
"description": "Passwort eingeben",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "submit_login",
|
||||
"name": "Submit Login",
|
||||
"description": "Anmelden klicken",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "handle_challenges",
|
||||
"name": "Handle Challenges",
|
||||
"description": "Eventuelle Sicherheitsabfragen behandeln",
|
||||
"required": False,
|
||||
"retry_count": 3
|
||||
},
|
||||
{
|
||||
"id": "verify_success",
|
||||
"name": "Verify Success",
|
||||
"description": "Erfolgreichen Login überprüfen",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
}
|
||||
],
|
||||
"timeout": 300, # 5 Minuten Gesamttimeout
|
||||
"checkpoints": ["enter_username", "submit_login", "verify_success"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_verification_workflow() -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den Workflow für die Account-Verifizierung zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Workflow-Definition
|
||||
"""
|
||||
return {
|
||||
"name": "X Account Verification",
|
||||
"steps": [
|
||||
{
|
||||
"id": "check_verification_needed",
|
||||
"name": "Check Verification",
|
||||
"description": "Prüfen ob Verifizierung erforderlich",
|
||||
"required": True,
|
||||
"retry_count": 1
|
||||
},
|
||||
{
|
||||
"id": "select_method",
|
||||
"name": "Select Method",
|
||||
"description": "Verifizierungsmethode auswählen",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "request_code",
|
||||
"name": "Request Code",
|
||||
"description": "Verifizierungscode anfordern",
|
||||
"required": True,
|
||||
"retry_count": 3
|
||||
},
|
||||
{
|
||||
"id": "enter_code",
|
||||
"name": "Enter Code",
|
||||
"description": "Verifizierungscode eingeben",
|
||||
"required": True,
|
||||
"retry_count": 3
|
||||
},
|
||||
{
|
||||
"id": "submit_verification",
|
||||
"name": "Submit Verification",
|
||||
"description": "Verifizierung abschließen",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
},
|
||||
{
|
||||
"id": "verify_success",
|
||||
"name": "Verify Success",
|
||||
"description": "Erfolgreiche Verifizierung überprüfen",
|
||||
"required": True,
|
||||
"retry_count": 2
|
||||
}
|
||||
],
|
||||
"timeout": 300, # 5 Minuten Gesamttimeout
|
||||
"checkpoints": ["enter_code", "verify_success"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_error_recovery_strategies() -> Dict[str, List[str]]:
|
||||
"""
|
||||
Gibt Fehlerbehandlungsstrategien zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: Fehler und ihre Behandlungsstrategien
|
||||
"""
|
||||
return {
|
||||
"rate_limit": [
|
||||
"wait_exponential_backoff",
|
||||
"rotate_proxy",
|
||||
"change_user_agent",
|
||||
"abort_with_retry_later"
|
||||
],
|
||||
"captcha": [
|
||||
"solve_captcha_manual",
|
||||
"solve_captcha_service",
|
||||
"rotate_proxy_retry",
|
||||
"abort_with_manual_intervention"
|
||||
],
|
||||
"suspended_account": [
|
||||
"log_suspension",
|
||||
"mark_account_suspended",
|
||||
"abort_immediately"
|
||||
],
|
||||
"network_error": [
|
||||
"retry_with_backoff",
|
||||
"check_proxy_health",
|
||||
"switch_proxy",
|
||||
"retry_direct_connection"
|
||||
],
|
||||
"element_not_found": [
|
||||
"wait_longer",
|
||||
"refresh_page",
|
||||
"check_alternate_selectors",
|
||||
"take_screenshot_debug"
|
||||
]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_validation_rules() -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt Validierungsregeln für verschiedene Eingaben zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Validierungsregeln
|
||||
"""
|
||||
return {
|
||||
"username": {
|
||||
"min_length": 1,
|
||||
"max_length": 15,
|
||||
"allowed_chars": r"^[a-zA-Z0-9_]+$",
|
||||
"forbidden_patterns": ["twitter", "admin", "x.com"]
|
||||
},
|
||||
"password": {
|
||||
"min_length": 8,
|
||||
"max_length": 128,
|
||||
"require_uppercase": False,
|
||||
"require_lowercase": True,
|
||||
"require_number": True,
|
||||
"require_special": False
|
||||
},
|
||||
"email": {
|
||||
"pattern": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
"forbidden_domains": ["example.com", "test.com"]
|
||||
},
|
||||
"age": {
|
||||
"minimum": 13,
|
||||
"maximum": 120
|
||||
}
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren