Initial commit
Dieser Commit ist enthalten in:
0
social_networks/__init__.py
Normale Datei
0
social_networks/__init__.py
Normale Datei
BIN
social_networks/__pycache__/__init__.cpython-310.pyc
Normale Datei
BIN
social_networks/__pycache__/__init__.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/__pycache__/__init__.cpython-313.pyc
Normale Datei
BIN
social_networks/__pycache__/__init__.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/__pycache__/base_automation.cpython-310.pyc
Normale Datei
BIN
social_networks/__pycache__/base_automation.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/__pycache__/base_automation.cpython-313.pyc
Normale Datei
BIN
social_networks/__pycache__/base_automation.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
487
social_networks/base_automation.py
Normale Datei
487
social_networks/base_automation.py
Normale Datei
@ -0,0 +1,487 @@
|
||||
"""
|
||||
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
|
||||
|
||||
# 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"):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
self.headless = headless
|
||||
self.use_proxy = use_proxy
|
||||
self.proxy_type = proxy_type
|
||||
self.save_screenshots = save_screenshots
|
||||
self.slowmo = slowmo
|
||||
self.debug = debug
|
||||
self.email_domain = email_domain
|
||||
|
||||
# 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
|
||||
|
||||
# Status und Ergebnis der Automatisierung
|
||||
self.status = {
|
||||
"success": False,
|
||||
"stage": "initialized",
|
||||
"error": None,
|
||||
"account_data": {}
|
||||
}
|
||||
|
||||
# Debug-Logging
|
||||
if self.debug:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
logger.info(f"Basis-Automatisierung initialisiert (Proxy: {use_proxy}, Typ: {proxy_type})")
|
||||
|
||||
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()
|
||||
|
||||
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 _close_browser(self) -> None:
|
||||
"""Schließt den Browser und gibt Ressourcen frei."""
|
||||
if self.browser:
|
||||
self.browser.close()
|
||||
self.browser = None
|
||||
logger.info("Browser geschlossen")
|
||||
|
||||
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 _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 _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 get_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den aktuellen Status der Automatisierung zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Aktueller Status
|
||||
"""
|
||||
return self.status
|
||||
|
||||
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
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']
|
||||
BIN
social_networks/instagram/__pycache__/__init__.cpython-310.pyc
Normale Datei
BIN
social_networks/instagram/__pycache__/__init__.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/instagram/__pycache__/__init__.cpython-313.pyc
Normale Datei
BIN
social_networks/instagram/__pycache__/__init__.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
BIN
social_networks/instagram/__pycache__/instagram_login.cpython-310.pyc
Normale Datei
BIN
social_networks/instagram/__pycache__/instagram_login.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/instagram/__pycache__/instagram_login.cpython-313.pyc
Normale Datei
BIN
social_networks/instagram/__pycache__/instagram_login.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
BIN
social_networks/instagram/__pycache__/instagram_utils.cpython-310.pyc
Normale Datei
BIN
social_networks/instagram/__pycache__/instagram_utils.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/instagram/__pycache__/instagram_utils.cpython-313.pyc
Normale Datei
BIN
social_networks/instagram/__pycache__/instagram_utils.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
BIN
social_networks/instagram/__pycache__/instagram_workflow.cpython-310.pyc
Normale Datei
BIN
social_networks/instagram/__pycache__/instagram_workflow.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/instagram/__pycache__/instagram_workflow.cpython-313.pyc
Normale Datei
BIN
social_networks/instagram/__pycache__/instagram_workflow.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
330
social_networks/instagram/instagram_automation.py
Normale Datei
330
social_networks/instagram/instagram_automation.py
Normale Datei
@ -0,0 +1,330 @@
|
||||
# social_networks/instagram/instagram_automation.py
|
||||
|
||||
"""
|
||||
Instagram-Automatisierung - Hauptklasse für Instagram-Automatisierungsfunktionalität
|
||||
"""
|
||||
|
||||
import logging
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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):
|
||||
"""
|
||||
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
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
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
|
||||
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}")
|
||||
|
||||
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 Instagram-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 Instagram-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 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()
|
||||
576
social_networks/instagram/instagram_login.py
Normale Datei
576
social_networks/instagram/instagram_login.py
Normale Datei
@ -0,0 +1,576 @@
|
||||
# social_networks/instagram/instagram_login.py
|
||||
|
||||
"""
|
||||
Instagram-Login - Klasse für die Anmeldefunktionalitä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
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("instagram_login")
|
||||
|
||||
class InstagramLogin:
|
||||
"""
|
||||
Klasse für die Anmeldung bei Instagram-Konten.
|
||||
Enthält alle Methoden für den Login-Prozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die Instagram-Login-Funktionalität.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
self.selectors = InstagramSelectors()
|
||||
self.workflow = InstagramWorkflow.get_login_workflow()
|
||||
|
||||
logger.debug("Instagram-Login initialisiert")
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Login-Prozess für ein Instagram-Konto durch.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis des Logins mit Status
|
||||
"""
|
||||
# Hole Browser-Referenz von der Hauptklasse
|
||||
self.browser = self.automation.browser
|
||||
|
||||
# Validiere die Eingaben
|
||||
if not self._validate_login_inputs(username_or_email, password):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültige Login-Eingaben",
|
||||
"stage": "input_validation"
|
||||
}
|
||||
|
||||
# Account-Daten für die Anmeldung
|
||||
account_data = {
|
||||
"username": username_or_email,
|
||||
"password": password,
|
||||
"handle_2fa": kwargs.get("handle_2fa", False),
|
||||
"two_factor_code": kwargs.get("two_factor_code"),
|
||||
"skip_save_login": kwargs.get("skip_save_login", True)
|
||||
}
|
||||
|
||||
logger.info(f"Starte Instagram-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. "Anmeldedaten speichern"-Dialog behandeln
|
||||
if account_data["skip_save_login"]:
|
||||
self._handle_save_login_prompt()
|
||||
|
||||
# 6. Benachrichtigungsdialog behandeln
|
||||
self._handle_notifications_prompt()
|
||||
|
||||
# 7. 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"Instagram-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 Instagram-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 Instagram-Login-Seite.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Zur Login-Seite navigieren
|
||||
self.browser.navigate_to(InstagramSelectors.LOGIN_URL)
|
||||
|
||||
# Warten, bis die Seite geladen ist
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("login_page")
|
||||
|
||||
# Prüfen, ob Login-Formular sichtbar ist
|
||||
if not self.browser.is_element_visible(InstagramSelectors.LOGIN_USERNAME_FIELD, timeout=5000):
|
||||
logger.warning("Login-Formular nicht sichtbar")
|
||||
return False
|
||||
|
||||
logger.info("Erfolgreich zur Login-Seite navigiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Navigieren zur Login-Seite: {e}")
|
||||
return False
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
# Cookie-Dialog-Erkennung
|
||||
if self.browser.is_element_visible(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.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
|
||||
|
||||
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:
|
||||
# Benutzername/E-Mail eingeben
|
||||
username_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
InstagramSelectors.get_field_labels("username"),
|
||||
account_data["username"],
|
||||
InstagramSelectors.LOGIN_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.LOGIN_PASSWORD_FIELD
|
||||
)
|
||||
|
||||
if not password_success:
|
||||
logger.error("Konnte Passwort-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot vorm Absenden
|
||||
self.automation._take_screenshot("login_form_filled")
|
||||
|
||||
# Formular absenden
|
||||
submit_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Anmelden", "Log in", "Login"],
|
||||
InstagramSelectors.SUBMIT_BUTTON
|
||||
)
|
||||
|
||||
if not submit_success:
|
||||
logger.error("Konnte Login-Formular nicht absenden")
|
||||
return False
|
||||
|
||||
# Nach dem Absenden warten
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Überprüfen, ob es eine Fehlermeldung gab
|
||||
error_message = self._get_login_error()
|
||||
if error_message:
|
||||
logger.error(f"Login-Fehler erkannt: {error_message}")
|
||||
return False
|
||||
|
||||
logger.info("Login-Formular erfolgreich ausgefüllt und abgesendet")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Login-Formulars: {e}")
|
||||
return False
|
||||
|
||||
def _get_login_error(self) -> Optional[str]:
|
||||
"""
|
||||
Überprüft, ob eine Login-Fehlermeldung angezeigt wird.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Fehlermeldung oder None, wenn keine gefunden wurde
|
||||
"""
|
||||
try:
|
||||
# Auf Fehlermeldungen prüfen
|
||||
error_selectors = [
|
||||
InstagramSelectors.ERROR_MESSAGE,
|
||||
"p[class*='error']",
|
||||
"div[role='alert']",
|
||||
"p[data-testid='login-error-message']"
|
||||
]
|
||||
|
||||
for selector in error_selectors:
|
||||
error_element = self.browser.wait_for_selector(selector, timeout=2000)
|
||||
if error_element:
|
||||
error_text = error_element.text_content()
|
||||
if error_text and len(error_text.strip()) > 0:
|
||||
return error_text.strip()
|
||||
|
||||
# Wenn keine spezifische Fehlermeldung gefunden wurde, nach bekannten Fehlermustern suchen
|
||||
error_texts = [
|
||||
"Falsches Passwort",
|
||||
"Benutzername nicht gefunden",
|
||||
"incorrect password",
|
||||
"username you entered doesn't belong",
|
||||
"please wait a few minutes",
|
||||
"try again later",
|
||||
"Bitte warte einige Minuten",
|
||||
"versuche es später noch einmal"
|
||||
]
|
||||
|
||||
page_content = self.browser.page.content()
|
||||
for error_text in error_texts:
|
||||
if error_text.lower() in page_content.lower():
|
||||
return f"Erkannter Fehler: {error_text}"
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Login-Fehler: {e}")
|
||||
return None
|
||||
|
||||
def _check_needs_two_factor_auth(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Überprüft, ob eine Zwei-Faktor-Authentifizierung erforderlich ist.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (2FA erforderlich, Fehlermeldung falls vorhanden)
|
||||
"""
|
||||
try:
|
||||
# Nach 2FA-Indikatoren suchen
|
||||
two_fa_selectors = [
|
||||
"input[name='verificationCode']",
|
||||
"input[aria-label='Sicherheitscode']",
|
||||
"input[aria-label='Security code']",
|
||||
"input[placeholder*='code']"
|
||||
]
|
||||
|
||||
for selector in two_fa_selectors:
|
||||
if self.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Zwei-Faktor-Authentifizierung erforderlich")
|
||||
return True, None
|
||||
|
||||
# Texte, die auf 2FA hinweisen
|
||||
two_fa_indicators = InstagramSelectors.get_two_fa_indicators()
|
||||
|
||||
# Seiteninhalt durchsuchen
|
||||
page_content = self.browser.page.content()
|
||||
|
||||
for indicator in two_fa_indicators:
|
||||
if indicator.lower() in page_content.lower():
|
||||
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[aria-label='Sicherheitscode']",
|
||||
"input[aria-label='Security code']",
|
||||
"input[placeholder*='code']"
|
||||
]
|
||||
|
||||
two_fa_field = None
|
||||
for selector in two_fa_selectors:
|
||||
element = self.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
two_fa_field = selector
|
||||
break
|
||||
|
||||
if not two_fa_field:
|
||||
logger.error("Konnte 2FA-Eingabefeld nicht finden")
|
||||
return False
|
||||
|
||||
# Wenn kein Code bereitgestellt wurde, Benutzer auffordern
|
||||
if not two_factor_code:
|
||||
logger.warning("Kein 2FA-Code bereitgestellt, kann nicht fortfahren")
|
||||
return False
|
||||
|
||||
# 2FA-Code eingeben
|
||||
code_success = self.browser.fill_form_field(two_fa_field, two_factor_code)
|
||||
|
||||
if not code_success:
|
||||
logger.error("Konnte 2FA-Code nicht eingeben")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Bestätigen-Button finden und klicken
|
||||
confirm_button_selectors = [
|
||||
"button[type='submit']",
|
||||
"//button[contains(text(), 'Bestätigen')]",
|
||||
"//button[contains(text(), 'Confirm')]",
|
||||
"//button[contains(text(), 'Verify')]"
|
||||
]
|
||||
|
||||
confirm_clicked = False
|
||||
for selector in confirm_button_selectors:
|
||||
if self.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.browser.click_element(selector):
|
||||
confirm_clicked = True
|
||||
break
|
||||
|
||||
if not confirm_clicked:
|
||||
# Alternative: Mit Tastendruck bestätigen
|
||||
self.browser.page.keyboard.press("Enter")
|
||||
logger.info("Enter-Taste gedrückt, um 2FA zu bestätigen")
|
||||
|
||||
# Warten nach der Bestätigung
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Überprüfen, ob 2FA erfolgreich war
|
||||
still_on_2fa = self._check_needs_two_factor_auth()[0]
|
||||
|
||||
if still_on_2fa:
|
||||
# Prüfen, ob Fehlermeldung angezeigt wird
|
||||
error_message = self._get_login_error()
|
||||
if error_message:
|
||||
logger.error(f"2FA-Fehler: {error_message}")
|
||||
else:
|
||||
logger.error("2FA fehlgeschlagen, immer noch auf 2FA-Seite")
|
||||
return False
|
||||
|
||||
logger.info("Zwei-Faktor-Authentifizierung erfolgreich")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Zwei-Faktor-Authentifizierung: {e}")
|
||||
return False
|
||||
|
||||
def _handle_save_login_prompt(self) -> bool:
|
||||
"""
|
||||
Behandelt den "Anmeldedaten speichern"-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(), 'Überspringen')]"
|
||||
]
|
||||
|
||||
for selector in not_now_selectors:
|
||||
if self.browser.is_element_visible(selector, timeout=3000):
|
||||
if self.browser.click_element(selector):
|
||||
logger.info("'Anmeldedaten speichern'-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 'Anmeldedaten speichern'-Dialog erkannt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Behandeln des 'Anmeldedaten speichern'-Dialogs: {e}")
|
||||
# Dies ist nicht kritisch, daher geben wir trotzdem True zurück
|
||||
return True
|
||||
|
||||
def _handle_notifications_prompt(self) -> bool:
|
||||
"""
|
||||
Behandelt den Benachrichtigungen-Dialog.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Nach "Nicht jetzt"-Button suchen
|
||||
not_now_selectors = [
|
||||
"//button[contains(text(), 'Nicht jetzt')]",
|
||||
"//button[contains(text(), 'Not now')]",
|
||||
"//button[contains(text(), 'Skip')]",
|
||||
"//button[contains(text(), 'Später')]",
|
||||
"//button[contains(text(), 'Later')]"
|
||||
]
|
||||
|
||||
for selector in not_now_selectors:
|
||||
if self.browser.is_element_visible(selector, timeout=3000):
|
||||
if self.browser.click_element(selector):
|
||||
logger.info("Benachrichtigungen-Dialog übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
# Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden
|
||||
logger.debug("Kein Benachrichtigungen-Dialog erkannt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Behandeln des Benachrichtigungen-Dialogs: {e}")
|
||||
# Dies ist nicht kritisch, daher geben wir trotzdem True zurück
|
||||
return True
|
||||
|
||||
def _check_login_success(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob der Login erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich, False sonst
|
||||
"""
|
||||
try:
|
||||
# Warten nach dem Login
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("login_final")
|
||||
|
||||
# Erfolg anhand verschiedener Indikatoren prüfen
|
||||
success_indicators = InstagramSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self.browser.is_element_visible(indicator, timeout=2000):
|
||||
logger.info(f"Login-Erfolgsindikator gefunden: {indicator}")
|
||||
return True
|
||||
|
||||
# Alternativ prüfen, ob wir auf der 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 or self.browser.is_element_visible(InstagramSelectors.LOGIN_USERNAME_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
|
||||
735
social_networks/instagram/instagram_registration.py
Normale Datei
735
social_networks/instagram/instagram_registration.py
Normale Datei
@ -0,0 +1,735 @@
|
||||
# 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
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
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
|
||||
"""
|
||||
# Hole Browser-Referenz von der Hauptklasse
|
||||
self.browser = self.automation.browser
|
||||
|
||||
# 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
|
||||
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 behandeln
|
||||
self._handle_cookie_banner()
|
||||
|
||||
# 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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")
|
||||
|
||||
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.browser.navigate_to(InstagramSelectors.SIGNUP_URL)
|
||||
|
||||
# Warten, bis die Seite geladen ist
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("signup_page")
|
||||
|
||||
# Prüfen, ob Registrierungsformular sichtbar ist
|
||||
if not self.browser.is_element_visible(InstagramSelectors.EMAIL_PHONE_FIELD, timeout=5000):
|
||||
logger.warning("Registrierungsformular nicht sichtbar")
|
||||
return False
|
||||
|
||||
logger.info("Erfolgreich zur Registrierungsseite navigiert")
|
||||
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.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
# Cookie-Dialog-Erkennung
|
||||
if self.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.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
|
||||
|
||||
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.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.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()
|
||||
|
||||
# Nach dem Absenden prüfen, ob das Formular für das Geburtsdatum erscheint
|
||||
birthday_visible = self.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.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.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.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.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.browser.is_element_visible(
|
||||
InstagramSelectors.CONFIRMATION_CODE_FIELD, timeout=10000
|
||||
) or self.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.browser.is_element_visible(InstagramSelectors.CONFIRMATION_CODE_FIELD, timeout=5000) and not \
|
||||
self.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(
|
||||
email_domain=self.automation.email_domain,
|
||||
platform="instagram",
|
||||
timeout=120 # Warte bis zu 2 Minuten auf den Code
|
||||
)
|
||||
|
||||
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.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.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.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
|
||||
242
social_networks/instagram/instagram_selectors.py
Normale Datei
242
social_networks/instagram/instagram_selectors.py
Normale Datei
@ -0,0 +1,242 @@
|
||||
"""
|
||||
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
|
||||
SUCCESS_INDICATORS = [
|
||||
"svg[aria-label='Home']",
|
||||
"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"]
|
||||
668
social_networks/instagram/instagram_ui_helper.py
Normale Datei
668
social_networks/instagram/instagram_ui_helper.py
Normale Datei
@ -0,0 +1,668 @@
|
||||
# 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
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
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.browser is None:
|
||||
self.browser = self.automation.browser
|
||||
|
||||
if self.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.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.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.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(placeholder_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(name_selector, timeout=1000):
|
||||
if self.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
|
||||
button_selector = f"button:has-text('{text}')"
|
||||
button = self.browser.page.query_selector(button_selector)
|
||||
if button:
|
||||
logger.info(f"Button mit exaktem Text gefunden: {text}")
|
||||
button.click()
|
||||
return True
|
||||
|
||||
# Alternativer Selektor für Links und andere klickbare Elemente
|
||||
link_selector = f"a:has-text('{text}')"
|
||||
link = self.browser.page.query_selector(link_selector)
|
||||
if link:
|
||||
logger.info(f"Link mit exaktem Text gefunden: {text}")
|
||||
link.click()
|
||||
return True
|
||||
|
||||
# Versuche alle Buttons auf der Seite zu finden und zu prüfen
|
||||
all_buttons = self.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()
|
||||
logger.debug(f"Button-Text: '{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}')")
|
||||
button.click()
|
||||
return True
|
||||
|
||||
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
|
||||
if fallback_selector:
|
||||
logger.info(f"Versuche Fallback-Selektor: {fallback_selector}")
|
||||
if self.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.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(xpath_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(link_selector, timeout=1000):
|
||||
if self.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.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 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.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.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.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.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.warning(f"Captcha erkannt (Selektor): {selector}")
|
||||
return True
|
||||
|
||||
# Nach Texten suchen
|
||||
page_content = self.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.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.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.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.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.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.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.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.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
|
||||
549
social_networks/instagram/instagram_utils.py
Normale Datei
549
social_networks/instagram/instagram_utils.py
Normale Datei
@ -0,0 +1,549 @@
|
||||
# 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
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
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.browser is None:
|
||||
self.browser = self.automation.browser
|
||||
|
||||
if self.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.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.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.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.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.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.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.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.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.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.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.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.browser.is_element_visible(selector, timeout=1000):
|
||||
logger.info("Konto ist privat (Icon gefunden)")
|
||||
return True
|
||||
|
||||
# Texte prüfen
|
||||
page_content = self.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.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.browser.page.content().lower()
|
||||
|
||||
for indicator in ban_indicators:
|
||||
if indicator in page_content:
|
||||
# Vollständigen Text der Fehlermeldung extrahieren
|
||||
error_element = self.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
|
||||
491
social_networks/instagram/instagram_verification.py
Normale Datei
491
social_networks/instagram/instagram_verification.py
Normale Datei
@ -0,0 +1,491 @@
|
||||
# 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
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
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
|
||||
"""
|
||||
# Hole Browser-Referenz von der Hauptklasse
|
||||
self.browser = self.automation.browser
|
||||
|
||||
# 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.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.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 = [
|
||||
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.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Codefeld gefunden mit Selektor: {selector}")
|
||||
if self.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.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Submit-Button gefunden mit Selektor: {selector}")
|
||||
if self.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.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.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.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.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.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(selector, timeout=2000):
|
||||
if self.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
|
||||
454
social_networks/instagram/instagram_workflow.py
Normale Datei
454
social_networks/instagram/instagram_workflow.py
Normale Datei
@ -0,0 +1,454 @@
|
||||
"""
|
||||
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
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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']}")
|
||||
0
social_networks/tiktok/__init__.py
Normale Datei
0
social_networks/tiktok/__init__.py
Normale Datei
BIN
social_networks/tiktok/__pycache__/__init__.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/__init__.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/__init__.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/__init__.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_automation.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_automation.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_automation.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_automation.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_login.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_login.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_login.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_login.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_registration.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_registration.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_registration.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_registration.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_selectors.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_selectors.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_selectors.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_selectors.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_utils.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_utils.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_utils.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_utils.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_verification.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_verification.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_verification.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_verification.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_workflow.cpython-310.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_workflow.cpython-310.pyc
Normale Datei
Binäre Datei nicht angezeigt.
BIN
social_networks/tiktok/__pycache__/tiktok_workflow.cpython-313.pyc
Normale Datei
BIN
social_networks/tiktok/__pycache__/tiktok_workflow.cpython-313.pyc
Normale Datei
Binäre Datei nicht angezeigt.
328
social_networks/tiktok/tiktok_automation.py
Normale Datei
328
social_networks/tiktok/tiktok_automation.py
Normale Datei
@ -0,0 +1,328 @@
|
||||
"""
|
||||
TikTok-Automatisierung - Hauptklasse für TikTok-Automatisierungsfunktionalität
|
||||
"""
|
||||
|
||||
import logging
|
||||
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
|
||||
|
||||
# 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 = logging.getLogger("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):
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
# 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
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
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
|
||||
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}")
|
||||
|
||||
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()
|
||||
582
social_networks/tiktok/tiktok_login.py
Normale Datei
582
social_networks/tiktok/tiktok_login.py
Normale Datei
@ -0,0 +1,582 @@
|
||||
"""
|
||||
TikTok-Login - Klasse für die Anmeldefunktionalität bei TikTok
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from .tiktok_workflow import TikTokWorkflow
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("tiktok_login")
|
||||
|
||||
class TikTokLogin:
|
||||
"""
|
||||
Klasse für die Anmeldung bei TikTok-Konten.
|
||||
Enthält alle Methoden für den Login-Prozess.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Login-Funktionalität.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
self.selectors = TikTokSelectors()
|
||||
self.workflow = TikTokWorkflow.get_login_workflow()
|
||||
|
||||
logger.debug("TikTok-Login initialisiert")
|
||||
|
||||
def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt den Login-Prozess für ein TikTok-Konto durch.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
**kwargs: Weitere optionale Parameter
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Ergebnis des Logins mit Status
|
||||
"""
|
||||
# Hole Browser-Referenz von der Hauptklasse
|
||||
self.browser = self.automation.browser
|
||||
|
||||
# Validiere die Eingaben
|
||||
if not self._validate_login_inputs(username_or_email, password):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Ungültige Login-Eingaben",
|
||||
"stage": "input_validation"
|
||||
}
|
||||
|
||||
# Account-Daten für die Anmeldung
|
||||
account_data = {
|
||||
"username": username_or_email,
|
||||
"password": password,
|
||||
"handle_2fa": kwargs.get("handle_2fa", False),
|
||||
"two_factor_code": kwargs.get("two_factor_code"),
|
||||
"skip_save_login": kwargs.get("skip_save_login", True)
|
||||
}
|
||||
|
||||
logger.info(f"Starte TikTok-Login für {username_or_email}")
|
||||
|
||||
try:
|
||||
# 1. Zur Login-Seite navigieren
|
||||
if not self._navigate_to_login_page():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht zur Login-Seite navigieren",
|
||||
"stage": "navigation"
|
||||
}
|
||||
|
||||
# 2. Cookie-Banner behandeln
|
||||
self._handle_cookie_banner()
|
||||
|
||||
# 3. Login-Formular ausfüllen
|
||||
if not self._fill_login_form(account_data):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Ausfüllen des Login-Formulars",
|
||||
"stage": "login_form"
|
||||
}
|
||||
|
||||
# 4. Auf 2FA prüfen und behandeln, falls nötig
|
||||
needs_2fa, two_fa_error = self._check_needs_two_factor_auth()
|
||||
|
||||
if needs_2fa:
|
||||
if not account_data["handle_2fa"]:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Zwei-Faktor-Authentifizierung erforderlich, aber nicht aktiviert",
|
||||
"stage": "two_factor_required"
|
||||
}
|
||||
|
||||
# 2FA behandeln
|
||||
if not self._handle_two_factor_auth(account_data["two_factor_code"]):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler bei der Zwei-Faktor-Authentifizierung",
|
||||
"stage": "two_factor_auth"
|
||||
}
|
||||
|
||||
# 5. Benachrichtigungserlaubnis-Dialog behandeln
|
||||
self._handle_notifications_prompt()
|
||||
|
||||
# 6. Erfolgreichen Login überprüfen
|
||||
if not self._check_login_success():
|
||||
error_message = self._get_login_error()
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Login fehlgeschlagen: {error_message or 'Unbekannter Fehler'}",
|
||||
"stage": "login_check"
|
||||
}
|
||||
|
||||
# Login erfolgreich
|
||||
logger.info(f"TikTok-Login für {username_or_email} erfolgreich")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stage": "completed",
|
||||
"username": username_or_email
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unerwarteter Fehler beim TikTok-Login: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": "exception"
|
||||
}
|
||||
|
||||
def _validate_login_inputs(self, username_or_email: str, password: str) -> bool:
|
||||
"""
|
||||
Validiert die Eingaben für den Login.
|
||||
|
||||
Args:
|
||||
username_or_email: Benutzername oder E-Mail-Adresse
|
||||
password: Passwort
|
||||
|
||||
Returns:
|
||||
bool: True wenn alle Eingaben gültig sind, False sonst
|
||||
"""
|
||||
if not username_or_email or len(username_or_email) < 3:
|
||||
logger.error("Ungültiger Benutzername oder E-Mail")
|
||||
return False
|
||||
|
||||
if not password or len(password) < 6:
|
||||
logger.error("Ungültiges Passwort")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _navigate_to_login_page(self) -> bool:
|
||||
"""
|
||||
Navigiert zur TikTok-Login-Seite.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Zur Login-Seite navigieren
|
||||
self.browser.navigate_to(TikTokSelectors.LOGIN_URL)
|
||||
|
||||
# Warten, bis die Seite geladen ist
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("login_page")
|
||||
|
||||
# Prüfen, ob Login-Dialog sichtbar ist
|
||||
if not self.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=5000):
|
||||
logger.warning("Login-Dialog nicht sichtbar, versuche Login-Button zu klicken")
|
||||
|
||||
# Versuche, den Login-Button zu klicken, um das Login-Modal zu öffnen
|
||||
login_buttons = [
|
||||
TikTokSelectors.LOGIN_BUTTON_TOP,
|
||||
TikTokSelectors.LOGIN_BUTTON_SIDEBAR
|
||||
]
|
||||
|
||||
button_clicked = False
|
||||
for button in login_buttons:
|
||||
if self.browser.is_element_visible(button, timeout=2000):
|
||||
self.browser.click_element(button)
|
||||
button_clicked = True
|
||||
break
|
||||
|
||||
if not button_clicked:
|
||||
logger.warning("Keine Login-Buttons gefunden")
|
||||
return False
|
||||
|
||||
# Warten, bis der Login-Dialog erscheint
|
||||
self.automation.human_behavior.wait_between_actions("decision", 1.5)
|
||||
|
||||
# Erneut prüfen, ob der Login-Dialog sichtbar ist
|
||||
if not self.browser.is_element_visible(TikTokSelectors.LOGIN_DIALOG, timeout=5000):
|
||||
logger.warning("Login-Dialog nach dem Klicken auf den Login-Button nicht sichtbar")
|
||||
return False
|
||||
|
||||
logger.info("Erfolgreich zur Login-Seite navigiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Navigieren zur Login-Seite: {e}")
|
||||
return False
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
# Cookie-Dialog-Erkennung
|
||||
if self.browser.is_element_visible(TikTokSelectors.COOKIE_DIALOG, timeout=2000):
|
||||
logger.info("Cookie-Banner erkannt")
|
||||
|
||||
# Ablehnen-Button suchen und klicken
|
||||
reject_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
TikTokSelectors.get_button_texts("reject_cookies"),
|
||||
TikTokSelectors.COOKIE_REJECT_BUTTON
|
||||
)
|
||||
|
||||
if reject_success:
|
||||
logger.info("Cookie-Banner erfolgreich abgelehnt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
else:
|
||||
logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren")
|
||||
|
||||
# Akzeptieren-Button als Fallback
|
||||
accept_success = self.browser.click_element(TikTokSelectors.COOKIE_ACCEPT_BUTTON)
|
||||
|
||||
if accept_success:
|
||||
logger.info("Cookie-Banner erfolgreich akzeptiert")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
else:
|
||||
logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren")
|
||||
return False
|
||||
else:
|
||||
logger.debug("Kein Cookie-Banner erkannt")
|
||||
return True
|
||||
|
||||
def _fill_login_form(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Füllt das Login-Formular aus und sendet es ab.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten für den Login
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# E-Mail/Telefon-Login-Option auswählen
|
||||
email_phone_option = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Telefon-Nr./E-Mail/Anmeldename nutzen", "Use phone / email / username"],
|
||||
TikTokSelectors.LOGIN_EMAIL_PHONE_OPTION
|
||||
)
|
||||
|
||||
if not email_phone_option:
|
||||
logger.warning("Konnte die E-Mail/Telefon-Option nicht auswählen, versuche direkt das Formular auszufüllen")
|
||||
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
|
||||
# E-Mail/Benutzername eingeben
|
||||
username_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
TikTokSelectors.get_field_labels("email_username"),
|
||||
account_data["username"],
|
||||
TikTokSelectors.LOGIN_EMAIL_FIELD
|
||||
)
|
||||
|
||||
if not username_success:
|
||||
logger.error("Konnte Benutzername-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
|
||||
# Passwort eingeben
|
||||
password_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
TikTokSelectors.get_field_labels("password"),
|
||||
account_data["password"],
|
||||
TikTokSelectors.LOGIN_PASSWORD_FIELD
|
||||
)
|
||||
|
||||
if not password_success:
|
||||
logger.error("Konnte Passwort-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Screenshot vorm Absenden
|
||||
self.automation._take_screenshot("login_form_filled")
|
||||
|
||||
# Formular absenden
|
||||
submit_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Anmelden", "Log in", "Login"],
|
||||
TikTokSelectors.LOGIN_SUBMIT_BUTTON
|
||||
)
|
||||
|
||||
if not submit_success:
|
||||
logger.error("Konnte Login-Formular nicht absenden")
|
||||
return False
|
||||
|
||||
# Nach dem Absenden warten
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Überprüfen, ob es eine Fehlermeldung gab
|
||||
error_message = self._get_login_error()
|
||||
if error_message:
|
||||
logger.error(f"Login-Fehler erkannt: {error_message}")
|
||||
return False
|
||||
|
||||
logger.info("Login-Formular erfolgreich ausgefüllt und abgesendet")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Login-Formulars: {e}")
|
||||
return False
|
||||
|
||||
def _get_login_error(self) -> Optional[str]:
|
||||
"""
|
||||
Überprüft, ob eine Login-Fehlermeldung angezeigt wird.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Fehlermeldung oder None, wenn keine gefunden wurde
|
||||
"""
|
||||
try:
|
||||
# Auf Fehlermeldungen prüfen
|
||||
error_selectors = [
|
||||
TikTokSelectors.ERROR_MESSAGE,
|
||||
"p[class*='error']",
|
||||
"div[role='alert']",
|
||||
"div[class*='error']",
|
||||
"div[class*='Error']"
|
||||
]
|
||||
|
||||
for selector in error_selectors:
|
||||
error_element = self.browser.wait_for_selector(selector, timeout=2000)
|
||||
if error_element:
|
||||
error_text = error_element.text_content()
|
||||
if error_text and len(error_text.strip()) > 0:
|
||||
return error_text.strip()
|
||||
|
||||
# Wenn keine spezifische Fehlermeldung gefunden wurde, nach bekannten Fehlermustern suchen
|
||||
error_texts = [
|
||||
"Falsches Passwort",
|
||||
"Benutzername nicht gefunden",
|
||||
"incorrect password",
|
||||
"username you entered doesn't belong",
|
||||
"please wait a few minutes",
|
||||
"try again later",
|
||||
"Bitte warte einige Minuten",
|
||||
"versuche es später noch einmal"
|
||||
]
|
||||
|
||||
page_content = self.browser.page.content()
|
||||
for error_text in error_texts:
|
||||
if error_text.lower() in page_content.lower():
|
||||
return f"Erkannter Fehler: {error_text}"
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf Login-Fehler: {e}")
|
||||
return None
|
||||
|
||||
def _check_needs_two_factor_auth(self) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Überprüft, ob eine Zwei-Faktor-Authentifizierung erforderlich ist.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (2FA erforderlich, Fehlermeldung falls vorhanden)
|
||||
"""
|
||||
try:
|
||||
# Nach 2FA-Indikatoren suchen
|
||||
two_fa_selectors = [
|
||||
"input[name='verificationCode']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder*='Code']",
|
||||
"div[class*='verification-code']",
|
||||
"div[class*='two-factor']"
|
||||
]
|
||||
|
||||
for selector in two_fa_selectors:
|
||||
if self.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info("Zwei-Faktor-Authentifizierung erforderlich")
|
||||
return True, None
|
||||
|
||||
# Texte, die auf 2FA hinweisen
|
||||
two_fa_indicators = [
|
||||
"Verifizierungscode",
|
||||
"Verification code",
|
||||
"Sicherheitscode",
|
||||
"Security code",
|
||||
"zwei-faktor",
|
||||
"two-factor",
|
||||
"2FA"
|
||||
]
|
||||
|
||||
# Seiteninhalt durchsuchen
|
||||
page_content = self.browser.page.content().lower()
|
||||
|
||||
for indicator in two_fa_indicators:
|
||||
if indicator.lower() in page_content:
|
||||
logger.info(f"Zwei-Faktor-Authentifizierung erkannt durch Text: {indicator}")
|
||||
return True, None
|
||||
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen auf 2FA: {e}")
|
||||
return False, f"Fehler bei der 2FA-Erkennung: {str(e)}"
|
||||
|
||||
def _handle_two_factor_auth(self, two_factor_code: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Behandelt die Zwei-Faktor-Authentifizierung.
|
||||
|
||||
Args:
|
||||
two_factor_code: Optional vorhandener 2FA-Code
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("two_factor_auth")
|
||||
|
||||
# 2FA-Eingabefeld finden
|
||||
two_fa_selectors = [
|
||||
"input[name='verificationCode']",
|
||||
"input[placeholder*='code']",
|
||||
"input[placeholder*='Code']"
|
||||
]
|
||||
|
||||
two_fa_field = None
|
||||
for selector in two_fa_selectors:
|
||||
element = self.browser.wait_for_selector(selector, timeout=2000)
|
||||
if element:
|
||||
two_fa_field = selector
|
||||
break
|
||||
|
||||
if not two_fa_field:
|
||||
logger.error("Konnte 2FA-Eingabefeld nicht finden")
|
||||
return False
|
||||
|
||||
# Wenn kein Code bereitgestellt wurde, Benutzer auffordern
|
||||
if not two_factor_code:
|
||||
logger.warning("Kein 2FA-Code bereitgestellt, kann nicht fortfahren")
|
||||
return False
|
||||
|
||||
# 2FA-Code eingeben
|
||||
code_success = self.browser.fill_form_field(two_fa_field, two_factor_code)
|
||||
|
||||
if not code_success:
|
||||
logger.error("Konnte 2FA-Code nicht eingeben")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Bestätigen-Button finden und klicken
|
||||
confirm_button_selectors = [
|
||||
"button[type='submit']",
|
||||
"//button[contains(text(), 'Bestätigen')]",
|
||||
"//button[contains(text(), 'Confirm')]",
|
||||
"//button[contains(text(), 'Verify')]"
|
||||
]
|
||||
|
||||
confirm_clicked = False
|
||||
for selector in confirm_button_selectors:
|
||||
if self.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.browser.click_element(selector):
|
||||
confirm_clicked = True
|
||||
break
|
||||
|
||||
if not confirm_clicked:
|
||||
# Alternative: Mit Tastendruck bestätigen
|
||||
self.browser.page.keyboard.press("Enter")
|
||||
logger.info("Enter-Taste gedrückt, um 2FA zu bestätigen")
|
||||
|
||||
# Warten nach der Bestätigung
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Überprüfen, ob 2FA erfolgreich war
|
||||
still_on_2fa = self._check_needs_two_factor_auth()[0]
|
||||
|
||||
if still_on_2fa:
|
||||
# Prüfen, ob Fehlermeldung angezeigt wird
|
||||
error_message = self._get_login_error()
|
||||
if error_message:
|
||||
logger.error(f"2FA-Fehler: {error_message}")
|
||||
else:
|
||||
logger.error("2FA fehlgeschlagen, immer noch auf 2FA-Seite")
|
||||
return False
|
||||
|
||||
logger.info("Zwei-Faktor-Authentifizierung erfolgreich")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Zwei-Faktor-Authentifizierung: {e}")
|
||||
return False
|
||||
|
||||
def _handle_notifications_prompt(self) -> bool:
|
||||
"""
|
||||
Behandelt den Benachrichtigungen-Dialog.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Nach "Nicht jetzt"-Button suchen
|
||||
not_now_selectors = [
|
||||
"//button[contains(text(), 'Nicht jetzt')]",
|
||||
"//button[contains(text(), 'Not now')]",
|
||||
"//button[contains(text(), 'Skip')]",
|
||||
"//button[contains(text(), 'Später')]",
|
||||
"//button[contains(text(), 'Later')]"
|
||||
]
|
||||
|
||||
for selector in not_now_selectors:
|
||||
if self.browser.is_element_visible(selector, timeout=3000):
|
||||
if self.browser.click_element(selector):
|
||||
logger.info("Benachrichtigungen-Dialog übersprungen")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
# Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden
|
||||
logger.debug("Kein Benachrichtigungen-Dialog erkannt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Behandeln des Benachrichtigungen-Dialogs: {e}")
|
||||
# Dies ist nicht kritisch, daher geben wir trotzdem True zurück
|
||||
return True
|
||||
|
||||
def _check_login_success(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob der Login erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich, False sonst
|
||||
"""
|
||||
try:
|
||||
# Warten nach dem Login
|
||||
self.automation.human_behavior.wait_for_page_load(multiplier=1.5)
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("login_final")
|
||||
|
||||
# Erfolg anhand verschiedener Indikatoren prüfen
|
||||
success_indicators = TikTokSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self.browser.is_element_visible(indicator, timeout=2000):
|
||||
logger.info(f"Login-Erfolgsindikator gefunden: {indicator}")
|
||||
return True
|
||||
|
||||
# Alternativ prüfen, ob wir auf der TikTok-Startseite sind
|
||||
current_url = self.browser.page.url
|
||||
if "tiktok.com" in current_url and "/login" not in current_url:
|
||||
logger.info(f"Login-Erfolg basierend auf URL: {current_url}")
|
||||
return True
|
||||
|
||||
# Prüfen, ob immer noch auf der Login-Seite
|
||||
if "/login" in current_url or self.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=1000):
|
||||
logger.warning("Immer noch auf der Login-Seite, Login fehlgeschlagen")
|
||||
return False
|
||||
|
||||
logger.warning("Keine Login-Erfolgsindikatoren gefunden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Überprüfen des Login-Erfolgs: {e}")
|
||||
return False
|
||||
987
social_networks/tiktok/tiktok_registration.py
Normale Datei
987
social_networks/tiktok/tiktok_registration.py
Normale Datei
@ -0,0 +1,987 @@
|
||||
# social_networks/tiktok/tiktok_registration.py
|
||||
|
||||
"""
|
||||
TikTok-Registrierung - Klasse für die Kontoerstellung bei TikTok
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from .tiktok_workflow import TikTokWorkflow
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("tiktok_registration")
|
||||
|
||||
class TikTokRegistration:
|
||||
"""
|
||||
Klasse für die Registrierung von TikTok-Konten.
|
||||
Enthält alle Methoden zur Kontoerstellung.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Registrierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
self.selectors = TikTokSelectors()
|
||||
self.workflow = TikTokWorkflow.get_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
|
||||
"""
|
||||
# Hole Browser-Referenz von der Hauptklasse
|
||||
self.browser = self.automation.browser
|
||||
|
||||
# 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 TikTok-Registrierung für {account_data['username']} via {registration_method}")
|
||||
|
||||
try:
|
||||
# 1. Zur Startseite navigieren
|
||||
if not self._navigate_to_homepage():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht zur TikTok-Startseite navigieren",
|
||||
"stage": "navigation",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 2. Cookie-Banner behandeln
|
||||
self._handle_cookie_banner()
|
||||
|
||||
# 3. Anmelden-Button klicken
|
||||
if not self._click_login_button():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht auf Anmelden-Button klicken",
|
||||
"stage": "login_button",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 4. Registrieren-Link klicken
|
||||
if not self._click_register_link():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht auf Registrieren-Link klicken",
|
||||
"stage": "register_link",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 5. Telefon/E-Mail-Option auswählen
|
||||
if not self._click_phone_email_option():
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Konnte nicht auf Telefon/E-Mail-Option klicken",
|
||||
"stage": "phone_email_option",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 6. E-Mail oder Telefon als 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
|
||||
}
|
||||
|
||||
# 7. Geburtsdatum eingeben
|
||||
if not self._enter_birthday(account_data["birthday"]):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Eingeben des Geburtsdatums",
|
||||
"stage": "birthday",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 8. Registrierungsformular ausfüllen
|
||||
if not self._fill_registration_form(account_data, registration_method):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Ausfüllen des Registrierungsformulars",
|
||||
"stage": "registration_form",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 9. Bestätigungscode abrufen und eingeben
|
||||
if not self._handle_verification(account_data, registration_method):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler bei der Verifizierung",
|
||||
"stage": "verification",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 10. Benutzernamen erstellen
|
||||
if not self._create_username(account_data):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Fehler beim Erstellen des Benutzernamens",
|
||||
"stage": "username",
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# 11. Erfolgreiche Registrierung überprüfen
|
||||
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"TikTok-Account {account_data['username']} 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 _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("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 _navigate_to_homepage(self) -> bool:
|
||||
"""
|
||||
Navigiert zur TikTok-Startseite.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Zur Startseite navigieren
|
||||
self.browser.navigate_to(TikTokSelectors.BASE_URL)
|
||||
|
||||
# Warten, bis die Seite geladen ist
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Screenshot erstellen
|
||||
self.automation._take_screenshot("tiktok_homepage")
|
||||
|
||||
# Prüfen, ob die Seite korrekt geladen wurde
|
||||
if not self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON, timeout=5000):
|
||||
logger.warning("TikTok-Startseite nicht korrekt geladen")
|
||||
return False
|
||||
|
||||
logger.info("Erfolgreich zur TikTok-Startseite navigiert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Navigieren zur TikTok-Startseite: {e}")
|
||||
return False
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""
|
||||
Behandelt den Cookie-Banner, falls angezeigt.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||
"""
|
||||
# Cookie-Dialog-Erkennung
|
||||
if self.browser.is_element_visible(TikTokSelectors.COOKIE_DIALOG, timeout=2000):
|
||||
logger.info("Cookie-Banner erkannt")
|
||||
|
||||
# Ablehnen-Button suchen und klicken
|
||||
reject_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
TikTokSelectors.get_button_texts("reject_cookies"),
|
||||
TikTokSelectors.COOKIE_REJECT_BUTTON
|
||||
)
|
||||
|
||||
if reject_success:
|
||||
logger.info("Cookie-Banner erfolgreich abgelehnt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
else:
|
||||
logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren")
|
||||
|
||||
# Akzeptieren-Button als Fallback
|
||||
accept_success = self.browser.click_element(TikTokSelectors.COOKIE_ACCEPT_BUTTON)
|
||||
|
||||
if accept_success:
|
||||
logger.info("Cookie-Banner erfolgreich akzeptiert")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
else:
|
||||
logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren")
|
||||
return False
|
||||
else:
|
||||
logger.debug("Kein Cookie-Banner erkannt")
|
||||
return True
|
||||
|
||||
def _click_login_button(self) -> bool:
|
||||
"""
|
||||
Klickt auf den Anmelden-Button auf der Startseite.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Versuche zuerst den Hauptbutton
|
||||
if self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON, timeout=2000):
|
||||
result = self.browser.click_element(TikTokSelectors.LOGIN_BUTTON)
|
||||
if result:
|
||||
logger.info("Anmelden-Button erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
# Versuche alternativ den Button in der oberen rechten Ecke
|
||||
if self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON_TOP_RIGHT, timeout=2000):
|
||||
result = self.browser.click_element(TikTokSelectors.LOGIN_BUTTON_TOP_RIGHT)
|
||||
if result:
|
||||
logger.info("Anmelden-Button (oben rechts) erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
# Versuche es mit Fuzzy-Button-Matching
|
||||
result = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Anmelden", "Log in", "Login"],
|
||||
TikTokSelectors.LOGIN_BUTTON_FALLBACK
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info("Anmelden-Button über Fuzzy-Matching erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
logger.error("Konnte keinen Anmelden-Button finden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken auf den Anmelden-Button: {e}")
|
||||
return False
|
||||
|
||||
def _click_register_link(self) -> bool:
|
||||
"""
|
||||
Klickt auf den Registrieren-Link im Login-Dialog.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Warten, bis der Login-Dialog angezeigt wird
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Prüfen, ob wir bereits im Registrierungsdialog sind
|
||||
if self.browser.is_element_visible(TikTokSelectors.REGISTER_DIALOG_TITLE, timeout=2000):
|
||||
logger.info("Bereits im Registrierungsdialog")
|
||||
return True
|
||||
|
||||
# Versuche, den Registrieren-Link zu finden und zu klicken
|
||||
if self.browser.is_element_visible(TikTokSelectors.REGISTER_LINK, timeout=2000):
|
||||
result = self.browser.click_element(TikTokSelectors.REGISTER_LINK)
|
||||
if result:
|
||||
logger.info("Registrieren-Link erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
# Versuche es mit Fuzzy-Button-Matching
|
||||
result = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Registrieren", "Sign up", "Konto erstellen", "Register"],
|
||||
TikTokSelectors.REGISTER_LINK_FALLBACK
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info("Registrieren-Link über Fuzzy-Matching erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
# Prüfe, ob der Text "Du hast noch kein Konto? Registrieren" vorhanden ist
|
||||
register_link_text = "Du hast noch kein Konto? Registrieren"
|
||||
elements = self.browser.page.query_selector_all("*")
|
||||
for element in elements:
|
||||
if register_link_text in element.inner_text():
|
||||
# Finde das "Registrieren"-Wort und klicke darauf
|
||||
matches = re.search(r"(.*?)(Registrieren)$", element.inner_text())
|
||||
if matches:
|
||||
# Versuche, nur auf das Wort "Registrieren" zu klicken
|
||||
element.click()
|
||||
logger.info("Auf 'Registrieren' Text geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
logger.error("Konnte keinen Registrieren-Link finden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken auf den Registrieren-Link: {e}")
|
||||
return False
|
||||
|
||||
def _click_phone_email_option(self) -> bool:
|
||||
"""
|
||||
Klickt auf die Telefon/E-Mail-Option im Registrierungsdialog.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Warten, bis der Registrierungsdialog angezeigt wird
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Prüfen, ob wir bereits die Optionen für Telefon/E-Mail sehen
|
||||
if self.browser.is_element_visible(TikTokSelectors.EMAIL_FIELD, timeout=2000) or \
|
||||
self.browser.is_element_visible(TikTokSelectors.PHONE_FIELD, timeout=2000):
|
||||
logger.info("Bereits auf der Telefon/E-Mail-Registrierungsseite")
|
||||
return True
|
||||
|
||||
# Versuche, die Telefon/E-Mail-Option zu finden und zu klicken
|
||||
if self.browser.is_element_visible(TikTokSelectors.PHONE_EMAIL_OPTION, timeout=2000):
|
||||
result = self.browser.click_element(TikTokSelectors.PHONE_EMAIL_OPTION)
|
||||
if result:
|
||||
logger.info("Telefon/E-Mail-Option erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
# Versuche es mit Fuzzy-Button-Matching
|
||||
result = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Telefonnummer oder E-Mail-Adresse nutzen", "Use phone or email", "Phone or email"],
|
||||
TikTokSelectors.PHONE_EMAIL_OPTION_FALLBACK
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info("Telefon/E-Mail-Option über Fuzzy-Matching erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
logger.error("Konnte keine Telefon/E-Mail-Option finden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken auf die Telefon/E-Mail-Option: {e}")
|
||||
return False
|
||||
|
||||
def _select_registration_method(self, registration_method: str) -> bool:
|
||||
"""
|
||||
Wählt die Registrierungsmethode (E-Mail oder Telefon).
|
||||
|
||||
Args:
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Warten, bis die Registrierungsmethoden-Seite geladen ist
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
if registration_method == "email":
|
||||
# Wenn bereits das E-Mail-Feld sichtbar ist, sind wir schon auf der richtigen Seite
|
||||
if self.browser.is_element_visible(TikTokSelectors.EMAIL_FIELD, timeout=1000):
|
||||
logger.info("Bereits auf der E-Mail-Registrierungsseite")
|
||||
return True
|
||||
|
||||
# Suche nach dem "Mit E-Mail-Adresse registrieren" Link
|
||||
if self.browser.is_element_visible(TikTokSelectors.EMAIL_OPTION, timeout=2000):
|
||||
result = self.browser.click_element(TikTokSelectors.EMAIL_OPTION)
|
||||
if result:
|
||||
logger.info("E-Mail-Registrierungsmethode erfolgreich ausgewählt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
# Versuche es mit Fuzzy-Button-Matching
|
||||
result = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Mit E-Mail-Adresse registrieren", "Register with email", "E-Mail-Adresse"],
|
||||
TikTokSelectors.EMAIL_OPTION_FALLBACK
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info("E-Mail-Option über Fuzzy-Matching erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
elif registration_method == "phone":
|
||||
# Wenn bereits das Telefon-Feld sichtbar ist, sind wir schon auf der richtigen Seite
|
||||
if self.browser.is_element_visible(TikTokSelectors.PHONE_FIELD, timeout=1000):
|
||||
logger.info("Bereits auf der Telefon-Registrierungsseite")
|
||||
return True
|
||||
|
||||
# Suche nach dem "Mit Telefonnummer registrieren" Link
|
||||
if self.browser.is_element_visible(TikTokSelectors.PHONE_OPTION, timeout=2000):
|
||||
result = self.browser.click_element(TikTokSelectors.PHONE_OPTION)
|
||||
if result:
|
||||
logger.info("Telefon-Registrierungsmethode erfolgreich ausgewählt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
# Versuche es mit Fuzzy-Button-Matching
|
||||
result = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Mit Telefonnummer registrieren", "Register with phone", "Telefonnummer"],
|
||||
TikTokSelectors.PHONE_OPTION_FALLBACK
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info("Telefon-Option über Fuzzy-Matching erfolgreich geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
return True
|
||||
|
||||
logger.error(f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Auswählen der Registrierungsmethode: {e}")
|
||||
return False
|
||||
|
||||
def _enter_birthday(self, birthday: Dict[str, int]) -> bool:
|
||||
"""
|
||||
Gibt das Geburtsdatum ein.
|
||||
|
||||
Args:
|
||||
birthday: Dictionary mit 'year', 'month', 'day' Schlüsseln
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Warten, bis die Geburtstagsauswahl angezeigt wird
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Monat auswählen
|
||||
month_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_MONTH_DROPDOWN, timeout=5000)
|
||||
if not month_dropdown:
|
||||
logger.error("Monat-Dropdown nicht gefunden")
|
||||
return False
|
||||
|
||||
month_dropdown.click()
|
||||
self.automation.human_behavior.random_delay(0.3, 0.8)
|
||||
|
||||
# Monat-Option auswählen
|
||||
month_option = self.browser.wait_for_selector(
|
||||
TikTokSelectors.get_month_option_selector(birthday["month"]),
|
||||
timeout=3000
|
||||
)
|
||||
if month_option:
|
||||
month_option.click()
|
||||
logger.info(f"Monat {birthday['month']} ausgewählt")
|
||||
else:
|
||||
# Fallback: Monat über Select-Funktion auswählen
|
||||
month_success = self.browser.select_option(
|
||||
TikTokSelectors.BIRTHDAY_MONTH_DROPDOWN,
|
||||
str(birthday["month"])
|
||||
)
|
||||
if not month_success:
|
||||
logger.error(f"Konnte Monat {birthday['month']} nicht auswählen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.3, 0.8)
|
||||
|
||||
# Tag auswählen
|
||||
day_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_DAY_DROPDOWN, timeout=3000)
|
||||
if not day_dropdown:
|
||||
logger.error("Tag-Dropdown nicht gefunden")
|
||||
return False
|
||||
|
||||
day_dropdown.click()
|
||||
self.automation.human_behavior.random_delay(0.3, 0.8)
|
||||
|
||||
# Tag-Option auswählen
|
||||
day_option = self.browser.wait_for_selector(
|
||||
TikTokSelectors.get_day_option_selector(birthday["day"]),
|
||||
timeout=3000
|
||||
)
|
||||
if day_option:
|
||||
day_option.click()
|
||||
logger.info(f"Tag {birthday['day']} ausgewählt")
|
||||
else:
|
||||
# Fallback: Tag über Select-Funktion auswählen
|
||||
day_success = self.browser.select_option(
|
||||
TikTokSelectors.BIRTHDAY_DAY_DROPDOWN,
|
||||
str(birthday["day"])
|
||||
)
|
||||
if not day_success:
|
||||
logger.error(f"Konnte Tag {birthday['day']} nicht auswählen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(0.3, 0.8)
|
||||
|
||||
# Jahr auswählen
|
||||
year_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_YEAR_DROPDOWN, timeout=3000)
|
||||
if not year_dropdown:
|
||||
logger.error("Jahr-Dropdown nicht gefunden")
|
||||
return False
|
||||
|
||||
year_dropdown.click()
|
||||
self.automation.human_behavior.random_delay(0.3, 0.8)
|
||||
|
||||
# Jahr-Option auswählen
|
||||
year_option = self.browser.wait_for_selector(
|
||||
TikTokSelectors.get_year_option_selector(birthday["year"]),
|
||||
timeout=3000
|
||||
)
|
||||
if year_option:
|
||||
year_option.click()
|
||||
logger.info(f"Jahr {birthday['year']} ausgewählt")
|
||||
else:
|
||||
# Fallback: Jahr über Select-Funktion auswählen
|
||||
year_success = self.browser.select_option(
|
||||
TikTokSelectors.BIRTHDAY_YEAR_DROPDOWN,
|
||||
str(birthday["year"])
|
||||
)
|
||||
if not year_success:
|
||||
logger.error(f"Konnte Jahr {birthday['year']} nicht auswählen")
|
||||
return False
|
||||
|
||||
logger.info(f"Geburtsdatum {birthday['month']}/{birthday['day']}/{birthday['year']} erfolgreich eingegeben")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Eingeben des Geburtsdatums: {e}")
|
||||
return False
|
||||
|
||||
def _fill_registration_form(self, account_data: Dict[str, Any], registration_method: str) -> bool:
|
||||
"""
|
||||
Füllt das Registrierungsformular aus.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten für die Registrierung
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Je nach Registrierungsmethode das entsprechende Feld ausfüllen
|
||||
if registration_method == "email":
|
||||
# E-Mail-Feld ausfüllen
|
||||
email_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["E-Mail-Adresse", "Email", "E-Mail"],
|
||||
account_data["email"],
|
||||
TikTokSelectors.EMAIL_FIELD
|
||||
)
|
||||
|
||||
if not email_success:
|
||||
logger.error("Konnte E-Mail-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
logger.info(f"E-Mail-Feld ausgefüllt: {account_data['email']}")
|
||||
|
||||
elif registration_method == "phone":
|
||||
# Telefonnummer-Feld ausfüllen (ohne Ländervorwahl)
|
||||
phone_number = account_data["phone"]
|
||||
if phone_number.startswith("+"):
|
||||
# Entferne Ländervorwahl, wenn vorhanden
|
||||
parts = phone_number.split(" ", 1)
|
||||
if len(parts) > 1:
|
||||
phone_number = parts[1]
|
||||
|
||||
phone_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Telefonnummer", "Phone number", "Phone"],
|
||||
phone_number,
|
||||
TikTokSelectors.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)
|
||||
|
||||
# Bei E-Mail-Registrierung auch das Passwort-Feld ausfüllen
|
||||
if registration_method == "email":
|
||||
password_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Passwort", "Password"],
|
||||
account_data["password"],
|
||||
TikTokSelectors.PASSWORD_FIELD
|
||||
)
|
||||
|
||||
if not password_success:
|
||||
logger.error("Konnte Passwort-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
logger.info("Passwort-Feld ausgefüllt")
|
||||
|
||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||
|
||||
# Code senden Button klicken
|
||||
send_code_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Code senden", "Send code", "Send verification code"],
|
||||
TikTokSelectors.SEND_CODE_BUTTON
|
||||
)
|
||||
|
||||
if not send_code_success:
|
||||
logger.error("Konnte 'Code senden'-Button nicht klicken")
|
||||
return False
|
||||
|
||||
logger.info("'Code senden'-Button erfolgreich geklickt")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Registrierungsformulars: {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 der Bestätigungscode gesendet wurde
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
self.automation.human_behavior.random_delay(2.0, 4.0)
|
||||
|
||||
# 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-Feld ausfüllen
|
||||
code_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Gib den sechsstelligen Code ein", "Enter verification code", "Verification code"],
|
||||
verification_code,
|
||||
TikTokSelectors.VERIFICATION_CODE_FIELD
|
||||
)
|
||||
|
||||
if not code_success:
|
||||
logger.error("Konnte Verifizierungscode-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Weiter-Button klicken
|
||||
continue_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Weiter", "Continue", "Next", "Submit"],
|
||||
TikTokSelectors.CONTINUE_BUTTON
|
||||
)
|
||||
|
||||
if not continue_success:
|
||||
logger.error("Konnte 'Weiter'-Button nicht klicken")
|
||||
return False
|
||||
|
||||
logger.info("Verifizierungscode eingegeben und 'Weiter' geklickt")
|
||||
|
||||
# Warten nach der Verifizierung
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
return True
|
||||
|
||||
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(
|
||||
email_domain=self.automation.email_domain,
|
||||
platform="tiktok",
|
||||
timeout=120 # Warte bis zu 2 Minuten auf den Code
|
||||
)
|
||||
|
||||
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.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 TikTok-Code",
|
||||
r"(\d{6}) is your TikTok 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 _create_username(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Erstellt einen Benutzernamen.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Warten, bis die Benutzernamen-Seite geladen ist
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
# Prüfen, ob wir auf der Benutzernamen-Seite sind
|
||||
if not self.browser.is_element_visible(TikTokSelectors.USERNAME_FIELD, timeout=5000):
|
||||
logger.warning("Benutzernamen-Feld nicht gefunden, möglicherweise ist dieser Schritt übersprungen worden")
|
||||
|
||||
# Versuche, den "Überspringen"-Button zu klicken, falls vorhanden
|
||||
skip_visible = self.browser.is_element_visible(TikTokSelectors.SKIP_USERNAME_BUTTON, timeout=2000)
|
||||
if skip_visible:
|
||||
self.browser.click_element(TikTokSelectors.SKIP_USERNAME_BUTTON)
|
||||
logger.info("Benutzernamen-Schritt übersprungen")
|
||||
return True
|
||||
|
||||
# Möglicherweise wurde der Benutzername automatisch erstellt
|
||||
logger.info("Benutzernamen-Schritt möglicherweise automatisch abgeschlossen")
|
||||
return True
|
||||
|
||||
# Benutzernamen eingeben
|
||||
username_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Benutzername", "Username"],
|
||||
account_data["username"],
|
||||
TikTokSelectors.USERNAME_FIELD
|
||||
)
|
||||
|
||||
if not username_success:
|
||||
logger.error("Konnte Benutzernamen-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
logger.info(f"Benutzernamen-Feld ausgefüllt: {account_data['username']}")
|
||||
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
# Registrieren-Button klicken
|
||||
register_success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Registrieren", "Register", "Sign up", "Submit"],
|
||||
TikTokSelectors.REGISTER_BUTTON
|
||||
)
|
||||
|
||||
if not register_success:
|
||||
logger.error("Konnte 'Registrieren'-Button nicht klicken")
|
||||
return False
|
||||
|
||||
logger.info("'Registrieren'-Button erfolgreich geklickt")
|
||||
|
||||
# Warten nach der Registrierung
|
||||
self.automation.human_behavior.wait_for_page_load()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen des Benutzernamens: {e}")
|
||||
return False
|
||||
|
||||
def _check_registration_success(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob die Registrierung erfolgreich war.
|
||||
|
||||
Returns:
|
||||
bool: True wenn erfolgreich, False sonst
|
||||
"""
|
||||
try:
|
||||
# Warten nach der Registrierung
|
||||
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 = TikTokSelectors.SUCCESS_INDICATORS
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self.browser.is_element_visible(indicator, timeout=3000):
|
||||
logger.info(f"Erfolgsindikator gefunden: {indicator}")
|
||||
return True
|
||||
|
||||
# Alternativ prüfen, ob wir auf der TikTok-Startseite sind
|
||||
current_url = self.browser.page.url
|
||||
if "tiktok.com" in current_url and "/signup" not in current_url and "/login" not in current_url:
|
||||
logger.info(f"Erfolg basierend auf URL: {current_url}")
|
||||
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
|
||||
150
social_networks/tiktok/tiktok_selectors.py
Normale Datei
150
social_networks/tiktok/tiktok_selectors.py
Normale Datei
@ -0,0 +1,150 @@
|
||||
"""
|
||||
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"
|
||||
|
||||
# Anmelden/Registrieren-Buttons Hauptseite
|
||||
LOGIN_BUTTON_LEFT = "button#header-login-button"
|
||||
LOGIN_BUTTON_RIGHT = "button#top-right-action-bar-login-button"
|
||||
SIGNUP_LINK = "a[href*='/signup']"
|
||||
|
||||
# Registrierungsdialog - Methoden
|
||||
REGISTRATION_DIALOG = "div[role='dialog']"
|
||||
PHONE_EMAIL_BUTTON = "div[data-e2e='channel-item']"
|
||||
REGISTER_WITH_EMAIL = "a[href*='/signup/phone-or-email/email']"
|
||||
REGISTER_WITH_PHONE = "a[href*='/signup/phone-or-email/phone']"
|
||||
|
||||
# Geburtsdatum-Selektoren
|
||||
BIRTHDAY_MONTH_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Monat')"
|
||||
BIRTHDAY_DAY_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Tag')"
|
||||
BIRTHDAY_YEAR_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Jahr')"
|
||||
BIRTHDAY_DROPDOWN_OPTION = "div[role='option']"
|
||||
|
||||
# Formularfelder - E-Mail-Registrierung
|
||||
EMAIL_FIELD = "input[placeholder='E-Mail-Adresse']"
|
||||
PASSWORD_FIELD = "input[placeholder='Passwort']"
|
||||
VERIFICATION_CODE_FIELD = "input[placeholder*='sechsstelligen Code']"
|
||||
USERNAME_FIELD = "input[placeholder='Benutzername']"
|
||||
|
||||
# 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']"
|
||||
REGISTER_BUTTON = "button:contains('Registrieren')"
|
||||
SKIP_BUTTON = "button:contains('Überspringen')"
|
||||
|
||||
# Checkbox
|
||||
NEWSLETTER_CHECKBOX = "input[type='checkbox']"
|
||||
|
||||
# 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')"
|
||||
|
||||
# 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"
|
||||
]
|
||||
}
|
||||
|
||||
@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"]
|
||||
523
social_networks/tiktok/tiktok_ui_helper.py
Normale Datei
523
social_networks/tiktok/tiktok_ui_helper.py
Normale Datei
@ -0,0 +1,523 @@
|
||||
"""
|
||||
TikTok-UI-Helper - Hilfsmethoden für die Interaktion mit der TikTok-UI
|
||||
"""
|
||||
|
||||
import logging
|
||||
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
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
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.browser is None:
|
||||
self.browser = self.automation.browser
|
||||
|
||||
if self.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.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.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.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(placeholder_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(name_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(selector, timeout=1000):
|
||||
button_element = self.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.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.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.browser.is_element_visible(aria_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(xpath_selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(link_selector, timeout=1000):
|
||||
if self.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.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.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.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.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.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.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.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.warning(f"Captcha erkannt (Selektor): {selector}")
|
||||
return True
|
||||
|
||||
# Nach Texten suchen
|
||||
page_content = self.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.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.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Registrierung erfolgreich (Indikator gefunden: {selector})")
|
||||
return True
|
||||
|
||||
# URL überprüfen
|
||||
current_url = self.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
|
||||
495
social_networks/tiktok/tiktok_utils.py
Normale Datei
495
social_networks/tiktok/tiktok_utils.py
Normale Datei
@ -0,0 +1,495 @@
|
||||
"""
|
||||
TikTok-Utils - Hilfsfunktionen für die TikTok-Automatisierung.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
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.browser is None:
|
||||
self.browser = self.automation.browser
|
||||
|
||||
if self.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.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.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.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.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.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.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.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.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.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.browser.click_element(selector):
|
||||
logger.info("Dialog geschlossen")
|
||||
return True
|
||||
|
||||
# Wenn kein Schließen-Button gefunden wurde, Escape-Taste drücken
|
||||
self.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.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.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.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.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.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Benutzer ist angemeldet (Indikator: {selector})")
|
||||
return True
|
||||
|
||||
# URL überprüfen
|
||||
current_url = self.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.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.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
|
||||
457
social_networks/tiktok/tiktok_verification.py
Normale Datei
457
social_networks/tiktok/tiktok_verification.py
Normale Datei
@ -0,0 +1,457 @@
|
||||
# social_networks/tiktok/tiktok_verification.py
|
||||
|
||||
"""
|
||||
TikTok-Verifizierung - Klasse für die Verifizierungsfunktionalität bei TikTok
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from .tiktok_workflow import TikTokWorkflow
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("tiktok_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
|
||||
self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt
|
||||
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
|
||||
"""
|
||||
# Hole Browser-Referenz von der Hauptklasse
|
||||
self.browser = self.automation.browser
|
||||
|
||||
# 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.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.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,
|
||||
"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.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Codefeld gefunden mit Selektor: {selector}")
|
||||
if self.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.WEITER_BUTTON,
|
||||
"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.browser.is_element_visible(selector, timeout=2000):
|
||||
logger.info(f"Weiter-Button gefunden mit Selektor: {selector}")
|
||||
if self.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.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.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.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.browser.is_element_visible(selector, timeout=1000):
|
||||
if self.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.browser.is_element_visible(selector, timeout=2000):
|
||||
if self.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
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import re
|
||||
|
||||
from utils.text_similarity import TextSimilarity
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("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
In neuem Issue referenzieren
Einen Benutzer sperren