Dieser Commit ist enthalten in:
Claude Project Manager
2025-08-01 23:50:28 +02:00
Commit 04585e95b6
290 geänderte Dateien mit 64086 neuen und 0 gelöschten Zeilen

0
utils/__init__.py Normale Datei
Datei anzeigen

304
utils/birthday_generator.py Normale Datei
Datei anzeigen

@ -0,0 +1,304 @@
"""
Geburtsdatumsgenerator für den Social Media Account Generator.
"""
import random
import datetime
import logging
from typing import Dict, List, Any, Optional, Tuple, Union
logger = logging.getLogger("birthday_generator")
class BirthdayGenerator:
"""Klasse zur Generierung von realistischen Geburtsdaten für Social-Media-Accounts."""
def __init__(self):
"""Initialisiert den BirthdayGenerator."""
# Plattformspezifische Richtlinien
self.platform_policies = {
"instagram": {
"min_age": 13,
"max_age": 100,
"date_format": "%Y-%m-%d" # ISO-Format
},
"facebook": {
"min_age": 13,
"max_age": 100,
"date_format": "%m/%d/%Y" # US-Format
},
"twitter": {
"min_age": 13,
"max_age": 100,
"date_format": "%Y-%m-%d" # ISO-Format
},
"tiktok": {
"min_age": 13,
"max_age": 100,
"date_format": "%Y-%m-%d" # ISO-Format
},
"x": {
"min_age": 13,
"max_age": 100,
"date_format": "%Y-%m-%d" # ISO-Format
},
"default": {
"min_age": 18,
"max_age": 80,
"date_format": "%Y-%m-%d" # ISO-Format
}
}
def get_platform_policy(self, platform: str) -> Dict[str, Any]:
"""
Gibt die Altersrichtlinie für eine bestimmte Plattform zurück.
Args:
platform: Name der Plattform
Returns:
Dictionary mit der Altersrichtlinie
"""
platform = platform.lower()
return self.platform_policies.get(platform, self.platform_policies["default"])
def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None:
"""
Setzt oder aktualisiert die Altersrichtlinie für eine Plattform.
Args:
platform: Name der Plattform
policy: Dictionary mit der Altersrichtlinie
"""
platform = platform.lower()
self.platform_policies[platform] = policy
logger.info(f"Altersrichtlinie für '{platform}' aktualisiert")
def generate_birthday(self, platform: str = "default", age: Optional[int] = None) -> Tuple[datetime.date, str]:
"""
Generiert ein Geburtsdatum gemäß den Plattformrichtlinien.
Args:
platform: Name der Plattform
age: Optionales spezifisches Alter
Returns:
(Geburtsdatum als datetime.date, Formatiertes Geburtsdatum als String)
"""
policy = self.get_platform_policy(platform)
# Aktuelles Datum
today = datetime.date.today()
# Altersbereich bestimmen
min_age = policy["min_age"]
max_age = policy["max_age"]
# Wenn ein spezifisches Alter angegeben ist, dieses verwenden
if age is not None:
if age < min_age:
logger.warning(f"Angegebenes Alter ({age}) ist kleiner als das Mindestalter "
f"({min_age}). Verwende Mindestalter.")
age = min_age
elif age > max_age:
logger.warning(f"Angegebenes Alter ({age}) ist größer als das Höchstalter "
f"({max_age}). Verwende Höchstalter.")
age = max_age
else:
# Zufälliges Alter im erlaubten Bereich
age = random.randint(min_age, max_age)
# Berechne das Geburtsjahr
birth_year = today.year - age
# Berücksichtige, ob der Geburtstag in diesem Jahr bereits stattgefunden hat
has_had_birthday_this_year = random.choice([True, False])
if not has_had_birthday_this_year:
birth_year -= 1
# Generiere Monat und Tag
if has_had_birthday_this_year:
# Geburtstag war bereits in diesem Jahr
birth_month = random.randint(1, today.month)
if birth_month == today.month:
# Wenn gleicher Monat, Tag muss vor oder gleich dem heutigen sein
birth_day = random.randint(1, today.day)
else:
# Wenn anderer Monat, beliebiger Tag
birth_day = random.randint(1, self._days_in_month(birth_month, birth_year))
else:
# Geburtstag ist noch in diesem Jahr
birth_month = random.randint(today.month, 12)
if birth_month == today.month:
# Wenn gleicher Monat, Tag muss nach dem heutigen sein
birth_day = random.randint(today.day + 1, self._days_in_month(birth_month, birth_year))
else:
# Wenn anderer Monat, beliebiger Tag
birth_day = random.randint(1, self._days_in_month(birth_month, birth_year))
# Erstelle und formatiere das Geburtsdatum
birth_date = datetime.date(birth_year, birth_month, birth_day)
formatted_date = birth_date.strftime(policy["date_format"])
logger.info(f"Geburtsdatum generiert: {formatted_date} (Alter: {age})")
return birth_date, formatted_date
def _days_in_month(self, month: int, year: int) -> int:
"""
Gibt die Anzahl der Tage in einem Monat zurück.
Args:
month: Monat (1-12)
year: Jahr
Returns:
Anzahl der Tage im angegebenen Monat
"""
if month in [4, 6, 9, 11]:
return 30
elif month == 2:
# Schaltjahr prüfen
if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
return 29
else:
return 28
else:
return 31
def generate_birthday_components(self, platform: str = "default", age: Optional[int] = None) -> Dict[str, int]:
"""
Generiert die Komponenten eines Geburtsdatums (Tag, Monat, Jahr).
Args:
platform: Name der Plattform
age: Optionales spezifisches Alter
Returns:
Dictionary mit den Komponenten des Geburtsdatums (year, month, day)
"""
birth_date, _ = self.generate_birthday(platform, age)
return {
"year": birth_date.year,
"month": birth_date.month,
"day": birth_date.day
}
def is_valid_age(self, birth_date: datetime.date, platform: str = "default") -> bool:
"""
Überprüft, ob ein Geburtsdatum für eine Plattform gültig ist.
Args:
birth_date: Geburtsdatum
platform: Name der Plattform
Returns:
True, wenn das Alter gültig ist, sonst False
"""
policy = self.get_platform_policy(platform)
# Aktuelles Datum
today = datetime.date.today()
# Alter berechnen
age = today.year - birth_date.year
# Berücksichtigen, ob der Geburtstag in diesem Jahr bereits stattgefunden hat
if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day):
age -= 1
return policy["min_age"] <= age <= policy["max_age"]
def generate_age_from_date(self, birth_date: datetime.date) -> int:
"""
Berechnet das Alter basierend auf einem Geburtsdatum.
Args:
birth_date: Geburtsdatum
Returns:
Berechnetes Alter
"""
# Aktuelles Datum
today = datetime.date.today()
# Alter berechnen
age = today.year - birth_date.year
# Berücksichtigen, ob der Geburtstag in diesem Jahr bereits stattgefunden hat
if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day):
age -= 1
return age
def generate_date_from_components(self, year: int, month: int, day: int, platform: str = "default") -> str:
"""
Formatiert ein Datum aus seinen Komponenten gemäß dem Plattformformat.
Args:
year: Jahr
month: Monat
day: Tag
platform: Name der Plattform
Returns:
Formatiertes Geburtsdatum als String
"""
policy = self.get_platform_policy(platform)
try:
birth_date = datetime.date(year, month, day)
formatted_date = birth_date.strftime(policy["date_format"])
return formatted_date
except ValueError as e:
logger.error(f"Ungültiges Datum: {year}-{month}-{day}, Fehler: {e}")
# Fallback: Gültiges Datum zurückgeben
return self.generate_birthday(platform)[1]
def generate_random_date(self, start_year: int, end_year: int, platform: str = "default") -> str:
"""
Generiert ein zufälliges Datum innerhalb eines Jahresbereichs.
Args:
start_year: Startjahr
end_year: Endjahr
platform: Name der Plattform
Returns:
Formatiertes Datum als String
"""
policy = self.get_platform_policy(platform)
year = random.randint(start_year, end_year)
month = random.randint(1, 12)
day = random.randint(1, self._days_in_month(month, year))
date = datetime.date(year, month, day)
return date.strftime(policy["date_format"])
def generate_birthday(age: int = None, platform: str = "default") -> str:
"""
Kompatibilitätsfunktion für ältere Codeversionen.
Generiert ein Geburtsdatum basierend auf einem Alter.
Args:
age: Alter in Jahren (optional)
platform: Name der Plattform
Returns:
Generiertes Geburtsdatum im Format "TT.MM.JJJJ"
"""
# Logger-Warnung für Legacy-Funktion
logger.warning("Die Funktion generate_birthday() ist für Kompatibilität, bitte verwende stattdessen die BirthdayGenerator-Klasse.")
# Eine Instanz der Generator-Klasse erstellen und die Methode aufrufen
generator = BirthdayGenerator()
# Geburtsdatum generieren
_, formatted_date = generator.generate_birthday(platform, age)
return formatted_date

687
utils/email_handler.py Normale Datei
Datei anzeigen

@ -0,0 +1,687 @@
"""
E-Mail-Handler für den Social Media Account Generator.
Verwaltet den Abruf von Bestätigungscodes und E-Mail-Verifizierungen.
"""
import os
import json
import logging
import time
import imaplib
import email
import re
from typing import Dict, List, Any, Optional, Tuple, Union
from email.header import decode_header
from datetime import datetime, timedelta
from utils.text_similarity import TextSimilarity
logger = logging.getLogger("email_handler")
class EmailHandler:
"""
Handler für den Zugriff auf E-Mail-Dienste und den Abruf von Bestätigungscodes.
"""
CONFIG_FILE = os.path.join("config", "email_config.json")
def __init__(self):
"""Initialisiert den EmailHandler und lädt die Konfiguration."""
self.config = self.load_config()
# Stelle sicher, dass das Konfigurationsverzeichnis existiert
os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True)
# TextSimilarity-Instanz für Fuzzy-Matching
self.text_similarity = TextSimilarity(default_threshold=0.75)
# Cache für die letzten erfolgreichen Verbindungsdaten
self.last_connection = None
# Typische Betreffzeilen für Verifizierungs-E-Mails nach Plattform
self.verification_subjects = {
"instagram": [
"Bestätige deine E-Mail-Adresse",
"Bestätigungscode für Instagram",
"Dein Instagram-Code",
"Bestätige deinen Instagram-Account",
"Verify your email address",
"Instagram Verification Code",
"Your Instagram Code",
"Verify your Instagram account",
"Instagram-Bestätigungscode",
"Instagram security code"
],
"facebook": [
"Bestätigungscode für Facebook",
"Facebook-Bestätigungscode",
"Dein Facebook-Code",
"Facebook Verification Code",
"Your Facebook Code"
],
"twitter": [
"Bestätige dein Twitter-Konto",
"Twitter-Bestätigungscode",
"Verify your Twitter account",
"Twitter Verification Code"
],
"x": [
"ist dein X Verifizierungscode",
"is your X verification code",
"X Verifizierungscode",
"X verification code",
"Bestätige dein X-Konto",
"Verify your X account",
"X Bestätigungscode",
"X confirmation code"
],
"tiktok": [
"ist dein Bestätigungscode",
"is your confirmation code",
"TikTok-Bestätigungscode",
"Bestätige dein TikTok-Konto",
"TikTok Verification Code",
"Verify your TikTok account"
],
"default": [
"Bestätigungscode",
"Verification Code",
"Account Verification",
"Konto-Bestätigung",
"Security Code",
"Sicherheitscode"
]
}
logger.info("E-Mail-Handler initialisiert")
def load_config(self) -> Dict[str, Any]:
"""
Lädt die E-Mail-Konfiguration aus der Konfigurationsdatei.
Returns:
Dict[str, Any]: Die geladene Konfiguration oder Standardwerte
"""
default_config = {
"imap_server": "imap.ionos.de",
"imap_port": 993,
"imap_user": "info@z5m7q9dk3ah2v1plx6ju.com",
"imap_pass": "cz&ie.O9$!:!tYY@"
}
try:
if os.path.exists(self.CONFIG_FILE):
with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
config = json.load(f)
logger.info("E-Mail-Konfiguration geladen")
return config
else:
# Standardwerte speichern
with open(self.CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(default_config, f, indent=2)
logger.info("Standard-E-Mail-Konfiguration erstellt")
return default_config
except Exception as e:
logger.error(f"Fehler beim Laden der E-Mail-Konfiguration: {e}")
return default_config
def save_config(self) -> bool:
"""
Speichert die aktuelle Konfiguration in die Konfigurationsdatei.
Returns:
bool: True bei Erfolg, False bei Fehler
"""
try:
with open(self.CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(self.config, f, indent=2)
logger.info("E-Mail-Konfiguration gespeichert")
return True
except Exception as e:
logger.error(f"Fehler beim Speichern der E-Mail-Konfiguration: {e}")
return False
def get_config(self) -> Dict[str, Any]:
"""
Gibt die aktuelle Konfiguration zurück.
Returns:
Dict[str, Any]: Die aktuelle Konfiguration
"""
return self.config
def update_config(self, new_config: Dict[str, Any]) -> bool:
"""
Aktualisiert die Konfiguration mit den neuen Werten.
Args:
new_config: Neue Konfiguration
Returns:
bool: True bei Erfolg, False bei Fehler
"""
try:
# Aktuelle Konfiguration speichern
old_config = self.config.copy()
# Neue Werte übernehmen
self.config.update(new_config)
# Konfiguration speichern
success = self.save_config()
if not success:
# Bei Speicherfehler zur alten Konfiguration zurückkehren
self.config = old_config
return False
return True
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der E-Mail-Konfiguration: {e}")
return False
def update_credentials(self, username: str, password: str) -> bool:
"""
Aktualisiert nur die Anmeldeinformationen.
Args:
username: Benutzername
password: Passwort
Returns:
bool: True bei Erfolg, False bei Fehler
"""
return self.update_config({
"imap_user": username,
"imap_pass": password
})
def update_server(self, server: str, port: int) -> bool:
"""
Aktualisiert nur die Serverinformationen.
Args:
server: IMAP-Server
port: IMAP-Port
Returns:
bool: True bei Erfolg, False bei Fehler
"""
return self.update_config({
"imap_server": server,
"imap_port": port
})
def test_connection(self) -> Dict[str, Any]:
"""
Testet die Verbindung zum IMAP-Server.
Returns:
Dict[str, Any]: Ergebnis des Tests
"""
try:
logger.info(f"Teste Verbindung zu {self.config['imap_server']}:{self.config['imap_port']}")
# SSL-Verbindung zum IMAP-Server herstellen
mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"])
# Anmelden
mail.login(self.config["imap_user"], self.config["imap_pass"])
# Verfügbare Postfächer auflisten
status, mailboxes = mail.list()
if status == 'OK':
mailbox_count = len(mailboxes)
# INBOX auswählen
mail.select("INBOX")
# Abmelden
mail.logout()
# Verbindungsdaten im Cache speichern
self.last_connection = {
"server": self.config["imap_server"],
"port": self.config["imap_port"],
"username": self.config["imap_user"],
"password": self.config["imap_pass"]
}
logger.info(f"Verbindungstest erfolgreich: {mailbox_count} Postfächer gefunden")
return {
"success": True,
"server": self.config["imap_server"],
"port": self.config["imap_port"],
"mailbox_count": mailbox_count
}
else:
logger.error(f"Fehler beim Abrufen der Postfächer: {status}")
mail.logout()
return {
"success": False,
"error": f"Fehler beim Abrufen der Postfächer: {status}"
}
except imaplib.IMAP4.error as e:
logger.error(f"IMAP-Fehler: {e}")
return {
"success": False,
"error": f"IMAP-Fehler: {e}"
}
except Exception as e:
logger.error(f"Allgemeiner Fehler: {e}")
return {
"success": False,
"error": f"Allgemeiner Fehler: {e}"
}
def search_emails(self, search_criteria: str = "ALL", max_emails: int = 5) -> List[Dict[str, Any]]:
"""
Sucht nach E-Mails mit den angegebenen Kriterien.
Args:
search_criteria: IMAP-Suchkriterien
max_emails: Maximale Anzahl der abzurufenden E-Mails
Returns:
List[Dict[str, Any]]: Liste der gefundenen E-Mails
"""
try:
# Verbindung zum IMAP-Server herstellen
mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"])
# Anmelden
mail.login(self.config["imap_user"], self.config["imap_pass"])
# INBOX auswählen
mail.select("INBOX")
# Nach E-Mails suchen
status, data = mail.search(None, search_criteria)
emails = []
if status == 'OK':
# E-Mail-IDs abrufen
email_ids = data[0].split()
# Newest emails first
email_ids = list(reversed(email_ids))
# Begrenze die Anzahl der abzurufenden E-Mails
if max_emails > 0:
email_ids = email_ids[:max_emails]
for email_id in email_ids:
# E-Mail abrufen
status, data = mail.fetch(email_id, '(RFC822)')
if status == 'OK':
# E-Mail-Inhalt parsen
raw_email = data[0][1]
msg = email.message_from_bytes(raw_email)
# Betreff decodieren (vollständig, alle Teile zusammenfügen)
subject_parts = decode_header(msg.get("Subject", ""))
subject = ""
for part, encoding in subject_parts:
if isinstance(part, bytes):
subject += part.decode(encoding or 'utf-8', errors='replace')
else:
subject += str(part) if part else ""
# Absender decodieren (vollständig, alle Teile zusammenfügen)
from_parts = decode_header(msg.get("From", ""))
from_addr = ""
for part, encoding in from_parts:
if isinstance(part, bytes):
from_addr += part.decode(encoding or 'utf-8', errors='replace')
else:
from_addr += str(part) if part else ""
# Empfänger decodieren (vollständig, alle Teile zusammenfügen)
to_parts = decode_header(msg.get("To", ""))
to_addr = ""
for part, encoding in to_parts:
if isinstance(part, bytes):
to_addr += part.decode(encoding or 'utf-8', errors='replace')
else:
to_addr += str(part) if part else ""
# Extrahiere E-Mail-Adresse aus dem To-Feld
to_email = self._extract_email_from_addr(to_addr)
# Datum decodieren
date = msg.get("Date", "")
# E-Mail-Text extrahieren
body = ""
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))
if "attachment" not in content_disposition:
if content_type == "text/plain":
try:
# Textinhalt decodieren
charset = part.get_content_charset() or 'utf-8'
body = part.get_payload(decode=True).decode(charset, errors='replace')
break
except:
body = "[Fehler beim Decodieren des Inhalts]"
elif content_type == "text/html" and not body:
try:
# HTML-Inhalt decodieren
charset = part.get_content_charset() or 'utf-8'
body = part.get_payload(decode=True).decode(charset, errors='replace')
except:
body = "[Fehler beim Decodieren des HTML-Inhalts]"
else:
try:
# Einzel-Teil-E-Mail decodieren
charset = msg.get_content_charset() or 'utf-8'
body = msg.get_payload(decode=True).decode(charset, errors='replace')
except:
body = "[Fehler beim Decodieren des Inhalts]"
# E-Mail-Informationen speichern
email_info = {
"id": email_id.decode(),
"subject": subject,
"from": from_addr,
"to": to_addr,
"to_email": to_email,
"date": date,
"body": body
}
emails.append(email_info)
# Abmelden
mail.logout()
logger.info(f"{len(emails)} E-Mails gefunden")
return emails
except Exception as e:
logger.error(f"Fehler beim Suchen nach E-Mails: {e}")
return []
def _extract_email_from_addr(self, addr_str: str) -> str:
"""
Extrahiert die E-Mail-Adresse aus einem Adressstring im Format 'Name <email@domain.com>'.
Args:
addr_str: Adressstring
Returns:
str: Die extrahierte E-Mail-Adresse oder der ursprüngliche String
"""
# Regulärer Ausdruck für die Extraktion der E-Mail-Adresse
email_pattern = r'<?([\w\.-]+@[\w\.-]+\.\w+)>?'
match = re.search(email_pattern, addr_str)
if match:
return match.group(1).lower()
return addr_str.lower()
def _is_subject_relevant(self, subject: str, platform: str) -> bool:
"""
Prüft, ob der Betreff relevant für eine Verifizierungs-E-Mail der angegebenen Plattform ist.
Verwendet Fuzzy-Matching für die Erkennung.
Args:
subject: Betreff der E-Mail
platform: Plattform (instagram, facebook, twitter, etc.)
Returns:
bool: True, wenn der Betreff relevant ist, False sonst
"""
# Standardschwellenwert für Fuzzy-Matching
threshold = 0.75
# Betreffzeilen für die angegebene Plattform und Standard
subject_patterns = self.verification_subjects.get(platform.lower(), [])
subject_patterns += self.verification_subjects["default"]
# Prüfe auf exakte Übereinstimmung (schneller)
for pattern in subject_patterns:
if pattern.lower() in subject.lower():
logger.debug(f"Relevanter Betreff gefunden (exakte Übereinstimmung): {subject}")
return True
# Wenn keine exakte Übereinstimmung, Fuzzy-Matching verwenden
for pattern in subject_patterns:
similarity = self.text_similarity.similarity_ratio(pattern.lower(), subject.lower())
if similarity >= threshold:
logger.debug(f"Relevanter Betreff gefunden (Fuzzy-Matching, {similarity:.2f}): {subject}")
return True
# Alternativ: Prüfe, ob der Betreff den Pattern enthält (mit Fuzzy-Matching)
if self.text_similarity.contains_similar_text(subject.lower(), [pattern.lower()], threshold=threshold):
logger.debug(f"Relevanter Betreff gefunden (Fuzzy-Contains): {subject}")
return True
return False
def get_verification_code(self, target_email: Optional[str] = None, platform: str = "instagram",
max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]:
"""
Ruft einen Bestätigungscode von einer E-Mail ab.
Args:
target_email: Ziel-E-Mail-Adresse oder None für alle
platform: Plattform (instagram, facebook, twitter, etc.)
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 {platform} mit E-Mail {target_email or 'alle'}")
# Bei Catch-All Domains ist die exakte E-Mail-Adresse wichtig!
if target_email:
logger.info(f"EXAKTE E-Mail-Suche: {target_email} (Catch-All Domain)")
# Letzter Tag als Suchkriterium
today = datetime.now()
yesterday = today - timedelta(days=1)
date_str = yesterday.strftime("%d-%b-%Y")
search_criteria = f'(SINCE "{date_str}")'
# E-Mail-Abruf mit Wiederholungsversuch
total_wait_time = max_attempts * delay_seconds
logger.info(f"Warte bis zu {total_wait_time} Sekunden ({total_wait_time/60:.1f} Minuten) auf E-Mail")
for attempt in range(max_attempts):
elapsed_time = attempt * delay_seconds
remaining_time = total_wait_time - elapsed_time
logger.debug(f"Versuch {attempt + 1}/{max_attempts} - Verstrichene Zeit: {elapsed_time}s, Verbleibend: {remaining_time}s")
# Alle neuen E-Mails abrufen
emails = self.search_emails(search_criteria, max_emails=10)
logger.debug(f"Gefundene E-Mails: {len(emails)}")
# E-Mails filtern und nach Bestätigungscode suchen
for idx, email_info in enumerate(emails):
logger.debug(f"E-Mail {idx+1}: To={email_info.get('to_email', 'N/A')}, Subject={email_info.get('subject', 'N/A')[:50]}...")
# Extrahierte E-Mail-Adresse des Empfängers
to_email = email_info.get("to_email", "").lower()
# WICHTIG: Bei Catch-All Domains MUSS die exakte E-Mail-Adresse übereinstimmen!
if target_email:
# NUR exakte E-Mail-Übereinstimmung zulassen
if target_email.lower() == to_email:
logger.debug(f"✓ E-Mail-Match: {to_email} == {target_email}")
else:
logger.debug(f"✗ E-Mail übersprungen: {to_email} != {target_email} (exakte Übereinstimmung erforderlich)")
continue
# Betreff auf Relevanz prüfen (mit Fuzzy-Matching)
subject = email_info.get("subject", "")
if not subject or not self._is_subject_relevant(subject, platform):
logger.debug(f"E-Mail übersprungen: Betreff '{subject}' ist nicht relevant")
continue
# Nach Bestätigungscode im Text suchen
body = email_info.get("body", "")
code = self._extract_verification_code(body, platform)
if code:
logger.info(f"Bestätigungscode gefunden: {code} (E-Mail an {to_email})")
return code
else:
logger.debug(f"Kein Code in relevanter E-Mail gefunden (Betreff: {subject})")
# Wenn kein Code gefunden wurde und noch Versuche übrig sind, warten und erneut versuchen
if attempt < max_attempts - 1:
logger.debug(f"Kein Code gefunden, warte {delay_seconds} Sekunden...")
time.sleep(delay_seconds)
logger.warning("Kein Bestätigungscode gefunden nach allen Versuchen")
return None
def _extract_verification_code(self, text: str, platform: str = "instagram") -> Optional[str]:
"""
Extrahiert einen Bestätigungscode aus einem Text.
Args:
text: Zu durchsuchender Text
platform: Plattform (instagram, facebook, twitter, etc.)
Returns:
Optional[str]: Der gefundene Bestätigungscode oder None
"""
# Plattformspezifische Muster für Bestätigungscodes
patterns = {
"instagram": [
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",
r"Instagram-Code: (\d{6})",
r"Instagram code: (\d{6})",
r"Instagram: (\d{6})",
r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern
],
"facebook": [
r"Dein Code ist (\d{5})",
r"Your code is (\d{5})",
r"Bestätigungscode: (\d{5})",
r"Confirmation code: (\d{5})",
r"Facebook-Code: (\d{5})",
r"Facebook code: (\d{5})",
r"Facebook: (\d{5})",
r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern
],
"twitter": [
r"Code: (\d{6})",
r"Verification code: (\d{6})",
r"Twitter-Code: (\d{6})",
r"Twitter code: (\d{6})",
r"Twitter: (\d{6})",
r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern
],
"x": [
r"(\d{6}) ist dein X Verifizierungscode",
r"(\d{6}) is your X verification code",
r"Code: (\d{6})",
r"Verification code: (\d{6})",
r"X-Code: (\d{6})",
r"X code: (\d{6})",
r"X: (\d{6})",
r"Verifizierungscode: (\d{6})",
r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern
],
"tiktok": [
r"(\d{6}) ist dein Bestätigungscode",
r"(\d{6}) is your confirmation code",
r"TikTok-Code: (\d{6})",
r"TikTok code: (\d{6})",
r"TikTok: (\d{6})",
r"Bestätigungscode[:\s]*(\d{6})",
r"Confirmation code[:\s]*(\d{6})",
r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern
],
"default": [
r"Code[:\s]*(\d{4,8})",
r"[Vv]erification [Cc]ode[:\s]*(\d{4,8})",
r"[Bb]estätigungscode[:\s]*(\d{4,8})",
r"(\d{4,8}) is your code",
r"(\d{4,8}) ist dein Code",
r"[^\d](\d{6})[^\d]", # 6-stellige Zahl umgeben von Nicht-Ziffern
r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern
]
}
# Plattformspezifische Muster verwenden
platform_patterns = patterns.get(platform.lower(), [])
# Alle Muster dieser Plattform durchsuchen
for pattern in platform_patterns:
match = re.search(pattern, text)
if match:
code = match.group(1)
logger.debug(f"Code gefunden mit Muster '{pattern}': {code}")
return code
# Wenn keine plattformspezifischen Muster gefunden wurden, Default-Muster verwenden
for pattern in patterns["default"]:
match = re.search(pattern, text)
if match:
code = match.group(1)
logger.debug(f"Code gefunden mit Default-Muster '{pattern}': {code}")
return code
# Generische Suche nach Zahlen (für die jeweilige Plattform typische Länge)
code_length = 6 # Standard
if platform.lower() == "facebook":
code_length = 5
# Suche nach alleinstehenden Zahlen der richtigen Länge
generic_pattern = r"\b(\d{" + str(code_length) + r"})\b"
matches = re.findall(generic_pattern, text)
if matches:
# Nehme die erste gefundene Zahl
code = matches[0]
logger.debug(f"Code gefunden mit generischem Muster: {code}")
return code
logger.debug("Kein Code gefunden")
return None
def get_confirmation_code(self, expected_email: str, search_criteria: str = "ALL",
max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]:
"""
Ruft einen Bestätigungscode von einer E-Mail ab (Kompatibilitätsmethode).
Args:
expected_email: E-Mail-Adresse, von der der Code erwartet wird
search_criteria: IMAP-Suchkriterien
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 {expected_email}")
# Vermutete Plattform basierend auf der E-Mail-Adresse oder dem Inhalt
platform = "instagram" # Standard
# Bestätigungscode abrufen
return self.get_verification_code(expected_email, platform, max_attempts, delay_seconds)

592
utils/human_behavior.py Normale Datei
Datei anzeigen

@ -0,0 +1,592 @@
"""
Menschliches Verhalten für den Social Media Account Generator.
"""
import random
import time
import logging
import math
from typing import Optional, Tuple, List, Dict, Any, Callable
logger = logging.getLogger("human_behavior")
class HumanBehavior:
"""Klasse zur Simulation von menschlichem Verhalten für die Automatisierung."""
def __init__(self, speed_factor: float = 1.0, randomness: float = 0.5):
"""
Initialisiert die HumanBehavior-Klasse.
Args:
speed_factor: Faktormultiplikator für die Geschwindigkeit (höher = schneller)
randomness: Faktor für die Zufälligkeit (0-1, höher = zufälliger)
"""
self.speed_factor = max(0.1, min(10.0, speed_factor)) # Begrenzung auf 0.1-10.0
self.randomness = max(0.0, min(1.0, randomness)) # Begrenzung auf 0.0-1.0
# Typische Verzögerungen (in Sekunden)
self.delays = {
"typing_per_char": 0.05, # Verzögerung pro Zeichen beim Tippen
"mouse_movement": 0.5, # Verzögerung für Mausbewegung
"click": 0.1, # Verzögerung für Mausklick
"page_load": 2.0, # Verzögerung für das Laden einer Seite
"form_fill": 1.0, # Verzögerung zwischen Formularfeldern
"decision": 1.5, # Verzögerung für Entscheidungen
"scroll": 0.3, # Verzögerung für Scrollbewegungen
"verification": 5.0, # Verzögerung für Verifizierungsprozesse
"image_upload": 3.0, # Verzögerung für Bildupload
"navigation": 1.0 # Verzögerung für Navigation
}
def sleep(self, delay_type: str, multiplier: float = 1.0) -> None:
"""
Pausiert die Ausführung für eine bestimmte Zeit, basierend auf dem Verzögerungstyp.
Args:
delay_type: Typ der Verzögerung (aus self.delays)
multiplier: Zusätzlicher Multiplikator für die Verzögerung
"""
base_delay = self.delays.get(delay_type, 0.5) # Standardverzögerung, wenn der Typ nicht bekannt ist
# Berechne die effektive Verzögerung
delay = base_delay * multiplier / self.speed_factor
# Füge Zufälligkeit hinzu
if self.randomness > 0:
# Variiere die Verzögerung um ±randomness%
variation = 1.0 + (random.random() * 2 - 1) * self.randomness
delay *= variation
# Stelle sicher, dass die Verzögerung nicht negativ ist
delay = max(0, delay)
logger.debug(f"Schlafe für {delay:.2f}s ({delay_type})")
time.sleep(delay)
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
"""
return self._random_delay(min_seconds, max_seconds)
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 type_text(self, text: str, on_char_typed: Optional[Callable[[str], None]] = None,
error_probability: float = 0.05, correction_probability: float = 0.9) -> str:
"""
Simuliert menschliches Tippen mit möglichen Tippfehlern und Korrekturen.
Args:
text: Zu tippender Text
on_char_typed: Optionale Funktion, die für jedes getippte Zeichen aufgerufen wird
error_probability: Wahrscheinlichkeit für Tippfehler (0-1)
correction_probability: Wahrscheinlichkeit, Tippfehler zu korrigieren (0-1)
Returns:
Der tatsächlich getippte Text (mit oder ohne Fehler)
"""
# Anpassen der Fehlerwahrscheinlichkeit basierend auf Zufälligkeit
adjusted_error_prob = error_probability * self.randomness
result = ""
i = 0
while i < len(text):
char = text[i]
# Potentieller Tippfehler
if random.random() < adjusted_error_prob:
# Auswahl eines Fehlertyps:
# - Falsches Zeichen (Tastatur-Nachbarn)
# - Ausgelassenes Zeichen
# - Doppeltes Zeichen
error_type = random.choices(
["wrong", "skip", "double"],
weights=[0.6, 0.2, 0.2],
k=1
)[0]
if error_type == "wrong":
# Falsches Zeichen tippen (Tastatur-Nachbarn)
keyboard_neighbors = self.get_keyboard_neighbors(char)
if keyboard_neighbors:
wrong_char = random.choice(keyboard_neighbors)
result += wrong_char
if on_char_typed:
on_char_typed(wrong_char)
self.sleep("typing_per_char")
# Entscheiden, ob der Fehler korrigiert wird
if random.random() < correction_probability:
# Löschen des falschen Zeichens
result = result[:-1]
if on_char_typed:
on_char_typed("\b") # Backspace
self.sleep("typing_per_char", 1.5) # Längere Pause für Korrektur
# Korrektes Zeichen tippen
result += char
if on_char_typed:
on_char_typed(char)
self.sleep("typing_per_char")
else:
# Wenn keine Nachbarn gefunden werden, normales Zeichen tippen
result += char
if on_char_typed:
on_char_typed(char)
self.sleep("typing_per_char")
elif error_type == "skip":
# Zeichen auslassen (nichts tun)
pass
elif error_type == "double":
# Zeichen doppelt tippen
result += char + char
if on_char_typed:
on_char_typed(char)
on_char_typed(char)
self.sleep("typing_per_char")
# Entscheiden, ob der Fehler korrigiert wird
if random.random() < correction_probability:
# Löschen des doppelten Zeichens
result = result[:-1]
if on_char_typed:
on_char_typed("\b") # Backspace
self.sleep("typing_per_char", 1.2)
else:
# Normales Tippen ohne Fehler
result += char
if on_char_typed:
on_char_typed(char)
self.sleep("typing_per_char")
i += 1
return result
def get_keyboard_neighbors(self, char: str) -> List[str]:
"""
Gibt die Tastatur-Nachbarn eines Zeichens zurück.
Args:
char: Das Zeichen, für das Nachbarn gefunden werden sollen
Returns:
Liste von benachbarten Zeichen
"""
# QWERTY-Tastaturlayout
keyboard_layout = {
"1": ["2", "q"],
"2": ["1", "3", "q", "w"],
"3": ["2", "4", "w", "e"],
"4": ["3", "5", "e", "r"],
"5": ["4", "6", "r", "t"],
"6": ["5", "7", "t", "y"],
"7": ["6", "8", "y", "u"],
"8": ["7", "9", "u", "i"],
"9": ["8", "0", "i", "o"],
"0": ["9", "-", "o", "p"],
"-": ["0", "=", "p", "["],
"=": ["-", "[", "]"],
"q": ["1", "2", "w", "a"],
"w": ["2", "3", "q", "e", "a", "s"],
"e": ["3", "4", "w", "r", "s", "d"],
"r": ["4", "5", "e", "t", "d", "f"],
"t": ["5", "6", "r", "y", "f", "g"],
"y": ["6", "7", "t", "u", "g", "h"],
"u": ["7", "8", "y", "i", "h", "j"],
"i": ["8", "9", "u", "o", "j", "k"],
"o": ["9", "0", "i", "p", "k", "l"],
"p": ["0", "-", "o", "[", "l", ";"],
"[": ["-", "=", "p", "]", ";", "'"],
"]": ["=", "[", "'", "\\"],
"a": ["q", "w", "s", "z"],
"s": ["w", "e", "a", "d", "z", "x"],
"d": ["e", "r", "s", "f", "x", "c"],
"f": ["r", "t", "d", "g", "c", "v"],
"g": ["t", "y", "f", "h", "v", "b"],
"h": ["y", "u", "g", "j", "b", "n"],
"j": ["u", "i", "h", "k", "n", "m"],
"k": ["i", "o", "j", "l", "m", ","],
"l": ["o", "p", "k", ";", ",", "."],
";": ["p", "[", "l", "'", ".", "/"],
"'": ["[", "]", ";", "/"],
"z": ["a", "s", "x"],
"x": ["s", "d", "z", "c"],
"c": ["d", "f", "x", "v"],
"v": ["f", "g", "c", "b"],
"b": ["g", "h", "v", "n"],
"n": ["h", "j", "b", "m"],
"m": ["j", "k", "n", ","],
",": ["k", "l", "m", "."],
".": ["l", ";", ",", "/"],
"/": [";", "'", "."],
" ": ["c", "v", "b", "n", "m"] # Leertaste hat viele benachbarte Tasten
}
# Für Großbuchstaben die Nachbarn der Kleinbuchstaben verwenden
if char.lower() != char and char.lower() in keyboard_layout:
return [neighbor.upper() if random.choice([True, False]) else neighbor
for neighbor in keyboard_layout[char.lower()]]
return keyboard_layout.get(char, [])
def mouse_move(self, from_point: Optional[Tuple[int, int]] = None,
to_point: Tuple[int, int] = (0, 0),
on_move: Optional[Callable[[Tuple[int, int]], None]] = None) -> None:
"""
Simuliert eine menschliche Mausbewegung mit natürlicher Beschleunigung und Verzögerung.
Args:
from_point: Startpunkt der Bewegung (oder None für aktuelle Position)
to_point: Zielpunkt der Bewegung
on_move: Optionale Funktion, die für jede Zwischenposition aufgerufen wird
"""
# Wenn kein Startpunkt angegeben ist, einen zufälligen verwenden
if from_point is None:
from_point = (random.randint(0, 1000), random.randint(0, 800))
# Berechne die Entfernung
dx = to_point[0] - from_point[0]
dy = to_point[1] - from_point[1]
distance = (dx**2 + dy**2)**0.5
# Anzahl der Zwischenschritte basierend auf der Entfernung
# Mehr Schritte für realistischere Bewegung
steps = max(20, int(distance / 5))
# Wähle zufällig einen Bewegungstyp
movement_type = random.choice(["bezier", "arc", "zigzag", "smooth"])
if movement_type == "bezier":
# Bézierkurve mit mehr Variation
control_variance = distance / 4
control_point_1 = (
from_point[0] + dx * random.uniform(0.2, 0.4) + random.randint(-int(control_variance), int(control_variance)),
from_point[1] + dy * random.uniform(0.1, 0.3) + random.randint(-int(control_variance), int(control_variance))
)
control_point_2 = (
from_point[0] + dx * random.uniform(0.6, 0.8) + random.randint(-int(control_variance), int(control_variance)),
from_point[1] + dy * random.uniform(0.7, 0.9) + random.randint(-int(control_variance), int(control_variance))
)
else:
# Standard Kontrollpunkte für andere Bewegungstypen
control_point_1 = (from_point[0] + dx * 0.3, from_point[1] + dy * 0.3)
control_point_2 = (from_point[0] + dx * 0.7, from_point[1] + dy * 0.7)
# Micro-Pauses und Geschwindigkeitsvariationen
micro_pause_probability = 0.1
speed_variations = [0.5, 0.8, 1.0, 1.2, 1.5]
# Bewegung durchführen
for i in range(steps + 1):
t = i / steps
if movement_type == "bezier":
# Kubische Bézierkurve
x = (1-t)**3 * from_point[0] + 3*(1-t)**2*t * control_point_1[0] + 3*(1-t)*t**2 * control_point_2[0] + t**3 * to_point[0]
y = (1-t)**3 * from_point[1] + 3*(1-t)**2*t * control_point_1[1] + 3*(1-t)*t**2 * control_point_2[1] + t**3 * to_point[1]
elif movement_type == "arc":
# Bogenbewegung
arc_height = distance * 0.2 * (1 if random.random() > 0.5 else -1)
x = from_point[0] + dx * t
y = from_point[1] + dy * t + arc_height * 4 * t * (1-t)
elif movement_type == "zigzag":
# Zickzack-Bewegung
zigzag_amplitude = distance * 0.05
x = from_point[0] + dx * t + zigzag_amplitude * math.sin(t * math.pi * 4)
y = from_point[1] + dy * t
else: # smooth
# Glatte S-Kurve
s_curve = t * t * (3 - 2 * t)
x = from_point[0] + dx * s_curve
y = from_point[1] + dy * s_curve
# Füge leichtes "Zittern" hinzu für mehr Realismus
if self.randomness > 0.3:
jitter = 2 * self.randomness
x += random.uniform(-jitter, jitter)
y += random.uniform(-jitter, jitter)
# Runde auf ganze Zahlen
curr_point = (int(x), int(y))
# Callback aufrufen, wenn vorhanden
if on_move:
on_move(curr_point)
# Micro-Pause einbauen
if random.random() < micro_pause_probability:
time.sleep(random.uniform(0.05, 0.2))
# Variable Geschwindigkeit
speed_factor = random.choice(speed_variations)
# Verzögerung basierend auf der Position in der Bewegung
# Am Anfang und Ende langsamer, in der Mitte schneller
if i < 0.15 * steps or i > 0.85 * steps:
self.sleep("mouse_movement", 2.0 * speed_factor / steps)
elif i < 0.3 * steps or i > 0.7 * steps:
self.sleep("mouse_movement", 1.5 * speed_factor / steps)
else:
self.sleep("mouse_movement", 0.8 * speed_factor / steps)
def click(self, double: bool = False, right: bool = False) -> None:
"""
Simuliert einen Mausklick mit menschlicher Verzögerung.
Args:
double: True für Doppelklick, False für Einzelklick
right: True für Rechtsklick, False für Linksklick
"""
click_type = "right" if right else "left"
click_count = 2 if double else 1
for _ in range(click_count):
logger.debug(f"{click_type.capitalize()}-Klick")
self.sleep("click")
if double and _ == 0:
# Kürzere Pause zwischen Doppelklicks
self.sleep("click", 0.3)
def scroll(self, direction: str = "down", amount: int = 5,
on_scroll: Optional[Callable[[int], None]] = None) -> None:
"""
Simuliert Scrollen mit menschlicher Verzögerung.
Args:
direction: "up" oder "down"
amount: Anzahl der Scroll-Ereignisse
on_scroll: Optionale Funktion, die für jedes Scroll-Ereignis aufgerufen wird
"""
if direction not in ["up", "down"]:
logger.warning(f"Ungültige Scrollrichtung: {direction}")
return
# Vorzeichenwechsel für die Richtung
scroll_factor = -1 if direction == "up" else 1
# Wähle ein Scroll-Pattern
patterns = ["smooth", "reading", "fast_scan", "search", "momentum"]
pattern = random.choice(patterns)
logger.debug(f"Verwende Scroll-Pattern: {pattern}")
# Pattern-spezifische Parameter
if pattern == "smooth":
# Gleichmäßiges Scrollen
for i in range(amount):
scroll_amount = scroll_factor * random.randint(2, 4)
if on_scroll:
on_scroll(scroll_amount)
if i < amount - 1:
self.sleep("scroll", random.uniform(0.8, 1.2))
elif pattern == "reading":
# Lese-Pattern: langsam mit Pausen
for i in range(amount):
scroll_amount = scroll_factor * 1
if on_scroll:
on_scroll(scroll_amount)
if i < amount - 1:
if random.random() < 0.3: # 30% Chance für Lese-Pause
time.sleep(random.uniform(0.5, 2.0))
else:
self.sleep("scroll", random.uniform(1.5, 2.5))
elif pattern == "fast_scan":
# Schnelles Überfliegen
for i in range(amount):
scroll_amount = scroll_factor * random.randint(5, 8)
if on_scroll:
on_scroll(scroll_amount)
if i < amount - 1:
self.sleep("scroll", random.uniform(0.1, 0.3))
elif pattern == "search":
# Suchen-Pattern: unregelmäßig, vor und zurück
total_scrolled = 0
for i in range(amount):
if random.random() < 0.2 and total_scrolled > 5: # 20% Chance zurückzuscrollen
scroll_amount = -scroll_factor * random.randint(1, 3)
else:
scroll_amount = scroll_factor * random.randint(2, 5)
total_scrolled += abs(scroll_amount)
if on_scroll:
on_scroll(scroll_amount)
if i < amount - 1:
self.sleep("scroll", random.uniform(0.3, 1.0))
else: # momentum
# Momentum-Scrolling (wie Touch-Geräte)
initial_speed = random.randint(8, 12)
deceleration = 0.85
current_speed = initial_speed
while current_speed > 0.5 and amount > 0:
scroll_amount = scroll_factor * int(current_speed)
if on_scroll:
on_scroll(scroll_amount)
current_speed *= deceleration
amount -= 1
if amount > 0:
self.sleep("scroll", 0.05) # Sehr kurze Pausen für flüssige Bewegung
# Gelegentliches "Overscroll" und Bounce-Back
if random.random() < 0.1 and pattern != "momentum":
time.sleep(0.1)
if on_scroll:
on_scroll(-scroll_factor * 2) # Kleiner Bounce-Back
def wait_for_page_load(self, multiplier: float = 1.0) -> None:
"""
Wartet eine angemessene Zeit auf das Laden einer Seite.
Args:
multiplier: Multiplikator für die Standardwartezeit
"""
self.sleep("page_load", multiplier)
def wait_between_actions(self, action_type: str = "decision", multiplier: float = 1.0) -> None:
"""
Wartet zwischen Aktionen, um menschliches Verhalten zu simulieren.
Args:
action_type: Art der Aktion
multiplier: Multiplikator für die Standardwartezeit
"""
self.sleep(action_type, multiplier)
def navigate_sequence(self, steps: int, min_delay: float = 0.5, max_delay: float = 2.0) -> None:
"""
Simuliert eine Sequenz von Navigationsschritten mit variierenden Verzögerungen.
Args:
steps: Anzahl der Navigationsschritte
min_delay: Minimale Verzögerung zwischen Schritten
max_delay: Maximale Verzögerung zwischen Schritten
"""
for i in range(steps):
# Zufällige Verzögerung zwischen Navigationsschritten
delay = random.uniform(min_delay, max_delay)
logger.debug(f"Navigationsschritt {i+1}/{steps}, Verzögerung: {delay:.2f}s")
time.sleep(delay / self.speed_factor)
def human_delay_pattern(self, action_type: str = "default", intensity: str = "medium") -> None:
"""
Erzeugt ein komplexes, menschliches Verzögerungsmuster.
Args:
action_type: Art der Aktion (entscheidet über Basismuster)
intensity: Intensität des Musters ("low", "medium", "high")
"""
# Verzögerungsmuster basierend auf Aktionstyp und Intensität
patterns = {
"default": {
"low": (0.2, 0.5),
"medium": (0.5, 1.0),
"high": (1.0, 2.0)
},
"reading": {
"low": (1.0, 2.0),
"medium": (2.0, 4.0),
"high": (3.0, 6.0)
},
"thinking": {
"low": (1.5, 3.0),
"medium": (3.0, 5.0),
"high": (5.0, 8.0)
},
"verification": {
"low": (3.0, 5.0),
"medium": (5.0, 8.0),
"high": (8.0, 12.0)
}
}
# Standardmuster verwenden, wenn nicht bekannt
pattern = patterns.get(action_type, patterns["default"])
delay_range = pattern.get(intensity, pattern["medium"])
# Zufällige Verzögerung im angegebenen Bereich
delay = random.uniform(delay_range[0], delay_range[1])
# Anpassung basierend auf Geschwindigkeit und Zufälligkeit
delay = delay / self.speed_factor
if self.randomness > 0:
# Füge ein zufälliges "Zittern" hinzu
jitter = random.uniform(-0.2, 0.2) * self.randomness * delay
delay += jitter
logger.debug(f"Menschliche Verzögerung ({action_type}, {intensity}): {delay:.2f}s")
time.sleep(max(0, delay))
def simulate_form_filling(self, fields: int, field_callback: Optional[Callable[[int], None]] = None) -> None:
"""
Simuliert das Ausfüllen eines Formulars mit menschlichem Verhalten.
Args:
fields: Anzahl der auszufüllenden Felder
field_callback: Optionale Funktion, die für jedes Feld aufgerufen wird
"""
for i in range(fields):
logger.debug(f"Fülle Formularfeld {i+1}/{fields} aus")
if field_callback:
field_callback(i)
# Verzögerung zwischen Feldern
if i < fields - 1: # Keine Verzögerung nach dem letzten Feld
# Gelegentlich längere Pausen einbauen
if random.random() < 0.2 * self.randomness:
self.human_delay_pattern("thinking", "low")
else:
self.sleep("form_fill")
def simulate_captcha_solving(self, on_progress: Optional[Callable[[float], None]] = None) -> None:
"""
Simuliert das Lösen eines CAPTCHAs mit menschlichem Verhalten.
Args:
on_progress: Optionale Funktion, die mit dem Fortschritt (0-1) aufgerufen wird
"""
# Simuliere einen komplexen Prozess mit mehreren Schritten
steps = random.randint(4, 8)
for i in range(steps):
progress = (i + 1) / steps
logger.debug(f"CAPTCHA-Lösung Fortschritt: {progress:.0%}")
if on_progress:
on_progress(progress)
# Verschiedene Verzögerungsmuster für die einzelnen Schritte
if i == 0:
# Anfängliches Lesen/Verstehen
self.human_delay_pattern("reading", "medium")
elif i == steps - 1:
# Abschließende Überprüfung/Bestätigung
self.human_delay_pattern("verification", "low")
else:
# Auswahl/Interaktion
self.human_delay_pattern("thinking", "medium")

69
utils/logger.py Normale Datei
Datei anzeigen

@ -0,0 +1,69 @@
"""
Logger-Konfiguration für die Social Media Account Generator Anwendung.
"""
import os
import logging
import sys
from PyQt5.QtWidgets import QTextEdit
from PyQt5.QtGui import QTextCursor
class LogHandler(logging.Handler):
"""Handler, der Logs an ein QTextEdit-Widget sendet."""
def __init__(self, text_widget=None):
super().__init__()
self.text_widget = text_widget
if self.text_widget:
self.text_widget.setReadOnly(True)
def emit(self, record):
msg = self.format(record)
if self.text_widget:
self.text_widget.append(msg)
# Scrolle nach unten
self.text_widget.moveCursor(QTextCursor.End)
def setup_logger(name="main", level=logging.DEBUG):
"""
Konfiguriert und gibt einen Logger zurück.
Args:
name: Name des Loggers
level: Logging-Level
Returns:
Konfigurierter Logger
"""
logger = logging.getLogger(name)
# Verhindere doppelte Handler
if logger.handlers:
return logger
logger.setLevel(level)
# Datehandler
log_file = os.path.join("logs", f"{name}.log")
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(file_handler)
# Konsolen-Handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(console_handler)
return logger
def add_gui_handler(logger, text_widget):
"""
Fügt einem Logger einen GUI-Handler hinzu.
Args:
logger: Logger, dem der Handler hinzugefügt werden soll
text_widget: QTextEdit-Widget für die Ausgabe
"""
gui_handler = LogHandler(text_widget)
gui_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(gui_handler)

386
utils/modal_manager.py Normale Datei
Datei anzeigen

@ -0,0 +1,386 @@
"""
Modal Manager - Zentraler Manager für alle Progress-Modals
"""
import logging
from typing import Optional, Dict, Any
from PyQt5.QtCore import QObject, pyqtSignal, QTimer
from PyQt5.QtWidgets import QWidget
from views.widgets.progress_modal import ProgressModal
from styles.modal_styles import ModalStyles
logger = logging.getLogger("modal_manager")
class ModalManager(QObject):
"""
Zentraler Manager für alle Progress-Modals.
Koordiniert das Anzeigen und Verstecken von Modals während Automatisierungsprozessen.
"""
# Signale
modal_shown = pyqtSignal(str) # Modal-Typ
modal_hidden = pyqtSignal(str) # Modal-Typ
modal_force_closed = pyqtSignal(str) # Modal-Typ
def __init__(self, parent_window: QWidget = None, language_manager=None, style_manager=None):
super().__init__()
self.parent_window = parent_window
self.language_manager = language_manager
self.style_manager = style_manager or ModalStyles()
# Aktive Modals verwalten
self.active_modals: Dict[str, ProgressModal] = {}
self.modal_stack = [] # Stack für verschachtelte Modals
# Auto-Hide Timer für Fehler-Modals
self.auto_hide_timers: Dict[str, QTimer] = {}
logger.info("ModalManager initialisiert")
def show_modal(self, modal_type: str, title: str = None, status: str = None, detail: str = None) -> bool:
"""
Zeigt ein Progress-Modal an.
Args:
modal_type: Typ des Modals ('account_creation', 'login_process', etc.)
title: Optional - benutzerdefinierter Titel
status: Optional - benutzerdefinierter Status
detail: Optional - benutzerdefinierter Detail-Text
Returns:
bool: True wenn Modal erfolgreich angezeigt wurde
"""
try:
# Prüfe ob Modal bereits aktiv ist
if modal_type in self.active_modals:
logger.warning(f"Modal '{modal_type}' ist bereits aktiv")
return False
# Erstelle neues Modal
modal = ProgressModal(
parent=self.parent_window,
modal_type=modal_type,
language_manager=self.language_manager,
style_manager=self.style_manager
)
# Verbinde Signale
modal.force_closed.connect(lambda: self._handle_force_close(modal_type))
# Speichere Modal
self.active_modals[modal_type] = modal
self.modal_stack.append(modal_type)
# Benutzerdefinierte Texte setzen (falls angegeben)
if title or status or detail:
if title:
modal.title_label.setText(title)
if status:
modal.status_label.setText(status)
if detail:
modal.detail_label.setText(detail)
modal.detail_label.setVisible(True)
# Modal anzeigen
modal.show_process(modal_type)
# Signal senden
self.modal_shown.emit(modal_type)
logger.info(f"Modal '{modal_type}' angezeigt")
return True
except Exception as e:
logger.error(f"Fehler beim Anzeigen des Modals '{modal_type}': {e}")
return False
def hide_modal(self, modal_type: str) -> bool:
"""
Versteckt ein spezifisches Modal.
Args:
modal_type: Typ des zu versteckenden Modals
Returns:
bool: True wenn Modal erfolgreich versteckt wurde
"""
try:
if modal_type not in self.active_modals:
logger.warning(f"Modal '{modal_type}' ist nicht aktiv")
return False
modal = self.active_modals[modal_type]
# Modal verstecken
modal.hide_process()
# Aus aktiven Modals entfernen
del self.active_modals[modal_type]
# Aus Stack entfernen
if modal_type in self.modal_stack:
self.modal_stack.remove(modal_type)
# Auto-Hide Timer stoppen (falls vorhanden)
if modal_type in self.auto_hide_timers:
self.auto_hide_timers[modal_type].stop()
del self.auto_hide_timers[modal_type]
# Modal löschen
modal.deleteLater()
# Signal senden
self.modal_hidden.emit(modal_type)
logger.info(f"Modal '{modal_type}' versteckt")
return True
except Exception as e:
logger.error(f"Fehler beim Verstecken des Modals '{modal_type}': {e}")
return False
def update_modal_status(self, modal_type: str, status: str, detail: str = None) -> bool:
"""
Aktualisiert den Status eines aktiven Modals.
Args:
modal_type: Typ des Modals
status: Neuer Status-Text
detail: Optional - neuer Detail-Text
Returns:
bool: True wenn Update erfolgreich war
"""
try:
if modal_type not in self.active_modals:
logger.warning(f"Modal '{modal_type}' ist nicht aktiv für Status-Update")
return False
modal = self.active_modals[modal_type]
modal.update_status(status, detail)
logger.debug(f"Modal '{modal_type}' Status aktualisiert: {status}")
return True
except Exception as e:
logger.error(f"Fehler beim Aktualisieren des Modal-Status '{modal_type}': {e}")
return False
def show_modal_error(self, modal_type: str, error_message: str, auto_close_seconds: int = 3) -> bool:
"""
Zeigt eine Fehlermeldung in einem Modal an.
Args:
modal_type: Typ des Modals
error_message: Fehlermeldung
auto_close_seconds: Sekunden bis automatisches Schließen
Returns:
bool: True wenn Fehler erfolgreich angezeigt wurde
"""
try:
if modal_type not in self.active_modals:
# Erstelle neues Error-Modal
self.show_modal(modal_type, "❌ Fehler aufgetreten", error_message)
modal = self.active_modals[modal_type]
else:
modal = self.active_modals[modal_type]
modal.show_error(error_message, auto_close_seconds)
# Auto-Hide Timer setzen
if auto_close_seconds > 0:
timer = QTimer()
timer.setSingleShot(True)
timer.timeout.connect(lambda: self.hide_modal(modal_type))
timer.start(auto_close_seconds * 1000)
self.auto_hide_timers[modal_type] = timer
logger.info(f"Fehler in Modal '{modal_type}' angezeigt: {error_message}")
return True
except Exception as e:
logger.error(f"Fehler beim Anzeigen des Modal-Fehlers '{modal_type}': {e}")
return False
def hide_all_modals(self):
"""Versteckt alle aktiven Modals"""
modal_types = list(self.active_modals.keys())
for modal_type in modal_types:
self.hide_modal(modal_type)
logger.info("Alle Modals versteckt")
def is_modal_active(self, modal_type: str) -> bool:
"""
Prüft ob ein bestimmtes Modal aktiv ist.
Args:
modal_type: Typ des zu prüfenden Modals
Returns:
bool: True wenn Modal aktiv ist
"""
return modal_type in self.active_modals
def get_active_modals(self) -> list:
"""
Gibt eine Liste aller aktiven Modal-Typen zurück.
Returns:
list: Liste der aktiven Modal-Typen
"""
return list(self.active_modals.keys())
def get_current_modal(self) -> Optional[str]:
"""
Gibt den aktuell obersten Modal-Typ zurück.
Returns:
Optional[str]: Modal-Typ oder None wenn kein Modal aktiv
"""
return self.modal_stack[-1] if self.modal_stack else None
def _handle_force_close(self, modal_type: str):
"""
Behandelt das zwangsweise Schließen eines Modals (durch Timeout).
Args:
modal_type: Typ des geschlossenen Modals
"""
logger.warning(f"Modal '{modal_type}' wurde zwangsweise geschlossen")
# Modal aus aktiven Modals entfernen
if modal_type in self.active_modals:
del self.active_modals[modal_type]
# Aus Stack entfernen
if modal_type in self.modal_stack:
self.modal_stack.remove(modal_type)
# Signal senden
self.modal_force_closed.emit(modal_type)
def handle_event(self, action: str, modal_type: str, **kwargs):
"""
Zentrale Event-Behandlung für Modal-Aktionen.
Args:
action: Aktion ('show', 'hide', 'update', 'error')
modal_type: Typ des Modals
**kwargs: Zusätzliche Parameter je nach Aktion
"""
try:
if action == 'show':
title = kwargs.get('title')
status = kwargs.get('status')
detail = kwargs.get('detail')
self.show_modal(modal_type, title, status, detail)
elif action == 'hide':
self.hide_modal(modal_type)
elif action == 'update':
status = kwargs.get('status', '')
detail = kwargs.get('detail')
self.update_modal_status(modal_type, status, detail)
elif action == 'error':
error_message = kwargs.get('error_message', 'Unbekannter Fehler')
auto_close = kwargs.get('auto_close_seconds', 3)
self.show_modal_error(modal_type, error_message, auto_close)
else:
logger.warning(f"Unbekannte Modal-Aktion: {action}")
except Exception as e:
logger.error(f"Fehler bei Modal-Event-Behandlung: {e}")
def set_parent_window(self, parent_window: QWidget):
"""
Setzt das Parent-Fenster für neue Modals.
Args:
parent_window: Das Parent-Widget
"""
self.parent_window = parent_window
logger.debug("Parent-Fenster für ModalManager gesetzt")
# Globale Instanz für einfachen Zugriff
_global_modal_manager: Optional[ModalManager] = None
def get_modal_manager() -> Optional[ModalManager]:
"""
Gibt die globale ModalManager-Instanz zurück.
Returns:
Optional[ModalManager]: Die globale Instanz oder None
"""
return _global_modal_manager
def set_modal_manager(modal_manager: ModalManager):
"""
Setzt die globale ModalManager-Instanz.
Args:
modal_manager: Die zu setzende ModalManager-Instanz
"""
global _global_modal_manager
_global_modal_manager = modal_manager
def show_progress_modal(modal_type: str, **kwargs) -> bool:
"""
Convenience-Funktion zum Anzeigen eines Progress-Modals.
Args:
modal_type: Typ des Modals
**kwargs: Zusätzliche Parameter
Returns:
bool: True wenn erfolgreich
"""
manager = get_modal_manager()
if manager:
return manager.show_modal(modal_type, **kwargs)
return False
def hide_progress_modal(modal_type: str) -> bool:
"""
Convenience-Funktion zum Verstecken eines Progress-Modals.
Args:
modal_type: Typ des Modals
Returns:
bool: True wenn erfolgreich
"""
manager = get_modal_manager()
if manager:
return manager.hide_modal(modal_type)
return False
def update_progress_modal(modal_type: str, status: str, detail: str = None) -> bool:
"""
Convenience-Funktion zum Aktualisieren eines Progress-Modals.
Args:
modal_type: Typ des Modals
status: Neuer Status
detail: Optional - neuer Detail-Text
Returns:
bool: True wenn erfolgreich
"""
manager = get_modal_manager()
if manager:
return manager.update_modal_status(modal_type, status, detail)
return False

195
utils/modal_test.py Normale Datei
Datei anzeigen

@ -0,0 +1,195 @@
"""
Modal System Test - Test-Funktionen für das Modal-System
"""
import logging
import time
from typing import Optional
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget
from PyQt5.QtCore import QTimer
from utils.modal_manager import ModalManager
from views.widgets.progress_modal import ProgressModal
from views.widgets.account_creation_modal import AccountCreationModal
from views.widgets.login_process_modal import LoginProcessModal
logger = logging.getLogger("modal_test")
class ModalTestWindow(QMainWindow):
"""Test-Fenster für Modal-System Tests"""
def __init__(self):
super().__init__()
self.setWindowTitle("AccountForger Modal System Test")
self.setGeometry(100, 100, 600, 400)
# Modal Manager
self.modal_manager = ModalManager(parent_window=self)
# Test UI
self.setup_ui()
def setup_ui(self):
"""Erstellt Test-UI"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# Test Buttons
btn_account_creation = QPushButton("Test Account Creation Modal")
btn_account_creation.clicked.connect(self.test_account_creation_modal)
layout.addWidget(btn_account_creation)
btn_login_process = QPushButton("Test Login Process Modal")
btn_login_process.clicked.connect(self.test_login_process_modal)
layout.addWidget(btn_login_process)
btn_generic_modal = QPushButton("Test Generic Progress Modal")
btn_generic_modal.clicked.connect(self.test_generic_modal)
layout.addWidget(btn_generic_modal)
btn_error_modal = QPushButton("Test Error Modal")
btn_error_modal.clicked.connect(self.test_error_modal)
layout.addWidget(btn_error_modal)
btn_modal_manager = QPushButton("Test Modal Manager")
btn_modal_manager.clicked.connect(self.test_modal_manager)
layout.addWidget(btn_modal_manager)
def test_account_creation_modal(self):
"""Testet Account Creation Modal"""
logger.info("Testing Account Creation Modal")
modal = AccountCreationModal(parent=self, platform="Instagram")
# Steps setzen
steps = [
"Browser wird vorbereitet",
"Formular wird ausgefüllt",
"Account wird erstellt",
"E-Mail wird verifiziert"
]
modal.set_steps(steps)
# Modal anzeigen
modal.show_platform_specific_process()
# Simuliere Steps
QTimer.singleShot(1000, lambda: modal.start_step("Browser wird vorbereitet"))
QTimer.singleShot(2000, lambda: modal.complete_step("Browser wird vorbereitet", "Formular wird ausgefüllt"))
QTimer.singleShot(3000, lambda: modal.start_step("Formular wird ausgefüllt"))
QTimer.singleShot(4000, lambda: modal.complete_step("Formular wird ausgefüllt", "Account wird erstellt"))
QTimer.singleShot(5000, lambda: modal.start_step("Account wird erstellt"))
QTimer.singleShot(6000, lambda: modal.complete_step("Account wird erstellt", "E-Mail wird verifiziert"))
QTimer.singleShot(7000, lambda: modal.start_step("E-Mail wird verifiziert"))
QTimer.singleShot(8000, lambda: modal.show_success({"username": "test_user", "platform": "Instagram"}))
def test_login_process_modal(self):
"""Testet Login Process Modal"""
logger.info("Testing Login Process Modal")
modal = LoginProcessModal(parent=self, platform="TikTok")
# Session Login testen
modal.show_session_login("test_account", "TikTok")
# Simuliere Login-Prozess
QTimer.singleShot(1000, lambda: modal.update_login_progress("browser_init", "Browser wird gestartet"))
QTimer.singleShot(2000, lambda: modal.update_login_progress("session_restore", "Session wird wiederhergestellt"))
QTimer.singleShot(3000, lambda: modal.update_login_progress("verification", "Login wird geprüft"))
QTimer.singleShot(4000, lambda: modal.show_session_restored())
def test_generic_modal(self):
"""Testet Generic Progress Modal"""
logger.info("Testing Generic Progress Modal")
modal = ProgressModal(parent=self, modal_type="verification")
modal.show_process()
# Simuliere Updates
QTimer.singleShot(1000, lambda: modal.update_status("Verbindung wird hergestellt...", "Server wird kontaktiert"))
QTimer.singleShot(2000, lambda: modal.update_status("Daten werden verarbeitet...", "Bitte warten"))
QTimer.singleShot(3000, lambda: modal.update_status("✅ Vorgang abgeschlossen!", "Erfolgreich"))
QTimer.singleShot(4000, lambda: modal.hide_process())
def test_error_modal(self):
"""Testet Error Modal"""
logger.info("Testing Error Modal")
modal = ProgressModal(parent=self, modal_type="generic")
modal.show_process()
# Nach kurzer Zeit Fehler anzeigen
QTimer.singleShot(1500, lambda: modal.show_error("Netzwerkfehler aufgetreten", auto_close_seconds=3))
def test_modal_manager(self):
"""Testet Modal Manager"""
logger.info("Testing Modal Manager")
# Zeige Account Creation Modal über Manager
self.modal_manager.show_modal(
'account_creation',
title="🔄 Test Account wird erstellt",
status="Modal Manager Test läuft...",
detail="Über ModalManager aufgerufen"
)
# Simuliere Updates über Manager
QTimer.singleShot(1000, lambda: self.modal_manager.update_modal_status(
'account_creation',
"Browser wird initialisiert...",
"Schritt 1 von 3"
))
QTimer.singleShot(2000, lambda: self.modal_manager.update_modal_status(
'account_creation',
"Formular wird ausgefüllt...",
"Schritt 2 von 3"
))
QTimer.singleShot(3000, lambda: self.modal_manager.update_modal_status(
'account_creation',
"Account wird finalisiert...",
"Schritt 3 von 3"
))
QTimer.singleShot(4000, lambda: self.modal_manager.update_modal_status(
'account_creation',
"✅ Account erfolgreich erstellt!",
"Test abgeschlossen"
))
QTimer.singleShot(5000, lambda: self.modal_manager.hide_modal('account_creation'))
def run_modal_test():
"""Führt den Modal-Test aus"""
import sys
# QApplication erstellen falls nicht vorhanden
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# Test-Fenster erstellen
test_window = ModalTestWindow()
test_window.show()
# App ausführen
if hasattr(app, 'exec'):
return app.exec()
else:
return app.exec_()
if __name__ == "__main__":
# Logging konfigurieren
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Test ausführen
run_modal_test()

338
utils/password_generator.py Normale Datei
Datei anzeigen

@ -0,0 +1,338 @@
"""
Passwortgenerator für den Social Media Account Generator.
"""
import random
import string
import logging
from typing import Dict, List, Any, Optional, Tuple, Union
logger = logging.getLogger("password_generator")
class PasswordGenerator:
"""Klasse zur Generierung sicherer und plattformkonformer Passwörter."""
def __init__(self):
"""Initialisiert den PasswordGenerator."""
# Passwort-Richtlinien für verschiedene Plattformen
self.platform_policies = {
"instagram": {
"min_length": 20,
"max_length": 20,
"require_uppercase": True,
"require_lowercase": True,
"require_digits": True,
"require_special": True,
"allowed_special": "!$§%?",
"disallowed_chars": ""
},
"facebook": {
"min_length": 8,
"max_length": 20,
"require_uppercase": False,
"require_lowercase": True,
"require_digits": False,
"require_special": False,
"allowed_special": "!@#$%^&*()",
"disallowed_chars": ""
},
"twitter": {
"min_length": 8,
"max_length": 20,
"require_uppercase": False,
"require_lowercase": True,
"require_digits": False,
"require_special": False,
"allowed_special": "!@#$%^&*()",
"disallowed_chars": ""
},
"tiktok": {
"min_length": 10,
"max_length": 10,
"require_uppercase": True,
"require_lowercase": True,
"require_digits": True,
"require_special": True,
"allowed_special": "!$%&/()=?",
"disallowed_chars": ""
},
"default": {
"min_length": 8,
"max_length": 16,
"require_uppercase": True,
"require_lowercase": True,
"require_digits": True,
"require_special": True,
"allowed_special": "!@#$%^&*()",
"disallowed_chars": ""
}
}
def get_platform_policy(self, platform: str) -> Dict[str, Any]:
"""
Gibt die Passwort-Richtlinie für eine bestimmte Plattform zurück.
Args:
platform: Name der Plattform
Returns:
Dictionary mit der Passwort-Richtlinie
"""
platform = platform.lower()
return self.platform_policies.get(platform, self.platform_policies["default"])
def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None:
"""
Setzt oder aktualisiert die Passwort-Richtlinie für eine Plattform.
Args:
platform: Name der Plattform
policy: Dictionary mit der Passwort-Richtlinie
"""
platform = platform.lower()
self.platform_policies[platform] = policy
logger.info(f"Passwort-Richtlinie für '{platform}' aktualisiert")
def generate_password(self, platform: str = "default", length: Optional[int] = None,
custom_policy: Optional[Dict[str, Any]] = None) -> str:
"""
Generiert ein Passwort gemäß den Richtlinien.
Args:
platform: Name der Plattform
length: Optionale Länge des Passworts (überschreibt die Plattformrichtlinie)
custom_policy: Optionale benutzerdefinierte Richtlinie
Returns:
Generiertes Passwort
"""
# Richtlinie bestimmen
if custom_policy:
policy = custom_policy
else:
policy = self.get_platform_policy(platform)
# Länge bestimmen
if length:
if length < policy["min_length"]:
logger.warning(f"Angeforderte Länge ({length}) ist kleiner als das Minimum "
f"({policy['min_length']}). Verwende Minimum.")
length = policy["min_length"]
elif length > policy["max_length"]:
logger.warning(f"Angeforderte Länge ({length}) ist größer als das Maximum "
f"({policy['max_length']}). Verwende Maximum.")
length = policy["max_length"]
else:
# Zufällige Länge im erlaubten Bereich
length = random.randint(policy["min_length"], policy["max_length"])
# Verfügbare Zeichen bestimmen
available_chars = ""
if policy["require_lowercase"] or not (policy["require_uppercase"] or
policy["require_digits"] or
policy["require_special"]):
available_chars += string.ascii_lowercase
if policy["require_uppercase"]:
available_chars += string.ascii_uppercase
if policy["require_digits"]:
available_chars += string.digits
if policy["require_special"] and policy["allowed_special"]:
available_chars += policy["allowed_special"]
# Entferne nicht erlaubte Zeichen
if policy["disallowed_chars"]:
available_chars = "".join(char for char in available_chars
if char not in policy["disallowed_chars"])
# Sicherstellen, dass keine leere Zeichenmenge vorliegt
if not available_chars:
logger.error("Keine Zeichen für die Passwortgenerierung verfügbar")
available_chars = string.ascii_lowercase
# Passwort generieren
password = "".join(random.choice(available_chars) for _ in range(length))
# Überprüfen, ob die Anforderungen erfüllt sind
meets_requirements = True
if policy["require_lowercase"] and not any(char.islower() for char in password):
meets_requirements = False
if policy["require_uppercase"] and not any(char.isupper() for char in password):
meets_requirements = False
if policy["require_digits"] and not any(char.isdigit() for char in password):
meets_requirements = False
if policy["require_special"] and not any(char in policy["allowed_special"] for char in password):
meets_requirements = False
# Falls die Anforderungen nicht erfüllt sind, erneut generieren
if not meets_requirements:
logger.debug("Generiertes Passwort erfüllt nicht alle Anforderungen, generiere neu")
return self.generate_password(platform, length, custom_policy)
logger.info(f"Passwort für '{platform}' generiert (Länge: {length})")
return password
def generate_platform_password(self, platform: str) -> str:
"""
Generiert ein Passwort für eine bestimmte Plattform.
Args:
platform: Name der Plattform
Returns:
Generiertes Passwort
"""
return self.generate_password(platform)
def generate_strong_password(self, length: int = 16) -> str:
"""
Generiert ein starkes Passwort.
Args:
length: Länge des Passworts
Returns:
Generiertes Passwort
"""
custom_policy = {
"min_length": length,
"max_length": length,
"require_uppercase": True,
"require_lowercase": True,
"require_digits": True,
"require_special": True,
"allowed_special": "!@#$%^&*()-_=+[]{}<>,.;:/?|",
"disallowed_chars": ""
}
return self.generate_password(custom_policy=custom_policy)
def generate_memorable_password(self, num_words: int = 3, separator: str = "-") -> str:
"""
Generiert ein einprägsames Passwort aus Wörtern und Zahlen.
Args:
num_words: Anzahl der Wörter
separator: Trennzeichen zwischen den Wörtern
Returns:
Generiertes Passwort
"""
# Liste von einfachen Wörtern (kann erweitert/angepasst werden)
words = [
"time", "year", "people", "way", "day", "man", "thing", "woman", "life", "child",
"world", "school", "state", "family", "student", "group", "country", "problem",
"hand", "part", "place", "case", "week", "company", "system", "program", "question",
"work", "government", "number", "night", "point", "home", "water", "room", "mother",
"area", "money", "story", "fact", "month", "lot", "right", "study", "book", "eye",
"job", "word", "business", "issue", "side", "kind", "head", "house", "service",
"friend", "father", "power", "hour", "game", "line", "end", "member", "law", "car",
"city", "name", "team", "minute", "idea", "kid", "body", "back", "parent", "face",
"level", "office", "door", "health", "person", "art", "war", "history", "party",
"result", "change", "morning", "reason", "research", "girl", "guy", "moment", "air",
"teacher", "force", "education"
]
# Zufällige Wörter auswählen
selected_words = random.sample(words, num_words)
# Groß- und Kleinschreibung variieren und Zahlen hinzufügen
for i in range(len(selected_words)):
if random.choice([True, False]):
selected_words[i] = selected_words[i].capitalize()
# Mit 50% Wahrscheinlichkeit eine Zahl anhängen
if random.choice([True, False]):
selected_words[i] += str(random.randint(0, 9))
# Passwort zusammensetzen
password = separator.join(selected_words)
logger.info(f"Einprägsames Passwort generiert (Länge: {len(password)})")
return password
def validate_password(self, password: str, platform: str = "default",
custom_policy: Optional[Dict[str, Any]] = None) -> Tuple[bool, str]:
"""
Überprüft, ob ein Passwort den Richtlinien entspricht.
Args:
password: Zu überprüfendes Passwort
platform: Name der Plattform
custom_policy: Optionale benutzerdefinierte Richtlinie
Returns:
(Gültigkeit, Fehlermeldung)
"""
# Richtlinie bestimmen
if custom_policy:
policy = custom_policy
else:
policy = self.get_platform_policy(platform)
# Prüfungen durchführen
if len(password) < policy["min_length"]:
return False, f"Passwort ist zu kurz (mindestens {policy['min_length']} Zeichen erforderlich)"
if len(password) > policy["max_length"]:
return False, f"Passwort ist zu lang (maximal {policy['max_length']} Zeichen erlaubt)"
if policy["require_lowercase"] and not any(char.islower() for char in password):
return False, "Passwort muss mindestens einen Kleinbuchstaben enthalten"
if policy["require_uppercase"] and not any(char.isupper() for char in password):
return False, "Passwort muss mindestens einen Großbuchstaben enthalten"
if policy["require_digits"] and not any(char.isdigit() for char in password):
return False, "Passwort muss mindestens eine Ziffer enthalten"
if policy["require_special"] and not any(char in policy["allowed_special"] for char in password):
return False, f"Passwort muss mindestens ein Sonderzeichen enthalten ({policy['allowed_special']})"
if policy["disallowed_chars"] and any(char in policy["disallowed_chars"] for char in password):
return False, f"Passwort enthält nicht erlaubte Zeichen ({policy['disallowed_chars']})"
return True, "Passwort ist gültig"
# Kompatibilitätsfunktion für Legacy-Code, der direkt generate_password() importiert
def generate_password(platform: str = "instagram", length: Optional[int] = None) -> str:
"""
Kompatibilitätsfunktion für ältere Codeversionen.
Generiert ein Passwort für die angegebene Plattform.
Args:
platform: Name der Plattform
length: Optionale Länge des Passworts
Returns:
Generiertes Passwort
"""
# Einmalige Logger-Warnung, wenn die Legacy-Funktion verwendet wird
logger.warning("Die Funktion generate_password() ist veraltet, bitte verwende stattdessen die PasswordGenerator-Klasse.")
# Eine Instanz der Generator-Klasse erstellen und die Methode aufrufen
generator = PasswordGenerator()
return generator.generate_password(platform, length)
# Weitere Legacy-Funktionen für Kompatibilität
def generate_strong_password(length: int = 16) -> str:
"""Legacy-Funktion für Kompatibilität."""
generator = PasswordGenerator()
return generator.generate_strong_password(length)
def generate_memorable_password(num_words: int = 3, separator: str = "-") -> str:
"""Legacy-Funktion für Kompatibilität."""
generator = PasswordGenerator()
return generator.generate_memorable_password(num_words, separator)

412
utils/performance_monitor.py Normale Datei
Datei anzeigen

@ -0,0 +1,412 @@
"""
Performance Monitor - Non-intrusive monitoring for race condition detection
Debug-only monitoring without production performance impact
"""
import time
import threading
import functools
import traceback
from typing import Dict, Any, Optional, Callable, List
from collections import defaultdict, deque
from datetime import datetime, timedelta
from dataclasses import dataclass, field
import logging
import json
import os
logger = logging.getLogger(__name__)
@dataclass
class OperationMetrics:
"""Metriken für eine einzelne Operation"""
operation_name: str
thread_id: int
thread_name: str
start_time: float
end_time: Optional[float] = None
duration: Optional[float] = None
success: bool = True
error_message: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
stack_trace: Optional[str] = None
def complete(self, success: bool = True, error_message: Optional[str] = None):
"""Markiert Operation als abgeschlossen"""
self.end_time = time.time()
self.duration = self.end_time - self.start_time
self.success = success
self.error_message = error_message
def to_dict(self) -> Dict[str, Any]:
"""Konvertiert zu Dictionary für Serialisierung"""
return {
'operation_name': self.operation_name,
'thread_id': self.thread_id,
'thread_name': self.thread_name,
'start_time': self.start_time,
'end_time': self.end_time,
'duration': self.duration,
'success': self.success,
'error_message': self.error_message,
'metadata': self.metadata,
'has_stack_trace': self.stack_trace is not None
}
class PerformanceMonitor:
"""
Performance-Monitor mit race condition detection
"""
def __init__(self, enabled: bool = None, max_history: int = 1000):
# Auto-detect based on debug settings oder environment
if enabled is None:
enabled = (
os.getenv('DEBUG_RACE_CONDITIONS', '').lower() in ['true', '1', 'yes'] or
os.getenv('PERFORMANCE_MONITORING', '').lower() in ['true', '1', 'yes']
)
self.enabled = enabled
self.max_history = max_history
# Monitoring data
self._operation_history: deque = deque(maxlen=max_history)
self._active_operations: Dict[str, OperationMetrics] = {}
self._operation_stats: Dict[str, Dict[str, Any]] = defaultdict(lambda: {
'total_calls': 0,
'successful_calls': 0,
'failed_calls': 0,
'total_duration': 0.0,
'min_duration': float('inf'),
'max_duration': 0.0,
'concurrent_executions': 0,
'max_concurrent': 0
})
# Thread safety
self._lock = threading.RLock()
# Race condition detection
self._potential_races: List[Dict[str, Any]] = []
self._long_operations: List[Dict[str, Any]] = []
# Thresholds
self.long_operation_threshold = 2.0 # seconds
self.race_detection_window = 0.1 # seconds
if self.enabled:
logger.info("Performance monitoring enabled")
def monitor_operation(self, operation_name: str, capture_stack: bool = False):
"""
Decorator für Operation-Monitoring
"""
def decorator(func: Callable) -> Callable:
if not self.enabled:
return func # No overhead when disabled
@functools.wraps(func)
def wrapper(*args, **kwargs):
return self._execute_monitored(
operation_name or func.__name__,
func,
capture_stack,
*args,
**kwargs
)
wrapper.original = func
wrapper.is_monitored = True
return wrapper
return decorator
def _execute_monitored(self, operation_name: str, func: Callable,
capture_stack: bool, *args, **kwargs) -> Any:
"""Führt eine überwachte Operation aus"""
if not self.enabled:
return func(*args, **kwargs)
thread_id = threading.current_thread().ident
thread_name = threading.current_thread().name
operation_key = f"{operation_name}_{thread_id}_{time.time()}"
# Metrics-Objekt erstellen
metrics = OperationMetrics(
operation_name=operation_name,
thread_id=thread_id,
thread_name=thread_name,
start_time=time.time(),
stack_trace=traceback.format_stack() if capture_stack else None
)
# Race condition detection
self._detect_potential_race(operation_name, metrics.start_time)
with self._lock:
# Concurrent execution tracking
concurrent_count = sum(
1 for op in self._active_operations.values()
if op.operation_name == operation_name
)
stats = self._operation_stats[operation_name]
stats['concurrent_executions'] = concurrent_count
stats['max_concurrent'] = max(stats['max_concurrent'], concurrent_count)
# Operation zu aktiven hinzufügen
self._active_operations[operation_key] = metrics
try:
# Operation ausführen
result = func(*args, **kwargs)
# Erfolg markieren
metrics.complete(success=True)
return result
except Exception as e:
# Fehler markieren
metrics.complete(success=False, error_message=str(e))
raise
finally:
# Cleanup und Statistik-Update
with self._lock:
self._active_operations.pop(operation_key, None)
self._update_statistics(metrics)
self._operation_history.append(metrics)
# Long operation detection
if metrics.duration and metrics.duration > self.long_operation_threshold:
self._record_long_operation(metrics)
def _detect_potential_race(self, operation_name: str, start_time: float):
"""Erkennt potentielle Race Conditions"""
if not self.enabled:
return
# Prüfe ob ähnliche Operationen zeitgleich laufen
concurrent_ops = []
with self._lock:
for op in self._active_operations.values():
if (op.operation_name == operation_name and
abs(op.start_time - start_time) < self.race_detection_window):
concurrent_ops.append(op)
if len(concurrent_ops) > 0:
race_info = {
'operation_name': operation_name,
'detected_at': start_time,
'concurrent_threads': [op.thread_id for op in concurrent_ops],
'time_window': self.race_detection_window,
'severity': 'high' if len(concurrent_ops) > 2 else 'medium'
}
self._potential_races.append(race_info)
logger.warning(f"Potential race condition detected: {operation_name} "
f"running on {len(concurrent_ops)} threads simultaneously")
def _record_long_operation(self, metrics: OperationMetrics):
"""Zeichnet lange Operationen auf"""
long_op_info = {
'operation_name': metrics.operation_name,
'duration': metrics.duration,
'thread_id': metrics.thread_id,
'start_time': metrics.start_time,
'success': metrics.success,
'metadata': metrics.metadata
}
self._long_operations.append(long_op_info)
logger.warning(f"Long operation detected: {metrics.operation_name} "
f"took {metrics.duration:.3f}s (threshold: {self.long_operation_threshold}s)")
def _update_statistics(self, metrics: OperationMetrics):
"""Aktualisiert Operation-Statistiken"""
stats = self._operation_stats[metrics.operation_name]
stats['total_calls'] += 1
if metrics.success:
stats['successful_calls'] += 1
else:
stats['failed_calls'] += 1
if metrics.duration:
stats['total_duration'] += metrics.duration
stats['min_duration'] = min(stats['min_duration'], metrics.duration)
stats['max_duration'] = max(stats['max_duration'], metrics.duration)
def get_statistics(self) -> Dict[str, Any]:
"""Gibt vollständige Monitoring-Statistiken zurück"""
if not self.enabled:
return {'monitoring_enabled': False}
with self._lock:
# Statistiken aufbereiten
processed_stats = {}
for op_name, stats in self._operation_stats.items():
processed_stats[op_name] = {
**stats,
'average_duration': (
stats['total_duration'] / stats['total_calls']
if stats['total_calls'] > 0 else 0
),
'success_rate': (
stats['successful_calls'] / stats['total_calls']
if stats['total_calls'] > 0 else 0
),
'min_duration': stats['min_duration'] if stats['min_duration'] != float('inf') else 0
}
return {
'monitoring_enabled': True,
'operation_statistics': processed_stats,
'race_conditions': {
'detected_count': len(self._potential_races),
'recent_races': self._potential_races[-10:], # Last 10
},
'long_operations': {
'detected_count': len(self._long_operations),
'threshold': self.long_operation_threshold,
'recent_long_ops': self._long_operations[-10:], # Last 10
},
'active_operations': len(self._active_operations),
'history_size': len(self._operation_history),
'thresholds': {
'long_operation_threshold': self.long_operation_threshold,
'race_detection_window': self.race_detection_window
}
}
def get_race_condition_report(self) -> Dict[str, Any]:
"""Gibt detaillierten Race Condition Report zurück"""
if not self.enabled:
return {'monitoring_enabled': False}
with self._lock:
# Gruppiere Race Conditions nach Operation
races_by_operation = defaultdict(list)
for race in self._potential_races:
races_by_operation[race['operation_name']].append(race)
# Analysiere Patterns
analysis = {}
for op_name, races in races_by_operation.items():
high_severity = sum(1 for r in races if r['severity'] == 'high')
analysis[op_name] = {
'total_races': len(races),
'high_severity_races': high_severity,
'affected_threads': len(set(
thread_id for race in races
for thread_id in race['concurrent_threads']
)),
'first_detected': min(r['detected_at'] for r in races),
'last_detected': max(r['detected_at'] for r in races),
'recommendation': self._get_race_recommendation(op_name, races)
}
return {
'monitoring_enabled': True,
'total_race_conditions': len(self._potential_races),
'affected_operations': len(races_by_operation),
'analysis_by_operation': analysis,
'raw_detections': self._potential_races
}
def _get_race_recommendation(self, operation_name: str, races: List[Dict]) -> str:
"""Gibt Empfehlungen für Race Condition Behebung"""
race_count = len(races)
high_severity_count = sum(1 for r in races if r['severity'] == 'high')
if high_severity_count > 5:
return f"CRITICAL: {operation_name} has {high_severity_count} high-severity race conditions. Implement ThreadSafetyMixin immediately."
elif race_count > 10:
return f"HIGH: {operation_name} frequently encounters race conditions. Consider adding thread synchronization."
elif race_count > 3:
return f"MEDIUM: {operation_name} occasionally has race conditions. Monitor and consider thread safety measures."
else:
return f"LOW: {operation_name} has minimal race condition risk."
def export_report(self, filename: Optional[str] = None) -> str:
"""Exportiert vollständigen Report als JSON"""
if not self.enabled:
return "Monitoring not enabled"
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"performance_report_{timestamp}.json"
report = {
'timestamp': datetime.now().isoformat(),
'statistics': self.get_statistics(),
'race_condition_report': self.get_race_condition_report(),
'operation_history': [op.to_dict() for op in list(self._operation_history)[-100:]] # Last 100
}
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
logger.info(f"Performance report exported to: {filename}")
return filename
except Exception as e:
logger.error(f"Failed to export performance report: {e}")
return f"Export failed: {e}"
def reset_statistics(self):
"""Setzt alle Statistiken zurück"""
with self._lock:
self._operation_history.clear()
self._operation_stats.clear()
self._potential_races.clear()
self._long_operations.clear()
# Aktive Operationen nicht löschen - könnten noch laufen
if self.enabled:
logger.info("Performance monitoring statistics reset")
# Global Monitor Instance
_global_monitor: Optional[PerformanceMonitor] = None
_monitor_init_lock = threading.RLock()
def get_performance_monitor() -> PerformanceMonitor:
"""Holt die globale Monitor-Instanz (Singleton)"""
global _global_monitor
if _global_monitor is None:
with _monitor_init_lock:
if _global_monitor is None:
_global_monitor = PerformanceMonitor()
return _global_monitor
# Convenience Decorators
def monitor_if_enabled(operation_name: str = None, capture_stack: bool = False):
"""Convenience decorator für conditional monitoring"""
monitor = get_performance_monitor()
return monitor.monitor_operation(operation_name, capture_stack)
def monitor_race_conditions(operation_name: str = None):
"""Speziell für Race Condition Detection"""
return monitor_if_enabled(operation_name, capture_stack=True)
def monitor_fingerprint_operations(operation_name: str = None):
"""Speziell für Fingerprint-Operationen"""
return monitor_if_enabled(f"fingerprint_{operation_name}", capture_stack=False)
def monitor_session_operations(operation_name: str = None):
"""Speziell für Session-Operationen"""
return monitor_if_enabled(f"session_{operation_name}", capture_stack=False)

413
utils/proxy_rotator.py Normale Datei
Datei anzeigen

@ -0,0 +1,413 @@
# Path: p:/Chimaira/Code-Playwright/utils/proxy_rotator.py
"""
Proxy-Rotations- und Verwaltungsfunktionalität.
"""
import os
import json
import random
import logging
import requests
import time
from typing import Dict, List, Any, Optional, Tuple, Union
logger = logging.getLogger("proxy_rotator")
class ProxyRotator:
"""Klasse zur Verwaltung und Rotation von Proxies."""
CONFIG_FILE = os.path.join("config", "proxies.json")
def __init__(self):
"""Initialisiert den ProxyRotator und lädt die Konfiguration."""
self.config = self.load_config()
self.current_proxy = None
self.last_rotation_time = 0
# Stelle sicher, dass das Konfigurationsverzeichnis existiert
os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True)
def load_config(self) -> Dict[str, Any]:
"""Lädt die Proxy-Konfiguration aus der Konfigurationsdatei."""
if not os.path.exists(self.CONFIG_FILE):
return {
"ipv4": [],
"ipv6": [],
"mobile": [],
"mobile_api": {
"marsproxies": "",
"iproyal": ""
},
"rotation_interval": 300 # 5 Minuten
}
try:
with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
config = json.load(f)
logger.info(f"Proxy-Konfiguration geladen: {len(config.get('ipv4', []))} IPv4, "
f"{len(config.get('ipv6', []))} IPv6, {len(config.get('mobile', []))} Mobile")
return config
except Exception as e:
logger.error(f"Fehler beim Laden der Proxy-Konfiguration: {e}")
return {
"ipv4": [],
"ipv6": [],
"mobile": [],
"mobile_api": {
"marsproxies": "",
"iproyal": ""
},
"rotation_interval": 300
}
def save_config(self) -> bool:
"""Speichert die Proxy-Konfiguration in die Konfigurationsdatei."""
try:
with open(self.CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(self.config, f, indent=2)
logger.info("Proxy-Konfiguration gespeichert")
return True
except Exception as e:
logger.error(f"Fehler beim Speichern der Proxy-Konfiguration: {e}")
return False
def get_config(self) -> Dict[str, Any]:
"""Gibt die aktuelle Konfiguration zurück."""
return self.config
def update_config(self, new_config: Dict[str, Any]) -> bool:
"""Aktualisiert die Konfiguration mit den neuen Werten."""
try:
# Aktualisiere nur die bereitgestellten Schlüssel
for key, value in new_config.items():
self.config[key] = value
# Konfiguration speichern
return self.save_config()
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Proxy-Konfiguration: {e}")
return False
def get_proxies_by_type(self, proxy_type: str) -> List[str]:
"""Gibt eine Liste von Proxies des angegebenen Typs zurück."""
if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]:
logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}")
return []
return self.config.get(proxy_type.lower(), [])
def get_random_proxy(self, proxy_type: str) -> Optional[str]:
"""Gibt einen zufälligen Proxy des angegebenen Typs zurück."""
proxies = self.get_proxies_by_type(proxy_type)
if not proxies:
logger.warning(f"Keine Proxies vom Typ '{proxy_type}' verfügbar")
return None
return random.choice(proxies)
def get_proxy(self, proxy_type=None):
"""
Gibt eine Proxy-Konfiguration für den angegebenen Typ zurück.
Args:
proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ
Returns:
Dict mit Proxy-Konfiguration oder None, wenn kein Proxy verfügbar ist
"""
try:
# Wenn kein Proxy-Typ angegeben ist, einen zufälligen verwenden
if proxy_type is None:
available_types = []
if self.config.get("ipv4"):
available_types.append("ipv4")
if self.config.get("ipv6"):
available_types.append("ipv6")
if self.config.get("mobile"):
available_types.append("mobile")
if not available_types:
logger.warning("Keine Proxies verfügbar")
return None
proxy_type = random.choice(available_types)
# Proxy vom angegebenen Typ holen
proxy_list = self.get_proxies_by_type(proxy_type)
if not proxy_list:
logger.warning(f"Keine Proxies vom Typ '{proxy_type}' verfügbar")
return None
# Zufälligen Proxy aus der Liste auswählen
proxy = random.choice(proxy_list)
# Proxy-URL parsen
parts = proxy.split(":")
if len(parts) >= 4:
# Format: host:port:username:password
host, port, username, password = parts[:4]
return {
"server": f"http://{host}:{port}",
"username": username,
"password": password
}
elif len(parts) >= 2:
# Format: host:port
host, port = parts[:2]
return {
"server": f"http://{host}:{port}"
}
else:
logger.warning(f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}")
return None
except Exception as e:
logger.error(f"Fehler beim Abrufen des Proxys: {e}")
return None
def get_next_proxy(self, proxy_type: str, force_rotation: bool = False) -> Optional[str]:
"""
Gibt den nächsten zu verwendenden Proxy zurück, unter Berücksichtigung des Rotationsintervalls.
Args:
proxy_type: Typ des Proxys (ipv4, ipv6, mobile)
force_rotation: Erzwingt eine Rotation, unabhängig vom Zeitintervall
Returns:
Proxy-String oder None, wenn kein Proxy verfügbar ist
"""
current_time = time.time()
interval = self.config.get("rotation_interval", 300) # Standardintervall: 5 Minuten
# Rotation durchführen, wenn das Intervall abgelaufen ist oder erzwungen wird
if force_rotation or self.current_proxy is None or (current_time - self.last_rotation_time) > interval:
self.current_proxy = self.get_random_proxy(proxy_type)
self.last_rotation_time = current_time
if self.current_proxy:
logger.info(f"Proxy rotiert zu: {self.mask_proxy_credentials(self.current_proxy)}")
return self.current_proxy
def test_proxy(self, proxy_type: str) -> Dict[str, Any]:
"""
Testet einen Proxy des angegebenen Typs.
Args:
proxy_type: Typ des zu testenden Proxys
Returns:
Dictionary mit Testergebnissen
"""
proxy = self.get_random_proxy(proxy_type)
if not proxy:
return {
"success": False,
"error": f"Keine Proxies vom Typ '{proxy_type}' verfügbar"
}
try:
# Proxy-URL parsen
parts = proxy.split(":")
if len(parts) >= 4:
# Format: host:port:username:password
host, port, username, password = parts[:4]
proxy_url = f"http://{username}:{password}@{host}:{port}"
elif len(parts) >= 2:
# Format: host:port
host, port = parts[:2]
proxy_url = f"http://{host}:{port}"
else:
return {
"success": False,
"error": f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}"
}
# Proxy-Konfiguration für requests
proxies = {
"http": proxy_url,
"https": proxy_url
}
# Startzeit für Antwortzeit-Messung
start_time = time.time()
# Test-Anfrage über den Proxy
response = requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=10)
# Antwortzeit berechnen
response_time = time.time() - start_time
if response.status_code == 200:
data = response.json()
ip = data.get("ip", "Unbekannt")
# Länderinformationen abrufen (optional)
country = self.get_country_for_ip(ip)
return {
"success": True,
"ip": ip,
"country": country,
"response_time": response_time,
"proxy_type": proxy_type
}
else:
return {
"success": False,
"error": f"Ungültige Antwort: HTTP {response.status_code}"
}
except requests.exceptions.Timeout:
return {
"success": False,
"error": "Zeitüberschreitung bei der Verbindung"
}
except requests.exceptions.ProxyError:
return {
"success": False,
"error": "Proxy-Fehler: Verbindung abgelehnt oder fehlgeschlagen"
}
except Exception as e:
logger.error(f"Fehler beim Testen des Proxys: {e}")
return {
"success": False,
"error": str(e)
}
def get_country_for_ip(self, ip: str) -> Optional[str]:
"""
Ermittelt das Land für eine IP-Adresse.
Args:
ip: IP-Adresse
Returns:
Ländername oder None im Fehlerfall
"""
try:
response = requests.get(f"https://ipapi.co/{ip}/json/", timeout=5)
if response.status_code == 200:
data = response.json()
return data.get("country_name")
return None
except Exception:
return None
def mask_proxy_credentials(self, proxy: str) -> str:
"""
Maskiert die Anmeldeinformationen in einem Proxy-String für die Protokollierung.
Args:
proxy: Original-Proxy-String
Returns:
Maskierter Proxy-String
"""
parts = proxy.split(":")
if len(parts) >= 4:
# Format: host:port:username:password
host, port = parts[0], parts[1]
return f"{host}:{port}:***:***"
return proxy
def add_proxy(self, proxy: str, proxy_type: str) -> bool:
"""
Fügt einen neuen Proxy zur Konfiguration hinzu.
Args:
proxy: Proxy-String im Format host:port:username:password
proxy_type: Typ des Proxys (ipv4, ipv6, mobile)
Returns:
True bei Erfolg, False bei Fehler
"""
if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]:
logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}")
return False
proxy_list = self.config.get(proxy_type.lower(), [])
if proxy not in proxy_list:
proxy_list.append(proxy)
self.config[proxy_type.lower()] = proxy_list
self.save_config()
logger.info(f"Proxy hinzugefügt: {self.mask_proxy_credentials(proxy)} (Typ: {proxy_type})")
return True
return False
def remove_proxy(self, proxy: str, proxy_type: str) -> bool:
"""
Entfernt einen Proxy aus der Konfiguration.
Args:
proxy: Proxy-String
proxy_type: Typ des Proxys (ipv4, ipv6, mobile)
Returns:
True bei Erfolg, False bei Fehler
"""
if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]:
logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}")
return False
proxy_list = self.config.get(proxy_type.lower(), [])
if proxy in proxy_list:
proxy_list.remove(proxy)
self.config[proxy_type.lower()] = proxy_list
self.save_config()
logger.info(f"Proxy entfernt: {self.mask_proxy_credentials(proxy)} (Typ: {proxy_type})")
return True
return False
def format_proxy_for_playwright(self, proxy: str) -> Dict[str, str]:
"""
Formatiert einen Proxy-String für die Verwendung mit Playwright.
Args:
proxy: Proxy-String im Format host:port:username:password
Returns:
Dictionary mit Playwright-Proxy-Konfiguration
"""
parts = proxy.split(":")
if len(parts) >= 4:
# Format: host:port:username:password
host, port, username, password = parts[:4]
return {
"server": f"{host}:{port}",
"username": username,
"password": password
}
elif len(parts) >= 2:
# Format: host:port
host, port = parts[:2]
return {
"server": f"{host}:{port}"
}
else:
logger.warning(f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}")
return {}

292
utils/result_decorators.py Normale Datei
Datei anzeigen

@ -0,0 +1,292 @@
"""
Result Enhancement Decorators - Backward-compatible result standardization
Erweitert bestehende Methoden ohne sie zu ändern
"""
import functools
import logging
import time
import threading
from typing import Any, Callable, Union
from domain.value_objects.operation_result import OperationResult, CommonErrorCodes
logger = logging.getLogger(__name__)
def result_enhanced(preserve_original: bool = True,
error_code_mapping: dict = None,
capture_metadata: bool = True):
"""
Decorator der bestehende Methoden erweitert ohne sie zu ändern.
Args:
preserve_original: Ob die Original-Methode verfügbar bleiben soll
error_code_mapping: Mapping von Exception-Types zu Error-Codes
capture_metadata: Ob Metadaten (Timing, Thread-Info) erfasst werden sollen
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> OperationResult:
start_time = time.time() if capture_metadata else None
metadata = {}
if capture_metadata:
metadata.update({
'function_name': func.__name__,
'thread_id': threading.current_thread().ident,
'thread_name': threading.current_thread().name
})
try:
# Original-Methode aufrufen
result = func(*args, **kwargs)
if capture_metadata:
metadata['execution_time'] = time.time() - start_time
# Boolean results zu OperationResult erweitern
if isinstance(result, bool):
return OperationResult.from_legacy_boolean(
result=result,
success_data=result if result else None,
error_message="Operation returned False" if not result else None
)
# Dict results erweitern
elif isinstance(result, dict) and 'success' in result:
op_result = OperationResult.from_legacy_dict(result)
if capture_metadata:
op_result.metadata.update(metadata)
return op_result
# None als Fehler behandeln
elif result is None:
return OperationResult.error_result(
message="Method returned None",
code=CommonErrorCodes.BROWSER_ERROR,
metadata=metadata
)
# Alle anderen Results als Erfolg
else:
return OperationResult.success_result(
data=result,
metadata=metadata,
legacy_result=result
)
except Exception as e:
if capture_metadata:
metadata.update({
'execution_time': time.time() - start_time,
'exception_occurred_at': time.time()
})
# Error code mapping anwenden
error_code = None
if error_code_mapping:
error_code = error_code_mapping.get(type(e), type(e).__name__)
return OperationResult.from_exception(
exception=e,
code=error_code,
metadata=metadata
)
# Original-Methode verfügbar machen wenn gewünscht
if preserve_original:
wrapper.original = func
wrapper.get_original = lambda: func
# Zusätzliche Utility-Methoden
wrapper.call_original = lambda *args, **kwargs: func(*args, **kwargs)
wrapper.is_enhanced = True
return wrapper
return decorator
def instagram_result_enhanced(func: Callable) -> Callable:
"""
Spezialisierter Decorator für Instagram-Operationen
"""
instagram_error_mapping = {
TimeoutError: CommonErrorCodes.NETWORK_TIMEOUT,
ConnectionError: CommonErrorCodes.PROXY_ERROR,
ValueError: CommonErrorCodes.SELECTOR_NOT_FOUND,
RuntimeError: CommonErrorCodes.BROWSER_ERROR,
Exception: CommonErrorCodes.BROWSER_ERROR
}
return result_enhanced(
preserve_original=True,
error_code_mapping=instagram_error_mapping,
capture_metadata=True
)(func)
def fingerprint_result_enhanced(func: Callable) -> Callable:
"""
Spezialisierter Decorator für Fingerprint-Operationen
"""
fingerprint_error_mapping = {
FileNotFoundError: CommonErrorCodes.FINGERPRINT_NOT_FOUND,
PermissionError: CommonErrorCodes.FINGERPRINT_GENERATION_FAILED,
ValueError: CommonErrorCodes.FINGERPRINT_GENERATION_FAILED,
RuntimeError: CommonErrorCodes.FINGERPRINT_RACE_CONDITION,
Exception: CommonErrorCodes.FINGERPRINT_GENERATION_FAILED
}
return result_enhanced(
preserve_original=True,
error_code_mapping=fingerprint_error_mapping,
capture_metadata=True
)(func)
def session_result_enhanced(func: Callable) -> Callable:
"""
Spezialisierter Decorator für Session-Operationen
"""
session_error_mapping = {
TimeoutError: CommonErrorCodes.SESSION_EXPIRED,
ValueError: CommonErrorCodes.SESSION_INVALID,
IOError: CommonErrorCodes.SESSION_SAVE_FAILED,
Exception: CommonErrorCodes.SESSION_INVALID
}
return result_enhanced(
preserve_original=True,
error_code_mapping=session_error_mapping,
capture_metadata=True
)(func)
class ResultEnhancer:
"""
Utility-Klasse für programmatische Result-Enhancement ohne Decorators
"""
@staticmethod
def enhance_method(obj: Any, method_name: str, enhancement_type: str = "general") -> None:
"""
Erweitert eine bestehende Methode eines Objekts zur Laufzeit
Args:
obj: Das Objekt dessen Methode erweitert werden soll
method_name: Name der zu erweiternden Methode
enhancement_type: Art der Erweiterung ("general", "instagram", "fingerprint", "session")
"""
if not hasattr(obj, method_name):
logger.warning(f"Method {method_name} not found on object {obj}")
return
original_method = getattr(obj, method_name)
# Bereits erweiterte Methoden überspringen
if getattr(original_method, 'is_enhanced', False):
logger.debug(f"Method {method_name} already enhanced")
return
# Entsprechenden Decorator wählen
if enhancement_type == "instagram":
enhanced_method = instagram_result_enhanced(original_method)
elif enhancement_type == "fingerprint":
enhanced_method = fingerprint_result_enhanced(original_method)
elif enhancement_type == "session":
enhanced_method = session_result_enhanced(original_method)
else:
enhanced_method = result_enhanced()(original_method)
# Methode ersetzen
setattr(obj, method_name, enhanced_method)
# Original unter anderem Namen verfügbar machen
setattr(obj, f"{method_name}_original", original_method)
logger.info(f"Enhanced method {method_name} on {type(obj).__name__}")
@staticmethod
def enhance_class_methods(cls: type, method_names: list, enhancement_type: str = "general") -> None:
"""
Erweitert mehrere Methoden einer Klasse
"""
for method_name in method_names:
if hasattr(cls, method_name):
original_method = getattr(cls, method_name)
if enhancement_type == "instagram":
enhanced_method = instagram_result_enhanced(original_method)
elif enhancement_type == "fingerprint":
enhanced_method = fingerprint_result_enhanced(original_method)
elif enhancement_type == "session":
enhanced_method = session_result_enhanced(original_method)
else:
enhanced_method = result_enhanced()(original_method)
setattr(cls, method_name, enhanced_method)
setattr(cls, f"{method_name}_original", original_method)
logger.info(f"Enhanced class method {cls.__name__}.{method_name}")
class BatchResultWrapper:
"""
Wrapper für Batch-Operationen mit einheitlicher Result-Struktur
"""
def __init__(self, operation_name: str = "batch_operation"):
self.operation_name = operation_name
self.results = []
self.success_count = 0
self.error_count = 0
def add_result(self, result: Union[OperationResult, bool, dict, Any]) -> None:
"""Fügt ein Result zur Batch hinzu"""
if isinstance(result, OperationResult):
op_result = result
elif isinstance(result, bool):
op_result = OperationResult.from_legacy_boolean(result)
elif isinstance(result, dict) and 'success' in result:
op_result = OperationResult.from_legacy_dict(result)
else:
op_result = OperationResult.success_result(data=result)
self.results.append(op_result)
if op_result.success:
self.success_count += 1
else:
self.error_count += 1
def get_batch_result(self) -> OperationResult:
"""Gibt das Gesamtergebnis der Batch zurück"""
total_count = len(self.results)
success_rate = self.success_count / total_count if total_count > 0 else 0
metadata = {
'total_operations': total_count,
'successful_operations': self.success_count,
'failed_operations': self.error_count,
'success_rate': success_rate,
'operation_name': self.operation_name
}
# Batch als erfolgreich bewerten wenn > 50% erfolgreich
batch_success = success_rate > 0.5
if batch_success:
return OperationResult.success_result(
data={
'results': [r.to_dict() for r in self.results],
'summary': metadata
},
metadata=metadata
)
else:
error_messages = [r.error_message for r in self.results if not r.success]
return OperationResult.error_result(
message=f"Batch operation failed: {'; '.join(error_messages[:3])}...",
code="BATCH_OPERATION_FAILED",
metadata=metadata
)

558
utils/text_similarity.py Normale Datei
Datei anzeigen

@ -0,0 +1,558 @@
"""
Textähnlichkeits-Funktionen für robustes UI-Element-Matching.
Ermöglicht flexibles Auffinden von UI-Elementen auch bei leichten Textänderungen.
"""
import re
import logging
from typing import List, Dict, Any, Optional, Tuple, Union, Callable
from difflib import SequenceMatcher
logger = logging.getLogger("text_similarity")
class TextSimilarity:
"""Klasse für Textähnlichkeitsfunktionen zum robusten UI-Element-Matching."""
def __init__(self, default_threshold: float = 0.8):
"""
Initialisiert die TextSimilarity-Klasse.
Args:
default_threshold: Standardschwellenwert für Ähnlichkeitsprüfungen (0-1)
"""
self.default_threshold = max(0.0, min(1.0, default_threshold))
def levenshtein_distance(self, s1: str, s2: str) -> int:
"""
Berechnet die Levenshtein-Distanz zwischen zwei Strings.
Args:
s1: Erster String
s2: Zweiter String
Returns:
Die Levenshtein-Distanz (kleinere Werte = ähnlichere Strings)
"""
if s1 == s2:
return 0
# Strings für die Berechnung vorbereiten
s1 = s1.lower().strip()
s2 = s2.lower().strip()
# Spezialfall: leere Strings
if len(s1) == 0: return len(s2)
if len(s2) == 0: return len(s1)
# Initialisiere die Distanzmatrix
matrix = [[0 for x in range(len(s2) + 1)] for x in range(len(s1) + 1)]
# Fülle die erste Zeile und Spalte
for i in range(len(s1) + 1):
matrix[i][0] = i
for j in range(len(s2) + 1):
matrix[0][j] = j
# Fülle die Matrix
for i in range(1, len(s1) + 1):
for j in range(1, len(s2) + 1):
cost = 0 if s1[i-1] == s2[j-1] else 1
matrix[i][j] = min(
matrix[i-1][j] + 1, # Löschen
matrix[i][j-1] + 1, # Einfügen
matrix[i-1][j-1] + cost # Ersetzen
)
return matrix[len(s1)][len(s2)]
def similarity_ratio(self, s1: str, s2: str) -> float:
"""
Berechnet das Ähnlichkeitsverhältnis zwischen zwei Strings (0-1).
Args:
s1: Erster String
s2: Zweiter String
Returns:
Ähnlichkeitsverhältnis zwischen 0 (unähnlich) und 1 (identisch)
"""
# Strings für Vergleich normalisieren
s1 = s1.lower().strip()
s2 = s2.lower().strip()
# Leere Strings behandeln
if len(s1) == 0 and len(s2) == 0:
return 1.0
# Maximale mögliche Distanz = Summe der Längen beider Strings
max_distance = max(len(s1), len(s2))
if max_distance == 0:
return 1.0
# Levenshtein-Distanz berechnen
distance = self.levenshtein_distance(s1, s2)
# Ähnlichkeitsverhältnis berechnen
similarity = 1.0 - (distance / max_distance)
return similarity
def sequence_matcher_ratio(self, s1: str, s2: str) -> float:
"""
Berechnet das Ähnlichkeitsverhältnis mit Pythons SequenceMatcher.
Oft genauer als einfaches Levenshtein für längere Texte.
Args:
s1: Erster String
s2: Zweiter String
Returns:
Ähnlichkeitsverhältnis zwischen 0 (unähnlich) und 1 (identisch)
"""
# Strings für Vergleich normalisieren
s1 = s1.lower().strip()
s2 = s2.lower().strip()
# SequenceMatcher verwenden
return SequenceMatcher(None, s1, s2).ratio()
def jaro_winkler_similarity(self, s1: str, s2: str) -> float:
"""
Berechnet die Jaro-Winkler-Ähnlichkeit, die Präfixübereinstimmungen berücksichtigt.
Gut für kurze Strings wie Namen oder IDs.
Args:
s1: Erster String
s2: Zweiter String
Returns:
Ähnlichkeitswert zwischen 0 (unähnlich) und 1 (identisch)
"""
# Strings für Vergleich normalisieren
s1 = s1.lower().strip()
s2 = s2.lower().strip()
# Identische Strings
if s1 == s2:
return 1.0
# Leere Strings behandeln
if len(s1) == 0 or len(s2) == 0:
return 0.0
# Berechne die Jaro-Ähnlichkeit
# Suche nach übereinstimmenden Zeichen innerhalb des Suchradius
search_range = max(len(s1), len(s2)) // 2 - 1
search_range = max(0, search_range)
# Initialisiere Übereinstimmungs- und Transpositionszähler
matches = 0
transpositions = 0
# Markiere übereinstimmende Zeichen
s1_matches = [False] * len(s1)
s2_matches = [False] * len(s2)
# Finde Übereinstimmungen
for i in range(len(s1)):
start = max(0, i - search_range)
end = min(i + search_range + 1, len(s2))
for j in range(start, end):
if not s2_matches[j] and s1[i] == s2[j]:
s1_matches[i] = True
s2_matches[j] = True
matches += 1
break
# Wenn keine Übereinstimmungen gefunden wurden
if matches == 0:
return 0.0
# Zähle Transpositionszeichen
k = 0
for i in range(len(s1)):
if s1_matches[i]:
while not s2_matches[k]:
k += 1
if s1[i] != s2[k]:
transpositions += 1
k += 1
# Berechne Jaro-Ähnlichkeit
jaro = (
matches / len(s1) +
matches / len(s2) +
(matches - transpositions // 2) / matches
) / 3.0
# Berechne Jaro-Winkler-Ähnlichkeit mit Präfixbonus
prefix_len = 0
max_prefix_len = min(4, min(len(s1), len(s2)))
# Zähle übereinstimmende Präfixzeichen
while prefix_len < max_prefix_len and s1[prefix_len] == s2[prefix_len]:
prefix_len += 1
# Skalierungsfaktor für Präfixanpassung (Standard: 0.1)
scaling_factor = 0.1
# Berechne Jaro-Winkler-Ähnlichkeit
jaro_winkler = jaro + prefix_len * scaling_factor * (1 - jaro)
return jaro_winkler
def is_similar(self, s1: str, s2: str, threshold: float = None, method: str = "sequence") -> bool:
"""
Prüft, ob zwei Strings ähnlich genug sind, basierend auf einem Schwellenwert.
Args:
s1: Erster String
s2: Zweiter String
threshold: Ähnlichkeitsschwellenwert (0-1), oder None für Standardwert
method: Ähnlichkeitsmethode ("levenshtein", "sequence", "jaro_winkler")
Returns:
True, wenn die Strings ähnlich genug sind, False sonst
"""
if threshold is None:
threshold = self.default_threshold
# Leere oder None-Strings behandeln
s1 = "" if s1 is None else str(s1)
s2 = "" if s2 is None else str(s2)
# Wenn beide Strings identisch sind
if s1 == s2:
return True
# Ähnlichkeitsmethode auswählen
if method == "levenshtein":
similarity = self.similarity_ratio(s1, s2)
elif method == "jaro_winkler":
similarity = self.jaro_winkler_similarity(s1, s2)
else: # "sequence" oder andere
similarity = self.sequence_matcher_ratio(s1, s2)
return similarity >= threshold
def find_most_similar(self, target: str, candidates: List[str],
method: str = "sequence") -> Tuple[str, float]:
"""
Findet den ähnlichsten String in einer Liste von Kandidaten.
Args:
target: Zieltext, zu dem der ähnlichste String gefunden werden soll
candidates: Liste von Kandidatenstrings
method: Ähnlichkeitsmethode ("levenshtein", "sequence", "jaro_winkler")
Returns:
Tuple (ähnlichster String, Ähnlichkeitswert)
"""
if not candidates:
return "", 0.0
# Ähnlichkeitsfunktion auswählen
if method == "levenshtein":
similarity_func = self.similarity_ratio
elif method == "jaro_winkler":
similarity_func = self.jaro_winkler_similarity
else: # "sequence" oder andere
similarity_func = self.sequence_matcher_ratio
# Finde den ähnlichsten Kandidaten
similarities = [(candidate, similarity_func(target, candidate)) for candidate in candidates]
most_similar = max(similarities, key=lambda x: x[1])
return most_similar
def get_similarity_scores(self, target: str, candidates: List[str],
method: str = "sequence") -> Dict[str, float]:
"""
Berechnet Ähnlichkeitswerte für alle Kandidaten.
Args:
target: Zieltext
candidates: Liste von Kandidatenstrings
method: Ähnlichkeitsmethode
Returns:
Dictionary mit {Kandidat: Ähnlichkeitswert}
"""
# Ähnlichkeitsfunktion auswählen
if method == "levenshtein":
similarity_func = self.similarity_ratio
elif method == "jaro_winkler":
similarity_func = self.jaro_winkler_similarity
else: # "sequence" oder andere
similarity_func = self.sequence_matcher_ratio
# Berechne Ähnlichkeiten für alle Kandidaten
return {candidate: similarity_func(target, candidate) for candidate in candidates}
def words_similarity(self, s1: str, s2: str) -> float:
"""
Berechnet die Ähnlichkeit basierend auf gemeinsamen Wörtern.
Args:
s1: Erster String
s2: Zweiter String
Returns:
Ähnlichkeitswert zwischen 0 (unähnlich) und 1 (identisch)
"""
# Strings in Wörter zerlegen
words1 = set(re.findall(r'\w+', s1.lower()))
words2 = set(re.findall(r'\w+', s2.lower()))
# Leere Wortmengen behandeln
if not words1 and not words2:
return 1.0
if not words1 or not words2:
return 0.0
# Berechne Jaccard-Ähnlichkeit
intersection = len(words1.intersection(words2))
union = len(words1.union(words2))
return intersection / union
def contains_similar_text(self, text: str, patterns: List[str],
threshold: float = None, method: str = "sequence") -> bool:
"""
Prüft, ob ein Text einen der Muster ähnlich enthält.
Args:
text: Zu durchsuchender Text
patterns: Liste von zu suchenden Mustern
threshold: Ähnlichkeitsschwellenwert
method: Ähnlichkeitsmethode
Returns:
True, wenn mindestens ein Muster ähnlich genug ist
"""
if threshold is None:
threshold = self.default_threshold
# Wenn patterns leer ist oder Text None ist
if not patterns or text is None:
return False
text = str(text).lower()
for pattern in patterns:
pattern = str(pattern).lower()
# Prüfe, ob der Text das Muster enthält
if pattern in text:
return True
# Prüfe Ähnlichkeit mit Wörtern im Text
words = re.findall(r'\w+', text)
for word in words:
if self.is_similar(word, pattern, threshold, method):
return True
return False
def fuzzy_find_element(page, text_or_patterns, selector_type="button", threshold=0.8,
method="sequence", wait_time=5000) -> Optional[Any]:
"""
Findet ein Element basierend auf Textähnlichkeit.
Args:
page: Playwright Page-Objekt
text_or_patterns: Zieltext oder Liste von Texten
selector_type: Art des Elements ("button", "link", "input", "any")
threshold: Ähnlichkeitsschwellenwert
method: Ähnlichkeitsmethode
wait_time: Wartezeit in Millisekunden
Returns:
Gefundenes Element oder None
"""
similarity = TextSimilarity(threshold)
patterns = [text_or_patterns] if isinstance(text_or_patterns, str) else text_or_patterns
try:
# Warte, bis die Seite geladen ist
page.wait_for_load_state("domcontentloaded", timeout=wait_time)
# Selektoren basierend auf dem Element-Typ
if selector_type == "button":
elements = page.query_selector_all("button, input[type='button'], input[type='submit'], [role='button']")
elif selector_type == "link":
elements = page.query_selector_all("a, [role='link']")
elif selector_type == "input":
elements = page.query_selector_all("input, textarea, select")
else: # "any"
elements = page.query_selector_all("*")
# Keine Elemente gefunden
if not elements:
logger.debug(f"Keine {selector_type}-Elemente auf der Seite gefunden")
return None
# Für jedes Element den Text und die Ähnlichkeit prüfen
best_match = None
best_similarity = -1
for element in elements:
# Text aus verschiedenen Attributen extrahieren
element_text = ""
# Inneren Text prüfen
inner_text = element.inner_text()
if inner_text and inner_text.strip():
element_text = inner_text.strip()
# Value-Attribut prüfen (für Eingabefelder)
if not element_text:
try:
value = element.get_attribute("value")
if value and value.strip():
element_text = value.strip()
except:
pass
# Placeholder prüfen
if not element_text:
try:
placeholder = element.get_attribute("placeholder")
if placeholder and placeholder.strip():
element_text = placeholder.strip()
except:
pass
# Aria-Label prüfen
if not element_text:
try:
aria_label = element.get_attribute("aria-label")
if aria_label and aria_label.strip():
element_text = aria_label.strip()
except:
pass
# Title-Attribut prüfen
if not element_text:
try:
title = element.get_attribute("title")
if title and title.strip():
element_text = title.strip()
except:
pass
# Wenn immer noch kein Text gefunden wurde, überspringen
if not element_text:
continue
# Ähnlichkeit für jeden Pattern prüfen
for pattern in patterns:
sim_score = similarity.sequence_matcher_ratio(pattern, element_text)
# Ist dieser Match besser als der bisherige beste?
if sim_score > best_similarity and sim_score >= threshold:
best_similarity = sim_score
best_match = element
# Bei perfekter Übereinstimmung sofort zurückgeben
if sim_score >= 0.99:
logger.info(f"Element mit perfekter Übereinstimmung gefunden: '{element_text}'")
return element
# Bestes Ergebnis zurückgeben, wenn es über dem Schwellenwert liegt
if best_match:
try:
match_text = best_match.inner_text() or best_match.get_attribute("value") or best_match.get_attribute("placeholder")
logger.info(f"Element mit Ähnlichkeit {best_similarity:.2f} gefunden: '{match_text}'")
except:
logger.info(f"Element mit Ähnlichkeit {best_similarity:.2f} gefunden")
return best_match
logger.debug(f"Kein passendes Element für die angegebenen Muster gefunden: {patterns}")
return None
except Exception as e:
logger.error(f"Fehler beim Suchen nach ähnlichem Element: {e}")
return None
def find_element_by_text(page, text, exact=False, selector="*", timeout=5000) -> Optional[Any]:
"""
Findet ein Element, das den angegebenen Text enthält oder ihm ähnlich ist.
Args:
page: Playwright Page-Objekt
text: Zu suchender Text
exact: Ob exakte Übereinstimmung erforderlich ist
selector: CSS-Selektor zum Einschränken der Suche
timeout: Timeout in Millisekunden
Returns:
Gefundenes Element oder None
"""
try:
if exact:
# Bei exakter Suche XPath verwenden
xpath = f"//{selector}[contains(text(), '{text}') or contains(@value, '{text}') or contains(@placeholder, '{text}')]"
return page.wait_for_selector(xpath, timeout=timeout)
else:
# Bei Ähnlichkeitssuche alle passenden Elemente finden
similarity = TextSimilarity(0.8) # 80% Schwellenwert
# Warten auf DOM-Bereitschaft
page.wait_for_load_state("domcontentloaded", timeout=timeout)
# Alle Elemente mit dem angegebenen Selektor finden
elements = page.query_selector_all(selector)
for element in elements:
# Verschiedene Textattribute prüfen
element_text = element.inner_text()
if not element_text:
element_text = element.get_attribute("value") or ""
if not element_text:
element_text = element.get_attribute("placeholder") or ""
# Ähnlichkeit prüfen
if similarity.is_similar(text, element_text):
return element
return None
except Exception as e:
logger.error(f"Fehler beim Suchen nach Element mit Text '{text}': {e}")
return None
def click_fuzzy_button(page, button_text, threshold=0.7, timeout=5000) -> bool:
"""
Klickt auf einen Button basierend auf Textähnlichkeit.
Args:
page: Playwright Page-Objekt
button_text: Text oder Textmuster des Buttons
threshold: Ähnlichkeitsschwellenwert
timeout: Timeout in Millisekunden
Returns:
True bei Erfolg, False bei Fehler
"""
try:
# Versuche, das Element zu finden
button = fuzzy_find_element(page, button_text, selector_type="button",
threshold=threshold, wait_time=timeout)
if button:
# Scrolle zum Button und klicke
button.scroll_into_view_if_needed()
button.click()
logger.info(f"Auf Button mit Text ähnlich zu '{button_text}' geklickt")
return True
else:
logger.warning(f"Kein Button mit Text ähnlich zu '{button_text}' gefunden")
return False
except Exception as e:
logger.error(f"Fehler beim Klicken auf Button mit Text '{button_text}': {e}")
return False

133
utils/theme_manager.py Normale Datei
Datei anzeigen

@ -0,0 +1,133 @@
"""
Theme Manager - Verwaltet das Erscheinungsbild der Anwendung (nur Light Mode)
"""
import os
import json
import logging
from typing import Dict, Any, Optional
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtCore import Qt, QSettings
logger = logging.getLogger("theme_manager")
class ThemeManager:
"""
Verwaltet das Erscheinungsbild der Anwendung.
"""
# Themennamen
LIGHT_THEME = "light"
def __init__(self, app: QApplication):
"""
Initialisiert den ThemeManager.
Args:
app: Die QApplication-Instanz
"""
self.app = app
self.settings = QSettings("Chimaira", "SocialMediaAccountGenerator")
self.current_theme = self.LIGHT_THEME
# Basisverzeichnis ermitteln
self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Stelle sicher, dass die Verzeichnisse existieren
os.makedirs(os.path.join(self.base_dir, "resources", "themes"), exist_ok=True)
os.makedirs(os.path.join(self.base_dir, "resources", "icons"), exist_ok=True)
# Lade QSS-Dateien für Themes
self.theme_stylesheets = {
self.LIGHT_THEME: self._load_stylesheet("light.qss")
}
# Wende das Light Theme an
self.apply_theme(self.LIGHT_THEME)
logger.info(f"ThemeManager initialisiert mit Theme: {self.current_theme}")
def _load_stylesheet(self, filename: str) -> str:
"""Lädt ein QSS-Stylesheet aus einer Datei."""
try:
stylesheet_path = os.path.join(self.base_dir, "resources", "themes", filename)
if os.path.exists(stylesheet_path):
with open(stylesheet_path, 'r', encoding='utf-8') as f:
return f.read()
else:
logger.warning(f"Stylesheet-Datei nicht gefunden: {stylesheet_path}")
# Erzeuge eine leere Stylesheet-Datei, wenn sie nicht existiert
with open(stylesheet_path, 'w', encoding='utf-8') as f:
f.write("/* Auto-generated empty stylesheet */\n")
return ""
except Exception as e:
logger.error(f"Fehler beim Laden des Stylesheets {filename}: {e}")
return ""
def apply_theme(self, theme_name: str) -> bool:
"""
Wendet das Light Theme auf die Anwendung an.
Args:
theme_name: Wird ignoriert, immer Light Theme verwendet
Returns:
bool: True, wenn das Theme erfolgreich angewendet wurde, sonst False
"""
try:
# Palette für das Light Theme erstellen
palette = QPalette()
# Light Theme Palette
palette.setColor(QPalette.Window, QColor(240, 240, 240))
palette.setColor(QPalette.WindowText, Qt.black)
palette.setColor(QPalette.Base, Qt.white)
palette.setColor(QPalette.AlternateBase, QColor(245, 245, 245))
palette.setColor(QPalette.ToolTipBase, Qt.white)
palette.setColor(QPalette.ToolTipText, Qt.black)
palette.setColor(QPalette.Text, Qt.black)
palette.setColor(QPalette.Button, QColor(240, 240, 240))
palette.setColor(QPalette.ButtonText, Qt.black)
palette.setColor(QPalette.BrightText, Qt.red)
palette.setColor(QPalette.Link, QColor(0, 0, 255))
palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
palette.setColor(QPalette.HighlightedText, Qt.white)
# Palette auf die Anwendung anwenden
self.app.setPalette(palette)
# Stylesheet anwenden
self.app.setStyleSheet(self.theme_stylesheets.get(self.LIGHT_THEME, ""))
# Aktuelles Theme speichern
self.current_theme = self.LIGHT_THEME
self.settings.setValue("theme", self.LIGHT_THEME)
logger.info(f"Theme '{self.LIGHT_THEME}' erfolgreich angewendet")
return True
except Exception as e:
logger.error(f"Fehler beim Anwenden des Themes '{self.LIGHT_THEME}': {e}")
return False
def get_current_theme(self) -> str:
"""Gibt den Namen des aktuellen Themes zurück."""
return self.LIGHT_THEME
def get_icon_path(self, icon_name: str) -> str:
"""
Gibt den Pfad zum Icon zurück.
Args:
icon_name: Name des Icons (ohne Dateierweiterung)
Returns:
str: Pfad zum Icon
"""
# Social Media Icons bleiben unverändert (immer farbig)
if icon_name in ["instagram", "facebook", "twitter", "tiktok", "vk"]:
return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg")
# Für andere Icons, die möglicherweise Theme-spezifisch sind
return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg")

362
utils/thread_safety_mixins.py Normale Datei
Datei anzeigen

@ -0,0 +1,362 @@
"""
Thread Safety Mixins - Non-intrusive thread safety for existing classes
Opt-in thread safety without changing existing logic
"""
import threading
import functools
import time
import weakref
from typing import Any, Dict, Optional, Callable, Set
from collections import defaultdict
import logging
logger = logging.getLogger(__name__)
class ThreadSafetyMixin:
"""
Mixin-Klasse die zu bestehenden Klassen hinzugefügt werden kann
für thread-sichere Operationen ohne Änderung der bestehenden Logik
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._operation_locks: Dict[str, threading.RLock] = {}
self._lock_manager = threading.RLock()
self._active_operations: Dict[str, Set[int]] = defaultdict(set)
self._operation_stats = {
'total_operations': 0,
'concurrent_operations': 0,
'lock_acquisitions': 0,
'lock_contentions': 0
}
self._stats_lock = threading.RLock()
def _thread_safe_operation(self, operation_key: str, operation_func: Callable,
*args, timeout: Optional[float] = None, **kwargs) -> Any:
"""
Wrapper für thread-sichere Operationen
Args:
operation_key: Eindeutiger Schlüssel für die Operation
operation_func: Die auszuführende Funktion
timeout: Optional timeout für Lock-Akquisition
"""
thread_id = threading.current_thread().ident
start_time = time.time()
# Operation-spezifischen Lock holen/erstellen
with self._lock_manager:
if operation_key not in self._operation_locks:
self._operation_locks[operation_key] = threading.RLock()
logger.debug(f"Created lock for operation: {operation_key}")
operation_lock = self._operation_locks[operation_key]
# Prüfen ob bereits aktive Operationen vorhanden
active_count = len(self._active_operations[operation_key])
if active_count > 0:
with self._stats_lock:
self._operation_stats['lock_contentions'] += 1
logger.debug(f"Lock contention detected for {operation_key}: {active_count} active operations")
# Lock akquirieren
lock_acquired = False
try:
if timeout:
lock_acquired = operation_lock.acquire(timeout=timeout)
if not lock_acquired:
raise TimeoutError(f"Failed to acquire lock for {operation_key} within {timeout}s")
else:
operation_lock.acquire()
lock_acquired = True
with self._stats_lock:
self._operation_stats['lock_acquisitions'] += 1
self._operation_stats['total_operations'] += 1
# Thread zu aktiven Operationen hinzufügen
with self._lock_manager:
self._active_operations[operation_key].add(thread_id)
concurrent_ops = len(self._active_operations[operation_key])
if concurrent_ops > 1:
with self._stats_lock:
self._operation_stats['concurrent_operations'] += 1
# Operation ausführen
logger.debug(f"Executing thread-safe operation {operation_key} (thread: {thread_id})")
result = operation_func(*args, **kwargs)
execution_time = time.time() - start_time
logger.debug(f"Completed operation {operation_key} in {execution_time:.3f}s")
return result
finally:
# Thread aus aktiven Operationen entfernen
with self._lock_manager:
self._active_operations[operation_key].discard(thread_id)
# Lock freigeben
if lock_acquired:
operation_lock.release()
def _get_operation_stats(self) -> Dict[str, Any]:
"""Gibt Thread-Safety-Statistiken zurück"""
with self._stats_lock:
stats = self._operation_stats.copy()
with self._lock_manager:
active_ops = {key: len(threads) for key, threads in self._active_operations.items() if threads}
return {
**stats,
'active_operations': active_ops,
'total_locks': len(self._operation_locks),
'current_thread': threading.current_thread().ident
}
def _cleanup_inactive_locks(self) -> int:
"""Bereinigt Locks für inaktive Operationen"""
cleaned_count = 0
with self._lock_manager:
# Locks ohne aktive Operationen identifizieren
inactive_operations = [
key for key, threads in self._active_operations.items()
if not threads and key in self._operation_locks
]
# Bereinigen
for key in inactive_operations:
if key in self._operation_locks:
del self._operation_locks[key]
cleaned_count += 1
if key in self._active_operations:
del self._active_operations[key]
if cleaned_count > 0:
logger.debug(f"Cleaned up {cleaned_count} inactive operation locks")
return cleaned_count
def thread_safe_method(operation_key: Optional[str] = None, timeout: Optional[float] = None):
"""
Decorator für thread-sichere Methoden
Args:
operation_key: Eindeutiger Schlüssel (default: Methodenname)
timeout: Timeout für Lock-Akquisition
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
# Prüfen ob Objekt ThreadSafetyMixin hat
if not hasattr(self, '_thread_safe_operation'):
logger.warning(f"Object {type(self).__name__} does not have ThreadSafetyMixin")
return func(self, *args, **kwargs)
key = operation_key or f"{type(self).__name__}.{func.__name__}"
return self._thread_safe_operation(key, func, self, *args, timeout=timeout, **kwargs)
# Original-Methode verfügbar machen
wrapper.original = func
wrapper.is_thread_safe = True
return wrapper
return decorator
class ResourcePoolMixin:
"""
Mixin für Pool-basierte Resource-Verwaltung (z.B. Browser-Sessions)
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._resource_pool: Dict[str, Any] = {}
self._resource_locks: Dict[str, threading.RLock] = {}
self._resource_usage: Dict[str, Dict[str, Any]] = {}
self._pool_lock = threading.RLock()
def _acquire_resource(self, resource_id: str, timeout: Optional[float] = None) -> bool:
"""
Akquiriert eine Resource aus dem Pool
"""
with self._pool_lock:
if resource_id not in self._resource_locks:
self._resource_locks[resource_id] = threading.RLock()
resource_lock = self._resource_locks[resource_id]
# Resource-Lock akquirieren
if timeout:
acquired = resource_lock.acquire(timeout=timeout)
else:
resource_lock.acquire()
acquired = True
if acquired:
# Usage tracking
thread_id = threading.current_thread().ident
with self._pool_lock:
if resource_id not in self._resource_usage:
self._resource_usage[resource_id] = {
'acquired_by': thread_id,
'acquired_at': time.time(),
'usage_count': 0
}
self._resource_usage[resource_id]['usage_count'] += 1
logger.debug(f"Acquired resource {resource_id} by thread {thread_id}")
return acquired
def _release_resource(self, resource_id: str) -> None:
"""
Gibt eine Resource zurück in den Pool
"""
thread_id = threading.current_thread().ident
with self._pool_lock:
if resource_id in self._resource_locks:
self._resource_locks[resource_id].release()
# Usage tracking aktualisieren
if resource_id in self._resource_usage:
usage_info = self._resource_usage[resource_id]
usage_info['released_at'] = time.time()
usage_duration = usage_info['released_at'] - usage_info['acquired_at']
logger.debug(f"Released resource {resource_id} by thread {thread_id} "
f"(used for {usage_duration:.3f}s)")
def _get_resource_stats(self) -> Dict[str, Any]:
"""Gibt Resource-Pool-Statistiken zurück"""
with self._pool_lock:
return {
'total_resources': len(self._resource_pool),
'active_locks': len(self._resource_locks),
'resource_usage': dict(self._resource_usage),
'available_resources': list(self._resource_pool.keys())
}
class ConcurrencyControlMixin:
"""
Mixin für erweiterte Concurrency-Kontrolle
"""
def __init__(self, max_concurrent_operations: int = 10, *args, **kwargs):
super().__init__(*args, **kwargs)
self.max_concurrent_operations = max_concurrent_operations
self._operation_semaphore = threading.Semaphore(max_concurrent_operations)
self._active_operation_count = 0
self._operation_queue = []
self._concurrency_lock = threading.RLock()
def _controlled_operation(self, operation_func: Callable, *args,
priority: int = 5, **kwargs) -> Any:
"""
Führt Operation mit Concurrency-Kontrolle aus
Args:
operation_func: Auszuführende Funktion
priority: Priorität (1=höchste, 10=niedrigste)
"""
thread_id = threading.current_thread().ident
# Semaphore akquirieren (begrenzt gleichzeitige Operationen)
logger.debug(f"Thread {thread_id} waiting for operation slot (priority: {priority})")
acquired = self._operation_semaphore.acquire(timeout=30) # 30s timeout
if not acquired:
raise TimeoutError("Failed to acquire operation slot within timeout")
try:
with self._concurrency_lock:
self._active_operation_count += 1
current_count = self._active_operation_count
logger.debug(f"Thread {thread_id} executing operation "
f"({current_count}/{self.max_concurrent_operations} slots used)")
# Operation ausführen
result = operation_func(*args, **kwargs)
return result
finally:
with self._concurrency_lock:
self._active_operation_count -= 1
self._operation_semaphore.release()
logger.debug(f"Thread {thread_id} released operation slot")
def _get_concurrency_stats(self) -> Dict[str, Any]:
"""Gibt Concurrency-Statistiken zurück"""
with self._concurrency_lock:
return {
'max_concurrent_operations': self.max_concurrent_operations,
'active_operations': self._active_operation_count,
'available_slots': self.max_concurrent_operations - self._active_operation_count,
'queue_length': len(self._operation_queue)
}
# Kombiniertes Mixin für vollständige Thread-Safety
class FullThreadSafetyMixin(ThreadSafetyMixin, ResourcePoolMixin, ConcurrencyControlMixin):
"""
Vollständiges Thread-Safety-Mixin mit allen Features
"""
def __init__(self, max_concurrent_operations: int = 5, *args, **kwargs):
super().__init__(max_concurrent_operations=max_concurrent_operations, *args, **kwargs)
def get_complete_stats(self) -> Dict[str, Any]:
"""Gibt vollständige Thread-Safety-Statistiken zurück"""
return {
'thread_safety': self._get_operation_stats(),
'resource_pool': self._get_resource_stats(),
'concurrency_control': self._get_concurrency_stats(),
'current_thread': {
'id': threading.current_thread().ident,
'name': threading.current_thread().name,
'is_daemon': threading.current_thread().daemon
}
}
# Utility-Funktionen für bestehende Klassen
def make_thread_safe(cls: type, method_names: list = None,
operation_timeout: Optional[float] = None) -> type:
"""
Macht eine bestehende Klasse thread-safe durch dynamisches Mixin
Args:
cls: Klasse die thread-safe gemacht werden soll
method_names: Liste der Methoden die geschützt werden sollen
operation_timeout: Timeout für Lock-Akquisition
"""
# Neue Klasse mit ThreadSafetyMixin erstellen
class ThreadSafeVersion(ThreadSafetyMixin, cls):
pass
ThreadSafeVersion.__name__ = f"ThreadSafe{cls.__name__}"
ThreadSafeVersion.__qualname__ = f"ThreadSafe{cls.__qualname__}"
# Methoden mit thread_safe_method decorator versehen
if method_names:
for method_name in method_names:
if hasattr(ThreadSafeVersion, method_name):
original_method = getattr(ThreadSafeVersion, method_name)
decorated_method = thread_safe_method(
operation_key=f"{cls.__name__}.{method_name}",
timeout=operation_timeout
)(original_method)
setattr(ThreadSafeVersion, method_name, decorated_method)
return ThreadSafeVersion

731
utils/update_checker.py Normale Datei

Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist

465
utils/username_generator.py Normale Datei
Datei anzeigen

@ -0,0 +1,465 @@
# utils/username_generator.py
"""
Benutzernamen-Generator für den Social Media Account Generator.
"""
import random
import string
import re
import logging
from typing import Dict, List, Any, Optional, Tuple, Union
logger = logging.getLogger("username_generator")
class UsernameGenerator:
"""Klasse zur Generierung von Benutzernamen für verschiedene Plattformen."""
def __init__(self):
"""Initialisiert den UsernameGenerator."""
# Plattformspezifische Richtlinien
self.platform_policies = {
"instagram": {
"min_length": 1,
"max_length": 30,
"allowed_chars": string.ascii_letters + string.digits + "._",
"allowed_start_chars": string.ascii_letters + string.digits,
"allowed_end_chars": string.ascii_letters + string.digits + ".",
"allowed_consecutive_special": False,
"disallowed_words": ["instagram", "admin", "official"],
"auto_suggestions": True
},
"facebook": {
"min_length": 5,
"max_length": 50,
"allowed_chars": string.ascii_letters + string.digits + ".-",
"allowed_start_chars": string.ascii_letters,
"allowed_end_chars": string.ascii_letters + string.digits,
"allowed_consecutive_special": True,
"disallowed_words": ["facebook", "meta", "admin"],
"auto_suggestions": False
},
"twitter": {
"min_length": 4,
"max_length": 15,
"allowed_chars": string.ascii_letters + string.digits + "_",
"allowed_start_chars": string.ascii_letters + string.digits,
"allowed_end_chars": string.ascii_letters + string.digits + "_",
"allowed_consecutive_special": True,
"disallowed_words": ["twitter", "admin", "official"],
"auto_suggestions": True
},
"tiktok": {
"min_length": 2,
"max_length": 24,
"allowed_chars": string.ascii_letters + string.digits + "._",
"allowed_start_chars": string.ascii_letters + string.digits,
"allowed_end_chars": string.ascii_letters + string.digits,
"allowed_consecutive_special": False,
"disallowed_words": ["tiktok", "admin", "official"],
"auto_suggestions": True
},
"default": {
"min_length": 3,
"max_length": 20,
"allowed_chars": string.ascii_letters + string.digits + "._-",
"allowed_start_chars": string.ascii_letters,
"allowed_end_chars": string.ascii_letters + string.digits,
"allowed_consecutive_special": False,
"disallowed_words": ["admin", "root", "system"],
"auto_suggestions": True
}
}
# Liste von Adjektiven und Substantiven für zufällige Benutzernamen
self.adjectives = [
"happy", "sunny", "clever", "brave", "mighty", "gentle", "wild", "calm", "bright",
"quiet", "swift", "bold", "wise", "fancy", "little", "big", "smart", "cool", "hot",
"super", "mega", "epic", "magic", "golden", "silver", "bronze", "shiny", "dark",
"light", "fast", "slow", "strong", "soft", "hard", "sweet", "sour", "tasty", "fresh",
"green", "blue", "red", "purple", "yellow", "orange", "pink", "white", "black"
]
self.nouns = [
"tiger", "eagle", "lion", "wolf", "bear", "fox", "owl", "hawk", "falcon", "dolphin",
"shark", "whale", "turtle", "panda", "koala", "monkey", "cat", "dog", "horse", "pony",
"unicorn", "dragon", "phoenix", "wizard", "knight", "warrior", "ninja", "samurai",
"queen", "king", "prince", "princess", "hero", "legend", "star", "moon", "sun", "sky",
"ocean", "river", "mountain", "forest", "tree", "flower", "rose", "tulip", "daisy"
]
# Internationaler Wortschatz (nur mit ASCII-Zeichen) für verschiedene Sprachen
# Jeweils 100 kurze, neutrale Begriffe pro Sprache
self.international_words = {
# Deutsch - 100 kurze, neutrale Begriffe ohne Umlaute oder Sonderzeichen
"de": [
"wald", "berg", "fluss", "tal", "see", "meer", "boot", "schiff", "haus", "dach",
"tuer", "fenster", "glas", "holz", "stein", "sand", "erde", "weg", "pfad", "strasse",
"auto", "rad", "ball", "spiel", "tisch", "stuhl", "bett", "kissen", "lampe", "licht",
"tag", "nacht", "sonne", "mond", "stern", "himmel", "wolke", "regen", "schnee", "wind",
"baum", "blume", "gras", "blatt", "frucht", "apfel", "brot", "wasser", "milch", "kaffee",
"buch", "brief", "stift", "musik", "lied", "tanz", "film", "bild", "farbe", "kunst",
"hand", "fuss", "kopf", "auge", "ohr", "nase", "mund", "zahn", "haar", "herz",
"zeit", "jahr", "monat", "woche", "tag", "stunde", "minute", "uhr", "zahl", "wort",
"name", "freund", "kind", "tier", "vogel", "fisch", "stadt", "land", "dorf", "garten",
"feld", "werk", "kraft", "geld", "gold", "bank", "markt", "preis", "karte", "punkt"
],
# Englisch - 100 kurze, neutrale Begriffe
"en": [
"wood", "hill", "river", "valley", "lake", "sea", "boat", "ship", "house", "roof",
"door", "window", "glass", "wood", "stone", "sand", "earth", "way", "path", "road",
"car", "wheel", "ball", "game", "table", "chair", "bed", "pillow", "lamp", "light",
"day", "night", "sun", "moon", "star", "sky", "cloud", "rain", "snow", "wind",
"tree", "flower", "grass", "leaf", "fruit", "apple", "bread", "water", "milk", "coffee",
"book", "letter", "pen", "music", "song", "dance", "film", "image", "color", "art",
"hand", "foot", "head", "eye", "ear", "nose", "mouth", "tooth", "hair", "heart",
"time", "year", "month", "week", "day", "hour", "minute", "clock", "number", "word",
"name", "friend", "child", "animal", "bird", "fish", "city", "country", "village", "garden",
"field", "work", "power", "money", "gold", "bank", "market", "price", "card", "point"
],
# Französisch - 100 kurze, neutrale Begriffe (ohne Akzente oder Sonderzeichen)
"fr": [
"bois", "mont", "fleuve", "vallee", "lac", "mer", "bateau", "navire", "maison", "toit",
"porte", "fenetre", "verre", "bois", "pierre", "sable", "terre", "voie", "sentier", "route",
"auto", "roue", "balle", "jeu", "table", "chaise", "lit", "coussin", "lampe", "lumiere",
"jour", "nuit", "soleil", "lune", "etoile", "ciel", "nuage", "pluie", "neige", "vent",
"arbre", "fleur", "herbe", "feuille", "fruit", "pomme", "pain", "eau", "lait", "cafe",
"livre", "lettre", "stylo", "musique", "chanson", "danse", "film", "image", "couleur", "art",
"main", "pied", "tete", "oeil", "oreille", "nez", "bouche", "dent", "cheveu", "coeur",
"temps", "annee", "mois", "semaine", "jour", "heure", "minute", "horloge", "nombre", "mot",
"nom", "ami", "enfant", "animal", "oiseau", "poisson", "ville", "pays", "village", "jardin",
"champ", "travail", "force", "argent", "or", "banque", "marche", "prix", "carte", "point"
],
# Spanisch - 100 kurze, neutrale Begriffe (ohne Akzente oder Sonderzeichen)
"es": [
"bosque", "monte", "rio", "valle", "lago", "mar", "barco", "nave", "casa", "techo",
"puerta", "ventana", "vidrio", "madera", "piedra", "arena", "tierra", "via", "ruta", "calle",
"coche", "rueda", "bola", "juego", "mesa", "silla", "cama", "cojin", "lampara", "luz",
"dia", "noche", "sol", "luna", "estrella", "cielo", "nube", "lluvia", "nieve", "viento",
"arbol", "flor", "hierba", "hoja", "fruta", "manzana", "pan", "agua", "leche", "cafe",
"libro", "carta", "pluma", "musica", "cancion", "baile", "pelicula", "imagen", "color", "arte",
"mano", "pie", "cabeza", "ojo", "oreja", "nariz", "boca", "diente", "pelo", "corazon",
"tiempo", "ano", "mes", "semana", "dia", "hora", "minuto", "reloj", "numero", "palabra",
"nombre", "amigo", "nino", "animal", "ave", "pez", "ciudad", "pais", "pueblo", "jardin",
"campo", "trabajo", "fuerza", "dinero", "oro", "banco", "mercado", "precio", "carta", "punto"
],
# Japanisch - 100 kurze, neutrale Begriffe (in romanisierter Form)
"ja": [
"ki", "yama", "kawa", "tani", "mizu", "umi", "fune", "ie", "yane", "kado",
"mado", "garasu", "ki", "ishi", "suna", "tsuchi", "michi", "kuruma", "wa", "tama",
"asobi", "tsukue", "isu", "neru", "makura", "akari", "hikari", "hi", "yoru", "taiyou",
"tsuki", "hoshi", "sora", "kumo", "ame", "yuki", "kaze", "ki", "hana", "kusa",
"ha", "kudamono", "ringo", "pan", "mizu", "gyunyu", "kohi", "hon", "tegami", "pen",
"ongaku", "uta", "odori", "eiga", "e", "iro", "geijutsu", "te", "ashi", "atama",
"me", "mimi", "hana", "kuchi", "ha", "kami", "kokoro", "jikan", "toshi", "tsuki",
"shukan", "hi", "jikan", "fun", "tokei", "kazu", "kotoba", "namae", "tomodachi", "kodomo",
"doubutsu", "tori", "sakana", "machi", "kuni", "mura", "niwa", "hatake", "shigoto", "chikara",
"okane", "kin", "ginko", "ichiba", "nedan", "kado", "ten", "ai", "heiwa", "yume"
]
}
def get_platform_policy(self, platform: str) -> Dict[str, Any]:
"""
Gibt die Benutzernamen-Richtlinie für eine bestimmte Plattform zurück.
Args:
platform: Name der Plattform
Returns:
Dictionary mit der Benutzernamen-Richtlinie
"""
platform = platform.lower()
return self.platform_policies.get(platform, self.platform_policies["default"])
def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None:
"""
Setzt oder aktualisiert die Benutzernamen-Richtlinie für eine Plattform.
Args:
platform: Name der Plattform
policy: Dictionary mit der Benutzernamen-Richtlinie
"""
platform = platform.lower()
self.platform_policies[platform] = policy
logger.info(f"Benutzernamen-Richtlinie für '{platform}' aktualisiert")
def generate_username(self, platform: str = "default", name: Optional[str] = None,
custom_policy: Optional[Dict[str, Any]] = None) -> str:
"""
Generiert einen Benutzernamen gemäß den Richtlinien.
Args:
platform: Name der Plattform
name: Optionaler vollständiger Name für die Generierung
custom_policy: Optionale benutzerdefinierte Richtlinie
Returns:
Generierter Benutzername
"""
# Richtlinie bestimmen
if custom_policy:
policy = custom_policy
else:
policy = self.get_platform_policy(platform)
# Wenn ein Name angegeben ist, versuche einen darauf basierenden Benutzernamen zu erstellen
if name:
return self.generate_from_name(name, policy)
else:
# Zufälligen Benutzernamen erstellen
return self.generate_random_username(policy)
def generate_from_name(self, name: str, policy: Dict[str, Any]) -> str:
"""
Generiert einen Benutzernamen aus einem vollständigen Namen im Format
Vorname_RandomBegriffAusDerAusgewähltenSprache_GeburtsjahrXX.
Args:
name: Vollständiger Name
policy: Benutzernamen-Richtlinie
Returns:
Generierter Benutzername
"""
# Name in Teile zerlegen
parts = name.lower().split()
# Sonderzeichen und Leerzeichen entfernen
parts = [re.sub(r'[^a-z0-9]', '', part) for part in parts]
parts = [part for part in parts if part]
if not parts:
# Falls keine gültigen Teile, zufälligen Benutzernamen generieren
return self.generate_random_username(policy)
# Vorname nehmen
firstname = parts[0]
# Zufällige Sprache auswählen
available_languages = list(self.international_words.keys())
chosen_language = random.choice(available_languages)
# Zufälliges Wort aus der gewählten Sprache wählen
random_word = random.choice(self.international_words[chosen_language])
# Geburtsjahr simulieren (zwischen 18 und 40 Jahre alt)
current_year = 2025 # Aktuelle Jahresangabe im Code
birth_year = current_year - random.randint(18, 40)
# Letzte zwei Ziffern vom Geburtsjahr plus eine Zufallszahl
year_suffix = str(birth_year)[-2:] + str(random.randint(0, 9))
# Benutzernamen im neuen Format zusammensetzen
username = f"{firstname}_{random_word}_{year_suffix}"
# Länge prüfen und anpassen
if len(username) > policy["max_length"]:
# Bei Überlänge, kürze den Mittelteil
max_word_length = policy["max_length"] - len(firstname) - len(year_suffix) - 2 # 2 für die Unterstriche
if max_word_length < 3: # Zu kurz für ein sinnvolles Wort
# Fallback: Nur Vorname + Jahreszahl
username = f"{firstname}_{year_suffix}"
else:
random_word = random_word[:max_word_length]
username = f"{firstname}_{random_word}_{year_suffix}"
# Überprüfen, ob die Richtlinien erfüllt sind
valid, error_msg = self.validate_username(username, policy=policy)
if not valid:
# Wenn nicht gültig, generiere einen alternativen Namen
logger.debug(f"Generierter Name '{username}' nicht gültig: {error_msg}")
# Einfachere Variante versuchen
username = f"{firstname}{year_suffix}"
valid, _ = self.validate_username(username, policy=policy)
if not valid:
# Wenn immer noch nicht gültig, Fallback auf Standard-Generator
return self.generate_random_username(policy)
logger.info(f"Aus Name generierter Benutzername: {username}")
return username
def generate_random_username(self, policy: Dict[str, Any]) -> str:
"""
Generiert einen zufälligen Benutzernamen.
Args:
policy: Benutzernamen-Richtlinie
Returns:
Generierter Benutzername
"""
# Verschiedene Muster für zufällige Benutzernamen
patterns = [
# Adjektiv + Substantiv
lambda: random.choice(self.adjectives) + random.choice(self.nouns),
# Substantiv + Zahlen
lambda: random.choice(self.nouns) + "".join(random.choices(string.digits, k=random.randint(1, 4))),
# Adjektiv + Substantiv + Zahlen
lambda: random.choice(self.adjectives) + random.choice(self.nouns) + "".join(random.choices(string.digits, k=random.randint(1, 3))),
# Substantiv + Unterstrich + Substantiv
lambda: random.choice(self.nouns) + ("_" if "_" in policy["allowed_chars"] else "") + random.choice(self.nouns),
# Benutzer + Zahlen
lambda: "user" + "".join(random.choices(string.digits, k=random.randint(3, 6)))
]
# Zufälliges Muster auswählen und Benutzernamen generieren
max_attempts = 10
for _ in range(max_attempts):
pattern_func = random.choice(patterns)
username = pattern_func()
# Zu lange Benutzernamen kürzen
if len(username) > policy["max_length"]:
username = username[:policy["max_length"]]
# Zu kurze Benutzernamen verlängern
if len(username) < policy["min_length"]:
username += "".join(random.choices(string.digits, k=policy["min_length"] - len(username)))
# Überprüfen, ob der Benutzername den Richtlinien entspricht
valid, _ = self.validate_username(username, policy=policy)
if valid:
logger.info(f"Zufälliger Benutzername generiert: {username}")
return username
# Fallback: Einfachen Benutzernamen mit Zufallsbuchstaben und Zahlen generieren
length = random.randint(policy["min_length"], min(policy["max_length"], policy["min_length"] + 5))
username = random.choice(string.ascii_lowercase) # Erster Buchstabe
allowed_chars = [c for c in policy["allowed_chars"] if c in (string.ascii_lowercase + string.digits)]
username += "".join(random.choice(allowed_chars) for _ in range(length - 1))
logger.info(f"Fallback-Benutzername generiert: {username}")
return username
def suggest_alternatives(self, username: str, platform: str = "default") -> List[str]:
"""
Schlägt alternative Benutzernamen vor, wenn der gewünschte bereits vergeben ist.
Args:
username: Gewünschter Benutzername
platform: Name der Plattform
Returns:
Liste mit alternativen Benutzernamen
"""
policy = self.get_platform_policy(platform)
# Wenn Auto-Suggestions deaktiviert sind, leere Liste zurückgeben
if not policy.get("auto_suggestions", True):
return []
alternatives = []
base_username = username
# Verschiedene Modifikationen ausprobieren
# Anhängen von Zahlen
for i in range(5):
suffix = str(random.randint(1, 999))
alt = base_username + suffix
if len(alt) <= policy["max_length"]:
alternatives.append(alt)
# Sonderzeichen einfügen
for special in ["_", ".", "-"]:
if special in policy["allowed_chars"]:
alt = base_username + special + str(random.randint(1, 99))
if len(alt) <= policy["max_length"]:
alternatives.append(alt)
# Adjektiv voranstellen
for _ in range(2):
prefix = random.choice(self.adjectives)
alt = prefix + base_username
if len(alt) <= policy["max_length"]:
alternatives.append(alt)
# Buchstaben ersetzen (z.B. 'o' durch '0')
if "0" in policy["allowed_chars"] and "o" in base_username.lower():
alt = base_username.lower().replace("o", "0")
if len(alt) <= policy["max_length"]:
alternatives.append(alt)
# Zufällige Buchstaben voranstellen
for _ in range(2):
prefix = "".join(random.choices(string.ascii_lowercase, k=random.randint(1, 3)))
alt = prefix + base_username
if len(alt) <= policy["max_length"]:
alternatives.append(alt)
# Validiere die alternativen Benutzernamen
valid_alternatives = []
for alt in alternatives:
valid, _ = self.validate_username(alt, policy=policy)
if valid:
valid_alternatives.append(alt)
# Zufällige Auswahl aus den gültigen Alternativen (maximal 5)
if len(valid_alternatives) > 5:
valid_alternatives = random.sample(valid_alternatives, 5)
logger.info(f"{len(valid_alternatives)} alternative Benutzernamen generiert für '{username}'")
return valid_alternatives
def validate_username(self, username: str, platform: str = "default",
policy: Optional[Dict[str, Any]] = None) -> Tuple[bool, str]:
"""
Überprüft, ob ein Benutzername den Richtlinien entspricht.
Args:
username: Zu überprüfender Benutzername
platform: Name der Plattform
policy: Optionale Richtlinie (sonst wird die der Plattform verwendet)
Returns:
(Gültigkeit, Fehlermeldung)
"""
# Richtlinie bestimmen
if not policy:
policy = self.get_platform_policy(platform)
# Länge prüfen
if len(username) < policy["min_length"]:
return False, f"Benutzername ist zu kurz (mindestens {policy['min_length']} Zeichen erforderlich)"
if len(username) > policy["max_length"]:
return False, f"Benutzername ist zu lang (maximal {policy['max_length']} Zeichen erlaubt)"
# Erlaubte Zeichen prüfen
for char in username:
if char not in policy["allowed_chars"]:
return False, f"Unerlaubtes Zeichen: '{char}'"
# Anfangszeichen prüfen
if username[0] not in policy["allowed_start_chars"]:
return False, f"Benutzername darf nicht mit '{username[0]}' beginnen"
# Endzeichen prüfen
if username[-1] not in policy["allowed_end_chars"]:
return False, f"Benutzername darf nicht mit '{username[-1]}' enden"
# Aufeinanderfolgende Sonderzeichen prüfen
if not policy["allowed_consecutive_special"]:
special_chars = set(policy["allowed_chars"]) - set(string.ascii_letters + string.digits)
for i in range(len(username) - 1):
if username[i] in special_chars and username[i+1] in special_chars:
return False, "Keine aufeinanderfolgenden Sonderzeichen erlaubt"
# Disallowed words
for word in policy["disallowed_words"]:
if word.lower() in username.lower():
return False, f"Der Benutzername darf '{word}' nicht enthalten"
return True, "Benutzername ist gültig"