Initial commit
Dieser Commit ist enthalten in:
0
social_networks/__init__.py
Normale Datei
0
social_networks/__init__.py
Normale Datei
899
social_networks/base_automation.py
Normale Datei
899
social_networks/base_automation.py
Normale Datei
@ -0,0 +1,899 @@
|
||||
"""
|
||||
Basis-Automatisierungsklasse für soziale Netzwerke
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from browser.playwright_manager import PlaywrightManager
|
||||
from utils.proxy_rotator import ProxyRotator
|
||||
from utils.email_handler import EmailHandler
|
||||
from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button
|
||||
from domain.value_objects.browser_protection_style import BrowserProtectionStyle, ProtectionLevel
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("base_automation")
|
||||
|
||||
class BaseAutomation(ABC):
|
||||
"""
|
||||
Abstrakte Basisklasse für die Automatisierung von sozialen Netzwerken.
|
||||
Definiert die gemeinsame Schnittstelle für alle Implementierungen.
|
||||
"""
|
||||
|
||||
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",
|
||||
session_manager=None,
|
||||
window_position: Optional[Tuple[int, int]] = None,
|
||||
auto_close_browser: bool = False):
|
||||
"""
|
||||
Initialisiert die Basis-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
|
||||
session_manager: Optional - Session Manager für Ein-Klick-Login
|
||||
window_position: Optional - Fensterposition als Tuple (x, y)
|
||||
auto_close_browser: Ob Browser automatisch geschlossen werden soll (Standard: False)
|
||||
"""
|
||||
self.headless = headless
|
||||
self.use_proxy = use_proxy
|
||||
self.proxy_type = proxy_type
|
||||
self.session_manager = session_manager
|
||||
self.save_screenshots = save_screenshots
|
||||
self.slowmo = slowmo
|
||||
self.debug = debug
|
||||
self.email_domain = email_domain
|
||||
self.auto_close_browser = auto_close_browser
|
||||
|
||||
# Verzeichnis für Screenshots
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
self.screenshots_dir = screenshots_dir or os.path.join(base_dir, "logs", "screenshots")
|
||||
os.makedirs(self.screenshots_dir, exist_ok=True)
|
||||
|
||||
# Initialisiere Hilfsklassen
|
||||
self.proxy_rotator = ProxyRotator()
|
||||
self.email_handler = EmailHandler()
|
||||
|
||||
# Initialisiere TextSimilarity für robustes UI-Element-Matching
|
||||
self.text_similarity = TextSimilarity(default_threshold=0.75)
|
||||
|
||||
# Playwright-Manager wird bei Bedarf initialisiert
|
||||
self.browser = None
|
||||
|
||||
# Session-bezogene Attribute
|
||||
self.current_session = None
|
||||
self.current_fingerprint = None
|
||||
self.session_restored = False
|
||||
|
||||
# Fensterposition
|
||||
self.window_position = window_position
|
||||
|
||||
# Status und Ergebnis der Automatisierung
|
||||
self.status = {
|
||||
"success": False,
|
||||
"stage": "initialized",
|
||||
"error": None,
|
||||
"account_data": {}
|
||||
}
|
||||
|
||||
# Customer log callback (wird vom Worker Thread gesetzt)
|
||||
self.customer_log_callback = None
|
||||
|
||||
# Status update callback für Login-Progress
|
||||
self.status_update_callback = None
|
||||
self.log_update_callback = None
|
||||
|
||||
# Debug-Logging
|
||||
if self.debug:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
logger.info(f"Basis-Automatisierung initialisiert (Proxy: {use_proxy}, Typ: {proxy_type})")
|
||||
|
||||
def set_customer_log_callback(self, callback):
|
||||
"""Setzt den Callback für kundenfreundliche Log-Nachrichten."""
|
||||
self.customer_log_callback = callback
|
||||
|
||||
def _emit_customer_log(self, message: str):
|
||||
"""Sendet eine kundenfreundliche Log-Nachricht."""
|
||||
if self.customer_log_callback:
|
||||
self.customer_log_callback(message)
|
||||
|
||||
def _initialize_browser(self) -> bool:
|
||||
"""
|
||||
Initialisiert den Browser mit den entsprechenden Einstellungen.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
self._emit_customer_log("🔄 Sichere Verbindung wird aufgebaut...")
|
||||
# Proxy-Konfiguration, falls aktiviert
|
||||
proxy_config = None
|
||||
if self.use_proxy:
|
||||
self._emit_customer_log("🌐 Optimale Verbindung wird ausgewählt...")
|
||||
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")
|
||||
|
||||
# Prüfe ob Session wiederhergestellt werden soll
|
||||
if self.current_session and self.current_fingerprint:
|
||||
# Verwende Session-Aware Playwright Manager
|
||||
from browser.session_aware_playwright_manager import SessionAwarePlaywrightManager
|
||||
|
||||
self.browser = SessionAwarePlaywrightManager(
|
||||
session=self.current_session,
|
||||
fingerprint=self.current_fingerprint,
|
||||
headless=self.headless,
|
||||
proxy=proxy_config,
|
||||
browser_type="chromium",
|
||||
screenshots_dir=self.screenshots_dir,
|
||||
slowmo=self.slowmo,
|
||||
window_position=self.window_position
|
||||
)
|
||||
|
||||
# Browser mit Session starten
|
||||
self.browser.start_with_session()
|
||||
self.session_restored = True
|
||||
logger.info("Browser mit wiederhergestellter Session initialisiert")
|
||||
else:
|
||||
# Normaler Browser ohne Session
|
||||
self.browser = PlaywrightManager(
|
||||
headless=self.headless,
|
||||
proxy=proxy_config,
|
||||
browser_type="chromium",
|
||||
screenshots_dir=self.screenshots_dir,
|
||||
slowmo=self.slowmo,
|
||||
window_position=self.window_position
|
||||
)
|
||||
|
||||
# Browser starten
|
||||
self.browser.start()
|
||||
logger.info("Browser erfolgreich initialisiert")
|
||||
|
||||
# Browser-Schutz anwenden wenn nicht headless
|
||||
if not self.headless:
|
||||
self._emit_customer_log("🛡️ Sicherheitseinstellungen werden konfiguriert...")
|
||||
# TEMPORÄR DEAKTIVIERT zum Testen
|
||||
# self._apply_browser_protection()
|
||||
logger.info("Browser-Schutz wurde temporär deaktiviert")
|
||||
|
||||
self._emit_customer_log("✅ Verbindung erfolgreich hergestellt")
|
||||
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 _close_browser(self) -> None:
|
||||
"""
|
||||
Schließt den Browser und gibt Ressourcen frei.
|
||||
Berücksichtigt die auto_close_browser Einstellung.
|
||||
"""
|
||||
if self.auto_close_browser and self.browser:
|
||||
self.browser.close()
|
||||
self.browser = None
|
||||
logger.info("Browser automatisch geschlossen")
|
||||
elif self.browser and not self.auto_close_browser:
|
||||
logger.info("Browser bleibt geöffnet (auto_close_browser=False)")
|
||||
|
||||
def close_browser(self) -> None:
|
||||
"""
|
||||
Explizite Methode zum manuellen Schließen des Browsers.
|
||||
Ignoriert die auto_close_browser Einstellung.
|
||||
"""
|
||||
if self.browser:
|
||||
self.browser.close()
|
||||
self.browser = None
|
||||
logger.info("Browser manuell geschlossen")
|
||||
|
||||
def is_browser_open(self) -> bool:
|
||||
"""
|
||||
Prüft, ob der Browser noch geöffnet ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Browser geöffnet ist, False sonst
|
||||
"""
|
||||
return self.browser is not None and hasattr(self.browser, 'page')
|
||||
|
||||
def get_browser(self) -> Optional[PlaywrightManager]:
|
||||
"""
|
||||
Gibt die Browser-Instanz zurück für weitere Operationen.
|
||||
|
||||
Returns:
|
||||
Optional[PlaywrightManager]: Browser-Instanz oder None
|
||||
"""
|
||||
return self.browser
|
||||
|
||||
def _take_screenshot(self, name: str) -> Optional[str]:
|
||||
"""
|
||||
Erstellt einen Screenshot der aktuellen Seite.
|
||||
|
||||
Args:
|
||||
name: Name für den Screenshot (ohne Dateierweiterung)
|
||||
|
||||
Returns:
|
||||
Optional[str]: Pfad zum erstellten Screenshot oder None bei Fehler
|
||||
"""
|
||||
if not self.save_screenshots:
|
||||
return None
|
||||
|
||||
try:
|
||||
if self.browser and hasattr(self.browser, 'take_screenshot'):
|
||||
return self.browser.take_screenshot(name)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Erstellen eines Screenshots: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _send_status_update(self, status: str) -> None:
|
||||
"""
|
||||
Sendet ein Status-Update über den Callback.
|
||||
|
||||
Args:
|
||||
status: Status-Nachricht
|
||||
"""
|
||||
if self.status_update_callback:
|
||||
try:
|
||||
self.status_update_callback(status)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Senden des Status-Updates: {e}")
|
||||
|
||||
def _send_log_update(self, message: str) -> None:
|
||||
"""
|
||||
Sendet ein Log-Update über den Callback.
|
||||
|
||||
Args:
|
||||
message: Log-Nachricht
|
||||
"""
|
||||
if self.log_update_callback:
|
||||
try:
|
||||
self.log_update_callback(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Senden des Log-Updates: {e}")
|
||||
|
||||
# Auch über customer_log_callback senden für Kompatibilität
|
||||
if self.customer_log_callback:
|
||||
try:
|
||||
self.customer_log_callback(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Senden des Customer-Log-Updates: {e}")
|
||||
|
||||
def _random_delay(self, min_seconds: float = 1.0, max_seconds: float = 3.0) -> None:
|
||||
"""
|
||||
Führt eine zufällige Wartezeit aus, um menschliches Verhalten zu simulieren.
|
||||
|
||||
Args:
|
||||
min_seconds: Minimale Wartezeit in Sekunden
|
||||
max_seconds: Maximale Wartezeit in Sekunden
|
||||
"""
|
||||
delay = random.uniform(min_seconds, max_seconds)
|
||||
logger.debug(f"Zufällige Wartezeit: {delay:.2f} Sekunden")
|
||||
time.sleep(delay)
|
||||
|
||||
def _fill_field_fuzzy(self, field_labels: List[str], value: str, fallback_selector: str = None) -> bool:
|
||||
"""
|
||||
Füllt ein Formularfeld mit Fuzzy-Text-Matching aus.
|
||||
|
||||
Args:
|
||||
field_labels: Liste mit möglichen Bezeichnungen des Feldes
|
||||
value: Einzugebender Wert
|
||||
fallback_selector: CSS-Selektor für Fallback
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
# Versuche, das Feld mit Fuzzy-Matching zu finden
|
||||
field = fuzzy_find_element(self.browser.page, field_labels, selector_type="input", threshold=0.6, wait_time=3000)
|
||||
|
||||
if field:
|
||||
try:
|
||||
field.fill(value)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Ausfüllen des Feldes mit Fuzzy-Match: {e}")
|
||||
# Fallback auf normales Ausfüllen
|
||||
|
||||
# Fallback: Versuche mit dem angegebenen Selektor
|
||||
if fallback_selector:
|
||||
field_success = self.browser.fill_form_field(fallback_selector, value)
|
||||
if field_success:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _click_button_fuzzy(self, button_texts: List[str], fallback_selector: str = None) -> bool:
|
||||
"""
|
||||
Klickt einen Button mit Fuzzy-Text-Matching.
|
||||
|
||||
Args:
|
||||
button_texts: Liste mit möglichen Button-Texten
|
||||
fallback_selector: CSS-Selektor für Fallback
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
# Versuche, den Button mit Fuzzy-Matching zu finden
|
||||
success = click_fuzzy_button(self.browser.page, button_texts, threshold=0.6, timeout=3000)
|
||||
|
||||
if success:
|
||||
return True
|
||||
|
||||
# Fallback: Versuche mit dem angegebenen Selektor
|
||||
if fallback_selector:
|
||||
return self.browser.click_element(fallback_selector)
|
||||
|
||||
return False
|
||||
|
||||
def _find_element_by_text(self, texts: List[str], selector_type: str = "any", threshold: float = 0.7) -> Optional[Any]:
|
||||
"""
|
||||
Findet ein Element basierend auf Textähnlichkeit.
|
||||
|
||||
Args:
|
||||
texts: Liste mit möglichen Texten
|
||||
selector_type: Art des Elements ("button", "link", "input", "any")
|
||||
threshold: Ähnlichkeitsschwellenwert
|
||||
|
||||
Returns:
|
||||
Das gefundene Element oder None
|
||||
"""
|
||||
return fuzzy_find_element(self.browser.page, texts, selector_type, threshold, wait_time=3000)
|
||||
|
||||
def _check_for_text_on_page(self, texts: List[str], threshold: float = 0.7) -> bool:
|
||||
"""
|
||||
Prüft, ob ein Text auf der Seite vorhanden ist.
|
||||
|
||||
Args:
|
||||
texts: Liste mit zu suchenden Texten
|
||||
threshold: Ähnlichkeitsschwellenwert
|
||||
|
||||
Returns:
|
||||
True wenn einer der Texte gefunden wurde, False sonst
|
||||
"""
|
||||
# Hole den gesamten Seiteninhalt
|
||||
try:
|
||||
page_content = self.browser.page.content()
|
||||
if not page_content:
|
||||
return False
|
||||
|
||||
# Versuche, Text im HTML zu finden (einfache Suche)
|
||||
for text in texts:
|
||||
if text.lower() in page_content.lower():
|
||||
return True
|
||||
|
||||
# Wenn nicht gefunden, versuche über alle sichtbaren Textelemente
|
||||
elements = self.browser.page.query_selector_all("p, h1, h2, h3, h4, h5, h6, span, div, button, a, label")
|
||||
|
||||
for element in elements:
|
||||
element_text = element.inner_text()
|
||||
if not element_text:
|
||||
continue
|
||||
|
||||
element_text = element_text.strip()
|
||||
|
||||
# Prüfe die Textähnlichkeit mit jedem der gesuchten Texte
|
||||
for text in texts:
|
||||
if self.text_similarity.is_similar(text, element_text, threshold=threshold):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Text auf der Seite: {e}")
|
||||
return False
|
||||
|
||||
def _check_for_error(self, error_selectors: List[str], error_texts: List[str]) -> Optional[str]:
|
||||
"""
|
||||
Prüft, ob Fehlermeldungen angezeigt werden.
|
||||
|
||||
Args:
|
||||
error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen
|
||||
error_texts: Liste mit typischen Fehlertexten
|
||||
|
||||
Returns:
|
||||
Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden
|
||||
"""
|
||||
try:
|
||||
# Prüfe selektoren
|
||||
for selector in error_selectors:
|
||||
element = self.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
error_text = element.text_content()
|
||||
if error_text:
|
||||
return error_text.strip()
|
||||
|
||||
# Fuzzy-Suche nach Fehlermeldungen
|
||||
elements = self.browser.page.query_selector_all("p, div[role='alert'], span.error, .error-message")
|
||||
|
||||
for element in elements:
|
||||
element_text = element.inner_text()
|
||||
if not element_text:
|
||||
continue
|
||||
|
||||
element_text = element_text.strip()
|
||||
|
||||
# Prüfe, ob der Text einem Fehlermuster ähnelt
|
||||
for error_text in error_texts:
|
||||
if self.text_similarity.is_similar(error_text, element_text, threshold=0.6):
|
||||
return element_text
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}")
|
||||
return None
|
||||
|
||||
def _attempt_ocr_fallback(self, action_name: str, target_text: str = None, value: str = None) -> bool:
|
||||
"""
|
||||
Versucht, eine Aktion mit OCR-Fallback durchzuführen, wenn Playwright fehlschlägt.
|
||||
|
||||
Args:
|
||||
action_name: Name der Aktion ("click", "type", "select")
|
||||
target_text: Text, nach dem gesucht werden soll
|
||||
value: Wert, der eingegeben werden soll (bei "type" oder "select")
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
# Diese Methode wird in abgeleiteten Klassen implementiert
|
||||
logger.warning(f"OCR-Fallback für '{action_name}' wurde aufgerufen, aber nicht implementiert")
|
||||
return False
|
||||
|
||||
def _rotate_proxy(self) -> bool:
|
||||
"""
|
||||
Rotiert den Proxy und aktualisiert die Browser-Sitzung.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self.use_proxy:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Browser schließen
|
||||
self._close_browser()
|
||||
|
||||
# Proxy rotieren
|
||||
new_proxy = self.proxy_rotator.rotate_proxy(self.proxy_type)
|
||||
if not new_proxy:
|
||||
logger.warning("Konnte Proxy nicht rotieren")
|
||||
return False
|
||||
|
||||
# Browser neu initialisieren
|
||||
success = self._initialize_browser()
|
||||
|
||||
logger.info(f"Proxy rotiert zu: {new_proxy['server']}")
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Proxy-Rotation: {e}")
|
||||
return False
|
||||
|
||||
def _generate_random_email(self, length: int = 10) -> str:
|
||||
"""
|
||||
Generiert eine zufällige E-Mail-Adresse.
|
||||
|
||||
Args:
|
||||
length: Länge des lokalen Teils der E-Mail
|
||||
|
||||
Returns:
|
||||
str: Die generierte E-Mail-Adresse
|
||||
"""
|
||||
import string
|
||||
local_chars = string.ascii_lowercase + string.digits
|
||||
local_part = ''.join(random.choice(local_chars) for _ in range(length))
|
||||
return f"{local_part}@{self.email_domain}"
|
||||
|
||||
def _get_confirmation_code(self, email_address: str, search_criteria: str,
|
||||
max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]:
|
||||
"""
|
||||
Ruft einen Bestätigungscode aus einer E-Mail ab.
|
||||
|
||||
Args:
|
||||
email_address: E-Mail-Adresse, an die der Code gesendet wurde
|
||||
search_criteria: Suchkriterium für die E-Mail
|
||||
max_attempts: Maximale Anzahl an Versuchen
|
||||
delay_seconds: Verzögerung zwischen Versuchen in Sekunden
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden
|
||||
"""
|
||||
logger.info(f"Suche nach Bestätigungscode für {email_address}")
|
||||
|
||||
code = self.email_handler.get_confirmation_code(
|
||||
expected_email=email_address,
|
||||
search_criteria=search_criteria,
|
||||
max_attempts=max_attempts,
|
||||
delay_seconds=delay_seconds
|
||||
)
|
||||
|
||||
if code:
|
||||
logger.info(f"Bestätigungscode gefunden: {code}")
|
||||
else:
|
||||
logger.warning(f"Kein Bestätigungscode für {email_address} gefunden")
|
||||
|
||||
return code
|
||||
|
||||
def _apply_browser_protection(self):
|
||||
"""Wendet Browser-Schutz an, um versehentliche Interaktionen zu verhindern."""
|
||||
try:
|
||||
# Lade Schutz-Einstellungen aus stealth_config.json
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
protection_config = None
|
||||
try:
|
||||
config_file = Path(__file__).parent.parent / "config" / "stealth_config.json"
|
||||
if config_file.exists():
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
stealth_config = json.load(f)
|
||||
protection_config = stealth_config.get("browser_protection", {})
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Browser-Schutz-Konfiguration nicht laden: {e}")
|
||||
|
||||
# Nutze Konfiguration oder Standardwerte
|
||||
if protection_config and protection_config.get("enabled", True):
|
||||
level_mapping = {
|
||||
"none": ProtectionLevel.NONE,
|
||||
"light": ProtectionLevel.LIGHT,
|
||||
"medium": ProtectionLevel.MEDIUM,
|
||||
"strong": ProtectionLevel.STRONG
|
||||
}
|
||||
|
||||
protection_style = BrowserProtectionStyle(
|
||||
level=level_mapping.get(protection_config.get("level", "medium"), ProtectionLevel.MEDIUM),
|
||||
show_border=protection_config.get("show_border", True),
|
||||
show_badge=protection_config.get("show_badge", True),
|
||||
blur_effect=protection_config.get("blur_effect", False),
|
||||
opacity=protection_config.get("opacity", 0.1),
|
||||
badge_text=protection_config.get("badge_text", "🔒 Account wird erstellt - Bitte nicht eingreifen"),
|
||||
badge_position=protection_config.get("badge_position", "top-right"),
|
||||
border_color=protection_config.get("border_color", "rgba(255, 0, 0, 0.5)")
|
||||
)
|
||||
|
||||
# Wende Schutz an
|
||||
if hasattr(self.browser, 'apply_protection'):
|
||||
self.browser.apply_protection(protection_style)
|
||||
logger.info("Browser-Schutz aktiviert")
|
||||
|
||||
except Exception as e:
|
||||
# Browser-Schutz ist optional, Fehler nicht kritisch
|
||||
logger.warning(f"Browser-Schutz konnte nicht aktiviert werden: {str(e)}")
|
||||
|
||||
def _is_text_similar(self, text1: str, text2: str, threshold: float = None) -> bool:
|
||||
"""
|
||||
Prüft, ob zwei Texte ähnlich sind.
|
||||
|
||||
Args:
|
||||
text1: Erster Text
|
||||
text2: Zweiter Text
|
||||
threshold: Ähnlichkeitsschwellenwert (None für Standardwert)
|
||||
|
||||
Returns:
|
||||
True wenn die Texte ähnlich sind, False sonst
|
||||
"""
|
||||
return self.text_similarity.is_similar(text1, text2, threshold)
|
||||
|
||||
@abstractmethod
|
||||
def register_account(self, full_name: str, age: int, registration_method: str = "email",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Registriert einen neuen Account im sozialen Netzwerk.
|
||||
|
||||
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
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Meldet sich bei einem bestehenden 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
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Verifiziert einen Account mit einem Bestätigungscode.
|
||||
|
||||
Args:
|
||||
verification_code: Der Bestätigungscode
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung mit Status
|
||||
"""
|
||||
pass
|
||||
|
||||
def login_with_session(self, session_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Template Method für Ein-Klick-Login mit gespeicherter Session.
|
||||
|
||||
Args:
|
||||
session_data: Session-Daten vom OneClickLoginUseCase
|
||||
|
||||
Returns:
|
||||
Dict mit Login-Ergebnis
|
||||
"""
|
||||
try:
|
||||
# Session und Fingerprint setzen
|
||||
self.current_session = session_data.get('session')
|
||||
self.current_fingerprint = session_data.get('fingerprint')
|
||||
|
||||
# Browser mit Session initialisieren
|
||||
if not self._initialize_browser():
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Browser-Initialisierung fehlgeschlagen'
|
||||
}
|
||||
|
||||
# Plattform-spezifische Login-Seite aufrufen
|
||||
login_url = self._get_login_url()
|
||||
self.browser.page.goto(login_url)
|
||||
|
||||
# Warte kurz für Session-Wiederherstellung
|
||||
time.sleep(2)
|
||||
|
||||
# Prüfe ob Login erfolgreich
|
||||
if self._check_logged_in_state():
|
||||
logger.info("Ein-Klick-Login erfolgreich")
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Erfolgreich mit Session eingeloggt',
|
||||
'session_restored': True
|
||||
}
|
||||
else:
|
||||
logger.warning("Session abgelaufen oder ungültig")
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Session abgelaufen',
|
||||
'requires_manual_login': True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Session-Login: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
finally:
|
||||
self._close_browser()
|
||||
|
||||
def create_with_session_persistence(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Template Method für Account-Erstellung mit Session-Speicherung.
|
||||
|
||||
Args:
|
||||
**kwargs: Plattformspezifische Parameter
|
||||
|
||||
Returns:
|
||||
Dict mit Ergebnis und Session-Daten
|
||||
"""
|
||||
# Normale Account-Erstellung
|
||||
result = self.register_account(**kwargs)
|
||||
|
||||
if result.get('success') and self.browser:
|
||||
try:
|
||||
# Session-Daten extrahieren
|
||||
from browser.session_aware_playwright_manager import SessionAwarePlaywrightManager
|
||||
|
||||
if isinstance(self.browser, SessionAwarePlaywrightManager):
|
||||
session = self.browser.save_current_session()
|
||||
platform_data = self.browser.extract_platform_session_data(
|
||||
self._get_platform_name()
|
||||
)
|
||||
|
||||
result['session_data'] = {
|
||||
'session': session,
|
||||
'platform_data': platform_data,
|
||||
'fingerprint': self.current_fingerprint
|
||||
}
|
||||
|
||||
logger.info("Session-Daten für späteren Login gespeichert")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Speichern der Session: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def _get_login_url(self) -> str:
|
||||
"""
|
||||
Gibt die Login-URL der Plattform zurück.
|
||||
Kann von Unterklassen überschrieben werden.
|
||||
"""
|
||||
# Standard-URLs für bekannte Plattformen
|
||||
urls = {
|
||||
'instagram': 'https://www.instagram.com/accounts/login/',
|
||||
'facebook': 'https://www.facebook.com/',
|
||||
'twitter': 'https://twitter.com/login',
|
||||
'tiktok': 'https://www.tiktok.com/login'
|
||||
}
|
||||
platform = self._get_platform_name().lower()
|
||||
return urls.get(platform, '')
|
||||
|
||||
def _check_logged_in_state(self) -> bool:
|
||||
"""
|
||||
Prüft ob der Benutzer eingeloggt ist.
|
||||
Kann von Unterklassen überschrieben werden.
|
||||
"""
|
||||
# Basis-Implementation: Prüfe auf typische Login-Indikatoren
|
||||
logged_in_indicators = [
|
||||
'logout', 'log out', 'sign out', 'abmelden',
|
||||
'profile', 'profil', 'dashboard', 'home'
|
||||
]
|
||||
|
||||
# Prüfe URL
|
||||
current_url = self.browser.page.url.lower()
|
||||
if any(indicator in current_url for indicator in ['home', 'feed', 'dashboard']):
|
||||
return True
|
||||
|
||||
# Prüfe Seiteninhalte
|
||||
return self._check_for_text_on_page(logged_in_indicators, threshold=0.7)
|
||||
|
||||
def _get_platform_name(self) -> str:
|
||||
"""
|
||||
Gibt den Namen der Plattform zurück.
|
||||
Sollte von Unterklassen überschrieben werden.
|
||||
"""
|
||||
# Versuche aus Klassennamen zu extrahieren
|
||||
class_name = self.__class__.__name__.lower()
|
||||
if 'instagram' in class_name:
|
||||
return 'instagram'
|
||||
elif 'facebook' in class_name:
|
||||
return 'facebook'
|
||||
elif 'twitter' in class_name:
|
||||
return 'twitter'
|
||||
elif 'tiktok' in class_name:
|
||||
return 'tiktok'
|
||||
return 'unknown'
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den aktuellen Status der Automatisierung zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Aktueller Status
|
||||
"""
|
||||
return self.status
|
||||
|
||||
def get_browser_context_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Extrahiert Browser-Context-Daten für Session-Speicherung.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Browser-Context-Daten
|
||||
"""
|
||||
try:
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
return {}
|
||||
|
||||
# Cookies extrahieren
|
||||
cookies = self.browser.page.context.cookies()
|
||||
|
||||
# User Agent und Viewport
|
||||
user_agent = self.browser.page.evaluate("() => navigator.userAgent")
|
||||
viewport = self.browser.page.viewport_size
|
||||
|
||||
# URL
|
||||
current_url = self.browser.page.url
|
||||
|
||||
return {
|
||||
'cookies': cookies,
|
||||
'user_agent': user_agent,
|
||||
'viewport_size': viewport,
|
||||
'current_url': current_url,
|
||||
'context_id': getattr(self.browser.page.context, '_guid', None)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren der Browser-Context-Daten: {e}")
|
||||
return {}
|
||||
|
||||
def extract_platform_session_data(self, platform: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Extrahiert plattform-spezifische Session-Daten.
|
||||
|
||||
Args:
|
||||
platform: Zielplattform
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Plattform-Session-Daten
|
||||
"""
|
||||
try:
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
return {}
|
||||
|
||||
# Local Storage extrahieren
|
||||
local_storage = {}
|
||||
try:
|
||||
local_storage_script = """
|
||||
() => {
|
||||
const storage = {};
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
storage[key] = localStorage.getItem(key);
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
"""
|
||||
local_storage = self.browser.page.evaluate(local_storage_script)
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Local Storage nicht extrahieren: {e}")
|
||||
|
||||
# Session Storage extrahieren
|
||||
session_storage = {}
|
||||
try:
|
||||
session_storage_script = """
|
||||
() => {
|
||||
const storage = {};
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
storage[key] = sessionStorage.getItem(key);
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
"""
|
||||
session_storage = self.browser.page.evaluate(session_storage_script)
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Session Storage nicht extrahieren: {e}")
|
||||
|
||||
return {
|
||||
'platform': platform,
|
||||
'local_storage': local_storage,
|
||||
'session_storage': session_storage,
|
||||
'url': self.browser.page.url,
|
||||
'created_at': time.time()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren der Plattform-Session-Daten: {e}")
|
||||
return {}
|
||||
|
||||
def __enter__(self):
|
||||
"""Kontext-Manager-Eintritt."""
|
||||
self._initialize_browser()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Kontext-Manager-Austritt."""
|
||||
self._close_browser()
|
||||
|
||||
|
||||
# Wenn direkt ausgeführt, zeige Informationen zur Klasse
|
||||
if __name__ == "__main__":
|
||||
print("Dies ist eine abstrakte Basisklasse und kann nicht direkt instanziiert werden.")
|
||||
print("Bitte verwende eine konkrete Implementierung wie InstagramAutomation.")
|
||||
0
social_networks/facebook/__init__.py
Normale Datei
0
social_networks/facebook/__init__.py
Normale Datei
0
social_networks/facebook/facebook_automation.py
Normale Datei
0
social_networks/facebook/facebook_automation.py
Normale Datei
0
social_networks/facebook/facebook_login.py
Normale Datei
0
social_networks/facebook/facebook_login.py
Normale Datei
0
social_networks/facebook/facebook_registration.py
Normale Datei
0
social_networks/facebook/facebook_registration.py
Normale Datei
0
social_networks/facebook/facebook_selectors.py
Normale Datei
0
social_networks/facebook/facebook_selectors.py
Normale Datei
0
social_networks/facebook/facebook_ui_helper.py
Normale Datei
0
social_networks/facebook/facebook_ui_helper.py
Normale Datei
0
social_networks/facebook/facebook_utils.py
Normale Datei
0
social_networks/facebook/facebook_utils.py
Normale Datei
0
social_networks/facebook/facebook_verification.py
Normale Datei
0
social_networks/facebook/facebook_verification.py
Normale Datei
0
social_networks/facebook/facebook_workflow.py
Normale Datei
0
social_networks/facebook/facebook_workflow.py
Normale Datei
0
social_networks/gmail/__init__.py
Normale Datei
0
social_networks/gmail/__init__.py
Normale Datei
336
social_networks/gmail/gmail_automation.py
Normale Datei
336
social_networks/gmail/gmail_automation.py
Normale Datei
@ -0,0 +1,336 @@
|
||||
"""
|
||||
Gmail Automatisierung - Hauptklasse
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, Optional, Tuple, Any
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from social_networks.base_automation import BaseAutomation
|
||||
from social_networks.gmail import gmail_selectors as selectors
|
||||
from social_networks.gmail.gmail_ui_helper import GmailUIHelper
|
||||
from social_networks.gmail.gmail_registration import GmailRegistration
|
||||
from social_networks.gmail.gmail_login import GmailLogin
|
||||
from social_networks.gmail.gmail_verification import GmailVerification
|
||||
from social_networks.gmail.gmail_utils import GmailUtils
|
||||
|
||||
logger = logging.getLogger("gmail_automation")
|
||||
|
||||
class GmailAutomation(BaseAutomation):
|
||||
"""
|
||||
Gmail/Google Account-spezifische Automatisierung
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Initialisiert die Gmail-Automatisierung
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.platform_name = "gmail"
|
||||
self.ui_helper = None
|
||||
self.registration = None
|
||||
self.login_helper = None
|
||||
self.verification = None
|
||||
self.utils = None
|
||||
|
||||
def _initialize_helpers(self, page: Page):
|
||||
"""
|
||||
Initialisiert die Hilfsklassen
|
||||
"""
|
||||
self.ui_helper = GmailUIHelper(page, self.screenshots_dir, self.save_screenshots)
|
||||
self.registration = GmailRegistration(page, self.ui_helper, self.screenshots_dir, self.save_screenshots)
|
||||
self.login_helper = GmailLogin(page, self.ui_helper, self.screenshots_dir, self.save_screenshots)
|
||||
self.verification = GmailVerification(page, self.ui_helper, self.email_handler, self.screenshots_dir, self.save_screenshots)
|
||||
self.utils = GmailUtils()
|
||||
|
||||
def register_account(self, full_name: str, age: int, registration_method: str = "email",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, any]:
|
||||
"""
|
||||
Erstellt einen neuen Gmail/Google Account
|
||||
|
||||
Args:
|
||||
full_name: Vollständiger Name für den Account
|
||||
age: Alter des Benutzers
|
||||
registration_method: Registrierungsmethode (nur "email" für Gmail)
|
||||
phone_number: Telefonnummer (optional, aber oft erforderlich)
|
||||
**kwargs: Weitere optionale Parameter
|
||||
"""
|
||||
try:
|
||||
logger.info(f"[GMAIL AUTOMATION] register_account aufgerufen")
|
||||
logger.info(f"[GMAIL AUTOMATION] full_name: {full_name}")
|
||||
logger.info(f"[GMAIL AUTOMATION] age: {age}")
|
||||
logger.info(f"[GMAIL AUTOMATION] phone_number: {phone_number}")
|
||||
logger.info(f"[GMAIL AUTOMATION] kwargs: {kwargs}")
|
||||
|
||||
# Erstelle account_data aus den Parametern
|
||||
account_data = {
|
||||
"full_name": full_name,
|
||||
"first_name": kwargs.get("first_name", full_name.split()[0] if full_name else ""),
|
||||
"last_name": kwargs.get("last_name", full_name.split()[-1] if full_name and len(full_name.split()) > 1 else ""),
|
||||
"age": age,
|
||||
"birthday": kwargs.get("birthday", self._generate_birthday(age)),
|
||||
"gender": kwargs.get("gender", random.choice(["male", "female"])),
|
||||
"username": kwargs.get("username", ""),
|
||||
"password": kwargs.get("password", ""),
|
||||
"phone": phone_number,
|
||||
"recovery_email": kwargs.get("recovery_email", "")
|
||||
}
|
||||
|
||||
# Initialisiere Browser, falls noch nicht geschehen
|
||||
logger.info(f"[GMAIL AUTOMATION] Prüfe Browser-Status...")
|
||||
logger.info(f"[GMAIL AUTOMATION] self.browser: {self.browser}")
|
||||
if self.browser:
|
||||
logger.info(f"[GMAIL AUTOMATION] hasattr(self.browser, 'page'): {hasattr(self.browser, 'page')}")
|
||||
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
logger.info(f"[GMAIL AUTOMATION] Browser muss initialisiert werden")
|
||||
if not self._initialize_browser():
|
||||
logger.error(f"[GMAIL AUTOMATION] Browser-Initialisierung fehlgeschlagen!")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Browser konnte nicht initialisiert werden",
|
||||
"message": "Browser-Initialisierung fehlgeschlagen"
|
||||
}
|
||||
logger.info(f"[GMAIL AUTOMATION] Browser erfolgreich initialisiert")
|
||||
|
||||
# Page-Objekt holen
|
||||
page = self.browser.page
|
||||
self._initialize_helpers(page)
|
||||
|
||||
# Direkt zur Registrierungs-URL navigieren
|
||||
logger.info("Navigiere zur Gmail Registrierungsseite")
|
||||
page.goto(selectors.REGISTRATION_URL, wait_until="networkidle")
|
||||
|
||||
# Warte auf vollständiges Laden der Seite
|
||||
logger.info("Warte auf vollständiges Laden der Seite...")
|
||||
time.sleep(random.uniform(5, 7))
|
||||
|
||||
# Prüfe ob wir auf der richtigen Seite sind
|
||||
current_url = page.url
|
||||
logger.info(f"Aktuelle URL nach Navigation: {current_url}")
|
||||
|
||||
# Screenshot der Startseite
|
||||
self.ui_helper.take_screenshot("gmail_start_page")
|
||||
|
||||
# Finde und klicke auf "Konto erstellen" Button (Dropdown)
|
||||
try:
|
||||
# Warte bis die Seite interaktiv ist
|
||||
logger.info("Warte auf vollständiges Laden der Gmail Workspace Seite...")
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(2)
|
||||
|
||||
# Debug: Alle sichtbaren Links/Buttons mit "Konto" ausgeben
|
||||
try:
|
||||
konto_elements = page.locator("*:has-text('Konto')").all()
|
||||
logger.info(f"Gefundene Elemente mit 'Konto': {len(konto_elements)}")
|
||||
for i, elem in enumerate(konto_elements[:5]): # Erste 5 Elemente
|
||||
try:
|
||||
tag = elem.evaluate("el => el.tagName")
|
||||
text = elem.inner_text()
|
||||
logger.info(f"Element {i}: <{tag}> - {text}")
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"Debug-Ausgabe fehlgeschlagen: {e}")
|
||||
|
||||
# Schritt 1: Klicke auf "Konto erstellen" Dropdown
|
||||
create_account_selectors = [
|
||||
"[aria-label='Konto erstellen']",
|
||||
"div[aria-label='Konto erstellen']",
|
||||
"[data-g-action='create an account']",
|
||||
"button:has-text('Konto erstellen')",
|
||||
"a:has-text('Konto erstellen')",
|
||||
"*:has-text('Konto erstellen')", # Beliebiges Element mit dem Text
|
||||
"[slot='label']:has-text('Konto erstellen')" # Spezifisch für Web Components
|
||||
]
|
||||
|
||||
clicked_dropdown = False
|
||||
for selector in create_account_selectors:
|
||||
try:
|
||||
elements = page.locator(selector).all()
|
||||
logger.info(f"Selector {selector}: {len(elements)} Elemente gefunden")
|
||||
|
||||
if page.locator(selector).is_visible(timeout=3000):
|
||||
# Versuche normale Klick-Methode
|
||||
try:
|
||||
page.locator(selector).first.click()
|
||||
logger.info(f"Dropdown 'Konto erstellen' geklickt mit: {selector}")
|
||||
clicked_dropdown = True
|
||||
break
|
||||
except:
|
||||
# Versuche JavaScript-Klick als Fallback
|
||||
page.locator(selector).first.evaluate("el => el.click()")
|
||||
logger.info(f"Dropdown 'Konto erstellen' via JS geklickt mit: {selector}")
|
||||
clicked_dropdown = True
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Fehler mit Selector {selector}: {e}")
|
||||
continue
|
||||
|
||||
if not clicked_dropdown:
|
||||
logger.error("Konnte 'Konto erstellen' Dropdown nicht finden")
|
||||
self.ui_helper.take_screenshot("konto_erstellen_not_found")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konto erstellen Dropdown nicht gefunden",
|
||||
"message": "Navigation fehlgeschlagen"
|
||||
}
|
||||
|
||||
# Kurz warten bis Dropdown geöffnet ist
|
||||
time.sleep(1)
|
||||
|
||||
# Schritt 2: Klicke auf "Für die private Nutzung"
|
||||
private_use_selectors = [
|
||||
"a[aria-label='Gmail - Für die private Nutzung']",
|
||||
"a:has-text('Für die private Nutzung')",
|
||||
"[data-g-action='für die private nutzung']",
|
||||
"span:has-text('Für die private Nutzung')"
|
||||
]
|
||||
|
||||
clicked_private = False
|
||||
for selector in private_use_selectors:
|
||||
try:
|
||||
if page.locator(selector).is_visible(timeout=2000):
|
||||
page.locator(selector).click()
|
||||
logger.info(f"'Für die private Nutzung' geklickt mit: {selector}")
|
||||
clicked_private = True
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not clicked_private:
|
||||
logger.error("Konnte 'Für die private Nutzung' nicht finden")
|
||||
self.ui_helper.take_screenshot("private_nutzung_not_found")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Für die private Nutzung Option nicht gefunden",
|
||||
"message": "Navigation fehlgeschlagen"
|
||||
}
|
||||
|
||||
# Warte auf die Registrierungsseite
|
||||
time.sleep(random.uniform(3, 5))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Navigieren zur Registrierung: {e}")
|
||||
|
||||
# Screenshot der Registrierungsseite
|
||||
self.ui_helper.take_screenshot("gmail_registration_page")
|
||||
|
||||
# Registrierungsprozess starten
|
||||
registration_result = self.registration.start_registration_flow(account_data)
|
||||
if not registration_result["success"]:
|
||||
return registration_result
|
||||
|
||||
# Nach erfolgreicher Registrierung
|
||||
logger.info("Gmail Account-Registrierung erfolgreich abgeschlossen")
|
||||
return {
|
||||
"success": True,
|
||||
"username": registration_result.get("username"),
|
||||
"password": account_data.get("password"),
|
||||
"email": registration_result.get("email"),
|
||||
"phone": account_data.get("phone"),
|
||||
"recovery_email": account_data.get("recovery_email"),
|
||||
"message": "Account erfolgreich erstellt"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Gmail-Registrierung: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Registrierung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
finally:
|
||||
self._close_browser()
|
||||
|
||||
def login(self, username: str, password: str) -> Dict[str, any]:
|
||||
"""
|
||||
Meldet sich bei einem bestehenden Gmail/Google Account an
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starte Gmail Login für {username}")
|
||||
|
||||
# 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",
|
||||
"message": "Browser-Initialisierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
# Page-Objekt holen
|
||||
page = self.browser.page
|
||||
self._initialize_helpers(page)
|
||||
|
||||
# Login durchführen
|
||||
return self.login_helper.login(username, password)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Gmail Login: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Login fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
finally:
|
||||
self._close_browser()
|
||||
|
||||
def get_account_info(self) -> Dict[str, any]:
|
||||
"""
|
||||
Ruft Informationen über den aktuellen Account ab
|
||||
"""
|
||||
# TODO: Implementierung
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Noch nicht implementiert"
|
||||
}
|
||||
|
||||
def logout(self) -> bool:
|
||||
"""
|
||||
Meldet sich vom aktuellen Account ab
|
||||
"""
|
||||
# TODO: Implementierung
|
||||
return False
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Meldet sich bei einem bestehenden Gmail Account an.
|
||||
Implementiert die abstrakte Methode aus BaseAutomation.
|
||||
"""
|
||||
return self.login(username_or_email, password)
|
||||
|
||||
def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Verifiziert einen Gmail Account mit einem Bestätigungscode.
|
||||
Implementiert die abstrakte Methode aus BaseAutomation.
|
||||
"""
|
||||
try:
|
||||
if self.verification:
|
||||
return self.verification.verify_with_code(verification_code)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Verification helper nicht initialisiert",
|
||||
"message": "Verifizierung kann nicht durchgeführt werden"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Verifizierung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _generate_birthday(self, age: int) -> str:
|
||||
"""
|
||||
Generiert ein Geburtsdatum basierend auf dem Alter
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
today = datetime.now()
|
||||
birth_year = today.year - age
|
||||
# Zufälliger Tag im Jahr
|
||||
random_days = random.randint(0, 364)
|
||||
birthday = datetime(birth_year, 1, 1) + timedelta(days=random_days)
|
||||
return birthday.strftime("%Y-%m-%d")
|
||||
157
social_networks/gmail/gmail_login.py
Normale Datei
157
social_networks/gmail/gmail_login.py
Normale Datei
@ -0,0 +1,157 @@
|
||||
"""
|
||||
Gmail Login - Handhabt den Login-Prozess
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from social_networks.gmail import gmail_selectors as selectors
|
||||
from social_networks.gmail.gmail_ui_helper import GmailUIHelper
|
||||
|
||||
logger = logging.getLogger("gmail_login")
|
||||
|
||||
class GmailLogin:
|
||||
"""
|
||||
Handhabt den Gmail/Google Account Login-Prozess
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, ui_helper: GmailUIHelper, screenshots_dir: str = None, save_screenshots: bool = True):
|
||||
"""
|
||||
Initialisiert den Login Handler
|
||||
"""
|
||||
self.page = page
|
||||
self.ui_helper = ui_helper
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.save_screenshots = save_screenshots
|
||||
|
||||
def login(self, username: str, password: str) -> Dict[str, any]:
|
||||
"""
|
||||
Führt den Login durch
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starte Gmail Login für {username}")
|
||||
|
||||
# Navigiere zur Login-Seite
|
||||
self.page.goto(selectors.LOGIN_URL, wait_until="domcontentloaded")
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
self.ui_helper.take_screenshot("login_page")
|
||||
|
||||
# Email eingeben
|
||||
if self.ui_helper.wait_for_element(selectors.LOGIN_EMAIL_INPUT, timeout=10000):
|
||||
logger.info("Gebe Email-Adresse ein")
|
||||
# Füge @gmail.com hinzu falls nicht vorhanden
|
||||
email = username if "@" in username else f"{username}@gmail.com"
|
||||
self.ui_helper.type_with_delay(selectors.LOGIN_EMAIL_INPUT, email)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Screenshot vor dem Weiter-Klick
|
||||
self.ui_helper.take_screenshot("email_entered")
|
||||
|
||||
# Weiter klicken
|
||||
logger.info("Klicke auf Weiter")
|
||||
self.ui_helper.click_with_retry(selectors.LOGIN_NEXT_BUTTON)
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
# Passwort eingeben
|
||||
if self.ui_helper.wait_for_element(selectors.LOGIN_PASSWORD_INPUT, timeout=10000):
|
||||
logger.info("Gebe Passwort ein")
|
||||
self.ui_helper.type_with_delay(selectors.LOGIN_PASSWORD_INPUT, password)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Screenshot vor dem Login
|
||||
self.ui_helper.take_screenshot("password_entered")
|
||||
|
||||
# Login Button klicken
|
||||
logger.info("Klicke auf Weiter")
|
||||
self.ui_helper.click_with_retry(selectors.LOGIN_NEXT_BUTTON)
|
||||
|
||||
# Warte auf Navigation
|
||||
self.ui_helper.wait_for_navigation()
|
||||
time.sleep(random.uniform(3, 5))
|
||||
|
||||
# Prüfe ob Login erfolgreich war
|
||||
if self._check_login_success():
|
||||
logger.info("Login erfolgreich")
|
||||
self.ui_helper.take_screenshot("login_success")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Login erfolgreich"
|
||||
}
|
||||
else:
|
||||
error_msg = self._get_error_message()
|
||||
logger.error(f"Login fehlgeschlagen: {error_msg}")
|
||||
self.ui_helper.take_screenshot("login_failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"message": f"Login fehlgeschlagen: {error_msg}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Login: {e}")
|
||||
self.ui_helper.take_screenshot("login_error")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Login fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _check_login_success(self) -> bool:
|
||||
"""
|
||||
Prüft ob der Login erfolgreich war
|
||||
"""
|
||||
try:
|
||||
# Prüfe ob wir auf einer Google-Seite sind
|
||||
current_url = self.page.url
|
||||
success_indicators = [
|
||||
"myaccount.google.com",
|
||||
"mail.google.com",
|
||||
"youtube.com",
|
||||
"google.com/webhp",
|
||||
"accounts.google.com/b/"
|
||||
]
|
||||
|
||||
for indicator in success_indicators:
|
||||
if indicator in current_url:
|
||||
return True
|
||||
|
||||
# Prüfe ob Login-Formular noch sichtbar ist
|
||||
if self.ui_helper.is_element_visible(selectors.LOGIN_EMAIL_INPUT):
|
||||
return False
|
||||
|
||||
# Prüfe auf Fehlermeldung
|
||||
if self.ui_helper.is_element_visible(selectors.ERROR_MESSAGE):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler bei der Login-Prüfung: {e}")
|
||||
return False
|
||||
|
||||
def _get_error_message(self) -> str:
|
||||
"""
|
||||
Holt die Fehlermeldung falls vorhanden
|
||||
"""
|
||||
try:
|
||||
# Prüfe verschiedene Fehlermeldungs-Selektoren
|
||||
error_selectors = [
|
||||
selectors.ERROR_MESSAGE,
|
||||
selectors.ERROR_MESSAGE_ALT,
|
||||
selectors.FORM_ERROR
|
||||
]
|
||||
|
||||
for selector in error_selectors:
|
||||
if self.ui_helper.is_element_visible(selector):
|
||||
error_text = self.ui_helper.get_element_text(selector)
|
||||
if error_text:
|
||||
return error_text
|
||||
|
||||
return "Login fehlgeschlagen"
|
||||
except:
|
||||
return "Unbekannter Fehler"
|
||||
548
social_networks/gmail/gmail_registration.py
Normale Datei
548
social_networks/gmail/gmail_registration.py
Normale Datei
@ -0,0 +1,548 @@
|
||||
"""
|
||||
Gmail Registrierung - Handhabt den Registrierungsprozess
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from social_networks.gmail import gmail_selectors as selectors
|
||||
from social_networks.gmail.gmail_ui_helper import GmailUIHelper
|
||||
|
||||
logger = logging.getLogger("gmail_registration")
|
||||
|
||||
class GmailRegistration:
|
||||
"""
|
||||
Handhabt den Gmail/Google Account Registrierungsprozess
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, ui_helper: GmailUIHelper, screenshots_dir: str = None, save_screenshots: bool = True):
|
||||
"""
|
||||
Initialisiert die Registrierung
|
||||
"""
|
||||
self.page = page
|
||||
self.ui_helper = ui_helper
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.save_screenshots = save_screenshots
|
||||
|
||||
def _click_next_button(self) -> bool:
|
||||
"""
|
||||
Versucht den Weiter-Button mit verschiedenen Selektoren zu klicken
|
||||
"""
|
||||
logger.info("Versuche Weiter-Button zu klicken")
|
||||
|
||||
# Liste von Selektoren zum Ausprobieren
|
||||
selectors_to_try = [
|
||||
("span[jsname='V67aGc']:has-text('Weiter')", "parent_click"), # Der exakte Span mit jsname
|
||||
("button:has(span.VfPpkd-vQzf8d:has-text('Weiter'))", "click"), # Button der den Span enthält
|
||||
("button:has(div.VfPpkd-RLmnJb)", "click"), # Button mit dem Material Ripple div
|
||||
(selectors.NEXT_BUTTON, "click"),
|
||||
(selectors.NEXT_BUTTON_MATERIAL, "parent_click"),
|
||||
(selectors.NEXT_BUTTON_SPAN, "click"),
|
||||
("button:has-text('Weiter')", "click"),
|
||||
("button:has-text('Next')", "click")
|
||||
]
|
||||
|
||||
for selector, method in selectors_to_try:
|
||||
try:
|
||||
if method == "click":
|
||||
if self.ui_helper.wait_for_element(selector, timeout=2000):
|
||||
self.ui_helper.click_with_retry(selector)
|
||||
logger.info(f"Erfolgreich geklickt mit Selektor: {selector}")
|
||||
return True
|
||||
elif method == "parent_click":
|
||||
if self.ui_helper.wait_for_element(selector, timeout=2000):
|
||||
# Versuche verschiedene Parent-Ebenen
|
||||
for parent_level in ['..', '../..', '../../..']:
|
||||
try:
|
||||
self.page.locator(selector).locator(parent_level).click()
|
||||
logger.info(f"Erfolgreich Parent geklickt mit Selektor: {selector} und Level: {parent_level}")
|
||||
return True
|
||||
except:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"Konnte nicht mit Selektor {selector} klicken: {e}")
|
||||
continue
|
||||
|
||||
# Letzter Versuch mit Playwright's get_by_role
|
||||
try:
|
||||
self.page.get_by_role("button", name="Weiter").click()
|
||||
logger.info("Erfolgreich mit get_by_role geklickt")
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.error("Konnte Weiter-Button nicht finden/klicken")
|
||||
return False
|
||||
|
||||
def start_registration_flow(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Startet den Registrierungsflow
|
||||
"""
|
||||
try:
|
||||
logger.info("Starte Gmail Registrierungsflow")
|
||||
|
||||
# Schritt 1: Name eingeben
|
||||
name_result = self._fill_name_form(account_data)
|
||||
if not name_result["success"]:
|
||||
return name_result
|
||||
|
||||
# Schritt 2: Geburtsdatum und Geschlecht
|
||||
birthday_result = self._fill_birthday_gender(account_data)
|
||||
if not birthday_result["success"]:
|
||||
return birthday_result
|
||||
|
||||
# Schritt 3: Gmail-Adresse wählen/erstellen
|
||||
gmail_result = self._create_gmail_address(account_data)
|
||||
if not gmail_result["success"]:
|
||||
return gmail_result
|
||||
|
||||
# Schritt 4: Passwort festlegen
|
||||
password_result = self._set_password(account_data)
|
||||
if not password_result["success"]:
|
||||
return password_result
|
||||
|
||||
# Schritt 5: Telefonnummer (optional/erforderlich)
|
||||
phone_result = self._handle_phone_verification(account_data)
|
||||
if not phone_result["success"]:
|
||||
return phone_result
|
||||
|
||||
# Schritt 6: Recovery Email (optional)
|
||||
recovery_result = self._handle_recovery_email(account_data)
|
||||
if not recovery_result["success"]:
|
||||
return recovery_result
|
||||
|
||||
# Schritt 7: Nutzungsbedingungen akzeptieren
|
||||
terms_result = self._accept_terms()
|
||||
if not terms_result["success"]:
|
||||
return terms_result
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"username": gmail_result.get("username"),
|
||||
"email": gmail_result.get("email"),
|
||||
"message": "Registrierung erfolgreich abgeschlossen"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im Registrierungsflow: {e}")
|
||||
self.ui_helper.take_screenshot("registration_error")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Registrierung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _fill_name_form(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Füllt das Namensformular aus
|
||||
"""
|
||||
try:
|
||||
logger.info("Fülle Namensformular aus")
|
||||
|
||||
# Screenshot der aktuellen Seite
|
||||
self.ui_helper.take_screenshot("before_name_form_search")
|
||||
|
||||
# Warte kurz, damit die Seite vollständig lädt
|
||||
time.sleep(2)
|
||||
|
||||
# Debug: Aktuelle URL ausgeben
|
||||
current_url = self.page.url
|
||||
logger.info(f"Aktuelle URL: {current_url}")
|
||||
|
||||
# Versuche Cookie-Banner zu schließen, falls vorhanden
|
||||
try:
|
||||
# Suche nach typischen Cookie-Akzeptieren-Buttons
|
||||
cookie_selectors = [
|
||||
"button:has-text('Alle akzeptieren')",
|
||||
"button:has-text('Accept all')",
|
||||
"button:has-text('Akzeptieren')",
|
||||
"button:has-text('Accept')",
|
||||
"[aria-label='Alle akzeptieren']"
|
||||
]
|
||||
|
||||
for selector in cookie_selectors:
|
||||
try:
|
||||
if self.page.locator(selector).is_visible(timeout=1000):
|
||||
self.page.locator(selector).click()
|
||||
logger.info(f"Cookie-Banner geschlossen mit: {selector}")
|
||||
time.sleep(1)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"Kein Cookie-Banner gefunden oder Fehler: {e}")
|
||||
|
||||
# Versuche verschiedene Selektoren für das Vorname-Feld
|
||||
first_name_selectors = [
|
||||
selectors.FIRST_NAME_INPUT,
|
||||
"input[aria-label='Vorname']",
|
||||
"input[aria-label='First name']",
|
||||
"#firstName",
|
||||
"input[type='text'][autocomplete='given-name']"
|
||||
]
|
||||
|
||||
first_name_found = False
|
||||
for selector in first_name_selectors:
|
||||
if self.ui_helper.wait_for_element(selector, timeout=3000):
|
||||
first_name_found = True
|
||||
selectors.FIRST_NAME_INPUT = selector # Update für diesen Durchlauf
|
||||
logger.info(f"Vorname-Feld gefunden mit Selektor: {selector}")
|
||||
break
|
||||
|
||||
if not first_name_found:
|
||||
# Screenshot bei Fehler
|
||||
self.ui_helper.take_screenshot("name_form_not_found_error")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Namensformular nicht gefunden",
|
||||
"message": "Registrierungsseite konnte nicht geladen werden"
|
||||
}
|
||||
|
||||
# Vorname eingeben
|
||||
first_name = account_data.get("first_name", "")
|
||||
logger.info(f"Gebe Vorname ein: {first_name}")
|
||||
self.ui_helper.type_with_delay(selectors.FIRST_NAME_INPUT, first_name)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Nachname eingeben - versuche verschiedene Selektoren
|
||||
last_name_selectors = [
|
||||
selectors.LAST_NAME_INPUT,
|
||||
"input[aria-label='Nachname']",
|
||||
"input[aria-label='Last name']",
|
||||
"#lastName",
|
||||
"input[type='text'][autocomplete='family-name']"
|
||||
]
|
||||
|
||||
last_name = account_data.get("last_name", "")
|
||||
logger.info(f"Gebe Nachname ein: {last_name}")
|
||||
|
||||
for selector in last_name_selectors:
|
||||
try:
|
||||
if self.ui_helper.wait_for_element(selector, timeout=2000):
|
||||
self.ui_helper.type_with_delay(selector, last_name)
|
||||
logger.info(f"Nachname eingegeben mit Selektor: {selector}")
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Screenshot vor dem Weiter-Klick
|
||||
self.ui_helper.take_screenshot("name_form_filled")
|
||||
|
||||
# Weiter Button klicken
|
||||
if not self._click_next_button():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte Weiter-Button nicht klicken",
|
||||
"message": "Navigation fehlgeschlagen"
|
||||
}
|
||||
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Namensformular ausgefüllt"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Namensformulars: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _fill_birthday_gender(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Füllt Geburtsdatum und Geschlecht aus
|
||||
"""
|
||||
try:
|
||||
logger.info("Fülle Geburtsdatum und Geschlecht aus")
|
||||
|
||||
# Warte auf Formular
|
||||
if not self.ui_helper.wait_for_element(selectors.BIRTHDAY_DAY, timeout=10000):
|
||||
logger.warning("Geburtsdatum-Formular nicht gefunden, überspringe...")
|
||||
return {"success": True}
|
||||
|
||||
# Geburtsdatum ausfüllen
|
||||
birthday = account_data.get("birthday", "1990-01-15")
|
||||
year, month, day = birthday.split("-")
|
||||
|
||||
# Tag eingeben
|
||||
logger.info(f"Gebe Geburtstag ein: {day}")
|
||||
self.ui_helper.type_with_delay(selectors.BIRTHDAY_DAY, day.lstrip("0"))
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
|
||||
# Monat auswählen
|
||||
logger.info(f"Wähle Geburtsmonat: {month}")
|
||||
month_value = str(int(month)) # Entferne führende Null
|
||||
self.ui_helper.select_dropdown_option(selectors.BIRTHDAY_MONTH, month_value)
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
|
||||
# Jahr eingeben
|
||||
logger.info(f"Gebe Geburtsjahr ein: {year}")
|
||||
self.ui_helper.type_with_delay(selectors.BIRTHDAY_YEAR, year)
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
|
||||
# Geschlecht auswählen
|
||||
gender = account_data.get("gender", "male").lower()
|
||||
gender_value = "1" if gender == "male" else "2" # 1=männlich, 2=weiblich
|
||||
logger.info(f"Wähle Geschlecht: {gender}")
|
||||
self.ui_helper.select_dropdown_option(selectors.GENDER_SELECT, gender_value)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Screenshot vor dem Weiter-Klick
|
||||
self.ui_helper.take_screenshot("birthday_gender_filled")
|
||||
|
||||
# Weiter klicken
|
||||
logger.info("Klicke auf Weiter")
|
||||
self._click_next_button()
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Geburtsdatum und Geschlecht ausgefüllt"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen von Geburtsdatum/Geschlecht: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _create_gmail_address(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Erstellt die Gmail-Adresse
|
||||
"""
|
||||
try:
|
||||
logger.info("Erstelle Gmail-Adresse")
|
||||
|
||||
# Warte auf Gmail-Erstellungsseite
|
||||
time.sleep(random.uniform(2, 3))
|
||||
self.ui_helper.take_screenshot("gmail_creation_page")
|
||||
|
||||
# Prüfe ob wir einen Benutzernamen eingeben können
|
||||
if self.ui_helper.wait_for_element(selectors.GMAIL_USERNAME_INPUT, timeout=10000):
|
||||
username = account_data.get("username", "")
|
||||
if not username:
|
||||
# Generiere einen Benutzernamen
|
||||
from social_networks.gmail.gmail_utils import GmailUtils
|
||||
utils = GmailUtils()
|
||||
username = utils.generate_gmail_username(
|
||||
account_data.get("first_name", ""),
|
||||
account_data.get("last_name", "")
|
||||
)
|
||||
|
||||
logger.info(f"Gebe Gmail-Benutzernamen ein: {username}")
|
||||
self.ui_helper.type_with_delay(selectors.GMAIL_USERNAME_INPUT, username)
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
# Weiter klicken
|
||||
self.ui_helper.click_with_retry(selectors.NEXT_BUTTON)
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
# Prüfe auf Fehler (Benutzername bereits vergeben)
|
||||
if self.ui_helper.is_element_visible(selectors.ERROR_MESSAGE):
|
||||
error_text = self.ui_helper.get_element_text(selectors.ERROR_MESSAGE)
|
||||
logger.warning(f"Benutzername-Fehler: {error_text}")
|
||||
# TODO: Implementiere alternative Benutzernamen-Vorschläge
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"username": username,
|
||||
"email": f"{username}@gmail.com"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Gmail-Adresse erstellt"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen der Gmail-Adresse: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _set_password(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Setzt das Passwort
|
||||
"""
|
||||
try:
|
||||
logger.info("Setze Passwort")
|
||||
|
||||
# Warte auf Passwort-Formular
|
||||
if not self.ui_helper.wait_for_element(selectors.PASSWORD_INPUT, timeout=10000):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Passwort-Formular nicht gefunden"
|
||||
}
|
||||
|
||||
password = account_data.get("password", "")
|
||||
if not password:
|
||||
# Generiere ein sicheres Passwort
|
||||
from social_networks.gmail.gmail_utils import GmailUtils
|
||||
utils = GmailUtils()
|
||||
password = utils.generate_secure_password()
|
||||
|
||||
# Passwort eingeben
|
||||
logger.info("Gebe Passwort ein")
|
||||
self.ui_helper.type_with_delay(selectors.PASSWORD_INPUT, password)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Passwort bestätigen
|
||||
if self.ui_helper.wait_for_element(selectors.PASSWORD_CONFIRM_INPUT, timeout=5000):
|
||||
logger.info("Bestätige Passwort")
|
||||
self.ui_helper.type_with_delay(selectors.PASSWORD_CONFIRM_INPUT, password)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Screenshot vor dem Weiter-Klick
|
||||
self.ui_helper.take_screenshot("password_set")
|
||||
|
||||
# Weiter klicken
|
||||
self.ui_helper.click_with_retry(selectors.NEXT_BUTTON)
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Passwort gesetzt"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Setzen des Passworts: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _handle_phone_verification(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Handhabt die Telefonnummer-Verifizierung (falls erforderlich)
|
||||
"""
|
||||
try:
|
||||
logger.info("Prüfe auf Telefonnummer-Verifizierung")
|
||||
|
||||
# Prüfe ob Telefonnummer erforderlich ist
|
||||
if not self.ui_helper.wait_for_element(selectors.PHONE_INPUT, timeout=5000):
|
||||
logger.info("Telefonnummer nicht erforderlich")
|
||||
return {"success": True}
|
||||
|
||||
# Wenn Telefonnummer optional ist, überspringe
|
||||
if self.ui_helper.is_element_visible(selectors.SKIP_BUTTON):
|
||||
logger.info("Überspringe Telefonnummer")
|
||||
self.ui_helper.click_with_retry(selectors.SKIP_BUTTON)
|
||||
time.sleep(random.uniform(2, 3))
|
||||
return {"success": True}
|
||||
|
||||
# Telefonnummer eingeben falls vorhanden
|
||||
phone = account_data.get("phone", "")
|
||||
if phone:
|
||||
logger.info(f"Gebe Telefonnummer ein: {phone}")
|
||||
self.ui_helper.type_with_delay(selectors.PHONE_INPUT, phone)
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
# Weiter klicken
|
||||
self.ui_helper.click_with_retry(selectors.NEXT_BUTTON)
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
|
||||
# TODO: SMS-Verifizierung implementieren
|
||||
logger.warning("SMS-Verifizierung noch nicht implementiert")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Telefonnummer-Schritt abgeschlossen"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Telefonnummer-Verifizierung: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _handle_recovery_email(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Handhabt die Recovery-Email (optional)
|
||||
"""
|
||||
try:
|
||||
logger.info("Prüfe auf Recovery-Email")
|
||||
|
||||
# Prüfe ob Recovery-Email Feld vorhanden ist
|
||||
if not self.ui_helper.wait_for_element(selectors.RECOVERY_EMAIL_INPUT, timeout=5000):
|
||||
logger.info("Recovery-Email nicht vorhanden")
|
||||
return {"success": True}
|
||||
|
||||
# Überspringe wenn möglich
|
||||
if self.ui_helper.is_element_visible(selectors.SKIP_BUTTON):
|
||||
logger.info("Überspringe Recovery-Email")
|
||||
self.ui_helper.click_with_retry(selectors.SKIP_BUTTON)
|
||||
time.sleep(random.uniform(2, 3))
|
||||
else:
|
||||
# Recovery-Email eingeben falls vorhanden
|
||||
recovery_email = account_data.get("recovery_email", "")
|
||||
if recovery_email:
|
||||
logger.info(f"Gebe Recovery-Email ein: {recovery_email}")
|
||||
self.ui_helper.type_with_delay(selectors.RECOVERY_EMAIL_INPUT, recovery_email)
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
# Weiter klicken
|
||||
self.ui_helper.click_with_retry(selectors.NEXT_BUTTON)
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Recovery-Email Schritt abgeschlossen"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Recovery-Email: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _accept_terms(self) -> Dict[str, any]:
|
||||
"""
|
||||
Akzeptiert die Nutzungsbedingungen
|
||||
"""
|
||||
try:
|
||||
logger.info("Akzeptiere Nutzungsbedingungen")
|
||||
|
||||
# Warte auf Nutzungsbedingungen
|
||||
time.sleep(random.uniform(2, 3))
|
||||
self.ui_helper.take_screenshot("terms_page")
|
||||
|
||||
# Scrolle nach unten (simuliere Lesen)
|
||||
self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
time.sleep(random.uniform(2, 4))
|
||||
|
||||
# Akzeptiere Button suchen und klicken
|
||||
if self.ui_helper.wait_for_element(selectors.AGREE_BUTTON, timeout=10000):
|
||||
logger.info("Klicke auf 'Ich stimme zu'")
|
||||
self.ui_helper.click_with_retry(selectors.AGREE_BUTTON)
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
time.sleep(random.uniform(3, 5))
|
||||
|
||||
# Screenshot nach Registrierung
|
||||
self.ui_helper.take_screenshot("registration_complete")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Nutzungsbedingungen akzeptiert"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Akzeptieren der Nutzungsbedingungen: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
59
social_networks/gmail/gmail_selectors.py
Normale Datei
59
social_networks/gmail/gmail_selectors.py
Normale Datei
@ -0,0 +1,59 @@
|
||||
"""
|
||||
Gmail/Google Account UI Selektoren und URLs
|
||||
"""
|
||||
|
||||
# URLs
|
||||
BASE_URL = "https://accounts.google.com/"
|
||||
REGISTRATION_URL = "https://workspace.google.com/intl/de/gmail/"
|
||||
LOGIN_URL = "https://accounts.google.com/ServiceLogin"
|
||||
|
||||
# Name Eingabe (Erster Schritt)
|
||||
FIRST_NAME_INPUT = "input[name='firstName']"
|
||||
LAST_NAME_INPUT = "input[name='lastName']"
|
||||
NEXT_BUTTON = "button[jsname='LgbsSe']"
|
||||
NEXT_BUTTON_SPAN = "span:has-text('Weiter')"
|
||||
NEXT_BUTTON_MATERIAL = "div.VfPpkd-RLmnJb" # Material Design Weiter-Button
|
||||
|
||||
# Geburtsdatum und Geschlecht
|
||||
BIRTHDAY_DAY = "input[name='day']"
|
||||
BIRTHDAY_MONTH = "select[name='month']"
|
||||
BIRTHDAY_YEAR = "input[name='year']"
|
||||
GENDER_SELECT = "select[name='gender']"
|
||||
|
||||
# Gmail-Adresse erstellen
|
||||
CREATE_GMAIL_RADIO = "div[data-value='createAccount']"
|
||||
GMAIL_USERNAME_INPUT = "input[name='Username']"
|
||||
|
||||
# Passwort
|
||||
PASSWORD_INPUT = "input[name='Passwd']"
|
||||
PASSWORD_CONFIRM_INPUT = "input[name='PasswdAgain']"
|
||||
|
||||
# Telefonnummer Verifizierung
|
||||
PHONE_INPUT = "input[id='phoneNumberId']"
|
||||
PHONE_COUNTRY_SELECT = "select[data-id='countryList']"
|
||||
|
||||
# SMS Verifizierung
|
||||
SMS_CODE_INPUT = "input[name='code']"
|
||||
VERIFY_BUTTON = "button:has-text('Bestätigen')"
|
||||
|
||||
# Recovery Email (Optional)
|
||||
RECOVERY_EMAIL_INPUT = "input[name='recoveryEmail']"
|
||||
SKIP_BUTTON = "button:has-text('Überspringen')"
|
||||
|
||||
# Nutzungsbedingungen
|
||||
AGREE_BUTTON = "button:has-text('Ich stimme zu')"
|
||||
TERMS_CHECKBOX = "input[type='checkbox']"
|
||||
|
||||
# Fehler- und Erfolgsmeldungen
|
||||
ERROR_MESSAGE = "div[jsname='B34EJ'] span"
|
||||
ERROR_MESSAGE_ALT = "div.LXRPh"
|
||||
CAPTCHA_CONTAINER = "div.aCsJod"
|
||||
|
||||
# Login Seite
|
||||
LOGIN_EMAIL_INPUT = "input[type='email']"
|
||||
LOGIN_PASSWORD_INPUT = "input[type='password'][name='password']"
|
||||
LOGIN_NEXT_BUTTON = "button:has-text('Weiter')"
|
||||
|
||||
# Allgemeine Elemente
|
||||
LOADING_SPINNER = "div.ANuIbb"
|
||||
FORM_ERROR = "div[jsname='B34EJ']"
|
||||
151
social_networks/gmail/gmail_ui_helper.py
Normale Datei
151
social_networks/gmail/gmail_ui_helper.py
Normale Datei
@ -0,0 +1,151 @@
|
||||
"""
|
||||
Gmail UI Helper - Hilfsfunktionen für UI-Interaktionen
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
import os
|
||||
from typing import Optional
|
||||
from playwright.sync_api import Page, ElementHandle
|
||||
|
||||
logger = logging.getLogger("gmail_ui_helper")
|
||||
|
||||
class GmailUIHelper:
|
||||
"""
|
||||
Hilfsklasse für Gmail UI-Interaktionen
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, screenshots_dir: str = None, save_screenshots: bool = True):
|
||||
"""
|
||||
Initialisiert den UI Helper
|
||||
"""
|
||||
self.page = page
|
||||
self.screenshots_dir = screenshots_dir or "logs/screenshots"
|
||||
self.save_screenshots = save_screenshots
|
||||
|
||||
# Screenshot-Verzeichnis erstellen falls nötig
|
||||
if self.save_screenshots and not os.path.exists(self.screenshots_dir):
|
||||
os.makedirs(self.screenshots_dir)
|
||||
|
||||
def take_screenshot(self, name: str) -> Optional[str]:
|
||||
"""
|
||||
Erstellt einen Screenshot
|
||||
"""
|
||||
if not self.save_screenshots:
|
||||
return None
|
||||
|
||||
try:
|
||||
timestamp = int(time.time())
|
||||
filename = f"{name}_{timestamp}.png"
|
||||
filepath = os.path.join(self.screenshots_dir, filename)
|
||||
self.page.screenshot(path=filepath)
|
||||
logger.debug(f"Screenshot gespeichert: {filepath}")
|
||||
return filepath
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Erstellen des Screenshots: {e}")
|
||||
return None
|
||||
|
||||
def wait_for_element(self, selector: str, timeout: int = 30000) -> bool:
|
||||
"""
|
||||
Wartet auf ein Element
|
||||
"""
|
||||
try:
|
||||
self.page.wait_for_selector(selector, timeout=timeout)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Element {selector} nicht gefunden nach {timeout}ms")
|
||||
return False
|
||||
|
||||
def type_with_delay(self, selector: str, text: str, delay_min: float = 0.05, delay_max: float = 0.15):
|
||||
"""
|
||||
Tippt Text mit menschenähnlicher Verzögerung
|
||||
"""
|
||||
try:
|
||||
element = self.page.locator(selector)
|
||||
element.click()
|
||||
|
||||
for char in text:
|
||||
element.type(char)
|
||||
time.sleep(random.uniform(delay_min, delay_max))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Tippen in {selector}: {e}")
|
||||
raise
|
||||
|
||||
def click_with_retry(self, selector: str, max_attempts: int = 3) -> bool:
|
||||
"""
|
||||
Klickt auf ein Element mit Wiederholungsversuchen
|
||||
"""
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
self.page.click(selector)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Klick-Versuch {attempt + 1} fehlgeschlagen: {e}")
|
||||
if attempt < max_attempts - 1:
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
return False
|
||||
|
||||
def scroll_to_element(self, selector: str):
|
||||
"""
|
||||
Scrollt zu einem Element
|
||||
"""
|
||||
try:
|
||||
self.page.locator(selector).scroll_into_view_if_needed()
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Scrollen zu {selector}: {e}")
|
||||
|
||||
def is_element_visible(self, selector: str) -> bool:
|
||||
"""
|
||||
Prüft ob ein Element sichtbar ist
|
||||
"""
|
||||
try:
|
||||
return self.page.locator(selector).is_visible()
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_element_text(self, selector: str) -> Optional[str]:
|
||||
"""
|
||||
Holt den Text eines Elements
|
||||
"""
|
||||
try:
|
||||
return self.page.locator(selector).text_content()
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Lesen des Texts von {selector}: {e}")
|
||||
return None
|
||||
|
||||
def select_dropdown_option(self, selector: str, value: str):
|
||||
"""
|
||||
Wählt eine Option aus einem Dropdown
|
||||
"""
|
||||
try:
|
||||
self.page.select_option(selector, value)
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Auswählen von {value} in {selector}: {e}")
|
||||
raise
|
||||
|
||||
def wait_for_navigation(self, timeout: int = 30000):
|
||||
"""
|
||||
Wartet auf Navigation
|
||||
"""
|
||||
try:
|
||||
self.page.wait_for_load_state("networkidle", timeout=timeout)
|
||||
except Exception as e:
|
||||
logger.warning(f"Navigation-Timeout nach {timeout}ms: {e}")
|
||||
|
||||
def wait_for_loading_to_finish(self):
|
||||
"""
|
||||
Wartet bis Ladeanimation verschwunden ist
|
||||
"""
|
||||
try:
|
||||
# Warte bis der Loading Spinner nicht mehr sichtbar ist
|
||||
from social_networks.gmail import gmail_selectors as selectors
|
||||
if self.is_element_visible(selectors.LOADING_SPINNER):
|
||||
self.page.wait_for_selector(selectors.LOADING_SPINNER, state="hidden", timeout=10000)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
except:
|
||||
pass
|
||||
122
social_networks/gmail/gmail_utils.py
Normale Datei
122
social_networks/gmail/gmail_utils.py
Normale Datei
@ -0,0 +1,122 @@
|
||||
"""
|
||||
Gmail Utils - Utility-Funktionen für Gmail
|
||||
"""
|
||||
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger("gmail_utils")
|
||||
|
||||
class GmailUtils:
|
||||
"""
|
||||
Utility-Funktionen für Gmail/Google Accounts
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def generate_gmail_username(first_name: str, last_name: str) -> str:
|
||||
"""
|
||||
Generiert einen Gmail-kompatiblen Benutzernamen
|
||||
"""
|
||||
# Basis aus Vor- und Nachname
|
||||
first_clean = ''.join(c.lower() for c in first_name if c.isalnum())
|
||||
last_clean = ''.join(c.lower() for c in last_name if c.isalnum())
|
||||
|
||||
# Verschiedene Varianten
|
||||
variants = [
|
||||
f"{first_clean}{last_clean}",
|
||||
f"{first_clean}.{last_clean}",
|
||||
f"{last_clean}{first_clean}",
|
||||
f"{first_clean[0]}{last_clean}",
|
||||
f"{first_clean}{last_clean[0]}"
|
||||
]
|
||||
|
||||
# Wähle eine zufällige Variante
|
||||
base = random.choice(variants)
|
||||
|
||||
# Füge zufällige Zahlen hinzu
|
||||
random_suffix = ''.join(random.choices(string.digits, k=random.randint(2, 4)))
|
||||
|
||||
return f"{base}{random_suffix}"
|
||||
|
||||
@staticmethod
|
||||
def generate_secure_password(length: int = 16) -> str:
|
||||
"""
|
||||
Generiert ein sicheres Passwort für Google-Anforderungen
|
||||
- Mindestens 8 Zeichen
|
||||
- Mischung aus Buchstaben, Zahlen und Symbolen
|
||||
"""
|
||||
# Stelle sicher dass alle Zeichentypen enthalten sind
|
||||
password_chars = []
|
||||
|
||||
# Mindestens 2 Kleinbuchstaben
|
||||
password_chars.extend(random.choices(string.ascii_lowercase, k=2))
|
||||
|
||||
# Mindestens 2 Großbuchstaben
|
||||
password_chars.extend(random.choices(string.ascii_uppercase, k=2))
|
||||
|
||||
# Mindestens 2 Zahlen
|
||||
password_chars.extend(random.choices(string.digits, k=2))
|
||||
|
||||
# Mindestens 2 Sonderzeichen
|
||||
special_chars = "!@#$%^&*"
|
||||
password_chars.extend(random.choices(special_chars, k=2))
|
||||
|
||||
# Fülle mit zufälligen Zeichen auf
|
||||
remaining_length = length - len(password_chars)
|
||||
all_chars = string.ascii_letters + string.digits + special_chars
|
||||
password_chars.extend(random.choices(all_chars, k=remaining_length))
|
||||
|
||||
# Mische die Zeichen
|
||||
random.shuffle(password_chars)
|
||||
|
||||
return ''.join(password_chars)
|
||||
|
||||
@staticmethod
|
||||
def is_valid_gmail_address(email: str) -> bool:
|
||||
"""
|
||||
Prüft ob eine Gmail-Adresse gültig ist
|
||||
"""
|
||||
if not email.endswith("@gmail.com"):
|
||||
return False
|
||||
|
||||
username = email.split("@")[0]
|
||||
|
||||
# Gmail-Regeln:
|
||||
# - 6-30 Zeichen
|
||||
# - Buchstaben, Zahlen und Punkte
|
||||
# - Muss mit Buchstabe oder Zahl beginnen
|
||||
# - Kein Punkt am Anfang oder Ende
|
||||
# - Keine aufeinanderfolgenden Punkte
|
||||
|
||||
if len(username) < 6 or len(username) > 30:
|
||||
return False
|
||||
|
||||
if not username[0].isalnum() or not username[-1].isalnum():
|
||||
return False
|
||||
|
||||
if ".." in username:
|
||||
return False
|
||||
|
||||
# Prüfe erlaubte Zeichen
|
||||
for char in username:
|
||||
if not (char.isalnum() or char == "."):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def format_phone_for_google(phone: str, country_code: str = "+1") -> str:
|
||||
"""
|
||||
Formatiert eine Telefonnummer für Google
|
||||
"""
|
||||
# Entferne alle nicht-numerischen Zeichen
|
||||
phone_digits = ''.join(c for c in phone if c.isdigit())
|
||||
|
||||
# Wenn die Nummer bereits mit Ländercode beginnt
|
||||
if phone.startswith("+"):
|
||||
return phone
|
||||
|
||||
# Füge Ländercode hinzu
|
||||
return f"{country_code}{phone_digits}"
|
||||
230
social_networks/gmail/gmail_verification.py
Normale Datei
230
social_networks/gmail/gmail_verification.py
Normale Datei
@ -0,0 +1,230 @@
|
||||
"""
|
||||
Gmail Verification - Handhabt die Verifizierungsprozesse
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, Optional
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from social_networks.gmail import gmail_selectors as selectors
|
||||
from social_networks.gmail.gmail_ui_helper import GmailUIHelper
|
||||
from utils.email_handler import EmailHandler
|
||||
|
||||
logger = logging.getLogger("gmail_verification")
|
||||
|
||||
class GmailVerification:
|
||||
"""
|
||||
Handhabt die Gmail/Google Account Verifizierung
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, ui_helper: GmailUIHelper, email_handler: EmailHandler = None,
|
||||
screenshots_dir: str = None, save_screenshots: bool = True):
|
||||
"""
|
||||
Initialisiert den Verification Handler
|
||||
"""
|
||||
self.page = page
|
||||
self.ui_helper = ui_helper
|
||||
self.email_handler = email_handler
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.save_screenshots = save_screenshots
|
||||
|
||||
def handle_phone_verification(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Handhabt die Telefonnummer-Verifizierung
|
||||
"""
|
||||
try:
|
||||
logger.info("Starte Telefon-Verifizierung")
|
||||
|
||||
# Warte auf Telefonnummer-Eingabefeld
|
||||
if not self.ui_helper.wait_for_element(selectors.PHONE_INPUT, timeout=10000):
|
||||
logger.info("Telefonnummer-Eingabefeld nicht gefunden")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Keine Telefon-Verifizierung erforderlich"
|
||||
}
|
||||
|
||||
self.ui_helper.take_screenshot("phone_verification_page")
|
||||
|
||||
# Telefonnummer eingeben
|
||||
phone = account_data.get("phone", "")
|
||||
if not phone:
|
||||
logger.warning("Keine Telefonnummer vorhanden, überspringe wenn möglich")
|
||||
# Versuche zu überspringen
|
||||
if self.ui_helper.is_element_visible(selectors.SKIP_BUTTON):
|
||||
self.ui_helper.click_with_retry(selectors.SKIP_BUTTON)
|
||||
time.sleep(random.uniform(2, 3))
|
||||
return {"success": True}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Telefonnummer erforderlich aber nicht vorhanden",
|
||||
"message": "Telefonnummer wird benötigt"
|
||||
}
|
||||
|
||||
logger.info(f"Gebe Telefonnummer ein: {phone}")
|
||||
|
||||
# Telefonnummer eingeben
|
||||
self.ui_helper.type_with_delay(selectors.PHONE_INPUT, phone)
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
# Screenshot vor dem Absenden
|
||||
self.ui_helper.take_screenshot("phone_entered")
|
||||
|
||||
# Absenden
|
||||
if self.ui_helper.wait_for_element(selectors.NEXT_BUTTON, timeout=5000):
|
||||
logger.info("Sende Telefonnummer ab")
|
||||
self.ui_helper.click_with_retry(selectors.NEXT_BUTTON)
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
time.sleep(random.uniform(3, 5))
|
||||
|
||||
# Warte auf SMS-Code Eingabefeld
|
||||
if self.ui_helper.wait_for_element(selectors.SMS_CODE_INPUT, timeout=15000):
|
||||
logger.info("SMS-Code Eingabefeld gefunden")
|
||||
self.ui_helper.take_screenshot("sms_code_page")
|
||||
|
||||
# Hier würde normalerweise der SMS-Code abgerufen werden
|
||||
sms_code = self._get_sms_code(phone)
|
||||
|
||||
if not sms_code:
|
||||
logger.error("Kein SMS-Code erhalten")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Kein SMS-Code erhalten",
|
||||
"message": "SMS-Verifizierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
# SMS-Code eingeben
|
||||
logger.info(f"Gebe SMS-Code ein: {sms_code}")
|
||||
self.ui_helper.type_with_delay(selectors.SMS_CODE_INPUT, sms_code)
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
# Code bestätigen
|
||||
if self.ui_helper.wait_for_element(selectors.VERIFY_BUTTON, timeout=5000):
|
||||
logger.info("Bestätige SMS-Code")
|
||||
self.ui_helper.click_with_retry(selectors.VERIFY_BUTTON)
|
||||
else:
|
||||
# Versuche alternativen Button
|
||||
self.ui_helper.click_with_retry(selectors.NEXT_BUTTON)
|
||||
|
||||
self.ui_helper.wait_for_loading_to_finish()
|
||||
time.sleep(random.uniform(3, 5))
|
||||
|
||||
# Prüfe auf Erfolg
|
||||
if self._check_verification_success():
|
||||
logger.info("Telefon-Verifizierung erfolgreich")
|
||||
self.ui_helper.take_screenshot("verification_success")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Verifizierung erfolgreich"
|
||||
}
|
||||
else:
|
||||
error_msg = self._get_verification_error()
|
||||
logger.error(f"Verifizierung fehlgeschlagen: {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"message": f"Verifizierung fehlgeschlagen: {error_msg}"
|
||||
}
|
||||
|
||||
else:
|
||||
logger.info("Kein SMS-Code erforderlich")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Telefon-Verifizierung abgeschlossen"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Telefon-Verifizierung: {e}")
|
||||
self.ui_helper.take_screenshot("verification_error")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Verifizierung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def handle_captcha(self) -> Dict[str, any]:
|
||||
"""
|
||||
Handhabt Captcha-Herausforderungen
|
||||
"""
|
||||
try:
|
||||
logger.info("Prüfe auf Captcha")
|
||||
|
||||
if self.ui_helper.is_element_visible(selectors.CAPTCHA_CONTAINER):
|
||||
logger.warning("Captcha erkannt - manuelle Lösung erforderlich")
|
||||
self.ui_helper.take_screenshot("captcha_detected")
|
||||
|
||||
# TODO: Implementiere Captcha-Lösung
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Captcha erkannt",
|
||||
"message": "Manuelle Captcha-Lösung erforderlich"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Kein Captcha vorhanden"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Captcha-Prüfung: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _get_sms_code(self, phone: str) -> Optional[str]:
|
||||
"""
|
||||
Ruft den SMS-Code ab
|
||||
TODO: Implementierung für echte SMS-Code Abfrage
|
||||
"""
|
||||
logger.warning("SMS-Code Abruf noch nicht implementiert - verwende Platzhalter")
|
||||
# In einer echten Implementierung würde hier der SMS-Code
|
||||
# von einem SMS-Service abgerufen werden
|
||||
return "123456" # Platzhalter
|
||||
|
||||
def _check_verification_success(self) -> bool:
|
||||
"""
|
||||
Prüft ob die Verifizierung erfolgreich war
|
||||
"""
|
||||
try:
|
||||
# Prüfe ob wir weitergeleitet wurden
|
||||
current_url = self.page.url
|
||||
if any(indicator in current_url for indicator in ["myaccount", "mail.google", "youtube"]):
|
||||
return True
|
||||
|
||||
# Prüfe ob SMS-Code Feld noch sichtbar ist
|
||||
if self.ui_helper.is_element_visible(selectors.SMS_CODE_INPUT):
|
||||
# Prüfe auf Fehlermeldung
|
||||
if self.ui_helper.is_element_visible(selectors.ERROR_MESSAGE):
|
||||
return False
|
||||
# Wenn kein Fehler aber noch SMS-Code Feld, warten wir noch
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler bei der Verifizierungs-Prüfung: {e}")
|
||||
return False
|
||||
|
||||
def _get_verification_error(self) -> str:
|
||||
"""
|
||||
Holt die Fehlermeldung falls vorhanden
|
||||
"""
|
||||
try:
|
||||
error_selectors = [
|
||||
selectors.ERROR_MESSAGE,
|
||||
selectors.ERROR_MESSAGE_ALT,
|
||||
selectors.FORM_ERROR
|
||||
]
|
||||
|
||||
for selector in error_selectors:
|
||||
if self.ui_helper.is_element_visible(selector):
|
||||
error_text = self.ui_helper.get_element_text(selector)
|
||||
if error_text:
|
||||
return error_text
|
||||
|
||||
return "Verifizierung fehlgeschlagen"
|
||||
except:
|
||||
return "Unbekannter Fehler"
|
||||
44
social_networks/gmail/gmail_workflow.py
Normale Datei
44
social_networks/gmail/gmail_workflow.py
Normale Datei
@ -0,0 +1,44 @@
|
||||
"""
|
||||
Gmail Workflow - Workflow-Definitionen für Gmail/Google Accounts
|
||||
"""
|
||||
|
||||
# Workflow-Schritte für Gmail
|
||||
REGISTRATION_WORKFLOW = [
|
||||
"navigate_to_registration",
|
||||
"fill_name_form",
|
||||
"fill_birthday_gender",
|
||||
"create_gmail_address",
|
||||
"set_password",
|
||||
"handle_phone_verification",
|
||||
"handle_recovery_email",
|
||||
"accept_terms",
|
||||
"verify_account_creation"
|
||||
]
|
||||
|
||||
LOGIN_WORKFLOW = [
|
||||
"navigate_to_login",
|
||||
"enter_email",
|
||||
"enter_password",
|
||||
"handle_2fa_if_needed",
|
||||
"verify_login_success"
|
||||
]
|
||||
|
||||
# Timeouts in Sekunden
|
||||
TIMEOUTS = {
|
||||
"page_load": 30,
|
||||
"element_wait": 10,
|
||||
"verification_wait": 60,
|
||||
"sms_wait": 120,
|
||||
"captcha_wait": 300
|
||||
}
|
||||
|
||||
# Fehler-Nachrichten
|
||||
ERROR_MESSAGES = {
|
||||
"username_taken": "Dieser Nutzername ist bereits vergeben",
|
||||
"invalid_phone": "Ungültige Telefonnummer",
|
||||
"invalid_code": "Der eingegebene Code ist ungültig",
|
||||
"too_many_attempts": "Zu viele Versuche",
|
||||
"account_suspended": "Dieses Konto wurde gesperrt",
|
||||
"captcha_required": "Bitte lösen Sie das Captcha",
|
||||
"age_restriction": "Sie müssen mindestens 13 Jahre alt sein"
|
||||
}
|
||||
4
social_networks/instagram/__init__.py
Normale Datei
4
social_networks/instagram/__init__.py
Normale Datei
@ -0,0 +1,4 @@
|
||||
# social_networks/instagram/__init__.py
|
||||
from .instagram_automation import InstagramAutomation
|
||||
|
||||
__all__ = ['InstagramAutomation']
|
||||
992
social_networks/instagram/instagram_automation.py
Normale Datei
992
social_networks/instagram/instagram_automation.py
Normale Datei
@ -0,0 +1,992 @@
|
||||
# social_networks/instagram/instagram_automation.py
|
||||
|
||||
"""
|
||||
Instagram-Automatisierung - Hauptklasse für Instagram-Automatisierungsfunktionalität
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from browser.playwright_manager import PlaywrightManager
|
||||
from browser.playwright_extensions import PlaywrightExtensions
|
||||
from browser.fingerprint_protection import FingerprintProtection
|
||||
from social_networks.base_automation import BaseAutomation
|
||||
from infrastructure.services.advanced_fingerprint_service import AdvancedFingerprintService
|
||||
from infrastructure.repositories.fingerprint_repository import FingerprintRepository
|
||||
from utils.password_generator import PasswordGenerator
|
||||
from utils.username_generator import UsernameGenerator
|
||||
from utils.birthday_generator import BirthdayGenerator
|
||||
from utils.human_behavior import HumanBehavior
|
||||
|
||||
# Importiere Helferklassen
|
||||
from .instagram_registration import InstagramRegistration
|
||||
from .instagram_login import InstagramLogin
|
||||
from .instagram_verification import InstagramVerification
|
||||
from .instagram_ui_helper import InstagramUIHelper
|
||||
from .instagram_utils import InstagramUtils
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("instagram_automation")
|
||||
|
||||
class InstagramAutomation(BaseAutomation):
|
||||
"""
|
||||
Hauptklasse für die Instagram-Automatisierung.
|
||||
Implementiert die Registrierung und Anmeldung bei Instagram.
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
Initialisiert die Instagram-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)
|
||||
"""
|
||||
# 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
|
||||
)
|
||||
|
||||
# Stealth-Modus-Einstellungen
|
||||
self.enhanced_stealth = enhanced_stealth
|
||||
self.fingerprint_noise = max(0.0, min(1.0, fingerprint_noise))
|
||||
|
||||
# Initialisiere Helferklassen
|
||||
self.registration = InstagramRegistration(self)
|
||||
self.login = InstagramLogin(self)
|
||||
self.verification = InstagramVerification(self)
|
||||
self.ui_helper = InstagramUIHelper(self)
|
||||
self.utils = InstagramUtils(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)
|
||||
|
||||
# Fingerprint Service für Account-gebundene Fingerprints
|
||||
self.fingerprint_service = AdvancedFingerprintService(FingerprintRepository())
|
||||
self.account_fingerprint = None
|
||||
|
||||
# Nutze übergebenen Fingerprint wenn vorhanden
|
||||
self.provided_fingerprint = fingerprint
|
||||
|
||||
logger.info("Instagram-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 Instagram-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 Instagram-Account-Registrierung für '{full_name}' via {registration_method}")
|
||||
self._emit_customer_log(f"📱 Instagram-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())}")
|
||||
|
||||
# Session-Daten extrahieren wenn Registrierung erfolgreich
|
||||
if result.get("success") and self.browser and hasattr(self.browser, 'page') and self.browser.page:
|
||||
try:
|
||||
if session_data:
|
||||
result["session_data"] = session_data
|
||||
logger.info("Session-Daten erfolgreich extrahiert")
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Session-Daten nicht extrahieren: {e}")
|
||||
|
||||
# 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:
|
||||
# GEÄNDERT: Browser NICHT schließen bei Account-Registrierung - User soll Kontrolle behalten
|
||||
logger.info("Account-Registrierung abgeschlossen - Browser bleibt offen für User-Kontrolle")
|
||||
# self._close_browser()
|
||||
|
||||
def _initialize_browser_with_fingerprint(self, account_id: str) -> bool:
|
||||
"""
|
||||
Initialize browser with account-bound fingerprint.
|
||||
|
||||
Args:
|
||||
account_id: The account ID to load fingerprint for
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Initializing browser with fingerprint for account: {account_id}")
|
||||
|
||||
# Get or create fingerprint for account
|
||||
logger.debug(f"Looking up fingerprint for account: {account_id}")
|
||||
fingerprint = self.fingerprint_service.get_account_fingerprint(account_id)
|
||||
|
||||
if not fingerprint:
|
||||
logger.info(f"No fingerprint found for account {account_id}, creating new one")
|
||||
proxy_location = None
|
||||
if self.use_proxy:
|
||||
try:
|
||||
proxy_info = self.proxy_rotator.get_proxy(self.proxy_type)
|
||||
proxy_location = proxy_info.get('location') if proxy_info else None
|
||||
logger.debug(f"Using proxy location: {proxy_location}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get proxy location: {e}")
|
||||
|
||||
fingerprint = self.fingerprint_service.create_account_fingerprint(
|
||||
account_id=account_id,
|
||||
profile_type="desktop",
|
||||
proxy_location=proxy_location
|
||||
)
|
||||
logger.info(f"Created new fingerprint {fingerprint.fingerprint_id} for account {account_id}")
|
||||
else:
|
||||
logger.info(f"Found existing fingerprint {fingerprint.fingerprint_id} for account {account_id}")
|
||||
|
||||
# Load fingerprint for current session
|
||||
logger.debug(f"Loading session fingerprint for {fingerprint.fingerprint_id}")
|
||||
self.account_fingerprint = self.fingerprint_service.load_for_session(fingerprint.fingerprint_id)
|
||||
logger.info(f"Successfully loaded fingerprint {fingerprint.fingerprint_id} for account {account_id}")
|
||||
|
||||
# Initialize browser with standard settings
|
||||
if not self._initialize_browser():
|
||||
return False
|
||||
|
||||
# Apply account-bound fingerprint if enhanced stealth is enabled
|
||||
if self.enhanced_stealth and self.account_fingerprint:
|
||||
# Create FingerprintProtection with loaded fingerprint
|
||||
protection = FingerprintProtection(
|
||||
context=self.browser.context,
|
||||
stealth_config={
|
||||
"noise_level": self.fingerprint_noise
|
||||
},
|
||||
fingerprint_config=self.account_fingerprint
|
||||
)
|
||||
protection.apply_to_context()
|
||||
logger.info(f"Applied account-bound fingerprint protection for {account_id}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to initialize browser with fingerprint for account {account_id}: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
print(f"[ERROR] {error_msg}")
|
||||
|
||||
# Try to continue without account-bound fingerprint
|
||||
fallback_msg = f"Falling back to standard browser initialization for account {account_id}"
|
||||
logger.warning(fallback_msg)
|
||||
print(f"[WARNING] {fallback_msg}")
|
||||
|
||||
try:
|
||||
if self._initialize_browser():
|
||||
print(f"[INFO] Fallback browser initialization successful")
|
||||
return True
|
||||
else:
|
||||
print(f"[ERROR] Fallback browser initialization failed")
|
||||
return False
|
||||
except Exception as fallback_error:
|
||||
fallback_error_msg = f"Fallback browser initialization also failed: {fallback_error}"
|
||||
logger.error(fallback_error_msg)
|
||||
print(f"[ERROR] {fallback_error_msg}")
|
||||
return False
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, account_id: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Meldet sich bei einem bestehenden Instagram-Account an.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
account_id: Optional account ID for loading saved fingerprint
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Anmeldung mit Status
|
||||
"""
|
||||
logger.info(f"Starte Instagram-Login für '{username_or_email}'")
|
||||
|
||||
try:
|
||||
# Initialize browser with account fingerprint if account_id provided
|
||||
# Prüfe ob Browser bereits offen ist (z.B. von fehlgeschlagenem Session-Login)
|
||||
if not self.browser or not hasattr(self.browser, 'page') or (hasattr(self.browser, 'page') and not self.browser.page):
|
||||
if account_id:
|
||||
# Use account-specific fingerprint
|
||||
if not self._initialize_browser_with_fingerprint(account_id):
|
||||
return {"success": False, "error": "Browser konnte nicht mit Account-Fingerprint initialisiert werden"}
|
||||
else:
|
||||
# Use random fingerprint
|
||||
if not self._initialize_browser():
|
||||
return {"success": False, "error": "Browser konnte nicht initialisiert werden"}
|
||||
else:
|
||||
logger.info("Verwende bereits geöffneten Browser für Login")
|
||||
|
||||
# No fingerprint rotation needed when using account-bound fingerprint
|
||||
if not account_id and 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())}")
|
||||
|
||||
# Session-Speicherung entfernt - nur normaler Login
|
||||
|
||||
# Update fingerprint statistics if account-bound fingerprint was used
|
||||
if account_id and self.account_fingerprint and result.get("success"):
|
||||
try:
|
||||
self.fingerprint_service.update_fingerprint_stats(
|
||||
self.account_fingerprint.fingerprint_id,
|
||||
account_id,
|
||||
success=True
|
||||
)
|
||||
logger.info(f"Updated fingerprint statistics for successful login")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update fingerprint statistics: {e}")
|
||||
|
||||
# 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"
|
||||
})
|
||||
|
||||
# Update fingerprint statistics for failure if account-bound fingerprint was used
|
||||
if account_id and self.account_fingerprint:
|
||||
try:
|
||||
self.fingerprint_service.update_fingerprint_stats(
|
||||
self.account_fingerprint.fingerprint_id,
|
||||
account_id,
|
||||
success=False
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update fingerprint statistics: {e}")
|
||||
|
||||
# KRITISCH: Session speichern VOR Browser-Schließung
|
||||
return self.status
|
||||
finally:
|
||||
# GEÄNDERT: Browser NICHT schließen bei Login - User soll Kontrolle behalten
|
||||
logger.info("Login abgeschlossen - Browser bleibt offen für User-Kontrolle")
|
||||
# self._close_browser()
|
||||
|
||||
def login_with_session(self, session_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt Login mit einer gespeicherten Session durch.
|
||||
|
||||
Args:
|
||||
session_data: Session-Daten vom OneClickLoginUseCase
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Login-Ergebnis
|
||||
"""
|
||||
try:
|
||||
logger.info("Starte Session-basierten Login")
|
||||
|
||||
# Session-Daten extrahieren
|
||||
fingerprint = session_data.get('fingerprint')
|
||||
platform_session_data = session_data.get('platform_session_data')
|
||||
browser_config = session_data.get('browser_config', {})
|
||||
|
||||
if not fingerprint:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Kein Fingerprint in Session-Daten vorhanden",
|
||||
"stage": "session_validation"
|
||||
}
|
||||
|
||||
# Browser mit Session und Fingerprint initialisieren
|
||||
logger.info("Initialisiere Browser mit Session und Fingerprint")
|
||||
|
||||
# Fingerprint als Dictionary für Browser-Initialisierung
|
||||
fingerprint_dict = {
|
||||
'canvas_noise': getattr(fingerprint, 'canvas_noise', 0.5),
|
||||
'webgl_vendor': getattr(fingerprint, 'webgl_vendor', 'Intel Inc.'),
|
||||
'webgl_renderer': getattr(fingerprint, 'webgl_renderer', 'Intel Iris OpenGL Engine'),
|
||||
'audio_context_base_latency': getattr(fingerprint, 'audio_context_base_latency', 0.01),
|
||||
'user_agent': browser_config.get('user_agent', ''),
|
||||
'viewport_size': browser_config.get('viewport_size', (1920, 1080)),
|
||||
'timezone': browser_config.get('timezone', 'Europe/Berlin'),
|
||||
'locale': browser_config.get('locale', 'de-DE')
|
||||
}
|
||||
|
||||
# Prüfen ob überhaupt Session-Daten vorhanden sind
|
||||
has_cookies = False
|
||||
cookie_count = 0
|
||||
|
||||
logger.debug(f"Checking session data for cookies...")
|
||||
logger.debug(f"Platform session data exists: {platform_session_data is not None}")
|
||||
|
||||
if platform_session_data:
|
||||
logger.debug(f"Platform session data type: {type(platform_session_data)}")
|
||||
logger.debug(f"Has cookies attribute: {hasattr(platform_session_data, 'cookies')}")
|
||||
|
||||
if hasattr(platform_session_data, 'cookies'):
|
||||
cookies_obj = platform_session_data.cookies
|
||||
logger.debug(f"Cookies object type: {type(cookies_obj)}")
|
||||
|
||||
if hasattr(cookies_obj, 'cookies'):
|
||||
cookie_count = len(cookies_obj.cookies)
|
||||
has_cookies = cookie_count > 0
|
||||
logger.debug(f"Found {cookie_count} cookies in cookies.cookies")
|
||||
elif isinstance(cookies_obj, list):
|
||||
cookie_count = len(cookies_obj)
|
||||
has_cookies = cookie_count > 0
|
||||
logger.debug(f"Found {cookie_count} cookies in list format")
|
||||
elif isinstance(cookies_obj, dict):
|
||||
cookie_count = len(cookies_obj)
|
||||
has_cookies = cookie_count > 0
|
||||
logger.debug(f"Found {cookie_count} cookies in dict format")
|
||||
else:
|
||||
logger.warning(f"Unknown cookies format: {type(cookies_obj)}")
|
||||
else:
|
||||
logger.warning(f"Platform session data has no 'cookies' attribute")
|
||||
else:
|
||||
logger.warning(f"No platform session data provided")
|
||||
|
||||
# ROBUSTE Session-Cookie-Validierung (FIX)
|
||||
critical_cookies_found = []
|
||||
critical_cookies_needed = ['sessionid', 'csrftoken', 'ds_user_id']
|
||||
|
||||
if has_cookies and platform_session_data and hasattr(platform_session_data, 'cookies'):
|
||||
cookies_obj = platform_session_data.cookies
|
||||
cookies_to_check = []
|
||||
|
||||
# Extrahiere Cookies je nach Format
|
||||
if hasattr(cookies_obj, 'cookies'):
|
||||
cookies_to_check = cookies_obj.cookies
|
||||
elif isinstance(cookies_obj, list):
|
||||
cookies_to_check = cookies_obj
|
||||
|
||||
# Prüfe auf kritische Session-Cookies
|
||||
for cookie in cookies_to_check:
|
||||
cookie_name = getattr(cookie, 'name', None) or cookie.get('name', '')
|
||||
if cookie_name in critical_cookies_needed:
|
||||
critical_cookies_found.append(cookie_name)
|
||||
logger.debug(f"Kritischer Session-Cookie gefunden: {cookie_name}")
|
||||
|
||||
logger.info(f"Session validation: has_cookies={has_cookies}, cookie_count={cookie_count}")
|
||||
logger.info(f"Kritische Session-Cookies gefunden: {critical_cookies_found}")
|
||||
|
||||
# Prüfe ob mindestens sessionid vorhanden ist
|
||||
if not has_cookies or 'sessionid' not in critical_cookies_found:
|
||||
missing_cookies = [c for c in critical_cookies_needed if c not in critical_cookies_found]
|
||||
logger.warning(f"KRITISCHE Session-Cookies fehlen: {missing_cookies}")
|
||||
logger.info("Session-Login nicht möglich - führe normalen Login durch")
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Kritische Session-Cookies fehlen: {missing_cookies} (gefunden: {critical_cookies_found})",
|
||||
"stage": "session_validation",
|
||||
"fallback_to_normal_login": True,
|
||||
"cookie_count": cookie_count,
|
||||
"critical_cookies_found": critical_cookies_found,
|
||||
"critical_cookies_missing": missing_cookies
|
||||
}
|
||||
|
||||
# Browser mit Session-Awareness starten
|
||||
success = self._initialize_browser_with_session(
|
||||
fingerprint_dict=fingerprint_dict,
|
||||
session_data=platform_session_data
|
||||
)
|
||||
|
||||
if not success:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Browser-Initialisierung mit Session fehlgeschlagen",
|
||||
"stage": "browser_init"
|
||||
}
|
||||
|
||||
# Zur Instagram-Hauptseite navigieren
|
||||
self.browser.navigate_to("https://www.instagram.com/")
|
||||
self.human_behavior.wait_for_page_load()
|
||||
|
||||
# Cookie-Consent behandeln falls nötig
|
||||
from browser.cookie_consent_handler import CookieConsentHandler
|
||||
try:
|
||||
if self.browser.page:
|
||||
consent_handled = CookieConsentHandler.check_and_handle_consent(self.browser.page, "instagram")
|
||||
if consent_handled:
|
||||
logger.info("Cookie-Consent bei Session-Wiederherstellung automatisch behandelt")
|
||||
# Warte kurz nach Consent-Behandlung
|
||||
time.sleep(3)
|
||||
|
||||
# Seite neu laden nach Cookie-Consent für bessere Session-Erkennung
|
||||
logger.info("Lade Seite neu nach Cookie-Consent...")
|
||||
self.browser.page.reload(wait_until='networkidle')
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler bei Cookie-Consent-Behandlung: {e}")
|
||||
|
||||
# Screenshot nach Session-Wiederherstellung
|
||||
self._take_screenshot("session_restored")
|
||||
|
||||
# Prüfen ob bereits eingeloggt (mit strengerer Validierung)
|
||||
if self._check_login_success_strict():
|
||||
logger.info("Session-basierter Login erfolgreich")
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "session_login_completed",
|
||||
"message": "Mit gespeicherter Session erfolgreich eingeloggt"
|
||||
}
|
||||
else:
|
||||
logger.warning("Session konnte nicht wiederhergestellt werden - falle zurück auf normalen Login")
|
||||
|
||||
# Browser NICHT schließen - wir verwenden ihn für den normalen Login weiter!
|
||||
# Der Browser ist bereits auf Instagram und hat eventuell schon Cookies akzeptiert
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Session-Wiederherstellung fehlgeschlagen - Session möglicherweise abgelaufen",
|
||||
"stage": "session_validation",
|
||||
"fallback_to_normal_login": True,
|
||||
"browser_already_open": True # Signal dass Browser offen ist
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Fehler beim Session-basierten Login: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Fehler-Screenshot
|
||||
self._take_screenshot(f"session_login_error_{int(time.time())}")
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "session_login_error"
|
||||
}
|
||||
|
||||
def _initialize_browser_with_session(self, fingerprint_dict: Dict[str, Any], session_data: Any) -> bool:
|
||||
"""
|
||||
Initialisiert Browser mit Session-Wiederherstellung.
|
||||
|
||||
Args:
|
||||
fingerprint_dict: Fingerprint-Daten
|
||||
session_data: Session-Daten zum Wiederherstellen
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg
|
||||
"""
|
||||
try:
|
||||
# Session-Aware Browser Manager verwenden
|
||||
from browser.session_aware_playwright_manager import SessionAwarePlaywrightManager
|
||||
from domain.entities.browser_session import BrowserSession
|
||||
|
||||
# BrowserSession-Objekt aus platform_session_data erstellen
|
||||
if session_data:
|
||||
# Session aus PlatformSessionData wiederherstellen
|
||||
browser_session = BrowserSession()
|
||||
|
||||
# Debug: Session-Daten-Struktur loggen
|
||||
logger.debug(f"Session-Daten-Typ: {type(session_data)}")
|
||||
logger.debug(f"Session-Daten-Attribute: {dir(session_data) if hasattr(session_data, '__dict__') else 'Keine Attribute'}")
|
||||
|
||||
# Cookies setzen - robustere Behandlung
|
||||
cookies_data = None
|
||||
if hasattr(session_data, 'cookies'):
|
||||
cookies_data = session_data.cookies
|
||||
elif isinstance(session_data, dict) and 'cookies' in session_data:
|
||||
cookies_data = session_data['cookies']
|
||||
|
||||
if cookies_data:
|
||||
logger.debug(f"Cookies-Daten-Typ: {type(cookies_data)}")
|
||||
|
||||
from domain.entities.browser_session import Cookie, SessionCookies
|
||||
session_cookies = SessionCookies()
|
||||
|
||||
# Behandle verschiedene Cookie-Formate
|
||||
try:
|
||||
if isinstance(cookies_data, list):
|
||||
# Liste von Cookie-Dictionaries
|
||||
for cookie_data in cookies_data:
|
||||
if isinstance(cookie_data, dict):
|
||||
cookie = Cookie(
|
||||
name=cookie_data.get('name', ''),
|
||||
value=cookie_data.get('value', ''),
|
||||
domain=cookie_data.get('domain', ''),
|
||||
path=cookie_data.get('path', '/'),
|
||||
secure=cookie_data.get('secure', False),
|
||||
http_only=cookie_data.get('httpOnly', False),
|
||||
same_site=cookie_data.get('sameSite', 'Lax')
|
||||
)
|
||||
session_cookies.add_cookie(cookie)
|
||||
elif hasattr(cookies_data, 'cookies'):
|
||||
# SessionCookies-Objekt
|
||||
if isinstance(cookies_data.cookies, list):
|
||||
for cookie in cookies_data.cookies:
|
||||
session_cookies.add_cookie(cookie)
|
||||
else:
|
||||
logger.debug("SessionCookies.cookies ist keine Liste")
|
||||
elif isinstance(cookies_data, dict):
|
||||
# Dictionary-Format
|
||||
for name, value in cookies_data.items():
|
||||
cookie = Cookie(
|
||||
name=name,
|
||||
value=str(value),
|
||||
domain='.instagram.com',
|
||||
path='/'
|
||||
)
|
||||
session_cookies.add_cookie(cookie)
|
||||
|
||||
browser_session.cookies = session_cookies
|
||||
logger.debug(f"Successfully converted {len(session_cookies.cookies)} cookies")
|
||||
|
||||
# Warnung wenn keine Cookies gefunden wurden
|
||||
if len(session_cookies.cookies) == 0:
|
||||
logger.warning("Keine Cookies in Session gefunden - Session ist möglicherweise leer oder abgelaufen")
|
||||
|
||||
except Exception as cookie_error:
|
||||
logger.error(f"Fehler beim Konvertieren der Cookies: {cookie_error}")
|
||||
# Fallback: Empty cookies
|
||||
browser_session.cookies = SessionCookies()
|
||||
|
||||
# Local/Session Storage setzen - robuste Behandlung
|
||||
try:
|
||||
if hasattr(session_data, 'local_storage') and session_data.local_storage:
|
||||
if isinstance(session_data.local_storage, dict):
|
||||
browser_session.local_storage.data = session_data.local_storage
|
||||
elif hasattr(session_data.local_storage, 'data'):
|
||||
# LocalStorageData-Objekt
|
||||
browser_session.local_storage.data = session_data.local_storage.data
|
||||
else:
|
||||
logger.warning(f"Local Storage hat unerwartetes Format: {type(session_data.local_storage)}")
|
||||
|
||||
if hasattr(session_data, 'session_storage') and session_data.session_storage:
|
||||
if isinstance(session_data.session_storage, dict):
|
||||
browser_session.session_storage.data = session_data.session_storage
|
||||
elif hasattr(session_data.session_storage, 'data'):
|
||||
# SessionStorageData-Objekt
|
||||
browser_session.session_storage.data = session_data.session_storage.data
|
||||
else:
|
||||
logger.warning(f"Session Storage hat unerwartetes Format: {type(session_data.session_storage)}")
|
||||
|
||||
except Exception as storage_error:
|
||||
logger.error(f"Fehler beim Setzen von Storage-Daten: {storage_error}")
|
||||
|
||||
# Session-Aware Manager erstellen
|
||||
try:
|
||||
self.browser = SessionAwarePlaywrightManager(
|
||||
headless=self.headless,
|
||||
user_agent=fingerprint_dict.get('user_agent'),
|
||||
proxy=getattr(self, 'proxy', None),
|
||||
session=browser_session,
|
||||
fingerprint=getattr(self, 'current_fingerprint', None)
|
||||
)
|
||||
logger.debug("SessionAwarePlaywrightManager erfolgreich erstellt")
|
||||
except Exception as manager_error:
|
||||
logger.error(f"Fehler beim Erstellen des SessionAwarePlaywrightManager: {manager_error}")
|
||||
# Fallback auf normalen Browser
|
||||
self.browser = PlaywrightManager(
|
||||
headless=self.headless,
|
||||
user_agent=fingerprint_dict.get('user_agent'),
|
||||
proxy=getattr(self, 'proxy', None)
|
||||
)
|
||||
else:
|
||||
# Fallback auf normalen Browser
|
||||
self.browser = PlaywrightManager(
|
||||
headless=self.headless,
|
||||
user_agent=fingerprint_dict.get('user_agent'),
|
||||
proxy=getattr(self, 'proxy', None)
|
||||
)
|
||||
|
||||
# Browser starten - prüfe ob Session-Methode vorhanden ist
|
||||
try:
|
||||
if hasattr(self.browser, 'start_with_session'):
|
||||
self.browser.start_with_session()
|
||||
else:
|
||||
# Fallback auf normalen Start
|
||||
self.browser.start()
|
||||
logger.warning("Session-Wiederherstellung nicht verfügbar, verwende normalen Browser-Start")
|
||||
except Exception as start_error:
|
||||
logger.error(f"Fehler beim Browser-Start: {start_error}")
|
||||
# Letzter Fallback
|
||||
self.browser = PlaywrightManager(
|
||||
headless=self.headless,
|
||||
user_agent=fingerprint_dict.get('user_agent'),
|
||||
proxy=getattr(self, 'proxy', None)
|
||||
)
|
||||
self.browser.start()
|
||||
|
||||
# Human Behavior initialisieren
|
||||
self.human_behavior = HumanBehavior()
|
||||
|
||||
logger.info("Browser mit Session erfolgreich initialisiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Browser-Initialisierung mit Session: {e}")
|
||||
return False
|
||||
|
||||
def _check_login_success(self) -> bool:
|
||||
"""Prüft ob der Login erfolgreich war (wiederverwendet von InstagramLogin)."""
|
||||
try:
|
||||
# Wiederverwendung der Logik aus instagram_login.py
|
||||
from .instagram_selectors import InstagramSelectors
|
||||
|
||||
# Erfolg anhand verschiedener Indikatoren prüfen (Updated UI)
|
||||
success_indicators = InstagramSelectors.SUCCESS_INDICATORS
|
||||
|
||||
found_indicators = 0
|
||||
for indicator in success_indicators:
|
||||
if self.browser.is_element_visible(indicator, timeout=2000):
|
||||
logger.debug(f"Login-Indikator gefunden: {indicator}")
|
||||
found_indicators += 1
|
||||
if found_indicators >= 1: # Mindestens ein Indikator reicht
|
||||
logger.info(f"Login erfolgreich - {found_indicators} Indikator(en) gefunden")
|
||||
return True
|
||||
|
||||
# Alternativ prüfen, ob wir auf der Instagram-Startseite sind
|
||||
current_url = self.browser.page.url
|
||||
if "instagram.com" in current_url and "/accounts/login" not in current_url:
|
||||
logger.info(f"Login-Erfolg basierend auf URL: {current_url}")
|
||||
return True
|
||||
|
||||
# Prüfen, ob immer noch auf der Login-Seite
|
||||
if "/accounts/login" in current_url:
|
||||
logger.warning("Immer noch auf der Login-Seite, Login fehlgeschlagen")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen des Login-Erfolgs: {e}")
|
||||
return False
|
||||
|
||||
def _check_login_success_strict(self) -> bool:
|
||||
"""Strenge Prüfung ob wirklich eingeloggt (nicht nur Homepage erreicht)."""
|
||||
try:
|
||||
from .instagram_selectors import InstagramSelectors
|
||||
|
||||
current_url = self.browser.page.url
|
||||
|
||||
# 1. Nicht auf Login-Seite sein
|
||||
if "/accounts/login" in current_url:
|
||||
logger.debug("Noch auf Login-Seite")
|
||||
return False
|
||||
|
||||
# 2. Spezifische Login-Indikatoren prüfen (Deutsch und Englisch)
|
||||
login_indicators = [
|
||||
"//a[@aria-label='Home']", # Home-Button
|
||||
"//a[@aria-label='Startseite']", # Home-Button Deutsch
|
||||
"//a[@aria-label='Search']", # Suche-Button
|
||||
"//a[@aria-label='Suchen']", # Suche-Button Deutsch
|
||||
"//a[@aria-label='New post']", # Neuer Post Button
|
||||
"//button[@aria-label='New post']", # Alternativer Neuer Post Button
|
||||
"//a[@aria-label='Neuer Beitrag']", # Neuer Post Deutsch
|
||||
"//button[@aria-label='Neuer Beitrag']", # Neuer Post Button Deutsch
|
||||
"//svg[@aria-label='Instagram']", # Instagram Logo im Header
|
||||
"//a[contains(@href, '/direct/')]", # Direct Messages
|
||||
"//input[@placeholder='Search']", # Suchfeld
|
||||
"//input[@placeholder='Suchen']", # Suchfeld Deutsch
|
||||
"//span[contains(text(), 'Stories')]", # Stories-Text
|
||||
"//span[contains(text(), 'Story')]" # Story-Text Deutsch
|
||||
]
|
||||
|
||||
found_indicators = 0
|
||||
for indicator in login_indicators:
|
||||
if self.browser.is_element_visible(indicator, timeout=1000):
|
||||
found_indicators += 1
|
||||
logger.debug(f"Login-Indikator gefunden: {indicator}")
|
||||
|
||||
# Mindestens 2 Indikatoren für validen Login
|
||||
if found_indicators >= 2:
|
||||
logger.info(f"Session-Login bestätigt ({found_indicators} Indikatoren gefunden)")
|
||||
return True
|
||||
|
||||
logger.warning(f"Nur {found_indicators} Login-Indikatoren gefunden, Login unsicher")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei strenger Login-Prüfung: {e}")
|
||||
return False
|
||||
|
||||
def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Verifiziert einen Instagram-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 Instagram-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 _extract_session_data(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Extrahiert Session-Daten aus dem aktuellen Browser-Kontext.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Session-Daten oder None bei Fehler
|
||||
"""
|
||||
try:
|
||||
if not self.browser or not hasattr(self.browser, 'page') or not self.browser.page:
|
||||
return None
|
||||
|
||||
# Browser-Kontext-Daten sammeln
|
||||
browser_context_data = {
|
||||
"cookies": self.browser.context.cookies() if hasattr(self.browser, 'context') else [],
|
||||
"local_storage": {},
|
||||
"session_storage": {}
|
||||
}
|
||||
|
||||
# Local Storage und Session Storage extrahieren
|
||||
try:
|
||||
local_storage = self.browser.page.evaluate("""() => {
|
||||
const items = {};
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
items[key] = localStorage.getItem(key);
|
||||
}
|
||||
return items;
|
||||
}""")
|
||||
browser_context_data["local_storage"] = local_storage
|
||||
|
||||
session_storage = self.browser.page.evaluate("""() => {
|
||||
const items = {};
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
items[key] = sessionStorage.getItem(key);
|
||||
}
|
||||
return items;
|
||||
}""")
|
||||
browser_context_data["session_storage"] = session_storage
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Storage-Daten nicht extrahieren: {e}")
|
||||
|
||||
# Fingerprint-Daten
|
||||
fingerprint = None
|
||||
if self.enhanced_stealth and hasattr(self.browser, 'get_current_fingerprint'):
|
||||
fingerprint = self.browser.get_current_fingerprint()
|
||||
|
||||
# Plattform-spezifische Daten
|
||||
platform_session_data = {
|
||||
"user_agent": self.browser.page.evaluate("() => navigator.userAgent") if self.browser.page else None,
|
||||
"viewport": self.browser.page.viewport_size if hasattr(self.browser.page, 'viewport_size') else None,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
return {
|
||||
"browser_context": browser_context_data,
|
||||
"fingerprint": fingerprint,
|
||||
"platform_data": platform_session_data
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren der Session-Daten: {e}")
|
||||
return None
|
||||
|
||||
# Session-Speicherung entfernt
|
||||
1586
social_networks/instagram/instagram_login.py
Normale Datei
1586
social_networks/instagram/instagram_login.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
779
social_networks/instagram/instagram_registration.py
Normale Datei
779
social_networks/instagram/instagram_registration.py
Normale Datei
@ -0,0 +1,779 @@
|
||||
# social_networks/instagram/instagram_registration.py
|
||||
|
||||
"""
|
||||
Instagram-Registrierung - Klasse für die Kontoerstellung bei Instagram
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .instagram_selectors import InstagramSelectors
|
||||
from .instagram_workflow import InstagramWorkflow
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("instagram_registration")
|
||||
|
||||
class InstagramRegistration:
|
||||
"""
|
||||
Klasse für die Registrierung von Instagram-Konten.
|
||||
Enthält alle Methoden zur Kontoerstellung.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die Instagram-Registrierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = InstagramSelectors()
|
||||
self.workflow = InstagramWorkflow.get_registration_workflow()
|
||||
|
||||
logger.debug("Instagram-Registrierung initialisiert")
|
||||
|
||||
def register_account(self, full_name: str, age: int, registration_method: str = "email",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den vollständigen Registrierungsprozess für einen Instagram-Account durch.
|
||||
|
||||
Args:
|
||||
full_name: Vollständiger Name für den Account
|
||||
age: Alter des Benutzers
|
||||
registration_method: "email" oder "phone"
|
||||
phone_number: Telefonnummer (nur bei registration_method="phone")
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten
|
||||
"""
|
||||
# Browser wird direkt von automation verwendet
|
||||
|
||||
# Validiere die Eingaben
|
||||
if not self._validate_registration_inputs(full_name, age, registration_method, phone_number):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültige Eingabeparameter",
|
||||
"stage": "input_validation"
|
||||
}
|
||||
|
||||
# Account-Daten generieren
|
||||
account_data = self._generate_account_data(full_name, age, registration_method, phone_number, **kwargs)
|
||||
|
||||
# Starte den Registrierungsprozess
|
||||
logger.info(f"Starte Instagram-Registrierung für {account_data['username']} via {registration_method}")
|
||||
|
||||
try:
|
||||
# 1. Zur Registrierungsseite navigieren
|
||||
self.automation._emit_customer_log("🌐 Mit Instagram verbinden...")
|
||||
if not self._navigate_to_signup_page():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht zur Registrierungsseite navigieren",
|
||||
"stage": "navigation",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 2. Cookie-Banner bereits in _navigate_to_signup_page behandelt (TIMING-FIX)
|
||||
self.automation._emit_customer_log("⚙️ Einstellungen wurden vorbereitet...")
|
||||
|
||||
# 3. Registrierungsmethode wählen
|
||||
if not self._select_registration_method(registration_method):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen",
|
||||
"stage": "registration_method",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 4. Registrierungsformular ausfüllen
|
||||
self.automation._emit_customer_log("📝 Persönliche Daten werden übertragen...")
|
||||
if not self._fill_registration_form(account_data):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Ausfüllen des Registrierungsformulars",
|
||||
"stage": "registration_form",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 5. Geburtsdatum eingeben
|
||||
self.automation._emit_customer_log("🎂 Geburtsdatum wird festgelegt...")
|
||||
if not self._select_birthday(account_data["birthday"]):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Eingeben des Geburtsdatums",
|
||||
"stage": "birthday",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 6. Bestätigungscode abrufen und eingeben
|
||||
self.automation._emit_customer_log("📧 Auf Bestätigungscode warten...")
|
||||
if not self._handle_verification(account_data, registration_method):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler bei der Verifizierung",
|
||||
"stage": "verification",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 7. Erfolgreiche Registrierung überprüfen
|
||||
self.automation._emit_customer_log("🔍 Account wird finalisiert...")
|
||||
if not self._check_registration_success():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Registrierung fehlgeschlagen oder konnte nicht verifiziert werden",
|
||||
"stage": "final_check",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# Registrierung erfolgreich abgeschlossen
|
||||
logger.info(f"Instagram-Account {account_data['username']} erfolgreich erstellt")
|
||||
self.automation._emit_customer_log("✅ Account erfolgreich erstellt!")
|
||||
|
||||
# 8. Cookie-Consent behandeln (falls am Ende der Registrierung angezeigt)
|
||||
try:
|
||||
logger.info("Prüfe auf Cookie-Consent Dialog nach Registrierung...")
|
||||
time.sleep(2) # Kurz warten, falls Dialog erscheint
|
||||
|
||||
# Import Cookie Handler (nur wenn benötigt)
|
||||
from browser.cookie_consent_handler import CookieConsentHandler
|
||||
|
||||
# Cookie-Consent behandeln
|
||||
if CookieConsentHandler.check_and_handle_consent(self.automation.browser.page, "instagram"):
|
||||
logger.info("Cookie-Consent nach Registrierung behandelt")
|
||||
self.automation._emit_customer_log("🍪 Cookie-Einstellungen akzeptiert")
|
||||
time.sleep(1) # Kurz warten nach Cookie-Consent
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler bei Cookie-Consent Behandlung: {e}")
|
||||
# Kein kritischer Fehler - Registrierung war bereits erfolgreich
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "completed",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der Instagram-Registrierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
def _validate_registration_inputs(self, full_name: str, age: int,
|
||||
registration_method: str, phone_number: str) -> bool:
|
||||
"""
|
||||
Validiert die Eingaben für die Registrierung.
|
||||
|
||||
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")
|
||||
|
||||
Returns:
|
||||
bool: True wenn alle Eingaben gültig sind, False sonst
|
||||
"""
|
||||
# Vollständiger Name prüfen
|
||||
if not full_name or len(full_name) < 3:
|
||||
logger.error("Ungültiger vollständiger Name")
|
||||
return False
|
||||
|
||||
# Alter prüfen
|
||||
if age < 13:
|
||||
logger.error("Benutzer muss mindestens 13 Jahre alt sein")
|
||||
return False
|
||||
|
||||
# Registrierungsmethode prüfen
|
||||
if registration_method not in ["email", "phone"]:
|
||||
logger.error(f"Ungültige Registrierungsmethode: {registration_method}")
|
||||
return False
|
||||
|
||||
# Telefonnummer prüfen, falls erforderlich
|
||||
if registration_method == "phone" and not phone_number:
|
||||
logger.error("Telefonnummer erforderlich für Registrierung via Telefon")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _generate_account_data(self, full_name: str, age: int, registration_method: str,
|
||||
phone_number: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Generiert Account-Daten für die Registrierung.
|
||||
|
||||
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]: Generierte Account-Daten
|
||||
"""
|
||||
# Benutzername generieren
|
||||
username = kwargs.get("username")
|
||||
if not username:
|
||||
username = self.automation.username_generator.generate_username("instagram", full_name)
|
||||
|
||||
# Passwort generieren
|
||||
password = kwargs.get("password")
|
||||
if not password:
|
||||
password = self.automation.password_generator.generate_password("instagram")
|
||||
|
||||
# E-Mail generieren (falls nötig)
|
||||
email = None
|
||||
if registration_method == "email":
|
||||
email_prefix = username.lower().replace(".", "").replace("_", "")
|
||||
email = f"{email_prefix}@{self.automation.email_domain}"
|
||||
|
||||
# Geburtsdatum generieren
|
||||
birthday = self.automation.birthday_generator.generate_birthday_components("instagram", age)
|
||||
|
||||
# Account-Daten zusammenstellen
|
||||
account_data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"full_name": full_name,
|
||||
"email": email,
|
||||
"phone": phone_number,
|
||||
"birthday": birthday,
|
||||
"age": age,
|
||||
"registration_method": registration_method
|
||||
}
|
||||
|
||||
logger.debug(f"Account-Daten generiert: {account_data['username']}")
|
||||
|
||||
return account_data
|
||||
|
||||
def _navigate_to_signup_page(self) -> bool:
|
||||
"""
|
||||
Navigiert zur Instagram-Registrierungsseite.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Zur Registrierungsseite navigieren
|
||||
self.automation.browser.navigate_to(InstagramSelectors.SIGNUP_URL)
|
||||
|
||||
# Warten, bis die Seite geladen ist
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# SOFORT Cookie-Banner behandeln BEVOR weitere Aktionen (TIMING-FIX)
|
||||
logger.info("Behandle Cookie-Banner SOFORT nach Navigation für korrekte Session-Cookies")
|
||||
cookie_handled = self._handle_cookie_banner()
|
||||
if not cookie_handled:
|
||||
logger.warning("Cookie-Banner konnte nicht behandelt werden - Session könnte beeinträchtigt sein")
|
||||
|
||||
# Kurz warten damit Cookies gesetzt werden können
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("signup_page")
|
||||
|
||||
# Prüfen, ob Registrierungsformular sichtbar ist
|
||||
if not self.automation.browser.is_element_visible(InstagramSelectors.EMAIL_PHONE_FIELD, timeout=5000):
|
||||
logger.warning("Registrierungsformular nicht sichtbar")
|
||||
return False
|
||||
|
||||
logger.info("Erfolgreich zur Registrierungsseite navigiert und Cookies akzeptiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Navigieren zur Registrierungsseite: {e}")
|
||||
return False
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
Akzeptiert IMMER Cookies für vollständiges Session-Management bei der Registrierung.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
# Cookie-Dialog-Erkennung
|
||||
if self.automation.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000):
|
||||
logger.info("Cookie-Banner erkannt - akzeptiere alle Cookies für Session-Management")
|
||||
|
||||
# Akzeptieren-Button suchen und klicken (PRIMÄR für Registrierung)
|
||||
accept_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
InstagramSelectors.get_button_texts("accept_cookies"),
|
||||
InstagramSelectors.COOKIE_ACCEPT_BUTTON
|
||||
)
|
||||
|
||||
if accept_success:
|
||||
logger.info("Cookie-Banner erfolgreich akzeptiert - Session-Cookies werden gespeichert")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
else:
|
||||
logger.warning("Konnte Cookie-Banner nicht akzeptieren, versuche alternativen Akzeptieren-Button")
|
||||
|
||||
# Alternative Akzeptieren-Selektoren versuchen
|
||||
alternative_accept_selectors = [
|
||||
"//button[contains(text(), 'Alle akzeptieren')]",
|
||||
"//button[contains(text(), 'Accept All')]",
|
||||
"//button[contains(text(), 'Zulassen')]",
|
||||
"//button[contains(text(), 'Allow All')]",
|
||||
"//button[contains(@aria-label, 'Accept')]",
|
||||
"[data-testid='accept-all-button']"
|
||||
]
|
||||
|
||||
for selector in alternative_accept_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Cookie-Banner mit alternativem Selector akzeptiert")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
logger.error("Konnte Cookie-Banner nicht akzeptieren - Session-Management könnte beeinträchtigt sein")
|
||||
return False
|
||||
else:
|
||||
logger.debug("Kein Cookie-Banner erkannt")
|
||||
return True
|
||||
|
||||
def _select_registration_method(self, method: str) -> bool:
|
||||
"""
|
||||
Wählt die Registrierungsmethode (E-Mail oder Telefon).
|
||||
|
||||
Args:
|
||||
method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
if method == "email":
|
||||
# Prüfen, ob der E-Mail-Tab ausgewählt werden muss
|
||||
if self.automation.browser.is_element_visible("//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]"):
|
||||
success = self.automation.ui_helper.click_button_fuzzy(
|
||||
InstagramSelectors.get_tab_texts("email"),
|
||||
InstagramSelectors.EMAIL_TAB
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("E-Mail-Registrierungsmethode ausgewählt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
else:
|
||||
logger.warning("Konnte E-Mail-Tab nicht auswählen")
|
||||
return False
|
||||
|
||||
# E-Mail ist vermutlich bereits ausgewählt
|
||||
logger.debug("E-Mail-Registrierung erscheint bereits ausgewählt")
|
||||
return True
|
||||
|
||||
elif method == "phone":
|
||||
# Prüfen, ob der Telefon-Tab sichtbar ist und klicken
|
||||
if self.automation.browser.is_element_visible("//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]"):
|
||||
success = self.automation.ui_helper.click_button_fuzzy(
|
||||
InstagramSelectors.get_tab_texts("phone"),
|
||||
InstagramSelectors.PHONE_TAB
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Telefon-Registrierungsmethode ausgewählt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
else:
|
||||
logger.warning("Konnte Telefon-Tab nicht auswählen")
|
||||
return False
|
||||
|
||||
# Telefon ist möglicherweise bereits ausgewählt
|
||||
logger.debug("Telefon-Registrierung erscheint bereits ausgewählt")
|
||||
return True
|
||||
|
||||
logger.error(f"Ungültige Registrierungsmethode: {method}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Auswählen der Registrierungsmethode: {e}")
|
||||
return False
|
||||
|
||||
def _fill_registration_form(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Füllt das Registrierungsformular aus und sendet es ab.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten für die Registrierung
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# E-Mail/Telefon eingeben
|
||||
if account_data["registration_method"] == "email":
|
||||
email_phone_value = account_data["email"]
|
||||
else:
|
||||
email_phone_value = account_data["phone"]
|
||||
|
||||
# E-Mail-/Telefon-Feld ausfüllen
|
||||
email_phone_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
InstagramSelectors.get_field_labels("email_phone"),
|
||||
email_phone_value,
|
||||
InstagramSelectors.EMAIL_PHONE_FIELD
|
||||
)
|
||||
|
||||
if not email_phone_success:
|
||||
logger.error("Konnte E-Mail/Telefon-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
|
||||
# Vollständigen Namen eingeben
|
||||
name_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
InstagramSelectors.get_field_labels("full_name"),
|
||||
account_data["full_name"],
|
||||
InstagramSelectors.FULLNAME_FIELD
|
||||
)
|
||||
|
||||
if not name_success:
|
||||
logger.error("Konnte Vollständiger-Name-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
|
||||
# Benutzernamen eingeben
|
||||
username_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
InstagramSelectors.get_field_labels("username"),
|
||||
account_data["username"],
|
||||
InstagramSelectors.USERNAME_FIELD
|
||||
)
|
||||
|
||||
if not username_success:
|
||||
logger.error("Konnte Benutzername-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
|
||||
# Passwort eingeben
|
||||
password_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
InstagramSelectors.get_field_labels("password"),
|
||||
account_data["password"],
|
||||
InstagramSelectors.PASSWORD_FIELD
|
||||
)
|
||||
|
||||
if not password_success:
|
||||
logger.error("Konnte Passwort-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot vorm Absenden
|
||||
self.automation._take_screenshot("registration_form_filled")
|
||||
|
||||
# Formular absenden
|
||||
submit_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
InstagramSelectors.get_button_texts("submit"),
|
||||
InstagramSelectors.SUBMIT_BUTTON
|
||||
)
|
||||
|
||||
if not submit_success:
|
||||
logger.error("Konnte Registrierungsformular nicht absenden")
|
||||
return False
|
||||
|
||||
# Prüfen, ob es Fehler gab
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
self.automation._emit_customer_log("⏳ Daten werden überprüft...")
|
||||
|
||||
# Nach dem Absenden prüfen, ob das Formular für das Geburtsdatum erscheint
|
||||
birthday_visible = self.automation.browser.is_element_visible(InstagramSelectors.BIRTHDAY_MONTH_SELECT, timeout=10000)
|
||||
|
||||
if not birthday_visible:
|
||||
# Auf mögliche Fehlermeldung prüfen
|
||||
error_message = self.automation.ui_helper.check_for_error(
|
||||
error_selectors=[InstagramSelectors.ERROR_MESSAGE],
|
||||
error_texts=InstagramSelectors.get_error_indicators()
|
||||
)
|
||||
|
||||
if error_message:
|
||||
logger.error(f"Fehler beim Absenden des Formulars: {error_message}")
|
||||
return False
|
||||
|
||||
logger.error("Geburtstagsformular nicht sichtbar nach Absenden")
|
||||
return False
|
||||
|
||||
logger.info("Registrierungsformular erfolgreich ausgefüllt und abgesendet")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Registrierungsformulars: {e}")
|
||||
return False
|
||||
|
||||
def _select_birthday(self, birthday: Dict[str, int]) -> bool:
|
||||
"""
|
||||
Wählt das Geburtsdatum aus den Dropdown-Menüs aus.
|
||||
|
||||
Args:
|
||||
birthday: Geburtsdatum als Dictionary mit 'year', 'month', 'day' Keys
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Warten, bis die Dropdowns sichtbar sind
|
||||
if not self.automation.browser.is_element_visible(InstagramSelectors.BIRTHDAY_MONTH_SELECT, timeout=5000):
|
||||
logger.error("Geburtstags-Dropdowns nicht sichtbar")
|
||||
return False
|
||||
|
||||
# Screenshot zu Beginn
|
||||
self.automation._take_screenshot("birthday_form")
|
||||
|
||||
# Monat auswählen
|
||||
month_success = self.automation.browser.select_option(
|
||||
InstagramSelectors.BIRTHDAY_MONTH_SELECT,
|
||||
str(birthday["month"])
|
||||
)
|
||||
|
||||
if not month_success:
|
||||
logger.error(f"Konnte Monat nicht auswählen: {birthday['month']}")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.3, 0.8)
|
||||
|
||||
# Tag auswählen
|
||||
day_success = self.automation.browser.select_option(
|
||||
InstagramSelectors.BIRTHDAY_DAY_SELECT,
|
||||
str(birthday["day"])
|
||||
)
|
||||
|
||||
if not day_success:
|
||||
logger.error(f"Konnte Tag nicht auswählen: {birthday['day']}")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.3, 0.8)
|
||||
|
||||
# Jahr auswählen
|
||||
year_success = self.automation.browser.select_option(
|
||||
InstagramSelectors.BIRTHDAY_YEAR_SELECT,
|
||||
str(birthday["year"])
|
||||
)
|
||||
|
||||
if not year_success:
|
||||
logger.error(f"Konnte Jahr nicht auswählen: {birthday['year']}")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot vor dem Absenden
|
||||
self.automation._take_screenshot("birthday_selected")
|
||||
|
||||
# Weiter-Button klicken
|
||||
next_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
InstagramSelectors.get_button_texts("next"),
|
||||
InstagramSelectors.NEXT_BUTTON
|
||||
)
|
||||
|
||||
if not next_success:
|
||||
logger.error("Konnte Weiter-Button nicht klicken")
|
||||
return False
|
||||
|
||||
# Prüfen, ob es zum Bestätigungscode-Formular weitergeht
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Nach dem Absenden auf den Bestätigungscode-Bildschirm warten
|
||||
confirmation_visible = self.automation.browser.is_element_visible(
|
||||
InstagramSelectors.CONFIRMATION_CODE_FIELD, timeout=10000
|
||||
) or self.automation.browser.is_element_visible(
|
||||
InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, timeout=2000
|
||||
)
|
||||
|
||||
if not confirmation_visible:
|
||||
# Auf mögliche Fehlermeldung prüfen
|
||||
error_message = self.automation.ui_helper.check_for_error(
|
||||
error_selectors=[InstagramSelectors.ERROR_MESSAGE],
|
||||
error_texts=InstagramSelectors.get_error_indicators()
|
||||
)
|
||||
|
||||
if error_message:
|
||||
logger.error(f"Fehler nach dem Absenden des Geburtsdatums: {error_message}")
|
||||
return False
|
||||
|
||||
logger.error("Bestätigungscode-Formular nicht sichtbar nach Absenden des Geburtsdatums")
|
||||
return False
|
||||
|
||||
logger.info("Geburtsdatum erfolgreich ausgewählt und abgesendet")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Auswählen des Geburtsdatums: {e}")
|
||||
return False
|
||||
|
||||
def _handle_verification(self, account_data: Dict[str, Any], registration_method: str) -> bool:
|
||||
"""
|
||||
Behandelt den Verifizierungsprozess (E-Mail/SMS).
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten mit E-Mail/Telefon
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Warten, bis die Bestätigungscode-Eingabe sichtbar ist
|
||||
if not self.automation.browser.is_element_visible(InstagramSelectors.CONFIRMATION_CODE_FIELD, timeout=5000) and not \
|
||||
self.automation.browser.is_element_visible(InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, timeout=1000):
|
||||
logger.error("Bestätigungscode-Eingabe nicht sichtbar")
|
||||
return False
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_page")
|
||||
|
||||
# Verifizierungscode je nach Methode abrufen
|
||||
if registration_method == "email":
|
||||
# Verifizierungscode von E-Mail abrufen
|
||||
verification_code = self._get_email_confirmation_code(account_data["email"])
|
||||
else:
|
||||
# Verifizierungscode von SMS abrufen
|
||||
verification_code = self._get_sms_confirmation_code(account_data["phone"])
|
||||
|
||||
if not verification_code:
|
||||
logger.error("Konnte keinen Verifizierungscode abrufen")
|
||||
return False
|
||||
|
||||
logger.info(f"Verifizierungscode erhalten: {verification_code}")
|
||||
|
||||
# Verifizierungscode eingeben und absenden
|
||||
return self.automation.verification.enter_and_submit_verification_code(verification_code)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Verifizierung: {e}")
|
||||
return False
|
||||
|
||||
def _get_email_confirmation_code(self, email: str) -> Optional[str]:
|
||||
"""
|
||||
Ruft den Bestätigungscode von einer E-Mail ab.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse, an die der Code gesendet wurde
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden
|
||||
"""
|
||||
try:
|
||||
# Warte auf die E-Mail
|
||||
verification_code = self.automation.email_handler.get_verification_code(
|
||||
target_email=email, # Verwende die vollständige E-Mail-Adresse
|
||||
platform="instagram",
|
||||
max_attempts=60, # 60 Versuche * 2 Sekunden = 120 Sekunden
|
||||
delay_seconds=2
|
||||
)
|
||||
|
||||
if verification_code:
|
||||
return verification_code
|
||||
|
||||
# Wenn kein Code gefunden wurde, prüfen, ob der Code vielleicht direkt angezeigt wird
|
||||
verification_code = self._extract_code_from_page()
|
||||
|
||||
if verification_code:
|
||||
logger.info(f"Verifizierungscode direkt von der Seite extrahiert: {verification_code}")
|
||||
return verification_code
|
||||
|
||||
logger.warning(f"Konnte keinen Verifizierungscode für {email} finden")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen des E-Mail-Bestätigungscodes: {e}")
|
||||
return None
|
||||
|
||||
def _get_sms_confirmation_code(self, phone: str) -> Optional[str]:
|
||||
"""
|
||||
Ruft den Bestätigungscode aus einer SMS ab.
|
||||
Hier müsste ein SMS-Empfangs-Service eingebunden werden.
|
||||
|
||||
Args:
|
||||
phone: Telefonnummer, an die der Code gesendet wurde
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden
|
||||
"""
|
||||
# Diese Implementierung ist ein Platzhalter
|
||||
# In einer echten Implementierung würde hier ein SMS-Empfangs-Service verwendet
|
||||
logger.warning("SMS-Verifizierung ist noch nicht implementiert")
|
||||
|
||||
# Versuche, den Code trotzdem zu extrahieren, falls er auf der Seite angezeigt wird
|
||||
return self._extract_code_from_page()
|
||||
|
||||
def _extract_code_from_page(self) -> Optional[str]:
|
||||
"""
|
||||
Versucht, einen Bestätigungscode direkt von der Seite zu extrahieren.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der extrahierte Code oder None, wenn nicht gefunden
|
||||
"""
|
||||
try:
|
||||
# Gesamten Seiteninhalt abrufen
|
||||
page_content = self.automation.browser.page.content()
|
||||
|
||||
# Mögliche Regex-Muster für Bestätigungscodes
|
||||
patterns = [
|
||||
r"Dein Code ist (\d{6})",
|
||||
r"Your code is (\d{6})",
|
||||
r"Bestätigungscode: (\d{6})",
|
||||
r"Confirmation code: (\d{6})",
|
||||
r"(\d{6}) ist dein Instagram-Code",
|
||||
r"(\d{6}) is your Instagram code"
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, page_content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren des Codes von der Seite: {e}")
|
||||
return None
|
||||
|
||||
def _check_registration_success(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob die Registrierung erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich, False sonst
|
||||
"""
|
||||
try:
|
||||
# Warten nach der Verifizierung
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=2.0)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("registration_final")
|
||||
|
||||
# Erfolg anhand verschiedener Indikatoren prüfen
|
||||
success_indicators = InstagramSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self.automation.browser.is_element_visible(indicator, timeout=2000):
|
||||
logger.info(f"Erfolgsindikator gefunden: {indicator}")
|
||||
return True
|
||||
|
||||
# Alternativ prüfen, ob wir auf der Instagram-Startseite sind
|
||||
current_url = self.automation.browser.page.url
|
||||
if "instagram.com" in current_url and "/accounts/" not in current_url:
|
||||
logger.info(f"Erfolg basierend auf URL: {current_url}")
|
||||
return True
|
||||
|
||||
# Prüfen, ob wir auf weitere Einrichtungsschritte weitergeleitet wurden
|
||||
# (z.B. "Folge anderen Benutzern" oder "Füge ein Profilbild hinzu")
|
||||
# Diese sind optional und gelten als erfolgreiche Registrierung
|
||||
if self.automation.ui_helper.check_for_next_steps():
|
||||
logger.info("Auf weitere Einrichtungsschritte weitergeleitet, Registrierung erfolgreich")
|
||||
return True
|
||||
|
||||
logger.warning("Keine Erfolgsindikatoren gefunden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen des Registrierungserfolgs: {e}")
|
||||
return False
|
||||
263
social_networks/instagram/instagram_selectors.py
Normale Datei
263
social_networks/instagram/instagram_selectors.py
Normale Datei
@ -0,0 +1,263 @@
|
||||
"""
|
||||
Instagram-Selektoren - CSS-Selektoren und XPath-Ausdrücke für die Instagram-Automatisierung
|
||||
Mit zusätzlichen Text-Matching-Funktionen für robustere Element-Erkennung
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
class InstagramSelectors:
|
||||
"""
|
||||
Zentrale Sammlung aller Selektoren für die Instagram-Automatisierung.
|
||||
Bei Änderungen der Instagram-Webseite müssen nur hier Anpassungen vorgenommen werden.
|
||||
Enthält auch Fuzzy-Text-Matching-Daten für robustere Element-Erkennung.
|
||||
"""
|
||||
|
||||
# URL-Konstanten
|
||||
BASE_URL = "https://www.instagram.com"
|
||||
SIGNUP_URL = "https://www.instagram.com/accounts/emailsignup/"
|
||||
LOGIN_URL = "https://www.instagram.com/accounts/login"
|
||||
|
||||
# Cookie-Banner
|
||||
COOKIE_DIALOG = "div[role='dialog']"
|
||||
COOKIE_REJECT_BUTTON = "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]"
|
||||
COOKIE_ACCEPT_BUTTON = "//button[contains(text(), 'Akzeptieren') or contains(text(), 'Accept') or contains(text(), 'Zulassen')]"
|
||||
|
||||
# Registrierungsformular - Hauptfelder
|
||||
EMAIL_PHONE_FIELD = "input[name='emailOrPhone']"
|
||||
FULLNAME_FIELD = "input[name='fullName']"
|
||||
USERNAME_FIELD = "input[name='username']"
|
||||
PASSWORD_FIELD = "input[name='password']"
|
||||
|
||||
# Registrierungsformular - Alternative Selektoren
|
||||
ALT_EMAIL_FIELD = "//input[@aria-label='Handynummer oder E-Mail-Adresse']"
|
||||
ALT_FULLNAME_FIELD = "//input[@aria-label='Vollständiger Name']"
|
||||
ALT_USERNAME_FIELD = "//input[@aria-label='Benutzername']"
|
||||
ALT_PASSWORD_FIELD = "//input[@aria-label='Passwort']"
|
||||
|
||||
# Geburtsdatum-Selektoren
|
||||
BIRTHDAY_MONTH_SELECT = "//select[@title='Monat:']"
|
||||
BIRTHDAY_DAY_SELECT = "//select[@title='Tag:']"
|
||||
BIRTHDAY_YEAR_SELECT = "//select[@title='Jahr:']"
|
||||
|
||||
# Buttons
|
||||
SUBMIT_BUTTON = "button[type='submit']"
|
||||
NEXT_BUTTON = "//button[contains(text(), 'Weiter') or contains(text(), 'Next')]"
|
||||
CONFIRMATION_BUTTON = "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]"
|
||||
|
||||
# Bestätigungscode
|
||||
CONFIRMATION_CODE_FIELD = "input[name='confirmationCode']"
|
||||
ALT_CONFIRMATION_CODE_FIELD = "//input[@aria-label='Bestätigungscode']"
|
||||
RESEND_CODE_BUTTON = "//button[contains(text(), 'Code erneut senden') or contains(text(), 'Resend code')]"
|
||||
|
||||
# Fehlermeldungen
|
||||
ERROR_MESSAGE = "p[class*='error'], div[role='alert']"
|
||||
CAPTCHA_CONTAINER = "div[class*='captcha']"
|
||||
|
||||
# Registrierungsmethode umschalten
|
||||
EMAIL_TAB = "//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]"
|
||||
PHONE_TAB = "//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]"
|
||||
|
||||
# Login-Felder
|
||||
LOGIN_USERNAME_FIELD = "input[name='username']"
|
||||
LOGIN_PASSWORD_FIELD = "input[name='password']"
|
||||
|
||||
# Erfolgs-Indikatoren für Login/Registrierung (Updated für 2025 Instagram-UI)
|
||||
SUCCESS_INDICATORS = [
|
||||
# Home-Indikatoren
|
||||
"svg[aria-label='Home'], svg[aria-label='Startseite']",
|
||||
"a[href='/']",
|
||||
|
||||
# Navigation-Indikatoren
|
||||
"svg[aria-label='Search'], svg[aria-label='Suchen']",
|
||||
"svg[aria-label='New post'], svg[aria-label='Neuer Beitrag']",
|
||||
"svg[aria-label='Direct'], svg[aria-label='Nachrichten']",
|
||||
|
||||
# Profil-Indikatoren
|
||||
"a[href*='/'] img[alt*='profilbild'], a[href*='/'] img[alt*='profile picture']",
|
||||
|
||||
# Navigation-Container
|
||||
"nav[role='navigation'], div[role='navigation']",
|
||||
|
||||
# Instagram-Logo
|
||||
"a[href='/'] svg[aria-label*='Instagram']",
|
||||
|
||||
# Story-Elemente
|
||||
"div[role='button'] canvas, div[role='button'] img[alt*='story']",
|
||||
|
||||
# Legacy-Support
|
||||
"img[alt='Instagram']",
|
||||
"a[href='/direct/inbox/']",
|
||||
"a[href='/explore/']"
|
||||
]
|
||||
|
||||
# OCR-Fallback-Texte
|
||||
OCR_TEXTS = {
|
||||
"email_field": ["Handynummer oder E-Mail", "Mobile number or email"],
|
||||
"fullname_field": ["Vollständiger Name", "Full name"],
|
||||
"username_field": ["Benutzername", "Username"],
|
||||
"password_field": ["Passwort", "Password"],
|
||||
"birthday_day": ["Tag", "Day"],
|
||||
"birthday_month": ["Monat", "Month"],
|
||||
"birthday_year": ["Jahr", "Year"],
|
||||
"confirmation_code": ["Bestätigungscode", "Confirmation code"],
|
||||
"next_button": ["Weiter", "Next"],
|
||||
"submit_button": ["Registrieren", "Sign up"],
|
||||
"confirm_button": ["Bestätigen", "Confirm"],
|
||||
"cookie_reject": ["Nur erforderliche Cookies erlauben", "Optionale Cookies ablehnen", "Reject"]
|
||||
}
|
||||
|
||||
# Text-Matching-Parameter für Fuzzy-Matching
|
||||
TEXT_MATCH = {
|
||||
# Formularfelder
|
||||
"form_fields": {
|
||||
"email_phone": ["Handynummer oder E-Mail-Adresse", "Mobile Number or Email",
|
||||
"Phone number or email", "E-Mail-Adresse oder Telefonnummer"],
|
||||
"full_name": ["Vollständiger Name", "Full Name", "Name"],
|
||||
"username": ["Benutzername", "Username"],
|
||||
"password": ["Passwort", "Password"],
|
||||
"confirmation_code": ["Bestätigungscode", "Verification code", "Confirmation code", "Code"],
|
||||
},
|
||||
|
||||
# Buttons
|
||||
"buttons": {
|
||||
"submit": ["Weiter", "Registrieren", "Next", "Sign up", "Continue", "Anmelden"],
|
||||
"confirm": ["Bestätigen", "Confirm", "Verify", "Weiter", "Next"],
|
||||
"next": ["Weiter", "Next", "Continue", "Fortfahren"],
|
||||
"reject_cookies": ["Ablehnen", "Nur erforderliche", "Reject", "Not now", "Decline",
|
||||
"Optionale Cookies ablehnen", "Nur notwendige Cookies"],
|
||||
"accept_cookies": ["Akzeptieren", "Accept", "Allow", "Zulassen", "Alle Cookies akzeptieren"],
|
||||
"skip": ["Überspringen", "Skip", "Später", "Later", "Nicht jetzt", "Not now"],
|
||||
},
|
||||
|
||||
# Tabs
|
||||
"tabs": {
|
||||
"email": ["E-Mail", "Email", "E-mail", "Mail"],
|
||||
"phone": ["Telefon", "Telefonnummer", "Phone", "Mobile"],
|
||||
},
|
||||
|
||||
# Erfolgs-Indikatoren
|
||||
"success_indicators": [
|
||||
"Home", "Startseite", "Feed", "Timeline", "Nachrichten",
|
||||
"Profil", "Suche", "Entdecken", "Explore"
|
||||
],
|
||||
|
||||
# Fehler-Indikatoren
|
||||
"error_indicators": [
|
||||
"Fehler", "Error", "Leider", "Ungültig", "Invalid", "Nicht verfügbar",
|
||||
"Fehlgeschlagen", "Problem", "Failed", "Nicht möglich", "Bereits verwendet"
|
||||
],
|
||||
|
||||
# 2FA-Indikatoren
|
||||
"two_fa_indicators": [
|
||||
"Bestätigungscode", "Verifizierungscode", "Sicherheitscode",
|
||||
"2-Faktor", "Verification code", "Two-factor", "2FA"
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def birthday_month_option(cls, month_num: int) -> str:
|
||||
"""
|
||||
Erstellt einen Selektor für eine bestimmte Monatsoption.
|
||||
|
||||
Args:
|
||||
month_num: Monatsnummer (1-12)
|
||||
|
||||
Returns:
|
||||
str: XPath-Selektor für die Monatsoption
|
||||
"""
|
||||
return f"//select[@title='Monat:']/option[@value='{month_num}']"
|
||||
|
||||
@classmethod
|
||||
def birthday_day_option(cls, day_num: int) -> str:
|
||||
"""
|
||||
Erstellt einen Selektor für eine bestimmte Tagesoption.
|
||||
|
||||
Args:
|
||||
day_num: Tagesnummer (1-31)
|
||||
|
||||
Returns:
|
||||
str: XPath-Selektor für die Tagesoption
|
||||
"""
|
||||
return f"//select[@title='Tag:']/option[@value='{day_num}']"
|
||||
|
||||
@classmethod
|
||||
def birthday_year_option(cls, year: int) -> str:
|
||||
"""
|
||||
Erstellt einen Selektor für eine bestimmte Jahresoption.
|
||||
|
||||
Args:
|
||||
year: Jahr (z.B. 1990)
|
||||
|
||||
Returns:
|
||||
str: XPath-Selektor für die Jahresoption
|
||||
"""
|
||||
return f"//select[@title='Jahr:']/option[@value='{year}']"
|
||||
|
||||
@classmethod
|
||||
def get_field_labels(cls, field_type: str) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Bezeichnungen für ein Formularfeld zurück.
|
||||
|
||||
Args:
|
||||
field_type: Typ des Formularfelds (z.B. "email_phone", "full_name")
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Bezeichnungen
|
||||
"""
|
||||
return cls.TEXT_MATCH["form_fields"].get(field_type, [])
|
||||
|
||||
@classmethod
|
||||
def get_button_texts(cls, button_type: str) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für einen Button zurück.
|
||||
|
||||
Args:
|
||||
button_type: Typ des Buttons (z.B. "submit", "confirm")
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Button-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["buttons"].get(button_type, [])
|
||||
|
||||
@classmethod
|
||||
def get_tab_texts(cls, tab_type: str) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für einen Tab zurück.
|
||||
|
||||
Args:
|
||||
tab_type: Typ des Tabs (z.B. "email", "phone")
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Tab-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["tabs"].get(tab_type, [])
|
||||
|
||||
@classmethod
|
||||
def get_success_indicators(cls) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für Erfolgsindikatoren zurück.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Erfolgsindikator-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["success_indicators"]
|
||||
|
||||
@classmethod
|
||||
def get_error_indicators(cls) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für Fehlerindikatoren zurück.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Fehlerindikator-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["error_indicators"]
|
||||
|
||||
@classmethod
|
||||
def get_two_fa_indicators(cls) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für 2FA-Indikatoren zurück.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen 2FA-Indikator-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["two_fa_indicators"]
|
||||
823
social_networks/instagram/instagram_ui_helper.py
Normale Datei
823
social_networks/instagram/instagram_ui_helper.py
Normale Datei
@ -0,0 +1,823 @@
|
||||
# social_networks/instagram/instagram_ui_helper.py
|
||||
|
||||
"""
|
||||
Instagram-UI-Helper - Hilfsmethoden für die Interaktion mit der Instagram-UI
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union, Callable
|
||||
|
||||
from .instagram_selectors import InstagramSelectors
|
||||
from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("instagram_ui_helper")
|
||||
|
||||
class InstagramUIHelper:
|
||||
"""
|
||||
Hilfsmethoden für die Interaktion mit der Instagram-Benutzeroberfläche.
|
||||
Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert den Instagram-UI-Helper.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = InstagramSelectors()
|
||||
|
||||
# Initialisiere TextSimilarity für Fuzzy-Matching
|
||||
self.text_similarity = TextSimilarity(default_threshold=0.7)
|
||||
|
||||
logger.debug("Instagram-UI-Helper initialisiert")
|
||||
|
||||
def _ensure_browser(self) -> bool:
|
||||
"""
|
||||
Stellt sicher, dass die Browser-Referenz verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Browser verfügbar, False sonst
|
||||
"""
|
||||
if self.automation.browser is None:
|
||||
logger.error("Browser-Referenz nicht verfügbar")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def fill_field_fuzzy(self, field_labels: Union[str, List[str]],
|
||||
value: str, fallback_selector: str = None,
|
||||
threshold: float = 0.7, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Füllt ein Formularfeld mit Fuzzy-Text-Matching aus.
|
||||
|
||||
Args:
|
||||
field_labels: Bezeichner oder Liste von Bezeichnern des Feldes
|
||||
value: Einzugebender Wert
|
||||
fallback_selector: CSS-Selektor für Fallback
|
||||
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
||||
timeout: Zeitlimit für die Suche in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Normalisiere field_labels zu einer Liste
|
||||
if isinstance(field_labels, str):
|
||||
field_labels = [field_labels]
|
||||
|
||||
# Versuche, das Feld mit Fuzzy-Matching zu finden
|
||||
element = fuzzy_find_element(
|
||||
self.automation.browser.page,
|
||||
field_labels,
|
||||
selector_type="input",
|
||||
threshold=threshold,
|
||||
wait_time=timeout
|
||||
)
|
||||
|
||||
if element:
|
||||
# Versuche, das Feld zu fokussieren und den Wert einzugeben
|
||||
element.focus()
|
||||
time.sleep(0.1)
|
||||
element.fill("") # Leere das Feld zuerst
|
||||
time.sleep(0.2)
|
||||
|
||||
# Text menschenähnlich eingeben
|
||||
for char in value:
|
||||
element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000)
|
||||
time.sleep(0.01)
|
||||
|
||||
logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}")
|
||||
return True
|
||||
|
||||
# Fuzzy-Matching fehlgeschlagen, versuche über Attribute
|
||||
if fallback_selector:
|
||||
field_success = self.automation.browser.fill_form_field(fallback_selector, value)
|
||||
if field_success:
|
||||
logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}")
|
||||
return True
|
||||
|
||||
# Versuche noch alternative Selektoren basierend auf field_labels
|
||||
for label in field_labels:
|
||||
# Versuche aria-label Attribut
|
||||
aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']"
|
||||
if self.automation.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(aria_selector, value):
|
||||
logger.info(f"Feld über aria-label gefüllt: {label}")
|
||||
return True
|
||||
|
||||
# Versuche placeholder Attribut
|
||||
placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']"
|
||||
if self.automation.browser.is_element_visible(placeholder_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(placeholder_selector, value):
|
||||
logger.info(f"Feld über placeholder gefüllt: {label}")
|
||||
return True
|
||||
|
||||
# Versuche name Attribut
|
||||
name_selector = f"input[name='{label.lower().replace(' ', '')}']"
|
||||
if self.automation.browser.is_element_visible(name_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(name_selector, value):
|
||||
logger.info(f"Feld über name-Attribut gefüllt: {label}")
|
||||
return True
|
||||
|
||||
logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {e}")
|
||||
return False
|
||||
|
||||
def click_button_fuzzy(self, button_texts: Union[str, List[str]],
|
||||
fallback_selector: str = None, threshold: float = 0.7,
|
||||
timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Klickt einen Button mit Fuzzy-Text-Matching.
|
||||
|
||||
Args:
|
||||
button_texts: Text oder Liste von Texten des Buttons
|
||||
fallback_selector: CSS-Selektor für Fallback
|
||||
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
||||
timeout: Zeitlimit für die Suche in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Normalisiere button_texts zu einer Liste
|
||||
if isinstance(button_texts, str):
|
||||
button_texts = [button_texts]
|
||||
|
||||
# Logging der Suche
|
||||
logger.info(f"Suche nach Button mit Texten: {button_texts}")
|
||||
|
||||
if not button_texts or button_texts == [[]]:
|
||||
logger.warning("Leere Button-Text-Liste angegeben!")
|
||||
return False
|
||||
|
||||
# Versuche, den Button mit Fuzzy-Matching zu finden und zu klicken
|
||||
try:
|
||||
# Direktes Matching für alle angegebenen Texte
|
||||
for text in button_texts:
|
||||
if not text or text == "[]":
|
||||
continue
|
||||
|
||||
# Direkte Selektoren versuchen mit robusten Click-Methoden
|
||||
button_selector = f"button:has-text('{text}')"
|
||||
if self.automation.browser.is_element_visible(button_selector, timeout=1000):
|
||||
logger.info(f"Button mit exaktem Text gefunden: {text}")
|
||||
# Verwende robuste Click-Strategien für Instagram Anti-Bot Bypass
|
||||
if hasattr(self.automation.browser, 'robust_click'):
|
||||
return self.automation.browser.robust_click(button_selector)
|
||||
else:
|
||||
return self.automation.browser.click_element(button_selector)
|
||||
|
||||
# Alternativer Selektor für Links und andere klickbare Elemente
|
||||
link_selector = f"a:has-text('{text}')"
|
||||
if self.automation.browser.is_element_visible(link_selector, timeout=1000):
|
||||
logger.info(f"Link mit exaktem Text gefunden: {text}")
|
||||
# Verwende robuste Click-Strategien für Instagram Anti-Bot Bypass
|
||||
if hasattr(self.automation.browser, 'robust_click'):
|
||||
return self.automation.browser.robust_click(link_selector)
|
||||
else:
|
||||
return self.automation.browser.click_element(link_selector)
|
||||
|
||||
# Versuche alle Buttons auf der Seite zu finden und zu prüfen
|
||||
all_buttons = self.automation.browser.page.query_selector_all("button, [role='button'], a.button, input[type='submit']")
|
||||
logger.info(f"Gefundene Button-ähnliche Elemente: {len(all_buttons)}")
|
||||
|
||||
for button in all_buttons:
|
||||
button_text = button.inner_text()
|
||||
if not button_text:
|
||||
continue
|
||||
|
||||
button_text = button_text.strip()
|
||||
# Remove emojis and special characters for logging to avoid encoding errors
|
||||
safe_button_text = ''.join(c for c in button_text if ord(c) < 127)[:50]
|
||||
logger.debug(f"Button-Text: '{safe_button_text}'")
|
||||
|
||||
# Prüfe auf Textähnlichkeit
|
||||
for search_text in button_texts:
|
||||
if not search_text:
|
||||
continue
|
||||
|
||||
if self.text_similarity.is_similar(search_text, button_text, threshold=threshold):
|
||||
logger.info(f"Button mit ähnlichem Text gefunden: '{button_text}' (ähnlich zu '{search_text}')")
|
||||
# Verwende robuste Click-Strategien für Instagram Anti-Bot Bypass
|
||||
try:
|
||||
# Versuche zuerst den Element-Click
|
||||
button.click()
|
||||
return True
|
||||
except Exception as click_error:
|
||||
logger.warning(f"Standard-Click fehlgeschlagen, verwende robuste Methoden: {click_error}")
|
||||
# Fallback zu robusten Click-Strategien
|
||||
if hasattr(self.automation.browser, '_strategy_javascript_click'):
|
||||
# Erstelle Selektor für das Element
|
||||
try:
|
||||
# Versuche JavaScript-Click als Fallback
|
||||
element_id = button.get_attribute('id')
|
||||
element_class = button.get_attribute('class')
|
||||
|
||||
if element_id:
|
||||
selector = f"#{element_id}"
|
||||
elif element_class:
|
||||
selector = f".{element_class.split()[0]}"
|
||||
else:
|
||||
# Verwende Text-basierten Selektor
|
||||
selector = f"button:has-text('{search_text}')"
|
||||
|
||||
return self.automation.browser._strategy_javascript_click(selector)
|
||||
except Exception as js_error:
|
||||
logger.error(f"JavaScript-Click auch fehlgeschlagen: {js_error}")
|
||||
return False
|
||||
|
||||
logger.warning(f"Kein Button mit Text ähnlich zu '{button_texts}' gefunden")
|
||||
except Exception as inner_e:
|
||||
logger.error(f"Innerer Fehler beim Fuzzy-Klicken: {inner_e}")
|
||||
|
||||
# Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector + robuste Methoden
|
||||
if fallback_selector:
|
||||
logger.info(f"Versuche Fallback-Selektor mit robusten Click-Methoden: {fallback_selector}")
|
||||
|
||||
# Zuerst robuste Click-Methoden versuchen
|
||||
if hasattr(self.automation.browser, 'robust_click'):
|
||||
if self.automation.browser.robust_click(fallback_selector):
|
||||
logger.info(f"Button mit Fallback-Selektor und robusten Methoden geklickt: {fallback_selector}")
|
||||
return True
|
||||
|
||||
# Standard-Click als Fallback
|
||||
if self.automation.browser.click_element(fallback_selector):
|
||||
logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}")
|
||||
return True
|
||||
|
||||
# Versuche alternative Methoden
|
||||
|
||||
# 1. Versuche über aria-label
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']"
|
||||
if self.automation.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
# Versuche robuste Click-Methoden
|
||||
if hasattr(self.automation.browser, 'robust_click'):
|
||||
if self.automation.browser.robust_click(aria_selector):
|
||||
logger.info(f"Button über aria-label mit robusten Methoden geklickt: {text}")
|
||||
return True
|
||||
|
||||
# Standard-Click als Fallback
|
||||
if self.automation.browser.click_element(aria_selector):
|
||||
logger.info(f"Button über aria-label geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 2. Versuche über role='button' mit Text
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
xpath_selector = f"//div[@role='button' and contains(., '{text}')]"
|
||||
if self.automation.browser.is_element_visible(xpath_selector, timeout=1000):
|
||||
# Versuche robuste Click-Methoden
|
||||
if hasattr(self.automation.browser, 'robust_click'):
|
||||
if self.automation.browser.robust_click(xpath_selector):
|
||||
logger.info(f"Button über role+text mit robusten Methoden geklickt: {text}")
|
||||
return True
|
||||
|
||||
# Standard-Click als Fallback
|
||||
if self.automation.browser.click_element(xpath_selector):
|
||||
logger.info(f"Button über role+text geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 3. Versuche über Link-Text
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
link_selector = f"//a[contains(text(), '{text}')]"
|
||||
if self.automation.browser.is_element_visible(link_selector, timeout=1000):
|
||||
# Versuche robuste Click-Methoden
|
||||
if hasattr(self.automation.browser, 'robust_click'):
|
||||
if self.automation.browser.robust_click(link_selector):
|
||||
logger.info(f"Link mit passendem Text und robusten Methoden geklickt: {text}")
|
||||
return True
|
||||
|
||||
# Standard-Click als Fallback
|
||||
if self.automation.browser.click_element(link_selector):
|
||||
logger.info(f"Link mit passendem Text geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 4. Als letzten Versuch mit robusten Methoden
|
||||
logger.warning("Kein spezifischer Button gefunden, versuche robuste Click-Methoden auf alle sichtbaren Buttons")
|
||||
|
||||
# Versuche alle Buttons mit robusten Methoden
|
||||
buttons = self.automation.browser.page.query_selector_all("button")
|
||||
if buttons and len(buttons) > 0:
|
||||
for i, button in enumerate(buttons):
|
||||
try:
|
||||
if button.is_visible():
|
||||
button_text = button.inner_text()[:30] # Erste 30 Zeichen für Logging
|
||||
logger.info(f"Versuche robusten Click auf Button {i}: '{button_text}'")
|
||||
|
||||
# Robuste Click-Strategien direkt anwenden
|
||||
success = self._try_robust_click_on_element(button, f"button:nth-child({i+1})")
|
||||
if success:
|
||||
logger.info(f"Button erfolgreich mit robusten Methoden geklickt: '{button_text}'")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Fehler bei Button {i}: {e}")
|
||||
continue
|
||||
|
||||
logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}")
|
||||
return False
|
||||
|
||||
def _try_robust_click_on_element(self, element, selector_fallback: str) -> bool:
|
||||
"""
|
||||
Versucht robuste Click-Strategien auf einem Element.
|
||||
|
||||
Args:
|
||||
element: Das Playwright Element
|
||||
selector_fallback: Fallback CSS-Selektor für das Element
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Strategie 1: Standard Element Click
|
||||
try:
|
||||
element.click(timeout=2000)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Standard Element Click fehlgeschlagen: {e}")
|
||||
|
||||
# Strategie 2: Force Click
|
||||
try:
|
||||
element.click(force=True, timeout=2000)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Force Click fehlgeschlagen: {e}")
|
||||
|
||||
# Strategie 3: JavaScript Click via Element
|
||||
try:
|
||||
result = self.automation.browser.page.evaluate("""
|
||||
(element) => {
|
||||
try {
|
||||
element.click();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
""", element)
|
||||
if result:
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"JavaScript Element Click fehlgeschlagen: {e}")
|
||||
|
||||
# Strategie 4: Event Dispatch auf Element
|
||||
try:
|
||||
result = self.automation.browser.page.evaluate("""
|
||||
(element) => {
|
||||
try {
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
""", element)
|
||||
if result:
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Event Dispatch fehlgeschlagen: {e}")
|
||||
|
||||
# Strategie 5: Verwende Browser's robuste Click-Methoden falls verfügbar
|
||||
if hasattr(self.automation.browser, 'robust_click') and selector_fallback:
|
||||
try:
|
||||
if self.automation.browser.robust_click(selector_fallback):
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Browser robust_click fehlgeschlagen: {e}")
|
||||
|
||||
# Strategie 6: Focus + Enter
|
||||
try:
|
||||
element.focus()
|
||||
self.automation.browser.page.keyboard.press("Enter")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Focus + Enter fehlgeschlagen: {e}")
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Alle robusten Click-Strategien fehlgeschlagen: {e}")
|
||||
return False
|
||||
|
||||
def check_for_error(self, error_selectors: List[str] = None,
|
||||
error_texts: List[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Überprüft, ob Fehlermeldungen angezeigt werden.
|
||||
|
||||
Args:
|
||||
error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen
|
||||
error_texts: Liste mit typischen Fehlertexten
|
||||
|
||||
Returns:
|
||||
Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Standardselektoren verwenden, wenn keine angegeben sind
|
||||
if error_selectors is None:
|
||||
error_selectors = [
|
||||
InstagramSelectors.ERROR_MESSAGE,
|
||||
"p[class*='error']",
|
||||
"div[role='alert']",
|
||||
"span[class*='error']",
|
||||
".error-message"
|
||||
]
|
||||
|
||||
# Standardfehlertexte verwenden, wenn keine angegeben sind
|
||||
if error_texts is None:
|
||||
error_texts = InstagramSelectors.get_error_indicators()
|
||||
|
||||
# 1. Nach Fehlerselektoren suchen
|
||||
for selector in error_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
error_text = element.text_content()
|
||||
if error_text and len(error_text.strip()) > 0:
|
||||
logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}")
|
||||
return error_text.strip()
|
||||
|
||||
# 2. Alle Texte auf der Seite durchsuchen
|
||||
page_content = self.automation.browser.page.content()
|
||||
|
||||
for error_text in error_texts:
|
||||
if error_text.lower() in page_content.lower():
|
||||
# Versuche, den genauen Fehlertext zu extrahieren
|
||||
matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower())
|
||||
if matches:
|
||||
full_error = matches[0].strip()
|
||||
logger.info(f"Fehlermeldung gefunden (Text): {full_error}")
|
||||
return full_error
|
||||
else:
|
||||
logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}")
|
||||
return error_text
|
||||
|
||||
# 3. Nach weiteren Fehlerelementen suchen
|
||||
elements = self.automation.browser.page.query_selector_all("p, div, span")
|
||||
|
||||
for element in elements:
|
||||
element_text = element.inner_text()
|
||||
if not element_text:
|
||||
continue
|
||||
|
||||
element_text = element_text.strip()
|
||||
|
||||
# Prüfe Textähnlichkeit mit Fehlertexten
|
||||
for error_text in error_texts:
|
||||
if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \
|
||||
self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7):
|
||||
logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}")
|
||||
return element_text
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}")
|
||||
return None
|
||||
|
||||
def check_for_captcha(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob ein Captcha angezeigt wird.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Captcha erkannt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Selektoren für Captcha-Erkennung
|
||||
captcha_selectors = [
|
||||
InstagramSelectors.CAPTCHA_CONTAINER,
|
||||
"div[class*='captcha']",
|
||||
"iframe[src*='captcha']",
|
||||
"iframe[title*='captcha']",
|
||||
"iframe[title*='reCAPTCHA']"
|
||||
]
|
||||
|
||||
# Captcha-Texte für textbasierte Erkennung
|
||||
captcha_texts = [
|
||||
"captcha", "recaptcha", "sicherheitsüberprüfung", "security check",
|
||||
"i'm not a robot", "ich bin kein roboter", "verify you're human",
|
||||
"bestätige, dass du ein mensch bist"
|
||||
]
|
||||
|
||||
# Nach Selektoren suchen
|
||||
for selector in captcha_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.warning(f"Captcha erkannt (Selektor): {selector}")
|
||||
return True
|
||||
|
||||
# Nach Texten suchen
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for text in captcha_texts:
|
||||
if text in page_content:
|
||||
logger.warning(f"Captcha erkannt (Text): {text}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Captcha-Erkennung: {e}")
|
||||
return False
|
||||
|
||||
def check_for_next_steps(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob Elemente für weitere Einrichtungsschritte angezeigt werden.
|
||||
|
||||
Returns:
|
||||
bool: True wenn weitere Schritte erkannt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Texte, die auf weitere Einrichtungsschritte hinweisen
|
||||
next_step_texts = [
|
||||
"profilbild hinzufügen", "add profile photo",
|
||||
"freunde finden", "find friends",
|
||||
"konten folgen", "follow accounts",
|
||||
"profilbild hochladen", "upload profile picture",
|
||||
"profil einrichten", "set up your profile",
|
||||
"willkommen bei instagram", "welcome to instagram",
|
||||
"personalisieren", "personalize",
|
||||
"für dich empfohlen", "recommended for you"
|
||||
]
|
||||
|
||||
# Seiteninhalt durchsuchen
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for text in next_step_texts:
|
||||
if text in page_content:
|
||||
logger.info(f"Weitere Einrichtungsschritte erkannt (Text): {text}")
|
||||
return True
|
||||
|
||||
# Nach typischen Buttons für weitere Schritte suchen
|
||||
next_step_button_texts = [
|
||||
"überspringen", "skip",
|
||||
"später", "later",
|
||||
"folgen", "follow",
|
||||
"hinzufügen", "add",
|
||||
"hochladen", "upload",
|
||||
"weiter", "next",
|
||||
"fertig", "done"
|
||||
]
|
||||
|
||||
# Textähnlichkeit mit Button-Texten prüfen
|
||||
elements = self.automation.browser.page.query_selector_all("button, [role='button'], a")
|
||||
|
||||
for element in elements:
|
||||
element_text = element.inner_text()
|
||||
if not element_text:
|
||||
continue
|
||||
|
||||
element_text = element_text.strip().lower()
|
||||
|
||||
for button_text in next_step_button_texts:
|
||||
if button_text in element_text or \
|
||||
self.text_similarity.is_similar(button_text, element_text, threshold=0.8):
|
||||
logger.info(f"Button für weitere Einrichtungsschritte erkannt: {element_text}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Erkennung weiterer Einrichtungsschritte: {e}")
|
||||
return False
|
||||
|
||||
def find_element_by_text(self, text: Union[str, List[str]],
|
||||
element_type: str = "any", threshold: float = 0.7) -> Optional[Any]:
|
||||
"""
|
||||
Findet ein Element basierend auf Textähnlichkeit.
|
||||
|
||||
Args:
|
||||
text: Zu suchender Text oder Liste von Texten
|
||||
element_type: Art des Elements ("button", "link", "input", "any")
|
||||
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
||||
|
||||
Returns:
|
||||
Optional[Any]: Das gefundene Element oder None, wenn keines gefunden wurde
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Normalisiere text zu einer Liste
|
||||
if isinstance(text, str):
|
||||
text = [text]
|
||||
|
||||
# Verwende die Fuzzy-Find-Funktion
|
||||
element = fuzzy_find_element(
|
||||
self.automation.browser.page,
|
||||
text,
|
||||
selector_type=element_type,
|
||||
threshold=threshold,
|
||||
wait_time=5000
|
||||
)
|
||||
|
||||
if element:
|
||||
logger.info(f"Element mit Text '{text}' gefunden")
|
||||
return element
|
||||
|
||||
logger.warning(f"Kein Element mit Text '{text}' gefunden")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Suchen nach Element mit Text '{text}': {e}")
|
||||
return None
|
||||
|
||||
def is_text_on_page(self, text: Union[str, List[str]], threshold: float = 0.7) -> bool:
|
||||
"""
|
||||
Überprüft, ob ein Text auf der Seite vorhanden ist.
|
||||
|
||||
Args:
|
||||
text: Zu suchender Text oder Liste von Texten
|
||||
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
||||
|
||||
Returns:
|
||||
bool: True wenn der Text gefunden wurde, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Normalisiere text zu einer Liste
|
||||
if isinstance(text, str):
|
||||
text = [text]
|
||||
|
||||
# Hole den gesamten Seiteninhalt
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
# Direkte Textsuche
|
||||
for t in text:
|
||||
if t.lower() in page_content:
|
||||
logger.info(f"Text '{t}' auf der Seite gefunden (exakte Übereinstimmung)")
|
||||
return True
|
||||
|
||||
# Suche in allen sichtbaren Textelementen mit Ähnlichkeitsvergleich
|
||||
elements = self.automation.browser.page.query_selector_all("p, h1, h2, h3, h4, h5, h6, span, div, button, a, label")
|
||||
|
||||
for element in elements:
|
||||
element_text = element.inner_text()
|
||||
if not element_text:
|
||||
continue
|
||||
|
||||
element_text = element_text.strip().lower()
|
||||
|
||||
for t in text:
|
||||
if self.text_similarity.is_similar(t.lower(), element_text, threshold=threshold) or \
|
||||
self.text_similarity.contains_similar_text(element_text, [t.lower()], threshold=threshold):
|
||||
logger.info(f"Text '{t}' auf der Seite gefunden (Ähnlichkeit)")
|
||||
return True
|
||||
|
||||
logger.info(f"Text '{text}' nicht auf der Seite gefunden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Suchen nach Text auf der Seite: {e}")
|
||||
return False
|
||||
|
||||
def wait_for_element(self, selectors: Union[str, List[str]],
|
||||
timeout: int = 10000, check_interval: int = 500) -> Optional[Any]:
|
||||
"""
|
||||
Wartet auf das Erscheinen eines Elements.
|
||||
|
||||
Args:
|
||||
selectors: CSS-Selektor oder Liste von Selektoren
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
||||
|
||||
Returns:
|
||||
Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Normalisiere selectors zu einer Liste
|
||||
if isinstance(selectors, str):
|
||||
selectors = [selectors]
|
||||
|
||||
start_time = time.time()
|
||||
end_time = start_time + (timeout / 1000)
|
||||
|
||||
while time.time() < end_time:
|
||||
for selector in selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=check_interval)
|
||||
if element:
|
||||
logger.info(f"Element mit Selektor '{selector}' gefunden")
|
||||
return element
|
||||
|
||||
# Kurze Pause vor der nächsten Prüfung
|
||||
time.sleep(check_interval / 1000)
|
||||
|
||||
logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Warten auf Element: {e}")
|
||||
return None
|
||||
|
||||
def is_page_loading(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob die Seite noch lädt.
|
||||
|
||||
Returns:
|
||||
bool: True wenn die Seite lädt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Lade-Indikatoren auf Instagram
|
||||
loading_selectors = [
|
||||
"div[class*='spinner']",
|
||||
"div[class*='loading']",
|
||||
"div[role='progressbar']",
|
||||
"svg[aria-label='Loading...']",
|
||||
"svg[aria-label='Wird geladen...']"
|
||||
]
|
||||
|
||||
for selector in loading_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=500):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung des Ladestatus: {e}")
|
||||
return False
|
||||
|
||||
def wait_for_page_load(self, timeout: int = 30000, check_interval: int = 500) -> bool:
|
||||
"""
|
||||
Wartet, bis die Seite vollständig geladen ist.
|
||||
|
||||
Args:
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn die Seite geladen wurde, False bei Zeitüberschreitung
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Warten auf Netzwerk-Idle
|
||||
self.automation.browser.page.wait_for_load_state("networkidle", timeout=timeout)
|
||||
|
||||
# Zusätzlich auf das Verschwinden der Ladeindikatoren warten
|
||||
start_time = time.time()
|
||||
end_time = start_time + (timeout / 1000)
|
||||
|
||||
while time.time() < end_time:
|
||||
if not self.is_page_loading():
|
||||
# Noch eine kurze Pause für Animationen
|
||||
time.sleep(0.5)
|
||||
logger.info("Seite vollständig geladen")
|
||||
return True
|
||||
|
||||
# Kurze Pause vor der nächsten Prüfung
|
||||
time.sleep(check_interval / 1000)
|
||||
|
||||
logger.warning("Zeitüberschreitung beim Warten auf das Laden der Seite")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Warten auf das Laden der Seite: {e}")
|
||||
return False
|
||||
547
social_networks/instagram/instagram_utils.py
Normale Datei
547
social_networks/instagram/instagram_utils.py
Normale Datei
@ -0,0 +1,547 @@
|
||||
# social_networks/instagram/instagram_utils.py
|
||||
|
||||
"""
|
||||
Instagram-Utils - Hilfsfunktionen für die Instagram-Automatisierung
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union
|
||||
|
||||
from .instagram_selectors import InstagramSelectors
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("instagram_utils")
|
||||
|
||||
class InstagramUtils:
|
||||
"""
|
||||
Hilfsfunktionen für die Instagram-Automatisierung.
|
||||
Enthält allgemeine Hilfsmethoden und kleinere Funktionen.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die Instagram-Utils.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = InstagramSelectors()
|
||||
|
||||
logger.debug("Instagram-Utils initialisiert")
|
||||
|
||||
def _ensure_browser(self) -> bool:
|
||||
"""
|
||||
Stellt sicher, dass die Browser-Referenz verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Browser verfügbar, False sonst
|
||||
"""
|
||||
if self.automation.browser is None:
|
||||
logger.error("Browser-Referenz nicht verfügbar")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Cookie-Dialog-Erkennung
|
||||
if self.automation.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000):
|
||||
logger.info("Cookie-Banner erkannt")
|
||||
|
||||
# Ablehnen-Button suchen und klicken
|
||||
reject_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
InstagramSelectors.get_button_texts("reject_cookies"),
|
||||
InstagramSelectors.COOKIE_REJECT_BUTTON
|
||||
)
|
||||
|
||||
if reject_success:
|
||||
logger.info("Cookie-Banner erfolgreich abgelehnt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
else:
|
||||
logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren")
|
||||
|
||||
# Akzeptieren-Button als Fallback
|
||||
accept_success = self.automation.browser.click_element(InstagramSelectors.COOKIE_ACCEPT_BUTTON)
|
||||
|
||||
if accept_success:
|
||||
logger.info("Cookie-Banner erfolgreich akzeptiert")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
else:
|
||||
logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren")
|
||||
return False
|
||||
else:
|
||||
logger.debug("Kein Cookie-Banner erkannt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Behandeln des Cookie-Banners: {e}")
|
||||
return False
|
||||
|
||||
def is_logged_in(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob der Benutzer bei Instagram angemeldet ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn angemeldet, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Login-Status über verschiedene Indikatoren prüfen
|
||||
|
||||
# 1. URL prüfen
|
||||
current_url = self.automation.browser.page.url
|
||||
if "/accounts/login" in current_url:
|
||||
logger.debug("Nicht angemeldet (Auf Login-Seite)")
|
||||
return False
|
||||
|
||||
# 2. Erfolgs-Indikatoren prüfen
|
||||
success_indicators = InstagramSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self.automation.browser.is_element_visible(indicator, timeout=1000):
|
||||
logger.debug(f"Angemeldet (Indikator: {indicator})")
|
||||
return True
|
||||
|
||||
# 3. Profil-Icon prüfen
|
||||
profile_selectors = [
|
||||
"img[data-testid='user-avatar']",
|
||||
"span[role='link'][aria-label*='profil']",
|
||||
"span[role='link'][aria-label*='profile']"
|
||||
]
|
||||
|
||||
for selector in profile_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
logger.debug(f"Angemeldet (Profil-Icon sichtbar: {selector})")
|
||||
return True
|
||||
|
||||
# 4. Login-Formular prüfen (umgekehrte Logik)
|
||||
login_selectors = [
|
||||
InstagramSelectors.LOGIN_USERNAME_FIELD,
|
||||
InstagramSelectors.LOGIN_PASSWORD_FIELD
|
||||
]
|
||||
|
||||
for selector in login_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
logger.debug(f"Nicht angemeldet (Login-Formular sichtbar: {selector})")
|
||||
return False
|
||||
|
||||
# Wenn wir hier ankommen, können wir den Status nicht eindeutig bestimmen
|
||||
# Wir gehen davon aus, dass wir nicht angemeldet sind
|
||||
logger.debug("Login-Status konnte nicht eindeutig bestimmt werden, nehme 'nicht angemeldet' an")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung des Login-Status: {e}")
|
||||
return False
|
||||
|
||||
def check_for_captcha(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob ein Captcha angezeigt wird.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Captcha erkannt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Delegiere an UI-Helper
|
||||
return self.automation.ui_helper.check_for_captcha()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Captcha-Erkennung: {e}")
|
||||
return False
|
||||
|
||||
def handle_rate_limiting(self, rotate_proxy: bool = True) -> bool:
|
||||
"""
|
||||
Behandelt eine Rate-Limiting-Situation.
|
||||
|
||||
Args:
|
||||
rotate_proxy: Ob der Proxy rotiert werden soll
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich behandelt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.warning("Rate-Limiting erkannt, warte und versuche es erneut")
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("rate_limit_detected")
|
||||
|
||||
# Proxy rotieren, falls gewünscht
|
||||
if rotate_proxy and self.automation.use_proxy:
|
||||
success = self.automation._rotate_proxy()
|
||||
if not success:
|
||||
logger.warning("Konnte Proxy nicht rotieren")
|
||||
|
||||
# Längere Wartezeit
|
||||
wait_time = random.uniform(120, 300) # 2-5 Minuten
|
||||
logger.info(f"Warte {wait_time:.1f} Sekunden vor dem nächsten Versuch")
|
||||
time.sleep(wait_time)
|
||||
|
||||
# Seite neuladen
|
||||
self.automation.browser.page.reload()
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Prüfen, ob Rate-Limiting noch aktiv ist
|
||||
rate_limit_texts = [
|
||||
"bitte warte einige minuten",
|
||||
"please wait a few minutes",
|
||||
"try again later",
|
||||
"versuche es später erneut",
|
||||
"zu viele anfragen",
|
||||
"too many requests"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
still_rate_limited = False
|
||||
for text in rate_limit_texts:
|
||||
if text in page_content:
|
||||
still_rate_limited = True
|
||||
break
|
||||
|
||||
if still_rate_limited:
|
||||
logger.warning("Immer noch Rate-Limited nach dem Warten")
|
||||
return False
|
||||
else:
|
||||
logger.info("Rate-Limiting scheint aufgehoben zu sein")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Behandlung des Rate-Limitings: {e}")
|
||||
return False
|
||||
|
||||
def scroll_page(self, direction: str = "down", amount: int = 3) -> bool:
|
||||
"""
|
||||
Scrollt die Seite.
|
||||
|
||||
Args:
|
||||
direction: "up" oder "down"
|
||||
amount: Scroll-Menge (1=wenig, 5=viel)
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Scroll-Faktor je nach Richtung
|
||||
scroll_factor = 1 if direction == "down" else -1
|
||||
|
||||
# Scroll-Menge in Pixeln
|
||||
pixel_amount = amount * 300 * scroll_factor
|
||||
|
||||
# Scroll ausführen
|
||||
self.automation.browser.page.evaluate(f"window.scrollBy(0, {pixel_amount})")
|
||||
|
||||
# Menschliche Verzögerung
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
|
||||
logger.debug(f"Seite ge{direction}scrollt um {abs(pixel_amount)} Pixel")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Scrollen der Seite: {e}")
|
||||
return False
|
||||
|
||||
def extract_username_from_url(self, url: str) -> Optional[str]:
|
||||
"""
|
||||
Extrahiert den Benutzernamen aus einer Instagram-URL.
|
||||
|
||||
Args:
|
||||
url: Die Instagram-URL
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der extrahierte Benutzername oder None
|
||||
"""
|
||||
try:
|
||||
# Muster für Profil-URLs
|
||||
patterns = [
|
||||
r'instagram\.com/([a-zA-Z0-9._]+)/?(?:$|\?|#)',
|
||||
r'instagram\.com/([a-zA-Z0-9._]+)/(?:saved|tagged|reels)/?',
|
||||
r'instagram\.com/p/[^/]+/(?:by|from)/([a-zA-Z0-9._]+)/?'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
username = match.group(1)
|
||||
# Einige Ausnahmen filtern
|
||||
if username not in ["explore", "accounts", "p", "reel", "stories", "direct"]:
|
||||
return username
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren des Benutzernamens aus der URL: {e}")
|
||||
return None
|
||||
|
||||
def get_current_username(self) -> Optional[str]:
|
||||
"""
|
||||
Versucht, den Benutzernamen des aktuell angemeldeten Kontos zu ermitteln.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der Benutzername oder None, wenn nicht gefunden
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Verschiedene Methoden zur Erkennung des Benutzernamens
|
||||
|
||||
# 1. Benutzername aus URL des Profils
|
||||
profile_link_selectors = [
|
||||
"a[href*='/profile/']",
|
||||
"a[href*='instagram.com/'][role='link']",
|
||||
"a[href*='instagram.com/']:not([href*='/explore/']):not([href*='/direct/'])"
|
||||
]
|
||||
|
||||
for selector in profile_link_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
href = element.get_attribute("href")
|
||||
if href:
|
||||
username = self.extract_username_from_url(href)
|
||||
if username:
|
||||
logger.info(f"Benutzername aus Profil-Link ermittelt: {username}")
|
||||
return username
|
||||
|
||||
# 2. Benutzername aus aria-label Attributen
|
||||
profile_aria_selectors = [
|
||||
"img[data-testid='user-avatar']",
|
||||
"span[role='link'][aria-label*='profil']",
|
||||
"span[role='link'][aria-label*='profile']"
|
||||
]
|
||||
|
||||
for selector in profile_aria_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
aria_label = element.get_attribute("aria-label")
|
||||
if aria_label:
|
||||
# Verschiedene Formate: "profil von username", "username's profile"
|
||||
for pattern in [r'profil von ([a-zA-Z0-9._]+)', r"([a-zA-Z0-9._]+)'s profile"]:
|
||||
match = re.search(pattern, aria_label, re.IGNORECASE)
|
||||
if match:
|
||||
username = match.group(1)
|
||||
logger.info(f"Benutzername aus aria-label ermittelt: {username}")
|
||||
return username
|
||||
|
||||
# 3. Fallback: Zur Profilseite navigieren und aus URL extrahieren
|
||||
# Dies sollte nur gemacht werden, wenn ein wichtiger Grund dafür besteht
|
||||
# und kein anderer Weg verfügbar ist, den Benutzernamen zu ermitteln
|
||||
|
||||
logger.warning("Konnte Benutzernamen nicht ermitteln")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Ermittlung des Benutzernamens: {e}")
|
||||
return None
|
||||
|
||||
def is_account_private(self) -> Optional[bool]:
|
||||
"""
|
||||
Überprüft, ob das aktuelle Konto privat ist.
|
||||
|
||||
Returns:
|
||||
Optional[bool]: True wenn privat, False wenn öffentlich, None wenn nicht erkennbar
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Texte, die auf ein privates Konto hinweisen
|
||||
private_indicators = [
|
||||
"this account is private",
|
||||
"dieses konto ist privat",
|
||||
"must be following",
|
||||
"musst diesem konto folgen"
|
||||
]
|
||||
|
||||
# Privat-Icon suchen
|
||||
private_icon_selectors = [
|
||||
"svg[aria-label='Private Account']",
|
||||
"svg[aria-label='Privates Konto']"
|
||||
]
|
||||
|
||||
for selector in private_icon_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
logger.info("Konto ist privat (Icon gefunden)")
|
||||
return True
|
||||
|
||||
# Texte prüfen
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for indicator in private_indicators:
|
||||
if indicator in page_content:
|
||||
logger.info(f"Konto ist privat (Text gefunden: {indicator})")
|
||||
return True
|
||||
|
||||
# Wenn keine Privat-Indikatoren gefunden wurden, gehen wir davon aus, dass das Konto öffentlich ist
|
||||
# Dies ist jedoch nicht 100% sicher
|
||||
logger.debug("Konto scheint öffentlich zu sein")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung des Privat-Status: {e}")
|
||||
return None
|
||||
|
||||
def count_followers(self) -> Optional[int]:
|
||||
"""
|
||||
Versucht, die Anzahl der Follower des aktuellen Kontos zu ermitteln.
|
||||
|
||||
Returns:
|
||||
Optional[int]: Die Anzahl der Follower oder None, wenn nicht ermittelbar
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Selektoren für Follower-Zähler
|
||||
follower_selectors = [
|
||||
"a[href*='/followers/'] span",
|
||||
"a[href*='/followers/']",
|
||||
"ul li:nth-child(2) span" # Typisches Layout auf Profilseiten
|
||||
]
|
||||
|
||||
for selector in follower_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
follower_text = element.inner_text()
|
||||
if follower_text:
|
||||
# Zahlen extrahieren (möglicherweise mit K, M, B für Tausend, Million, Milliarde)
|
||||
follower_text = follower_text.strip().replace(',', '').replace('.', '')
|
||||
|
||||
# Direkter Zahlenwert
|
||||
if follower_text.isdigit():
|
||||
return int(follower_text)
|
||||
|
||||
# Wert mit K (Tausend)
|
||||
if 'k' in follower_text.lower():
|
||||
value = float(follower_text.lower().replace('k', '')) * 1000
|
||||
return int(value)
|
||||
|
||||
# Wert mit M (Million)
|
||||
if 'm' in follower_text.lower():
|
||||
value = float(follower_text.lower().replace('m', '')) * 1000000
|
||||
return int(value)
|
||||
|
||||
# Wert mit B (Milliarde)
|
||||
if 'b' in follower_text.lower():
|
||||
value = float(follower_text.lower().replace('b', '')) * 1000000000
|
||||
return int(value)
|
||||
|
||||
logger.warning("Konnte Follower-Anzahl nicht ermitteln")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Ermittlung der Follower-Anzahl: {e}")
|
||||
return None
|
||||
|
||||
def get_account_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Sammelt verfügbare Statistiken zum aktuellen Konto.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Konto-Statistiken
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return {}
|
||||
|
||||
try:
|
||||
stats = {}
|
||||
|
||||
# Benutzername ermitteln
|
||||
username = self.get_current_username()
|
||||
if username:
|
||||
stats["username"] = username
|
||||
|
||||
# Privat-Status prüfen
|
||||
is_private = self.is_account_private()
|
||||
if is_private is not None:
|
||||
stats["is_private"] = is_private
|
||||
|
||||
# Follower-Anzahl
|
||||
followers = self.count_followers()
|
||||
if followers is not None:
|
||||
stats["followers"] = followers
|
||||
|
||||
# Optional: Weitere Statistiken sammeln
|
||||
# ...
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Sammeln der Konto-Statistiken: {e}")
|
||||
return {}
|
||||
|
||||
def check_login_banned(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Überprüft, ob der Login gesperrt wurde.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (Gesperrt, Fehlermeldung falls vorhanden)
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False, None
|
||||
|
||||
try:
|
||||
# Texte, die auf eine Sperrung hinweisen
|
||||
ban_indicators = [
|
||||
"your account has been disabled",
|
||||
"dein konto wurde deaktiviert",
|
||||
"your account has been locked",
|
||||
"dein konto wurde gesperrt",
|
||||
"suspicious activity",
|
||||
"verdächtige aktivität",
|
||||
"we've detected suspicious activity",
|
||||
"wir haben verdächtige aktivitäten festgestellt",
|
||||
"your account has been temporarily locked",
|
||||
"dein konto wurde vorübergehend gesperrt"
|
||||
]
|
||||
|
||||
# Seiteninhalt durchsuchen
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for indicator in ban_indicators:
|
||||
if indicator in page_content:
|
||||
# Vollständigen Text der Fehlermeldung extrahieren
|
||||
error_element = self.automation.browser.wait_for_selector("p[data-testid='login-error-message'], div[role='alert'], p[class*='error']", timeout=2000)
|
||||
error_message = None
|
||||
|
||||
if error_element:
|
||||
error_message = error_element.inner_text().strip()
|
||||
|
||||
if not error_message:
|
||||
error_message = f"Konto gesperrt oder eingeschränkt (Indikator: {indicator})"
|
||||
|
||||
logger.warning(f"Konto-Sperrung erkannt: {error_message}")
|
||||
return True, error_message
|
||||
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung auf Konto-Sperrung: {e}")
|
||||
return False, None
|
||||
492
social_networks/instagram/instagram_verification.py
Normale Datei
492
social_networks/instagram/instagram_verification.py
Normale Datei
@ -0,0 +1,492 @@
|
||||
# social_networks/instagram/instagram_verification.py
|
||||
|
||||
"""
|
||||
Instagram-Verifizierung - Klasse für die Verifizierungsfunktionalität bei Instagram
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .instagram_selectors import InstagramSelectors
|
||||
from .instagram_workflow import InstagramWorkflow
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("instagram_verification")
|
||||
|
||||
class InstagramVerification:
|
||||
"""
|
||||
Klasse für die Verifizierung von Instagram-Konten.
|
||||
Enthält alle Methoden für den Verifizierungsprozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die Instagram-Verifizierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = InstagramSelectors()
|
||||
self.workflow = InstagramWorkflow.get_verification_workflow()
|
||||
|
||||
logger.debug("Instagram-Verifizierung initialisiert")
|
||||
|
||||
def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Verifizierungsprozess für ein Instagram-Konto durch.
|
||||
|
||||
Args:
|
||||
verification_code: Der Bestätigungscode
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung mit Status
|
||||
"""
|
||||
# Browser wird direkt von automation verwendet
|
||||
|
||||
# Validiere den Verifizierungscode
|
||||
if not self._validate_verification_code(verification_code):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültiger Verifizierungscode",
|
||||
"stage": "code_validation"
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. Überprüfen, ob wir auf der Verifizierungsseite sind
|
||||
if not self._is_on_verification_page():
|
||||
# Versuche, zur Verifizierungsseite zu navigieren, falls möglich
|
||||
# Direktnavigation ist jedoch normalerweise nicht möglich
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Nicht auf der Verifizierungsseite",
|
||||
"stage": "page_check"
|
||||
}
|
||||
|
||||
# 2. Verifizierungscode eingeben und absenden
|
||||
if not self.enter_and_submit_verification_code(verification_code):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Eingeben oder Absenden des Verifizierungscodes",
|
||||
"stage": "code_entry"
|
||||
}
|
||||
|
||||
# 3. Überprüfen, ob die Verifizierung erfolgreich war
|
||||
success, error_message = self._check_verification_success()
|
||||
|
||||
if not success:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Verifizierung fehlgeschlagen: {error_message or 'Unbekannter Fehler'}",
|
||||
"stage": "verification_check"
|
||||
}
|
||||
|
||||
# 4. Zusätzliche Dialoge behandeln
|
||||
self._handle_post_verification_dialogs()
|
||||
|
||||
# Verifizierung erfolgreich
|
||||
logger.info("Instagram-Verifizierung erfolgreich abgeschlossen")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "completed"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der Instagram-Verifizierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception"
|
||||
}
|
||||
|
||||
def _validate_verification_code(self, verification_code: str) -> bool:
|
||||
"""
|
||||
Validiert den Verifizierungscode.
|
||||
|
||||
Args:
|
||||
verification_code: Der zu validierende Code
|
||||
|
||||
Returns:
|
||||
bool: True wenn der Code gültig ist, False sonst
|
||||
"""
|
||||
# Leerer Code
|
||||
if not verification_code:
|
||||
logger.error("Verifizierungscode ist leer")
|
||||
return False
|
||||
|
||||
# Code-Format prüfen (normalerweise 6-stellige Zahl)
|
||||
if not re.match(r"^\d{6}$", verification_code):
|
||||
logger.warning(f"Verifizierungscode hat unerwartetes Format: {verification_code}")
|
||||
# Wir geben trotzdem True zurück, da einige Codes andere Formate haben könnten
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def _is_on_verification_page(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob wir auf der Verifizierungsseite sind.
|
||||
|
||||
Returns:
|
||||
bool: True wenn auf der Verifizierungsseite, False sonst
|
||||
"""
|
||||
try:
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_page_check")
|
||||
|
||||
# Nach Verifizierungsfeld suchen
|
||||
verification_selectors = [
|
||||
InstagramSelectors.CONFIRMATION_CODE_FIELD,
|
||||
InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD,
|
||||
"input[name='confirmationCode']",
|
||||
"input[aria-label='Bestätigungscode']",
|
||||
"input[aria-label='Confirmation code']",
|
||||
"input[aria-label='Verification code']"
|
||||
]
|
||||
|
||||
for selector in verification_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
logger.info("Auf Verifizierungsseite")
|
||||
return True
|
||||
|
||||
# Textbasierte Erkennung
|
||||
verification_texts = [
|
||||
"Bestätigungscode",
|
||||
"Confirmation code",
|
||||
"Verification code",
|
||||
"Sicherheitscode",
|
||||
"Security code",
|
||||
"Enter the code we sent to"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for text in verification_texts:
|
||||
if text.lower() in page_content:
|
||||
logger.info(f"Auf Verifizierungsseite (erkannt durch Text: {text})")
|
||||
return True
|
||||
|
||||
logger.warning("Nicht auf der Verifizierungsseite")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen der Verifizierungsseite: {e}")
|
||||
return False
|
||||
|
||||
def enter_and_submit_verification_code(self, verification_code: str) -> bool:
|
||||
"""
|
||||
Gibt den Verifizierungscode ein und sendet ihn ab.
|
||||
|
||||
Args:
|
||||
verification_code: Der einzugebende Verifizierungscode
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Versuche Verifizierungscode einzugeben: {verification_code}")
|
||||
self.automation._emit_customer_log("🔐 Bestätigungscode wird eingegeben...")
|
||||
|
||||
# Mögliche Selektoren für das Verifizierungscode-Feld
|
||||
code_field_selectors = [
|
||||
InstagramSelectors.CONFIRMATION_CODE_FIELD,
|
||||
InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD,
|
||||
"input[name='email_confirmation_code']",
|
||||
"input[name='confirmationCode']",
|
||||
"input[aria-label='Bestätigungscode']",
|
||||
"input[aria-label='Confirmation code']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder='Bestätigungscode']"
|
||||
]
|
||||
|
||||
# Versuche, das Feld zu finden und auszufüllen
|
||||
code_field_found = False
|
||||
|
||||
for selector in code_field_selectors:
|
||||
logger.debug(f"Versuche Selektor: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Codefeld gefunden mit Selektor: {selector}")
|
||||
if self.automation.browser.fill_form_field(selector, verification_code):
|
||||
code_field_found = True
|
||||
logger.info(f"Verifizierungscode eingegeben: {verification_code}")
|
||||
break
|
||||
|
||||
# Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen
|
||||
if not code_field_found:
|
||||
logger.info("Versuche Fuzzy-Matching für Codefeld")
|
||||
code_field_found = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Bestätigungscode", "Confirmation code", "Verification code", "Code"],
|
||||
verification_code
|
||||
)
|
||||
|
||||
if not code_field_found:
|
||||
logger.error("Konnte Verifizierungscode-Feld nicht finden oder ausfüllen")
|
||||
|
||||
# Erstelle einen Screenshot zum Debuggen
|
||||
self.automation._take_screenshot("code_field_not_found")
|
||||
return False
|
||||
|
||||
# Menschliche Verzögerung vor dem Absenden
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_code_entered")
|
||||
|
||||
# Absenden-Button finden und klicken
|
||||
submit_button_selectors = [
|
||||
"button[type='submit']",
|
||||
InstagramSelectors.CONFIRMATION_BUTTON,
|
||||
"//button[contains(text(), 'Bestätigen')]",
|
||||
"//button[contains(text(), 'Confirm')]",
|
||||
"//button[contains(text(), 'Verify')]",
|
||||
"//button[contains(text(), 'Next')]",
|
||||
"//button[contains(text(), 'Weiter')]",
|
||||
"button" # Falls alle anderen fehlschlagen, probiere jeden Button
|
||||
]
|
||||
|
||||
submit_button_found = False
|
||||
|
||||
logger.info("Suche nach Submit-Button")
|
||||
for selector in submit_button_selectors:
|
||||
logger.debug(f"Versuche Submit-Button-Selektor: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Submit-Button gefunden mit Selektor: {selector}")
|
||||
if self.automation.browser.click_element(selector):
|
||||
submit_button_found = True
|
||||
logger.info("Verifizierungscode-Formular abgesendet")
|
||||
break
|
||||
|
||||
# Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen
|
||||
if not submit_button_found:
|
||||
logger.info("Versuche Fuzzy-Matching für Submit-Button")
|
||||
weiter_buttons = ["Weiter", "Next", "Continue", "Bestätigen", "Confirm", "Submit", "Verify", "Senden"]
|
||||
submit_button_found = self.automation.ui_helper.click_button_fuzzy(
|
||||
weiter_buttons
|
||||
)
|
||||
|
||||
if not submit_button_found:
|
||||
# Erstelle einen Screenshot zum Debuggen
|
||||
self.automation._take_screenshot("submit_button_not_found")
|
||||
|
||||
# Versuche es mit Enter-Taste als letzten Ausweg
|
||||
logger.info("Konnte Submit-Button nicht finden, versuche Enter-Taste")
|
||||
self.automation.browser.page.keyboard.press("Enter")
|
||||
logger.info("Enter-Taste zur Bestätigung des Verifizierungscodes gedrückt")
|
||||
submit_button_found = True
|
||||
|
||||
# Warten nach dem Absenden
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
return submit_button_found
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Eingeben und Absenden des Verifizierungscodes: {e}")
|
||||
return False
|
||||
|
||||
def _check_verification_success(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Überprüft, ob die Verifizierung erfolgreich war.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (Erfolg, Fehlermeldung falls vorhanden)
|
||||
"""
|
||||
try:
|
||||
# Warten nach der Verifizierung
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_result")
|
||||
|
||||
# Immer noch auf der Verifizierungsseite?
|
||||
still_on_verification = self._is_on_verification_page()
|
||||
|
||||
if still_on_verification:
|
||||
# Fehlermeldung suchen
|
||||
error_message = self.automation.ui_helper.check_for_error(
|
||||
error_selectors=[InstagramSelectors.ERROR_MESSAGE],
|
||||
error_texts=InstagramSelectors.get_error_indicators()
|
||||
)
|
||||
|
||||
if error_message:
|
||||
logger.error(f"Verifizierungsfehler: {error_message}")
|
||||
return False, error_message
|
||||
else:
|
||||
logger.error("Verifizierung fehlgeschlagen, immer noch auf der Verifizierungsseite")
|
||||
return False, "Immer noch auf der Verifizierungsseite"
|
||||
|
||||
# Erfolg anhand verschiedener Indikatoren prüfen
|
||||
current_url = self.automation.browser.page.url
|
||||
|
||||
# Wenn wir auf der Startseite sind
|
||||
if "instagram.com" in current_url and "/accounts/" not in current_url:
|
||||
logger.info("Verifizierung erfolgreich, jetzt auf der Startseite")
|
||||
return True, None
|
||||
|
||||
# Prüfe auf weitere Einrichtungsschritte (auch ein Erfolgszeichen)
|
||||
setup_indicators = [
|
||||
"add profile photo",
|
||||
"profilbild hinzufügen",
|
||||
"find friends",
|
||||
"freunde finden",
|
||||
"follow accounts",
|
||||
"konten folgen",
|
||||
"set up your profile",
|
||||
"einrichten deines profils"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for indicator in setup_indicators:
|
||||
if indicator in page_content:
|
||||
logger.info(f"Verifizierung erfolgreich, jetzt bei weiteren Einrichtungsschritten: {indicator}")
|
||||
return True, None
|
||||
|
||||
# Erfolg anhand von UI-Elementen prüfen
|
||||
success_indicators = InstagramSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self.automation.browser.is_element_visible(indicator, timeout=2000):
|
||||
logger.info(f"Verifizierung erfolgreich, Erfolgsindikator gefunden: {indicator}")
|
||||
return True, None
|
||||
|
||||
# Wenn keine eindeutigen Indikatoren gefunden wurden, aber auch keine Fehler
|
||||
logger.warning("Keine eindeutigen Erfolgsindikatoren für die Verifizierung gefunden")
|
||||
return True, None # Wir gehen davon aus, dass es erfolgreich war
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen des Verifizierungserfolgs: {e}")
|
||||
return False, f"Fehler bei der Erfolgsprüfung: {str(e)}"
|
||||
|
||||
def _handle_post_verification_dialogs(self) -> None:
|
||||
"""
|
||||
Behandelt Dialoge, die nach erfolgreicher Verifizierung erscheinen können.
|
||||
"""
|
||||
try:
|
||||
# Liste der möglichen Dialoge und wie man sie überspringt
|
||||
dialogs_to_handle = [
|
||||
{
|
||||
"name": "profile_photo",
|
||||
"skip_texts": ["Überspringen", "Skip"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"]
|
||||
},
|
||||
{
|
||||
"name": "find_friends",
|
||||
"skip_texts": ["Nicht jetzt", "Später", "Not now", "Later"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"]
|
||||
},
|
||||
{
|
||||
"name": "follow_accounts",
|
||||
"skip_texts": ["Überspringen", "Skip"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"]
|
||||
},
|
||||
{
|
||||
"name": "save_login_info",
|
||||
"skip_texts": ["Nicht jetzt", "Not now"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"]
|
||||
},
|
||||
{
|
||||
"name": "notifications",
|
||||
"skip_texts": ["Nicht jetzt", "Not now"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"]
|
||||
}
|
||||
]
|
||||
|
||||
# Versuche, jeden möglichen Dialog zu behandeln
|
||||
for dialog in dialogs_to_handle:
|
||||
self._try_skip_dialog(dialog)
|
||||
|
||||
logger.info("Nachverifikations-Dialoge behandelt")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Behandeln der Nachverifikations-Dialoge: {e}")
|
||||
# Nicht kritisch, daher keine Fehlerbehandlung
|
||||
|
||||
def _try_skip_dialog(self, dialog: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Versucht, einen bestimmten Dialog zu überspringen.
|
||||
|
||||
Args:
|
||||
dialog: Informationen zum Dialog
|
||||
|
||||
Returns:
|
||||
bool: True wenn Dialog gefunden und übersprungen, False sonst
|
||||
"""
|
||||
try:
|
||||
# Zuerst mit Fuzzy-Matching versuchen
|
||||
skip_clicked = self.automation.ui_helper.click_button_fuzzy(
|
||||
dialog["skip_texts"],
|
||||
threshold=0.7,
|
||||
timeout=3000
|
||||
)
|
||||
|
||||
if skip_clicked:
|
||||
logger.info(f"Dialog '{dialog['name']}' mit Fuzzy-Matching übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
# Wenn Fuzzy-Matching fehlschlägt, direkte Selektoren versuchen
|
||||
for selector in dialog["skip_selectors"]:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info(f"Dialog '{dialog['name']}' mit direktem Selektor übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Versuch, Dialog '{dialog['name']}' zu überspringen: {e}")
|
||||
return False
|
||||
|
||||
def resend_verification_code(self) -> bool:
|
||||
"""
|
||||
Versucht, den Verifizierungscode erneut senden zu lassen.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Resend-Button suchen und klicken
|
||||
resend_selectors = [
|
||||
InstagramSelectors.RESEND_CODE_BUTTON,
|
||||
"//button[contains(text(), 'Code erneut senden')]",
|
||||
"//button[contains(text(), 'Resend code')]",
|
||||
"//button[contains(text(), 'Send again')]",
|
||||
"//button[contains(text(), 'Noch einmal senden')]",
|
||||
"a[href*='resend']"
|
||||
]
|
||||
|
||||
for selector in resend_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Code erneut angefordert")
|
||||
self.automation.human_behavior.wait_between_actions("decision", 1.5)
|
||||
return True
|
||||
|
||||
# Fuzzy-Matching versuchen
|
||||
resend_texts = ["Code erneut senden", "Resend code", "Send again", "Noch einmal senden"]
|
||||
|
||||
resend_clicked = self.automation.ui_helper.click_button_fuzzy(
|
||||
resend_texts,
|
||||
threshold=0.7,
|
||||
timeout=3000
|
||||
)
|
||||
|
||||
if resend_clicked:
|
||||
logger.info("Code erneut angefordert (über Fuzzy-Matching)")
|
||||
self.automation.human_behavior.wait_between_actions("decision", 1.5)
|
||||
return True
|
||||
|
||||
logger.warning("Konnte keinen 'Code erneut senden'-Button finden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim erneuten Anfordern des Codes: {e}")
|
||||
return False
|
||||
455
social_networks/instagram/instagram_workflow.py
Normale Datei
455
social_networks/instagram/instagram_workflow.py
Normale Datei
@ -0,0 +1,455 @@
|
||||
"""
|
||||
Instagram-Workflow - Definiert die Schritte für die Instagram-Anmeldung und -Registrierung
|
||||
Mit TextSimilarity-Integration für robusteres Element-Matching
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import re
|
||||
|
||||
from utils.text_similarity import TextSimilarity
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("instagram_workflow")
|
||||
|
||||
class InstagramWorkflow:
|
||||
"""
|
||||
Definiert die Workflow-Schritte für verschiedene Instagram-Aktionen
|
||||
wie Registrierung, Anmeldung und Verifizierung.
|
||||
Enthält TextSimilarity-Integration für robusteres Element-Matching.
|
||||
"""
|
||||
|
||||
# Text-Ähnlichkeits-Threshold für Fuzzy-Matching
|
||||
SIMILARITY_THRESHOLD = 0.7
|
||||
|
||||
# Initialisiere TextSimilarity für Matching
|
||||
text_similarity = TextSimilarity(default_threshold=SIMILARITY_THRESHOLD)
|
||||
|
||||
# Mögliche alternative Texte für verschiedene UI-Elemente
|
||||
TEXT_ALTERNATIVES = {
|
||||
"email": ["E-Mail", "Email", "E-mail", "Mail", "email"],
|
||||
"phone": ["Telefon", "Telefonnummer", "Phone", "Mobile", "mobile"],
|
||||
"fullname": ["Vollständiger Name", "Full Name", "Name", "full name"],
|
||||
"username": ["Benutzername", "Username", "user name"],
|
||||
"password": ["Passwort", "Password", "pass"],
|
||||
"submit": ["Registrieren", "Sign up", "Anmelden", "Login", "Log in", "Submit"],
|
||||
"next": ["Weiter", "Next", "Continue", "Fortfahren"],
|
||||
"confirm": ["Bestätigen", "Confirm", "Verify", "Verifizieren"],
|
||||
"reject_cookies": ["Ablehnen", "Nur erforderliche", "Decline", "Reject", "Only necessary"],
|
||||
"skip": ["Überspringen", "Skip", "Later", "Später", "Not now", "Nicht jetzt"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_registration_workflow(registration_method: str = "email") -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die Instagram-Registrierung zurück.
|
||||
|
||||
Args:
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
# Basisschritte für beide Methoden
|
||||
common_steps = [
|
||||
{
|
||||
"name": "navigate_to_signup",
|
||||
"description": "Zur Registrierungsseite navigieren",
|
||||
"url": "https://www.instagram.com/accounts/emailsignup/",
|
||||
"wait_for": ["input[name='emailOrPhone']", "div[role='dialog']"],
|
||||
"fuzzy_match": None # Kein Fuzzy-Matching für die Navigation
|
||||
},
|
||||
{
|
||||
"name": "handle_cookie_banner",
|
||||
"description": "Cookie-Banner behandeln",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]",
|
||||
"optional": True,
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["reject_cookies"] # Fuzzy-Matching für Cookie-Ablehnung
|
||||
}
|
||||
]
|
||||
|
||||
# Registrierungsmethode wechseln, falls nötig
|
||||
method_steps = []
|
||||
if registration_method == "phone":
|
||||
method_steps.append({
|
||||
"name": "switch_to_phone",
|
||||
"description": "Zur Telefon-Registrierungsmethode wechseln",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]",
|
||||
"wait_for": ["input[name='emailOrPhone']"],
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["phone"] # Fuzzy-Matching für Telefon-Tab
|
||||
})
|
||||
elif registration_method == "email":
|
||||
method_steps.append({
|
||||
"name": "switch_to_email",
|
||||
"description": "Zur E-Mail-Registrierungsmethode wechseln",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]",
|
||||
"wait_for": ["input[name='emailOrPhone']"],
|
||||
"optional": True, # Meist Standard
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["email"] # Fuzzy-Matching für E-Mail-Tab
|
||||
})
|
||||
|
||||
# Formularausfüllung für E-Mail/Telefon
|
||||
form_steps = [
|
||||
{
|
||||
"name": "fill_email_or_phone",
|
||||
"description": f"E-Mail/Telefon eingeben ({registration_method})",
|
||||
"action": "fill",
|
||||
"target": "input[name='emailOrPhone']",
|
||||
"value": "{EMAIL_OR_PHONE}",
|
||||
"fuzzy_match": ["Handynummer oder E-Mail-Adresse", "Mobile Number or Email",
|
||||
"Phone number or email", "E-Mail-Adresse oder Telefonnummer"]
|
||||
},
|
||||
{
|
||||
"name": "fill_fullname",
|
||||
"description": "Vollständigen Namen eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[name='fullName']",
|
||||
"value": "{FULL_NAME}",
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["fullname"]
|
||||
},
|
||||
{
|
||||
"name": "fill_username",
|
||||
"description": "Benutzernamen eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[name='username']",
|
||||
"value": "{USERNAME}",
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["username"]
|
||||
},
|
||||
{
|
||||
"name": "fill_password",
|
||||
"description": "Passwort eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[name='password']",
|
||||
"value": "{PASSWORD}",
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["password"]
|
||||
},
|
||||
{
|
||||
"name": "submit_form",
|
||||
"description": "Formular absenden",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["select[title='Monat:']", "select[title='Tag:']", "select[title='Jahr:']"],
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["submit"]
|
||||
}
|
||||
]
|
||||
|
||||
# Geburtsdatumschritte
|
||||
birthday_steps = [
|
||||
{
|
||||
"name": "select_month",
|
||||
"description": "Monat auswählen",
|
||||
"action": "select",
|
||||
"target": "select[title='Monat:']",
|
||||
"value": "{MONTH}",
|
||||
"fuzzy_match": ["Monat", "Month"]
|
||||
},
|
||||
{
|
||||
"name": "select_day",
|
||||
"description": "Tag auswählen",
|
||||
"action": "select",
|
||||
"target": "select[title='Tag:']",
|
||||
"value": "{DAY}",
|
||||
"fuzzy_match": ["Tag", "Day"]
|
||||
},
|
||||
{
|
||||
"name": "select_year",
|
||||
"description": "Jahr auswählen",
|
||||
"action": "select",
|
||||
"target": "select[title='Jahr:']",
|
||||
"value": "{YEAR}",
|
||||
"fuzzy_match": ["Jahr", "Year"]
|
||||
},
|
||||
{
|
||||
"name": "submit_birthday",
|
||||
"description": "Geburtsdatum bestätigen",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'Weiter') or contains(text(), 'Next')]",
|
||||
"wait_for": ["input[name='confirmationCode']", "input[aria-label='Bestätigungscode']"],
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["next"]
|
||||
}
|
||||
]
|
||||
|
||||
# Bestätigungscodeschritte
|
||||
verification_steps = [
|
||||
{
|
||||
"name": "enter_confirmation_code",
|
||||
"description": "Bestätigungscode eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[name='confirmationCode']",
|
||||
"alternative_target": "input[aria-label='Bestätigungscode']",
|
||||
"value": "{CONFIRMATION_CODE}",
|
||||
"fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"]
|
||||
},
|
||||
{
|
||||
"name": "submit_verification",
|
||||
"description": "Bestätigungscode absenden",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]",
|
||||
"wait_for": ["img[alt='Instagram']"],
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["confirm"]
|
||||
}
|
||||
]
|
||||
|
||||
# Vollständigen Workflow zusammenstellen
|
||||
workflow = common_steps + method_steps + form_steps + birthday_steps + verification_steps
|
||||
|
||||
return workflow
|
||||
|
||||
@staticmethod
|
||||
def get_login_workflow() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die Instagram-Anmeldung zurück.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
login_steps = [
|
||||
{
|
||||
"name": "navigate_to_login",
|
||||
"description": "Zur Anmeldeseite navigieren",
|
||||
"url": "https://www.instagram.com/accounts/login/",
|
||||
"wait_for": ["input[name='username']", "div[role='dialog']"],
|
||||
"fuzzy_match": None # Kein Fuzzy-Matching für die Navigation
|
||||
},
|
||||
{
|
||||
"name": "handle_cookie_banner",
|
||||
"description": "Cookie-Banner behandeln",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]",
|
||||
"optional": True,
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["reject_cookies"]
|
||||
},
|
||||
{
|
||||
"name": "fill_username_or_email",
|
||||
"description": "Benutzername oder E-Mail eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[name='username']",
|
||||
"value": "{USERNAME_OR_EMAIL}",
|
||||
"fuzzy_match": ["Benutzername", "Username", "E-Mail", "Email", "Telefonnummer", "Phone number"]
|
||||
},
|
||||
{
|
||||
"name": "fill_password",
|
||||
"description": "Passwort eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[name='password']",
|
||||
"value": "{PASSWORD}",
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["password"]
|
||||
},
|
||||
{
|
||||
"name": "submit_login",
|
||||
"description": "Anmeldung absenden",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["svg[aria-label='Home']", "img[alt='Instagram']"],
|
||||
"fuzzy_match": ["Anmelden", "Log in", "Einloggen", "Login"]
|
||||
},
|
||||
{
|
||||
"name": "handle_save_info_prompt",
|
||||
"description": "Optional: 'Anmeldedaten speichern'-Prompt ablehnen",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'Nicht jetzt') or contains(text(), 'Not now')]",
|
||||
"optional": True,
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["skip"]
|
||||
},
|
||||
{
|
||||
"name": "handle_notifications_prompt",
|
||||
"description": "Optional: Benachrichtigungen-Prompt ablehnen",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'Nicht jetzt') or contains(text(), 'Not now')]",
|
||||
"optional": True,
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["skip"]
|
||||
}
|
||||
]
|
||||
|
||||
return login_steps
|
||||
|
||||
@staticmethod
|
||||
def get_verification_workflow(verification_method: str = "email") -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die Instagram-Verifizierung zurück.
|
||||
|
||||
Args:
|
||||
verification_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
verification_steps = [
|
||||
{
|
||||
"name": "wait_for_verification_page",
|
||||
"description": "Auf Verifizierungsseite warten",
|
||||
"wait_for": ["input[name='confirmationCode']", "input[aria-label='Bestätigungscode']"],
|
||||
"fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"]
|
||||
},
|
||||
{
|
||||
"name": "enter_verification_code",
|
||||
"description": f"Verifizierungscode eingeben ({verification_method})",
|
||||
"action": "fill",
|
||||
"target": "input[name='confirmationCode']",
|
||||
"alternative_target": "input[aria-label='Bestätigungscode']",
|
||||
"value": "{VERIFICATION_CODE}",
|
||||
"fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"]
|
||||
},
|
||||
{
|
||||
"name": "submit_verification",
|
||||
"description": "Verifizierungscode absenden",
|
||||
"action": "click",
|
||||
"target": "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]",
|
||||
"wait_for": ["img[alt='Instagram']"],
|
||||
"fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["confirm"]
|
||||
}
|
||||
]
|
||||
|
||||
return verification_steps
|
||||
|
||||
@staticmethod
|
||||
def identify_current_step(page_title: str, page_url: str, visible_elements: List[str]) -> str:
|
||||
"""
|
||||
Identifiziert den aktuellen Schritt basierend auf dem Seitentitel, der URL und sichtbaren Elementen.
|
||||
|
||||
Args:
|
||||
page_title: Titel der Seite
|
||||
page_url: URL der Seite
|
||||
visible_elements: Liste sichtbarer Elemente (Selektoren)
|
||||
|
||||
Returns:
|
||||
str: Name des identifizierten Schritts
|
||||
"""
|
||||
# Registrierungsseite
|
||||
if "signup" in page_url:
|
||||
if "input[name='emailOrPhone']" in visible_elements:
|
||||
return "fill_registration_form"
|
||||
elif "select[title='Monat:']" in visible_elements:
|
||||
return "select_birthday"
|
||||
elif "input[name='confirmationCode']" in visible_elements:
|
||||
return "enter_confirmation_code"
|
||||
|
||||
# Anmeldeseite
|
||||
elif "login" in page_url:
|
||||
return "fill_login_form"
|
||||
|
||||
# Verifizierungsseite - robuste Erkennung mit Text-Matching
|
||||
elif any(element for element in visible_elements if
|
||||
InstagramWorkflow.text_similarity.contains_similar_text(element,
|
||||
["Bestätigungscode", "Verification", "Code"],
|
||||
threshold=InstagramWorkflow.SIMILARITY_THRESHOLD)):
|
||||
return "enter_verification_code"
|
||||
|
||||
# Verifizierungsseite - Fallback mit Selektoren
|
||||
elif "input[name='confirmationCode']" in visible_elements:
|
||||
return "enter_verification_code"
|
||||
|
||||
# Startseite / Dashboard - robuste Erkennung mit Text-Matching
|
||||
elif "instagram.com/" in page_url and any(element for element in visible_elements if
|
||||
InstagramWorkflow.text_similarity.contains_similar_text(element,
|
||||
["Home", "Feed", "Startseite"],
|
||||
threshold=InstagramWorkflow.SIMILARITY_THRESHOLD)):
|
||||
return "logged_in"
|
||||
|
||||
# Startseite / Dashboard - Fallback mit Selektoren
|
||||
elif "instagram.com/" in page_url and ("svg[aria-label='Home']" in visible_elements or "img[alt='Instagram']" in visible_elements):
|
||||
return "logged_in"
|
||||
|
||||
# Nicht identifizierbar
|
||||
return "unknown"
|
||||
|
||||
@staticmethod
|
||||
def find_similar_element(elements: List[Dict[str, str]], target_text: str, threshold: float = None) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Findet ein Element, das dem Zieltext ähnlich ist.
|
||||
|
||||
Args:
|
||||
elements: Liste von Elementen mit Text-Eigenschaft
|
||||
target_text: Zu suchender Text
|
||||
threshold: Ähnlichkeitsschwellenwert (None für Standardwert)
|
||||
|
||||
Returns:
|
||||
Element oder None, wenn keines gefunden wurde
|
||||
"""
|
||||
if threshold is None:
|
||||
threshold = InstagramWorkflow.SIMILARITY_THRESHOLD
|
||||
|
||||
for element in elements:
|
||||
element_text = element.get("text", "")
|
||||
if not element_text:
|
||||
continue
|
||||
|
||||
if InstagramWorkflow.text_similarity.is_similar(target_text, element_text, threshold=threshold):
|
||||
return element
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_alternative_texts(element_type: str) -> List[str]:
|
||||
"""
|
||||
Gibt alternative Texte für einen Elementtyp zurück.
|
||||
|
||||
Args:
|
||||
element_type: Typ des Elements (z.B. "email", "submit")
|
||||
|
||||
Returns:
|
||||
Liste mit alternativen Texten
|
||||
"""
|
||||
return InstagramWorkflow.TEXT_ALTERNATIVES.get(element_type, [])
|
||||
|
||||
@staticmethod
|
||||
def fuzzy_find_step_by_name(workflow: List[Dict[str, Any]], step_name_pattern: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Findet einen Workflow-Schritt anhand eines Namensmusters.
|
||||
|
||||
Args:
|
||||
workflow: Workflow-Schritte
|
||||
step_name_pattern: Name oder Muster für den gesuchten Schritt
|
||||
|
||||
Returns:
|
||||
Der gefundene Schritt oder None
|
||||
"""
|
||||
# Exakte Übereinstimmung prüfen
|
||||
for step in workflow:
|
||||
if step["name"] == step_name_pattern:
|
||||
return step
|
||||
|
||||
# Mustersuche mit regulären Ausdrücken
|
||||
pattern = re.compile(step_name_pattern, re.IGNORECASE)
|
||||
for step in workflow:
|
||||
if pattern.search(step["name"]) or pattern.search(step.get("description", "")):
|
||||
return step
|
||||
|
||||
# Fuzzy-Matching als letzter Ausweg
|
||||
for step in workflow:
|
||||
name_similarity = InstagramWorkflow.text_similarity.similarity_ratio(step_name_pattern, step["name"])
|
||||
desc_similarity = InstagramWorkflow.text_similarity.similarity_ratio(step_name_pattern, step.get("description", ""))
|
||||
|
||||
if name_similarity > 0.8 or desc_similarity > 0.8:
|
||||
return step
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Beispielnutzung, wenn direkt ausgeführt
|
||||
if __name__ == "__main__":
|
||||
# Konfiguriere Logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Beispiel für Workflow-Generierung
|
||||
email_workflow = InstagramWorkflow.get_registration_workflow("email")
|
||||
phone_workflow = InstagramWorkflow.get_registration_workflow("phone")
|
||||
login_workflow = InstagramWorkflow.get_login_workflow()
|
||||
|
||||
print(f"E-Mail-Registrierung: {len(email_workflow)} Schritte")
|
||||
print(f"Telefon-Registrierung: {len(phone_workflow)} Schritte")
|
||||
print(f"Anmeldung: {len(login_workflow)} Schritte")
|
||||
|
||||
# Beispiel für Workflow-Details
|
||||
print("\nDetails zum E-Mail-Registrierungs-Workflow:")
|
||||
for i, step in enumerate(email_workflow):
|
||||
print(f"{i+1}. {step['name']}: {step['description']}")
|
||||
if 'fuzzy_match' in step and step['fuzzy_match']:
|
||||
print(f" Fuzzy-Match-Texte: {step['fuzzy_match']}")
|
||||
7
social_networks/ok_ru/__init__.py
Normale Datei
7
social_networks/ok_ru/__init__.py
Normale Datei
@ -0,0 +1,7 @@
|
||||
"""
|
||||
OK.ru (Odnoklassniki) automation package.
|
||||
"""
|
||||
|
||||
from .ok_ru_automation import OkRuAutomation
|
||||
|
||||
__all__ = ['OkRuAutomation']
|
||||
303
social_networks/ok_ru/ok_ru_automation.py
Normale Datei
303
social_networks/ok_ru/ok_ru_automation.py
Normale Datei
@ -0,0 +1,303 @@
|
||||
"""
|
||||
OK.ru (Odnoklassniki) Automatisierung - Hauptklasse für OK.ru-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 .ok_ru_registration import OkRuRegistration
|
||||
from .ok_ru_login import OkRuLogin
|
||||
from .ok_ru_verification import OkRuVerification
|
||||
from .ok_ru_ui_helper import OkRuUIHelper
|
||||
from .ok_ru_utils import OkRuUtils
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("ok_ru_automation")
|
||||
|
||||
class OkRuAutomation(BaseAutomation):
|
||||
"""
|
||||
Hauptklasse für die OK.ru (Odnoklassniki) Automatisierung.
|
||||
Implementiert die Registrierung und Anmeldung bei OK.ru.
|
||||
"""
|
||||
|
||||
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 OK.ru-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 = OkRuRegistration(self)
|
||||
self.login = OkRuLogin(self)
|
||||
self.verification = OkRuVerification(self)
|
||||
self.ui_helper = OkRuUIHelper(self)
|
||||
self.utils = OkRuUtils(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("OK.ru-Automatisierung initialisiert")
|
||||
|
||||
def _initialize_browser(self) -> bool:
|
||||
"""
|
||||
Initialisiert den Browser mit den entsprechenden Einstellungen.
|
||||
|
||||
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/Moscow" # Russische Zeitzone für OK.ru
|
||||
}
|
||||
|
||||
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 = "phone",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Registriert einen neuen OK.ru-Account.
|
||||
|
||||
Args:
|
||||
full_name: Vollständiger Name für den Account
|
||||
age: Alter des Benutzers
|
||||
registration_method: "phone" (OK.ru unterstützt hauptsächlich Telefon-Registrierung)
|
||||
phone_number: Telefonnummer (erforderlich)
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten
|
||||
"""
|
||||
logger.info(f"Starte OK.ru-Account-Registrierung für '{full_name}'")
|
||||
self._emit_customer_log(f"📱 OK.ru-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 Registrierung
|
||||
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=full_name,
|
||||
age=age,
|
||||
registration_method=registration_method,
|
||||
phone_number=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 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": "exception"
|
||||
})
|
||||
|
||||
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 OK.ru-Account an.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername, E-Mail oder Telefonnummer
|
||||
password: Passwort
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Anmeldung mit Status
|
||||
"""
|
||||
logger.info(f"Starte OK.ru-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": "exception"
|
||||
})
|
||||
|
||||
return self.status
|
||||
|
||||
finally:
|
||||
# Browser schließen wenn auto_close aktiviert
|
||||
if self.auto_close_browser:
|
||||
self._close_browser()
|
||||
53
social_networks/ok_ru/ok_ru_login.py
Normale Datei
53
social_networks/ok_ru/ok_ru_login.py
Normale Datei
@ -0,0 +1,53 @@
|
||||
# social_networks/ok_ru/ok_ru_login.py
|
||||
|
||||
"""
|
||||
OK.ru Login - Klasse für die Anmeldung bei OK.ru-Konten
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from .ok_ru_selectors import OkRuSelectors
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("ok_ru_login")
|
||||
|
||||
class OkRuLogin:
|
||||
"""
|
||||
Klasse für die Anmeldung bei OK.ru-Konten.
|
||||
Behandelt den kompletten Login-Prozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die OK.ru-Login-Klasse.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
self.selectors = OkRuSelectors()
|
||||
|
||||
logger.debug("OK.ru-Login initialisiert")
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Login-Prozess für einen OK.ru-Account durch.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername, E-Mail oder Telefonnummer
|
||||
password: Passwort
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis des Logins mit Status
|
||||
"""
|
||||
logger.info(f"Starte OK.ru-Login für '{username_or_email}'")
|
||||
|
||||
# Temporäre Implementierung
|
||||
return {
|
||||
"success": False,
|
||||
"error": "OK.ru Login noch nicht implementiert",
|
||||
"stage": "not_implemented"
|
||||
}
|
||||
178
social_networks/ok_ru/ok_ru_registration.py
Normale Datei
178
social_networks/ok_ru/ok_ru_registration.py
Normale Datei
@ -0,0 +1,178 @@
|
||||
# social_networks/ok_ru/ok_ru_registration.py
|
||||
|
||||
"""
|
||||
OK.ru Registration - Klasse für die Registrierung bei OK.ru
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from .ok_ru_selectors import OkRuSelectors
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("ok_ru_registration")
|
||||
|
||||
class OkRuRegistration:
|
||||
"""
|
||||
Klasse für die Registrierung bei OK.ru.
|
||||
Behandelt den kompletten Registrierungsprozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die OK.ru-Registration-Klasse.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
self.selectors = OkRuSelectors()
|
||||
|
||||
logger.debug("OK.ru-Registration initialisiert")
|
||||
|
||||
def register_account(self, full_name: str, age: int, registration_method: str = "phone",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Registrierungsprozess für einen OK.ru-Account durch.
|
||||
|
||||
Args:
|
||||
full_name: Vollständiger Name
|
||||
age: Alter
|
||||
registration_method: Registrierungsmethode (normalerweise "phone")
|
||||
phone_number: Telefonnummer
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Registrierung mit Status
|
||||
"""
|
||||
logger.info(f"Starte OK.ru-Registrierung für '{full_name}'")
|
||||
|
||||
try:
|
||||
# 1. Zur OK.ru mobilen Seite navigieren
|
||||
self.automation._emit_customer_log("🌐 Verbinde mit OK.ru...")
|
||||
if not self._navigate_to_homepage():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht zur OK.ru-Startseite navigieren",
|
||||
"stage": "navigation"
|
||||
}
|
||||
|
||||
# 2. Cookie-Banner behandeln (falls vorhanden)
|
||||
self._handle_cookie_banner()
|
||||
|
||||
# 3. Registrieren-Button klicken
|
||||
self.automation._emit_customer_log("📝 Öffne Registrierungsformular...")
|
||||
if not self._click_register_button():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte Registrieren-Button nicht finden oder klicken",
|
||||
"stage": "register_button"
|
||||
}
|
||||
|
||||
# Weitere Schritte müssen noch implementiert werden
|
||||
return {
|
||||
"success": False,
|
||||
"error": "OK.ru Registrierung in Entwicklung - weitere Schritte folgen",
|
||||
"stage": "in_development"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei OK.ru-Registrierung: {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 OK.ru Startseite.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
logger.info("Navigiere zur OK.ru Startseite")
|
||||
page.goto("https://m.ok.ru/", wait_until="domcontentloaded", timeout=30000)
|
||||
|
||||
# Warte auf Seitenladung
|
||||
self.automation.human_behavior.random_delay(2, 4)
|
||||
|
||||
# Screenshot
|
||||
self.automation._take_screenshot("ok_ru_homepage")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Navigieren zur OK.ru Startseite: {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_register_button(self) -> bool:
|
||||
"""
|
||||
Klickt auf den Registrieren-Button.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
page = self.automation.browser.page
|
||||
|
||||
# Warte kurz
|
||||
self.automation.human_behavior.random_delay(1, 2)
|
||||
|
||||
# Versuche verschiedene Registrieren-Button-Selektoren
|
||||
register_selectors = [
|
||||
self.selectors.REGISTRATION["register_button"],
|
||||
self.selectors.REGISTRATION["register_button_alt"],
|
||||
self.selectors.REGISTRATION["register_button_data"]
|
||||
]
|
||||
|
||||
for selector in register_selectors:
|
||||
try:
|
||||
if page.is_visible(selector, timeout=3000):
|
||||
logger.info(f"Registrieren-Button gefunden: {selector}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
page.click(selector)
|
||||
self.automation.human_behavior.random_delay(2, 3)
|
||||
|
||||
# Screenshot nach Klick
|
||||
self.automation._take_screenshot("after_register_click")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Selektor {selector} nicht gefunden: {e}")
|
||||
continue
|
||||
|
||||
# Screenshot für Debugging
|
||||
self.automation._take_screenshot("register_button_not_found")
|
||||
logger.error("Keinen Registrieren-Button gefunden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken auf Registrieren-Button: {e}")
|
||||
return False
|
||||
168
social_networks/ok_ru/ok_ru_selectors.py
Normale Datei
168
social_networks/ok_ru/ok_ru_selectors.py
Normale Datei
@ -0,0 +1,168 @@
|
||||
# social_networks/ok_ru/ok_ru_selectors.py
|
||||
|
||||
"""
|
||||
OK.ru (Odnoklassniki) Selektoren - Zentrale Sammlung aller CSS-Selektoren für OK.ru
|
||||
"""
|
||||
|
||||
class OkRuSelectors:
|
||||
"""
|
||||
Zentrale Klasse für alle OK.ru-spezifischen CSS-Selektoren.
|
||||
Optimiert für die mobile Version (m.ok.ru).
|
||||
"""
|
||||
|
||||
# === ALLGEMEINE SELEKTOREN ===
|
||||
COOKIE_ACCEPT_BUTTONS = [
|
||||
"button:has-text('Принять')",
|
||||
"button:has-text('Accept')",
|
||||
"button:has-text('OK')",
|
||||
"[class*='cookie'] button"
|
||||
]
|
||||
|
||||
# === REGISTRIERUNG ===
|
||||
REGISTRATION = {
|
||||
# Hauptseite
|
||||
"register_button": 'input[name="registerButton"][value="Registrieren"]',
|
||||
"register_button_alt": 'input[type="submit"][value="Registrieren"]',
|
||||
"register_button_data": 'input[data-log-click*="register"]',
|
||||
|
||||
# Formularfelder
|
||||
"phone_input": 'input[name="st.email"]',
|
||||
"phone_input_alt": 'input[type="tel"]',
|
||||
"country_code_select": 'select[name="st.countryCode"]',
|
||||
|
||||
# Buttons
|
||||
"get_code_button": 'input[type="submit"][value*="Получить код"]',
|
||||
"get_code_button_alt": 'button:has-text("Получить код")',
|
||||
|
||||
# Verifizierung
|
||||
"verification_code_input": 'input[name="st.smsCode"]',
|
||||
"verification_code_inputs": 'input[maxlength="1"]', # Einzelne Ziffern-Inputs
|
||||
|
||||
# Profil-Erstellung
|
||||
"first_name_input": 'input[name="st.firstName"]',
|
||||
"last_name_input": 'input[name="st.lastName"]',
|
||||
"password_input": 'input[name="st.password"]',
|
||||
"password_confirm_input": 'input[name="st.passwordConfirm"]',
|
||||
|
||||
# Geburtsdatum
|
||||
"birth_day_select": 'select[name="st.bday"]',
|
||||
"birth_month_select": 'select[name="st.bmonth"]',
|
||||
"birth_year_select": 'select[name="st.byear"]',
|
||||
|
||||
# Geschlecht
|
||||
"gender_male": 'input[name="st.gender"][value="1"]',
|
||||
"gender_female": 'input[name="st.gender"][value="2"]',
|
||||
|
||||
# Submit
|
||||
"register_submit": 'input[type="submit"][value*="Зарегистрироваться"]',
|
||||
"register_submit_alt": 'button:has-text("Зарегистрироваться")'
|
||||
}
|
||||
|
||||
# === LOGIN ===
|
||||
LOGIN = {
|
||||
# Login-Link
|
||||
"login_link": 'a[href*="st.cmd=anonymMain&st.login"]',
|
||||
"login_link_alt": 'a:has-text("Войти")',
|
||||
"login_link_en": 'a:has-text("Log in")',
|
||||
|
||||
# Formularfelder
|
||||
"username_input": 'input[name="st.email"]',
|
||||
"username_input_alt": 'input[type="text"][name="st.email"]',
|
||||
"password_input": 'input[name="st.password"]',
|
||||
"password_input_alt": 'input[type="password"]',
|
||||
|
||||
# Submit
|
||||
"login_submit": 'input[type="submit"][value*="Войти"]',
|
||||
"login_submit_alt": 'button:has-text("Войти")',
|
||||
"login_submit_en": 'button:has-text("Log in")'
|
||||
}
|
||||
|
||||
# === NAVIGATION ===
|
||||
NAVIGATION = {
|
||||
# Hauptnavigation
|
||||
"home_link": 'a[href*="/main"]',
|
||||
"profile_link": 'a[href*="/profile"]',
|
||||
"friends_link": 'a[href*="/friends"]',
|
||||
"messages_link": 'a[href*="/messages"]',
|
||||
"groups_link": 'a[href*="/groups"]',
|
||||
|
||||
# Mobile Menü
|
||||
"menu_button": '[class*="menu-button"]',
|
||||
"mobile_menu": '[class*="mobile-menu"]'
|
||||
}
|
||||
|
||||
# === PROFIL ===
|
||||
PROFILE = {
|
||||
# Profilbearbeitung
|
||||
"edit_profile_button": 'a:has-text("Редактировать")',
|
||||
"edit_profile_button_en": 'a:has-text("Edit")',
|
||||
|
||||
# Avatar
|
||||
"avatar_upload": 'input[type="file"]',
|
||||
"avatar_container": '[class*="avatar"]'
|
||||
}
|
||||
|
||||
# === VERIFIZIERUNG ===
|
||||
VERIFICATION = {
|
||||
# SMS-Verifizierung
|
||||
"resend_code_link": 'a:has-text("Отправить код еще раз")',
|
||||
"resend_code_timer": '[class*="timer"]',
|
||||
|
||||
# Captcha
|
||||
"captcha_container": '[class*="captcha"]',
|
||||
"captcha_input": 'input[name*="captcha"]'
|
||||
}
|
||||
|
||||
# === FEHLER UND WARNUNGEN ===
|
||||
ERRORS = {
|
||||
# Fehlermeldungen
|
||||
"error_message": '[class*="error"]',
|
||||
"error_text": '[class*="error-text"]',
|
||||
"validation_error": '[class*="validation-error"]',
|
||||
|
||||
# Spezifische Fehler
|
||||
"phone_taken": 'text="Этот номер уже используется"',
|
||||
"invalid_code": 'text="Неверный код"',
|
||||
"weak_password": 'text="Слишком простой пароль"'
|
||||
}
|
||||
|
||||
# === MODALE DIALOGE ===
|
||||
MODALS = {
|
||||
# Allgemeine Modale
|
||||
"modal_container": '[class*="modal"]',
|
||||
"modal_close": '[class*="modal-close"]',
|
||||
|
||||
# Bestätigung
|
||||
"confirm_button": 'button:has-text("Подтвердить")',
|
||||
"cancel_button": 'button:has-text("Отмена")'
|
||||
}
|
||||
|
||||
@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(), {})
|
||||
14
social_networks/ok_ru/ok_ru_ui_helper.py
Normale Datei
14
social_networks/ok_ru/ok_ru_ui_helper.py
Normale Datei
@ -0,0 +1,14 @@
|
||||
# social_networks/ok_ru/ok_ru_ui_helper.py
|
||||
|
||||
"""
|
||||
OK.ru UI Helper - Hilfsfunktionen für UI-Interaktionen
|
||||
"""
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger("ok_ru_ui_helper")
|
||||
|
||||
class OkRuUIHelper:
|
||||
def __init__(self, automation):
|
||||
self.automation = automation
|
||||
logger.debug("OK.ru-UIHelper initialisiert")
|
||||
14
social_networks/ok_ru/ok_ru_utils.py
Normale Datei
14
social_networks/ok_ru/ok_ru_utils.py
Normale Datei
@ -0,0 +1,14 @@
|
||||
# social_networks/ok_ru/ok_ru_utils.py
|
||||
|
||||
"""
|
||||
OK.ru Utils - Hilfsfunktionen für OK.ru
|
||||
"""
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger("ok_ru_utils")
|
||||
|
||||
class OkRuUtils:
|
||||
def __init__(self, automation):
|
||||
self.automation = automation
|
||||
logger.debug("OK.ru-Utils initialisiert")
|
||||
14
social_networks/ok_ru/ok_ru_verification.py
Normale Datei
14
social_networks/ok_ru/ok_ru_verification.py
Normale Datei
@ -0,0 +1,14 @@
|
||||
# social_networks/ok_ru/ok_ru_verification.py
|
||||
|
||||
"""
|
||||
OK.ru Verification - Klasse für Verifizierungsprozesse
|
||||
"""
|
||||
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger("ok_ru_verification")
|
||||
|
||||
class OkRuVerification:
|
||||
def __init__(self, automation):
|
||||
self.automation = automation
|
||||
logger.debug("OK.ru-Verification initialisiert")
|
||||
0
social_networks/tiktok/__init__.py
Normale Datei
0
social_networks/tiktok/__init__.py
Normale Datei
392
social_networks/tiktok/tiktok_automation.py
Normale Datei
392
social_networks/tiktok/tiktok_automation.py
Normale Datei
@ -0,0 +1,392 @@
|
||||
"""
|
||||
TikTok-Automatisierung - Hauptklasse für TikTok-Automatisierungsfunktionalität
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from browser.playwright_manager import PlaywrightManager
|
||||
from browser.playwright_extensions import PlaywrightExtensions
|
||||
from social_networks.base_automation import BaseAutomation
|
||||
from utils.password_generator import PasswordGenerator
|
||||
from utils.username_generator import UsernameGenerator
|
||||
from utils.birthday_generator import BirthdayGenerator
|
||||
from utils.human_behavior import HumanBehavior
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Importiere Helferklassen
|
||||
from .tiktok_registration import TikTokRegistration
|
||||
from .tiktok_login import TikTokLogin
|
||||
from .tiktok_verification import TikTokVerification
|
||||
from .tiktok_ui_helper import TikTokUIHelper
|
||||
from .tiktok_utils import TikTokUtils
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_automation")
|
||||
|
||||
class TikTokAutomation(BaseAutomation):
|
||||
"""
|
||||
Hauptklasse für die TikTok-Automatisierung.
|
||||
Implementiert die Registrierung und Anmeldung bei TikTok.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
headless: bool = False,
|
||||
use_proxy: bool = False,
|
||||
proxy_type: str = None,
|
||||
save_screenshots: bool = True,
|
||||
screenshots_dir: str = None,
|
||||
slowmo: int = 0,
|
||||
debug: bool = False,
|
||||
email_domain: str = "z5m7q9dk3ah2v1plx6ju.com",
|
||||
enhanced_stealth: bool = True,
|
||||
fingerprint_noise: float = 0.5,
|
||||
window_position = None,
|
||||
fingerprint = None,
|
||||
auto_close_browser: bool = False):
|
||||
"""
|
||||
Initialisiert die TikTok-Automatisierung.
|
||||
|
||||
Args:
|
||||
headless: Ob der Browser im Headless-Modus ausgeführt werden soll
|
||||
use_proxy: Ob ein Proxy verwendet werden soll
|
||||
proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ
|
||||
save_screenshots: Ob Screenshots gespeichert werden sollen
|
||||
screenshots_dir: Verzeichnis für Screenshots
|
||||
slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging)
|
||||
debug: Ob Debug-Informationen angezeigt werden sollen
|
||||
email_domain: Domain für generierte E-Mail-Adressen
|
||||
enhanced_stealth: Ob erweiterter Stealth-Modus aktiviert werden soll
|
||||
fingerprint_noise: Menge an Rauschen für Fingerprint-Verschleierung (0.0-1.0)
|
||||
window_position: Optional - Fensterposition als Tuple (x, y)
|
||||
fingerprint: Optional - Vordefinierter Browser-Fingerprint
|
||||
auto_close_browser: Ob Browser automatisch geschlossen werden soll (Standard: False)
|
||||
"""
|
||||
# Initialisiere die Basisklasse
|
||||
super().__init__(
|
||||
headless=headless,
|
||||
use_proxy=use_proxy,
|
||||
proxy_type=proxy_type,
|
||||
save_screenshots=save_screenshots,
|
||||
screenshots_dir=screenshots_dir,
|
||||
slowmo=slowmo,
|
||||
debug=debug,
|
||||
email_domain=email_domain,
|
||||
window_position=window_position,
|
||||
auto_close_browser=auto_close_browser
|
||||
)
|
||||
|
||||
# Stealth-Modus-Einstellungen
|
||||
self.enhanced_stealth = enhanced_stealth
|
||||
self.fingerprint_noise = max(0.0, min(1.0, fingerprint_noise))
|
||||
|
||||
# Initialisiere Helferklassen
|
||||
self.registration = TikTokRegistration(self)
|
||||
self.login = TikTokLogin(self)
|
||||
self.verification = TikTokVerification(self)
|
||||
self.ui_helper = TikTokUIHelper(self)
|
||||
self.utils = TikTokUtils(self)
|
||||
|
||||
# Zusätzliche Hilfsklassen
|
||||
self.password_generator = PasswordGenerator()
|
||||
self.username_generator = UsernameGenerator()
|
||||
self.birthday_generator = BirthdayGenerator()
|
||||
self.human_behavior = HumanBehavior(speed_factor=0.8, randomness=0.6)
|
||||
|
||||
# Nutze übergebenen Fingerprint wenn vorhanden
|
||||
self.provided_fingerprint = fingerprint
|
||||
|
||||
logger.info("TikTok-Automatisierung initialisiert")
|
||||
|
||||
def _initialize_browser(self) -> bool:
|
||||
"""
|
||||
Initialisiert den Browser mit den entsprechenden Einstellungen.
|
||||
Diese Methode überschreibt die Methode der Basisklasse, um den erweiterten
|
||||
Fingerprint-Schutz zu aktivieren.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Proxy-Konfiguration, falls aktiviert
|
||||
proxy_config = None
|
||||
if self.use_proxy:
|
||||
proxy_config = self.proxy_rotator.get_proxy(self.proxy_type)
|
||||
if not proxy_config:
|
||||
logger.warning(f"Kein Proxy vom Typ '{self.proxy_type}' verfügbar, verwende direkten Zugriff")
|
||||
|
||||
# Browser initialisieren
|
||||
self.browser = PlaywrightManager(
|
||||
headless=self.headless,
|
||||
proxy=proxy_config,
|
||||
browser_type="chromium",
|
||||
screenshots_dir=self.screenshots_dir,
|
||||
slowmo=self.slowmo
|
||||
)
|
||||
|
||||
# Browser starten
|
||||
self.browser.start()
|
||||
|
||||
# Erweiterten Fingerprint-Schutz aktivieren, wenn gewünscht
|
||||
if self.enhanced_stealth:
|
||||
# Erstelle Extensions-Objekt
|
||||
extensions = PlaywrightExtensions(self.browser)
|
||||
|
||||
# Methoden anhängen
|
||||
extensions.hook_into_playwright_manager()
|
||||
|
||||
# Fingerprint-Schutz aktivieren mit angepasster Konfiguration
|
||||
if self.provided_fingerprint:
|
||||
# Nutze den bereitgestellten Fingerprint
|
||||
logger.info("Verwende bereitgestellten Fingerprint für Account-Erstellung")
|
||||
# Konvertiere Dict zu BrowserFingerprint wenn nötig
|
||||
if isinstance(self.provided_fingerprint, dict):
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
fingerprint_obj = BrowserFingerprint.from_dict(self.provided_fingerprint)
|
||||
else:
|
||||
fingerprint_obj = self.provided_fingerprint
|
||||
|
||||
# Wende Fingerprint über FingerprintProtection an
|
||||
from browser.fingerprint_protection import FingerprintProtection
|
||||
protection = FingerprintProtection(
|
||||
context=self.browser.context,
|
||||
fingerprint_config=fingerprint_obj
|
||||
)
|
||||
protection.apply_to_context(self.browser.context)
|
||||
logger.info(f"Fingerprint {fingerprint_obj.fingerprint_id} angewendet")
|
||||
else:
|
||||
# Fallback: Zufällige Fingerprint-Konfiguration
|
||||
fingerprint_config = {
|
||||
"noise_level": self.fingerprint_noise,
|
||||
"canvas_noise": True,
|
||||
"audio_noise": True,
|
||||
"webgl_noise": True,
|
||||
"hardware_concurrency": random.choice([4, 6, 8]),
|
||||
"device_memory": random.choice([4, 8]),
|
||||
"timezone_id": "Europe/Berlin"
|
||||
}
|
||||
|
||||
success = self.browser.enable_enhanced_fingerprint_protection(fingerprint_config)
|
||||
if success:
|
||||
logger.info("Erweiterter Fingerprint-Schutz erfolgreich aktiviert")
|
||||
else:
|
||||
logger.warning("Erweiterter Fingerprint-Schutz konnte nicht aktiviert werden")
|
||||
|
||||
logger.info("Browser erfolgreich initialisiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Browser-Initialisierung: {e}")
|
||||
self.status["error"] = f"Browser-Initialisierungsfehler: {str(e)}"
|
||||
return False
|
||||
|
||||
def register_account(self, full_name: str, age: int, registration_method: str = "email",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Registriert einen neuen TikTok-Account.
|
||||
|
||||
Args:
|
||||
full_name: Vollständiger Name für den Account
|
||||
age: Alter des Benutzers
|
||||
registration_method: "email" oder "phone"
|
||||
phone_number: Telefonnummer (nur bei registration_method="phone")
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten
|
||||
"""
|
||||
logger.info(f"Starte TikTok-Account-Registrierung für '{full_name}' via {registration_method}")
|
||||
self._emit_customer_log(f"🎵 TikTok-Account wird erstellt für: {full_name}")
|
||||
|
||||
try:
|
||||
# Initialisiere Browser, falls noch nicht geschehen
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
if not self._initialize_browser():
|
||||
return {"success": False, "error": "Browser konnte nicht initialisiert werden"}
|
||||
|
||||
# Rotiere Fingerprint vor der Hauptaktivität, um Erkennung weiter zu erschweren
|
||||
if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'):
|
||||
self.browser.rotate_fingerprint()
|
||||
logger.info("Browser-Fingerprint vor der Registrierung rotiert")
|
||||
|
||||
# Delegiere die Hauptregistrierungslogik an die Registration-Klasse
|
||||
result = self.registration.register_account(full_name, age, registration_method, phone_number, **kwargs)
|
||||
|
||||
# Nehme Abschlussfoto auf
|
||||
self._take_screenshot(f"registration_finished_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update(result)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der Account-Registrierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Fehler-Screenshot
|
||||
self._take_screenshot(f"registration_error_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update({
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "error"
|
||||
})
|
||||
|
||||
return self.status
|
||||
finally:
|
||||
# Browser schließen
|
||||
self._close_browser()
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Meldet sich bei einem bestehenden TikTok-Account an.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Anmeldung mit Status
|
||||
"""
|
||||
logger.info(f"Starte TikTok-Login für '{username_or_email}'")
|
||||
|
||||
try:
|
||||
# Initialisiere Browser, falls noch nicht geschehen
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
if not self._initialize_browser():
|
||||
return {"success": False, "error": "Browser konnte nicht initialisiert werden"}
|
||||
|
||||
# Rotiere Fingerprint vor dem Login
|
||||
if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'):
|
||||
self.browser.rotate_fingerprint()
|
||||
logger.info("Browser-Fingerprint vor dem Login rotiert")
|
||||
|
||||
# Delegiere die Hauptlogin-Logik an die Login-Klasse
|
||||
result = self.login.login_account(username_or_email, password, **kwargs)
|
||||
|
||||
# Nehme Abschlussfoto auf
|
||||
self._take_screenshot(f"login_finished_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update(result)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler beim Login: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Fehler-Screenshot
|
||||
self._take_screenshot(f"login_error_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update({
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "error"
|
||||
})
|
||||
|
||||
return self.status
|
||||
finally:
|
||||
# Browser schließen
|
||||
self._close_browser()
|
||||
|
||||
def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Verifiziert einen TikTok-Account mit einem Bestätigungscode.
|
||||
|
||||
Args:
|
||||
verification_code: Der Bestätigungscode
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung mit Status
|
||||
"""
|
||||
logger.info(f"Starte TikTok-Account-Verifizierung mit Code: {verification_code}")
|
||||
|
||||
try:
|
||||
# Initialisiere Browser, falls noch nicht geschehen
|
||||
if not self.browser or not hasattr(self.browser, 'page'):
|
||||
if not self._initialize_browser():
|
||||
return {"success": False, "error": "Browser konnte nicht initialisiert werden"}
|
||||
|
||||
# Delegiere die Hauptverifizierungslogik an die Verification-Klasse
|
||||
result = self.verification.verify_account(verification_code, **kwargs)
|
||||
|
||||
# Nehme Abschlussfoto auf
|
||||
self._take_screenshot(f"verification_finished_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update(result)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der Account-Verifizierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Fehler-Screenshot
|
||||
self._take_screenshot(f"verification_error_{int(time.time())}")
|
||||
|
||||
# Aktualisiere Status
|
||||
self.status.update({
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "error"
|
||||
})
|
||||
|
||||
return self.status
|
||||
finally:
|
||||
# Browser schließen
|
||||
self._close_browser()
|
||||
|
||||
def get_fingerprint_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den aktuellen Status des Fingerprint-Schutzes zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Status des Fingerprint-Schutzes
|
||||
"""
|
||||
if not self.enhanced_stealth or not hasattr(self.browser, 'get_fingerprint_status'):
|
||||
return {
|
||||
"active": False,
|
||||
"message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert"
|
||||
}
|
||||
|
||||
return self.browser.get_fingerprint_status()
|
||||
|
||||
def get_current_page(self):
|
||||
"""
|
||||
Gibt die aktuelle Playwright Page-Instanz zurück.
|
||||
|
||||
Returns:
|
||||
Page: Die aktuelle Seite oder None
|
||||
"""
|
||||
if self.browser and hasattr(self.browser, 'page'):
|
||||
return self.browser.page
|
||||
return None
|
||||
|
||||
def get_session_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Extrahiert Session-Daten (Cookies, LocalStorage, etc.) aus dem aktuellen Browser.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Session-Daten
|
||||
"""
|
||||
if not self.is_browser_open():
|
||||
return {}
|
||||
|
||||
try:
|
||||
return {
|
||||
"cookies": self.browser.page.context.cookies(),
|
||||
"local_storage": self.browser.page.evaluate("() => Object.assign({}, window.localStorage)"),
|
||||
"session_storage": self.browser.page.evaluate("() => Object.assign({}, window.sessionStorage)"),
|
||||
"url": self.browser.page.url
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren der Session-Daten: {e}")
|
||||
return {}
|
||||
825
social_networks/tiktok/tiktok_login.py
Normale Datei
825
social_networks/tiktok/tiktok_login.py
Normale Datei
@ -0,0 +1,825 @@
|
||||
"""
|
||||
TikTok-Login - Klasse für die Anmeldefunktionalität bei TikTok
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from .tiktok_workflow import TikTokWorkflow
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_login")
|
||||
|
||||
class TikTokLogin:
|
||||
"""
|
||||
Klasse für die Anmeldung bei TikTok-Konten.
|
||||
Enthält alle Methoden für den Login-Prozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Login-Funktionalität.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = TikTokSelectors()
|
||||
self.workflow = TikTokWorkflow.get_login_workflow()
|
||||
|
||||
logger.debug("TikTok-Login initialisiert")
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Login-Prozess für ein TikTok-Konto durch.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis des Logins mit Status
|
||||
"""
|
||||
# Browser wird direkt von automation verwendet
|
||||
|
||||
# Validiere die Eingaben
|
||||
if not self._validate_login_inputs(username_or_email, password):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültige Login-Eingaben",
|
||||
"stage": "input_validation"
|
||||
}
|
||||
|
||||
# Account-Daten für die Anmeldung
|
||||
account_data = {
|
||||
"username": username_or_email,
|
||||
"password": password,
|
||||
"handle_2fa": kwargs.get("handle_2fa", False),
|
||||
"two_factor_code": kwargs.get("two_factor_code"),
|
||||
"skip_save_login": kwargs.get("skip_save_login", True)
|
||||
}
|
||||
|
||||
logger.info(f"Starte TikTok-Login für {username_or_email}")
|
||||
|
||||
try:
|
||||
# 1. Zur Login-Seite navigieren
|
||||
if not self._navigate_to_login_page():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht zur Login-Seite navigieren",
|
||||
"stage": "navigation"
|
||||
}
|
||||
|
||||
# 2. Cookie-Banner behandeln
|
||||
self._handle_cookie_banner()
|
||||
|
||||
# 3. Login-Formular ausfüllen
|
||||
if not self._fill_login_form(account_data):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Ausfüllen des Login-Formulars",
|
||||
"stage": "login_form"
|
||||
}
|
||||
|
||||
# 4. Auf 2FA prüfen und behandeln, falls nötig
|
||||
needs_2fa, two_fa_error = self._check_needs_two_factor_auth()
|
||||
|
||||
if needs_2fa:
|
||||
if not account_data["handle_2fa"]:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Zwei-Faktor-Authentifizierung erforderlich, aber nicht aktiviert",
|
||||
"stage": "two_factor_required"
|
||||
}
|
||||
|
||||
# 2FA behandeln
|
||||
if not self._handle_two_factor_auth(account_data["two_factor_code"]):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler bei der Zwei-Faktor-Authentifizierung",
|
||||
"stage": "two_factor_auth"
|
||||
}
|
||||
|
||||
# 5. Benachrichtigungserlaubnis-Dialog behandeln
|
||||
self._handle_notifications_prompt()
|
||||
|
||||
# 6. Erfolgreichen Login überprüfen
|
||||
if not self._check_login_success():
|
||||
error_message = self._get_login_error()
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Login fehlgeschlagen: {error_message or 'Unbekannter Fehler'}",
|
||||
"stage": "login_check"
|
||||
}
|
||||
|
||||
# Login erfolgreich
|
||||
logger.info(f"TikTok-Login für {username_or_email} erfolgreich")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "completed",
|
||||
"username": username_or_email
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler beim TikTok-Login: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception"
|
||||
}
|
||||
|
||||
def _validate_login_inputs(self, username_or_email: str, password: str) -> bool:
|
||||
"""
|
||||
Validiert die Eingaben für den Login.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
|
||||
Returns:
|
||||
bool: True wenn alle Eingaben gültig sind, False sonst
|
||||
"""
|
||||
if not username_or_email or len(username_or_email) < 3:
|
||||
logger.error("Ungültiger Benutzername oder E-Mail")
|
||||
return False
|
||||
|
||||
if not password or len(password) < 6:
|
||||
logger.error("Ungültiges Passwort")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _navigate_to_login_page(self) -> bool:
|
||||
"""
|
||||
Navigiert zur TikTok-Login-Seite über die Explore-Seite.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Zur Explore-Seite navigieren
|
||||
logger.info("Navigiere zur TikTok Explore-Seite")
|
||||
self.automation.browser.navigate_to(TikTokSelectors.EXPLORE_URL)
|
||||
|
||||
# Warten, bis die Seite geladen ist
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("tiktok_explore_page")
|
||||
|
||||
# Login-Button auf der Explore-Seite suchen und klicken
|
||||
logger.info("Suche Anmelden-Button auf Explore-Seite")
|
||||
login_button_selectors = [
|
||||
"button#header-login-button",
|
||||
"button.TUXButton--primary",
|
||||
"button:has-text('Anmelden')",
|
||||
TikTokSelectors.LOGIN_BUTTON_HEADER,
|
||||
"button[class*='StyledLeftSidePrimaryButton']"
|
||||
]
|
||||
|
||||
button_clicked = False
|
||||
for selector in login_button_selectors:
|
||||
logger.debug(f"Versuche Login-Button: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Kurz warten vor dem Klick
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
if self.automation.browser.click_element(selector):
|
||||
button_clicked = True
|
||||
logger.info(f"Anmelden-Button erfolgreich geklickt: {selector}")
|
||||
break
|
||||
|
||||
if not button_clicked:
|
||||
logger.error("Konnte keinen Anmelden-Button auf der Explore-Seite finden")
|
||||
self.automation._take_screenshot("no_login_button_found")
|
||||
return False
|
||||
|
||||
# Warten, bis der Login-Dialog erscheint
|
||||
logger.info("Warte auf Login-Dialog")
|
||||
self.automation.human_behavior.random_delay(2.0, 3.0)
|
||||
|
||||
# Prüfen, ob der Login-Dialog sichtbar ist
|
||||
dialog_visible = False
|
||||
dialog_selectors = [
|
||||
"div[role='dialog']",
|
||||
TikTokSelectors.LOGIN_DIALOG,
|
||||
"div[class*='login-modal']",
|
||||
"div[class*='DivLoginContainer']"
|
||||
]
|
||||
|
||||
for dialog_selector in dialog_selectors:
|
||||
if self.automation.browser.is_element_visible(dialog_selector, timeout=5000):
|
||||
dialog_visible = True
|
||||
logger.info(f"Login-Dialog erschienen: {dialog_selector}")
|
||||
break
|
||||
|
||||
if not dialog_visible:
|
||||
logger.error("Login-Dialog ist nach 5 Sekunden nicht erschienen")
|
||||
self.automation._take_screenshot("no_login_dialog")
|
||||
return False
|
||||
|
||||
# Screenshot vom geöffneten Dialog
|
||||
self.automation._take_screenshot("login_dialog_opened")
|
||||
|
||||
logger.info("Erfolgreich zum Login-Dialog navigiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Navigieren zur Login-Seite: {e}")
|
||||
return False
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
Akzeptiert IMMER Cookies für vollständiges Session-Management beim Login.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
# Cookie-Dialog-Erkennung
|
||||
if self.automation.browser.is_element_visible(TikTokSelectors.COOKIE_DIALOG, timeout=2000):
|
||||
logger.info("Cookie-Banner erkannt - akzeptiere alle Cookies für Session-Management")
|
||||
|
||||
# Akzeptieren-Button suchen und klicken (PRIMÄR für Login)
|
||||
accept_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
TikTokSelectors.get_button_texts("accept_cookies"),
|
||||
TikTokSelectors.COOKIE_ACCEPT_BUTTON
|
||||
)
|
||||
|
||||
if accept_success:
|
||||
logger.info("Cookie-Banner erfolgreich akzeptiert - Session-Cookies werden gespeichert")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
else:
|
||||
logger.warning("Konnte Cookie-Banner nicht akzeptieren, versuche alternativen Akzeptieren-Button")
|
||||
|
||||
# Alternative Akzeptieren-Selektoren versuchen
|
||||
alternative_accept_selectors = [
|
||||
"//button[contains(text(), 'Alle akzeptieren')]",
|
||||
"//button[contains(text(), 'Accept All')]",
|
||||
"//button[contains(text(), 'Zulassen')]",
|
||||
"//button[contains(text(), 'Allow All')]",
|
||||
"//button[contains(@aria-label, 'Accept')]",
|
||||
"[data-testid='accept-all-button']"
|
||||
]
|
||||
|
||||
for selector in alternative_accept_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Cookie-Banner mit alternativem Selector akzeptiert")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
logger.error("Konnte Cookie-Banner nicht akzeptieren - Session-Management könnte beeinträchtigt sein")
|
||||
return False
|
||||
else:
|
||||
logger.debug("Kein Cookie-Banner erkannt")
|
||||
return True
|
||||
|
||||
def _fill_login_form(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Füllt das Login-Formular aus und sendet es ab.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten für den Login
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# 1. E-Mail/Telefon-Login-Option auswählen
|
||||
logger.info("Klicke auf 'Telefon-Nr./E-Mail/Anmeldename nutzen'")
|
||||
|
||||
# Warte kurz, damit Dialog vollständig geladen ist
|
||||
self.automation.human_behavior.random_delay(1.0, 1.5)
|
||||
|
||||
# Selektoren für die Option - spezifischer für E-Mail/Telefon
|
||||
phone_email_selectors = [
|
||||
"div[data-e2e='channel-item']:has(p:has-text('Telefon-Nr./E-Mail/Anmeldename nutzen'))",
|
||||
"div[role='link']:has-text('Telefon-Nr./E-Mail/Anmeldename nutzen')",
|
||||
"//div[@data-e2e='channel-item'][.//p[contains(text(), 'Telefon-Nr./E-Mail/Anmeldename nutzen')]]",
|
||||
"div.css-17hparj-DivBoxContainer:has-text('Telefon-Nr./E-Mail/Anmeldename nutzen')",
|
||||
"//div[contains(@class, 'DivBoxContainer') and contains(., 'Telefon-Nr./E-Mail/Anmeldename nutzen')]"
|
||||
]
|
||||
|
||||
email_phone_clicked = False
|
||||
for selector in phone_email_selectors:
|
||||
logger.debug(f"Versuche E-Mail/Telefon-Selektor: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Kurz warten vor dem Klick
|
||||
self.automation.human_behavior.random_delay(0.3, 0.5)
|
||||
if self.automation.browser.click_element(selector):
|
||||
email_phone_clicked = True
|
||||
logger.info(f"E-Mail/Telefon-Option erfolgreich geklickt: {selector}")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Klick fehlgeschlagen für Selektor: {selector}")
|
||||
|
||||
if not email_phone_clicked:
|
||||
logger.error("Konnte 'Telefon-Nr./E-Mail/Anmeldename nutzen' nicht klicken")
|
||||
self.automation._take_screenshot("phone_email_option_not_found")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# 2. "Mit E-Mail-Adresse oder Benutzernamen anmelden" Link klicken
|
||||
logger.info("Klicke auf 'Mit E-Mail-Adresse oder Benutzernamen anmelden'")
|
||||
|
||||
email_link_selectors = [
|
||||
"a[href='/login/phone-or-email/email']",
|
||||
"a.css-1mgli76-ALink-StyledLink",
|
||||
"a:has-text('Mit E-Mail-Adresse oder Benutzernamen anmelden')",
|
||||
"//a[contains(text(), 'Mit E-Mail-Adresse oder Benutzernamen anmelden')]"
|
||||
]
|
||||
|
||||
email_login_clicked = False
|
||||
for selector in email_link_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
email_login_clicked = True
|
||||
logger.info(f"E-Mail-Login-Link geklickt: {selector}")
|
||||
break
|
||||
|
||||
if not email_login_clicked:
|
||||
logger.error("Konnte E-Mail-Login-Link nicht klicken")
|
||||
self.automation._take_screenshot("email_login_link_not_found")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.5, 2.5)
|
||||
|
||||
# 3. E-Mail/Benutzername eingeben (Character-by-Character)
|
||||
logger.info(f"Gebe E-Mail/Benutzername ein: {account_data['username']}")
|
||||
|
||||
# Warte bis Formular geladen ist
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
|
||||
username_selectors = [
|
||||
"input[name='username']",
|
||||
"input[placeholder='E-Mail-Adresse oder Benutzername']",
|
||||
"input.css-11to27l-InputContainer[name='username']",
|
||||
"input[type='text'][autocomplete='webauthn']"
|
||||
]
|
||||
|
||||
username_success = False
|
||||
for selector in username_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
username_success = self._fill_username_field_character_by_character(selector, account_data["username"])
|
||||
if username_success:
|
||||
logger.info(f"Benutzername erfolgreich eingegeben mit Selektor: {selector}")
|
||||
break
|
||||
|
||||
if not username_success:
|
||||
logger.error("Konnte Benutzername-Feld nicht ausfüllen")
|
||||
self.automation._take_screenshot("username_field_not_found")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
|
||||
# 4. Passwort eingeben (mit Character-by-Character für bessere Kompatibilität)
|
||||
logger.info("Gebe Passwort ein")
|
||||
password_selectors = [
|
||||
"input[type='password']",
|
||||
"input[placeholder='Passwort']",
|
||||
"input.css-wv3bkt-InputContainer[type='password']",
|
||||
"input[autocomplete='new-password']"
|
||||
]
|
||||
|
||||
password_success = False
|
||||
for selector in password_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Verwende character-by-character Eingabe
|
||||
password_success = self._fill_password_field_character_by_character(selector, account_data["password"])
|
||||
if password_success:
|
||||
logger.info(f"Passwort erfolgreich eingegeben mit Selektor: {selector}")
|
||||
break
|
||||
|
||||
if not password_success:
|
||||
logger.error("Konnte Passwort-Feld nicht ausfüllen")
|
||||
self.automation._take_screenshot("password_field_not_found")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot vorm Absenden
|
||||
self.automation._take_screenshot("login_form_filled")
|
||||
|
||||
# 5. Prüfe ob Login-Button aktiviert ist
|
||||
logger.info("Prüfe Login-Button Status")
|
||||
login_button_selectors = [
|
||||
"button[data-e2e='login-button']",
|
||||
"button[type='submit'][data-e2e='login-button']",
|
||||
"button.css-11sviba-Button-StyledButton",
|
||||
"button:has-text('Anmelden')"
|
||||
]
|
||||
|
||||
button_ready = False
|
||||
active_selector = None
|
||||
for selector in login_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
element = self.automation.browser.page.locator(selector).first
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
if not is_disabled:
|
||||
button_ready = True
|
||||
active_selector = selector
|
||||
logger.info(f"Login-Button ist aktiviert: {selector}")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"Login-Button ist disabled: {selector}")
|
||||
|
||||
if not button_ready:
|
||||
logger.warning("Login-Button ist nicht bereit, warte zusätzlich")
|
||||
self.automation.human_behavior.random_delay(2.0, 3.0)
|
||||
# Nochmal prüfen
|
||||
for selector in login_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
element = self.automation.browser.page.locator(selector).first
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
if not is_disabled:
|
||||
button_ready = True
|
||||
active_selector = selector
|
||||
break
|
||||
|
||||
# 6. Login-Button klicken
|
||||
logger.info("Klicke auf Anmelden-Button")
|
||||
if button_ready and active_selector:
|
||||
if self.automation.browser.click_element(active_selector):
|
||||
logger.info(f"Login-Button erfolgreich geklickt: {active_selector}")
|
||||
else:
|
||||
logger.error("Klick auf Login-Button fehlgeschlagen")
|
||||
return False
|
||||
else:
|
||||
logger.error("Konnte keinen aktivierten Login-Button finden")
|
||||
self.automation._take_screenshot("no_active_login_button")
|
||||
return False
|
||||
|
||||
# Nach dem Absenden warten
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=2.0)
|
||||
|
||||
# Überprüfen, ob es eine Fehlermeldung gab
|
||||
error_message = self._get_login_error()
|
||||
if error_message:
|
||||
logger.error(f"Login-Fehler erkannt: {error_message}")
|
||||
return False
|
||||
|
||||
logger.info("Login-Formular erfolgreich ausgefüllt und abgesendet")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Login-Formulars: {e}")
|
||||
return False
|
||||
|
||||
def _fill_username_field_character_by_character(self, selector: str, username: str) -> bool:
|
||||
"""
|
||||
Füllt das Benutzername-Feld Zeichen für Zeichen aus.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor für das Username-Feld
|
||||
username: Der einzugebende Benutzername
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
element = self.automation.browser.page.locator(selector).first
|
||||
if not element.is_visible():
|
||||
return False
|
||||
|
||||
logger.info("Verwende Character-by-Character Eingabe für Benutzername-Feld")
|
||||
|
||||
# Fokussiere und lösche das Feld
|
||||
element.click()
|
||||
self.automation.human_behavior.random_delay(0.1, 0.2)
|
||||
|
||||
# Lösche existierenden Inhalt
|
||||
element.select_text()
|
||||
element.press("Delete")
|
||||
self.automation.human_behavior.random_delay(0.1, 0.2)
|
||||
|
||||
# Tippe jeden Buchstaben einzeln
|
||||
import random
|
||||
for i, char in enumerate(username):
|
||||
element.type(char, delay=random.randint(50, 150)) # Zufällige Tippgeschwindigkeit
|
||||
|
||||
# Nach jedem 4. Zeichen eine kleine Pause (simuliert echtes Tippen)
|
||||
if (i + 1) % 4 == 0:
|
||||
self.automation.human_behavior.random_delay(0.1, 0.3)
|
||||
|
||||
# Fokus verlassen
|
||||
self.automation.human_behavior.random_delay(0.2, 0.4)
|
||||
element.press("Tab")
|
||||
|
||||
logger.info(f"Benutzername character-by-character eingegeben: {len(username)} Zeichen")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Character-by-Character Benutzername-Eingabe: {e}")
|
||||
return False
|
||||
|
||||
def _fill_password_field_character_by_character(self, selector: str, password: str) -> bool:
|
||||
"""
|
||||
Füllt das Passwort-Feld Zeichen für Zeichen aus, um React's State korrekt zu aktualisieren.
|
||||
|
||||
Args:
|
||||
selector: CSS-Selektor für das Passwort-Feld
|
||||
password: Das einzugebende Passwort
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
element = self.automation.browser.page.locator(selector).first
|
||||
if not element.is_visible():
|
||||
return False
|
||||
|
||||
logger.info("Verwende Character-by-Character Eingabe für Passwort-Feld")
|
||||
|
||||
# Fokussiere und lösche das Feld
|
||||
element.click()
|
||||
self.automation.human_behavior.random_delay(0.1, 0.2)
|
||||
|
||||
# Lösche existierenden Inhalt
|
||||
element.select_text()
|
||||
element.press("Delete")
|
||||
self.automation.human_behavior.random_delay(0.1, 0.2)
|
||||
|
||||
# Tippe jeden Buchstaben einzeln
|
||||
import random
|
||||
for i, char in enumerate(password):
|
||||
element.type(char, delay=random.randint(50, 150)) # Zufällige Tippgeschwindigkeit
|
||||
|
||||
# Nach jedem 3. Zeichen eine kleine Pause (simuliert echtes Tippen)
|
||||
if (i + 1) % 3 == 0:
|
||||
self.automation.human_behavior.random_delay(0.1, 0.3)
|
||||
|
||||
# Fokus verlassen, um Validierung zu triggern
|
||||
self.automation.human_behavior.random_delay(0.2, 0.4)
|
||||
element.press("Tab")
|
||||
|
||||
logger.info(f"Passwort character-by-character eingegeben: {len(password)} Zeichen")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Character-by-Character Passwort-Eingabe: {e}")
|
||||
return False
|
||||
|
||||
def _get_login_error(self) -> Optional[str]:
|
||||
"""
|
||||
Überprüft, ob eine Login-Fehlermeldung angezeigt wird.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Fehlermeldung oder None, wenn keine gefunden wurde
|
||||
"""
|
||||
try:
|
||||
# Auf Fehlermeldungen prüfen
|
||||
error_selectors = [
|
||||
TikTokSelectors.ERROR_MESSAGE,
|
||||
"p[class*='error']",
|
||||
"div[role='alert']",
|
||||
"div[class*='error']",
|
||||
"div[class*='Error']"
|
||||
]
|
||||
|
||||
for selector in error_selectors:
|
||||
error_element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if error_element:
|
||||
error_text = error_element.text_content()
|
||||
if error_text and len(error_text.strip()) > 0:
|
||||
return error_text.strip()
|
||||
|
||||
# Wenn keine spezifische Fehlermeldung gefunden wurde, nach bekannten Fehlermustern suchen
|
||||
error_texts = [
|
||||
"Falsches Passwort",
|
||||
"Benutzername nicht gefunden",
|
||||
"incorrect password",
|
||||
"username you entered doesn't belong",
|
||||
"please wait a few minutes",
|
||||
"try again later",
|
||||
"Bitte warte einige Minuten",
|
||||
"versuche es später noch einmal"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content()
|
||||
for error_text in error_texts:
|
||||
if error_text.lower() in page_content.lower():
|
||||
return f"Erkannter Fehler: {error_text}"
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Login-Fehler: {e}")
|
||||
return None
|
||||
|
||||
def _check_needs_two_factor_auth(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Überprüft, ob eine Zwei-Faktor-Authentifizierung erforderlich ist.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (2FA erforderlich, Fehlermeldung falls vorhanden)
|
||||
"""
|
||||
try:
|
||||
# Nach 2FA-Indikatoren suchen
|
||||
two_fa_selectors = [
|
||||
"input[name='verificationCode']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder*='Code']",
|
||||
"div[class*='verification-code']",
|
||||
"div[class*='two-factor']"
|
||||
]
|
||||
|
||||
for selector in two_fa_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Zwei-Faktor-Authentifizierung erforderlich")
|
||||
return True, None
|
||||
|
||||
# Texte, die auf 2FA hinweisen
|
||||
two_fa_indicators = [
|
||||
"Verifizierungscode",
|
||||
"Verification code",
|
||||
"Sicherheitscode",
|
||||
"Security code",
|
||||
"zwei-faktor",
|
||||
"two-factor",
|
||||
"2FA"
|
||||
]
|
||||
|
||||
# Seiteninhalt durchsuchen
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for indicator in two_fa_indicators:
|
||||
if indicator.lower() in page_content:
|
||||
logger.info(f"Zwei-Faktor-Authentifizierung erkannt durch Text: {indicator}")
|
||||
return True, None
|
||||
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf 2FA: {e}")
|
||||
return False, f"Fehler bei der 2FA-Erkennung: {str(e)}"
|
||||
|
||||
def _handle_two_factor_auth(self, two_factor_code: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Behandelt die Zwei-Faktor-Authentifizierung.
|
||||
|
||||
Args:
|
||||
two_factor_code: Optional vorhandener 2FA-Code
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("two_factor_auth")
|
||||
|
||||
# 2FA-Eingabefeld finden
|
||||
two_fa_selectors = [
|
||||
"input[name='verificationCode']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder*='Code']"
|
||||
]
|
||||
|
||||
two_fa_field = None
|
||||
for selector in two_fa_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
two_fa_field = selector
|
||||
break
|
||||
|
||||
if not two_fa_field:
|
||||
logger.error("Konnte 2FA-Eingabefeld nicht finden")
|
||||
return False
|
||||
|
||||
# Wenn kein Code bereitgestellt wurde, Benutzer auffordern
|
||||
if not two_factor_code:
|
||||
logger.warning("Kein 2FA-Code bereitgestellt, kann nicht fortfahren")
|
||||
return False
|
||||
|
||||
# 2FA-Code eingeben
|
||||
code_success = self.automation.browser.fill_form_field(two_fa_field, two_factor_code)
|
||||
|
||||
if not code_success:
|
||||
logger.error("Konnte 2FA-Code nicht eingeben")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Bestätigen-Button finden und klicken
|
||||
confirm_button_selectors = [
|
||||
"button[type='submit']",
|
||||
"//button[contains(text(), 'Bestätigen')]",
|
||||
"//button[contains(text(), 'Confirm')]",
|
||||
"//button[contains(text(), 'Verify')]"
|
||||
]
|
||||
|
||||
confirm_clicked = False
|
||||
for selector in confirm_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
confirm_clicked = True
|
||||
break
|
||||
|
||||
if not confirm_clicked:
|
||||
# Alternative: Mit Tastendruck bestätigen
|
||||
self.automation.browser.page.keyboard.press("Enter")
|
||||
logger.info("Enter-Taste gedrückt, um 2FA zu bestätigen")
|
||||
|
||||
# Warten nach der Bestätigung
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Überprüfen, ob 2FA erfolgreich war
|
||||
still_on_2fa = self._check_needs_two_factor_auth()[0]
|
||||
|
||||
if still_on_2fa:
|
||||
# Prüfen, ob Fehlermeldung angezeigt wird
|
||||
error_message = self._get_login_error()
|
||||
if error_message:
|
||||
logger.error(f"2FA-Fehler: {error_message}")
|
||||
else:
|
||||
logger.error("2FA fehlgeschlagen, immer noch auf 2FA-Seite")
|
||||
return False
|
||||
|
||||
logger.info("Zwei-Faktor-Authentifizierung erfolgreich")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Zwei-Faktor-Authentifizierung: {e}")
|
||||
return False
|
||||
|
||||
def _handle_notifications_prompt(self) -> bool:
|
||||
"""
|
||||
Behandelt den Benachrichtigungen-Dialog.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Nach "Nicht jetzt"-Button suchen
|
||||
not_now_selectors = [
|
||||
"//button[contains(text(), 'Nicht jetzt')]",
|
||||
"//button[contains(text(), 'Not now')]",
|
||||
"//button[contains(text(), 'Skip')]",
|
||||
"//button[contains(text(), 'Später')]",
|
||||
"//button[contains(text(), 'Later')]"
|
||||
]
|
||||
|
||||
for selector in not_now_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Benachrichtigungen-Dialog übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
# Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden
|
||||
logger.debug("Kein Benachrichtigungen-Dialog erkannt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Behandeln des Benachrichtigungen-Dialogs: {e}")
|
||||
# Dies ist nicht kritisch, daher geben wir trotzdem True zurück
|
||||
return True
|
||||
|
||||
def _check_login_success(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob der Login erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich, False sonst
|
||||
"""
|
||||
try:
|
||||
# Warten nach dem Login
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("login_final")
|
||||
|
||||
# Erfolg anhand verschiedener Indikatoren prüfen
|
||||
success_indicators = TikTokSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self.automation.browser.is_element_visible(indicator, timeout=2000):
|
||||
logger.info(f"Login-Erfolgsindikator gefunden: {indicator}")
|
||||
return True
|
||||
|
||||
# Alternativ prüfen, ob wir auf der TikTok-Startseite sind
|
||||
current_url = self.automation.browser.page.url
|
||||
if "tiktok.com" in current_url and "/login" not in current_url:
|
||||
logger.info(f"Login-Erfolg basierend auf URL: {current_url}")
|
||||
return True
|
||||
|
||||
# Prüfen, ob immer noch auf der Login-Seite
|
||||
if "/login" in current_url or self.automation.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=1000):
|
||||
logger.warning("Immer noch auf der Login-Seite, Login fehlgeschlagen")
|
||||
return False
|
||||
|
||||
logger.warning("Keine Login-Erfolgsindikatoren gefunden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen des Login-Erfolgs: {e}")
|
||||
return False
|
||||
2455
social_networks/tiktok/tiktok_registration.py
Normale Datei
2455
social_networks/tiktok/tiktok_registration.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
2203
social_networks/tiktok/tiktok_registration_backup.py
Normale Datei
2203
social_networks/tiktok/tiktok_registration_backup.py
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
115
social_networks/tiktok/tiktok_registration_final.py
Normale Datei
115
social_networks/tiktok/tiktok_registration_final.py
Normale Datei
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
801
social_networks/tiktok/tiktok_registration_new.py
Normale Datei
801
social_networks/tiktok/tiktok_registration_new.py
Normale Datei
@ -0,0 +1,801 @@
|
||||
# social_networks/tiktok/tiktok_registration_new.py
|
||||
|
||||
"""
|
||||
TikTok-Registrierung - Optimierte Klasse für die Kontoerstellung bei TikTok
|
||||
NEUE IMPLEMENTIERUNG mit korrekter Workflow-Reihenfolge für maximale Stabilität.
|
||||
|
||||
OPTIMIERTER WORKFLOW:
|
||||
1. E-Mail eingeben
|
||||
2. Passwort eingeben
|
||||
3. Code senden Button klicken
|
||||
4. Code empfangen und eingeben
|
||||
5. Weiter Button wird automatisch aktiviert
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from .tiktok_workflow import TikTokWorkflow
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_registration")
|
||||
|
||||
class TikTokRegistration:
|
||||
"""
|
||||
Optimierte Klasse für die Registrierung von TikTok-Konten.
|
||||
Implementiert einen robusten, zukunftssicheren Workflow.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Registrierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
self.selectors = TikTokSelectors()
|
||||
self.workflow = TikTokWorkflow.get_registration_workflow()
|
||||
|
||||
logger.debug("TikTok-Registrierung initialisiert")
|
||||
|
||||
def register_account(self, full_name: str, age: int, registration_method: str = "email",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den vollständigen Registrierungsprozess für einen TikTok-Account durch.
|
||||
|
||||
Args:
|
||||
full_name: Vollständiger Name für den Account
|
||||
age: Alter des Benutzers
|
||||
registration_method: "email" oder "phone"
|
||||
phone_number: Telefonnummer (nur bei registration_method="phone")
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten
|
||||
"""
|
||||
# Validiere die Eingaben
|
||||
if not self._validate_registration_inputs(full_name, age, registration_method, phone_number):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültige Eingabeparameter",
|
||||
"stage": "input_validation"
|
||||
}
|
||||
|
||||
# Account-Daten generieren
|
||||
account_data = self._generate_account_data(full_name, age, registration_method, phone_number, **kwargs)
|
||||
|
||||
# Starte den Registrierungsprozess
|
||||
logger.info(f"Starte optimierten TikTok-Registrierungsprozess für {account_data['username']} via {registration_method}")
|
||||
|
||||
try:
|
||||
# 1. Zur Startseite navigieren
|
||||
self.automation._emit_customer_log("🌐 Mit TikTok verbinden...")
|
||||
if not self._navigate_to_homepage():
|
||||
return self._create_error_result("Konnte nicht zur TikTok-Startseite navigieren", "navigation", account_data)
|
||||
|
||||
# 2. Cookie-Banner behandeln
|
||||
self.automation._emit_customer_log("⚙️ Einstellungen werden vorbereitet...")
|
||||
self._handle_cookie_banner()
|
||||
|
||||
# 3. Anmelden-Button klicken
|
||||
self.automation._emit_customer_log("📋 Registrierungsformular wird geöffnet...")
|
||||
if not self._click_login_button():
|
||||
return self._create_error_result("Konnte nicht auf Anmelden-Button klicken", "login_button", account_data)
|
||||
|
||||
# 4. Registrieren-Link klicken
|
||||
if not self._click_register_link():
|
||||
return self._create_error_result("Konnte nicht auf Registrieren-Link klicken", "register_link", account_data)
|
||||
|
||||
# 5. Telefon/E-Mail-Option auswählen
|
||||
if not self._click_phone_email_option():
|
||||
return self._create_error_result("Konnte nicht auf Telefon/E-Mail-Option klicken", "phone_email_option", account_data)
|
||||
|
||||
# 6. E-Mail oder Telefon als Registrierungsmethode wählen
|
||||
if not self._select_registration_method(registration_method):
|
||||
return self._create_error_result(f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen", "registration_method", account_data)
|
||||
|
||||
# 7. Geburtsdatum eingeben
|
||||
self.automation._emit_customer_log("🎂 Geburtsdatum wird festgelegt...")
|
||||
if not self._enter_birthday(account_data["birthday"]):
|
||||
return self._create_error_result("Fehler beim Eingeben des Geburtsdatums", "birthday", account_data)
|
||||
|
||||
# 8. OPTIMIERTER REGISTRIERUNGSWORKFLOW
|
||||
self.automation._emit_customer_log("📝 Persönliche Daten werden übertragen...")
|
||||
if not self._execute_optimized_registration_workflow(account_data, registration_method):
|
||||
return self._create_error_result("Fehler im Registrierungsworkflow", "registration_workflow", account_data)
|
||||
|
||||
# 9. Benutzernamen erstellen
|
||||
self.automation._emit_customer_log("👤 Benutzername wird erstellt...")
|
||||
if not self._create_username(account_data):
|
||||
return self._create_error_result("Fehler beim Erstellen des Benutzernamens", "username", account_data)
|
||||
|
||||
# 10. Erfolgreiche Registrierung überprüfen
|
||||
self.automation._emit_customer_log("🔍 Account wird finalisiert...")
|
||||
if not self._check_registration_success():
|
||||
return self._create_error_result("Registrierung fehlgeschlagen oder konnte nicht verifiziert werden", "final_check", account_data)
|
||||
|
||||
# Registrierung erfolgreich abgeschlossen
|
||||
logger.info(f"TikTok-Account {account_data['username']} erfolgreich erstellt")
|
||||
self.automation._emit_customer_log("✅ Account erfolgreich erstellt!")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "completed",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der TikTok-Registrierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
def _execute_optimized_registration_workflow(self, account_data: Dict[str, Any], registration_method: str) -> bool:
|
||||
"""
|
||||
Führt den optimierten Registrierungsworkflow aus.
|
||||
|
||||
KORRIGIERTE REIHENFOLGE für E-Mail-Registrierung:
|
||||
1. E-Mail eingeben
|
||||
2. Code senden Button klicken
|
||||
3. Code empfangen und eingeben
|
||||
4. Passwort eingeben
|
||||
5. Dummy-Input-Trick anwenden
|
||||
6. Weiter Button klicken
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
if registration_method == "email":
|
||||
return self._execute_email_workflow(account_data)
|
||||
elif registration_method == "phone":
|
||||
return self._execute_phone_workflow(account_data)
|
||||
else:
|
||||
logger.error(f"Unbekannte Registrierungsmethode: {registration_method}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im optimierten Registrierungsworkflow: {e}")
|
||||
return False
|
||||
|
||||
def _execute_email_workflow(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Führt den optimierten E-Mail-Registrierungsworkflow aus.
|
||||
|
||||
KORRIGIERTER WORKFLOW: E-Mail → Code senden → Code eingeben → Passwort → Dummy-Trick → Weiter
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("=== STARTE OPTIMIERTEN E-MAIL-WORKFLOW ===")
|
||||
|
||||
# SCHRITT 1: E-Mail-Feld ausfüllen
|
||||
logger.info("SCHRITT 1/6: E-Mail-Adresse eingeben")
|
||||
if not self._fill_email_field(account_data["email"]):
|
||||
logger.error("Fehler beim Ausfüllen des E-Mail-Feldes")
|
||||
return False
|
||||
|
||||
# SCHRITT 2: Code senden Button klicken (VOR Passwort!)
|
||||
logger.info("SCHRITT 2/6: Code senden Button klicken")
|
||||
if not self._click_send_code_button():
|
||||
logger.error("Fehler beim Klicken des Code-senden-Buttons")
|
||||
return False
|
||||
|
||||
# SCHRITT 3: Verifizierungscode empfangen und eingeben
|
||||
logger.info("SCHRITT 3/6: Auf Code warten und eingeben")
|
||||
if not self._handle_email_verification(account_data["email"]):
|
||||
logger.error("Fehler bei der E-Mail-Verifizierung")
|
||||
return False
|
||||
|
||||
# SCHRITT 4: Passwort-Feld ausfüllen (NACH Code-Eingabe!)
|
||||
logger.info("SCHRITT 4/6: Passwort eingeben (nach Code-Verifizierung)")
|
||||
if not self._fill_password_field(account_data["password"]):
|
||||
logger.error("Fehler beim Ausfüllen des Passwort-Feldes")
|
||||
return False
|
||||
|
||||
# SCHRITT 5: Dummy-Input-Trick anwenden
|
||||
logger.info("SCHRITT 5/6: Dummy-Input-Trick anwenden")
|
||||
if not self._apply_dummy_input_trick():
|
||||
logger.error("Fehler beim Dummy-Input-Trick")
|
||||
return False
|
||||
|
||||
# SCHRITT 6: Weiter Button klicken
|
||||
logger.info("SCHRITT 6/6: Weiter Button klicken")
|
||||
if not self._click_continue_button():
|
||||
logger.error("Fehler beim Klicken des Weiter-Buttons")
|
||||
return False
|
||||
|
||||
logger.info("=== E-MAIL-WORKFLOW ERFOLGREICH ABGESCHLOSSEN ===")
|
||||
|
||||
# Kurze Pause für UI-Updates - das Weiter-Button sollte jetzt aktiviert sein
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im E-Mail-Workflow: {e}")
|
||||
return False
|
||||
|
||||
def _fill_email_field(self, email: str) -> bool:
|
||||
"""
|
||||
Füllt das E-Mail-Feld mit robusten Selektoren aus.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste E-Mail-Feld-Selektoren (in Prioritätsreihenfolge)
|
||||
email_selectors = [
|
||||
"input[placeholder*='E-Mail']",
|
||||
"input[placeholder*='Email']",
|
||||
"input[type='email']",
|
||||
"input[name='email']",
|
||||
"input[aria-label*='Email']",
|
||||
"input[aria-label*='E-Mail']",
|
||||
self.selectors.EMAIL_FIELD,
|
||||
self.selectors.EMAIL_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(email_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
success = self.automation.browser.fill_form_field(selector, email, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"E-Mail-Feld erfolgreich ausgefüllt mit Selektor {i+1}: {email}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"E-Mail-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["E-Mail-Adresse", "Email", "E-Mail"],
|
||||
email,
|
||||
email_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"E-Mail-Feld über Fuzzy-Matching ausgefüllt: {email}")
|
||||
return True
|
||||
|
||||
logger.error("Konnte E-Mail-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des E-Mail-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _fill_password_field(self, password: str) -> bool:
|
||||
"""
|
||||
Füllt das Passwort-Feld mit robusten Selektoren aus.
|
||||
|
||||
Args:
|
||||
password: Passwort
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste Passwort-Feld-Selektoren (in Prioritätsreihenfolge)
|
||||
password_selectors = [
|
||||
"input[type='password'][placeholder*='Passwort']",
|
||||
"input[type='password'][placeholder*='Password']",
|
||||
"input[type='password']",
|
||||
"input[name='password']",
|
||||
"input[aria-label*='Password']",
|
||||
"input[aria-label*='Passwort']",
|
||||
self.selectors.PASSWORD_FIELD,
|
||||
self.selectors.PASSWORD_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(password_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
success = self.automation.browser.fill_form_field(selector, password, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"Passwort-Feld erfolgreich ausgefüllt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Passwort-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Passwort", "Password"],
|
||||
password,
|
||||
password_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Passwort-Feld über Fuzzy-Matching ausgefüllt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte Passwort-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Passwort-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _click_send_code_button(self) -> bool:
|
||||
"""
|
||||
Klickt den 'Code senden'-Button mit robusten Selektoren.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Kurze Pause vor dem Klicken
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
|
||||
# Robuste Send-Code-Button-Selektoren
|
||||
send_code_selectors = [
|
||||
"button[data-e2e='send-code-button']",
|
||||
"button:has-text('Code senden')",
|
||||
"button:has-text('Send code')",
|
||||
"button[type='submit']",
|
||||
"button.css-10nhlj9-Button-StyledButton",
|
||||
self.selectors.SEND_CODE_BUTTON
|
||||
]
|
||||
|
||||
for i, selector in enumerate(send_code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Prüfe, ob Button enabled ist
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
||||
if element:
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
if is_disabled:
|
||||
logger.debug(f"Send-Code-Button {i+1} ist disabled, versuche nächsten")
|
||||
continue
|
||||
|
||||
success = self.automation.browser.click_element(selector)
|
||||
if success:
|
||||
logger.info(f"'Code senden'-Button erfolgreich geklickt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Send-Code-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Button-Matching
|
||||
success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Code senden", "Send code", "Senden"],
|
||||
send_code_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("'Code senden'-Button über Fuzzy-Matching geklickt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte 'Code senden'-Button mit keinem Selektor klicken")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken des 'Code senden'-Buttons: {e}")
|
||||
return False
|
||||
|
||||
def _handle_email_verification(self, email: str) -> bool:
|
||||
"""
|
||||
Behandelt die E-Mail-Verifizierung mit verbessertem Timing.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("Warte auf E-Mail-Verifizierungscode...")
|
||||
|
||||
# Warte auf den Code mit exponential backoff
|
||||
verification_code = self._get_email_verification_code_with_retry(email)
|
||||
|
||||
if not verification_code:
|
||||
logger.error("Konnte keinen Verifizierungscode empfangen")
|
||||
return False
|
||||
|
||||
logger.info(f"Verifizierungscode empfangen: {verification_code}")
|
||||
|
||||
# Code-Feld ausfüllen
|
||||
if not self._fill_verification_code_field(verification_code):
|
||||
logger.error("Konnte Verifizierungscode-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
logger.info("Verifizierungscode erfolgreich eingegeben")
|
||||
|
||||
# Kurze Pause nach Code-Eingabe
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der E-Mail-Verifizierung: {e}")
|
||||
return False
|
||||
|
||||
def _get_email_verification_code_with_retry(self, email: str, max_attempts: int = 30) -> Optional[str]:
|
||||
"""
|
||||
Ruft den E-Mail-Verifizierungscode mit Retry-Logik ab.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
max_attempts: Maximale Anzahl Versuche
|
||||
|
||||
Returns:
|
||||
Optional[str]: Verifizierungscode oder None
|
||||
"""
|
||||
try:
|
||||
for attempt in range(max_attempts):
|
||||
# Exponential backoff: 2s, 3s, 4.5s, 6.75s, ... (max 30s)
|
||||
delay = min(2 * (1.5 ** attempt), 30)
|
||||
|
||||
logger.debug(f"E-Mail-Abruf Versuch {attempt + 1}/{max_attempts} (Wartezeit: {delay:.1f}s)")
|
||||
|
||||
# Versuche Code abzurufen
|
||||
code = self.automation.email_handler.get_verification_code(
|
||||
target_email=email,
|
||||
platform="tiktok",
|
||||
max_attempts=1, # Nur ein Versuch pro Iteration
|
||||
delay_seconds=1
|
||||
)
|
||||
|
||||
if code:
|
||||
logger.info(f"E-Mail-Code nach {attempt + 1} Versuchen empfangen")
|
||||
return code
|
||||
|
||||
# Warte vor nächstem Versuch
|
||||
time.sleep(delay)
|
||||
|
||||
logger.warning(f"Kein E-Mail-Code nach {max_attempts} Versuchen empfangen")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim E-Mail-Code-Abruf: {e}")
|
||||
return None
|
||||
|
||||
def _fill_verification_code_field(self, code: str) -> bool:
|
||||
"""
|
||||
Füllt das Verifizierungscode-Feld aus.
|
||||
|
||||
Args:
|
||||
code: Verifizierungscode
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste Verifizierungscode-Feld-Selektoren
|
||||
code_selectors = [
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[placeholder*='verification code']",
|
||||
"input[placeholder*='Code']",
|
||||
"input[name='verificationCode']",
|
||||
"input[type='text'][maxlength='6']",
|
||||
self.selectors.VERIFICATION_CODE_FIELD,
|
||||
self.selectors.VERIFICATION_CODE_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
# Normale Code-Eingabe (Dummy-Trick wird separat angewendet)
|
||||
success = self.automation.browser.fill_form_field(selector, code, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"Verifizierungscode-Feld erfolgreich ausgefüllt mit Selektor {i+1}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Code-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Gib den sechsstelligen Code ein", "Enter verification code", "Verification code"],
|
||||
code,
|
||||
code_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Verifizierungscode-Feld über Fuzzy-Matching ausgefüllt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte Verifizierungscode-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Verifizierungscode-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _execute_phone_workflow(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Führt den Telefon-Registrierungsworkflow aus.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("=== STARTE TELEFON-WORKFLOW ===")
|
||||
|
||||
# Telefonnummer aufbereiten
|
||||
phone_number = account_data["phone"]
|
||||
if phone_number.startswith("+"):
|
||||
parts = phone_number.split(" ", 1)
|
||||
if len(parts) > 1:
|
||||
phone_number = parts[1]
|
||||
|
||||
# Telefonnummer eingeben
|
||||
phone_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Telefonnummer", "Phone number", "Phone"],
|
||||
phone_number,
|
||||
self.selectors.PHONE_FIELD
|
||||
)
|
||||
|
||||
if not phone_success:
|
||||
logger.error("Konnte Telefonnummer-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
logger.info(f"Telefonnummer-Feld ausgefüllt: {phone_number}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
|
||||
# Code senden
|
||||
if not self._click_send_code_button():
|
||||
return False
|
||||
|
||||
# SMS-Code behandeln (Platzhalter)
|
||||
logger.warning("SMS-Verifizierung ist noch nicht vollständig implementiert")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im Telefon-Workflow: {e}")
|
||||
return False
|
||||
|
||||
# Hilfsmethoden für die Basis-Funktionalität
|
||||
def _validate_registration_inputs(self, full_name: str, age: int,
|
||||
registration_method: str, phone_number: str) -> bool:
|
||||
"""Validiert die Eingaben für die Registrierung."""
|
||||
if not full_name or len(full_name) < 3:
|
||||
logger.error("Ungültiger vollständiger Name")
|
||||
return False
|
||||
|
||||
if age < 13:
|
||||
logger.error("Benutzer muss mindestens 13 Jahre alt sein")
|
||||
return False
|
||||
|
||||
if registration_method not in ["email", "phone"]:
|
||||
logger.error(f"Ungültige Registrierungsmethode: {registration_method}")
|
||||
return False
|
||||
|
||||
if registration_method == "phone" and not phone_number:
|
||||
logger.error("Telefonnummer erforderlich für Registrierung via Telefon")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _generate_account_data(self, full_name: str, age: int, registration_method: str,
|
||||
phone_number: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Generiert Account-Daten für die Registrierung."""
|
||||
# Benutzername generieren
|
||||
username = kwargs.get("username")
|
||||
if not username:
|
||||
username = self.automation.username_generator.generate_username("tiktok", full_name)
|
||||
|
||||
# Passwort generieren
|
||||
password = kwargs.get("password")
|
||||
if not password:
|
||||
password = self.automation.password_generator.generate_password("tiktok")
|
||||
|
||||
# E-Mail generieren (falls nötig)
|
||||
email = None
|
||||
if registration_method == "email":
|
||||
email_prefix = username.lower().replace(".", "").replace("_", "")
|
||||
email = f"{email_prefix}@{self.automation.email_domain}"
|
||||
|
||||
# Geburtsdatum generieren
|
||||
birthday = self.automation.birthday_generator.generate_birthday_components("tiktok", age)
|
||||
|
||||
# Account-Daten zusammenstellen
|
||||
account_data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"full_name": full_name,
|
||||
"email": email,
|
||||
"phone": phone_number,
|
||||
"birthday": birthday,
|
||||
"age": age,
|
||||
"registration_method": registration_method
|
||||
}
|
||||
|
||||
logger.debug(f"Account-Daten generiert: {account_data['username']}")
|
||||
return account_data
|
||||
|
||||
def _create_error_result(self, error_msg: str, stage: str, account_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Erstellt ein standardisiertes Fehler-Result."""
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": stage,
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# Platzhalter für weitere Methoden (Navigation, etc.)
|
||||
def _navigate_to_homepage(self) -> bool:
|
||||
"""Navigiert zur TikTok-Startseite."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""Behandelt den Cookie-Banner."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_login_button(self) -> bool:
|
||||
"""Klickt auf den Anmelden-Button."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_register_link(self) -> bool:
|
||||
"""Klickt auf den Registrieren-Link."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_phone_email_option(self) -> bool:
|
||||
"""Klickt auf die Telefon/E-Mail-Option."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _select_registration_method(self, method: str) -> bool:
|
||||
"""Wählt die Registrierungsmethode aus."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _enter_birthday(self, birthday: Dict[str, Any]) -> bool:
|
||||
"""Gibt das Geburtsdatum ein."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _create_username(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""Erstellt einen Benutzernamen."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _check_registration_success(self) -> bool:
|
||||
"""Überprüft, ob die Registrierung erfolgreich war."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _apply_dummy_input_trick(self) -> bool:
|
||||
"""
|
||||
Wendet den Dummy-Input-Trick auf das Code-Feld an.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.debug("Wende Dummy-Input-Trick an")
|
||||
|
||||
# Code-Feld-Selektoren
|
||||
code_selectors = [
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[placeholder*='verification code']",
|
||||
"input[placeholder*='Code']",
|
||||
"input[name='verificationCode']",
|
||||
"input[type='text'][maxlength='6']",
|
||||
self.selectors.VERIFICATION_CODE_FIELD,
|
||||
self.selectors.VERIFICATION_CODE_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Dummy-Input-Trick anwenden
|
||||
success = self.automation.browser.fill_form_field_with_dummy_trick(
|
||||
selector, "123456", timeout=3000
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"Dummy-Input-Trick erfolgreich angewendet mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Dummy-Input-Trick Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
logger.warning("Dummy-Input-Trick konnte nicht angewendet werden")
|
||||
return True # Nicht kritisch - fortfahren
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Kritischer Fehler beim Dummy-Input-Trick: {e}")
|
||||
return True # Nicht kritisch - fortfahren
|
||||
|
||||
def _click_continue_button(self) -> bool:
|
||||
"""
|
||||
Klickt den Weiter/Continue-Button mit robusten Selektoren.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.debug("Klicke Weiter-Button")
|
||||
|
||||
# Robuste Continue-Button-Selektoren
|
||||
continue_selectors = [
|
||||
"button[data-e2e='continue-button']",
|
||||
"button:has-text('Weiter')",
|
||||
"button:has-text('Continue')",
|
||||
"button:has-text('Fortfahren')",
|
||||
"button[type='submit']",
|
||||
"button.css-10nhlj9-Button-StyledButton:not([disabled])"
|
||||
]
|
||||
|
||||
for i, selector in enumerate(continue_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
# Prüfe, ob Button enabled ist
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
||||
if element:
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
aria_disabled = element.get_attribute("aria-disabled")
|
||||
|
||||
if is_disabled or aria_disabled == "true":
|
||||
logger.debug(f"Continue-Button {i+1} ist disabled, versuche nächsten")
|
||||
continue
|
||||
|
||||
# Button klicken
|
||||
success = self.automation.browser.click_element(selector)
|
||||
if success:
|
||||
logger.info(f"Weiter-Button erfolgreich geklickt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Continue-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Button-Matching
|
||||
try:
|
||||
success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Weiter", "Continue", "Fortfahren", "Next"],
|
||||
continue_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Weiter-Button über Fuzzy-Matching geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Continue Fuzzy-Matching fehlgeschlagen: {e}")
|
||||
|
||||
logger.error("Weiter-Button konnte nicht geklickt werden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Kritischer Fehler beim Weiter-Button: {e}")
|
||||
return False
|
||||
225
social_networks/tiktok/tiktok_selectors.py
Normale Datei
225
social_networks/tiktok/tiktok_selectors.py
Normale Datei
@ -0,0 +1,225 @@
|
||||
"""
|
||||
TikTok-Selektoren - CSS-Selektoren und XPath-Ausdrücke für die TikTok-Automatisierung
|
||||
Mit Text-Matching-Funktionen für robuste Element-Erkennung
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
class TikTokSelectors:
|
||||
"""
|
||||
Zentrale Sammlung aller Selektoren für die TikTok-Automatisierung.
|
||||
Bei Änderungen der TikTok-Webseite müssen nur hier Anpassungen vorgenommen werden.
|
||||
Enthält auch Fuzzy-Text-Matching-Daten für robustere Element-Erkennung.
|
||||
"""
|
||||
|
||||
# URL-Konstanten
|
||||
BASE_URL = "https://www.tiktok.com"
|
||||
SIGNUP_URL = "https://www.tiktok.com/signup"
|
||||
LOGIN_URL = "https://www.tiktok.com/login"
|
||||
EXPLORE_URL = "https://www.tiktok.com/explore"
|
||||
|
||||
# Anmelden/Registrieren-Buttons Hauptseite
|
||||
LOGIN_BUTTON = "button#header-login-button"
|
||||
LOGIN_BUTTON_HEADER = "button#header-login-button"
|
||||
LOGIN_BUTTON_CLASS = "button.TUXButton:has-text('Anmelden')"
|
||||
LOGIN_BUTTON_LEFT = "button#header-login-button"
|
||||
LOGIN_BUTTON_RIGHT = "button#top-right-action-bar-login-button"
|
||||
LOGIN_BUTTON_TOP = "button#header-login-button"
|
||||
LOGIN_BUTTON_TOP_RIGHT = "button#top-right-action-bar-login-button"
|
||||
LOGIN_BUTTON_SIDEBAR = "button[data-e2e='login-button-sidebar']"
|
||||
LOGIN_BUTTON_FALLBACK = "//button[contains(text(), 'Anmelden')]"
|
||||
SIGNUP_LINK = "span[data-e2e='bottom-sign-up']"
|
||||
SIGNUP_LINK_FALLBACK = "a[href*='/signup']"
|
||||
|
||||
# Login-Dialog Optionen
|
||||
LOGIN_DIALOG = "div[role='dialog']"
|
||||
LOGIN_EMAIL_PHONE_OPTION = "div[data-e2e='channel-item']"
|
||||
LOGIN_EMAIL_USERNAME_LINK = "a[href='/login/phone-or-email/email']"
|
||||
|
||||
# Cookie-Dialog
|
||||
COOKIE_DIALOG = "div[role='dialog'][data-testid='cookie-banner']"
|
||||
COOKIE_ACCEPT_BUTTON = "button[data-testid='accept-all-cookies']"
|
||||
|
||||
# Registrierungsdialog - Methoden
|
||||
REGISTER_DIALOG_TITLE = "h1:contains('Registrieren')"
|
||||
REGISTER_LINK = "a:contains('Registrieren')"
|
||||
REGISTER_LINK_FALLBACK = "//a[contains(text(), 'Registrieren')]"
|
||||
REGISTRATION_DIALOG = "div[role='dialog']"
|
||||
PHONE_EMAIL_BUTTON = "div[data-e2e='channel-item']"
|
||||
PHONE_EMAIL_OPTION = "div[data-e2e='channel-item']"
|
||||
PHONE_EMAIL_OPTION_FALLBACK = "//div[contains(text(), 'Telefonnummer oder E-Mail')]"
|
||||
EMAIL_OPTION = "a[href*='/signup/phone-or-email/email']"
|
||||
EMAIL_OPTION_FALLBACK = "//a[contains(text(), 'E-Mail')]"
|
||||
PHONE_OPTION = "a[href*='/signup/phone-or-email/phone']"
|
||||
PHONE_OPTION_FALLBACK = "//a[contains(text(), 'Telefon')]"
|
||||
REGISTER_WITH_EMAIL = "a[href*='/signup/phone-or-email/email']"
|
||||
REGISTER_WITH_PHONE = "a[href*='/signup/phone-or-email/phone']"
|
||||
|
||||
# Geburtsdatum-Selektoren
|
||||
BIRTHDAY_MONTH_DROPDOWN = "select[name='month']"
|
||||
BIRTHDAY_DAY_DROPDOWN = "select[name='day']"
|
||||
BIRTHDAY_YEAR_DROPDOWN = "select[name='year']"
|
||||
BIRTHDAY_MONTH_SELECT = "div.css-1leicpq-DivSelectLabel:contains('Monat')"
|
||||
BIRTHDAY_DAY_SELECT = "div.css-1leicpq-DivSelectLabel:contains('Tag')"
|
||||
BIRTHDAY_YEAR_SELECT = "div.css-1leicpq-DivSelectLabel:contains('Jahr')"
|
||||
BIRTHDAY_DROPDOWN_OPTION = "div[role='option']"
|
||||
BIRTHDAY_DROPDOWN_CONTAINER = "div.css-1leicpq-DivSelectLabel"
|
||||
BIRTHDAY_ARROW = "svg.css-gz151e-StyledArrowTriangleDownLargeFill"
|
||||
|
||||
# Formularfelder - E-Mail-Registrierung
|
||||
EMAIL_FIELD = "input[placeholder='E-Mail-Adresse']"
|
||||
EMAIL_FIELD_ALT = "input[name='email']"
|
||||
PASSWORD_FIELD = "input[placeholder='Passwort']"
|
||||
PASSWORD_FIELD_ALT = "input[type='password']"
|
||||
VERIFICATION_CODE_FIELD = "input[placeholder*='sechsstelligen Code']"
|
||||
VERIFICATION_CODE_FIELD_ALT = "input[placeholder='Gib den sechsstelligen Code ein']"
|
||||
USERNAME_FIELD = "input[placeholder='Benutzername']"
|
||||
USERNAME_FIELD_ALT = "input[name='new-username']"
|
||||
|
||||
# Formularfelder - Telefon-Registrierung
|
||||
COUNTRY_CODE_SELECT = "div[role='combobox']"
|
||||
PHONE_FIELD = "input[placeholder='Telefonnummer']"
|
||||
|
||||
# Buttons
|
||||
SEND_CODE_BUTTON = "button[data-e2e='send-code-button']"
|
||||
RESEND_CODE_BUTTON = "button:contains('Code erneut senden')"
|
||||
CONTINUE_BUTTON = "button[type='submit']"
|
||||
CONTINUE_BUTTON_ALT = "button.e1w6iovg0"
|
||||
REGISTER_BUTTON = "button:contains('Registrieren')"
|
||||
REGISTER_BUTTON_ALT = "button[type='submit']:contains('Registrieren')"
|
||||
SKIP_BUTTON = "div:contains('Überspringen')"
|
||||
SKIP_BUTTON_ALT = "div.css-4y1w75-DivTextContainer"
|
||||
SKIP_USERNAME_BUTTON = "button:contains('Überspringen')"
|
||||
|
||||
# Checkbox
|
||||
NEWSLETTER_CHECKBOX = "input[type='checkbox']"
|
||||
|
||||
# Login-Formularfelder
|
||||
LOGIN_EMAIL_FIELD = "input[name='username'][placeholder='E-Mail-Adresse oder Benutzername']"
|
||||
LOGIN_EMAIL_FIELD_ALT = "input.tiktok-11to27l-InputContainer[name='username']"
|
||||
LOGIN_PASSWORD_FIELD = "input[type='password'][placeholder='Passwort']"
|
||||
LOGIN_PASSWORD_FIELD_ALT = "input.tiktok-wv3bkt-InputContainer[type='password']"
|
||||
LOGIN_SUBMIT_BUTTON = "button[type='submit'][data-e2e='login-button']"
|
||||
LOGIN_SUBMIT_BUTTON_ALT = "button.tiktok-11sviba-Button-StyledButton[data-e2e='login-button']"
|
||||
|
||||
# Erfolgs-Indikatoren für Registrierung
|
||||
SUCCESS_INDICATORS = [
|
||||
"a[href='/foryou']",
|
||||
"a[href='/explore']",
|
||||
"button[data-e2e='profile-icon']",
|
||||
"svg[data-e2e='profile-icon']"
|
||||
]
|
||||
|
||||
# Links für Nutzungsbedingungen und Datenschutz
|
||||
TERMS_LINK = "a:contains('Nutzungsbedingungen')"
|
||||
PRIVACY_LINK = "a:contains('Datenschutzerklärung')"
|
||||
|
||||
# Login-Fehler
|
||||
ERROR_MESSAGE = "span.error-message"
|
||||
LOGIN_ERROR_CONTAINER = "div[class*='error']"
|
||||
|
||||
# Text-Matching-Parameter für Fuzzy-Matching
|
||||
TEXT_MATCH = {
|
||||
# Formularfelder
|
||||
"form_fields": {
|
||||
"email": ["E-Mail-Adresse", "E-Mail", "Email", "Mail"],
|
||||
"phone": ["Telefonnummer", "Telefon", "Phone", "Mobile"],
|
||||
"password": ["Passwort", "Password"],
|
||||
"verification_code": ["Bestätigungscode", "Code", "Verifizierungscode", "Sicherheitscode"],
|
||||
"username": ["Benutzername", "Username", "Name"]
|
||||
},
|
||||
|
||||
# Buttons
|
||||
"buttons": {
|
||||
"send_code": ["Code senden", "Senden", "Send code", "Verification code", "Send"],
|
||||
"continue": ["Weiter", "Continue", "Next", "Fortfahren"],
|
||||
"register": ["Registrieren", "Register", "Sign up", "Konto erstellen"],
|
||||
"skip": ["Überspringen", "Skip", "Later", "Später", "Nicht jetzt"],
|
||||
},
|
||||
|
||||
# Fehler-Indikatoren
|
||||
"error_indicators": [
|
||||
"Fehler", "Error", "Leider", "Ungültig", "Invalid", "Nicht verfügbar",
|
||||
"Fehlgeschlagen", "Problem", "Failed", "Nicht möglich", "Bereits verwendet",
|
||||
"Too many attempts", "Zu viele Versuche", "Rate limit", "Bitte warte"
|
||||
],
|
||||
|
||||
# Bestätigungscode-Texte in E-Mails
|
||||
"email_verification_patterns": [
|
||||
"ist dein Bestätigungscode",
|
||||
"ist dein TikTok-Code",
|
||||
"is your TikTok code",
|
||||
"is your verification code",
|
||||
"Dein Bestätigungscode lautet",
|
||||
"Your verification code is"
|
||||
],
|
||||
|
||||
# E-Mail-Betreff-Muster für TikTok
|
||||
"email_subject_patterns": [
|
||||
"ist dein Bestätigungscode",
|
||||
"is your confirmation code",
|
||||
"TikTok verification code",
|
||||
"TikTok Bestätigungscode"
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_field_labels(cls, field_type: str) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Bezeichnungen für ein Formularfeld zurück.
|
||||
|
||||
Args:
|
||||
field_type: Typ des Formularfelds (z.B. "email", "phone")
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Bezeichnungen
|
||||
"""
|
||||
return cls.TEXT_MATCH["form_fields"].get(field_type, [])
|
||||
|
||||
@classmethod
|
||||
def get_button_texts(cls, button_type: str) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für einen Button zurück.
|
||||
|
||||
Args:
|
||||
button_type: Typ des Buttons (z.B. "send_code", "continue")
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Button-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["buttons"].get(button_type, [])
|
||||
|
||||
@classmethod
|
||||
def get_error_indicators(cls) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für Fehlerindikatoren zurück.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen Fehlerindikator-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["error_indicators"]
|
||||
|
||||
@classmethod
|
||||
def get_email_verification_patterns(cls) -> List[str]:
|
||||
"""
|
||||
Gibt die möglichen Texte für Bestätigungscodes in E-Mails zurück.
|
||||
|
||||
Returns:
|
||||
List[str]: Liste mit möglichen E-Mail-Bestätigungscode-Texten
|
||||
"""
|
||||
return cls.TEXT_MATCH["email_verification_patterns"]
|
||||
|
||||
@classmethod
|
||||
def get_month_option_selector(cls, month: int) -> str:
|
||||
"""Returns selector for month option."""
|
||||
return f"option[value='{month}']"
|
||||
|
||||
@classmethod
|
||||
def get_day_option_selector(cls, day: int) -> str:
|
||||
"""Returns selector for day option."""
|
||||
return f"option[value='{day}']"
|
||||
|
||||
@classmethod
|
||||
def get_year_option_selector(cls, year: int) -> str:
|
||||
"""Returns selector for year option."""
|
||||
return f"option[value='{year}']"
|
||||
520
social_networks/tiktok/tiktok_ui_helper.py
Normale Datei
520
social_networks/tiktok/tiktok_ui_helper.py
Normale Datei
@ -0,0 +1,520 @@
|
||||
"""
|
||||
TikTok-UI-Helper - Hilfsmethoden für die Interaktion mit der TikTok-UI
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union, Callable
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_ui_helper")
|
||||
|
||||
class TikTokUIHelper:
|
||||
"""
|
||||
Hilfsmethoden für die Interaktion mit der TikTok-Benutzeroberfläche.
|
||||
Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert den TikTok-UI-Helper.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = TikTokSelectors()
|
||||
|
||||
# Initialisiere TextSimilarity für Fuzzy-Matching
|
||||
self.text_similarity = TextSimilarity(default_threshold=0.7)
|
||||
|
||||
logger.debug("TikTok-UI-Helper initialisiert")
|
||||
|
||||
def _ensure_browser(self) -> bool:
|
||||
"""
|
||||
Stellt sicher, dass die Browser-Referenz verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Browser verfügbar, False sonst
|
||||
"""
|
||||
if self.automation.browser is None:
|
||||
logger.error("Browser-Referenz nicht verfügbar")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def fill_field_fuzzy(self, field_labels: Union[str, List[str]],
|
||||
value: str, fallback_selector: str = None,
|
||||
threshold: float = 0.7, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Füllt ein Formularfeld mit Fuzzy-Text-Matching aus.
|
||||
|
||||
Args:
|
||||
field_labels: Bezeichner oder Liste von Bezeichnern des Feldes
|
||||
value: Einzugebender Wert
|
||||
fallback_selector: CSS-Selektor für Fallback
|
||||
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
||||
timeout: Zeitlimit für die Suche in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Normalisiere field_labels zu einer Liste
|
||||
if isinstance(field_labels, str):
|
||||
field_labels = [field_labels]
|
||||
|
||||
# Versuche, das Feld mit Fuzzy-Matching zu finden
|
||||
element = fuzzy_find_element(
|
||||
self.automation.browser.page,
|
||||
field_labels,
|
||||
selector_type="input",
|
||||
threshold=threshold,
|
||||
wait_time=timeout
|
||||
)
|
||||
|
||||
if element:
|
||||
# Versuche, das Feld zu fokussieren und den Wert einzugeben
|
||||
element.focus()
|
||||
time.sleep(0.1)
|
||||
element.fill("") # Leere das Feld zuerst
|
||||
time.sleep(0.2)
|
||||
|
||||
# Text menschenähnlich eingeben
|
||||
for char in value:
|
||||
element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000)
|
||||
time.sleep(0.01)
|
||||
|
||||
logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}")
|
||||
return True
|
||||
|
||||
# Fuzzy-Matching fehlgeschlagen, versuche über Attribute
|
||||
if fallback_selector:
|
||||
field_success = self.automation.browser.fill_form_field(fallback_selector, value)
|
||||
if field_success:
|
||||
logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}")
|
||||
return True
|
||||
|
||||
# Versuche noch alternative Selektoren basierend auf field_labels
|
||||
for label in field_labels:
|
||||
# Versuche aria-label Attribut
|
||||
aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']"
|
||||
if self.automation.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(aria_selector, value):
|
||||
logger.info(f"Feld über aria-label gefüllt: {label}")
|
||||
return True
|
||||
|
||||
# Versuche placeholder Attribut
|
||||
placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']"
|
||||
if self.automation.browser.is_element_visible(placeholder_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(placeholder_selector, value):
|
||||
logger.info(f"Feld über placeholder gefüllt: {label}")
|
||||
return True
|
||||
|
||||
# Versuche name Attribut
|
||||
name_selector = f"input[name='{label.lower().replace(' ', '')}']"
|
||||
if self.automation.browser.is_element_visible(name_selector, timeout=1000):
|
||||
if self.automation.browser.fill_form_field(name_selector, value):
|
||||
logger.info(f"Feld über name-Attribut gefüllt: {label}")
|
||||
return True
|
||||
|
||||
logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {e}")
|
||||
return False
|
||||
|
||||
def click_button_fuzzy(self, button_texts: Union[str, List[str]],
|
||||
fallback_selector: str = None, threshold: float = 0.7,
|
||||
timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Klickt einen Button mit Fuzzy-Text-Matching.
|
||||
|
||||
Args:
|
||||
button_texts: Text oder Liste von Texten des Buttons
|
||||
fallback_selector: CSS-Selektor für Fallback
|
||||
threshold: Schwellenwert für die Textähnlichkeit (0-1)
|
||||
timeout: Zeitlimit für die Suche in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Normalisiere button_texts zu einer Liste
|
||||
if isinstance(button_texts, str):
|
||||
button_texts = [button_texts]
|
||||
|
||||
# Logging der Suche
|
||||
logger.info(f"Suche nach Button mit Texten: {button_texts}")
|
||||
|
||||
if not button_texts or button_texts == [[]]:
|
||||
logger.warning("Leere Button-Text-Liste angegeben!")
|
||||
return False
|
||||
|
||||
# TikTok-spezifische Selektoren zuerst prüfen
|
||||
# Diese Selektoren sind häufig in TikTok's UI zu finden
|
||||
tiktok_button_selectors = [
|
||||
"button[type='submit']",
|
||||
"button[data-e2e='send-code-button']",
|
||||
"button.e1w6iovg0",
|
||||
"button.css-10nhlj9-Button-StyledButton"
|
||||
]
|
||||
|
||||
for selector in tiktok_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
button_element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
||||
if button_element:
|
||||
button_text = button_element.inner_text().strip()
|
||||
|
||||
# Überprüfe, ob der Button-Text mit einem der gesuchten Texte übereinstimmt
|
||||
for text in button_texts:
|
||||
if self.text_similarity.is_similar(text, button_text, threshold=threshold):
|
||||
logger.info(f"Button mit passendem Text gefunden: '{button_text}'")
|
||||
button_element.click()
|
||||
return True
|
||||
|
||||
# Die allgemeine fuzzy_click_button-Funktion verwenden
|
||||
result = click_fuzzy_button(
|
||||
self.automation.browser.page,
|
||||
button_texts,
|
||||
threshold=threshold,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info(f"Button mit Fuzzy-Matching geklickt")
|
||||
return True
|
||||
|
||||
# Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector
|
||||
if fallback_selector:
|
||||
logger.info(f"Versuche Fallback-Selektor: {fallback_selector}")
|
||||
if self.automation.browser.click_element(fallback_selector):
|
||||
logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}")
|
||||
return True
|
||||
|
||||
# Versuche alternative Methoden
|
||||
|
||||
# 1. Versuche über aria-label
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']"
|
||||
if self.automation.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.automation.browser.click_element(aria_selector):
|
||||
logger.info(f"Button über aria-label geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 2. Versuche über role='button' mit Text
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
xpath_selector = f"//div[@role='button' and contains(., '{text}')]"
|
||||
if self.automation.browser.is_element_visible(xpath_selector, timeout=1000):
|
||||
if self.automation.browser.click_element(xpath_selector):
|
||||
logger.info(f"Button über role+text geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 3. Versuche über Link-Text
|
||||
for text in button_texts:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
link_selector = f"//a[contains(text(), '{text}')]"
|
||||
if self.automation.browser.is_element_visible(link_selector, timeout=1000):
|
||||
if self.automation.browser.click_element(link_selector):
|
||||
logger.info(f"Link mit passendem Text geklickt: {text}")
|
||||
return True
|
||||
|
||||
# 4. Als letzten Versuch, klicke auf einen beliebigen Button
|
||||
logger.warning("Kein spezifischer Button gefunden, versuche beliebigen Button zu klicken")
|
||||
buttons = self.automation.browser.page.query_selector_all("button")
|
||||
if buttons and len(buttons) > 0:
|
||||
for button in buttons:
|
||||
visible = button.is_visible()
|
||||
if visible:
|
||||
logger.info("Klicke auf beliebigen sichtbaren Button")
|
||||
button.click()
|
||||
return True
|
||||
|
||||
logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}")
|
||||
return False
|
||||
|
||||
def select_dropdown_option(self, dropdown_selector: str, option_value: str,
|
||||
option_type: str = "text", timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Wählt eine Option aus einer Dropdown-Liste aus.
|
||||
|
||||
Args:
|
||||
dropdown_selector: Selektor für das Dropdown-Element
|
||||
option_value: Wert oder Text der auszuwählenden Option
|
||||
option_type: "text" für Text-Matching, "value" für Wert-Matching
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Auf Dropdown-Element klicken, um die Optionen anzuzeigen
|
||||
dropdown_element = self.automation.browser.wait_for_selector(dropdown_selector, timeout=timeout)
|
||||
if not dropdown_element:
|
||||
logger.warning(f"Dropdown-Element nicht gefunden: {dropdown_selector}")
|
||||
return False
|
||||
|
||||
# Dropdown öffnen
|
||||
dropdown_element.click()
|
||||
time.sleep(0.5) # Kurz warten, damit die Optionen angezeigt werden
|
||||
|
||||
# Optionen suchen
|
||||
option_selector = "div[role='option']"
|
||||
options = self.automation.browser.page.query_selector_all(option_selector)
|
||||
|
||||
if not options or len(options) == 0:
|
||||
logger.warning(f"Keine Optionen gefunden für Dropdown: {dropdown_selector}")
|
||||
return False
|
||||
|
||||
# Option nach Text oder Wert suchen
|
||||
selected = False
|
||||
for option in options:
|
||||
option_text = option.inner_text().strip()
|
||||
|
||||
if option_type == "text":
|
||||
if option_text == option_value or self.text_similarity.is_similar(option_text, option_value, threshold=0.9):
|
||||
option.click()
|
||||
selected = True
|
||||
break
|
||||
elif option_type == "value":
|
||||
option_val = option.get_attribute("value") or ""
|
||||
if option_val == option_value:
|
||||
option.click()
|
||||
selected = True
|
||||
break
|
||||
|
||||
if not selected:
|
||||
logger.warning(f"Keine passende Option für '{option_value}' gefunden")
|
||||
return False
|
||||
|
||||
logger.info(f"Option '{option_value}' im Dropdown ausgewählt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Auswahl der Dropdown-Option: {e}")
|
||||
return False
|
||||
|
||||
def check_for_error(self, error_selectors: List[str] = None,
|
||||
error_texts: List[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Überprüft, ob Fehlermeldungen angezeigt werden.
|
||||
|
||||
Args:
|
||||
error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen
|
||||
error_texts: Liste mit typischen Fehlertexten
|
||||
|
||||
Returns:
|
||||
Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Standardselektoren verwenden, wenn keine angegeben sind
|
||||
if error_selectors is None:
|
||||
error_selectors = [
|
||||
"div[role='alert']",
|
||||
"p[class*='error']",
|
||||
"span[class*='error']",
|
||||
".error-message"
|
||||
]
|
||||
|
||||
# Standardfehlertexte verwenden, wenn keine angegeben sind
|
||||
if error_texts is None:
|
||||
error_texts = TikTokSelectors.get_error_indicators()
|
||||
|
||||
# 1. Nach Fehlerselektoren suchen
|
||||
for selector in error_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
error_text = element.text_content()
|
||||
if error_text and len(error_text.strip()) > 0:
|
||||
logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}")
|
||||
return error_text.strip()
|
||||
|
||||
# 2. Alle Texte auf der Seite durchsuchen
|
||||
page_content = self.automation.browser.page.content()
|
||||
|
||||
for error_text in error_texts:
|
||||
if error_text.lower() in page_content.lower():
|
||||
# Versuche, den genauen Fehlertext zu extrahieren
|
||||
matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower())
|
||||
if matches:
|
||||
full_error = matches[0].strip()
|
||||
logger.info(f"Fehlermeldung gefunden (Text): {full_error}")
|
||||
return full_error
|
||||
else:
|
||||
logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}")
|
||||
return error_text
|
||||
|
||||
# 3. Nach weiteren Fehlerelementen suchen
|
||||
elements = self.automation.browser.page.query_selector_all("p, div, span")
|
||||
|
||||
for element in elements:
|
||||
element_text = element.inner_text()
|
||||
if not element_text:
|
||||
continue
|
||||
|
||||
element_text = element_text.strip()
|
||||
|
||||
# Prüfe Textähnlichkeit mit Fehlertexten
|
||||
for error_text in error_texts:
|
||||
if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \
|
||||
self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7):
|
||||
logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}")
|
||||
return element_text
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}")
|
||||
return None
|
||||
|
||||
def check_for_captcha(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob ein Captcha angezeigt wird.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Captcha erkannt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Selektoren für Captcha-Erkennung
|
||||
captcha_selectors = [
|
||||
"div[data-testid='captcha']",
|
||||
"iframe[src*='captcha']",
|
||||
"iframe[title*='captcha']",
|
||||
"iframe[title*='reCAPTCHA']"
|
||||
]
|
||||
|
||||
# Captcha-Texte für textbasierte Erkennung
|
||||
captcha_texts = [
|
||||
"captcha", "recaptcha", "sicherheitsüberprüfung", "security check",
|
||||
"i'm not a robot", "ich bin kein roboter", "verify you're human",
|
||||
"bestätige, dass du ein mensch bist"
|
||||
]
|
||||
|
||||
# Nach Selektoren suchen
|
||||
for selector in captcha_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.warning(f"Captcha erkannt (Selektor): {selector}")
|
||||
return True
|
||||
|
||||
# Nach Texten suchen
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for text in captcha_texts:
|
||||
if text in page_content:
|
||||
logger.warning(f"Captcha erkannt (Text): {text}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Captcha-Erkennung: {e}")
|
||||
return False
|
||||
|
||||
def wait_for_element(self, selectors: Union[str, List[str]],
|
||||
timeout: int = 10000, check_interval: int = 500) -> Optional[Any]:
|
||||
"""
|
||||
Wartet auf das Erscheinen eines Elements.
|
||||
|
||||
Args:
|
||||
selectors: CSS-Selektor oder Liste von Selektoren
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
||||
|
||||
Returns:
|
||||
Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Normalisiere selectors zu einer Liste
|
||||
if isinstance(selectors, str):
|
||||
selectors = [selectors]
|
||||
|
||||
start_time = time.time()
|
||||
end_time = start_time + (timeout / 1000)
|
||||
|
||||
while time.time() < end_time:
|
||||
for selector in selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=check_interval)
|
||||
if element:
|
||||
logger.info(f"Element mit Selektor '{selector}' gefunden")
|
||||
return element
|
||||
|
||||
# Kurze Pause vor der nächsten Prüfung
|
||||
time.sleep(check_interval / 1000)
|
||||
|
||||
logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Warten auf Element: {e}")
|
||||
return None
|
||||
|
||||
def is_registration_successful(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob die Registrierung erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich, False sonst
|
||||
"""
|
||||
try:
|
||||
# Erfolgsindikatoren überprüfen
|
||||
success_indicators = TikTokSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for selector in success_indicators:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Registrierung erfolgreich (Indikator gefunden: {selector})")
|
||||
return True
|
||||
|
||||
# URL überprüfen
|
||||
current_url = self.automation.browser.page.url
|
||||
if "/foryou" in current_url or "tiktok.com/explore" in current_url:
|
||||
logger.info("Registrierung erfolgreich (Erfolgreiche Navigation erkannt)")
|
||||
return True
|
||||
|
||||
# Überprüfen, ob Fehler angezeigt werden
|
||||
error_message = self.check_for_error()
|
||||
if error_message:
|
||||
logger.warning(f"Registrierung nicht erfolgreich: {error_message}")
|
||||
return False
|
||||
|
||||
logger.warning("Konnte Registrierungserfolg nicht bestätigen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung des Registrierungserfolgs: {e}")
|
||||
return False
|
||||
492
social_networks/tiktok/tiktok_utils.py
Normale Datei
492
social_networks/tiktok/tiktok_utils.py
Normale Datei
@ -0,0 +1,492 @@
|
||||
"""
|
||||
TikTok-Utils - Hilfsfunktionen für die TikTok-Automatisierung.
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_utils")
|
||||
|
||||
class TikTokUtils:
|
||||
"""
|
||||
Hilfsfunktionen für die TikTok-Automatisierung.
|
||||
Enthält allgemeine Hilfsmethoden und kleinere Funktionen.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Utils.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = TikTokSelectors()
|
||||
|
||||
logger.debug("TikTok-Utils initialisiert")
|
||||
|
||||
def _ensure_browser(self) -> bool:
|
||||
"""
|
||||
Stellt sicher, dass die Browser-Referenz verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Browser verfügbar, False sonst
|
||||
"""
|
||||
if self.automation.browser is None:
|
||||
logger.error("Browser-Referenz nicht verfügbar")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Cookie-Dialoge in TikTok prüfen
|
||||
cookie_selectors = [
|
||||
"button[data-e2e='cookie-banner-reject']",
|
||||
"button:contains('Ablehnen')",
|
||||
"button:contains('Nur erforderliche')",
|
||||
"button:contains('Reject')",
|
||||
"button[data-e2e='cookie-banner-accept']"
|
||||
]
|
||||
|
||||
for selector in cookie_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Cookie-Banner erkannt: {selector}")
|
||||
|
||||
# Versuche, den Ablehnen-Button zu klicken
|
||||
if "reject" in selector.lower() or "ablehnen" in selector.lower() or "erforderliche" in selector.lower():
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Cookie-Banner erfolgreich abgelehnt")
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
return True
|
||||
|
||||
# Fallback: Akzeptieren-Button klicken, wenn Ablehnen nicht funktioniert
|
||||
else:
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Cookie-Banner erfolgreich akzeptiert")
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
return True
|
||||
|
||||
# Wenn kein Cookie-Banner gefunden wurde
|
||||
logger.debug("Kein Cookie-Banner erkannt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Behandeln des Cookie-Banners: {e}")
|
||||
return False
|
||||
|
||||
def extract_username_from_url(self, url: str) -> Optional[str]:
|
||||
"""
|
||||
Extrahiert den Benutzernamen aus einer TikTok-URL.
|
||||
|
||||
Args:
|
||||
url: Die TikTok-URL
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der extrahierte Benutzername oder None
|
||||
"""
|
||||
try:
|
||||
# Muster für Profil-URLs
|
||||
patterns = [
|
||||
r'tiktok\.com/@([a-zA-Z0-9._]+)/?(?:$|\?|#)',
|
||||
r'tiktok\.com/user/([a-zA-Z0-9._]+)/?',
|
||||
r'tiktok\.com/video/[^/]+/by/([a-zA-Z0-9._]+)/?'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
username = match.group(1)
|
||||
# Einige Ausnahmen filtern
|
||||
if username not in ["explore", "accounts", "video", "foryou", "trending"]:
|
||||
return username
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren des Benutzernamens aus der URL: {e}")
|
||||
return None
|
||||
|
||||
def get_current_username(self) -> Optional[str]:
|
||||
"""
|
||||
Versucht, den Benutzernamen des aktuell angemeldeten Kontos zu ermitteln.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der Benutzername oder None, wenn nicht gefunden
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Verschiedene Methoden zur Erkennung des Benutzernamens
|
||||
|
||||
# 1. Benutzername aus URL des Profils
|
||||
profile_link_selectors = [
|
||||
"a[href*='/@']",
|
||||
"a[href*='/user/']"
|
||||
]
|
||||
|
||||
for selector in profile_link_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
href = element.get_attribute("href")
|
||||
if href:
|
||||
username = self.extract_username_from_url(href)
|
||||
if username:
|
||||
logger.info(f"Benutzername aus Profil-Link ermittelt: {username}")
|
||||
return username
|
||||
|
||||
# 2. Profilicon prüfen auf data-e2e-Attribut
|
||||
profile_icon_selectors = [
|
||||
"button[data-e2e='profile-icon']",
|
||||
"svg[data-e2e='profile-icon']"
|
||||
]
|
||||
|
||||
for selector in profile_icon_selectors:
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
# Prüfen, ob ein Elternelement möglicherweise ein data-e2e-Attribut mit dem Benutzernamen hat
|
||||
parent = element.evaluate("node => node.parentElement")
|
||||
if parent:
|
||||
data_e2e = parent.get_attribute("data-e2e")
|
||||
if data_e2e and "profile" in data_e2e:
|
||||
username_match = re.search(r'profile-([a-zA-Z0-9._]+)', data_e2e)
|
||||
if username_match:
|
||||
username = username_match.group(1)
|
||||
logger.info(f"Benutzername aus data-e2e-Attribut ermittelt: {username}")
|
||||
return username
|
||||
|
||||
# 3. TikTok-spezifisches Element mit Benutzername suchen
|
||||
username_element = self.automation.browser.wait_for_selector("h1[data-e2e='user-title']", timeout=2000)
|
||||
if username_element:
|
||||
username = username_element.inner_text().strip()
|
||||
if username:
|
||||
logger.info(f"Benutzername aus user-title-Element ermittelt: {username}")
|
||||
return username
|
||||
|
||||
logger.warning("Konnte Benutzernamen nicht ermitteln")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Ermittlung des Benutzernamens: {e}")
|
||||
return None
|
||||
|
||||
def wait_for_navigation(self, expected_url_pattern: str = None,
|
||||
timeout: int = 30000, check_interval: int = 500) -> bool:
|
||||
"""
|
||||
Wartet, bis die Seite zu einer URL mit einem bestimmten Muster navigiert.
|
||||
|
||||
Args:
|
||||
expected_url_pattern: Erwartetes Muster der URL (Regex)
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn die Navigation erfolgreich war, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
end_time = start_time + (timeout / 1000)
|
||||
|
||||
while time.time() < end_time:
|
||||
current_url = self.automation.browser.page.url
|
||||
|
||||
if expected_url_pattern and re.search(expected_url_pattern, current_url):
|
||||
logger.info(f"Navigation zu URL mit Muster '{expected_url_pattern}' erfolgreich")
|
||||
return True
|
||||
|
||||
# Kurze Pause vor der nächsten Prüfung
|
||||
time.sleep(check_interval / 1000)
|
||||
|
||||
logger.warning(f"Zeitüberschreitung bei Navigation zu URL mit Muster '{expected_url_pattern}'")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Warten auf Navigation: {e}")
|
||||
return False
|
||||
|
||||
def handle_dialog_or_popup(self, expected_text: Union[str, List[str]] = None,
|
||||
action: str = "close", timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Behandelt einen Dialog oder Popup.
|
||||
|
||||
Args:
|
||||
expected_text: Erwarteter Text im Dialog oder Liste von Texten
|
||||
action: Aktion ("close", "confirm", "cancel")
|
||||
timeout: Zeitlimit in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn der Dialog erfolgreich behandelt wurde, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Dialog-Element suchen
|
||||
dialog_selector = "div[role='dialog']"
|
||||
dialog_element = self.automation.browser.wait_for_selector(dialog_selector, timeout=timeout)
|
||||
|
||||
if not dialog_element:
|
||||
logger.debug("Kein Dialog gefunden")
|
||||
return False
|
||||
|
||||
logger.info("Dialog gefunden")
|
||||
|
||||
# Text im Dialog prüfen, falls angegeben
|
||||
if expected_text:
|
||||
if isinstance(expected_text, str):
|
||||
expected_text = [expected_text]
|
||||
|
||||
dialog_text = dialog_element.inner_text()
|
||||
text_found = False
|
||||
|
||||
for text in expected_text:
|
||||
if text in dialog_text:
|
||||
logger.info(f"Erwarteter Text im Dialog gefunden: '{text}'")
|
||||
text_found = True
|
||||
break
|
||||
|
||||
if not text_found:
|
||||
logger.warning(f"Erwarteter Text nicht im Dialog gefunden: {expected_text}")
|
||||
return False
|
||||
|
||||
# Aktion ausführen
|
||||
if action == "close":
|
||||
# Schließen-Button suchen und klicken
|
||||
close_button_selectors = [
|
||||
"button[data-e2e='modal-close']",
|
||||
"svg[data-e2e='modal-close']",
|
||||
"button.css-1afoydx-StyledCloseButton",
|
||||
"div[role='dialog'] button:first-child"
|
||||
]
|
||||
|
||||
for selector in close_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Dialog geschlossen")
|
||||
return True
|
||||
|
||||
# Wenn kein Schließen-Button gefunden wurde, Escape-Taste drücken
|
||||
self.automation.browser.page.keyboard.press("Escape")
|
||||
logger.info("Dialog mit Escape-Taste geschlossen")
|
||||
|
||||
elif action == "confirm":
|
||||
# Bestätigen-Button suchen und klicken
|
||||
confirm_button_selectors = [
|
||||
"button[type='submit']",
|
||||
"button:contains('OK')",
|
||||
"button:contains('Ja')",
|
||||
"button:contains('Yes')",
|
||||
"button:contains('Bestätigen')",
|
||||
"button:contains('Confirm')"
|
||||
]
|
||||
|
||||
for selector in confirm_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Dialog bestätigt")
|
||||
return True
|
||||
|
||||
elif action == "cancel":
|
||||
# Abbrechen-Button suchen und klicken
|
||||
cancel_button_selectors = [
|
||||
"button:contains('Abbrechen')",
|
||||
"button:contains('Cancel')",
|
||||
"button:contains('Nein')",
|
||||
"button:contains('No')"
|
||||
]
|
||||
|
||||
for selector in cancel_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Dialog abgebrochen")
|
||||
return True
|
||||
|
||||
logger.warning(f"Konnte keine {action}-Aktion für den Dialog ausführen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Dialog-Behandlung: {e}")
|
||||
return False
|
||||
|
||||
def handle_rate_limiting(self, rotate_proxy: bool = True) -> bool:
|
||||
"""
|
||||
Behandelt eine Rate-Limiting-Situation.
|
||||
|
||||
Args:
|
||||
rotate_proxy: Ob der Proxy rotiert werden soll
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich behandelt, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.warning("Rate-Limiting erkannt, warte und versuche es erneut")
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("rate_limit_detected")
|
||||
|
||||
# Proxy rotieren, falls gewünscht
|
||||
if rotate_proxy and self.automation.use_proxy:
|
||||
success = self.automation._rotate_proxy()
|
||||
if not success:
|
||||
logger.warning("Konnte Proxy nicht rotieren")
|
||||
|
||||
# Längere Wartezeit
|
||||
wait_time = random.uniform(120, 300) # 2-5 Minuten
|
||||
logger.info(f"Warte {wait_time:.1f} Sekunden vor dem nächsten Versuch")
|
||||
time.sleep(wait_time)
|
||||
|
||||
# Seite neuladen
|
||||
self.automation.browser.page.reload()
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Prüfen, ob Rate-Limiting noch aktiv ist
|
||||
rate_limit_texts = [
|
||||
"bitte warte einige minuten",
|
||||
"please wait a few minutes",
|
||||
"try again later",
|
||||
"versuche es später erneut",
|
||||
"zu viele anfragen",
|
||||
"too many requests"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
still_rate_limited = False
|
||||
for text in rate_limit_texts:
|
||||
if text in page_content:
|
||||
still_rate_limited = True
|
||||
break
|
||||
|
||||
if still_rate_limited:
|
||||
logger.warning("Immer noch Rate-Limited nach dem Warten")
|
||||
return False
|
||||
else:
|
||||
logger.info("Rate-Limiting scheint aufgehoben zu sein")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Behandlung des Rate-Limitings: {e}")
|
||||
return False
|
||||
|
||||
def is_logged_in(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob der Benutzer bei TikTok angemeldet ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn angemeldet, False sonst
|
||||
"""
|
||||
if not self._ensure_browser():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Erfolgsindikatoren überprüfen
|
||||
success_indicators = TikTokSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for selector in success_indicators:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Benutzer ist angemeldet (Indikator: {selector})")
|
||||
return True
|
||||
|
||||
# URL überprüfen
|
||||
current_url = self.automation.browser.page.url
|
||||
if "/foryou" in current_url or "tiktok.com/explore" in current_url:
|
||||
logger.info("Benutzer ist angemeldet (URL-Check)")
|
||||
return True
|
||||
|
||||
# Anmelden-Button prüfen - wenn sichtbar, dann nicht angemeldet
|
||||
login_button_selectors = [
|
||||
TikTokSelectors.LOGIN_BUTTON_LEFT,
|
||||
TikTokSelectors.LOGIN_BUTTON_RIGHT
|
||||
]
|
||||
|
||||
for selector in login_button_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Benutzer ist nicht angemeldet (Anmelde-Button sichtbar)")
|
||||
return False
|
||||
|
||||
# Profilicon checken - wenn sichtbar, dann angemeldet
|
||||
profile_selectors = [
|
||||
"button[data-e2e='profile-icon']",
|
||||
"svg[data-e2e='profile-icon']"
|
||||
]
|
||||
|
||||
for selector in profile_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Benutzer ist angemeldet (Profilicon sichtbar)")
|
||||
return True
|
||||
|
||||
logger.warning("Konnte Login-Status nicht eindeutig bestimmen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung des Login-Status: {e}")
|
||||
return False
|
||||
|
||||
def extract_verification_code_from_email(self, email_body: str) -> Optional[str]:
|
||||
"""
|
||||
Extrahiert den Verifizierungscode aus einer E-Mail.
|
||||
|
||||
Args:
|
||||
email_body: Der E-Mail-Text
|
||||
|
||||
Returns:
|
||||
Optional[str]: Der Verifizierungscode oder None, wenn nicht gefunden
|
||||
"""
|
||||
try:
|
||||
# Muster für TikTok-Verifizierungscodes
|
||||
patterns = [
|
||||
r'(\d{6}) ist dein Bestätigungscode',
|
||||
r'(\d{6}) ist dein TikTok-Code',
|
||||
r'(\d{6}) is your TikTok code',
|
||||
r'(\d{6}) is your verification code',
|
||||
r'Dein Bestätigungscode lautet (\d{6})',
|
||||
r'Your verification code is (\d{6})',
|
||||
r'Verification code: (\d{6})',
|
||||
r'Bestätigungscode: (\d{6})',
|
||||
r'TikTok code: (\d{6})',
|
||||
r'TikTok-Code: (\d{6})'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, email_body)
|
||||
if match:
|
||||
code = match.group(1)
|
||||
logger.info(f"Verifizierungscode aus E-Mail extrahiert: {code}")
|
||||
return code
|
||||
|
||||
# Allgemeine Suche nach 6-stelligen Zahlen, wenn keine spezifischen Muster passen
|
||||
general_match = re.search(r'[^\d](\d{6})[^\d]', email_body)
|
||||
if general_match:
|
||||
code = general_match.group(1)
|
||||
logger.info(f"6-stelliger Code aus E-Mail extrahiert: {code}")
|
||||
return code
|
||||
|
||||
logger.warning("Kein Verifizierungscode in der E-Mail gefunden")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Extrahieren des Verifizierungscodes aus der E-Mail: {e}")
|
||||
return None
|
||||
458
social_networks/tiktok/tiktok_verification.py
Normale Datei
458
social_networks/tiktok/tiktok_verification.py
Normale Datei
@ -0,0 +1,458 @@
|
||||
# social_networks/tiktok/tiktok_verification.py
|
||||
|
||||
"""
|
||||
TikTok-Verifizierung - Klasse für die Verifizierungsfunktionalität bei TikTok
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from .tiktok_workflow import TikTokWorkflow
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_verification")
|
||||
|
||||
class TikTokVerification:
|
||||
"""
|
||||
Klasse für die Verifizierung von TikTok-Konten.
|
||||
Enthält alle Methoden für den Verifizierungsprozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Verifizierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
# Browser wird direkt von automation verwendet
|
||||
self.selectors = TikTokSelectors()
|
||||
self.workflow = TikTokWorkflow.get_verification_workflow()
|
||||
|
||||
logger.debug("TikTok-Verifizierung initialisiert")
|
||||
|
||||
def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Verifizierungsprozess für ein TikTok-Konto durch.
|
||||
|
||||
Args:
|
||||
verification_code: Der Bestätigungscode
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis der Verifizierung mit Status
|
||||
"""
|
||||
# Browser wird direkt von automation verwendet
|
||||
|
||||
# Validiere den Verifizierungscode
|
||||
if not self._validate_verification_code(verification_code):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültiger Verifizierungscode",
|
||||
"stage": "code_validation"
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. Überprüfen, ob wir auf der Verifizierungsseite sind
|
||||
if not self._is_on_verification_page():
|
||||
# Versuche, zur Verifizierungsseite zu navigieren, falls möglich
|
||||
# Direktnavigation ist jedoch normalerweise nicht möglich
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Nicht auf der Verifizierungsseite",
|
||||
"stage": "page_check"
|
||||
}
|
||||
|
||||
# 2. Verifizierungscode eingeben und absenden
|
||||
if not self.enter_and_submit_verification_code(verification_code):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Eingeben oder Absenden des Verifizierungscodes",
|
||||
"stage": "code_entry"
|
||||
}
|
||||
|
||||
# 3. Überprüfen, ob die Verifizierung erfolgreich war
|
||||
success, error_message = self._check_verification_success()
|
||||
|
||||
if not success:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Verifizierung fehlgeschlagen: {error_message or 'Unbekannter Fehler'}",
|
||||
"stage": "verification_check"
|
||||
}
|
||||
|
||||
# 4. Zusätzliche Dialoge behandeln
|
||||
self._handle_post_verification_dialogs()
|
||||
|
||||
# Verifizierung erfolgreich
|
||||
logger.info("TikTok-Verifizierung erfolgreich abgeschlossen")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "completed"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler bei der TikTok-Verifizierung: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception"
|
||||
}
|
||||
|
||||
def _validate_verification_code(self, verification_code: str) -> bool:
|
||||
"""
|
||||
Validiert den Verifizierungscode.
|
||||
|
||||
Args:
|
||||
verification_code: Der zu validierende Code
|
||||
|
||||
Returns:
|
||||
bool: True wenn der Code gültig ist, False sonst
|
||||
"""
|
||||
# Leerer Code
|
||||
if not verification_code:
|
||||
logger.error("Verifizierungscode ist leer")
|
||||
return False
|
||||
|
||||
# Code-Format prüfen (normalerweise 6-stellige Zahl)
|
||||
if not re.match(r"^\d{6}$", verification_code):
|
||||
logger.warning(f"Verifizierungscode hat unerwartetes Format: {verification_code}")
|
||||
# Wir geben trotzdem True zurück, da einige Codes andere Formate haben könnten
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def _is_on_verification_page(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob wir auf der Verifizierungsseite sind.
|
||||
|
||||
Returns:
|
||||
bool: True wenn auf der Verifizierungsseite, False sonst
|
||||
"""
|
||||
try:
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_page_check")
|
||||
|
||||
# Nach Verifizierungsfeld suchen
|
||||
verification_selectors = [
|
||||
TikTokSelectors.VERIFICATION_CODE_FIELD,
|
||||
"input[placeholder*='Code']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder*='sechsstelligen']",
|
||||
"input[data-e2e='verification-code-input']"
|
||||
]
|
||||
|
||||
for selector in verification_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
logger.info("Auf Verifizierungsseite")
|
||||
return True
|
||||
|
||||
# Textbasierte Erkennung
|
||||
verification_texts = [
|
||||
"Bestätigungscode",
|
||||
"Verification code",
|
||||
"sechsstelligen Code",
|
||||
"6-digit code",
|
||||
"Code senden",
|
||||
"Send code"
|
||||
]
|
||||
|
||||
page_content = self.automation.browser.page.content().lower()
|
||||
|
||||
for text in verification_texts:
|
||||
if text.lower() in page_content:
|
||||
logger.info(f"Auf Verifizierungsseite (erkannt durch Text: {text})")
|
||||
return True
|
||||
|
||||
logger.warning("Nicht auf der Verifizierungsseite")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen der Verifizierungsseite: {e}")
|
||||
return False
|
||||
|
||||
def enter_and_submit_verification_code(self, verification_code: str) -> bool:
|
||||
"""
|
||||
Gibt den Verifizierungscode ein und sendet ihn ab.
|
||||
|
||||
Args:
|
||||
verification_code: Der einzugebende Verifizierungscode
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Versuche Verifizierungscode einzugeben: {verification_code}")
|
||||
|
||||
# Mögliche Selektoren für das Verifizierungscode-Feld
|
||||
code_field_selectors = [
|
||||
TikTokSelectors.VERIFICATION_CODE_FIELD,
|
||||
TikTokSelectors.VERIFICATION_CODE_FIELD_ALT,
|
||||
"input[placeholder*='Code']",
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[data-e2e='verification-code-input']"
|
||||
]
|
||||
|
||||
# Versuche, das Feld zu finden und auszufüllen
|
||||
code_field_found = False
|
||||
|
||||
for selector in code_field_selectors:
|
||||
logger.debug(f"Versuche Selektor: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Codefeld gefunden mit Selektor: {selector}")
|
||||
if self.automation.browser.fill_form_field(selector, verification_code):
|
||||
code_field_found = True
|
||||
logger.info(f"Verifizierungscode eingegeben: {verification_code}")
|
||||
break
|
||||
|
||||
# Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen
|
||||
if not code_field_found:
|
||||
logger.info("Versuche Fuzzy-Matching für Codefeld")
|
||||
code_field_found = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Bestätigungscode", "Code eingeben", "Verification code", "6-digit code"],
|
||||
verification_code
|
||||
)
|
||||
|
||||
if not code_field_found:
|
||||
logger.error("Konnte Verifizierungscode-Feld nicht finden oder ausfüllen")
|
||||
|
||||
# Erstelle einen Screenshot zum Debuggen
|
||||
self.automation._take_screenshot("code_field_not_found")
|
||||
return False
|
||||
|
||||
# Menschliche Verzögerung vor dem Absenden
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_code_entered")
|
||||
|
||||
# "Weiter"-Button finden und klicken
|
||||
weiter_button_selectors = [
|
||||
TikTokSelectors.CONTINUE_BUTTON,
|
||||
TikTokSelectors.CONTINUE_BUTTON_ALT,
|
||||
"button[type='submit']",
|
||||
"button.e1w6iovg0",
|
||||
"button[data-e2e='next-button']",
|
||||
"//button[contains(text(), 'Weiter')]"
|
||||
]
|
||||
|
||||
weiter_button_found = False
|
||||
|
||||
logger.info("Suche nach Weiter-Button")
|
||||
for selector in weiter_button_selectors:
|
||||
logger.debug(f"Versuche Weiter-Button-Selektor: {selector}")
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Weiter-Button gefunden mit Selektor: {selector}")
|
||||
if self.automation.browser.click_element(selector):
|
||||
weiter_button_found = True
|
||||
logger.info("Verifizierungscode-Formular abgesendet")
|
||||
break
|
||||
|
||||
# Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen
|
||||
if not weiter_button_found:
|
||||
logger.info("Versuche Fuzzy-Matching für Weiter-Button")
|
||||
weiter_buttons = ["Weiter", "Next", "Continue", "Fertig", "Submit", "Verify", "Senden"]
|
||||
weiter_button_found = self.automation.ui_helper.click_button_fuzzy(
|
||||
weiter_buttons
|
||||
)
|
||||
|
||||
if not weiter_button_found:
|
||||
# Erstelle einen Screenshot zum Debuggen
|
||||
self.automation._take_screenshot("weiter_button_not_found")
|
||||
|
||||
# Versuche es mit Enter-Taste als letzten Ausweg
|
||||
logger.info("Konnte Weiter-Button nicht finden, versuche Enter-Taste")
|
||||
self.automation.browser.page.keyboard.press("Enter")
|
||||
logger.info("Enter-Taste zur Bestätigung des Verifizierungscodes gedrückt")
|
||||
weiter_button_found = True
|
||||
|
||||
# Warten nach dem Absenden
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
return weiter_button_found
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Eingeben und Absenden des Verifizierungscodes: {e}")
|
||||
return False
|
||||
|
||||
def _check_verification_success(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Überprüft, ob die Verifizierung erfolgreich war.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (Erfolg, Fehlermeldung falls vorhanden)
|
||||
"""
|
||||
try:
|
||||
# Warten nach der Verifizierung
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("verification_result")
|
||||
|
||||
# Immer noch auf der Verifizierungsseite?
|
||||
still_on_verification = self._is_on_verification_page()
|
||||
|
||||
if still_on_verification:
|
||||
# Fehlermeldung suchen
|
||||
error_message = self.automation.ui_helper.check_for_error()
|
||||
|
||||
if error_message:
|
||||
logger.error(f"Verifizierungsfehler: {error_message}")
|
||||
return False, error_message
|
||||
else:
|
||||
logger.error("Verifizierung fehlgeschlagen, immer noch auf der Verifizierungsseite")
|
||||
return False, "Immer noch auf der Verifizierungsseite"
|
||||
|
||||
# Prüfe, ob wir zur Benutzernamen-Erstellung weitergeleitet wurden
|
||||
username_selectors = [
|
||||
"input[placeholder='Benutzername']",
|
||||
"input[name='new-username']",
|
||||
"//input[@placeholder='Benutzername']"
|
||||
]
|
||||
|
||||
for selector in username_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Verifizierung erfolgreich, zur Benutzernamenauswahl weitergeleitet")
|
||||
return True, None
|
||||
|
||||
# Prüfe auf TikTok-Startseite
|
||||
current_url = self.automation.browser.page.url
|
||||
if "tiktok.com" in current_url and "/login" not in current_url and "/signup" not in current_url:
|
||||
logger.info("Verifizierung erfolgreich, jetzt auf der Startseite")
|
||||
return True, None
|
||||
|
||||
# Wenn keine eindeutigen Indikatoren gefunden wurden, aber auch keine Fehler
|
||||
logger.warning("Keine eindeutigen Erfolgsindikatoren für die Verifizierung gefunden")
|
||||
return True, None # Wir gehen davon aus, dass es erfolgreich war
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen des Verifizierungserfolgs: {e}")
|
||||
return False, f"Fehler bei der Erfolgsprüfung: {str(e)}"
|
||||
|
||||
def _handle_post_verification_dialogs(self) -> None:
|
||||
"""
|
||||
Behandelt Dialoge, die nach erfolgreicher Verifizierung erscheinen können.
|
||||
"""
|
||||
try:
|
||||
# Liste der möglichen Dialoge und wie man sie überspringt
|
||||
dialogs_to_handle = [
|
||||
{
|
||||
"name": "username_setup",
|
||||
"skip_texts": ["Überspringen", "Skip"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"]
|
||||
},
|
||||
{
|
||||
"name": "interests",
|
||||
"skip_texts": ["Überspringen", "Skip"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"]
|
||||
},
|
||||
{
|
||||
"name": "follow_accounts",
|
||||
"skip_texts": ["Überspringen", "Skip"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"]
|
||||
},
|
||||
{
|
||||
"name": "notifications",
|
||||
"skip_texts": ["Später", "Nein", "Nicht jetzt", "Later", "No", "Not now"],
|
||||
"skip_selectors": ["//button[contains(text(), 'Später')]", "//button[contains(text(), 'Not now')]"]
|
||||
}
|
||||
]
|
||||
|
||||
# Versuche, jeden möglichen Dialog zu behandeln
|
||||
for dialog in dialogs_to_handle:
|
||||
self._try_skip_dialog(dialog)
|
||||
|
||||
logger.info("Nachverifizierungs-Dialoge behandelt")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Behandeln der Nachverifizierungs-Dialoge: {e}")
|
||||
# Nicht kritisch, daher keine Fehlerbehandlung
|
||||
|
||||
def _try_skip_dialog(self, dialog: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Versucht, einen bestimmten Dialog zu überspringen.
|
||||
|
||||
Args:
|
||||
dialog: Informationen zum Dialog
|
||||
|
||||
Returns:
|
||||
bool: True wenn Dialog gefunden und übersprungen, False sonst
|
||||
"""
|
||||
try:
|
||||
# Zuerst mit Fuzzy-Matching versuchen
|
||||
skip_clicked = self.automation.ui_helper.click_button_fuzzy(
|
||||
dialog["skip_texts"],
|
||||
threshold=0.7,
|
||||
timeout=3000
|
||||
)
|
||||
|
||||
if skip_clicked:
|
||||
logger.info(f"Dialog '{dialog['name']}' mit Fuzzy-Matching übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
# Wenn Fuzzy-Matching fehlschlägt, direkte Selektoren versuchen
|
||||
for selector in dialog["skip_selectors"]:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info(f"Dialog '{dialog['name']}' mit direktem Selektor übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Versuch, Dialog '{dialog['name']}' zu überspringen: {e}")
|
||||
return False
|
||||
|
||||
def resend_verification_code(self) -> bool:
|
||||
"""
|
||||
Versucht, den Verifizierungscode erneut senden zu lassen.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Resend-Button suchen und klicken
|
||||
resend_selectors = [
|
||||
"button[data-e2e='send-code-button']",
|
||||
"//button[contains(text(), 'Code senden')]",
|
||||
"//button[contains(text(), 'Code erneut senden')]",
|
||||
"//button[contains(text(), 'Erneut senden')]",
|
||||
"a[data-e2e='resend-code-link']"
|
||||
]
|
||||
|
||||
for selector in resend_selectors:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
if self.automation.browser.click_element(selector):
|
||||
logger.info("Code erneut angefordert")
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
return True
|
||||
|
||||
# Fuzzy-Matching versuchen
|
||||
resend_texts = ["Code senden", "Code erneut senden", "Erneut senden", "Resend code", "Send again"]
|
||||
|
||||
resend_clicked = self.automation.ui_helper.click_button_fuzzy(
|
||||
resend_texts,
|
||||
threshold=0.7,
|
||||
timeout=3000
|
||||
)
|
||||
|
||||
if resend_clicked:
|
||||
logger.info("Code erneut angefordert (über Fuzzy-Matching)")
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
return True
|
||||
|
||||
logger.warning("Konnte keinen 'Code erneut senden'-Button finden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim erneuten Anfordern des Codes: {e}")
|
||||
return False
|
||||
427
social_networks/tiktok/tiktok_workflow.py
Normale Datei
427
social_networks/tiktok/tiktok_workflow.py
Normale Datei
@ -0,0 +1,427 @@
|
||||
"""
|
||||
TikTok-Workflow - Definiert die Schritte für die TikTok-Anmeldung und -Registrierung
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import re
|
||||
|
||||
from utils.text_similarity import TextSimilarity
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_workflow")
|
||||
|
||||
class TikTokWorkflow:
|
||||
"""
|
||||
Definiert die Workflow-Schritte für verschiedene TikTok-Aktionen
|
||||
wie Registrierung, Anmeldung und Verifizierung.
|
||||
"""
|
||||
|
||||
# Text-Ähnlichkeits-Threshold für Fuzzy-Matching
|
||||
SIMILARITY_THRESHOLD = 0.7
|
||||
|
||||
# Initialisiere TextSimilarity für Matching
|
||||
text_similarity = TextSimilarity(default_threshold=SIMILARITY_THRESHOLD)
|
||||
|
||||
# Mögliche alternative Texte für verschiedene UI-Elemente
|
||||
TEXT_ALTERNATIVES = {
|
||||
"email": ["E-Mail", "Email", "E-mail", "Mail", "email"],
|
||||
"phone": ["Telefon", "Telefonnummer", "Phone", "Mobile", "mobile"],
|
||||
"password": ["Passwort", "Password", "pass"],
|
||||
"code": ["Code", "Bestätigungscode", "Verification code", "Sicherheitscode"],
|
||||
"username": ["Benutzername", "Username", "user name"],
|
||||
"submit": ["Registrieren", "Sign up", "Anmelden", "Login", "Log in", "Submit"],
|
||||
"next": ["Weiter", "Next", "Continue", "Fortfahren"],
|
||||
"skip": ["Überspringen", "Skip", "Later", "Später", "Not now", "Nicht jetzt"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_registration_workflow(registration_method: str = "email") -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die TikTok-Registrierung zurück.
|
||||
|
||||
Args:
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
# Basisschritte für beide Methoden
|
||||
common_steps = [
|
||||
{
|
||||
"name": "navigate_to_signup",
|
||||
"description": "Zur TikTok-Startseite navigieren",
|
||||
"url": "https://www.tiktok.com",
|
||||
"wait_for": ["button#header-login-button", "button#top-right-action-bar-login-button"],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "click_login_button",
|
||||
"description": "Anmelden-Button klicken",
|
||||
"action": "click",
|
||||
"target": "button#top-right-action-bar-login-button",
|
||||
"wait_for": ["a[href*='/signup']", "div[role='dialog']"],
|
||||
"fuzzy_match": ["Anmelden", "Sign in", "Log in"]
|
||||
},
|
||||
{
|
||||
"name": "click_register_link",
|
||||
"description": "Registrieren-Link klicken",
|
||||
"action": "click",
|
||||
"target": "a[href*='/signup']",
|
||||
"wait_for": ["div[data-e2e='channel-item']"],
|
||||
"fuzzy_match": ["Registrieren", "Sign up", "Register"]
|
||||
},
|
||||
{
|
||||
"name": "click_phone_email_button",
|
||||
"description": "Telefon/E-Mail-Option auswählen",
|
||||
"action": "click",
|
||||
"target": "div[data-e2e='channel-item']",
|
||||
"wait_for": ["a[href*='/signup/phone-or-email/email']", "a[href*='/signup/phone-or-email/phone']"],
|
||||
"fuzzy_match": ["Telefonnummer oder E-Mail-Adresse", "Phone or Email"]
|
||||
}
|
||||
]
|
||||
|
||||
# Spezifische Schritte je nach Registrierungsmethode
|
||||
method_steps = []
|
||||
if registration_method == "email":
|
||||
method_steps.append({
|
||||
"name": "click_email_registration",
|
||||
"description": "Mit E-Mail registrieren auswählen",
|
||||
"action": "click",
|
||||
"target": "a[href*='/signup/phone-or-email/email']",
|
||||
"wait_for": ["input[placeholder='E-Mail-Adresse']"],
|
||||
"fuzzy_match": ["Mit E-Mail-Adresse registrieren", "Email", "E-Mail"]
|
||||
})
|
||||
else: # phone
|
||||
method_steps.append({
|
||||
"name": "click_phone_registration",
|
||||
"description": "Mit Telefonnummer registrieren auswählen",
|
||||
"action": "click",
|
||||
"target": "a[href*='/signup/phone-or-email/phone']",
|
||||
"wait_for": ["input[placeholder='Telefonnummer']"],
|
||||
"fuzzy_match": ["Mit Telefonnummer registrieren", "Phone", "Telefon"]
|
||||
})
|
||||
|
||||
# Geburtsdatum-Schritte
|
||||
birthday_steps = [
|
||||
{
|
||||
"name": "select_birth_month",
|
||||
"description": "Geburtsmonat auswählen",
|
||||
"action": "click",
|
||||
"target": "div.css-1fi2hzv-DivSelectLabel:contains('Monat')",
|
||||
"wait_for": ["div[role='option']"],
|
||||
"fuzzy_match": ["Monat", "Month"]
|
||||
},
|
||||
{
|
||||
"name": "select_month_option",
|
||||
"description": "Monats-Option auswählen",
|
||||
"action": "select_option",
|
||||
"target": "div[role='option']",
|
||||
"value": "{MONTH_NAME}",
|
||||
"wait_for": [],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "select_birth_day",
|
||||
"description": "Geburtstag auswählen",
|
||||
"action": "click",
|
||||
"target": "div.css-1fi2hzv-DivSelectLabel:contains('Tag')",
|
||||
"wait_for": ["div[role='option']"],
|
||||
"fuzzy_match": ["Tag", "Day"]
|
||||
},
|
||||
{
|
||||
"name": "select_day_option",
|
||||
"description": "Tags-Option auswählen",
|
||||
"action": "select_option",
|
||||
"target": "div[role='option']",
|
||||
"value": "{DAY}",
|
||||
"wait_for": [],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "select_birth_year",
|
||||
"description": "Geburtsjahr auswählen",
|
||||
"action": "click",
|
||||
"target": "div.css-1fi2hzv-DivSelectLabel:contains('Jahr')",
|
||||
"wait_for": ["div[role='option']"],
|
||||
"fuzzy_match": ["Jahr", "Year"]
|
||||
},
|
||||
{
|
||||
"name": "select_year_option",
|
||||
"description": "Jahres-Option auswählen",
|
||||
"action": "select_option",
|
||||
"target": "div[role='option']",
|
||||
"value": "{YEAR}",
|
||||
"wait_for": [],
|
||||
"fuzzy_match": None
|
||||
}
|
||||
]
|
||||
|
||||
# Formularschritte für E-Mail
|
||||
email_form_steps = [
|
||||
{
|
||||
"name": "fill_email",
|
||||
"description": "E-Mail-Adresse eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder='E-Mail-Adresse']",
|
||||
"value": "{EMAIL}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["email"]
|
||||
},
|
||||
{
|
||||
"name": "fill_password",
|
||||
"description": "Passwort eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder='Passwort']",
|
||||
"value": "{PASSWORD}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["password"]
|
||||
},
|
||||
{
|
||||
"name": "click_send_code",
|
||||
"description": "Code senden klicken",
|
||||
"action": "click",
|
||||
"target": "button[data-e2e='send-code-button']",
|
||||
"wait_for": ["input[placeholder*='sechsstelligen Code']"],
|
||||
"fuzzy_match": ["Code senden", "Send code", "Senden"]
|
||||
},
|
||||
{
|
||||
"name": "fill_verification_code",
|
||||
"description": "Bestätigungscode eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder*='sechsstelligen Code']",
|
||||
"value": "{VERIFICATION_CODE}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"]
|
||||
},
|
||||
{
|
||||
"name": "click_continue",
|
||||
"description": "Weiter klicken",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["input[placeholder='Benutzername']"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"]
|
||||
}
|
||||
]
|
||||
|
||||
# Formularschritte für Telefon
|
||||
phone_form_steps = [
|
||||
{
|
||||
"name": "select_country_code",
|
||||
"description": "Ländervorwahl auswählen",
|
||||
"action": "click",
|
||||
"target": "div[role='combobox']",
|
||||
"wait_for": ["div[role='option']"],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "select_country_option",
|
||||
"description": "Land auswählen",
|
||||
"action": "select_option",
|
||||
"target": "div[role='option']",
|
||||
"value": "{COUNTRY_NAME}",
|
||||
"wait_for": [],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "fill_phone",
|
||||
"description": "Telefonnummer eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder='Telefonnummer']",
|
||||
"value": "{PHONE}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["phone"]
|
||||
},
|
||||
{
|
||||
"name": "click_send_code",
|
||||
"description": "Code senden klicken",
|
||||
"action": "click",
|
||||
"target": "button[data-e2e='send-code-button']",
|
||||
"wait_for": ["input[placeholder*='sechsstelligen Code']"],
|
||||
"fuzzy_match": ["Code senden", "Send code", "Senden"]
|
||||
},
|
||||
{
|
||||
"name": "fill_verification_code",
|
||||
"description": "Bestätigungscode eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder*='sechsstelligen Code']",
|
||||
"value": "{VERIFICATION_CODE}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"]
|
||||
},
|
||||
{
|
||||
"name": "click_continue",
|
||||
"description": "Weiter klicken",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["input[placeholder='Benutzername']"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"]
|
||||
}
|
||||
]
|
||||
|
||||
# Benutzername-Schritte
|
||||
username_steps = [
|
||||
{
|
||||
"name": "fill_username",
|
||||
"description": "Benutzernamen eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder='Benutzername']",
|
||||
"value": "{USERNAME}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["username"]
|
||||
},
|
||||
{
|
||||
"name": "click_register",
|
||||
"description": "Registrieren klicken",
|
||||
"action": "click",
|
||||
"target": "button:contains('Registrieren')",
|
||||
"wait_for": ["a[href='/foryou']", "button:contains('Überspringen')"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["submit"]
|
||||
},
|
||||
{
|
||||
"name": "handle_skip_option",
|
||||
"description": "Optional: Überspringen klicken",
|
||||
"action": "click",
|
||||
"target": "button:contains('Überspringen')",
|
||||
"optional": True,
|
||||
"wait_for": ["a[href='/foryou']"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["skip"]
|
||||
}
|
||||
]
|
||||
|
||||
# Vollständigen Workflow zusammenstellen
|
||||
if registration_method == "email":
|
||||
workflow = common_steps + method_steps + birthday_steps + email_form_steps + username_steps
|
||||
else: # phone
|
||||
workflow = common_steps + method_steps + birthday_steps + phone_form_steps + username_steps
|
||||
|
||||
return workflow
|
||||
|
||||
@staticmethod
|
||||
def get_login_workflow() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die TikTok-Anmeldung zurück.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
login_steps = [
|
||||
{
|
||||
"name": "navigate_to_login",
|
||||
"description": "Zur TikTok-Startseite navigieren",
|
||||
"url": "https://www.tiktok.com",
|
||||
"wait_for": ["button#header-login-button", "button#top-right-action-bar-login-button"],
|
||||
"fuzzy_match": None
|
||||
},
|
||||
{
|
||||
"name": "click_login_button",
|
||||
"description": "Anmelden-Button klicken",
|
||||
"action": "click",
|
||||
"target": "button#top-right-action-bar-login-button",
|
||||
"wait_for": ["div[role='dialog']"],
|
||||
"fuzzy_match": ["Anmelden", "Sign in", "Log in"]
|
||||
},
|
||||
{
|
||||
"name": "click_phone_email_button",
|
||||
"description": "Telefon/E-Mail-Option auswählen",
|
||||
"action": "click",
|
||||
"target": "div[data-e2e='channel-item']",
|
||||
"wait_for": ["input[type='text']"],
|
||||
"fuzzy_match": ["Telefon-Nr./E-Mail/Anmeldename", "Phone or Email"]
|
||||
},
|
||||
{
|
||||
"name": "fill_login_field",
|
||||
"description": "Benutzername/E-Mail/Telefon eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[type='text']",
|
||||
"value": "{USERNAME_OR_EMAIL}",
|
||||
"fuzzy_match": ["Email", "Benutzername", "Telefon"]
|
||||
},
|
||||
{
|
||||
"name": "fill_password",
|
||||
"description": "Passwort eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[type='password']",
|
||||
"value": "{PASSWORD}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["password"]
|
||||
},
|
||||
{
|
||||
"name": "click_login",
|
||||
"description": "Anmelden klicken",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["a[href='/foryou']"],
|
||||
"fuzzy_match": ["Anmelden", "Log in", "Login"]
|
||||
}
|
||||
]
|
||||
|
||||
return login_steps
|
||||
|
||||
@staticmethod
|
||||
def get_verification_workflow() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt den Workflow für die TikTok-Verifizierung zurück.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Liste von Workflow-Schritten
|
||||
"""
|
||||
verification_steps = [
|
||||
{
|
||||
"name": "fill_verification_code",
|
||||
"description": "Bestätigungscode eingeben",
|
||||
"action": "fill",
|
||||
"target": "input[placeholder*='sechsstelligen Code']",
|
||||
"value": "{VERIFICATION_CODE}",
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"]
|
||||
},
|
||||
{
|
||||
"name": "click_continue",
|
||||
"description": "Weiter klicken",
|
||||
"action": "click",
|
||||
"target": "button[type='submit']",
|
||||
"wait_for": ["input[placeholder='Benutzername']", "a[href='/foryou']"],
|
||||
"fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"]
|
||||
}
|
||||
]
|
||||
|
||||
return verification_steps
|
||||
|
||||
@staticmethod
|
||||
def identify_current_step(page_title: str, page_url: str, visible_elements: List[str]) -> str:
|
||||
"""
|
||||
Identifiziert den aktuellen Schritt basierend auf dem Seitentitel, der URL und sichtbaren Elementen.
|
||||
|
||||
Args:
|
||||
page_title: Titel der Seite
|
||||
page_url: URL der Seite
|
||||
visible_elements: Liste sichtbarer Elemente (Selektoren)
|
||||
|
||||
Returns:
|
||||
str: Name des identifizierten Schritts
|
||||
"""
|
||||
# Auf der Startseite
|
||||
if "tiktok.com" in page_url and not "/signup" in page_url and not "/login" in page_url:
|
||||
return "navigate_to_signup"
|
||||
|
||||
# Anmelde-/Registrierungsauswahl
|
||||
if "signup" in page_url or "login" in page_url:
|
||||
if any("channel-item" in element for element in visible_elements):
|
||||
return "click_phone_email_button"
|
||||
|
||||
# Geburtsdatum
|
||||
if "Monat" in page_title or "Month" in page_title or any("Geburtsdatum" in element for element in visible_elements):
|
||||
return "select_birth_month"
|
||||
|
||||
# E-Mail-/Telefon-Eingabe
|
||||
if any("E-Mail-Adresse" in element for element in visible_elements):
|
||||
return "fill_email"
|
||||
if any("Telefonnummer" in element for element in visible_elements):
|
||||
return "fill_phone"
|
||||
|
||||
# Bestätigungscode
|
||||
if any("sechsstelligen Code" in element for element in visible_elements):
|
||||
return "fill_verification_code"
|
||||
|
||||
# Benutzernamen-Erstellung
|
||||
if any("Benutzername" in element for element in visible_elements):
|
||||
return "fill_username"
|
||||
|
||||
# Erfolgreiche Anmeldung
|
||||
if "foryou" in page_url or any("Für dich" in element for element in visible_elements):
|
||||
return "logged_in"
|
||||
|
||||
return "unknown"
|
||||
0
social_networks/twitter/__init__.py
Normale Datei
0
social_networks/twitter/__init__.py
Normale Datei
0
social_networks/twitter/twitter_automation.py
Normale Datei
0
social_networks/twitter/twitter_automation.py
Normale Datei
0
social_networks/twitter/twitter_login.py
Normale Datei
0
social_networks/twitter/twitter_login.py
Normale Datei
0
social_networks/twitter/twitter_registration.py
Normale Datei
0
social_networks/twitter/twitter_registration.py
Normale Datei
0
social_networks/twitter/twitter_selectors.py
Normale Datei
0
social_networks/twitter/twitter_selectors.py
Normale Datei
0
social_networks/twitter/twitter_ui_helper.py
Normale Datei
0
social_networks/twitter/twitter_ui_helper.py
Normale Datei
0
social_networks/twitter/twitter_utils.py
Normale Datei
0
social_networks/twitter/twitter_utils.py
Normale Datei
0
social_networks/twitter/twitter_verification.py
Normale Datei
0
social_networks/twitter/twitter_verification.py
Normale Datei
0
social_networks/twitter/twitter_workflow.py
Normale Datei
0
social_networks/twitter/twitter_workflow.py
Normale Datei
0
social_networks/vk/__init__.py
Normale Datei
0
social_networks/vk/__init__.py
Normale Datei
240
social_networks/vk/vk_automation.py
Normale Datei
240
social_networks/vk/vk_automation.py
Normale Datei
@ -0,0 +1,240 @@
|
||||
"""
|
||||
VK Automatisierung - Hauptklasse
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, Optional, Tuple
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from social_networks.base_automation import BaseAutomation
|
||||
from social_networks.vk import vk_selectors as selectors
|
||||
from social_networks.vk.vk_ui_helper import VKUIHelper
|
||||
from social_networks.vk.vk_registration import VKRegistration
|
||||
from social_networks.vk.vk_login import VKLogin
|
||||
from social_networks.vk.vk_verification import VKVerification
|
||||
from social_networks.vk.vk_utils import VKUtils
|
||||
|
||||
logger = logging.getLogger("vk_automation")
|
||||
|
||||
class VKAutomation(BaseAutomation):
|
||||
"""
|
||||
VK-spezifische Automatisierung
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Initialisiert die VK-Automatisierung
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.platform_name = "vk"
|
||||
self.ui_helper = None
|
||||
self.registration = None
|
||||
self.login_helper = None
|
||||
self.verification = None
|
||||
self.utils = None
|
||||
|
||||
def _initialize_helpers(self, page: Page):
|
||||
"""
|
||||
Initialisiert die Hilfsklassen
|
||||
"""
|
||||
self.ui_helper = VKUIHelper(page, self.screenshots_dir, self.save_screenshots)
|
||||
self.registration = VKRegistration(page, self.ui_helper, self.screenshots_dir, self.save_screenshots)
|
||||
self.login_helper = VKLogin(page, self.ui_helper, self.screenshots_dir, self.save_screenshots)
|
||||
self.verification = VKVerification(page, self.ui_helper, self.email_handler, self.screenshots_dir, self.save_screenshots)
|
||||
self.utils = VKUtils()
|
||||
|
||||
def register_account(self, full_name: str, age: int, registration_method: str = "phone",
|
||||
phone_number: str = None, **kwargs) -> Dict[str, any]:
|
||||
"""
|
||||
Erstellt einen neuen VK-Account
|
||||
|
||||
Args:
|
||||
full_name: Vollständiger Name für den Account
|
||||
age: Alter des Benutzers
|
||||
registration_method: Registrierungsmethode (nur "phone" für VK)
|
||||
phone_number: Telefonnummer (erforderlich für VK)
|
||||
**kwargs: Weitere optionale Parameter
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starte VK Account-Registrierung für {full_name}")
|
||||
|
||||
# Erstelle account_data aus den Parametern
|
||||
account_data = {
|
||||
"full_name": full_name,
|
||||
"first_name": kwargs.get("first_name", full_name.split()[0] if full_name else ""),
|
||||
"last_name": kwargs.get("last_name", full_name.split()[-1] if full_name and len(full_name.split()) > 1 else ""),
|
||||
"age": age,
|
||||
"birthday": kwargs.get("birthday", self._generate_birthday(age)),
|
||||
"gender": kwargs.get("gender", random.choice(["male", "female"])),
|
||||
"username": kwargs.get("username", ""),
|
||||
"password": kwargs.get("password", ""),
|
||||
"email": kwargs.get("email", ""),
|
||||
"phone": phone_number or ""
|
||||
}
|
||||
|
||||
# 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",
|
||||
"message": "Browser-Initialisierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
# Page-Objekt holen
|
||||
page = self.browser.page
|
||||
self._initialize_helpers(page)
|
||||
|
||||
# VK Homepage öffnen
|
||||
logger.info("Navigiere zu VK Homepage")
|
||||
page.goto(selectors.BASE_URL, wait_until="domcontentloaded")
|
||||
time.sleep(random.uniform(2, 4))
|
||||
|
||||
# Screenshot der Startseite
|
||||
self.ui_helper.take_screenshot("vk_homepage")
|
||||
|
||||
# Cookie Banner handhaben
|
||||
self._handle_cookie_banner(page)
|
||||
|
||||
# "Konto erstellen" Button klicken
|
||||
logger.info("Suche 'Konto erstellen' Button")
|
||||
try:
|
||||
# Versuche verschiedene Selektoren
|
||||
create_button_clicked = False
|
||||
|
||||
# Versuche CSS Selektor
|
||||
if page.locator(selectors.CREATE_ACCOUNT_BUTTON).count() > 0:
|
||||
page.click(selectors.CREATE_ACCOUNT_BUTTON)
|
||||
create_button_clicked = True
|
||||
logger.info("Button mit CSS Selektor gefunden und geklickt")
|
||||
|
||||
# Versuche XPath wenn CSS nicht funktioniert
|
||||
elif page.locator(selectors.CREATE_ACCOUNT_BUTTON_XPATH).count() > 0:
|
||||
page.click(selectors.CREATE_ACCOUNT_BUTTON_XPATH)
|
||||
create_button_clicked = True
|
||||
logger.info("Button mit XPath gefunden und geklickt")
|
||||
|
||||
# Versuche alternativen Selektor
|
||||
elif page.locator(selectors.CREATE_ACCOUNT_BUTTON_ALTERNATE).count() > 0:
|
||||
page.click(selectors.CREATE_ACCOUNT_BUTTON_ALTERNATE)
|
||||
create_button_clicked = True
|
||||
logger.info("Button mit alternativem Selektor gefunden und geklickt")
|
||||
|
||||
if not create_button_clicked:
|
||||
raise Exception("'Konto erstellen' Button nicht gefunden")
|
||||
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken des 'Konto erstellen' Buttons: {e}")
|
||||
self.ui_helper.take_screenshot("create_account_button_error")
|
||||
raise
|
||||
|
||||
# Registrierungsformular ausfüllen
|
||||
registration_result = self.registration.fill_registration_form(account_data)
|
||||
if not registration_result["success"]:
|
||||
return registration_result
|
||||
|
||||
# Telefonnummer-Verifizierung
|
||||
verification_result = self.verification.handle_phone_verification(account_data)
|
||||
if not verification_result["success"]:
|
||||
return verification_result
|
||||
|
||||
# Erfolg
|
||||
logger.info("VK Account-Registrierung erfolgreich abgeschlossen")
|
||||
return {
|
||||
"success": True,
|
||||
"username": account_data.get("username"),
|
||||
"password": account_data.get("password"),
|
||||
"email": account_data.get("email"),
|
||||
"phone": account_data.get("phone"),
|
||||
"message": "Account erfolgreich erstellt"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der VK-Registrierung: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Registrierung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
finally:
|
||||
self._close_browser()
|
||||
|
||||
def login(self, username: str, password: str) -> Dict[str, any]:
|
||||
"""
|
||||
Meldet sich bei einem bestehenden VK-Account an
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starte VK Login für {username}")
|
||||
|
||||
# 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",
|
||||
"message": "Browser-Initialisierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
# Page-Objekt holen
|
||||
page = self.browser.page
|
||||
self._initialize_helpers(page)
|
||||
|
||||
# Login durchführen
|
||||
return self.login_helper.login(username, password)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim VK Login: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Login fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
finally:
|
||||
self._close_browser()
|
||||
|
||||
def _handle_cookie_banner(self, page: Page):
|
||||
"""
|
||||
Handhabt Cookie-Banner falls vorhanden
|
||||
"""
|
||||
try:
|
||||
if page.locator(selectors.COOKIE_BANNER).count() > 0:
|
||||
logger.info("Cookie Banner gefunden")
|
||||
if page.locator(selectors.COOKIE_ACCEPT_BUTTON).count() > 0:
|
||||
page.click(selectors.COOKIE_ACCEPT_BUTTON)
|
||||
logger.info("Cookie Banner akzeptiert")
|
||||
time.sleep(random.uniform(1, 2))
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Handhaben des Cookie Banners: {e}")
|
||||
|
||||
def get_account_info(self) -> Dict[str, any]:
|
||||
"""
|
||||
Ruft Informationen über den aktuellen Account ab
|
||||
"""
|
||||
# TODO: Implementierung
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Noch nicht implementiert"
|
||||
}
|
||||
|
||||
def logout(self) -> bool:
|
||||
"""
|
||||
Meldet sich vom aktuellen Account ab
|
||||
"""
|
||||
# TODO: Implementierung
|
||||
return False
|
||||
|
||||
def _generate_birthday(self, age: int) -> str:
|
||||
"""
|
||||
Generiert ein Geburtsdatum basierend auf dem Alter
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
today = datetime.now()
|
||||
birth_year = today.year - age
|
||||
# Zufälliger Tag im Jahr
|
||||
random_days = random.randint(0, 364)
|
||||
birthday = datetime(birth_year, 1, 1) + timedelta(days=random_days)
|
||||
return birthday.strftime("%Y-%m-%d")
|
||||
127
social_networks/vk/vk_login.py
Normale Datei
127
social_networks/vk/vk_login.py
Normale Datei
@ -0,0 +1,127 @@
|
||||
"""
|
||||
VK Login - Handhabt den Login-Prozess
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from social_networks.vk import vk_selectors as selectors
|
||||
from social_networks.vk.vk_ui_helper import VKUIHelper
|
||||
|
||||
logger = logging.getLogger("vk_login")
|
||||
|
||||
class VKLogin:
|
||||
"""
|
||||
Handhabt den VK Login-Prozess
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, ui_helper: VKUIHelper, screenshots_dir: str = None, save_screenshots: bool = True):
|
||||
"""
|
||||
Initialisiert den Login Handler
|
||||
"""
|
||||
self.page = page
|
||||
self.ui_helper = ui_helper
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.save_screenshots = save_screenshots
|
||||
|
||||
def login(self, username: str, password: str) -> Dict[str, any]:
|
||||
"""
|
||||
Führt den Login durch
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starte VK Login für {username}")
|
||||
|
||||
# Navigiere zur Login-Seite
|
||||
self.page.goto(selectors.LOGIN_URL, wait_until="domcontentloaded")
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
self.ui_helper.take_screenshot("login_page")
|
||||
|
||||
# Email/Telefonnummer eingeben
|
||||
if self.ui_helper.wait_for_element(selectors.LOGIN_EMAIL_INPUT, timeout=10000):
|
||||
logger.info("Gebe Benutzernamen ein")
|
||||
self.ui_helper.type_with_delay(selectors.LOGIN_EMAIL_INPUT, username)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Passwort eingeben
|
||||
if self.ui_helper.wait_for_element(selectors.LOGIN_PASSWORD_INPUT, timeout=5000):
|
||||
logger.info("Gebe Passwort ein")
|
||||
self.ui_helper.type_with_delay(selectors.LOGIN_PASSWORD_INPUT, password)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Screenshot vor dem Login
|
||||
self.ui_helper.take_screenshot("login_form_filled")
|
||||
|
||||
# Login Button klicken
|
||||
if self.ui_helper.wait_for_element(selectors.LOGIN_SUBMIT_BUTTON, timeout=5000):
|
||||
logger.info("Klicke auf Login Button")
|
||||
self.ui_helper.click_with_retry(selectors.LOGIN_SUBMIT_BUTTON)
|
||||
|
||||
# Warte auf Navigation
|
||||
self.ui_helper.wait_for_navigation()
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
# Prüfe ob Login erfolgreich war
|
||||
if self._check_login_success():
|
||||
logger.info("Login erfolgreich")
|
||||
self.ui_helper.take_screenshot("login_success")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Login erfolgreich"
|
||||
}
|
||||
else:
|
||||
error_msg = self._get_error_message()
|
||||
logger.error(f"Login fehlgeschlagen: {error_msg}")
|
||||
self.ui_helper.take_screenshot("login_failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"message": f"Login fehlgeschlagen: {error_msg}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Login: {e}")
|
||||
self.ui_helper.take_screenshot("login_error")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Login fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _check_login_success(self) -> bool:
|
||||
"""
|
||||
Prüft ob der Login erfolgreich war
|
||||
"""
|
||||
try:
|
||||
# Prüfe ob wir auf der Hauptseite sind
|
||||
current_url = self.page.url
|
||||
if "feed" in current_url or "id" in current_url:
|
||||
return True
|
||||
|
||||
# Prüfe ob Login-Formular noch sichtbar ist
|
||||
if self.ui_helper.is_element_visible(selectors.LOGIN_EMAIL_INPUT):
|
||||
return False
|
||||
|
||||
# Prüfe auf Fehlermeldung
|
||||
if self.ui_helper.is_element_visible(selectors.ERROR_MESSAGE):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler bei der Login-Prüfung: {e}")
|
||||
return False
|
||||
|
||||
def _get_error_message(self) -> str:
|
||||
"""
|
||||
Holt die Fehlermeldung falls vorhanden
|
||||
"""
|
||||
try:
|
||||
if self.ui_helper.is_element_visible(selectors.ERROR_MESSAGE):
|
||||
return self.ui_helper.get_element_text(selectors.ERROR_MESSAGE) or "Unbekannter Fehler"
|
||||
return "Login fehlgeschlagen"
|
||||
except:
|
||||
return "Unbekannter Fehler"
|
||||
132
social_networks/vk/vk_registration.py
Normale Datei
132
social_networks/vk/vk_registration.py
Normale Datei
@ -0,0 +1,132 @@
|
||||
"""
|
||||
VK Registrierung - Handhabt den Registrierungsprozess
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from social_networks.vk import vk_selectors as selectors
|
||||
from social_networks.vk.vk_ui_helper import VKUIHelper
|
||||
|
||||
logger = logging.getLogger("vk_registration")
|
||||
|
||||
class VKRegistration:
|
||||
"""
|
||||
Handhabt den VK Registrierungsprozess
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, ui_helper: VKUIHelper, screenshots_dir: str = None, save_screenshots: bool = True):
|
||||
"""
|
||||
Initialisiert die Registrierung
|
||||
"""
|
||||
self.page = page
|
||||
self.ui_helper = ui_helper
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.save_screenshots = save_screenshots
|
||||
|
||||
def fill_registration_form(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Füllt das Registrierungsformular aus
|
||||
"""
|
||||
try:
|
||||
logger.info("Fülle VK Registrierungsformular aus")
|
||||
|
||||
# Warte auf Registrierungsseite
|
||||
time.sleep(random.uniform(2, 3))
|
||||
self.ui_helper.take_screenshot("registration_form")
|
||||
|
||||
# Vorname eingeben
|
||||
if self.ui_helper.wait_for_element(selectors.REGISTRATION_FIRST_NAME, timeout=10000):
|
||||
first_name = account_data.get("first_name", "")
|
||||
logger.info(f"Gebe Vorname ein: {first_name}")
|
||||
self.ui_helper.type_with_delay(selectors.REGISTRATION_FIRST_NAME, first_name)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Nachname eingeben
|
||||
if self.ui_helper.wait_for_element(selectors.REGISTRATION_LAST_NAME, timeout=5000):
|
||||
last_name = account_data.get("last_name", "")
|
||||
logger.info(f"Gebe Nachname ein: {last_name}")
|
||||
self.ui_helper.type_with_delay(selectors.REGISTRATION_LAST_NAME, last_name)
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
|
||||
# Geburtstag auswählen
|
||||
self._select_birthday(account_data)
|
||||
|
||||
# Geschlecht auswählen
|
||||
self._select_gender(account_data)
|
||||
|
||||
# Screenshot vor dem Fortfahren
|
||||
self.ui_helper.take_screenshot("registration_form_filled")
|
||||
|
||||
# Fortfahren Button klicken
|
||||
if self.ui_helper.wait_for_element(selectors.REGISTRATION_CONTINUE_BUTTON, timeout=5000):
|
||||
logger.info("Klicke auf Fortfahren")
|
||||
self.ui_helper.click_with_retry(selectors.REGISTRATION_CONTINUE_BUTTON)
|
||||
time.sleep(random.uniform(2, 3))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Registrierungsformular ausgefüllt"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Registrierungsformulars: {e}")
|
||||
self.ui_helper.take_screenshot("registration_form_error")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Fehler beim Ausfüllen des Formulars: {str(e)}"
|
||||
}
|
||||
|
||||
def _select_birthday(self, account_data: Dict[str, str]):
|
||||
"""
|
||||
Wählt das Geburtsdatum aus
|
||||
"""
|
||||
try:
|
||||
birthday = account_data.get("birthday", "1990-01-15")
|
||||
year, month, day = birthday.split("-")
|
||||
|
||||
# Tag auswählen
|
||||
if self.ui_helper.wait_for_element(selectors.REGISTRATION_BIRTHDAY_DAY, timeout=5000):
|
||||
logger.info(f"Wähle Geburtstag: {day}")
|
||||
self.ui_helper.select_dropdown_option(selectors.REGISTRATION_BIRTHDAY_DAY, day.lstrip("0"))
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
|
||||
# Monat auswählen
|
||||
if self.ui_helper.wait_for_element(selectors.REGISTRATION_BIRTHDAY_MONTH, timeout=5000):
|
||||
logger.info(f"Wähle Geburtsmonat: {month}")
|
||||
self.ui_helper.select_dropdown_option(selectors.REGISTRATION_BIRTHDAY_MONTH, month.lstrip("0"))
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
|
||||
# Jahr auswählen
|
||||
if self.ui_helper.wait_for_element(selectors.REGISTRATION_BIRTHDAY_YEAR, timeout=5000):
|
||||
logger.info(f"Wähle Geburtsjahr: {year}")
|
||||
self.ui_helper.select_dropdown_option(selectors.REGISTRATION_BIRTHDAY_YEAR, year)
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler bei der Geburtsdatum-Auswahl: {e}")
|
||||
|
||||
def _select_gender(self, account_data: Dict[str, str]):
|
||||
"""
|
||||
Wählt das Geschlecht aus
|
||||
"""
|
||||
try:
|
||||
gender = account_data.get("gender", "male").lower()
|
||||
|
||||
if gender == "female":
|
||||
if self.ui_helper.wait_for_element(selectors.REGISTRATION_GENDER_FEMALE, timeout=5000):
|
||||
logger.info("Wähle Geschlecht: Weiblich")
|
||||
self.ui_helper.check_radio_button(selectors.REGISTRATION_GENDER_FEMALE)
|
||||
else:
|
||||
if self.ui_helper.wait_for_element(selectors.REGISTRATION_GENDER_MALE, timeout=5000):
|
||||
logger.info("Wähle Geschlecht: Männlich")
|
||||
self.ui_helper.check_radio_button(selectors.REGISTRATION_GENDER_MALE)
|
||||
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler bei der Geschlechtsauswahl: {e}")
|
||||
51
social_networks/vk/vk_selectors.py
Normale Datei
51
social_networks/vk/vk_selectors.py
Normale Datei
@ -0,0 +1,51 @@
|
||||
"""
|
||||
VK UI Selektoren und URLs
|
||||
"""
|
||||
|
||||
# URLs
|
||||
BASE_URL = "https://vk.com/"
|
||||
REGISTRATION_URL = "https://vk.com/join"
|
||||
LOGIN_URL = "https://vk.com/login"
|
||||
|
||||
# Startseite
|
||||
CREATE_ACCOUNT_BUTTON = "span.vkuiButton__content:has-text('Konto erstellen')"
|
||||
CREATE_ACCOUNT_BUTTON_XPATH = "//span[@class='vkuiButton__content' and text()='Konto erstellen']"
|
||||
CREATE_ACCOUNT_BUTTON_ALTERNATE = "button:has-text('Konto erstellen')"
|
||||
|
||||
# Login Seite
|
||||
LOGIN_EMAIL_INPUT = "input[name='email']"
|
||||
LOGIN_PASSWORD_INPUT = "input[name='password']"
|
||||
LOGIN_SUBMIT_BUTTON = "button[type='submit']"
|
||||
|
||||
# Registrierungsformular
|
||||
REGISTRATION_FIRST_NAME = "input[name='first_name']"
|
||||
REGISTRATION_LAST_NAME = "input[name='last_name']"
|
||||
REGISTRATION_BIRTHDAY_DAY = "select[name='bday']"
|
||||
REGISTRATION_BIRTHDAY_MONTH = "select[name='bmonth']"
|
||||
REGISTRATION_BIRTHDAY_YEAR = "select[name='byear']"
|
||||
REGISTRATION_GENDER_MALE = "input[value='2']"
|
||||
REGISTRATION_GENDER_FEMALE = "input[value='1']"
|
||||
REGISTRATION_CONTINUE_BUTTON = "button[type='submit']"
|
||||
|
||||
# Telefonnummer Verifizierung
|
||||
PHONE_INPUT = "input[name='phone']"
|
||||
PHONE_COUNTRY_CODE = "div.PhoneInput__countryCode"
|
||||
PHONE_SUBMIT_BUTTON = "button[type='submit']"
|
||||
|
||||
# SMS Verifizierung
|
||||
SMS_CODE_INPUT = "input[name='code']"
|
||||
SMS_SUBMIT_BUTTON = "button[type='submit']"
|
||||
SMS_RESEND_LINK = "a:has-text('Code erneut senden')"
|
||||
|
||||
# Captcha
|
||||
CAPTCHA_IMAGE = "img.vkc__Captcha__image"
|
||||
CAPTCHA_INPUT = "input[name='captcha']"
|
||||
|
||||
# Fehler- und Erfolgsmeldungen
|
||||
ERROR_MESSAGE = "div.error"
|
||||
SUCCESS_MESSAGE = "div.success"
|
||||
PHONE_ERROR = "div.phone_error"
|
||||
|
||||
# Cookies Banner
|
||||
COOKIE_ACCEPT_BUTTON = "button:has-text('Akzeptieren')"
|
||||
COOKIE_BANNER = "div[class*='cookie']"
|
||||
149
social_networks/vk/vk_ui_helper.py
Normale Datei
149
social_networks/vk/vk_ui_helper.py
Normale Datei
@ -0,0 +1,149 @@
|
||||
"""
|
||||
VK UI Helper - Hilfsfunktionen für UI-Interaktionen
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
import os
|
||||
from typing import Optional
|
||||
from playwright.sync_api import Page, ElementHandle
|
||||
|
||||
logger = logging.getLogger("vk_ui_helper")
|
||||
|
||||
class VKUIHelper:
|
||||
"""
|
||||
Hilfsklasse für VK UI-Interaktionen
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, screenshots_dir: str = None, save_screenshots: bool = True):
|
||||
"""
|
||||
Initialisiert den UI Helper
|
||||
"""
|
||||
self.page = page
|
||||
self.screenshots_dir = screenshots_dir or "logs/screenshots"
|
||||
self.save_screenshots = save_screenshots
|
||||
|
||||
# Screenshot-Verzeichnis erstellen falls nötig
|
||||
if self.save_screenshots and not os.path.exists(self.screenshots_dir):
|
||||
os.makedirs(self.screenshots_dir)
|
||||
|
||||
def take_screenshot(self, name: str) -> Optional[str]:
|
||||
"""
|
||||
Erstellt einen Screenshot
|
||||
"""
|
||||
if not self.save_screenshots:
|
||||
return None
|
||||
|
||||
try:
|
||||
timestamp = int(time.time())
|
||||
filename = f"{name}_{timestamp}.png"
|
||||
filepath = os.path.join(self.screenshots_dir, filename)
|
||||
self.page.screenshot(path=filepath)
|
||||
logger.debug(f"Screenshot gespeichert: {filepath}")
|
||||
return filepath
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Erstellen des Screenshots: {e}")
|
||||
return None
|
||||
|
||||
def wait_for_element(self, selector: str, timeout: int = 30000) -> bool:
|
||||
"""
|
||||
Wartet auf ein Element
|
||||
"""
|
||||
try:
|
||||
self.page.wait_for_selector(selector, timeout=timeout)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Element {selector} nicht gefunden nach {timeout}ms")
|
||||
return False
|
||||
|
||||
def type_with_delay(self, selector: str, text: str, delay_min: float = 0.05, delay_max: float = 0.15):
|
||||
"""
|
||||
Tippt Text mit menschenähnlicher Verzögerung
|
||||
"""
|
||||
try:
|
||||
element = self.page.locator(selector)
|
||||
element.click()
|
||||
|
||||
for char in text:
|
||||
element.type(char)
|
||||
time.sleep(random.uniform(delay_min, delay_max))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Tippen in {selector}: {e}")
|
||||
raise
|
||||
|
||||
def click_with_retry(self, selector: str, max_attempts: int = 3) -> bool:
|
||||
"""
|
||||
Klickt auf ein Element mit Wiederholungsversuchen
|
||||
"""
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
self.page.click(selector)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Klick-Versuch {attempt + 1} fehlgeschlagen: {e}")
|
||||
if attempt < max_attempts - 1:
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
return False
|
||||
|
||||
def scroll_to_element(self, selector: str):
|
||||
"""
|
||||
Scrollt zu einem Element
|
||||
"""
|
||||
try:
|
||||
self.page.locator(selector).scroll_into_view_if_needed()
|
||||
time.sleep(random.uniform(0.5, 1))
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Scrollen zu {selector}: {e}")
|
||||
|
||||
def is_element_visible(self, selector: str) -> bool:
|
||||
"""
|
||||
Prüft ob ein Element sichtbar ist
|
||||
"""
|
||||
try:
|
||||
return self.page.locator(selector).is_visible()
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_element_text(self, selector: str) -> Optional[str]:
|
||||
"""
|
||||
Holt den Text eines Elements
|
||||
"""
|
||||
try:
|
||||
return self.page.locator(selector).text_content()
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Lesen des Texts von {selector}: {e}")
|
||||
return None
|
||||
|
||||
def select_dropdown_option(self, selector: str, value: str):
|
||||
"""
|
||||
Wählt eine Option aus einem Dropdown
|
||||
"""
|
||||
try:
|
||||
self.page.select_option(selector, value)
|
||||
time.sleep(random.uniform(0.3, 0.6))
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Auswählen von {value} in {selector}: {e}")
|
||||
raise
|
||||
|
||||
def check_radio_button(self, selector: str):
|
||||
"""
|
||||
Wählt einen Radio Button aus
|
||||
"""
|
||||
try:
|
||||
self.page.check(selector)
|
||||
time.sleep(random.uniform(0.2, 0.4))
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Auswählen des Radio Buttons {selector}: {e}")
|
||||
raise
|
||||
|
||||
def wait_for_navigation(self, timeout: int = 30000):
|
||||
"""
|
||||
Wartet auf Navigation
|
||||
"""
|
||||
try:
|
||||
self.page.wait_for_load_state("networkidle", timeout=timeout)
|
||||
except Exception as e:
|
||||
logger.warning(f"Navigation-Timeout nach {timeout}ms: {e}")
|
||||
89
social_networks/vk/vk_utils.py
Normale Datei
89
social_networks/vk/vk_utils.py
Normale Datei
@ -0,0 +1,89 @@
|
||||
"""
|
||||
VK Utils - Utility-Funktionen für VK
|
||||
"""
|
||||
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger("vk_utils")
|
||||
|
||||
class VKUtils:
|
||||
"""
|
||||
Utility-Funktionen für VK
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def generate_vk_username(first_name: str, last_name: str) -> str:
|
||||
"""
|
||||
Generiert einen VK-kompatiblen Benutzernamen
|
||||
"""
|
||||
# VK verwendet normalerweise id123456789 Format
|
||||
# Aber für die URL kann man einen benutzerdefinierten Namen verwenden
|
||||
base = f"{first_name.lower()}{last_name.lower()}"
|
||||
# Entferne Sonderzeichen
|
||||
base = ''.join(c for c in base if c.isalnum())
|
||||
|
||||
# Füge zufällige Zahlen hinzu
|
||||
random_suffix = ''.join(random.choices(string.digits, k=random.randint(2, 4)))
|
||||
|
||||
return f"{base}{random_suffix}"
|
||||
|
||||
@staticmethod
|
||||
def format_phone_number(phone: str, country_code: str = "+7") -> str:
|
||||
"""
|
||||
Formatiert eine Telefonnummer für VK
|
||||
VK ist primär in Russland, daher Standard +7
|
||||
"""
|
||||
# Entferne alle nicht-numerischen Zeichen
|
||||
phone_digits = ''.join(c for c in phone if c.isdigit())
|
||||
|
||||
# Wenn die Nummer bereits mit Ländercode beginnt
|
||||
if phone.startswith("+"):
|
||||
return phone
|
||||
|
||||
# Füge Ländercode hinzu
|
||||
return f"{country_code}{phone_digits}"
|
||||
|
||||
@staticmethod
|
||||
def is_valid_vk_password(password: str) -> bool:
|
||||
"""
|
||||
Prüft ob ein Passwort den VK-Anforderungen entspricht
|
||||
- Mindestens 6 Zeichen
|
||||
- Enthält Buchstaben und Zahlen
|
||||
"""
|
||||
if len(password) < 6:
|
||||
return False
|
||||
|
||||
has_letter = any(c.isalpha() for c in password)
|
||||
has_digit = any(c.isdigit() for c in password)
|
||||
|
||||
return has_letter and has_digit
|
||||
|
||||
@staticmethod
|
||||
def generate_vk_password(length: int = 12) -> str:
|
||||
"""
|
||||
Generiert ein VK-kompatibles Passwort
|
||||
"""
|
||||
# Stelle sicher dass Buchstaben und Zahlen enthalten sind
|
||||
password_chars = []
|
||||
|
||||
# Mindestens 2 Kleinbuchstaben
|
||||
password_chars.extend(random.choices(string.ascii_lowercase, k=2))
|
||||
|
||||
# Mindestens 2 Großbuchstaben
|
||||
password_chars.extend(random.choices(string.ascii_uppercase, k=2))
|
||||
|
||||
# Mindestens 2 Zahlen
|
||||
password_chars.extend(random.choices(string.digits, k=2))
|
||||
|
||||
# Fülle mit zufälligen Zeichen auf
|
||||
remaining_length = length - len(password_chars)
|
||||
all_chars = string.ascii_letters + string.digits
|
||||
password_chars.extend(random.choices(all_chars, k=remaining_length))
|
||||
|
||||
# Mische die Zeichen
|
||||
random.shuffle(password_chars)
|
||||
|
||||
return ''.join(password_chars)
|
||||
186
social_networks/vk/vk_verification.py
Normale Datei
186
social_networks/vk/vk_verification.py
Normale Datei
@ -0,0 +1,186 @@
|
||||
"""
|
||||
VK Verification - Handhabt die Telefon-Verifizierung
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, Optional
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from social_networks.vk import vk_selectors as selectors
|
||||
from social_networks.vk.vk_ui_helper import VKUIHelper
|
||||
from utils.email_handler import EmailHandler
|
||||
|
||||
logger = logging.getLogger("vk_verification")
|
||||
|
||||
class VKVerification:
|
||||
"""
|
||||
Handhabt die VK Telefon-Verifizierung
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, ui_helper: VKUIHelper, email_handler: EmailHandler = None,
|
||||
screenshots_dir: str = None, save_screenshots: bool = True):
|
||||
"""
|
||||
Initialisiert den Verification Handler
|
||||
"""
|
||||
self.page = page
|
||||
self.ui_helper = ui_helper
|
||||
self.email_handler = email_handler
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.save_screenshots = save_screenshots
|
||||
|
||||
def handle_phone_verification(self, account_data: Dict[str, str]) -> Dict[str, any]:
|
||||
"""
|
||||
Handhabt die Telefonnummer-Verifizierung
|
||||
"""
|
||||
try:
|
||||
logger.info("Starte Telefon-Verifizierung")
|
||||
|
||||
# Warte auf Telefonnummer-Eingabefeld
|
||||
if not self.ui_helper.wait_for_element(selectors.PHONE_INPUT, timeout=10000):
|
||||
logger.error("Telefonnummer-Eingabefeld nicht gefunden")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Telefonnummer-Eingabefeld nicht gefunden",
|
||||
"message": "Verifizierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
self.ui_helper.take_screenshot("phone_verification_page")
|
||||
|
||||
# Telefonnummer eingeben
|
||||
phone = account_data.get("phone", "")
|
||||
if not phone:
|
||||
logger.error("Keine Telefonnummer in account_data vorhanden")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Keine Telefonnummer angegeben",
|
||||
"message": "Telefonnummer erforderlich"
|
||||
}
|
||||
|
||||
logger.info(f"Gebe Telefonnummer ein: {phone}")
|
||||
|
||||
# Prüfe ob Ländercode-Auswahl vorhanden ist
|
||||
if self.ui_helper.is_element_visible(selectors.PHONE_COUNTRY_CODE):
|
||||
# TODO: Ländercode auswählen falls nötig
|
||||
pass
|
||||
|
||||
# Telefonnummer eingeben
|
||||
self.ui_helper.type_with_delay(selectors.PHONE_INPUT, phone)
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
# Screenshot vor dem Absenden
|
||||
self.ui_helper.take_screenshot("phone_entered")
|
||||
|
||||
# Absenden
|
||||
if self.ui_helper.wait_for_element(selectors.PHONE_SUBMIT_BUTTON, timeout=5000):
|
||||
logger.info("Sende Telefonnummer ab")
|
||||
self.ui_helper.click_with_retry(selectors.PHONE_SUBMIT_BUTTON)
|
||||
time.sleep(random.uniform(3, 5))
|
||||
|
||||
# Warte auf SMS-Code Eingabefeld
|
||||
if self.ui_helper.wait_for_element(selectors.SMS_CODE_INPUT, timeout=15000):
|
||||
logger.info("SMS-Code Eingabefeld gefunden")
|
||||
self.ui_helper.take_screenshot("sms_code_page")
|
||||
|
||||
# Hier würde normalerweise der SMS-Code abgerufen werden
|
||||
# Für Demo-Zwecke verwenden wir einen Platzhalter
|
||||
sms_code = self._get_sms_code(phone)
|
||||
|
||||
if not sms_code:
|
||||
logger.error("Kein SMS-Code erhalten")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Kein SMS-Code erhalten",
|
||||
"message": "SMS-Verifizierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
# SMS-Code eingeben
|
||||
logger.info(f"Gebe SMS-Code ein: {sms_code}")
|
||||
self.ui_helper.type_with_delay(selectors.SMS_CODE_INPUT, sms_code)
|
||||
time.sleep(random.uniform(1, 2))
|
||||
|
||||
# Code absenden
|
||||
if self.ui_helper.wait_for_element(selectors.SMS_SUBMIT_BUTTON, timeout=5000):
|
||||
logger.info("Sende SMS-Code ab")
|
||||
self.ui_helper.click_with_retry(selectors.SMS_SUBMIT_BUTTON)
|
||||
time.sleep(random.uniform(3, 5))
|
||||
|
||||
# Prüfe auf Erfolg
|
||||
if self._check_verification_success():
|
||||
logger.info("Telefon-Verifizierung erfolgreich")
|
||||
self.ui_helper.take_screenshot("verification_success")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Verifizierung erfolgreich"
|
||||
}
|
||||
else:
|
||||
error_msg = self._get_verification_error()
|
||||
logger.error(f"Verifizierung fehlgeschlagen: {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"message": f"Verifizierung fehlgeschlagen: {error_msg}"
|
||||
}
|
||||
|
||||
else:
|
||||
logger.error("SMS-Code Eingabefeld nicht gefunden")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "SMS-Code Eingabefeld nicht gefunden",
|
||||
"message": "Verifizierung fehlgeschlagen"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Telefon-Verifizierung: {e}")
|
||||
self.ui_helper.take_screenshot("verification_error")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": f"Verifizierung fehlgeschlagen: {str(e)}"
|
||||
}
|
||||
|
||||
def _get_sms_code(self, phone: str) -> Optional[str]:
|
||||
"""
|
||||
Ruft den SMS-Code ab
|
||||
TODO: Implementierung für echte SMS-Code Abfrage
|
||||
"""
|
||||
logger.warning("SMS-Code Abruf noch nicht implementiert - verwende Platzhalter")
|
||||
# In einer echten Implementierung würde hier der SMS-Code
|
||||
# von einem SMS-Service abgerufen werden
|
||||
return "123456" # Platzhalter
|
||||
|
||||
def _check_verification_success(self) -> bool:
|
||||
"""
|
||||
Prüft ob die Verifizierung erfolgreich war
|
||||
"""
|
||||
try:
|
||||
# Prüfe ob wir weitergeleitet wurden
|
||||
current_url = self.page.url
|
||||
if "welcome" in current_url or "feed" in current_url:
|
||||
return True
|
||||
|
||||
# Prüfe ob SMS-Code Feld noch sichtbar ist
|
||||
if self.ui_helper.is_element_visible(selectors.SMS_CODE_INPUT):
|
||||
return False
|
||||
|
||||
# Prüfe auf Fehlermeldung
|
||||
if self.ui_helper.is_element_visible(selectors.PHONE_ERROR):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler bei der Verifizierungs-Prüfung: {e}")
|
||||
return False
|
||||
|
||||
def _get_verification_error(self) -> str:
|
||||
"""
|
||||
Holt die Fehlermeldung falls vorhanden
|
||||
"""
|
||||
try:
|
||||
if self.ui_helper.is_element_visible(selectors.PHONE_ERROR):
|
||||
return self.ui_helper.get_element_text(selectors.PHONE_ERROR) or "Verifizierung fehlgeschlagen"
|
||||
return "Verifizierung fehlgeschlagen"
|
||||
except:
|
||||
return "Unbekannter Fehler"
|
||||
37
social_networks/vk/vk_workflow.py
Normale Datei
37
social_networks/vk/vk_workflow.py
Normale Datei
@ -0,0 +1,37 @@
|
||||
"""
|
||||
VK Workflow - Workflow-Definitionen für VK
|
||||
"""
|
||||
|
||||
# Workflow-Schritte für VK
|
||||
REGISTRATION_WORKFLOW = [
|
||||
"navigate_to_homepage",
|
||||
"click_create_account",
|
||||
"fill_registration_form",
|
||||
"handle_phone_verification",
|
||||
"complete_profile",
|
||||
"verify_account_creation"
|
||||
]
|
||||
|
||||
LOGIN_WORKFLOW = [
|
||||
"navigate_to_login",
|
||||
"enter_credentials",
|
||||
"handle_2fa_if_needed",
|
||||
"verify_login_success"
|
||||
]
|
||||
|
||||
# Timeouts in Sekunden
|
||||
TIMEOUTS = {
|
||||
"page_load": 30,
|
||||
"element_wait": 10,
|
||||
"verification_wait": 60,
|
||||
"sms_wait": 120
|
||||
}
|
||||
|
||||
# Fehler-Nachrichten
|
||||
ERROR_MESSAGES = {
|
||||
"phone_already_used": "Diese Telefonnummer wird bereits verwendet",
|
||||
"invalid_phone": "Ungültige Telefonnummer",
|
||||
"invalid_code": "Ungültiger Verifizierungscode",
|
||||
"too_many_attempts": "Zu viele Versuche",
|
||||
"account_blocked": "Account wurde blockiert"
|
||||
}
|
||||
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