Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-03 21:11:05 +02:00
Commit 08ed938105
239 geänderte Dateien mit 21554 neuen und 0 gelöschten Zeilen

0
utils/__init__.py Normale Datei
Datei anzeigen

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

299
utils/birthday_generator.py Normale Datei
Datei anzeigen

@ -0,0 +1,299 @@
"""
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
},
"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

646
utils/email_handler.py Normale Datei
Datei anzeigen

@ -0,0 +1,646 @@
"""
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"
],
"tiktok": [
"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
subject = decode_header(msg.get("Subject", ""))[0]
if isinstance(subject[0], bytes):
subject = subject[0].decode(subject[1] or 'utf-8', errors='replace')
else:
subject = subject[0]
# Absender decodieren
from_addr = decode_header(msg.get("From", ""))[0]
if isinstance(from_addr[0], bytes):
from_addr = from_addr[0].decode(from_addr[1] or 'utf-8', errors='replace')
else:
from_addr = from_addr[0]
# Empfänger decodieren
to_addr = decode_header(msg.get("To", ""))[0]
if isinstance(to_addr[0], bytes):
to_addr = to_addr[0].decode(to_addr[1] or 'utf-8', errors='replace')
else:
to_addr = to_addr[0]
# 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'}")
# Domain aus der Ziel-E-Mail-Adresse extrahieren, falls vorhanden
target_domain = None
if target_email and "@" in target_email:
target_domain = target_email.split("@")[-1].lower()
logger.debug(f"Ziel-Domain: {target_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
for attempt in range(max_attempts):
logger.debug(f"Versuch {attempt + 1}/{max_attempts}, Abruf neuer E-Mails...")
# Alle neuen E-Mails abrufen
emails = self.search_emails(search_criteria, max_emails=10)
# E-Mails filtern und nach Bestätigungscode suchen
for email_info in emails:
# Extrahierte E-Mail-Adresse des Empfängers
to_email = email_info.get("to_email", "").lower()
# Wenn eine bestimmte Ziel-E-Mail angegeben ist, prüfe auf exakte Übereinstimmung
if target_email and target_email.lower() != to_email:
# Wenn keine exakte Übereinstimmung, prüfe auf Domain-Übereinstimmung (für Catch-All)
if not target_domain or target_domain not in to_email:
logger.debug(f"E-Mail übersprungen: Empfänger {to_email} stimmt nicht mit Ziel {target_email} überein")
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
],
"tiktok": [
r"TikTok-Code: (\d{6})",
r"TikTok code: (\d{6})",
r"TikTok: (\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)

488
utils/human_behavior.py Normale Datei
Datei anzeigen

@ -0,0 +1,488 @@
"""
Menschliches Verhalten für den Social Media Account Generator.
"""
import random
import time
import logging
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
steps = max(10, int(distance / 10))
# Berechne die Bewegungskurve (Bézierkurve)
# Zufällige Kontrollpunkte für eine natürliche Bewegung
control_point_1 = (
from_point[0] + dx * 0.3 + random.randint(-int(distance/10), int(distance/10)),
from_point[1] + dy * 0.1 + random.randint(-int(distance/10), int(distance/10))
)
control_point_2 = (
from_point[0] + dx * 0.7 + random.randint(-int(distance/10), int(distance/10)),
from_point[1] + dy * 0.9 + random.randint(-int(distance/10), int(distance/10))
)
# Bewegung durchführen
for i in range(steps + 1):
t = i / steps
# 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]
# Runde auf ganze Zahlen
curr_point = (int(x), int(y))
# Callback aufrufen, wenn vorhanden
if on_move:
on_move(curr_point)
# Verzögerung basierend auf der Position in der Bewegung
# Am Anfang und Ende langsamer, in der Mitte schneller
if i < 0.2 * steps or i > 0.8 * steps:
self.sleep("mouse_movement", 1.5 / steps)
else:
self.sleep("mouse_movement", 1.0 / 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
for i in range(amount):
# Zufällige Variation der Scrollmenge
scroll_amount = scroll_factor * (random.randint(1, 3) if self.randomness > 0.5 else 1)
logger.debug(f"Scrolle {direction} ({scroll_amount})")
if on_scroll:
on_scroll(scroll_amount)
# Verzögerung zwischen Scroll-Ereignissen
if i < amount - 1: # Keine Verzögerung nach dem letzten Scroll
self.sleep("scroll")
def wait_for_page_load(self, multiplier: float = 1.0) -> None:
"""
Wartet eine angemessene Zeit auf das Laden einer Seite.
Args:
multiplier: Multiplikator für die Standardwartezeit
"""
self.sleep("page_load", multiplier)
def wait_between_actions(self, action_type: str = "decision", multiplier: float = 1.0) -> None:
"""
Wartet zwischen Aktionen, um menschliches Verhalten zu simulieren.
Args:
action_type: Art der Aktion
multiplier: Multiplikator für die Standardwartezeit
"""
self.sleep(action_type, multiplier)
def navigate_sequence(self, steps: int, min_delay: float = 0.5, max_delay: float = 2.0) -> None:
"""
Simuliert eine Sequenz von Navigationsschritten mit variierenden Verzögerungen.
Args:
steps: Anzahl der Navigationsschritte
min_delay: Minimale Verzögerung zwischen Schritten
max_delay: Maximale Verzögerung zwischen Schritten
"""
for i in range(steps):
# Zufällige Verzögerung zwischen Navigationsschritten
delay = random.uniform(min_delay, max_delay)
logger.debug(f"Navigationsschritt {i+1}/{steps}, Verzögerung: {delay:.2f}s")
time.sleep(delay / self.speed_factor)
def human_delay_pattern(self, action_type: str = "default", intensity: str = "medium") -> None:
"""
Erzeugt ein komplexes, menschliches Verzögerungsmuster.
Args:
action_type: Art der Aktion (entscheidet über Basismuster)
intensity: Intensität des Musters ("low", "medium", "high")
"""
# Verzögerungsmuster basierend auf Aktionstyp und Intensität
patterns = {
"default": {
"low": (0.2, 0.5),
"medium": (0.5, 1.0),
"high": (1.0, 2.0)
},
"reading": {
"low": (1.0, 2.0),
"medium": (2.0, 4.0),
"high": (3.0, 6.0)
},
"thinking": {
"low": (1.5, 3.0),
"medium": (3.0, 5.0),
"high": (5.0, 8.0)
},
"verification": {
"low": (3.0, 5.0),
"medium": (5.0, 8.0),
"high": (8.0, 12.0)
}
}
# Standardmuster verwenden, wenn nicht bekannt
pattern = patterns.get(action_type, patterns["default"])
delay_range = pattern.get(intensity, pattern["medium"])
# Zufällige Verzögerung im angegebenen Bereich
delay = random.uniform(delay_range[0], delay_range[1])
# Anpassung basierend auf Geschwindigkeit und Zufälligkeit
delay = delay / self.speed_factor
if self.randomness > 0:
# Füge ein zufälliges "Zittern" hinzu
jitter = random.uniform(-0.2, 0.2) * self.randomness * delay
delay += jitter
logger.debug(f"Menschliche Verzögerung ({action_type}, {intensity}): {delay:.2f}s")
time.sleep(max(0, delay))
def simulate_form_filling(self, fields: int, field_callback: Optional[Callable[[int], None]] = None) -> None:
"""
Simuliert das Ausfüllen eines Formulars mit menschlichem Verhalten.
Args:
fields: Anzahl der auszufüllenden Felder
field_callback: Optionale Funktion, die für jedes Feld aufgerufen wird
"""
for i in range(fields):
logger.debug(f"Fülle Formularfeld {i+1}/{fields} aus")
if field_callback:
field_callback(i)
# Verzögerung zwischen Feldern
if i < fields - 1: # Keine Verzögerung nach dem letzten Feld
# Gelegentlich längere Pausen einbauen
if random.random() < 0.2 * self.randomness:
self.human_delay_pattern("thinking", "low")
else:
self.sleep("form_fill")
def simulate_captcha_solving(self, on_progress: Optional[Callable[[float], None]] = None) -> None:
"""
Simuliert das Lösen eines CAPTCHAs mit menschlichem Verhalten.
Args:
on_progress: Optionale Funktion, die mit dem Fortschritt (0-1) aufgerufen wird
"""
# Simuliere einen komplexen Prozess mit mehreren Schritten
steps = random.randint(4, 8)
for i in range(steps):
progress = (i + 1) / steps
logger.debug(f"CAPTCHA-Lösung Fortschritt: {progress:.0%}")
if on_progress:
on_progress(progress)
# Verschiedene Verzögerungsmuster für die einzelnen Schritte
if i == 0:
# Anfängliches Lesen/Verstehen
self.human_delay_pattern("reading", "medium")
elif i == steps - 1:
# Abschließende Überprüfung/Bestätigung
self.human_delay_pattern("verification", "low")
else:
# Auswahl/Interaktion
self.human_delay_pattern("thinking", "medium")

69
utils/logger.py Normale Datei
Datei anzeigen

@ -0,0 +1,69 @@
"""
Logger-Konfiguration für die Social Media Account Generator Anwendung.
"""
import os
import logging
import sys
from PyQt5.QtWidgets import QTextEdit
from PyQt5.QtGui import QTextCursor
class LogHandler(logging.Handler):
"""Handler, der Logs an ein QTextEdit-Widget sendet."""
def __init__(self, text_widget=None):
super().__init__()
self.text_widget = text_widget
if self.text_widget:
self.text_widget.setReadOnly(True)
def emit(self, record):
msg = self.format(record)
if self.text_widget:
self.text_widget.append(msg)
# Scrolle nach unten
self.text_widget.moveCursor(QTextCursor.End)
def setup_logger(name="main", level=logging.INFO):
"""
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)

338
utils/password_generator.py Normale Datei
Datei anzeigen

@ -0,0 +1,338 @@
"""
Passwortgenerator für den Social Media Account Generator.
"""
import random
import string
import logging
from typing import Dict, List, Any, Optional, Tuple, Union
logger = logging.getLogger("password_generator")
class PasswordGenerator:
"""Klasse zur Generierung sicherer und plattformkonformer Passwörter."""
def __init__(self):
"""Initialisiert den PasswordGenerator."""
# Passwort-Richtlinien für verschiedene Plattformen
self.platform_policies = {
"instagram": {
"min_length": 6,
"max_length": 20,
"require_uppercase": False,
"require_lowercase": True,
"require_digits": False,
"require_special": False,
"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": 8,
"max_length": 20,
"require_uppercase": False,
"require_lowercase": True,
"require_digits": True,
"require_special": False,
"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)

413
utils/proxy_rotator.py Normale Datei
Datei anzeigen

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

558
utils/text_similarity.py Normale Datei
Datei anzeigen

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

133
utils/theme_manager.py Normale Datei
Datei anzeigen

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

733
utils/update_checker.py Normale Datei

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

465
utils/username_generator.py Normale Datei
Datei anzeigen

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