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