379 Zeilen
13 KiB
Python
379 Zeilen
13 KiB
Python
# social_networks/x/x_utils.py
|
|
|
|
"""
|
|
X (Twitter) Utils - Utility-Funktionen für X-Automatisierung
|
|
"""
|
|
|
|
import re
|
|
import time
|
|
import random
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
from datetime import datetime, timedelta
|
|
|
|
from utils.logger import setup_logger
|
|
|
|
# Konfiguriere Logger
|
|
logger = setup_logger("x_utils")
|
|
|
|
class XUtils:
|
|
"""
|
|
Utility-Klasse mit Hilfsfunktionen für X-Automatisierung.
|
|
"""
|
|
|
|
def __init__(self, automation):
|
|
"""
|
|
Initialisiert X Utils.
|
|
|
|
Args:
|
|
automation: Referenz auf die Hauptautomatisierungsklasse
|
|
"""
|
|
self.automation = automation
|
|
|
|
logger.debug("X Utils initialisiert")
|
|
|
|
@staticmethod
|
|
def validate_username(username: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validiert einen X-Benutzernamen.
|
|
|
|
Args:
|
|
username: Zu validierender Benutzername
|
|
|
|
Returns:
|
|
Tuple[bool, Optional[str]]: (Gültig, Fehlermeldung wenn ungültig)
|
|
"""
|
|
# Längenprüfung
|
|
if len(username) < 1:
|
|
return False, "Benutzername ist zu kurz (mindestens 1 Zeichen)"
|
|
if len(username) > 15:
|
|
return False, "Benutzername ist zu lang (maximal 15 Zeichen)"
|
|
|
|
# Zeichenprüfung (nur Buchstaben, Zahlen und Unterstrich)
|
|
if not re.match(r'^[a-zA-Z0-9_]+$', username):
|
|
return False, "Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten"
|
|
|
|
# Verbotene Muster
|
|
forbidden_patterns = ["twitter", "admin", "x.com", "root", "system"]
|
|
username_lower = username.lower()
|
|
for pattern in forbidden_patterns:
|
|
if pattern in username_lower:
|
|
return False, f"Benutzername darf '{pattern}' nicht enthalten"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def validate_email(email: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validiert eine E-Mail-Adresse für X.
|
|
|
|
Args:
|
|
email: Zu validierende E-Mail
|
|
|
|
Returns:
|
|
Tuple[bool, Optional[str]]: (Gültig, Fehlermeldung wenn ungültig)
|
|
"""
|
|
# Grundlegendes E-Mail-Pattern
|
|
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
|
|
if not re.match(email_pattern, email):
|
|
return False, "Ungültiges E-Mail-Format"
|
|
|
|
# Verbotene Domains
|
|
forbidden_domains = ["example.com", "test.com", "temp-mail.com"]
|
|
domain = email.split('@')[1].lower()
|
|
|
|
for forbidden in forbidden_domains:
|
|
if forbidden in domain:
|
|
return False, f"E-Mail-Domain '{forbidden}' ist nicht erlaubt"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def validate_password(password: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validiert ein Passwort für X.
|
|
|
|
Args:
|
|
password: Zu validierendes Passwort
|
|
|
|
Returns:
|
|
Tuple[bool, Optional[str]]: (Gültig, Fehlermeldung wenn ungültig)
|
|
"""
|
|
# Längenprüfung
|
|
if len(password) < 8:
|
|
return False, "Passwort muss mindestens 8 Zeichen lang sein"
|
|
if len(password) > 128:
|
|
return False, "Passwort darf maximal 128 Zeichen lang sein"
|
|
|
|
# Mindestens ein Kleinbuchstabe
|
|
if not re.search(r'[a-z]', password):
|
|
return False, "Passwort muss mindestens einen Kleinbuchstaben enthalten"
|
|
|
|
# Mindestens eine Zahl
|
|
if not re.search(r'\d', password):
|
|
return False, "Passwort muss mindestens eine Zahl enthalten"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def generate_device_info() -> Dict[str, Any]:
|
|
"""
|
|
Generiert realistische Geräteinformationen.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Geräteinformationen
|
|
"""
|
|
devices = [
|
|
{
|
|
"type": "desktop",
|
|
"os": "Windows",
|
|
"browser": "Chrome",
|
|
"screen": {"width": 1920, "height": 1080},
|
|
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
},
|
|
{
|
|
"type": "desktop",
|
|
"os": "macOS",
|
|
"browser": "Safari",
|
|
"screen": {"width": 2560, "height": 1440},
|
|
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
|
},
|
|
{
|
|
"type": "mobile",
|
|
"os": "iOS",
|
|
"browser": "Safari",
|
|
"screen": {"width": 414, "height": 896},
|
|
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X)"
|
|
},
|
|
{
|
|
"type": "mobile",
|
|
"os": "Android",
|
|
"browser": "Chrome",
|
|
"screen": {"width": 412, "height": 915},
|
|
"user_agent": "Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36"
|
|
}
|
|
]
|
|
|
|
return random.choice(devices)
|
|
|
|
@staticmethod
|
|
def generate_session_id() -> str:
|
|
"""
|
|
Generiert eine realistische Session-ID.
|
|
|
|
Returns:
|
|
str: Session-ID
|
|
"""
|
|
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
return ''.join(random.choice(chars) for _ in range(32))
|
|
|
|
def detect_language(self) -> str:
|
|
"""
|
|
Erkennt die aktuelle Sprache der X-Oberfläche.
|
|
|
|
Returns:
|
|
str: Sprachcode (z.B. "de", "en")
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
|
|
# Prüfe HTML lang-Attribut
|
|
lang = page.evaluate("() => document.documentElement.lang")
|
|
if lang:
|
|
return lang.split('-')[0] # z.B. "de" aus "de-DE"
|
|
|
|
# Fallback: Prüfe bekannte Texte
|
|
if page.is_visible('text="Anmelden"'):
|
|
return "de"
|
|
elif page.is_visible('text="Log in"'):
|
|
return "en"
|
|
|
|
return "en" # Standard
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Spracherkennung: {e}")
|
|
return "en"
|
|
|
|
@staticmethod
|
|
def format_phone_number(phone: str, country_code: str = "+49") -> str:
|
|
"""
|
|
Formatiert eine Telefonnummer für X.
|
|
|
|
Args:
|
|
phone: Rohe Telefonnummer
|
|
country_code: Ländervorwahl
|
|
|
|
Returns:
|
|
str: Formatierte Telefonnummer
|
|
"""
|
|
# Entferne alle Nicht-Ziffern
|
|
digits = re.sub(r'\D', '', phone)
|
|
|
|
# Füge Ländervorwahl hinzu wenn nicht vorhanden
|
|
if not phone.startswith('+'):
|
|
return f"{country_code}{digits}"
|
|
|
|
return f"+{digits}"
|
|
|
|
@staticmethod
|
|
def parse_error_message(error_text: str) -> Dict[str, Any]:
|
|
"""
|
|
Analysiert X-Fehlermeldungen.
|
|
|
|
Args:
|
|
error_text: Fehlermeldungstext
|
|
|
|
Returns:
|
|
Dict[str, Any]: Analysierte Fehlerinformationen
|
|
"""
|
|
error_info = {
|
|
"type": "unknown",
|
|
"message": error_text,
|
|
"recoverable": True,
|
|
"action": "retry"
|
|
}
|
|
|
|
# Rate Limit
|
|
if "zu viele" in error_text.lower() or "too many" in error_text.lower():
|
|
error_info.update({
|
|
"type": "rate_limit",
|
|
"recoverable": True,
|
|
"action": "wait",
|
|
"wait_time": 900 # 15 Minuten
|
|
})
|
|
|
|
# Account gesperrt
|
|
elif "gesperrt" in error_text.lower() or "suspended" in error_text.lower():
|
|
error_info.update({
|
|
"type": "suspended",
|
|
"recoverable": False,
|
|
"action": "abort"
|
|
})
|
|
|
|
# Ungültige Anmeldedaten
|
|
elif "passwort" in error_text.lower() or "password" in error_text.lower():
|
|
error_info.update({
|
|
"type": "invalid_credentials",
|
|
"recoverable": True,
|
|
"action": "check_credentials"
|
|
})
|
|
|
|
# E-Mail bereits verwendet
|
|
elif "bereits verwendet" in error_text.lower() or "already" in error_text.lower():
|
|
error_info.update({
|
|
"type": "duplicate",
|
|
"recoverable": True,
|
|
"action": "use_different_email"
|
|
})
|
|
|
|
return error_info
|
|
|
|
def wait_for_rate_limit(self, wait_time: int = None):
|
|
"""
|
|
Wartet bei Rate Limiting mit visueller Anzeige.
|
|
|
|
Args:
|
|
wait_time: Wartezeit in Sekunden (None für zufällige Zeit)
|
|
"""
|
|
if wait_time is None:
|
|
wait_time = random.randint(300, 600) # 5-10 Minuten
|
|
|
|
logger.info(f"Rate Limit erkannt - warte {wait_time} Sekunden")
|
|
self.automation._emit_customer_log(f"⏳ Rate Limit - warte {wait_time // 60} Minuten...")
|
|
|
|
# Warte in Intervallen mit Status-Updates
|
|
intervals = min(10, wait_time // 10)
|
|
for i in range(intervals):
|
|
time.sleep(wait_time // intervals)
|
|
remaining = wait_time - (i + 1) * (wait_time // intervals)
|
|
if remaining > 60:
|
|
self.automation._emit_customer_log(f"⏳ Noch {remaining // 60} Minuten...")
|
|
|
|
@staticmethod
|
|
def generate_bio() -> str:
|
|
"""
|
|
Generiert eine realistische Bio für ein X-Profil.
|
|
|
|
Returns:
|
|
str: Generierte Bio
|
|
"""
|
|
templates = [
|
|
"✈️ Explorer | 📚 Book lover | ☕ Coffee enthusiast",
|
|
"Life is a journey 🌟 | Making memories 📸",
|
|
"Student 📖 | Dreamer 💭 | Music lover 🎵",
|
|
"Tech enthusiast 💻 | Always learning 🎯",
|
|
"Living life one day at a time ✨",
|
|
"Passionate about {interest} | {city} 📍",
|
|
"Just here to share thoughts 💭",
|
|
"{hobby} in my free time | DM for collabs",
|
|
"Spreading positivity 🌈 | {emoji} lover"
|
|
]
|
|
|
|
interests = ["photography", "travel", "coding", "art", "fitness", "cooking"]
|
|
cities = ["Berlin", "Munich", "Hamburg", "Frankfurt", "Cologne"]
|
|
hobbies = ["Gaming", "Reading", "Hiking", "Painting", "Yoga"]
|
|
emojis = ["🎨", "🎮", "📚", "🎯", "🌸", "⭐"]
|
|
|
|
bio = random.choice(templates)
|
|
bio = bio.replace("{interest}", random.choice(interests))
|
|
bio = bio.replace("{city}", random.choice(cities))
|
|
bio = bio.replace("{hobby}", random.choice(hobbies))
|
|
bio = bio.replace("{emoji}", random.choice(emojis))
|
|
|
|
return bio
|
|
|
|
@staticmethod
|
|
def calculate_age_from_birthday(birthday: Dict[str, int]) -> int:
|
|
"""
|
|
Berechnet das Alter aus einem Geburtstagsdatum.
|
|
|
|
Args:
|
|
birthday: Dictionary mit 'day', 'month', 'year'
|
|
|
|
Returns:
|
|
int: Berechnetes Alter
|
|
"""
|
|
birth_date = datetime(birthday['year'], birthday['month'], birthday['day'])
|
|
today = datetime.now()
|
|
age = today.year - birth_date.year
|
|
|
|
# Prüfe ob Geburtstag dieses Jahr schon war
|
|
if (today.month, today.day) < (birth_date.month, birth_date.day):
|
|
age -= 1
|
|
|
|
return age
|
|
|
|
def check_account_restrictions(self) -> Dict[str, Any]:
|
|
"""
|
|
Prüft auf Account-Einschränkungen.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Informationen über Einschränkungen
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
restrictions = {
|
|
"limited": False,
|
|
"locked": False,
|
|
"suspended": False,
|
|
"verification_required": False
|
|
}
|
|
|
|
# Prüfe auf verschiedene Einschränkungen
|
|
if page.is_visible('text="Dein Account ist eingeschränkt"', timeout=1000):
|
|
restrictions["limited"] = True
|
|
logger.warning("Account ist eingeschränkt")
|
|
|
|
if page.is_visible('text="Account gesperrt"', timeout=1000):
|
|
restrictions["suspended"] = True
|
|
logger.error("Account ist gesperrt")
|
|
|
|
if page.is_visible('text="Verifizierung erforderlich"', timeout=1000):
|
|
restrictions["verification_required"] = True
|
|
logger.warning("Verifizierung erforderlich")
|
|
|
|
return restrictions
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Einschränkungsprüfung: {e}")
|
|
return {"error": str(e)} |