Initial commit
Dieser Commit ist enthalten in:
0
utils/__init__.py
Normale Datei
0
utils/__init__.py
Normale Datei
304
utils/birthday_generator.py
Normale Datei
304
utils/birthday_generator.py
Normale Datei
@ -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
687
utils/email_handler.py
Normale Datei
@ -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
592
utils/human_behavior.py
Normale Datei
@ -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
69
utils/logger.py
Normale Datei
@ -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
386
utils/modal_manager.py
Normale Datei
@ -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
195
utils/modal_test.py
Normale Datei
@ -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
338
utils/password_generator.py
Normale Datei
@ -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
412
utils/performance_monitor.py
Normale Datei
@ -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
413
utils/proxy_rotator.py
Normale Datei
@ -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
292
utils/result_decorators.py
Normale Datei
@ -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
558
utils/text_similarity.py
Normale Datei
@ -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
133
utils/theme_manager.py
Normale Datei
@ -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
362
utils/thread_safety_mixins.py
Normale Datei
@ -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
731
utils/update_checker.py
Normale Datei
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
465
utils/username_generator.py
Normale Datei
465
utils/username_generator.py
Normale Datei
@ -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"
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren