Initial commit
188
CLAUDE_PROJECT_README.md
Normale Datei
@ -0,0 +1,188 @@
|
||||
# test-main
|
||||
|
||||
*This README was automatically generated by Claude Project Manager*
|
||||
|
||||
## Project Overview
|
||||
|
||||
- **Path**: `C:/Users/hendr/Desktop/IntelSight/test-main`
|
||||
- **Files**: 125 files
|
||||
- **Size**: 884.4 KB
|
||||
- **Last Modified**: 2025-07-01 22:08
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Languages
|
||||
- Python
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
CLAUDE_PROJECT_README.md
|
||||
main.py
|
||||
README.md
|
||||
requirements.txt
|
||||
browser/
|
||||
│ ├── fingerprint_protection.py
|
||||
│ ├── playwright_extensions.py
|
||||
│ ├── playwright_manager.py
|
||||
│ ├── stealth_config.py
|
||||
│ └── __init__.py
|
||||
config/
|
||||
│ ├── app_version.json
|
||||
│ ├── browser_config.json
|
||||
│ ├── email_config.json
|
||||
│ ├── facebook_config.json
|
||||
│ ├── instagram_config.json
|
||||
│ ├── license.json
|
||||
│ ├── license_config.json
|
||||
│ ├── proxy_config.json
|
||||
│ └── stealth_config.json
|
||||
controllers/
|
||||
│ ├── account_controller.py
|
||||
│ ├── main_controller.py
|
||||
│ ├── settings_controller.py
|
||||
│ └── platform_controllers/
|
||||
│ ├── base_controller.py
|
||||
│ ├── instagram_controller.py
|
||||
│ └── tiktok_controller.py
|
||||
database/
|
||||
│ ├── accounts.db
|
||||
│ ├── account_repository.py
|
||||
│ ├── db_manager.py
|
||||
│ ├── instagram_accounts.db
|
||||
│ ├── schema.sql
|
||||
│ └── __init__.py
|
||||
licensing/
|
||||
│ ├── hardware_fingerprint.py
|
||||
│ ├── license_manager.py
|
||||
│ ├── license_validator.py
|
||||
│ └── __init__.py
|
||||
localization/
|
||||
│ ├── language_manager.py
|
||||
│ ├── __init__.py
|
||||
│ └── languages/
|
||||
│ ├── de.json
|
||||
│ ├── en.json
|
||||
│ ├── es.json
|
||||
│ ├── fr.json
|
||||
│ └── ja.json
|
||||
logs/
|
||||
│ ├── main.log
|
||||
│ └── screenshots
|
||||
resources/
|
||||
│ ├── icons/
|
||||
│ │ ├── de.svg
|
||||
│ │ ├── en.svg
|
||||
│ │ ├── es.svg
|
||||
│ │ ├── facebook.svg
|
||||
│ │ ├── fr.svg
|
||||
│ │ ├── instagram.svg
|
||||
│ │ ├── ja.svg
|
||||
│ │ ├── moon.svg
|
||||
│ │ ├── sun.svg
|
||||
│ │ └── tiktok.svg
|
||||
│ └── themes/
|
||||
│ ├── dark.qss
|
||||
│ └── light.qss
|
||||
social_networks/
|
||||
│ ├── base_automation.py
|
||||
│ ├── __init__.py
|
||||
│ ├── facebook/
|
||||
│ │ ├── facebook_automation.py
|
||||
│ │ ├── facebook_login.py
|
||||
│ │ ├── facebook_registration.py
|
||||
│ │ ├── facebook_selectors.py
|
||||
│ │ ├── facebook_ui_helper.py
|
||||
│ │ ├── facebook_utils.py
|
||||
│ │ ├── facebook_verification.py
|
||||
│ │ ├── facebook_workflow.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── instagram/
|
||||
│ │ ├── instagram_automation.py
|
||||
│ │ ├── instagram_login.py
|
||||
│ │ ├── instagram_registration.py
|
||||
│ │ ├── instagram_selectors.py
|
||||
│ │ ├── instagram_ui_helper.py
|
||||
│ │ ├── instagram_utils.py
|
||||
│ │ ├── instagram_verification.py
|
||||
│ │ ├── instagram_workflow.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── tiktok/
|
||||
│ │ ├── tiktok_automation.py
|
||||
│ │ ├── tiktok_login.py
|
||||
│ │ ├── tiktok_registration.py
|
||||
│ │ ├── tiktok_selectors.py
|
||||
│ │ ├── tiktok_ui_helper.py
|
||||
│ │ ├── tiktok_utils.py
|
||||
│ │ ├── tiktok_verification.py
|
||||
│ │ ├── tiktok_workflow.py
|
||||
│ │ └── __init__.py
|
||||
│ └── twitter/
|
||||
│ ├── twitter_automation.py
|
||||
│ ├── twitter_login.py
|
||||
│ ├── twitter_registration.py
|
||||
│ ├── twitter_selectors.py
|
||||
│ ├── twitter_ui_helper.py
|
||||
│ ├── twitter_utils.py
|
||||
│ ├── twitter_verification.py
|
||||
│ ├── twitter_workflow.py
|
||||
│ └── __init__.py
|
||||
testcases/
|
||||
│ └── imap_test.py
|
||||
updates/
|
||||
│ ├── downloader.py
|
||||
│ ├── update_checker.py
|
||||
│ ├── update_v1.1.0.zip
|
||||
│ ├── version.py
|
||||
│ └── __init__.py
|
||||
utils/
|
||||
│ ├── birthday_generator.py
|
||||
│ ├── email_handler.py
|
||||
│ ├── human_behavior.py
|
||||
│ ├── logger.py
|
||||
│ ├── password_generator.py
|
||||
│ ├── proxy_rotator.py
|
||||
│ ├── text_similarity.py
|
||||
│ ├── theme_manager.py
|
||||
│ ├── update_checker.py
|
||||
│ └── username_generator.py
|
||||
views/
|
||||
├── about_dialog.py
|
||||
├── main_window.py
|
||||
├── platform_selector.py
|
||||
├── tabs/
|
||||
│ ├── accounts_tab.py
|
||||
│ ├── generator_tab.py
|
||||
│ └── settings_tab.py
|
||||
└── widgets/
|
||||
├── language_dropdown.py
|
||||
└── platform_button.py
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
- `README.md`
|
||||
- `requirements.txt`
|
||||
|
||||
## Claude Integration
|
||||
|
||||
This project is managed with Claude Project Manager. To work with this project:
|
||||
|
||||
1. Open Claude Project Manager
|
||||
2. Click on this project's tile
|
||||
3. Claude will open in the project directory
|
||||
|
||||
## Notes
|
||||
|
||||
*Add your project-specific notes here*
|
||||
|
||||
---
|
||||
|
||||
## Development Log
|
||||
|
||||
- README generated on 2025-07-01 20:43:39
|
||||
- README updated on 2025-07-01 21:09:06
|
||||
- README updated on 2025-07-01 21:59:23
|
||||
- README updated on 2025-07-01 22:08:40
|
||||
- README updated on 2025-07-01 22:08:50
|
||||
- README updated on 2025-07-01 22:09:15
|
||||
130
README.md
Normale Datei
@ -0,0 +1,130 @@
|
||||
# Social Media Account Generator
|
||||
|
||||
Dieses Repository enthält eine Desktopanwendung zur automatisierten Erstellung und Verwaltung von Social‑Media‑Accounts. Die grafische Oberfläche basiert auf **PyQt5**, die Browser‑Automatisierung erfolgt mit **Playwright**. Der Code ist modular aufgebaut und kann leicht um weitere Plattformen erweitert werden.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Python 3.8 oder neuer installieren.
|
||||
2. Abhängigkeiten mit `pip install -r requirements.txt` einrichten.
|
||||
|
||||
## Anwendung starten
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
Beim ersten Start werden benötigte Ordner wie `logs`, `config` und `resources` automatisch angelegt. Einstellungen können im Ordner `config` angepasst werden.
|
||||
|
||||
## Projektstruktur (Auszug)
|
||||
|
||||
```text
|
||||
.
|
||||
├── main.py
|
||||
├── browser/
|
||||
│ ├── playwright_manager.py
|
||||
│ └── stealth_config.py
|
||||
├── controllers/
|
||||
│ ├── main_controller.py
|
||||
│ ├── account_controller.py
|
||||
│ ├── settings_controller.py
|
||||
│ └── platform_controllers/
|
||||
│ ├── base_controller.py
|
||||
│ ├── instagram_controller.py
|
||||
│ └── tiktok_controller.py
|
||||
├── views/
|
||||
│ ├── main_window.py
|
||||
│ ├── platform_selector.py
|
||||
│ ├── about_dialog.py
|
||||
│ ├── widgets/
|
||||
│ │ └── platform_button.py
|
||||
│ └── tabs/
|
||||
│ ├── generator_tab.py
|
||||
│ ├── accounts_tab.py
|
||||
│ └── settings_tab.py
|
||||
├── social_networks/
|
||||
│ ├── base_automation.py
|
||||
│ ├── instagram/
|
||||
│ │ └── ...
|
||||
│ ├── tiktok/
|
||||
│ │ └── ...
|
||||
│ ├── facebook/
|
||||
│ │ └── ...
|
||||
│ └── twitter/
|
||||
│ └── ...
|
||||
├── localization/
|
||||
│ ├── language_manager.py
|
||||
│ └── languages/
|
||||
│ ├── de.json
|
||||
│ ├── en.json
|
||||
│ ├── es.json
|
||||
│ ├── fr.json
|
||||
│ └── ja.json
|
||||
├── utils/
|
||||
│ ├── logger.py
|
||||
│ ├── password_generator.py
|
||||
│ ├── username_generator.py
|
||||
│ ├── birthday_generator.py
|
||||
│ ├── email_handler.py
|
||||
│ ├── proxy_rotator.py
|
||||
│ ├── human_behavior.py
|
||||
│ ├── text_similarity.py
|
||||
│ └── theme_manager.py
|
||||
├── database/
|
||||
│ ├── db_manager.py
|
||||
│ └── ...
|
||||
├── licensing/
|
||||
│ ├── license_manager.py
|
||||
│ ├── hardware_fingerprint.py
|
||||
│ └── license_validator.py
|
||||
├── updates/
|
||||
│ ├── update_checker.py
|
||||
│ ├── downloader.py
|
||||
│ ├── version.py
|
||||
│ └── ...
|
||||
├── config/
|
||||
│ ├── browser_config.json
|
||||
│ ├── email_config.json
|
||||
│ ├── proxy_config.json
|
||||
│ ├── stealth_config.json
|
||||
│ ├── license_config.json
|
||||
│ ├── instagram_config.json
|
||||
│ ├── facebook_config.json
|
||||
│ ├── twitter_config.json
|
||||
│ ├── tiktok_config.json
|
||||
│ ├── theme.json
|
||||
│ ├── app_version.json
|
||||
│ └── update_config.json
|
||||
├── resources/
|
||||
│ ├── icons/
|
||||
│ │ ├── instagram.svg
|
||||
│ │ ├── facebook.svg
|
||||
│ │ ├── twitter.svg
|
||||
│ │ ├── tiktok.svg
|
||||
│ │ └── vk.svg
|
||||
│ └── themes/
|
||||
│ ├── light.qss
|
||||
│ └── dark.qss
|
||||
├── testcases/
|
||||
│ └── imap_test.py
|
||||
├── requirements.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Weitere Ordner:
|
||||
|
||||
- `logs/` – Protokolldateien und Screenshots
|
||||
- `resources/` – Icons und Theme‑Dateien
|
||||
- `updates/` – heruntergeladene Updates
|
||||
|
||||
## Lokalisierung
|
||||
|
||||
Im Ordner `localization/languages` befinden sich Übersetzungsdateien für Deutsch, Englisch, Spanisch, Französisch und Japanisch. Die aktuelle Sprache kann zur Laufzeit gewechselt werden.
|
||||
|
||||
## Lizenz und Updates
|
||||
|
||||
Die Ordner `licensing` und `updates` enthalten die Logik zur Lizenzprüfung und zum Update‑Management. Versionsinformationen werden in `updates/version.py` verwaltet.
|
||||
|
||||
## Tests
|
||||
|
||||
Im Ordner `testcases` liegt beispielhaft `imap_test.py`, mit dem die IMAP‑Konfiguration getestet werden kann.
|
||||
|
||||
0
browser/__init__.py
Normale Datei
BIN
browser/__pycache__/__init__.cpython-310.pyc
Normale Datei
BIN
browser/__pycache__/__init__.cpython-313.pyc
Normale Datei
BIN
browser/__pycache__/fingerprint_protection.cpython-310.pyc
Normale Datei
BIN
browser/__pycache__/fingerprint_protection.cpython-313.pyc
Normale Datei
BIN
browser/__pycache__/playwright_extensions.cpython-310.pyc
Normale Datei
BIN
browser/__pycache__/playwright_extensions.cpython-313.pyc
Normale Datei
BIN
browser/__pycache__/playwright_manager.cpython-310.pyc
Normale Datei
BIN
browser/__pycache__/playwright_manager.cpython-313.pyc
Normale Datei
721
browser/fingerprint_protection.py
Normale Datei
@ -0,0 +1,721 @@
|
||||
# browser/fingerprint_protection.py
|
||||
|
||||
"""
|
||||
Schutz vor Browser-Fingerprinting - Erweiterte Methoden zum Schutz vor verschiedenen Fingerprinting-Techniken
|
||||
"""
|
||||
|
||||
import random
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger("fingerprint_protection")
|
||||
|
||||
class FingerprintProtection:
|
||||
"""
|
||||
Bietet erweiterte Schutzmaßnahmen gegen verschiedene Browser-Fingerprinting-Techniken.
|
||||
Kann mit dem PlaywrightManager integriert werden, um die Anonymität zu verbessern.
|
||||
"""
|
||||
|
||||
def __init__(self, context=None, stealth_config=None):
|
||||
"""
|
||||
Initialisiert den Fingerprint-Schutz.
|
||||
|
||||
Args:
|
||||
context: Der Browser-Kontext, auf den die Schutzmaßnahmen angewendet werden sollen
|
||||
stealth_config: Konfigurationseinstellungen für das Stealth-Verhalten
|
||||
"""
|
||||
self.context = context
|
||||
self.stealth_config = stealth_config or {}
|
||||
self.scripts = []
|
||||
self.noise_level = self.stealth_config.get("noise_level", 0.5) # 0.0-1.0
|
||||
|
||||
# Standardwerte für Fingerprinting-Schutz
|
||||
self.defaults = {
|
||||
"webgl_vendor": "Google Inc. (Intel)",
|
||||
"webgl_renderer": "Intel Iris OpenGL Engine",
|
||||
"canvas_noise": True,
|
||||
"audio_noise": True,
|
||||
"webgl_noise": True,
|
||||
"hardware_concurrency": 8,
|
||||
"device_memory": 8,
|
||||
"timezone_id": "Europe/Berlin"
|
||||
}
|
||||
|
||||
# Einstellungen mit benutzerdefinierten Werten überschreiben
|
||||
for key, value in self.stealth_config.items():
|
||||
if key in self.defaults:
|
||||
self.defaults[key] = value
|
||||
|
||||
# Schutzmaßnahmen initialisieren
|
||||
self._init_protections()
|
||||
|
||||
def set_context(self, context):
|
||||
"""Setzt den Browser-Kontext nach der Initialisierung."""
|
||||
self.context = context
|
||||
|
||||
def _init_protections(self):
|
||||
"""Initialisiert alle Fingerprint-Schutzmaßnahmen."""
|
||||
self._init_canvas_protection()
|
||||
self._init_webgl_protection()
|
||||
self._init_audio_protection()
|
||||
self._init_navigator_protection()
|
||||
self._init_misc_protections()
|
||||
|
||||
def _init_canvas_protection(self):
|
||||
"""
|
||||
Initialisiert den Schutz gegen Canvas-Fingerprinting.
|
||||
Dies modifiziert das Canvas-Element, um leicht abweichende Werte zurückzugeben.
|
||||
"""
|
||||
script = """
|
||||
() => {
|
||||
// Originalmethoden speichern
|
||||
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
||||
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
||||
const originalToBlob = HTMLCanvasElement.prototype.toBlob;
|
||||
|
||||
// Funktion zum Hinzufügen von Rauschen zu Bilddaten
|
||||
const addNoise = (data, noise) => {
|
||||
const noiseFactor = noise || 0.03; // Standardwert für das Rauschlevel
|
||||
|
||||
// Nur auf Canvas über Mindestgröße anwenden, um normale Canvas nicht zu stören
|
||||
if (data.width > 16 && data.height > 16) {
|
||||
const pixelCount = data.width * data.height * 4; // RGBA-Kanäle
|
||||
|
||||
// Präzise, aber wenig wahrnehmbare Änderungen
|
||||
const minPixels = Math.max(10, Math.floor(pixelCount * 0.005)); // Mindestens 10 Pixel
|
||||
const maxPixels = Math.min(100, Math.floor(pixelCount * 0.01)); // Höchstens 100 Pixel
|
||||
|
||||
// Zufällige Anzahl an Pixeln auswählen
|
||||
const pixelsToModify = Math.floor(Math.random() * (maxPixels - minPixels)) + minPixels;
|
||||
|
||||
// Pixel modifizieren
|
||||
for (let i = 0; i < pixelsToModify; i++) {
|
||||
// Zufälligen Pixel auswählen
|
||||
const offset = Math.floor(Math.random() * pixelCount / 4) * 4;
|
||||
|
||||
// Nur RGB modifizieren, Alpha-Kanal unverändert lassen
|
||||
for (let j = 0; j < 3; j++) {
|
||||
// Subtile Änderungen hinzufügen (±1-2)
|
||||
const mod = Math.floor(Math.random() * 3) - 1;
|
||||
data.data[offset + j] = Math.max(0, Math.min(255, data.data[offset + j] + mod));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// getImageData überschreiben
|
||||
CanvasRenderingContext2D.prototype.getImageData = function() {
|
||||
const imageData = originalGetImageData.apply(this, arguments);
|
||||
return addNoise(imageData, NOISE_LEVEL);
|
||||
};
|
||||
|
||||
// toDataURL überschreiben
|
||||
HTMLCanvasElement.prototype.toDataURL = function() {
|
||||
// Temporäre Modifikation für getImageData
|
||||
const tempGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
||||
CanvasRenderingContext2D.prototype.getImageData = originalGetImageData;
|
||||
|
||||
// toDataURL aufrufen
|
||||
let dataURL = originalToDataURL.apply(this, arguments);
|
||||
|
||||
// Wenn das Canvas groß genug ist und nicht von kritischen Anwendungen verwendet wird
|
||||
if (this.width > 16 && this.height > 16 && !this.hasAttribute('data-fingerprint-protect-ignore')) {
|
||||
// Rauschen in binären Teil des DataURL einfügen
|
||||
const parts = dataURL.split(',');
|
||||
if (parts.length === 2) {
|
||||
// Binäre Daten dekodieren
|
||||
const binary = atob(parts[1]);
|
||||
// Geringfügige Änderungen bei etwa 0,1% der Bytes
|
||||
let modifiedBinary = '';
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
if (Math.random() < 0.001 * NOISE_LEVEL) {
|
||||
// Byte leicht ändern
|
||||
const byte = binary.charCodeAt(i);
|
||||
const mod = Math.floor(Math.random() * 3) - 1;
|
||||
modifiedBinary += String.fromCharCode(Math.max(0, Math.min(255, byte + mod)));
|
||||
} else {
|
||||
modifiedBinary += binary[i];
|
||||
}
|
||||
}
|
||||
// Binäre Daten wieder kodieren
|
||||
dataURL = parts[0] + ',' + btoa(modifiedBinary);
|
||||
}
|
||||
}
|
||||
|
||||
// getImageData wiederherstellen
|
||||
CanvasRenderingContext2D.prototype.getImageData = tempGetImageData;
|
||||
|
||||
return dataURL;
|
||||
};
|
||||
|
||||
// toBlob überschreiben
|
||||
HTMLCanvasElement.prototype.toBlob = function(callback) {
|
||||
// Original toBlob aufrufen
|
||||
originalToBlob.apply(this, [function(blob) {
|
||||
// Wenn das Canvas groß genug ist und nicht von kritischen Anwendungen verwendet wird
|
||||
if (this.width > 16 && this.height > 16 && !this.hasAttribute('data-fingerprint-protect-ignore')) {
|
||||
// Blob zu ArrayBuffer konvertieren
|
||||
const reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
const arrayBuffer = reader.result;
|
||||
const array = new Uint8Array(arrayBuffer);
|
||||
|
||||
// Geringfügige Änderungen bei etwa 0,1% der Bytes
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (Math.random() < 0.001 * NOISE_LEVEL) {
|
||||
// Byte leicht ändern
|
||||
const mod = Math.floor(Math.random() * 3) - 1;
|
||||
array[i] = Math.max(0, Math.min(255, array[i] + mod));
|
||||
}
|
||||
}
|
||||
|
||||
// Neuen Blob erstellen
|
||||
const modifiedBlob = new Blob([array], {type: blob.type});
|
||||
callback(modifiedBlob);
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
} else {
|
||||
// Unveränderten Blob zurückgeben
|
||||
callback(blob);
|
||||
}
|
||||
}.bind(this)]);
|
||||
};
|
||||
}
|
||||
""".replace("NOISE_LEVEL", str(self.noise_level))
|
||||
|
||||
self.scripts.append(script)
|
||||
|
||||
def _init_webgl_protection(self):
|
||||
"""
|
||||
Initialisiert den Schutz gegen WebGL-Fingerprinting.
|
||||
Dies modifiziert WebGL-spezifische Werte, die für Fingerprinting verwendet werden.
|
||||
"""
|
||||
webgl_vendor = self.defaults["webgl_vendor"]
|
||||
webgl_renderer = self.defaults["webgl_renderer"]
|
||||
|
||||
script = f"""
|
||||
() => {{
|
||||
// WebGL Vendor und Renderer spoofen
|
||||
const getParameterProxies = [
|
||||
WebGLRenderingContext.prototype,
|
||||
WebGL2RenderingContext.prototype
|
||||
];
|
||||
|
||||
getParameterProxies.forEach(contextPrototype => {{
|
||||
if (!contextPrototype) return;
|
||||
|
||||
const originalGetParameter = contextPrototype.getParameter;
|
||||
contextPrototype.getParameter = function(parameter) {{
|
||||
// WebGL Vendor (VENDOR)
|
||||
if (parameter === 0x1F00) {{
|
||||
return "{webgl_vendor}";
|
||||
}}
|
||||
|
||||
// WebGL Renderer (RENDERER)
|
||||
if (parameter === 0x1F01) {{
|
||||
return "{webgl_renderer}";
|
||||
}}
|
||||
|
||||
// Unshaded Language Version (SHADING_LANGUAGE_VERSION)
|
||||
if (parameter === 0x8B8C) {{
|
||||
const originalValue = originalGetParameter.call(this, parameter);
|
||||
|
||||
// Subtile Änderungen an der Version
|
||||
const versionMatch = originalValue.match(/^WebGL GLSL ES ([0-9]\\.[0-9][0-9])/);
|
||||
if (versionMatch) {{
|
||||
return originalValue.replace(versionMatch[1],
|
||||
(parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(2));
|
||||
}}
|
||||
}}
|
||||
|
||||
// VERSION
|
||||
if (parameter === 0x1F02) {{
|
||||
const originalValue = originalGetParameter.call(this, parameter);
|
||||
|
||||
// Subtile Änderungen an der Version
|
||||
const versionMatch = originalValue.match(/^WebGL ([0-9]\\.[0-9])/);
|
||||
if (versionMatch) {{
|
||||
return originalValue.replace(versionMatch[1],
|
||||
(parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(1));
|
||||
}}
|
||||
}}
|
||||
|
||||
return originalGetParameter.apply(this, arguments);
|
||||
}};
|
||||
}});
|
||||
|
||||
// WebGL Vertex und Fragment Shader spoofen
|
||||
const shaderSourceProxies = [
|
||||
WebGLRenderingContext.prototype,
|
||||
WebGL2RenderingContext.prototype
|
||||
];
|
||||
|
||||
shaderSourceProxies.forEach(contextPrototype => {{
|
||||
if (!contextPrototype) return;
|
||||
|
||||
const originalShaderSource = contextPrototype.shaderSource;
|
||||
contextPrototype.shaderSource = function(shader, source) {{
|
||||
// Füge geringfügige Unterschiede in Kommentaren ein, ohne die Funktionalität zu beeinträchtigen
|
||||
if (source.indexOf('//') !== -1) {{
|
||||
// Zufälligen Kommentar leicht modifizieren
|
||||
source = source.replace(/\\/\\/(.*?)\\n/g, (match, comment) => {{
|
||||
if (Math.random() < 0.1) {{
|
||||
// Füge ein Leerzeichen oder einen Bindestrich hinzu oder entferne eines
|
||||
const modifications = [
|
||||
' ', '-', '', ' '
|
||||
];
|
||||
const mod = modifications[Math.floor(Math.random() * modifications.length)];
|
||||
return `//` + comment + mod + `\\n`;
|
||||
}}
|
||||
return match;
|
||||
}});
|
||||
}}
|
||||
|
||||
// Ersetze bestimmte Whitespace-Muster
|
||||
source = source.replace(/\\s{2,}/g, match => {{
|
||||
if (Math.random() < 0.05) {{
|
||||
return ' '.repeat(match.length + (Math.random() < 0.5 ? 1 : -1));
|
||||
}}
|
||||
return match;
|
||||
}});
|
||||
|
||||
return originalShaderSource.call(this, shader, source);
|
||||
}};
|
||||
}});
|
||||
|
||||
// Canvas-Kontext spoofen
|
||||
const getContextProxies = [
|
||||
HTMLCanvasElement.prototype
|
||||
];
|
||||
|
||||
getContextProxies.forEach(canvasPrototype => {{
|
||||
const originalGetContext = canvasPrototype.getContext;
|
||||
canvasPrototype.getContext = function(contextType, contextAttributes) {{
|
||||
const context = originalGetContext.apply(this, arguments);
|
||||
|
||||
if (context && (contextType === 'webgl' || contextType === 'experimental-webgl' || contextType === 'webgl2')) {{
|
||||
// Zufällige Werte für verschiedene Parameter einführen
|
||||
if ({str(self.defaults["webgl_noise"]).lower()}) {{
|
||||
// Zufällige Modifikation für MAX_VERTEX_UNIFORM_VECTORS
|
||||
const MAX_VERTEX_UNIFORM_VECTORS = context.getParameter(context.MAX_VERTEX_UNIFORM_VECTORS);
|
||||
Object.defineProperty(context, 'MAX_VERTEX_UNIFORM_VECTORS', {{
|
||||
get: () => MAX_VERTEX_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1
|
||||
}});
|
||||
|
||||
// Zufällige Modifikation für MAX_FRAGMENT_UNIFORM_VECTORS
|
||||
const MAX_FRAGMENT_UNIFORM_VECTORS = context.getParameter(context.MAX_FRAGMENT_UNIFORM_VECTORS);
|
||||
Object.defineProperty(context, 'MAX_FRAGMENT_UNIFORM_VECTORS', {{
|
||||
get: () => MAX_FRAGMENT_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1
|
||||
}});
|
||||
}}
|
||||
}}
|
||||
|
||||
return context;
|
||||
}};
|
||||
}});
|
||||
}}
|
||||
"""
|
||||
|
||||
self.scripts.append(script)
|
||||
|
||||
def _init_audio_protection(self):
|
||||
"""
|
||||
Initialisiert den Schutz gegen Audio-Fingerprinting.
|
||||
Dies modifiziert die Audio-API-Funktionen, die für Fingerprinting verwendet werden.
|
||||
"""
|
||||
script = f"""
|
||||
() => {{
|
||||
// Audio-Kontext spoofen
|
||||
if (window.AudioContext || window.webkitAudioContext) {{
|
||||
const AudioContextProxy = window.AudioContext || window.webkitAudioContext;
|
||||
const originalAudioContext = AudioContextProxy;
|
||||
|
||||
// AudioContext überschreiben
|
||||
window.AudioContext = window.webkitAudioContext = function() {{
|
||||
const context = new originalAudioContext();
|
||||
|
||||
// createOscillator überschreiben
|
||||
const originalCreateOscillator = context.createOscillator;
|
||||
context.createOscillator = function() {{
|
||||
const oscillator = originalCreateOscillator.apply(this, arguments);
|
||||
|
||||
// Frequenz leicht modifizieren
|
||||
const originalFrequency = oscillator.frequency;
|
||||
Object.defineProperty(oscillator, 'frequency', {{
|
||||
get: function() {{
|
||||
return originalFrequency;
|
||||
}},
|
||||
set: function(value) {{
|
||||
if (typeof value === 'number') {{
|
||||
// Leichte Änderung hinzufügen
|
||||
const noise = (Math.random() * 0.02 - 0.01) * value;
|
||||
originalFrequency.value = value + noise;
|
||||
}} else {{
|
||||
originalFrequency.value = value;
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
|
||||
return oscillator;
|
||||
}};
|
||||
|
||||
// getChannelData überschreiben
|
||||
const originalGetChannelData = context.createBuffer.prototype?.getChannelData || OfflineAudioContext.prototype?.getChannelData;
|
||||
if (originalGetChannelData) {{
|
||||
context.createBuffer.prototype.getChannelData = function(channel) {{
|
||||
const array = originalGetChannelData.call(this, channel);
|
||||
|
||||
if ({str(self.defaults["audio_noise"]).lower()} && this.length > 20) {{
|
||||
// Sehr subtiles Rauschen hinzufügen (bei etwa 0,1% der Samples)
|
||||
const noise = {self.noise_level} * 0.0001;
|
||||
|
||||
// Effiziente Implementierung
|
||||
const samples = Math.min(200, Math.floor(array.length * 0.001));
|
||||
for (let i = 0; i < samples; i++) {{
|
||||
const idx = Math.floor(Math.random() * array.length);
|
||||
array[idx] += (Math.random() * 2 - 1) * noise;
|
||||
}}
|
||||
}}
|
||||
|
||||
return array;
|
||||
}};
|
||||
}}
|
||||
|
||||
// AnalyserNode.getFloatFrequencyData überschreiben
|
||||
if (context.createAnalyser) {{
|
||||
const originalCreateAnalyser = context.createAnalyser;
|
||||
context.createAnalyser = function() {{
|
||||
const analyser = originalCreateAnalyser.apply(this, arguments);
|
||||
const originalGetFloatFrequencyData = analyser.getFloatFrequencyData;
|
||||
|
||||
analyser.getFloatFrequencyData = function(array) {{
|
||||
originalGetFloatFrequencyData.call(this, array);
|
||||
|
||||
if ({str(self.defaults["audio_noise"]).lower()} && array.length > 20) {{
|
||||
// Sehr subtiles Rauschen hinzufügen
|
||||
const noise = {self.noise_level} * 0.001;
|
||||
|
||||
// Effiziente Implementierung für große Arrays
|
||||
const samples = Math.min(20, Math.floor(array.length * 0.01));
|
||||
for (let i = 0; i < samples; i++) {{
|
||||
const idx = Math.floor(Math.random() * array.length);
|
||||
array[idx] += (Math.random() * 2 - 1) * noise;
|
||||
}}
|
||||
}}
|
||||
|
||||
return array;
|
||||
}};
|
||||
|
||||
return analyser;
|
||||
}};
|
||||
}}
|
||||
|
||||
return context;
|
||||
}};
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
self.scripts.append(script)
|
||||
|
||||
def _init_navigator_protection(self):
|
||||
"""
|
||||
Initialisiert den Schutz gegen Navigator-Objekt-Fingerprinting.
|
||||
Dies modifiziert verschiedene Navigator-Eigenschaften, die für Fingerprinting verwendet werden.
|
||||
"""
|
||||
hardware_concurrency = self.defaults["hardware_concurrency"]
|
||||
device_memory = self.defaults["device_memory"]
|
||||
|
||||
script = f"""
|
||||
() => {{
|
||||
// Navigator-Eigenschaften überschreiben
|
||||
|
||||
// hardwareConcurrency (CPU-Kerne)
|
||||
if (navigator.hardwareConcurrency) {{
|
||||
Object.defineProperty(navigator, 'hardwareConcurrency', {{
|
||||
get: () => {hardware_concurrency}
|
||||
}});
|
||||
}}
|
||||
|
||||
// deviceMemory (RAM)
|
||||
if (navigator.deviceMemory) {{
|
||||
Object.defineProperty(navigator, 'deviceMemory', {{
|
||||
get: () => {device_memory}
|
||||
}});
|
||||
}}
|
||||
|
||||
// language und languages
|
||||
if (navigator.language) {{
|
||||
const originalLanguage = navigator.language;
|
||||
const originalLanguages = navigator.languages;
|
||||
|
||||
Object.defineProperty(navigator, 'language', {{
|
||||
get: () => originalLanguage
|
||||
}});
|
||||
|
||||
Object.defineProperty(navigator, 'languages', {{
|
||||
get: () => originalLanguages
|
||||
}});
|
||||
}}
|
||||
|
||||
// userAgent-Konsistenz sicherstellen
|
||||
if (navigator.userAgent) {{
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
// Wenn der userAgent bereits überschrieben wurde, stellen wir
|
||||
// sicher, dass die appVersion und platform konsistent sind
|
||||
const browserInfo = {{
|
||||
chrome: /Chrome\\/(\\d+)/.exec(userAgent),
|
||||
firefox: /Firefox\\/(\\d+)/.exec(userAgent),
|
||||
safari: /Safari\\/(\\d+)/.exec(userAgent),
|
||||
edge: /Edg(e|)\\/(\\d+)/.exec(userAgent)
|
||||
}};
|
||||
|
||||
// Platform basierend auf userAgent bestimmen
|
||||
let platform = '{self.defaults.get("platform", "Win32")}';
|
||||
if (/Windows/.test(userAgent)) platform = 'Win32';
|
||||
else if (/Macintosh/.test(userAgent)) platform = 'MacIntel';
|
||||
else if (/Linux/.test(userAgent)) platform = 'Linux x86_64';
|
||||
else if (/Android/.test(userAgent)) platform = 'Linux armv8l';
|
||||
else if (/iPhone|iPad/.test(userAgent)) platform = 'iPhone';
|
||||
|
||||
Object.defineProperty(navigator, 'platform', {{
|
||||
get: () => platform
|
||||
}});
|
||||
|
||||
// appVersion konsistent machen
|
||||
if (navigator.appVersion) {{
|
||||
Object.defineProperty(navigator, 'appVersion', {{
|
||||
get: () => userAgent.substring(8)
|
||||
}});
|
||||
}}
|
||||
|
||||
// vendor basierend auf Browser setzen
|
||||
let vendor = '{self.defaults.get("vendor", "Google Inc.")}';
|
||||
if (browserInfo.safari) vendor = 'Apple Computer, Inc.';
|
||||
else if (browserInfo.firefox) vendor = '';
|
||||
|
||||
Object.defineProperty(navigator, 'vendor', {{
|
||||
get: () => vendor
|
||||
}});
|
||||
}}
|
||||
|
||||
"""
|
||||
self.scripts.append(script)
|
||||
|
||||
def _init_misc_protections(self):
|
||||
"""
|
||||
Initialisiert verschiedene weitere Schutzmaßnahmen gegen Fingerprinting.
|
||||
"""
|
||||
timezone_id = self.defaults["timezone_id"]
|
||||
|
||||
script = f"""
|
||||
() => {{
|
||||
// Date.prototype.getTimezoneOffset überschreiben
|
||||
const originalGetTimezoneOffset = Date.prototype.getTimezoneOffset;
|
||||
Date.prototype.getTimezoneOffset = function() {{
|
||||
// Zeitzonendaten für '{timezone_id}'
|
||||
// Mitteleuropäische Zeit (CET/CEST): UTC+1 / UTC+2 (Sommerzeit)
|
||||
const date = new Date(this);
|
||||
|
||||
// Prüfen, ob Sommerzeit
|
||||
const jan = new Date(date.getFullYear(), 0, 1);
|
||||
const jul = new Date(date.getFullYear(), 6, 1);
|
||||
const standardOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
||||
|
||||
// Sommerzeit in Europa: Ende März bis Ende Oktober
|
||||
const isDST = date.getMonth() > 2 && date.getMonth() < 10;
|
||||
|
||||
// Offset in Minuten: CET = -60, CEST = -120
|
||||
return isDST ? -120 : -60;
|
||||
}};
|
||||
|
||||
// Plugins und MimeTypes Schutz
|
||||
if (navigator.plugins) {{
|
||||
// Leere oder gefälschte Plugins
|
||||
Object.defineProperty(navigator, 'plugins', {{
|
||||
get: () => {{
|
||||
// Plugins-Array-Eigenschaften simulieren
|
||||
const plugins = {{
|
||||
length: 0,
|
||||
item: () => null,
|
||||
namedItem: () => null,
|
||||
refresh: () => {{}},
|
||||
[Symbol.iterator]: function* () {{}}
|
||||
}};
|
||||
|
||||
return plugins;
|
||||
}}
|
||||
}});
|
||||
|
||||
// MimeTypes ebenfalls leeren
|
||||
if (navigator.mimeTypes) {{
|
||||
Object.defineProperty(navigator, 'mimeTypes', {{
|
||||
get: () => {{
|
||||
// MimeTypes-Array-Eigenschaften simulieren
|
||||
const mimeTypes = {{
|
||||
length: 0,
|
||||
item: () => null,
|
||||
namedItem: () => null,
|
||||
[Symbol.iterator]: function* () {{}}
|
||||
}};
|
||||
|
||||
return mimeTypes;
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
}}
|
||||
|
||||
// Performance.now() und Date.now() - Schutz gegen Timing-Angriffe
|
||||
if (window.performance && performance.now) {{
|
||||
const originalNow = performance.now;
|
||||
|
||||
performance.now = function() {{
|
||||
const value = originalNow.call(performance);
|
||||
// Subtile Abweichung hinzufügen
|
||||
return value + (Math.random() * 0.01);
|
||||
}};
|
||||
}}
|
||||
|
||||
// Date.now() ebenfalls mit subtiler Abweichung
|
||||
const originalDateNow = Date.now;
|
||||
Date.now = function() {{
|
||||
const value = originalDateNow.call(Date);
|
||||
// Subtile Abweichung hinzufügen (±1ms)
|
||||
return value + (Math.random() < 0.5 ? 1 : 0);
|
||||
}};
|
||||
|
||||
// screen-Eigenschaften konsistent machen
|
||||
if (window.screen) {{
|
||||
const originalWidth = screen.width;
|
||||
const originalHeight = screen.height;
|
||||
const originalColorDepth = screen.colorDepth;
|
||||
const originalPixelDepth = screen.pixelDepth;
|
||||
|
||||
// Abweichungen verhindern - konsistente Werte liefern
|
||||
Object.defineProperties(screen, {{
|
||||
'width': {{ get: () => originalWidth }},
|
||||
'height': {{ get: () => originalHeight }},
|
||||
'availWidth': {{ get: () => originalWidth }},
|
||||
'availHeight': {{ get: () => originalHeight - 40 }}, // Taskleiste simulieren
|
||||
'colorDepth': {{ get: () => originalColorDepth }},
|
||||
'pixelDepth': {{ get: () => originalPixelDepth }}
|
||||
}});
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
self.scripts.append(script)
|
||||
|
||||
def apply_to_context(self, context=None):
|
||||
"""
|
||||
Wendet alle Skripte auf den Browser-Kontext an.
|
||||
|
||||
Args:
|
||||
context: Der Browser-Kontext, falls er noch nicht gesetzt wurde
|
||||
"""
|
||||
if context:
|
||||
self.context = context
|
||||
|
||||
if not self.context:
|
||||
logger.warning("Kein Browser-Kontext zum Anwenden der Fingerprint-Schutzmaßnahmen")
|
||||
return
|
||||
|
||||
for script in self.scripts:
|
||||
self.context.add_init_script(script)
|
||||
|
||||
logger.info(f"Fingerprint-Schutzmaßnahmen auf Browser-Kontext angewendet ({len(self.scripts)} Skripte)")
|
||||
|
||||
def get_fingerprint_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den aktuellen Status der Fingerprint-Schutzmaßnahmen zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Status der Fingerprint-Schutzmaßnahmen
|
||||
"""
|
||||
status = {
|
||||
"active": self.context is not None,
|
||||
"script_count": len(self.scripts),
|
||||
"protections": {
|
||||
"canvas": self.defaults["canvas_noise"],
|
||||
"webgl": self.defaults["webgl_noise"],
|
||||
"audio": self.defaults["audio_noise"],
|
||||
"navigator": True,
|
||||
"battery": "getBattery" in self.scripts[-1],
|
||||
"timing": "performance.now" in self.scripts[-1]
|
||||
},
|
||||
"noise_level": self.noise_level,
|
||||
"custom_values": {
|
||||
"webgl_vendor": self.defaults["webgl_vendor"],
|
||||
"webgl_renderer": self.defaults["webgl_renderer"],
|
||||
"hardware_concurrency": self.defaults["hardware_concurrency"],
|
||||
"device_memory": self.defaults["device_memory"],
|
||||
"timezone_id": self.defaults["timezone_id"]
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
|
||||
def rotate_fingerprint(self, noise_level: Optional[float] = None):
|
||||
"""
|
||||
Rotiert den Fingerprint durch Neugenerierung der Schutzmaßnahmen.
|
||||
|
||||
Args:
|
||||
noise_level: Optionales neues Rauschniveau (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if noise_level is not None:
|
||||
self.noise_level = max(0.0, min(1.0, noise_level))
|
||||
|
||||
try:
|
||||
# Skripte zurücksetzen
|
||||
self.scripts = []
|
||||
|
||||
# Neues WebGL-Vendor/Renderer-Paar generieren
|
||||
webgl_vendors = [
|
||||
"Google Inc.",
|
||||
"Google Inc. (Intel)",
|
||||
"Google Inc. (NVIDIA)",
|
||||
"Google Inc. (AMD)",
|
||||
"Intel Inc.",
|
||||
"NVIDIA Corporation",
|
||||
"AMD"
|
||||
]
|
||||
|
||||
webgl_renderers = [
|
||||
"ANGLE (Intel, Intel(R) HD Graphics 620 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"Intel Iris OpenGL Engine",
|
||||
"NVIDIA GeForce GTX 980 OpenGL Engine",
|
||||
"AMD Radeon Pro 560 OpenGL Engine",
|
||||
"Mesa DRI Intel(R) UHD Graphics 620 (KBL GT2)",
|
||||
"Mesa DRI NVIDIA GeForce GTX 1650"
|
||||
]
|
||||
|
||||
# Zufällige Werte wählen
|
||||
self.defaults["webgl_vendor"] = random.choice(webgl_vendors)
|
||||
self.defaults["webgl_renderer"] = random.choice(webgl_renderers)
|
||||
|
||||
# Hardware-Concurrency und Device-Memory variieren
|
||||
self.defaults["hardware_concurrency"] = random.choice([2, 4, 6, 8, 12, 16])
|
||||
self.defaults["device_memory"] = random.choice([2, 4, 8, 16])
|
||||
|
||||
# Schutzmaßnahmen neu initialisieren
|
||||
self._init_protections()
|
||||
|
||||
# Auf Kontext anwenden, falls vorhanden
|
||||
if self.context:
|
||||
self.apply_to_context()
|
||||
|
||||
logger.info(f"Fingerprint erfolgreich rotiert (Noise-Level: {self.noise_level:.2f})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Rotation des Fingerprints: {e}")
|
||||
return False
|
||||
127
browser/playwright_extensions.py
Normale Datei
@ -0,0 +1,127 @@
|
||||
# browser/playwright_extensions.py
|
||||
|
||||
"""
|
||||
Erweiterungen für den PlaywrightManager - Fügt zusätzliche Funktionalität hinzu
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from browser.fingerprint_protection import FingerprintProtection
|
||||
|
||||
logger = logging.getLogger("playwright_extensions")
|
||||
|
||||
class PlaywrightExtensions:
|
||||
"""
|
||||
Erweiterungsklasse für den PlaywrightManager.
|
||||
Bietet zusätzliche Funktionalität, ohne die Hauptklasse zu verändern.
|
||||
"""
|
||||
|
||||
def __init__(self, playwright_manager):
|
||||
"""
|
||||
Initialisiert die Erweiterungsklasse.
|
||||
|
||||
Args:
|
||||
playwright_manager: Eine Instanz des PlaywrightManager
|
||||
"""
|
||||
self.playwright_manager = playwright_manager
|
||||
self.fingerprint_protection = None
|
||||
self.enhanced_stealth_enabled = False
|
||||
|
||||
def enable_enhanced_fingerprint_protection(self, config: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""
|
||||
Aktiviert den erweiterten Fingerprint-Schutz.
|
||||
|
||||
Args:
|
||||
config: Optionale Konfiguration für den Fingerprint-Schutz
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Sicherstellen, dass der Browser gestartet wurde
|
||||
if not hasattr(self.playwright_manager, 'context') or self.playwright_manager.context is None:
|
||||
logger.warning("Browser muss zuerst gestartet werden, bevor der Fingerprint-Schutz aktiviert werden kann")
|
||||
return False
|
||||
|
||||
# Basis-Stealth-Konfiguration aus dem PlaywrightManager verwenden
|
||||
stealth_config = getattr(self.playwright_manager, 'stealth_config', {})
|
||||
|
||||
# Mit der benutzerdefinierten Konfiguration erweitern, falls vorhanden
|
||||
if config:
|
||||
stealth_config.update(config)
|
||||
|
||||
# Fingerprint-Schutz initialisieren
|
||||
self.fingerprint_protection = FingerprintProtection(
|
||||
context=self.playwright_manager.context,
|
||||
stealth_config=stealth_config
|
||||
)
|
||||
|
||||
# Schutzmaßnahmen auf den Kontext anwenden
|
||||
self.fingerprint_protection.apply_to_context()
|
||||
|
||||
# Status aktualisieren
|
||||
self.enhanced_stealth_enabled = True
|
||||
|
||||
logger.info("Erweiterter Fingerprint-Schutz aktiviert")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktivieren des erweiterten Fingerprint-Schutzes: {e}")
|
||||
return False
|
||||
|
||||
def rotate_fingerprint(self, noise_level: Optional[float] = None) -> bool:
|
||||
"""
|
||||
Rotiert den Browser-Fingerprint.
|
||||
|
||||
Args:
|
||||
noise_level: Optionales neues Rauschniveau (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if not self.enhanced_stealth_enabled or self.fingerprint_protection is None:
|
||||
logger.warning("Erweiterter Fingerprint-Schutz ist nicht aktiviert")
|
||||
return False
|
||||
|
||||
return self.fingerprint_protection.rotate_fingerprint(noise_level)
|
||||
|
||||
def get_fingerprint_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gibt den aktuellen Status des Fingerprint-Schutzes zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Status des Fingerprint-Schutzes
|
||||
"""
|
||||
if not self.enhanced_stealth_enabled or self.fingerprint_protection is None:
|
||||
return {"active": False, "message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert"}
|
||||
|
||||
return self.fingerprint_protection.get_fingerprint_status()
|
||||
|
||||
def hook_into_playwright_manager(self) -> None:
|
||||
"""
|
||||
Hängt die Erweiterungsmethoden an den PlaywrightManager.
|
||||
"""
|
||||
if not self.playwright_manager:
|
||||
logger.error("Kein PlaywrightManager zum Anhängen der Erweiterungen")
|
||||
return
|
||||
|
||||
# Originalstart-Methode speichern
|
||||
original_start = self.playwright_manager.start
|
||||
|
||||
# Die start-Methode überschreiben, um den Fingerprint-Schutz automatisch zu aktivieren
|
||||
def enhanced_start(*args, **kwargs):
|
||||
result = original_start(*args, **kwargs)
|
||||
|
||||
# Wenn start erfolgreich war und erweiterter Schutz aktiviert ist,
|
||||
# wenden wir den Fingerprint-Schutz auf den neuen Kontext an
|
||||
if result and self.enhanced_stealth_enabled and self.fingerprint_protection:
|
||||
self.fingerprint_protection.set_context(self.playwright_manager.context)
|
||||
self.fingerprint_protection.apply_to_context()
|
||||
|
||||
return result
|
||||
|
||||
# Methoden dynamisch zum PlaywrightManager hinzufügen
|
||||
self.playwright_manager.enable_enhanced_fingerprint_protection = self.enable_enhanced_fingerprint_protection
|
||||
self.playwright_manager.rotate_fingerprint = self.rotate_fingerprint
|
||||
self.playwright_manager.get_fingerprint_status = self.get_fingerprint_status
|
||||
self.playwright_manager.start = enhanced_start
|
||||
517
browser/playwright_manager.py
Normale Datei
@ -0,0 +1,517 @@
|
||||
"""
|
||||
Playwright Manager - Hauptklasse für die Browser-Steuerung mit Anti-Bot-Erkennung
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List, Any, Tuple
|
||||
from playwright.sync_api import sync_playwright, Browser, Page, BrowserContext, ElementHandle
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("playwright_manager")
|
||||
|
||||
class PlaywrightManager:
|
||||
"""
|
||||
Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
headless: bool = False,
|
||||
proxy: Optional[Dict[str, str]] = None,
|
||||
browser_type: str = "chromium",
|
||||
user_agent: Optional[str] = None,
|
||||
screenshots_dir: str = "screenshots",
|
||||
slowmo: int = 0):
|
||||
"""
|
||||
Initialisiert den PlaywrightManager.
|
||||
|
||||
Args:
|
||||
headless: Ob der Browser im Headless-Modus ausgeführt werden soll
|
||||
proxy: Proxy-Konfiguration (z.B. {'server': 'http://myproxy.com:3128', 'username': 'user', 'password': 'pass'})
|
||||
browser_type: Welcher Browser-Typ verwendet werden soll ("chromium", "firefox", oder "webkit")
|
||||
user_agent: Benutzerdefinierter User-Agent
|
||||
screenshots_dir: Verzeichnis für Screenshots
|
||||
slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging)
|
||||
"""
|
||||
self.headless = headless
|
||||
self.proxy = proxy
|
||||
self.browser_type = browser_type
|
||||
self.user_agent = user_agent
|
||||
self.screenshots_dir = screenshots_dir
|
||||
self.slowmo = slowmo
|
||||
|
||||
# Stelle sicher, dass das Screenshots-Verzeichnis existiert
|
||||
os.makedirs(self.screenshots_dir, exist_ok=True)
|
||||
|
||||
# Playwright-Instanzen
|
||||
self.playwright = None
|
||||
self.browser = None
|
||||
self.context = None
|
||||
self.page = None
|
||||
|
||||
# Zähler für Wiederhholungsversuche
|
||||
self.retry_counter = {}
|
||||
|
||||
# Lade Stealth-Konfigurationen
|
||||
self.stealth_config = self._load_stealth_config()
|
||||
|
||||
def _load_stealth_config(self) -> Dict[str, Any]:
|
||||
"""Lädt die Stealth-Konfigurationen aus der Datei oder verwendet Standardwerte."""
|
||||
try:
|
||||
config_dir = Path(__file__).parent.parent / "config"
|
||||
stealth_config_path = config_dir / "stealth_config.json"
|
||||
|
||||
if stealth_config_path.exists():
|
||||
with open(stealth_config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Stealth-Konfiguration nicht laden: {e}")
|
||||
|
||||
# Verwende Standardwerte, wenn das Laden fehlschlägt
|
||||
return {
|
||||
"vendor": "Google Inc.",
|
||||
"platform": "Win32",
|
||||
"webdriver": False,
|
||||
"accept_language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"timezone_id": "Europe/Berlin",
|
||||
"fingerprint_noise": True,
|
||||
"device_scale_factor": 1.0,
|
||||
}
|
||||
|
||||
def start(self) -> Page:
|
||||
"""
|
||||
Startet die Playwright-Sitzung und gibt die Browser-Seite zurück.
|
||||
|
||||
Returns:
|
||||
Page: Die Browser-Seite
|
||||
"""
|
||||
if self.page is not None:
|
||||
return self.page
|
||||
|
||||
try:
|
||||
self.playwright = sync_playwright().start()
|
||||
|
||||
# Wähle den Browser-Typ
|
||||
if self.browser_type == "firefox":
|
||||
browser_instance = self.playwright.firefox
|
||||
elif self.browser_type == "webkit":
|
||||
browser_instance = self.playwright.webkit
|
||||
else:
|
||||
browser_instance = self.playwright.chromium
|
||||
|
||||
# Browser-Startoptionen
|
||||
browser_args = []
|
||||
|
||||
if self.browser_type == "chromium":
|
||||
# Chrome-spezifische Argumente für Anti-Bot-Erkennung
|
||||
browser_args.extend([
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--disable-features=IsolateOrigins,site-per-process',
|
||||
'--disable-site-isolation-trials',
|
||||
])
|
||||
|
||||
# Browser starten
|
||||
self.browser = browser_instance.launch(
|
||||
headless=self.headless,
|
||||
args=browser_args,
|
||||
slow_mo=self.slowmo
|
||||
)
|
||||
|
||||
# Kontext-Optionen für Stealth-Modus
|
||||
context_options = {
|
||||
"viewport": {"width": 1920, "height": 1080},
|
||||
"device_scale_factor": self.stealth_config.get("device_scale_factor", 1.0),
|
||||
"locale": "de-DE",
|
||||
"timezone_id": self.stealth_config.get("timezone_id", "Europe/Berlin"),
|
||||
"accept_downloads": True,
|
||||
}
|
||||
|
||||
# User-Agent setzen
|
||||
if self.user_agent:
|
||||
context_options["user_agent"] = self.user_agent
|
||||
|
||||
# Proxy-Einstellungen, falls vorhanden
|
||||
if self.proxy:
|
||||
context_options["proxy"] = self.proxy
|
||||
|
||||
# Browserkontext erstellen
|
||||
self.context = self.browser.new_context(**context_options)
|
||||
|
||||
# JavaScript-Fingerprinting-Schutz
|
||||
self._apply_stealth_scripts()
|
||||
|
||||
# Neue Seite erstellen
|
||||
self.page = self.context.new_page()
|
||||
|
||||
# Event-Listener für Konsolen-Logs
|
||||
self.page.on("console", lambda msg: logger.debug(f"BROWSER CONSOLE: {msg.text}"))
|
||||
|
||||
return self.page
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Starten des Browsers: {e}")
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def _apply_stealth_scripts(self):
|
||||
"""Wendet JavaScript-Skripte an, um Browser-Fingerprinting zu umgehen."""
|
||||
# Diese Skripte überschreiben Eigenschaften, die für Bot-Erkennung verwendet werden
|
||||
scripts = [
|
||||
# WebDriver-Eigenschaft überschreiben
|
||||
"""
|
||||
() => {
|
||||
Object.defineProperty(navigator, 'webdriver', {
|
||||
get: () => false,
|
||||
});
|
||||
}
|
||||
""",
|
||||
|
||||
# Navigator-Eigenschaften überschreiben
|
||||
f"""
|
||||
() => {{
|
||||
const newProto = navigator.__proto__;
|
||||
delete newProto.webdriver;
|
||||
navigator.__proto__ = newProto;
|
||||
|
||||
Object.defineProperty(navigator, 'platform', {{
|
||||
get: () => '{self.stealth_config.get("platform", "Win32")}'
|
||||
}});
|
||||
|
||||
Object.defineProperty(navigator, 'languages', {{
|
||||
get: () => ['de-DE', 'de', 'en-US', 'en']
|
||||
}});
|
||||
|
||||
Object.defineProperty(navigator, 'vendor', {{
|
||||
get: () => '{self.stealth_config.get("vendor", "Google Inc.")}'
|
||||
}});
|
||||
}}
|
||||
""",
|
||||
|
||||
# Chrome-Objekte hinzufügen, die in normalen Browsern vorhanden sind
|
||||
"""
|
||||
() => {
|
||||
// Fügt chrome.runtime hinzu, falls nicht vorhanden
|
||||
if (!window.chrome) {
|
||||
window.chrome = {};
|
||||
}
|
||||
if (!window.chrome.runtime) {
|
||||
window.chrome.runtime = {};
|
||||
window.chrome.runtime.sendMessage = function() {};
|
||||
}
|
||||
}
|
||||
""",
|
||||
|
||||
# Plugin-Fingerprinting
|
||||
"""
|
||||
() => {
|
||||
const originalQuery = window.navigator.permissions.query;
|
||||
window.navigator.permissions.query = (parameters) => (
|
||||
parameters.name === 'notifications' ?
|
||||
Promise.resolve({ state: Notification.permission }) :
|
||||
originalQuery(parameters)
|
||||
);
|
||||
}
|
||||
"""
|
||||
]
|
||||
|
||||
# Wenn Fingerprint-Noise aktiviert ist, füge zufällige Variationen hinzu
|
||||
if self.stealth_config.get("fingerprint_noise", True):
|
||||
scripts.append("""
|
||||
() => {
|
||||
// Canvas-Fingerprinting leicht verändern
|
||||
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
||||
HTMLCanvasElement.prototype.toDataURL = function(type) {
|
||||
const result = originalToDataURL.apply(this, arguments);
|
||||
|
||||
if (this.width > 16 && this.height > 16) {
|
||||
// Kleines Rauschen in Pixels einfügen
|
||||
const context = this.getContext('2d');
|
||||
const imageData = context.getImageData(0, 0, 2, 2);
|
||||
const pixelArray = imageData.data;
|
||||
|
||||
// Ändere einen zufälligen Pixel leicht
|
||||
const randomPixel = Math.floor(Math.random() * pixelArray.length / 4) * 4;
|
||||
pixelArray[randomPixel] = (pixelArray[randomPixel] + Math.floor(Math.random() * 10)) % 256;
|
||||
|
||||
context.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
""")
|
||||
|
||||
# Skripte auf den Browser-Kontext anwenden
|
||||
for script in scripts:
|
||||
self.context.add_init_script(script)
|
||||
|
||||
def navigate_to(self, url: str, wait_until: str = "networkidle", timeout: int = 30000) -> bool:
|
||||
"""
|
||||
Navigiert zu einer bestimmten URL und wartet, bis die Seite geladen ist.
|
||||
|
||||
Args:
|
||||
url: Die Ziel-URL
|
||||
wait_until: Wann die Navigation als abgeschlossen gilt ("load", "domcontentloaded", "networkidle")
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei erfolgreicher Navigation, False sonst
|
||||
"""
|
||||
if self.page is None:
|
||||
self.start()
|
||||
|
||||
try:
|
||||
logger.info(f"Navigiere zu: {url}")
|
||||
self.page.goto(url, wait_until=wait_until, timeout=timeout)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Navigation zu {url}: {e}")
|
||||
self.take_screenshot(f"navigation_error_{int(time.time())}")
|
||||
return False
|
||||
|
||||
def wait_for_selector(self, selector: str, timeout: int = 30000) -> Optional[ElementHandle]:
|
||||
"""
|
||||
Wartet auf ein Element mit dem angegebenen Selektor.
|
||||
|
||||
Args:
|
||||
selector: CSS- oder XPath-Selektor
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
Optional[ElementHandle]: Das Element oder None, wenn nicht gefunden
|
||||
"""
|
||||
if self.page is None:
|
||||
raise ValueError("Browser nicht gestartet. Rufe zuerst start() auf.")
|
||||
|
||||
try:
|
||||
element = self.page.wait_for_selector(selector, timeout=timeout)
|
||||
return element
|
||||
except Exception as e:
|
||||
logger.warning(f"Element nicht gefunden: {selector} - {e}")
|
||||
return None
|
||||
|
||||
def fill_form_field(self, selector: str, value: str, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Füllt ein Formularfeld aus.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Feld
|
||||
value: Einzugebender Wert
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Auf Element warten
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
|
||||
# Element fokussieren
|
||||
element.focus()
|
||||
time.sleep(random.uniform(0.1, 0.3))
|
||||
|
||||
# Vorhandenen Text löschen (optional)
|
||||
current_value = element.evaluate("el => el.value")
|
||||
if current_value:
|
||||
element.fill("")
|
||||
time.sleep(random.uniform(0.1, 0.2))
|
||||
|
||||
# Text menschenähnlich eingeben
|
||||
for char in value:
|
||||
element.type(char, delay=random.uniform(20, 100))
|
||||
time.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
logger.info(f"Feld {selector} gefüllt mit: {value}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen von {selector}: {e}")
|
||||
key = f"fill_{selector}"
|
||||
return self._retry_action(key, lambda: self.fill_form_field(selector, value, timeout))
|
||||
|
||||
def click_element(self, selector: str, force: bool = False, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Klickt auf ein Element.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Element
|
||||
force: Force-Click verwenden
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Auf Element warten
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
|
||||
# Scroll zum Element
|
||||
self.page.evaluate("element => element.scrollIntoView({ behavior: 'smooth', block: 'center' })", element)
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
|
||||
# Menschenähnliches Verhalten - leichte Verzögerung vor dem Klick
|
||||
time.sleep(random.uniform(0.2, 0.5))
|
||||
|
||||
# Element klicken
|
||||
element.click(force=force, delay=random.uniform(20, 100))
|
||||
|
||||
logger.info(f"Element geklickt: {selector}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken auf {selector}: {e}")
|
||||
key = f"click_{selector}"
|
||||
return self._retry_action(key, lambda: self.click_element(selector, force, timeout))
|
||||
|
||||
def select_option(self, selector: str, value: str, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Wählt eine Option aus einem Dropdown-Menü.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Dropdown
|
||||
value: Wert oder sichtbarer Text der Option
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Auf Element warten
|
||||
element = self.wait_for_selector(selector, timeout)
|
||||
if not element:
|
||||
return False
|
||||
|
||||
# Option auswählen
|
||||
self.page.select_option(selector, value=value)
|
||||
|
||||
logger.info(f"Option '{value}' ausgewählt in {selector}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Auswahl von '{value}' in {selector}: {e}")
|
||||
key = f"select_{selector}"
|
||||
return self._retry_action(key, lambda: self.select_option(selector, value, timeout))
|
||||
|
||||
def is_element_visible(self, selector: str, timeout: int = 5000) -> bool:
|
||||
"""
|
||||
Prüft, ob ein Element sichtbar ist.
|
||||
|
||||
Args:
|
||||
selector: Selektor für das Element
|
||||
timeout: Timeout in Millisekunden
|
||||
|
||||
Returns:
|
||||
bool: True wenn sichtbar, False sonst
|
||||
"""
|
||||
try:
|
||||
element = self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
return element is not None
|
||||
except:
|
||||
return False
|
||||
|
||||
def take_screenshot(self, name: str = None) -> str:
|
||||
"""
|
||||
Erstellt einen Screenshot der aktuellen Seite.
|
||||
|
||||
Args:
|
||||
name: Name für den Screenshot (ohne Dateierweiterung)
|
||||
|
||||
Returns:
|
||||
str: Pfad zum erstellten Screenshot
|
||||
"""
|
||||
if self.page is None:
|
||||
raise ValueError("Browser nicht gestartet. Rufe zuerst start() auf.")
|
||||
|
||||
timestamp = int(time.time())
|
||||
filename = f"{name}_{timestamp}.png" if name else f"screenshot_{timestamp}.png"
|
||||
path = os.path.join(self.screenshots_dir, filename)
|
||||
|
||||
self.page.screenshot(path=path, full_page=True)
|
||||
logger.info(f"Screenshot erstellt: {path}")
|
||||
return path
|
||||
|
||||
def _retry_action(self, key: str, action_func, max_retries: int = 3) -> bool:
|
||||
"""
|
||||
Wiederholt eine Aktion bei Fehler.
|
||||
|
||||
Args:
|
||||
key: Eindeutiger Schlüssel für die Aktion
|
||||
action_func: Funktion, die ausgeführt werden soll
|
||||
max_retries: Maximale Anzahl der Wiederholungen
|
||||
|
||||
Returns:
|
||||
bool: Ergebnis der Aktion
|
||||
"""
|
||||
if key not in self.retry_counter:
|
||||
self.retry_counter[key] = 0
|
||||
|
||||
self.retry_counter[key] += 1
|
||||
|
||||
if self.retry_counter[key] <= max_retries:
|
||||
logger.info(f"Wiederhole Aktion {key} (Versuch {self.retry_counter[key]} von {max_retries})")
|
||||
time.sleep(random.uniform(0.5, 1.0))
|
||||
return action_func()
|
||||
else:
|
||||
logger.warning(f"Maximale Anzahl von Wiederholungen für {key} erreicht")
|
||||
self.retry_counter[key] = 0
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""Schließt den Browser und gibt Ressourcen frei."""
|
||||
try:
|
||||
if self.page:
|
||||
self.page.close()
|
||||
self.page = None
|
||||
|
||||
if self.context:
|
||||
self.context.close()
|
||||
self.context = None
|
||||
|
||||
if self.browser:
|
||||
self.browser.close()
|
||||
self.browser = None
|
||||
|
||||
if self.playwright:
|
||||
self.playwright.stop()
|
||||
self.playwright = None
|
||||
|
||||
logger.info("Browser-Sitzung geschlossen")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Schließen des Browsers: {e}")
|
||||
|
||||
def __enter__(self):
|
||||
"""Kontext-Manager-Eintritt."""
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Kontext-Manager-Austritt."""
|
||||
self.close()
|
||||
|
||||
|
||||
# Beispielnutzung, wenn direkt ausgeführt
|
||||
if __name__ == "__main__":
|
||||
# Konfiguriere Logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Beispiel für einen Proxy (ohne Anmeldedaten)
|
||||
proxy_config = {
|
||||
"server": "http://example-proxy.com:8080"
|
||||
}
|
||||
|
||||
# Browser starten und zu einer Seite navigieren
|
||||
with PlaywrightManager(headless=False) as manager:
|
||||
manager.navigate_to("https://www.instagram.com")
|
||||
time.sleep(5) # Kurze Pause zum Anzeigen der Seite
|
||||
216
browser/stealth_config.py
Normale Datei
@ -0,0 +1,216 @@
|
||||
"""
|
||||
Stealth-Konfiguration für Playwright - Anti-Bot-Erkennung
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("stealth_config")
|
||||
|
||||
class StealthConfig:
|
||||
"""
|
||||
Konfiguriert Anti-Bot-Erkennungs-Einstellungen für Playwright.
|
||||
Generiert und verwaltet verschiedene Fingerprint-Einstellungen.
|
||||
"""
|
||||
|
||||
# Standardwerte für User-Agents
|
||||
CHROME_DESKTOP_AGENTS = [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||
]
|
||||
|
||||
MOBILE_AGENTS = [
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/135.0.0.0 Mobile/15E148 Safari/604.1"
|
||||
]
|
||||
|
||||
# Plattformen
|
||||
PLATFORMS = {
|
||||
"windows": "Win32",
|
||||
"macos": "MacIntel",
|
||||
"linux": "Linux x86_64",
|
||||
"android": "Linux armv8l",
|
||||
"ios": "iPhone"
|
||||
}
|
||||
|
||||
# Browser-Sprachen
|
||||
LANGUAGES = [
|
||||
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"de-DE,de;q=0.9,en;q=0.8",
|
||||
"de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"en-US,en;q=0.9,de;q=0.8"
|
||||
]
|
||||
|
||||
# Zeitzone für Deutschland
|
||||
TIMEZONE_ID = "Europe/Berlin"
|
||||
|
||||
def __init__(self, config_dir: str = None):
|
||||
"""
|
||||
Initialisiert die Stealth-Konfiguration.
|
||||
|
||||
Args:
|
||||
config_dir: Verzeichnis für Konfigurationsdateien
|
||||
"""
|
||||
self.config_dir = config_dir or os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config")
|
||||
os.makedirs(self.config_dir, exist_ok=True)
|
||||
|
||||
self.config_path = os.path.join(self.config_dir, "stealth_config.json")
|
||||
|
||||
# Lade benutzerdefinierte User-Agents, falls vorhanden
|
||||
self.user_agents = self._load_user_agents()
|
||||
|
||||
# Lade gespeicherte Konfiguration oder erstelle eine neue
|
||||
self.config = self._load_or_create_config()
|
||||
|
||||
def _load_user_agents(self) -> Dict[str, List[str]]:
|
||||
"""Lädt benutzerdefinierte User-Agents aus der Konfigurationsdatei."""
|
||||
user_agents_path = os.path.join(self.config_dir, "user_agents.json")
|
||||
|
||||
if os.path.exists(user_agents_path):
|
||||
try:
|
||||
with open(user_agents_path, 'r', encoding='utf-8') as f:
|
||||
agents = json.load(f)
|
||||
|
||||
if isinstance(agents, dict) and "desktop" in agents and "mobile" in agents:
|
||||
return agents
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Laden von user_agents.json: {e}")
|
||||
|
||||
# Standardwerte zurückgeben
|
||||
return {
|
||||
"desktop": self.CHROME_DESKTOP_AGENTS,
|
||||
"mobile": self.MOBILE_AGENTS
|
||||
}
|
||||
|
||||
def _load_or_create_config(self) -> Dict[str, Any]:
|
||||
"""Lädt die Konfiguration oder erstellt eine neue, falls keine existiert."""
|
||||
if os.path.exists(self.config_path):
|
||||
try:
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
logger.info("Stealth-Konfiguration geladen")
|
||||
return config
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Stealth-Konfiguration nicht laden: {e}")
|
||||
|
||||
# Erstelle eine neue Konfiguration
|
||||
config = self.generate_config()
|
||||
self.save_config(config)
|
||||
return config
|
||||
|
||||
def generate_config(self, device_type: str = "desktop") -> Dict[str, Any]:
|
||||
"""
|
||||
Generiert eine neue Stealth-Konfiguration.
|
||||
|
||||
Args:
|
||||
device_type: "desktop" oder "mobile"
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Die generierte Konfiguration
|
||||
"""
|
||||
# Wähle Plattform und entsprechenden User-Agent
|
||||
if device_type == "mobile":
|
||||
platform_name = random.choice(["android", "ios"])
|
||||
user_agent = random.choice(self.user_agents["mobile"])
|
||||
else:
|
||||
# Wähle eine Plattform, die zum System passt
|
||||
system = platform.system().lower()
|
||||
if system == "darwin":
|
||||
platform_name = "macos"
|
||||
elif system == "windows":
|
||||
platform_name = "windows"
|
||||
else:
|
||||
platform_name = "linux"
|
||||
|
||||
user_agent = random.choice(self.user_agents["desktop"])
|
||||
|
||||
platform_value = self.PLATFORMS.get(platform_name, "Win32")
|
||||
|
||||
# Wähle weitere Konfigurationen
|
||||
config = {
|
||||
"user_agent": user_agent,
|
||||
"platform": platform_value,
|
||||
"vendor": "Google Inc." if "Chrome" in user_agent else "Apple Computer, Inc.",
|
||||
"accept_language": random.choice(self.LANGUAGES),
|
||||
"timezone_id": self.TIMEZONE_ID,
|
||||
"device_scale_factor": random.choice([1.0, 1.25, 1.5, 2.0]) if random.random() < 0.3 else 1.0,
|
||||
"color_depth": random.choice([24, 30, 48]),
|
||||
"hardware_concurrency": random.choice([2, 4, 8, 12, 16]),
|
||||
"device_memory": random.choice([2, 4, 8, 16]),
|
||||
"webdriver": False,
|
||||
"fingerprint_noise": True,
|
||||
"device_type": device_type
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
def save_config(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Speichert die Konfiguration in einer Datei.
|
||||
|
||||
Args:
|
||||
config: Die zu speichernde Konfiguration
|
||||
"""
|
||||
try:
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
logger.info(f"Stealth-Konfiguration gespeichert in: {self.config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Speichern der Stealth-Konfiguration: {e}")
|
||||
|
||||
def get_config(self) -> Dict[str, Any]:
|
||||
"""Gibt die aktuelle Konfiguration zurück."""
|
||||
return self.config
|
||||
|
||||
def rotate_config(self, device_type: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Generiert eine neue Konfiguration und speichert sie.
|
||||
|
||||
Args:
|
||||
device_type: "desktop" oder "mobile", oder None für bestehenden Typ
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Die neue Konfiguration
|
||||
"""
|
||||
if device_type is None:
|
||||
device_type = self.config.get("device_type", "desktop")
|
||||
|
||||
self.config = self.generate_config(device_type)
|
||||
self.save_config(self.config)
|
||||
return self.config
|
||||
|
||||
def get_user_agent(self) -> str:
|
||||
"""Gibt den aktuellen User-Agent aus der Konfiguration zurück."""
|
||||
return self.config.get("user_agent", self.CHROME_DESKTOP_AGENTS[0])
|
||||
|
||||
|
||||
# Beispielnutzung, wenn direkt ausgeführt
|
||||
if __name__ == "__main__":
|
||||
# Konfiguriere Logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Beispiel für Konfigurationserstellung
|
||||
stealth = StealthConfig()
|
||||
|
||||
print("Aktuelle Konfiguration:")
|
||||
print(json.dumps(stealth.get_config(), indent=2))
|
||||
|
||||
print("\nNeue Desktop-Konfiguration:")
|
||||
desktop_config = stealth.rotate_config("desktop")
|
||||
print(json.dumps(desktop_config, indent=2))
|
||||
|
||||
print("\nNeue Mobile-Konfiguration:")
|
||||
mobile_config = stealth.rotate_config("mobile")
|
||||
print(json.dumps(mobile_config, indent=2))
|
||||
1
config/.machine_id
Normale Datei
@ -0,0 +1 @@
|
||||
ae30d891-0b45-408e-8f47-75fada7cb094
|
||||
0
config/__init__.py
Normale Datei
7
config/app_version.json
Normale Datei
@ -0,0 +1,7 @@
|
||||
{
|
||||
"current_version": "1.0.0",
|
||||
"last_check": "2025-06-22T17:43:13.744626",
|
||||
"channel": "stable",
|
||||
"auto_check": true,
|
||||
"auto_download": false
|
||||
}
|
||||
0
config/browser_config.json
Normale Datei
6
config/email_config.json
Normale Datei
@ -0,0 +1,6 @@
|
||||
{
|
||||
"imap_server": "imap.ionos.de",
|
||||
"imap_port": 993,
|
||||
"imap_user": "info@z5m7q9dk3ah2v1plx6ju.com",
|
||||
"imap_pass": "cz&ie.O9$!:!tYY@"
|
||||
}
|
||||
0
config/facebook_config.json
Normale Datei
0
config/instagram_config.json
Normale Datei
10
config/license.json
Normale Datei
@ -0,0 +1,10 @@
|
||||
{
|
||||
"key": "",
|
||||
"activation_date": "",
|
||||
"expiry_date": "",
|
||||
"status": "inactive",
|
||||
"status_text": "Keine Lizenz aktiviert",
|
||||
"features": [],
|
||||
"last_online_check": "",
|
||||
"signature": ""
|
||||
}
|
||||
9
config/license_config.json
Normale Datei
@ -0,0 +1,9 @@
|
||||
{
|
||||
"key": "",
|
||||
"status": "inactive",
|
||||
"hardware_id": "",
|
||||
"activation_date": null,
|
||||
"expiry_date": null,
|
||||
"features": [],
|
||||
"last_check": null
|
||||
}
|
||||
15
config/proxy_config.json
Normale Datei
@ -0,0 +1,15 @@
|
||||
{
|
||||
"ipv4": [
|
||||
"85.254.81.222:44444:14a38ed2efe94:04ed25fb1b"
|
||||
],
|
||||
"ipv6": [
|
||||
"92.119.89.251:30015:14a4622431481:a488401704"
|
||||
],
|
||||
"mobile": [
|
||||
"de1.4g.iproyal.com:7296:1rtSh0G:XswBCIqi1joy5dX"
|
||||
],
|
||||
"mobile_api": {
|
||||
"marsproxies": "9zKXWpMEA1",
|
||||
"iproyal": ""
|
||||
}
|
||||
}
|
||||
14
config/stealth_config.json
Normale Datei
@ -0,0 +1,14 @@
|
||||
{
|
||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||
"platform": "Win32",
|
||||
"vendor": "Google Inc.",
|
||||
"accept_language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"timezone_id": "Europe/Berlin",
|
||||
"device_scale_factor": 1.0,
|
||||
"color_depth": 24,
|
||||
"hardware_concurrency": 8,
|
||||
"device_memory": 8,
|
||||
"webdriver": false,
|
||||
"fingerprint_noise": true,
|
||||
"device_type": "desktop"
|
||||
}
|
||||
46
config/theme.json
Normale Datei
@ -0,0 +1,46 @@
|
||||
# Path: config/theme.json
|
||||
|
||||
{
|
||||
"dark": {
|
||||
"name": "Dark",
|
||||
"palette": {
|
||||
"Window": "#1E1E1E",
|
||||
"WindowText": "#FFFFFF",
|
||||
"Base": "#2D2D30",
|
||||
"AlternateBase": "#252526",
|
||||
"ToolTipBase": "#2D2D30",
|
||||
"ToolTipText": "#FFFFFF",
|
||||
"Text": "#FFFFFF",
|
||||
"Button": "#0E639C",
|
||||
"ButtonText": "#FFFFFF",
|
||||
"BrightText": "#FF0000",
|
||||
"Link": "#3794FF",
|
||||
"Highlight": "#264F78",
|
||||
"HighlightedText": "#FFFFFF"
|
||||
},
|
||||
"icons": {
|
||||
"path_suffix": "dark"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"name": "Light",
|
||||
"palette": {
|
||||
"Window": "#FFFFFF",
|
||||
"WindowText": "#1E1E1E",
|
||||
"Base": "#F5F5F5",
|
||||
"AlternateBase": "#E5E5E5",
|
||||
"ToolTipBase": "#F5F5F5",
|
||||
"ToolTipText": "#1E1E1E",
|
||||
"Text": "#1E1E1E",
|
||||
"Button": "#0078D7",
|
||||
"ButtonText": "#FFFFFF",
|
||||
"BrightText": "#FF0000",
|
||||
"Link": "#0066CC",
|
||||
"Highlight": "#CCE8FF",
|
||||
"HighlightedText": "#1E1E1E"
|
||||
},
|
||||
"icons": {
|
||||
"path_suffix": "light"
|
||||
}
|
||||
}
|
||||
}
|
||||
0
config/tiktok_config.json
Normale Datei
0
config/twitter_config.json
Normale Datei
9
config/update_config.json
Normale Datei
@ -0,0 +1,9 @@
|
||||
{
|
||||
"last_check": "2025-04-01 12:00:00",
|
||||
"check_interval": 86400,
|
||||
"auto_check": true,
|
||||
"auto_download": false,
|
||||
"update_channel": "stable",
|
||||
"download_path": "updates",
|
||||
"downloaded_updates": []
|
||||
}
|
||||
31
config/user_agents.json
Normale Datei
@ -0,0 +1,31 @@
|
||||
{
|
||||
"desktop": [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0"
|
||||
],
|
||||
"mobile": [
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/135.0.0.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/134.0.0.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/123.0 Mobile/15E148 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Linux; Android 14; SM-S9180) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 14; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 14; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A536B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/133.0.0.0 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A546B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/133.0.0.0 Mobile Safari/537.36"
|
||||
]
|
||||
}
|
||||
BIN
controllers/__pycache__/account_controller.cpython-310.pyc
Normale Datei
BIN
controllers/__pycache__/account_controller.cpython-313.pyc
Normale Datei
BIN
controllers/__pycache__/main_controller.cpython-310.pyc
Normale Datei
BIN
controllers/__pycache__/main_controller.cpython-313.pyc
Normale Datei
BIN
controllers/__pycache__/settings_controller.cpython-310.pyc
Normale Datei
BIN
controllers/__pycache__/settings_controller.cpython-313.pyc
Normale Datei
149
controllers/account_controller.py
Normale Datei
@ -0,0 +1,149 @@
|
||||
"""
|
||||
Controller für die Verwaltung von Accounts.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
logger = logging.getLogger("account_controller")
|
||||
|
||||
class AccountController(QObject):
|
||||
"""Controller für die Verwaltung von Accounts."""
|
||||
|
||||
def __init__(self, db_manager):
|
||||
super().__init__()
|
||||
self.db_manager = db_manager
|
||||
self.parent_view = None
|
||||
|
||||
def set_parent_view(self, view):
|
||||
"""Setzt die übergeordnete View für Dialoge."""
|
||||
self.parent_view = view
|
||||
|
||||
def on_account_created(self, platform: str, account_data: dict):
|
||||
"""Wird aufgerufen, wenn ein Account erstellt wurde."""
|
||||
account = {
|
||||
"platform": platform.lower(),
|
||||
"username": account_data.get("username", ""),
|
||||
"password": account_data.get("password", ""),
|
||||
"email": account_data.get("email", ""),
|
||||
"phone": account_data.get("phone", ""),
|
||||
"full_name": account_data.get("full_name", ""),
|
||||
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
self.db_manager.add_account(account)
|
||||
logger.info(f"Account in Datenbank gespeichert: {account['username']}")
|
||||
|
||||
# Erfolgsmeldung anzeigen
|
||||
if self.parent_view:
|
||||
QMessageBox.information(
|
||||
self.parent_view,
|
||||
"Erfolg",
|
||||
f"Account erfolgreich erstellt!\n\nBenutzername: {account['username']}\nPasswort: {account['password']}\nE-Mail/Telefon: {account['email'] or account['phone']}"
|
||||
)
|
||||
|
||||
def load_accounts(self, platform=None):
|
||||
"""Lädt Accounts aus der Datenbank."""
|
||||
try:
|
||||
if platform and hasattr(self.db_manager, "get_accounts_by_platform"):
|
||||
accounts = self.db_manager.get_accounts_by_platform(platform.lower())
|
||||
else:
|
||||
accounts = self.db_manager.get_all_accounts()
|
||||
if platform:
|
||||
accounts = [acc for acc in accounts if acc.get("platform", "").lower() == platform.lower()]
|
||||
|
||||
return accounts
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Accounts: {e}")
|
||||
if self.parent_view:
|
||||
QMessageBox.critical(
|
||||
self.parent_view,
|
||||
"Fehler",
|
||||
f"Fehler beim Laden der Accounts:\n{str(e)}"
|
||||
)
|
||||
return []
|
||||
|
||||
def export_accounts(self, platform=None):
|
||||
"""Exportiert Accounts in eine CSV-Datei."""
|
||||
parent = self.parent_view or None
|
||||
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
parent,
|
||||
"Konten exportieren",
|
||||
"",
|
||||
"CSV-Dateien (*.csv);;Alle Dateien (*)"
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
try:
|
||||
# Accounts laden
|
||||
accounts = self.load_accounts(platform)
|
||||
|
||||
with open(file_path, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
|
||||
# Header
|
||||
writer.writerow([
|
||||
"ID", "Plattform", "Benutzername", "Passwort",
|
||||
"E-Mail", "Telefon", "Name", "Erstellt am"
|
||||
])
|
||||
|
||||
# Daten
|
||||
for account in accounts:
|
||||
writer.writerow([
|
||||
account.get("id", ""),
|
||||
account.get("platform", ""),
|
||||
account.get("username", ""),
|
||||
account.get("password", ""),
|
||||
account.get("email", ""),
|
||||
account.get("phone", ""),
|
||||
account.get("full_name", ""),
|
||||
account.get("created_at", "")
|
||||
])
|
||||
|
||||
logger.info(f"Accounts erfolgreich nach {file_path} exportiert")
|
||||
|
||||
if parent:
|
||||
QMessageBox.information(
|
||||
parent,
|
||||
"Export erfolgreich",
|
||||
f"Konten wurden erfolgreich nach {file_path} exportiert."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Exportieren der Accounts: {e}")
|
||||
if parent:
|
||||
QMessageBox.critical(
|
||||
parent,
|
||||
"Export fehlgeschlagen",
|
||||
f"Fehler beim Exportieren der Konten:\n{str(e)}"
|
||||
)
|
||||
|
||||
def delete_account(self, account_id):
|
||||
"""Löscht einen Account aus der Datenbank."""
|
||||
try:
|
||||
success = self.db_manager.delete_account(account_id)
|
||||
|
||||
if not success:
|
||||
if self.parent_view:
|
||||
QMessageBox.critical(
|
||||
self.parent_view,
|
||||
"Fehler",
|
||||
f"Konto mit ID {account_id} konnte nicht gelöscht werden."
|
||||
)
|
||||
|
||||
return success
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Löschen des Accounts: {e}")
|
||||
if self.parent_view:
|
||||
QMessageBox.critical(
|
||||
self.parent_view,
|
||||
"Fehler",
|
||||
f"Fehler beim Löschen des Kontos:\n{str(e)}"
|
||||
)
|
||||
return False
|
||||
231
controllers/main_controller.py
Normale Datei
@ -0,0 +1,231 @@
|
||||
"""
|
||||
Hauptcontroller für die Social Media Account Generator Anwendung.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from PyQt5.QtWidgets import QMessageBox, QApplication
|
||||
|
||||
from views.main_window import MainWindow
|
||||
from controllers.platform_controllers.instagram_controller import InstagramController
|
||||
from controllers.platform_controllers.tiktok_controller import TikTokController
|
||||
from controllers.account_controller import AccountController
|
||||
from controllers.settings_controller import SettingsController
|
||||
|
||||
from database.db_manager import DatabaseManager
|
||||
from utils.proxy_rotator import ProxyRotator
|
||||
from utils.email_handler import EmailHandler
|
||||
from utils.theme_manager import ThemeManager
|
||||
from localization.language_manager import LanguageManager
|
||||
from licensing.license_manager import LicenseManager
|
||||
from updates.update_checker import UpdateChecker
|
||||
|
||||
logger = logging.getLogger("main")
|
||||
|
||||
class MainController:
|
||||
"""Hauptcontroller, der die Anwendung koordiniert."""
|
||||
|
||||
def __init__(self, app):
|
||||
# QApplication Referenz speichern
|
||||
self.app = app
|
||||
|
||||
# Theme Manager initialisieren
|
||||
self.theme_manager = ThemeManager(app)
|
||||
|
||||
# Language Manager initialisieren
|
||||
self.language_manager = LanguageManager(app)
|
||||
|
||||
# Modelle initialisieren
|
||||
self.db_manager = DatabaseManager()
|
||||
self.proxy_rotator = ProxyRotator()
|
||||
self.email_handler = EmailHandler()
|
||||
self.license_manager = LicenseManager()
|
||||
self.update_checker = UpdateChecker()
|
||||
|
||||
# Haupt-View erstellen
|
||||
self.view = MainWindow(self.theme_manager, self.language_manager, self.db_manager)
|
||||
|
||||
# Untercontroller erstellen
|
||||
self.account_controller = AccountController(self.db_manager)
|
||||
self.settings_controller = SettingsController(
|
||||
self.proxy_rotator,
|
||||
self.email_handler,
|
||||
self.license_manager
|
||||
)
|
||||
|
||||
# Plattform-Controller initialisieren
|
||||
self.platform_controllers = {}
|
||||
|
||||
# Instagram Controller hinzufügen
|
||||
self.platform_controllers["instagram"] = InstagramController(
|
||||
self.db_manager,
|
||||
self.proxy_rotator,
|
||||
self.email_handler,
|
||||
self.language_manager
|
||||
)
|
||||
|
||||
# TikTok Controller hinzufügen
|
||||
self.platform_controllers["tiktok"] = TikTokController(
|
||||
self.db_manager,
|
||||
self.proxy_rotator,
|
||||
self.email_handler,
|
||||
self.language_manager
|
||||
)
|
||||
|
||||
# Hier können in Zukunft weitere Controller hinzugefügt werden:
|
||||
# self.platform_controllers["facebook"] = FacebookController(...)
|
||||
# self.platform_controllers["twitter"] = TwitterController(...)
|
||||
# self.platform_controllers["tiktok"] = TikTokController(...)
|
||||
|
||||
# Signals verbinden
|
||||
self.connect_signals()
|
||||
|
||||
# Lizenz überprüfen
|
||||
self.check_license()
|
||||
|
||||
# Auf Updates prüfen
|
||||
self.check_for_updates()
|
||||
|
||||
# Hauptfenster anzeigen
|
||||
self.view.show()
|
||||
|
||||
def connect_signals(self):
|
||||
"""Verbindet alle Signale mit den entsprechenden Slots."""
|
||||
# Plattformauswahl-Signal verbinden
|
||||
self.view.platform_selected.connect(self.on_platform_selected)
|
||||
|
||||
# Zurück-Button verbinden
|
||||
self.view.back_to_selector_requested.connect(self.show_platform_selector)
|
||||
|
||||
# Theme-Toggle verbinden
|
||||
self.view.theme_toggled.connect(self.on_theme_toggled)
|
||||
|
||||
def on_platform_selected(self, platform: str):
|
||||
"""Wird aufgerufen, wenn eine Plattform ausgewählt wird."""
|
||||
logger.info(f"Plattform ausgewählt: {platform}")
|
||||
|
||||
# Aktuelle Plattform setzen
|
||||
self.current_platform = platform.lower()
|
||||
|
||||
# Prüfen, ob die Plattform unterstützt wird
|
||||
if self.current_platform not in self.platform_controllers:
|
||||
logger.error(f"Plattform '{platform}' wird nicht unterstützt")
|
||||
QMessageBox.critical(
|
||||
self.view,
|
||||
"Nicht unterstützt",
|
||||
f"Die Plattform '{platform}' ist noch nicht implementiert."
|
||||
)
|
||||
return
|
||||
|
||||
# Plattformspezifischen Controller abrufen
|
||||
platform_controller = self.platform_controllers.get(self.current_platform)
|
||||
|
||||
# Plattform-View initialisieren
|
||||
self.view.init_platform_ui(platform, platform_controller)
|
||||
|
||||
# Tab-Hooks verbinden
|
||||
self.connect_tab_hooks(platform_controller)
|
||||
|
||||
# Plattformspezifische Ansicht anzeigen
|
||||
self.view.show_platform_ui()
|
||||
|
||||
def on_theme_toggled(self):
|
||||
"""Wird aufgerufen, wenn das Theme gewechselt wird."""
|
||||
if self.theme_manager:
|
||||
theme_name = self.theme_manager.get_current_theme()
|
||||
logger.info(f"Theme gewechselt zu: {theme_name}")
|
||||
|
||||
# Hier kann zusätzliche Logik für Theme-Wechsel hinzugefügt werden
|
||||
# z.B. UI-Elemente aktualisieren, die nicht automatisch aktualisiert werden
|
||||
|
||||
def connect_tab_hooks(self, platform_controller):
|
||||
"""Verbindet die Tab-Hooks mit dem Plattform-Controller."""
|
||||
# Generator-Tab-Hooks
|
||||
if hasattr(platform_controller, "get_generator_tab"):
|
||||
generator_tab = platform_controller.get_generator_tab()
|
||||
generator_tab.account_created.connect(self.account_controller.on_account_created)
|
||||
|
||||
# Einstellungen-Tab-Hooks
|
||||
if hasattr(platform_controller, "get_settings_tab"):
|
||||
settings_tab = platform_controller.get_settings_tab()
|
||||
settings_tab.proxy_settings_saved.connect(self.settings_controller.save_proxy_settings)
|
||||
settings_tab.proxy_tested.connect(self.settings_controller.test_proxy)
|
||||
settings_tab.email_settings_saved.connect(self.settings_controller.save_email_settings)
|
||||
settings_tab.email_tested.connect(self.settings_controller.test_email)
|
||||
settings_tab.license_activated.connect(self.settings_controller.activate_license)
|
||||
|
||||
def show_platform_selector(self):
|
||||
"""Zeigt den Plattform-Selektor an."""
|
||||
logger.info("Zurück zur Plattformauswahl")
|
||||
self.view.show_platform_selector()
|
||||
if hasattr(self.view, "platform_selector"):
|
||||
self.view.platform_selector.load_accounts()
|
||||
|
||||
def check_license(self):
|
||||
"""Überprüft, ob eine gültige Lizenz vorhanden ist."""
|
||||
is_licensed = self.license_manager.is_licensed()
|
||||
|
||||
if not is_licensed:
|
||||
license_info = self.license_manager.get_license_info()
|
||||
status = license_info.get("status_text", "Inaktiv")
|
||||
|
||||
# Wenn keine Lizenz vorhanden ist, zeigen wir eine Warnung an
|
||||
QMessageBox.warning(
|
||||
self.view,
|
||||
"Keine gültige Lizenz",
|
||||
f"Status: {status}\n\nBitte aktivieren Sie eine Lizenz, um die Software zu nutzen."
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def check_for_updates(self):
|
||||
"""Prüft auf Updates."""
|
||||
try:
|
||||
update_info = self.update_checker.check_for_updates()
|
||||
|
||||
if update_info["has_update"]:
|
||||
reply = QMessageBox.question(
|
||||
self.view,
|
||||
"Update verfügbar",
|
||||
f"Eine neue Version ist verfügbar: {update_info['latest_version']}\n"
|
||||
f"(Aktuelle Version: {update_info['current_version']})\n\n"
|
||||
f"Release-Datum: {update_info['release_date']}\n"
|
||||
f"Release-Notes:\n{update_info['release_notes']}\n\n"
|
||||
"Möchten Sie das Update jetzt herunterladen?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.Yes
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
self.download_update(update_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Update-Prüfung: {e}")
|
||||
|
||||
def download_update(self, update_info):
|
||||
"""Lädt ein Update herunter."""
|
||||
try:
|
||||
download_result = self.update_checker.download_update(
|
||||
update_info["download_url"],
|
||||
update_info["latest_version"]
|
||||
)
|
||||
|
||||
if download_result["success"]:
|
||||
QMessageBox.information(
|
||||
self.view,
|
||||
"Download erfolgreich",
|
||||
f"Update wurde heruntergeladen: {download_result['file_path']}\n\n"
|
||||
"Bitte schließen Sie die Anwendung und führen Sie das Update aus."
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self.view,
|
||||
"Download fehlgeschlagen",
|
||||
f"Fehler beim Herunterladen des Updates:\n{download_result['error']}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Herunterladen des Updates: {e}")
|
||||
QMessageBox.critical(
|
||||
self.view,
|
||||
"Fehler",
|
||||
f"Fehler beim Herunterladen des Updates:\n{str(e)}"
|
||||
)
|
||||
132
controllers/platform_controllers/base_controller.py
Normale Datei
@ -0,0 +1,132 @@
|
||||
"""
|
||||
Basis-Controller für Plattform-spezifische Funktionalität.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
from views.tabs.generator_tab import GeneratorTab
|
||||
from views.tabs.accounts_tab import AccountsTab
|
||||
from views.tabs.settings_tab import SettingsTab
|
||||
|
||||
class BasePlatformController(QObject):
|
||||
"""Basis-Controller-Klasse für Plattformspezifische Logik."""
|
||||
|
||||
def __init__(self, platform_name, db_manager, proxy_rotator, email_handler, language_manager=None):
|
||||
super().__init__()
|
||||
self.platform_name = platform_name
|
||||
self.logger = logging.getLogger(f"{platform_name.lower()}_controller")
|
||||
|
||||
# Modelle
|
||||
self.db_manager = db_manager
|
||||
self.proxy_rotator = proxy_rotator
|
||||
self.email_handler = email_handler
|
||||
self.language_manager = language_manager
|
||||
|
||||
# Tabs
|
||||
self._generator_tab = None
|
||||
self._accounts_tab = None
|
||||
self._settings_tab = None
|
||||
|
||||
# Plattformspezifische Initialisierungen
|
||||
self.init_platform()
|
||||
|
||||
def init_platform(self):
|
||||
"""
|
||||
Initialisiert plattformspezifische Komponenten.
|
||||
Diese Methode sollte von Unterklassen überschrieben werden.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_generator_tab(self):
|
||||
"""Gibt den Generator-Tab zurück oder erstellt ihn bei Bedarf."""
|
||||
if not self._generator_tab:
|
||||
self._generator_tab = self.create_generator_tab()
|
||||
return self._generator_tab
|
||||
|
||||
def get_accounts_tab(self):
|
||||
"""Gibt den Accounts-Tab zurück oder erstellt ihn bei Bedarf."""
|
||||
if not self._accounts_tab:
|
||||
self._accounts_tab = self.create_accounts_tab()
|
||||
return self._accounts_tab
|
||||
|
||||
def get_settings_tab(self):
|
||||
"""Gibt den Settings-Tab zurück oder erstellt ihn bei Bedarf."""
|
||||
if not self._settings_tab:
|
||||
self._settings_tab = self.create_settings_tab()
|
||||
return self._settings_tab
|
||||
|
||||
|
||||
def create_generator_tab(self):
|
||||
"""
|
||||
Erstellt den Generator-Tab.
|
||||
Diese Methode sollte von Unterklassen überschrieben werden.
|
||||
"""
|
||||
return GeneratorTab(self.platform_name, self.language_manager)
|
||||
|
||||
def create_accounts_tab(self):
|
||||
"""
|
||||
Erstellt den Accounts-Tab.
|
||||
Diese Methode sollte von Unterklassen überschrieben werden.
|
||||
"""
|
||||
return AccountsTab(self.platform_name, self.db_manager, self.language_manager)
|
||||
|
||||
def create_settings_tab(self):
|
||||
"""
|
||||
Erstellt den Settings-Tab.
|
||||
Diese Methode sollte von Unterklassen überschrieben werden.
|
||||
"""
|
||||
return SettingsTab(
|
||||
self.platform_name,
|
||||
self.proxy_rotator,
|
||||
self.email_handler,
|
||||
self.language_manager
|
||||
)
|
||||
|
||||
|
||||
def start_account_creation(self, params):
|
||||
"""
|
||||
Startet die Account-Erstellung.
|
||||
Diese Methode sollte von Unterklassen überschrieben werden.
|
||||
|
||||
Args:
|
||||
params: Parameter für die Account-Erstellung
|
||||
"""
|
||||
self.logger.info(f"Account-Erstellung für {self.platform_name} gestartet")
|
||||
# In Unterklassen implementieren
|
||||
|
||||
def validate_inputs(self, inputs):
|
||||
"""
|
||||
Validiert die Eingaben für die Account-Erstellung.
|
||||
|
||||
Args:
|
||||
inputs: Eingaben für die Account-Erstellung
|
||||
|
||||
Returns:
|
||||
(bool, str): (Ist gültig, Fehlermeldung falls nicht gültig)
|
||||
"""
|
||||
# Basis-Validierungen
|
||||
if not inputs.get("full_name"):
|
||||
return False, "Bitte geben Sie einen vollständigen Namen ein."
|
||||
|
||||
# Alter prüfen
|
||||
age_text = inputs.get("age_text", "")
|
||||
if not age_text:
|
||||
return False, "Bitte geben Sie ein Alter ein."
|
||||
|
||||
# Alter muss eine Zahl sein
|
||||
try:
|
||||
age = int(age_text)
|
||||
inputs["age"] = age # Füge das konvertierte Alter zu den Parametern hinzu
|
||||
except ValueError:
|
||||
return False, "Das Alter muss eine ganze Zahl sein."
|
||||
|
||||
# Alter-Bereich prüfen
|
||||
if age < 13 or age > 99:
|
||||
return False, "Das Alter muss zwischen 13 und 99 liegen."
|
||||
|
||||
# Telefonnummer prüfen, falls erforderlich
|
||||
if inputs.get("registration_method") == "phone" and not inputs.get("phone_number"):
|
||||
return False, "Telefonnummer erforderlich für Registrierung via Telefon."
|
||||
|
||||
return True, ""
|
||||
275
controllers/platform_controllers/instagram_controller.py
Normale Datei
@ -0,0 +1,275 @@
|
||||
"""
|
||||
Controller für Instagram-spezifische Funktionalität.
|
||||
Mit TextSimilarity-Integration für robusteres UI-Element-Matching.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QObject
|
||||
|
||||
from controllers.platform_controllers.base_controller import BasePlatformController
|
||||
from views.tabs.generator_tab import GeneratorTab
|
||||
from views.tabs.accounts_tab import AccountsTab
|
||||
from views.tabs.settings_tab import SettingsTab
|
||||
|
||||
from social_networks.instagram.instagram_automation import InstagramAutomation
|
||||
from utils.text_similarity import TextSimilarity
|
||||
|
||||
logger = logging.getLogger("instagram_controller")
|
||||
|
||||
class InstagramWorkerThread(QThread):
|
||||
"""Thread für die Instagram-Account-Erstellung."""
|
||||
|
||||
# Signale
|
||||
update_signal = pyqtSignal(str)
|
||||
log_signal = pyqtSignal(str)
|
||||
progress_signal = pyqtSignal(int)
|
||||
finished_signal = pyqtSignal(dict)
|
||||
error_signal = pyqtSignal(str)
|
||||
|
||||
def __init__(self, params):
|
||||
super().__init__()
|
||||
self.params = params
|
||||
self.running = True
|
||||
|
||||
# TextSimilarity für robustes Fehler-Matching
|
||||
self.text_similarity = TextSimilarity(default_threshold=0.7)
|
||||
|
||||
# Fehler-Patterns für robustes Fehler-Matching
|
||||
self.error_patterns = [
|
||||
"Fehler", "Error", "Fehlgeschlagen", "Failed", "Problem", "Issue",
|
||||
"Nicht möglich", "Not possible", "Bitte versuchen Sie es erneut",
|
||||
"Please try again", "Konnte nicht", "Could not", "Timeout"
|
||||
]
|
||||
|
||||
def run(self):
|
||||
"""Führt die Account-Erstellung aus."""
|
||||
try:
|
||||
self.log_signal.emit("Instagram-Account-Erstellung gestartet...")
|
||||
self.progress_signal.emit(10)
|
||||
|
||||
# Instagram-Automation initialisieren
|
||||
automation = InstagramAutomation(
|
||||
headless=self.params.get("headless", False),
|
||||
use_proxy=self.params.get("use_proxy", False),
|
||||
proxy_type=self.params.get("proxy_type"),
|
||||
save_screenshots=True,
|
||||
debug=self.params.get("debug", False),
|
||||
email_domain=self.params.get("email_domain", "z5m7q9dk3ah2v1plx6ju.com")
|
||||
)
|
||||
|
||||
self.update_signal.emit("Instagram-Automation initialisiert")
|
||||
self.progress_signal.emit(20)
|
||||
|
||||
# Account registrieren
|
||||
self.log_signal.emit(f"Registriere Account für: {self.params['full_name']}")
|
||||
|
||||
registration_method = self.params.get("registration_method", "email")
|
||||
phone_number = self.params.get("phone_number")
|
||||
|
||||
# Account registrieren
|
||||
result = automation.register_account(
|
||||
full_name=self.params["full_name"],
|
||||
age=self.params["age"],
|
||||
registration_method=registration_method,
|
||||
phone_number=phone_number,
|
||||
**self.params.get("additional_params", {})
|
||||
)
|
||||
|
||||
self.progress_signal.emit(100)
|
||||
|
||||
if result["success"]:
|
||||
self.log_signal.emit("Account erfolgreich erstellt!")
|
||||
self.finished_signal.emit(result)
|
||||
else:
|
||||
# Robuste Fehlerbehandlung mit TextSimilarity
|
||||
error_msg = result.get("error", "Unbekannter Fehler")
|
||||
|
||||
# Versuche, Fehler nutzerfreundlicher zu interpretieren
|
||||
user_friendly_error = self._interpret_error(error_msg)
|
||||
|
||||
self.log_signal.emit(f"Fehler bei der Account-Erstellung: {user_friendly_error}")
|
||||
self.error_signal.emit(user_friendly_error)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im Worker-Thread: {e}")
|
||||
self.log_signal.emit(f"Schwerwiegender Fehler: {str(e)}")
|
||||
self.error_signal.emit(str(e))
|
||||
self.progress_signal.emit(0)
|
||||
|
||||
def _interpret_error(self, error_msg: str) -> str:
|
||||
"""
|
||||
Interpretiert Fehlermeldungen und gibt eine benutzerfreundlichere Version zurück.
|
||||
Verwendet TextSimilarity für robusteres Fehler-Matching.
|
||||
|
||||
Args:
|
||||
error_msg: Die ursprüngliche Fehlermeldung
|
||||
|
||||
Returns:
|
||||
str: Benutzerfreundliche Fehlermeldung
|
||||
"""
|
||||
# Bekannte Fehlermuster und deren Interpretationen
|
||||
error_interpretations = {
|
||||
"captcha": "Instagram hat einen Captcha-Test angefordert. Versuchen Sie es später erneut oder nutzen Sie einen anderen Proxy.",
|
||||
"verification": "Es gab ein Problem mit der Verifizierung des Accounts. Bitte prüfen Sie die E-Mail-Einstellungen.",
|
||||
"proxy": "Problem mit der Proxy-Verbindung. Bitte prüfen Sie Ihre Proxy-Einstellungen.",
|
||||
"timeout": "Zeitüberschreitung bei der Verbindung. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||
"username": "Der gewählte Benutzername ist bereits vergeben oder nicht zulässig.",
|
||||
"password": "Das Passwort erfüllt nicht die Anforderungen von Instagram.",
|
||||
"email": "Die E-Mail-Adresse konnte nicht verwendet werden. Bitte nutzen Sie eine andere E-Mail-Domain.",
|
||||
"phone": "Die Telefonnummer konnte nicht für die Registrierung verwendet werden."
|
||||
}
|
||||
|
||||
# Versuche, den Fehler zu kategorisieren
|
||||
for pattern, interpretation in error_interpretations.items():
|
||||
for error_term in self.error_patterns:
|
||||
if (pattern in error_msg.lower() or
|
||||
self.text_similarity.is_similar(error_term, error_msg, threshold=0.7)):
|
||||
return interpretation
|
||||
|
||||
# Fallback: Originale Fehlermeldung zurückgeben
|
||||
return error_msg
|
||||
|
||||
def stop(self):
|
||||
"""Stoppt den Thread."""
|
||||
self.running = False
|
||||
self.terminate()
|
||||
|
||||
class InstagramController(BasePlatformController):
|
||||
"""Controller für Instagram-spezifische Funktionalität."""
|
||||
|
||||
def __init__(self, db_manager, proxy_rotator, email_handler, language_manager=None):
|
||||
super().__init__("Instagram", db_manager, proxy_rotator, email_handler, language_manager)
|
||||
self.worker_thread = None
|
||||
|
||||
# TextSimilarity für robustes UI-Element-Matching
|
||||
self.text_similarity = TextSimilarity(default_threshold=0.75)
|
||||
|
||||
def create_generator_tab(self):
|
||||
"""Erstellt den Instagram-Generator-Tab."""
|
||||
generator_tab = GeneratorTab(self.platform_name, self.language_manager)
|
||||
|
||||
# Instagram-spezifische Anpassungen
|
||||
# Diese Methode überschreiben, wenn spezifische Anpassungen benötigt werden
|
||||
|
||||
# Signale verbinden
|
||||
generator_tab.start_requested.connect(self.start_account_creation)
|
||||
generator_tab.stop_requested.connect(self.stop_account_creation)
|
||||
|
||||
return generator_tab
|
||||
|
||||
def start_account_creation(self, params):
|
||||
"""Startet die Instagram-Account-Erstellung."""
|
||||
super().start_account_creation(params)
|
||||
|
||||
# Validiere Eingaben
|
||||
is_valid, error_msg = self.validate_inputs(params)
|
||||
if not is_valid:
|
||||
self.get_generator_tab().show_error(error_msg)
|
||||
return
|
||||
|
||||
# UI aktualisieren
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(True)
|
||||
generator_tab.clear_log()
|
||||
generator_tab.set_progress(0)
|
||||
|
||||
# Worker-Thread starten
|
||||
self.worker_thread = InstagramWorkerThread(params)
|
||||
self.worker_thread.update_signal.connect(lambda msg: generator_tab.set_status(msg))
|
||||
self.worker_thread.log_signal.connect(lambda msg: generator_tab.add_log(msg))
|
||||
self.worker_thread.error_signal.connect(lambda msg: (generator_tab.show_error(msg), generator_tab.set_running(False)))
|
||||
self.worker_thread.finished_signal.connect(lambda result: self.handle_account_created(result))
|
||||
self.worker_thread.progress_signal.connect(lambda value: generator_tab.set_progress(value))
|
||||
|
||||
self.worker_thread.start()
|
||||
|
||||
def stop_account_creation(self):
|
||||
"""Stoppt die Instagram-Account-Erstellung."""
|
||||
if self.worker_thread and self.worker_thread.isRunning():
|
||||
self.worker_thread.stop()
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.add_log("Account-Erstellung wurde abgebrochen")
|
||||
generator_tab.set_running(False)
|
||||
generator_tab.set_progress(0)
|
||||
|
||||
def handle_account_created(self, result):
|
||||
"""Verarbeitet erfolgreich erstellte Accounts."""
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(False)
|
||||
|
||||
# Account-Daten aus dem Ergebnis holen
|
||||
account_data = result.get("account_data", {})
|
||||
|
||||
# Account-Erfolgsereignis auslösen
|
||||
generator_tab.account_created.emit(self.platform_name, account_data)
|
||||
|
||||
# Account in der Datenbank speichern
|
||||
self.save_account_to_db(account_data)
|
||||
|
||||
def save_account_to_db(self, account_data):
|
||||
"""Speichert einen erstellten Account in der Datenbank."""
|
||||
account = {
|
||||
"platform": self.platform_name.lower(),
|
||||
"username": account_data.get("username", ""),
|
||||
"password": account_data.get("password", ""),
|
||||
"email": account_data.get("email", ""),
|
||||
"phone": account_data.get("phone", ""),
|
||||
"full_name": account_data.get("full_name", ""),
|
||||
"created_at": time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
self.db_manager.add_account(account)
|
||||
logger.info(f"Account in Datenbank gespeichert: {account['username']}")
|
||||
|
||||
def validate_inputs(self, inputs):
|
||||
"""
|
||||
Validiert die Eingaben für die Account-Erstellung.
|
||||
Verwendet TextSimilarity für robustere Validierung.
|
||||
"""
|
||||
# Basis-Validierungen von BasePlatformController verwenden
|
||||
valid, error_msg = super().validate_inputs(inputs)
|
||||
if not valid:
|
||||
return valid, error_msg
|
||||
|
||||
# Instagram-spezifische Validierungen
|
||||
age = inputs.get("age", 0)
|
||||
if age < 13: # Änderung von 14 auf 13
|
||||
return False, "Das Alter muss mindestens 13 sein (Instagram-Anforderung)."
|
||||
|
||||
# E-Mail-Domain-Validierung
|
||||
if inputs.get("registration_method") == "email":
|
||||
email_domain = inputs.get("email_domain", "")
|
||||
# Blacklist von bekannten problematischen Domains
|
||||
blacklisted_domains = ["temp-mail.org", "guerrillamail.com", "maildrop.cc"]
|
||||
|
||||
# Prüfe mit TextSimilarity auf Ähnlichkeit mit Blacklist
|
||||
for domain in blacklisted_domains:
|
||||
if self.text_similarity.is_similar(email_domain, domain, threshold=0.8):
|
||||
return False, f"Die E-Mail-Domain '{email_domain}' kann problematisch für die Instagram-Registrierung sein. Bitte verwenden Sie eine andere Domain."
|
||||
|
||||
return True, ""
|
||||
|
||||
def get_form_field_label(self, field_type: str) -> str:
|
||||
"""
|
||||
Gibt einen Label-Text für ein Formularfeld basierend auf dem Feldtyp zurück.
|
||||
|
||||
Args:
|
||||
field_type: Typ des Formularfelds
|
||||
|
||||
Returns:
|
||||
str: Label-Text für das Formularfeld
|
||||
"""
|
||||
# Mapping von Feldtypen zu Labels
|
||||
field_labels = {
|
||||
"full_name": "Vollständiger Name",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"email": "E-Mail-Adresse",
|
||||
"phone": "Telefonnummer",
|
||||
"age": "Alter",
|
||||
"birthday": "Geburtsdatum"
|
||||
}
|
||||
|
||||
return field_labels.get(field_type, field_type.capitalize())
|
||||
277
controllers/platform_controllers/tiktok_controller.py
Normale Datei
@ -0,0 +1,277 @@
|
||||
"""
|
||||
Controller für TikTok-spezifische Funktionalität.
|
||||
Mit TextSimilarity-Integration für robusteres UI-Element-Matching.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import random
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QObject
|
||||
|
||||
from controllers.platform_controllers.base_controller import BasePlatformController
|
||||
from views.tabs.generator_tab import GeneratorTab
|
||||
from views.tabs.accounts_tab import AccountsTab
|
||||
from views.tabs.settings_tab import SettingsTab
|
||||
|
||||
from social_networks.tiktok.tiktok_automation import TikTokAutomation
|
||||
from utils.text_similarity import TextSimilarity
|
||||
|
||||
logger = logging.getLogger("tiktok_controller")
|
||||
|
||||
class TikTokWorkerThread(QThread):
|
||||
"""Thread für die TikTok-Account-Erstellung."""
|
||||
|
||||
# Signale
|
||||
update_signal = pyqtSignal(str)
|
||||
log_signal = pyqtSignal(str)
|
||||
progress_signal = pyqtSignal(int)
|
||||
finished_signal = pyqtSignal(dict)
|
||||
error_signal = pyqtSignal(str)
|
||||
|
||||
def __init__(self, params):
|
||||
super().__init__()
|
||||
self.params = params
|
||||
self.running = True
|
||||
|
||||
# TextSimilarity für robustes Fehler-Matching
|
||||
self.text_similarity = TextSimilarity(default_threshold=0.7)
|
||||
|
||||
# Fehler-Patterns für robustes Fehler-Matching
|
||||
self.error_patterns = [
|
||||
"Fehler", "Error", "Fehlgeschlagen", "Failed", "Problem", "Issue",
|
||||
"Nicht möglich", "Not possible", "Bitte versuchen Sie es erneut",
|
||||
"Please try again", "Konnte nicht", "Could not", "Timeout"
|
||||
]
|
||||
|
||||
def run(self):
|
||||
"""Führt die Account-Erstellung aus."""
|
||||
try:
|
||||
self.log_signal.emit("TikTok-Account-Erstellung gestartet...")
|
||||
self.progress_signal.emit(10)
|
||||
|
||||
# TikTok-Automation initialisieren
|
||||
automation = TikTokAutomation(
|
||||
headless=self.params.get("headless", False),
|
||||
use_proxy=self.params.get("use_proxy", False),
|
||||
proxy_type=self.params.get("proxy_type"),
|
||||
save_screenshots=True,
|
||||
debug=self.params.get("debug", False),
|
||||
email_domain=self.params.get("email_domain", "z5m7q9dk3ah2v1plx6ju.com")
|
||||
)
|
||||
|
||||
self.update_signal.emit("TikTok-Automation initialisiert")
|
||||
self.progress_signal.emit(20)
|
||||
|
||||
# Account registrieren
|
||||
self.log_signal.emit(f"Registriere Account für: {self.params['full_name']}")
|
||||
|
||||
registration_method = self.params.get("registration_method", "email")
|
||||
phone_number = self.params.get("phone_number")
|
||||
|
||||
# Account registrieren
|
||||
result = automation.register_account(
|
||||
full_name=self.params["full_name"],
|
||||
age=self.params["age"],
|
||||
registration_method=registration_method,
|
||||
phone_number=phone_number,
|
||||
**self.params.get("additional_params", {})
|
||||
)
|
||||
|
||||
self.progress_signal.emit(100)
|
||||
|
||||
if result["success"]:
|
||||
self.log_signal.emit("Account erfolgreich erstellt!")
|
||||
self.finished_signal.emit(result)
|
||||
else:
|
||||
# Robuste Fehlerbehandlung mit TextSimilarity
|
||||
error_msg = result.get("error", "Unbekannter Fehler")
|
||||
|
||||
# Versuche, Fehler nutzerfreundlicher zu interpretieren
|
||||
user_friendly_error = self._interpret_error(error_msg)
|
||||
|
||||
self.log_signal.emit(f"Fehler bei der Account-Erstellung: {user_friendly_error}")
|
||||
self.error_signal.emit(user_friendly_error)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im Worker-Thread: {e}")
|
||||
self.log_signal.emit(f"Schwerwiegender Fehler: {str(e)}")
|
||||
self.error_signal.emit(str(e))
|
||||
self.progress_signal.emit(0)
|
||||
|
||||
def _interpret_error(self, error_msg: str) -> str:
|
||||
"""
|
||||
Interpretiert Fehlermeldungen und gibt eine benutzerfreundlichere Version zurück.
|
||||
Verwendet TextSimilarity für robusteres Fehler-Matching.
|
||||
|
||||
Args:
|
||||
error_msg: Die ursprüngliche Fehlermeldung
|
||||
|
||||
Returns:
|
||||
str: Benutzerfreundliche Fehlermeldung
|
||||
"""
|
||||
# Bekannte Fehlermuster und deren Interpretationen
|
||||
error_interpretations = {
|
||||
"captcha": "TikTok hat einen Captcha-Test angefordert. Versuchen Sie es später erneut oder nutzen Sie einen anderen Proxy.",
|
||||
"verification": "Es gab ein Problem mit der Verifizierung des Accounts. Bitte prüfen Sie die E-Mail-Einstellungen.",
|
||||
"proxy": "Problem mit der Proxy-Verbindung. Bitte prüfen Sie Ihre Proxy-Einstellungen.",
|
||||
"timeout": "Zeitüberschreitung bei der Verbindung. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||
"username": "Der gewählte Benutzername ist bereits vergeben oder nicht zulässig.",
|
||||
"password": "Das Passwort erfüllt nicht die Anforderungen von TikTok.",
|
||||
"email": "Die E-Mail-Adresse konnte nicht verwendet werden. Bitte nutzen Sie eine andere E-Mail-Domain.",
|
||||
"phone": "Die Telefonnummer konnte nicht für die Registrierung verwendet werden.",
|
||||
"age": "Das eingegebene Alter erfüllt nicht die Anforderungen von TikTok.",
|
||||
"too_many_attempts": "Zu viele Registrierungsversuche. Bitte warten Sie und versuchen Sie es später erneut."
|
||||
}
|
||||
|
||||
# Versuche, den Fehler zu kategorisieren
|
||||
for pattern, interpretation in error_interpretations.items():
|
||||
for error_term in self.error_patterns:
|
||||
if (pattern in error_msg.lower() or
|
||||
self.text_similarity.is_similar(error_term, error_msg, threshold=0.7)):
|
||||
return interpretation
|
||||
|
||||
# Fallback: Originale Fehlermeldung zurückgeben
|
||||
return error_msg
|
||||
|
||||
def stop(self):
|
||||
"""Stoppt den Thread."""
|
||||
self.running = False
|
||||
self.terminate()
|
||||
|
||||
class TikTokController(BasePlatformController):
|
||||
"""Controller für TikTok-spezifische Funktionalität."""
|
||||
|
||||
def __init__(self, db_manager, proxy_rotator, email_handler, language_manager=None):
|
||||
super().__init__("TikTok", db_manager, proxy_rotator, email_handler, language_manager)
|
||||
self.worker_thread = None
|
||||
|
||||
# TextSimilarity für robustes UI-Element-Matching
|
||||
self.text_similarity = TextSimilarity(default_threshold=0.75)
|
||||
|
||||
def create_generator_tab(self):
|
||||
"""Erstellt den TikTok-Generator-Tab."""
|
||||
generator_tab = GeneratorTab(self.platform_name, self.language_manager)
|
||||
|
||||
# TikTok-spezifische Anpassungen
|
||||
# Diese Methode überschreiben, wenn spezifische Anpassungen benötigt werden
|
||||
|
||||
# Signale verbinden
|
||||
generator_tab.start_requested.connect(self.start_account_creation)
|
||||
generator_tab.stop_requested.connect(self.stop_account_creation)
|
||||
|
||||
return generator_tab
|
||||
|
||||
def start_account_creation(self, params):
|
||||
"""Startet die TikTok-Account-Erstellung."""
|
||||
super().start_account_creation(params)
|
||||
|
||||
# Validiere Eingaben
|
||||
is_valid, error_msg = self.validate_inputs(params)
|
||||
if not is_valid:
|
||||
self.get_generator_tab().show_error(error_msg)
|
||||
return
|
||||
|
||||
# UI aktualisieren
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(True)
|
||||
generator_tab.clear_log()
|
||||
generator_tab.set_progress(0)
|
||||
|
||||
# Worker-Thread starten
|
||||
self.worker_thread = TikTokWorkerThread(params)
|
||||
self.worker_thread.update_signal.connect(lambda msg: generator_tab.set_status(msg))
|
||||
self.worker_thread.log_signal.connect(lambda msg: generator_tab.add_log(msg))
|
||||
self.worker_thread.error_signal.connect(lambda msg: (generator_tab.show_error(msg), generator_tab.set_running(False)))
|
||||
self.worker_thread.finished_signal.connect(lambda result: self.handle_account_created(result))
|
||||
self.worker_thread.progress_signal.connect(lambda value: generator_tab.set_progress(value))
|
||||
|
||||
self.worker_thread.start()
|
||||
|
||||
def stop_account_creation(self):
|
||||
"""Stoppt die TikTok-Account-Erstellung."""
|
||||
if self.worker_thread and self.worker_thread.isRunning():
|
||||
self.worker_thread.stop()
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.add_log("Account-Erstellung wurde abgebrochen")
|
||||
generator_tab.set_running(False)
|
||||
generator_tab.set_progress(0)
|
||||
|
||||
def handle_account_created(self, result):
|
||||
"""Verarbeitet erfolgreich erstellte Accounts."""
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(False)
|
||||
|
||||
# Account-Daten aus dem Ergebnis holen
|
||||
account_data = result.get("account_data", {})
|
||||
|
||||
# Account-Erfolgsereignis auslösen
|
||||
generator_tab.account_created.emit(self.platform_name, account_data)
|
||||
|
||||
# Account in der Datenbank speichern
|
||||
self.save_account_to_db(account_data)
|
||||
|
||||
def save_account_to_db(self, account_data):
|
||||
"""Speichert einen erstellten Account in der Datenbank."""
|
||||
account = {
|
||||
"platform": self.platform_name.lower(),
|
||||
"username": account_data.get("username", ""),
|
||||
"password": account_data.get("password", ""),
|
||||
"email": account_data.get("email", ""),
|
||||
"phone": account_data.get("phone", ""),
|
||||
"full_name": account_data.get("full_name", ""),
|
||||
"created_at": time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
self.db_manager.add_account(account)
|
||||
logger.info(f"Account in Datenbank gespeichert: {account['username']}")
|
||||
|
||||
def validate_inputs(self, inputs):
|
||||
"""
|
||||
Validiert die Eingaben für die Account-Erstellung.
|
||||
Verwendet TextSimilarity für robustere Validierung.
|
||||
"""
|
||||
# Basis-Validierungen von BasePlatformController verwenden
|
||||
valid, error_msg = super().validate_inputs(inputs)
|
||||
if not valid:
|
||||
return valid, error_msg
|
||||
|
||||
# TikTok-spezifische Validierungen
|
||||
age = inputs.get("age", 0)
|
||||
if age < 13:
|
||||
return False, "Das Alter muss mindestens 13 sein (TikTok-Anforderung)."
|
||||
|
||||
# E-Mail-Domain-Validierung
|
||||
if inputs.get("registration_method") == "email":
|
||||
email_domain = inputs.get("email_domain", "")
|
||||
# Blacklist von bekannten problematischen Domains
|
||||
blacklisted_domains = ["temp-mail.org", "guerrillamail.com", "maildrop.cc"]
|
||||
|
||||
# Prüfe mit TextSimilarity auf Ähnlichkeit mit Blacklist
|
||||
for domain in blacklisted_domains:
|
||||
if self.text_similarity.is_similar(email_domain, domain, threshold=0.8):
|
||||
return False, f"Die E-Mail-Domain '{email_domain}' kann problematisch für die TikTok-Registrierung sein. Bitte verwenden Sie eine andere Domain."
|
||||
|
||||
return True, ""
|
||||
|
||||
def get_form_field_label(self, field_type: str) -> str:
|
||||
"""
|
||||
Gibt einen Label-Text für ein Formularfeld basierend auf dem Feldtyp zurück.
|
||||
|
||||
Args:
|
||||
field_type: Typ des Formularfelds
|
||||
|
||||
Returns:
|
||||
str: Label-Text für das Formularfeld
|
||||
"""
|
||||
# Mapping von Feldtypen zu Labels
|
||||
field_labels = {
|
||||
"full_name": "Vollständiger Name",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"email": "E-Mail-Adresse",
|
||||
"phone": "Telefonnummer",
|
||||
"age": "Alter",
|
||||
"birthday": "Geburtsdatum"
|
||||
}
|
||||
|
||||
return field_labels.get(field_type, field_type.capitalize())
|
||||
294
controllers/settings_controller.py
Normale Datei
@ -0,0 +1,294 @@
|
||||
"""
|
||||
Controller für die Verwaltung von Einstellungen.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import random
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
logger = logging.getLogger("settings_controller")
|
||||
|
||||
class SettingsController(QObject):
|
||||
"""Controller für die Verwaltung von Einstellungen."""
|
||||
|
||||
def __init__(self, proxy_rotator, email_handler, license_manager):
|
||||
super().__init__()
|
||||
self.proxy_rotator = proxy_rotator
|
||||
self.email_handler = email_handler
|
||||
self.license_manager = license_manager
|
||||
self.parent_view = None
|
||||
|
||||
def set_parent_view(self, view):
|
||||
"""Setzt die übergeordnete View für Dialoge."""
|
||||
self.parent_view = view
|
||||
|
||||
def load_proxy_settings(self):
|
||||
"""Lädt die Proxy-Einstellungen."""
|
||||
try:
|
||||
proxy_config = self.proxy_rotator.get_config() or {}
|
||||
|
||||
settings = {
|
||||
"ipv4_proxies": proxy_config.get("ipv4", []),
|
||||
"ipv6_proxies": proxy_config.get("ipv6", []),
|
||||
"mobile_proxies": proxy_config.get("mobile", []),
|
||||
"mobile_api": proxy_config.get("mobile_api", {})
|
||||
}
|
||||
|
||||
return settings
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Proxy-Einstellungen: {e}")
|
||||
return {}
|
||||
|
||||
def save_proxy_settings(self, settings):
|
||||
"""Speichert die Proxy-Einstellungen."""
|
||||
try:
|
||||
# IPv4 Proxies
|
||||
ipv4_proxies = settings.get("ipv4_proxies", [])
|
||||
if isinstance(ipv4_proxies, str):
|
||||
ipv4_proxies = [line.strip() for line in ipv4_proxies.splitlines() if line.strip()]
|
||||
|
||||
# IPv6 Proxies
|
||||
ipv6_proxies = settings.get("ipv6_proxies", [])
|
||||
if isinstance(ipv6_proxies, str):
|
||||
ipv6_proxies = [line.strip() for line in ipv6_proxies.splitlines() if line.strip()]
|
||||
|
||||
# Mobile Proxies
|
||||
mobile_proxies = settings.get("mobile_proxies", [])
|
||||
if isinstance(mobile_proxies, str):
|
||||
mobile_proxies = [line.strip() for line in mobile_proxies.splitlines() if line.strip()]
|
||||
|
||||
# API Keys
|
||||
mobile_api = settings.get("mobile_api", {})
|
||||
|
||||
# Konfiguration aktualisieren
|
||||
self.proxy_rotator.update_config({
|
||||
"ipv4": ipv4_proxies,
|
||||
"ipv6": ipv6_proxies,
|
||||
"mobile": mobile_proxies,
|
||||
"mobile_api": mobile_api
|
||||
})
|
||||
|
||||
logger.info("Proxy-Einstellungen gespeichert")
|
||||
|
||||
if self.parent_view:
|
||||
QMessageBox.information(
|
||||
self.parent_view,
|
||||
"Erfolg",
|
||||
"Proxy-Einstellungen wurden gespeichert."
|
||||
)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Speichern der Proxy-Einstellungen: {e}")
|
||||
|
||||
if self.parent_view:
|
||||
QMessageBox.critical(
|
||||
self.parent_view,
|
||||
"Fehler",
|
||||
f"Proxy-Einstellungen konnten nicht gespeichert werden:\n{str(e)}"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def test_proxy(self, proxy_type):
|
||||
"""Testet einen zufälligen Proxy des ausgewählten Typs."""
|
||||
try:
|
||||
# Überprüfe, ob Proxies konfiguriert sind
|
||||
proxies = self.proxy_rotator.get_proxies_by_type(proxy_type)
|
||||
if not proxies:
|
||||
if self.parent_view:
|
||||
QMessageBox.warning(
|
||||
self.parent_view,
|
||||
"Keine Proxies",
|
||||
f"Keine {proxy_type.upper()}-Proxies konfiguriert.\nBitte fügen Sie Proxies in den Einstellungen hinzu."
|
||||
)
|
||||
return False
|
||||
|
||||
# Zufälligen Proxy auswählen
|
||||
proxy = random.choice(proxies)
|
||||
|
||||
# Proxy testen
|
||||
result = self.proxy_rotator.test_proxy(proxy_type)
|
||||
|
||||
if result["success"]:
|
||||
if self.parent_view:
|
||||
QMessageBox.information(
|
||||
self.parent_view,
|
||||
"Proxy-Test erfolgreich",
|
||||
f"IP: {result['ip']}\nLand: {result['country'] or 'Unbekannt'}\nAntwortzeit: {result['response_time']:.2f}s"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
if self.parent_view:
|
||||
QMessageBox.warning(
|
||||
self.parent_view,
|
||||
"Proxy-Test fehlgeschlagen",
|
||||
f"Fehler: {result['error']}"
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Testen des Proxy: {e}")
|
||||
|
||||
if self.parent_view:
|
||||
QMessageBox.critical(
|
||||
self.parent_view,
|
||||
"Fehler",
|
||||
f"Fehler beim Testen des Proxy:\n{str(e)}"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def load_email_settings(self):
|
||||
"""Lädt die E-Mail-Einstellungen."""
|
||||
try:
|
||||
email_config = self.email_handler.get_config() or {}
|
||||
|
||||
settings = {
|
||||
"imap_server": email_config.get("imap_server", ""),
|
||||
"imap_port": email_config.get("imap_port", 993),
|
||||
"imap_user": email_config.get("imap_user", ""),
|
||||
"imap_pass": email_config.get("imap_pass", "")
|
||||
}
|
||||
|
||||
return settings
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der E-Mail-Einstellungen: {e}")
|
||||
return {}
|
||||
|
||||
def save_email_settings(self, settings):
|
||||
"""Speichert die E-Mail-Einstellungen."""
|
||||
try:
|
||||
# Einstellungen aktualisieren
|
||||
self.email_handler.update_config(settings)
|
||||
|
||||
logger.info("E-Mail-Einstellungen gespeichert")
|
||||
|
||||
if self.parent_view:
|
||||
QMessageBox.information(
|
||||
self.parent_view,
|
||||
"Erfolg",
|
||||
"E-Mail-Einstellungen wurden gespeichert."
|
||||
)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Speichern der E-Mail-Einstellungen: {e}")
|
||||
|
||||
if self.parent_view:
|
||||
QMessageBox.critical(
|
||||
self.parent_view,
|
||||
"Fehler",
|
||||
f"E-Mail-Einstellungen konnten nicht gespeichert werden:\n{str(e)}"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def test_email(self, settings=None):
|
||||
"""Testet die E-Mail-Verbindung."""
|
||||
try:
|
||||
if settings:
|
||||
# Temporär Einstellungen aktualisieren
|
||||
self.email_handler.update_credentials(
|
||||
settings.get("imap_user", ""),
|
||||
settings.get("imap_pass", "")
|
||||
)
|
||||
self.email_handler.update_server(
|
||||
settings.get("imap_server", ""),
|
||||
settings.get("imap_port", 993)
|
||||
)
|
||||
|
||||
# Verbindung testen
|
||||
result = self.email_handler.test_connection()
|
||||
|
||||
if result["success"]:
|
||||
if self.parent_view:
|
||||
QMessageBox.information(
|
||||
self.parent_view,
|
||||
"E-Mail-Test erfolgreich",
|
||||
f"Verbindung zu {result['server']}:{result['port']} hergestellt.\nGefundene Postfächer: {result['mailbox_count']}"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
if self.parent_view:
|
||||
QMessageBox.warning(
|
||||
self.parent_view,
|
||||
"E-Mail-Test fehlgeschlagen",
|
||||
f"Fehler: {result['error']}"
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Testen der E-Mail-Verbindung: {e}")
|
||||
|
||||
if self.parent_view:
|
||||
QMessageBox.critical(
|
||||
self.parent_view,
|
||||
"Fehler",
|
||||
f"Fehler beim Testen der E-Mail-Verbindung:\n{str(e)}"
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def load_license_info(self):
|
||||
"""Lädt die Lizenzinformationen."""
|
||||
try:
|
||||
license_info = self.license_manager.get_license_info()
|
||||
return license_info
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Lizenzinformationen: {e}")
|
||||
return {}
|
||||
|
||||
def activate_license(self, license_key):
|
||||
"""Aktiviert eine Lizenz."""
|
||||
try:
|
||||
success, message = self.license_manager.activate_license(license_key)
|
||||
|
||||
if success:
|
||||
if self.parent_view:
|
||||
QMessageBox.information(
|
||||
self.parent_view,
|
||||
"Lizenz aktiviert",
|
||||
message
|
||||
)
|
||||
else:
|
||||
if self.parent_view:
|
||||
QMessageBox.warning(
|
||||
self.parent_view,
|
||||
"Lizenzaktivierung fehlgeschlagen",
|
||||
message
|
||||
)
|
||||
|
||||
return success, message
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Lizenzaktivierung: {e}")
|
||||
|
||||
if self.parent_view:
|
||||
QMessageBox.critical(
|
||||
self.parent_view,
|
||||
"Fehler",
|
||||
f"Fehler bei der Lizenzaktivierung:\n{str(e)}"
|
||||
)
|
||||
|
||||
return False, str(e)
|
||||
|
||||
def check_license(self):
|
||||
"""Überprüft, ob eine gültige Lizenz vorhanden ist."""
|
||||
try:
|
||||
is_licensed = self.license_manager.is_licensed()
|
||||
|
||||
if not is_licensed and self.parent_view:
|
||||
license_info = self.license_manager.get_license_info()
|
||||
status = license_info.get("status_text", "Inaktiv")
|
||||
|
||||
QMessageBox.warning(
|
||||
self.parent_view,
|
||||
"Keine gültige Lizenz",
|
||||
f"Status: {status}\n\nBitte aktivieren Sie eine Lizenz, um die Software zu nutzen."
|
||||
)
|
||||
|
||||
return is_licensed
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Lizenzprüfung: {e}")
|
||||
return False
|
||||
0
database/__init__.py
Normale Datei
BIN
database/__pycache__/__init__.cpython-310.pyc
Normale Datei
BIN
database/__pycache__/__init__.cpython-313.pyc
Normale Datei
BIN
database/__pycache__/db_manager.cpython-310.pyc
Normale Datei
BIN
database/__pycache__/db_manager.cpython-313.pyc
Normale Datei
0
database/account_repository.py
Normale Datei
BIN
database/accounts.db
Normale Datei
480
database/db_manager.py
Normale Datei
@ -0,0 +1,480 @@
|
||||
"""
|
||||
Datenbankmanager für den Social Media Account Generator.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import sqlite3
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union
|
||||
|
||||
logger = logging.getLogger("db_manager")
|
||||
|
||||
class DatabaseManager:
|
||||
"""Klasse zur Verwaltung der Datenbank für Account-Informationen."""
|
||||
|
||||
def __init__(self, db_path: str = "database/accounts.db"):
|
||||
"""
|
||||
Initialisiert den DatabaseManager.
|
||||
|
||||
Args:
|
||||
db_path: Pfad zur Datenbank-Datei
|
||||
"""
|
||||
self.db_path = db_path
|
||||
|
||||
# Stelle sicher, dass das Datenbankverzeichnis existiert
|
||||
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
||||
|
||||
# Datenbank initialisieren
|
||||
self.init_db()
|
||||
|
||||
def init_db(self) -> None:
|
||||
"""Initialisiert die Datenbank und erstellt die benötigten Tabellen, wenn sie nicht existieren."""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Accounts-Tabelle erstellen
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
full_name TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
last_login TEXT,
|
||||
notes TEXT,
|
||||
cookies TEXT,
|
||||
status TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
# Settings-Tabelle erstellen
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info("Datenbank initialisiert")
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler bei der Datenbankinitialisierung: {e}")
|
||||
|
||||
def add_account(self, account_data: Dict[str, Any]) -> int:
|
||||
"""
|
||||
Fügt einen Account zur Datenbank hinzu.
|
||||
|
||||
Args:
|
||||
account_data: Dictionary mit Account-Daten
|
||||
|
||||
Returns:
|
||||
ID des hinzugefügten Accounts oder -1 im Fehlerfall
|
||||
"""
|
||||
try:
|
||||
# Prüfe, ob erforderliche Felder vorhanden sind
|
||||
required_fields = ["platform", "username", "password"]
|
||||
for field in required_fields:
|
||||
if field not in account_data:
|
||||
logger.error(f"Fehlendes Pflichtfeld: {field}")
|
||||
return -1
|
||||
|
||||
# Sicherstellen, dass created_at vorhanden ist
|
||||
if "created_at" not in account_data:
|
||||
account_data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# SQL-Anweisung vorbereiten
|
||||
fields = ", ".join(account_data.keys())
|
||||
placeholders = ", ".join(["?" for _ in account_data])
|
||||
|
||||
query = f"INSERT INTO accounts ({fields}) VALUES ({placeholders})"
|
||||
|
||||
# Anweisung ausführen
|
||||
cursor.execute(query, list(account_data.values()))
|
||||
|
||||
# ID des hinzugefügten Datensatzes abrufen
|
||||
account_id = cursor.lastrowid
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Account hinzugefügt: {account_data['username']} (ID: {account_id})")
|
||||
|
||||
return account_id
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Hinzufügen des Accounts: {e}")
|
||||
return -1
|
||||
|
||||
def get_account(self, account_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt einen Account anhand seiner ID zurück.
|
||||
|
||||
Args:
|
||||
account_id: ID des Accounts
|
||||
|
||||
Returns:
|
||||
Dictionary mit Account-Daten oder None, wenn der Account nicht gefunden wurde
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row # Für dict-like Zugriff auf Zeilen
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT * FROM accounts WHERE id = ?", (account_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
# Konvertiere Row in Dictionary
|
||||
account = dict(row)
|
||||
logger.debug(f"Account gefunden: {account['username']} (ID: {account_id})")
|
||||
return account
|
||||
else:
|
||||
logger.warning(f"Account nicht gefunden: ID {account_id}")
|
||||
return None
|
||||
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Abrufen des Accounts: {e}")
|
||||
return None
|
||||
|
||||
def get_all_accounts(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt alle Accounts zurück.
|
||||
|
||||
Returns:
|
||||
Liste von Dictionaries mit Account-Daten
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT * FROM accounts ORDER BY id DESC")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
# Konvertiere Rows in Dictionaries
|
||||
accounts = [dict(row) for row in rows]
|
||||
|
||||
logger.info(f"{len(accounts)} Accounts abgerufen")
|
||||
|
||||
return accounts
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Abrufen aller Accounts: {e}")
|
||||
return []
|
||||
|
||||
def get_accounts_by_platform(self, platform: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Gibt alle Accounts einer bestimmten Plattform zurück.
|
||||
|
||||
Args:
|
||||
platform: Plattformname (z.B. "instagram")
|
||||
|
||||
Returns:
|
||||
Liste von Dictionaries mit Account-Daten
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT * FROM accounts WHERE platform = ? ORDER BY id DESC", (platform.lower(),))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
# Konvertiere Rows in Dictionaries
|
||||
accounts = [dict(row) for row in rows]
|
||||
|
||||
logger.info(f"{len(accounts)} Accounts für Plattform '{platform}' abgerufen")
|
||||
|
||||
return accounts
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Abrufen der Accounts für Plattform '{platform}': {e}")
|
||||
return []
|
||||
|
||||
def update_account(self, account_id: int, update_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Aktualisiert einen Account in der Datenbank.
|
||||
|
||||
Args:
|
||||
account_id: ID des zu aktualisierenden Accounts
|
||||
update_data: Dictionary mit zu aktualisierenden Feldern
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False im Fehlerfall
|
||||
"""
|
||||
if not update_data:
|
||||
logger.warning("Keine Aktualisierungsdaten bereitgestellt")
|
||||
return False
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# SQL-Anweisung vorbereiten
|
||||
set_clause = ", ".join([f"{field} = ?" for field in update_data.keys()])
|
||||
values = list(update_data.values())
|
||||
values.append(account_id)
|
||||
|
||||
query = f"UPDATE accounts SET {set_clause} WHERE id = ?"
|
||||
|
||||
# Anweisung ausführen
|
||||
cursor.execute(query, values)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Account aktualisiert: ID {account_id}")
|
||||
|
||||
return True
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Aktualisieren des Accounts: {e}")
|
||||
return False
|
||||
|
||||
def delete_account(self, account_id: int) -> bool:
|
||||
"""
|
||||
Löscht einen Account aus der Datenbank.
|
||||
|
||||
Args:
|
||||
account_id: ID des zu löschenden Accounts
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False im Fehlerfall
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("DELETE FROM accounts WHERE id = ?", (account_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Account gelöscht: ID {account_id}")
|
||||
|
||||
return True
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Löschen des Accounts: {e}")
|
||||
return False
|
||||
|
||||
def search_accounts(self, query: str, platform: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Sucht nach Accounts in der Datenbank.
|
||||
|
||||
Args:
|
||||
query: Suchbegriff
|
||||
platform: Optional, Plattform für die Einschränkung der Suche
|
||||
|
||||
Returns:
|
||||
Liste von Dictionaries mit gefundenen Account-Daten
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Suchbegriff für LIKE-Operator vorbereiten
|
||||
search_term = f"%{query}%"
|
||||
|
||||
if platform:
|
||||
query_sql = """
|
||||
SELECT * FROM accounts
|
||||
WHERE (username LIKE ? OR email LIKE ? OR phone LIKE ? OR full_name LIKE ?)
|
||||
AND platform = ?
|
||||
ORDER BY id DESC
|
||||
"""
|
||||
cursor.execute(query_sql, (search_term, search_term, search_term, search_term, platform.lower()))
|
||||
else:
|
||||
query_sql = """
|
||||
SELECT * FROM accounts
|
||||
WHERE username LIKE ? OR email LIKE ? OR phone LIKE ? OR full_name LIKE ?
|
||||
ORDER BY id DESC
|
||||
"""
|
||||
cursor.execute(query_sql, (search_term, search_term, search_term, search_term))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
# Konvertiere Rows in Dictionaries
|
||||
accounts = [dict(row) for row in rows]
|
||||
|
||||
logger.info(f"{len(accounts)} Accounts gefunden für Suchbegriff '{query}'")
|
||||
|
||||
return accounts
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler bei der Suche nach Accounts: {e}")
|
||||
return []
|
||||
|
||||
def get_account_count(self, platform: Optional[str] = None) -> int:
|
||||
"""
|
||||
Gibt die Anzahl der Accounts zurück.
|
||||
|
||||
Args:
|
||||
platform: Optional, Plattform für die Einschränkung der Zählung
|
||||
|
||||
Returns:
|
||||
Anzahl der Accounts
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
if platform:
|
||||
cursor.execute("SELECT COUNT(*) FROM accounts WHERE platform = ?", (platform.lower(),))
|
||||
else:
|
||||
cursor.execute("SELECT COUNT(*) FROM accounts")
|
||||
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
return count
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Zählen der Accounts: {e}")
|
||||
return 0
|
||||
|
||||
def get_setting(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Gibt einen Einstellungswert zurück.
|
||||
|
||||
Args:
|
||||
key: Schlüssel der Einstellung
|
||||
default: Standardwert, falls die Einstellung nicht gefunden wurde
|
||||
|
||||
Returns:
|
||||
Wert der Einstellung oder der Standardwert
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT value FROM settings WHERE key = ?", (key,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
# Versuche, den Wert als JSON zu parsen
|
||||
try:
|
||||
return json.loads(row[0])
|
||||
except json.JSONDecodeError:
|
||||
# Wenn kein gültiges JSON, gib den Rohwert zurück
|
||||
return row[0]
|
||||
else:
|
||||
return default
|
||||
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Abrufen der Einstellung '{key}': {e}")
|
||||
return default
|
||||
|
||||
def set_setting(self, key: str, value: Any) -> bool:
|
||||
"""
|
||||
Setzt einen Einstellungswert.
|
||||
|
||||
Args:
|
||||
key: Schlüssel der Einstellung
|
||||
value: Wert der Einstellung (wird als JSON gespeichert, wenn es kein String ist)
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False im Fehlerfall
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Wert als JSON speichern, wenn es kein String ist
|
||||
if not isinstance(value, str):
|
||||
value = json.dumps(value)
|
||||
|
||||
# Prüfen, ob die Einstellung bereits existiert
|
||||
cursor.execute("SELECT COUNT(*) FROM settings WHERE key = ?", (key,))
|
||||
exists = cursor.fetchone()[0] > 0
|
||||
|
||||
if exists:
|
||||
cursor.execute("UPDATE settings SET value = ? WHERE key = ?", (value, key))
|
||||
else:
|
||||
cursor.execute("INSERT INTO settings (key, value) VALUES (?, ?)", (key, value))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Einstellung gespeichert: {key}")
|
||||
|
||||
return True
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Speichern der Einstellung '{key}': {e}")
|
||||
return False
|
||||
|
||||
def delete_setting(self, key: str) -> bool:
|
||||
"""
|
||||
Löscht eine Einstellung.
|
||||
|
||||
Args:
|
||||
key: Schlüssel der zu löschenden Einstellung
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False im Fehlerfall
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("DELETE FROM settings WHERE key = ?", (key,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Einstellung gelöscht: {key}")
|
||||
|
||||
return True
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Löschen der Einstellung '{key}': {e}")
|
||||
return False
|
||||
|
||||
def backup_database(self, backup_path: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Erstellt ein Backup der Datenbank.
|
||||
|
||||
Args:
|
||||
backup_path: Optional, Pfad für das Backup
|
||||
|
||||
Returns:
|
||||
True bei Erfolg, False im Fehlerfall
|
||||
"""
|
||||
if not backup_path:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = f"database/backup/accounts_{timestamp}.db"
|
||||
|
||||
# Stelle sicher, dass das Backup-Verzeichnis existiert
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
|
||||
try:
|
||||
# SQLite-Backup-API verwenden
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
backup_conn = sqlite3.connect(backup_path)
|
||||
|
||||
conn.backup(backup_conn)
|
||||
|
||||
conn.close()
|
||||
backup_conn.close()
|
||||
|
||||
logger.info(f"Datenbank-Backup erstellt: {backup_path}")
|
||||
|
||||
return True
|
||||
except sqlite3.Error as e:
|
||||
logger.error(f"Fehler beim Erstellen des Datenbank-Backups: {e}")
|
||||
return False
|
||||
BIN
database/instagram_accounts.db
Normale Datei
112
database/schema.sql
Normale Datei
@ -0,0 +1,112 @@
|
||||
-- SQLite-Datenbankschema für Instagram Account Generator
|
||||
|
||||
-- Accounts-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT,
|
||||
phone TEXT,
|
||||
full_name TEXT,
|
||||
created_at TEXT,
|
||||
notes TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
proxy_used TEXT,
|
||||
metadata TEXT
|
||||
);
|
||||
|
||||
-- Proxy-Nutzungen-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS proxy_usage (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
proxy_type TEXT NOT NULL,
|
||||
proxy_string TEXT NOT NULL,
|
||||
used_at TEXT NOT NULL,
|
||||
success INTEGER DEFAULT 0,
|
||||
account_id INTEGER,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
||||
|
||||
-- Fehler-Protokoll-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS error_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
error_type TEXT NOT NULL,
|
||||
error_message TEXT NOT NULL,
|
||||
stack_trace TEXT,
|
||||
timestamp TEXT NOT NULL,
|
||||
account_id INTEGER,
|
||||
proxy_used TEXT,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
||||
|
||||
-- Einstellungen-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- SMS-Verifizierungen-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS sms_verifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phone_number TEXT NOT NULL,
|
||||
verification_code TEXT,
|
||||
service_name TEXT NOT NULL DEFAULT 'instagram',
|
||||
timestamp TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
account_id INTEGER,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
||||
|
||||
-- E-Mail-Verifizierungen-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS email_verifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email_address TEXT NOT NULL,
|
||||
verification_code TEXT,
|
||||
service_name TEXT NOT NULL DEFAULT 'instagram',
|
||||
timestamp TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
account_id INTEGER,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
||||
|
||||
-- Lizenzen-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS licenses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
license_key TEXT NOT NULL UNIQUE,
|
||||
activated_at TEXT,
|
||||
expires_at TEXT,
|
||||
status TEXT DEFAULT 'inactive',
|
||||
hardware_id TEXT,
|
||||
metadata TEXT
|
||||
);
|
||||
|
||||
-- Nutzungsdaten-Tabelle (für Statistiken)
|
||||
CREATE TABLE IF NOT EXISTS usage_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
action_type TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
success INTEGER DEFAULT 0,
|
||||
details TEXT
|
||||
);
|
||||
|
||||
-- Indizes für bessere Performance
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_username ON accounts(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_created_at ON accounts(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxy_usage_proxy_type ON proxy_usage(proxy_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxy_usage_used_at ON proxy_usage(used_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_error_logs_error_type ON error_logs(error_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_error_logs_timestamp ON error_logs(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_sms_verifications_phone ON sms_verifications(phone_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_email_verifications_email ON email_verifications(email_address);
|
||||
CREATE INDEX IF NOT EXISTS idx_licenses_key ON licenses(license_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_stats_action_type ON usage_stats(action_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_stats_timestamp ON usage_stats(timestamp);
|
||||
|
||||
-- Beispieldaten für Testzwecke
|
||||
INSERT OR IGNORE INTO settings (key, value, updated_at)
|
||||
VALUES ('app_version', '1.0.0', datetime('now'));
|
||||
|
||||
INSERT OR IGNORE INTO settings (key, value, updated_at)
|
||||
VALUES ('last_update_check', datetime('now'), datetime('now'));
|
||||
0
licensing/__init__.py
Normale Datei
BIN
licensing/__pycache__/__init__.cpython-310.pyc
Normale Datei
BIN
licensing/__pycache__/__init__.cpython-313.pyc
Normale Datei
BIN
licensing/__pycache__/license_manager.cpython-310.pyc
Normale Datei
BIN
licensing/__pycache__/license_manager.cpython-313.pyc
Normale Datei
0
licensing/hardware_fingerprint.py
Normale Datei
450
licensing/license_manager.py
Normale Datei
@ -0,0 +1,450 @@
|
||||
"""
|
||||
Lizenzverwaltungsfunktionalität für den Social Media Account Generator.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
import hmac
|
||||
import hashlib
|
||||
import logging
|
||||
import requests
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union
|
||||
|
||||
logger = logging.getLogger("license_manager")
|
||||
|
||||
class LicenseManager:
|
||||
"""Klasse zur Verwaltung von Softwarelizenzen."""
|
||||
|
||||
CONFIG_FILE = os.path.join("config", "license.json")
|
||||
LICENSE_SERVER_URL = "https://api.example.com/license" # Platzhalter - in der Produktion anpassen
|
||||
|
||||
def __init__(self):
|
||||
"""Initialisiert den LicenseManager und lädt die Konfiguration."""
|
||||
self.license_data = self.load_license_data()
|
||||
self.machine_id = self.get_machine_id()
|
||||
|
||||
# Stelle sicher, dass das Konfigurationsverzeichnis existiert
|
||||
os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True)
|
||||
|
||||
# Prüfe die Lizenz beim Start
|
||||
self.verify_license()
|
||||
|
||||
def load_license_data(self) -> Dict[str, Any]:
|
||||
"""Lädt die Lizenzdaten aus der Konfigurationsdatei."""
|
||||
if not os.path.exists(self.CONFIG_FILE):
|
||||
return {
|
||||
"key": "",
|
||||
"activation_date": "",
|
||||
"expiry_date": "",
|
||||
"status": "inactive",
|
||||
"status_text": "Keine Lizenz aktiviert",
|
||||
"features": [],
|
||||
"last_online_check": "",
|
||||
"signature": ""
|
||||
}
|
||||
|
||||
try:
|
||||
with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||
license_data = json.load(f)
|
||||
|
||||
logger.info(f"Lizenzdaten geladen: Status '{license_data.get('status', 'unbekannt')}'")
|
||||
|
||||
return license_data
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Lizenzdaten: {e}")
|
||||
return {
|
||||
"key": "",
|
||||
"activation_date": "",
|
||||
"expiry_date": "",
|
||||
"status": "inactive",
|
||||
"status_text": "Fehler beim Laden der Lizenz",
|
||||
"features": [],
|
||||
"last_online_check": "",
|
||||
"signature": ""
|
||||
}
|
||||
|
||||
def save_license_data(self) -> bool:
|
||||
"""Speichert die Lizenzdaten in die Konfigurationsdatei."""
|
||||
try:
|
||||
with open(self.CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self.license_data, f, indent=2)
|
||||
|
||||
logger.info("Lizenzdaten gespeichert")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Speichern der Lizenzdaten: {e}")
|
||||
return False
|
||||
|
||||
def get_license_info(self) -> Dict[str, Any]:
|
||||
"""Gibt die aktuellen Lizenzdaten zurück."""
|
||||
return self.license_data
|
||||
|
||||
def get_machine_id(self) -> str:
|
||||
"""
|
||||
Generiert eine eindeutige Maschinen-ID.
|
||||
|
||||
Returns:
|
||||
Eindeutige Maschinen-ID
|
||||
"""
|
||||
try:
|
||||
# Versuche, eine eindeutige Hardware-ID zu generieren
|
||||
# In der Produktion sollte dies mit einer robusteren Methode ersetzt werden
|
||||
|
||||
machine_id_file = os.path.join("config", ".machine_id")
|
||||
|
||||
if os.path.exists(machine_id_file):
|
||||
# Bestehende ID laden
|
||||
with open(machine_id_file, "r") as f:
|
||||
return f.read().strip()
|
||||
else:
|
||||
# Neue ID generieren
|
||||
machine_id = str(uuid.uuid4())
|
||||
|
||||
with open(machine_id_file, "w") as f:
|
||||
f.write(machine_id)
|
||||
|
||||
return machine_id
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Generierung der Maschinen-ID: {e}")
|
||||
|
||||
# Fallback: UUID auf Basis der aktuellen Zeit
|
||||
return str(uuid.uuid5(uuid.NAMESPACE_DNS, f"fallback-{time.time()}"))
|
||||
|
||||
def is_licensed(self) -> bool:
|
||||
"""
|
||||
Überprüft, ob eine gültige Lizenz vorhanden ist.
|
||||
|
||||
Returns:
|
||||
True, wenn eine gültige Lizenz vorhanden ist, sonst False
|
||||
"""
|
||||
# Prüfe den Status der Lizenz
|
||||
if self.license_data["status"] not in ["active", "trial"]:
|
||||
return False
|
||||
|
||||
# Prüfe, ob die Lizenz abgelaufen ist
|
||||
if self.license_data["expiry_date"]:
|
||||
try:
|
||||
expiry_date = datetime.fromisoformat(self.license_data["expiry_date"])
|
||||
|
||||
if datetime.now() > expiry_date:
|
||||
logger.warning("Lizenz ist abgelaufen")
|
||||
self.license_data["status"] = "expired"
|
||||
self.license_data["status_text"] = "Lizenz abgelaufen"
|
||||
self.save_license_data()
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Parsen des Ablaufdatums: {e}")
|
||||
return False
|
||||
|
||||
# Prüfe, ob regelmäßige Online-Verifizierung erforderlich ist
|
||||
if self.license_data["last_online_check"]:
|
||||
try:
|
||||
last_check = datetime.fromisoformat(self.license_data["last_online_check"])
|
||||
max_offline_days = 7 # Maximale Tage ohne Online-Check
|
||||
|
||||
if datetime.now() > last_check + timedelta(days=max_offline_days):
|
||||
logger.warning(f"Letzte Online-Überprüfung ist mehr als {max_offline_days} Tage her")
|
||||
|
||||
# Versuche, eine Online-Überprüfung durchzuführen
|
||||
if not self.online_verification():
|
||||
self.license_data["status"] = "verification_required"
|
||||
self.license_data["status_text"] = "Online-Überprüfung erforderlich"
|
||||
self.save_license_data()
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Überprüfung der Online-Verifizierung: {e}")
|
||||
|
||||
# Prüfe die Signatur (in der Produktion sollte dies erweitert werden)
|
||||
if not self.verify_signature():
|
||||
logger.warning("Ungültige Lizenzsignatur")
|
||||
self.license_data["status"] = "invalid"
|
||||
self.license_data["status_text"] = "Ungültige Lizenz (manipuliert)"
|
||||
self.save_license_data()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def verify_license(self) -> bool:
|
||||
"""
|
||||
Überprüft die aktuelle Lizenz.
|
||||
|
||||
Returns:
|
||||
True, wenn die Lizenz gültig ist, sonst False
|
||||
"""
|
||||
# Lizenzschlüssel vorhanden?
|
||||
if not self.license_data["key"]:
|
||||
logger.info("Kein Lizenzschlüssel vorhanden")
|
||||
self.license_data["status"] = "inactive"
|
||||
self.license_data["status_text"] = "Keine Lizenz aktiviert"
|
||||
self.save_license_data()
|
||||
return False
|
||||
|
||||
return self.is_licensed()
|
||||
|
||||
def create_signature(self, data: str) -> str:
|
||||
"""
|
||||
Erstellt eine Signatur für die angegebenen Daten.
|
||||
|
||||
Args:
|
||||
data: Zu signierende Daten
|
||||
|
||||
Returns:
|
||||
Signatur als Hexadezimalstring
|
||||
"""
|
||||
# In der Produktion sollte ein sicherer Schlüssel verwendet werden
|
||||
secret_key = "development_secret_key"
|
||||
|
||||
# HMAC-SHA256-Signatur erstellen
|
||||
signature = hmac.new(
|
||||
secret_key.encode(),
|
||||
data.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return signature
|
||||
|
||||
def verify_signature(self) -> bool:
|
||||
"""
|
||||
Überprüft die Signatur der Lizenzdaten.
|
||||
|
||||
Returns:
|
||||
True, wenn die Signatur gültig ist, sonst False
|
||||
"""
|
||||
if not self.license_data["signature"]:
|
||||
return False
|
||||
|
||||
# Daten für die Signaturprüfung vorbereiten
|
||||
data_to_verify = f"{self.license_data['key']}|{self.machine_id}|{self.license_data['activation_date']}|{self.license_data['expiry_date']}"
|
||||
|
||||
# Signatur erstellen
|
||||
computed_signature = self.create_signature(data_to_verify)
|
||||
|
||||
# Signatur vergleichen
|
||||
return computed_signature == self.license_data["signature"]
|
||||
|
||||
def online_verification(self) -> bool:
|
||||
"""
|
||||
Führt eine Online-Überprüfung der Lizenz durch.
|
||||
|
||||
Returns:
|
||||
True, wenn die Überprüfung erfolgreich war, sonst False
|
||||
"""
|
||||
if not self.license_data["key"]:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Daten für die Lizenzüberprüfung
|
||||
verification_data = {
|
||||
"license_key": self.license_data["key"],
|
||||
"machine_id": self.machine_id,
|
||||
"product_version": "1.0.0", # In der Produktion aus einer Konfiguration laden
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
# Anfrage an den Lizenzserver senden
|
||||
response = requests.post(
|
||||
self.LICENSE_SERVER_URL + "/verify",
|
||||
json=verification_data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
|
||||
if result.get("status") == "active":
|
||||
# Lizenz ist gültig
|
||||
logger.info("Online-Lizenzüberprüfung erfolgreich")
|
||||
|
||||
# Aktualisiere das Datum der letzten Überprüfung
|
||||
self.license_data["last_online_check"] = datetime.now().isoformat()
|
||||
self.save_license_data()
|
||||
|
||||
return True
|
||||
else:
|
||||
# Lizenz ist ungültig
|
||||
logger.warning(f"Lizenz ungültig: {result.get('message', 'Unbekannter Fehler')}")
|
||||
|
||||
self.license_data["status"] = result.get("status", "invalid")
|
||||
self.license_data["status_text"] = result.get("message", "Lizenz ungültig")
|
||||
self.save_license_data()
|
||||
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"Fehler bei der Online-Überprüfung: HTTP {response.status_code}")
|
||||
return False
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Netzwerkfehler bei der Online-Überprüfung: {e}")
|
||||
|
||||
# Bei Verbindungsproblemen sollte die lokale Lizenz weiterhin gültig bleiben
|
||||
# In der Produktion kann hier eine Begrenzung der Offline-Zeit implementiert werden
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Unerwarteter Fehler bei der Online-Überprüfung: {e}")
|
||||
return False
|
||||
|
||||
def activate_license(self, license_key: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
Aktiviert eine Lizenz mit dem angegebenen Schlüssel.
|
||||
|
||||
Args:
|
||||
license_key: Zu aktivierender Lizenzschlüssel
|
||||
|
||||
Returns:
|
||||
(Erfolg, Nachricht)
|
||||
"""
|
||||
if not license_key:
|
||||
return False, "Bitte geben Sie einen Lizenzschlüssel ein."
|
||||
|
||||
try:
|
||||
# In der Produktionsumgebung sollte hier eine Online-Aktivierung erfolgen
|
||||
# Für Entwicklungszwecke implementieren wir eine einfache lokale Aktivierung
|
||||
|
||||
# Simulierte Online-Aktivierung
|
||||
activation_data = {
|
||||
"license_key": license_key,
|
||||
"machine_id": self.machine_id,
|
||||
"product_version": "1.0.0",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
# Nur für Entwicklung: Prüfe, ob der Lizenzschlüssel bekannt ist
|
||||
if license_key.startswith("DEV-"):
|
||||
# Entwicklungslizenzen haben unbegrenzte Laufzeit
|
||||
expiry_date = (datetime.now() + timedelta(days=365)).isoformat()
|
||||
activation_response = {
|
||||
"status": "active",
|
||||
"message": "Entwicklungslizenz aktiviert",
|
||||
"activation_date": datetime.now().isoformat(),
|
||||
"expiry_date": expiry_date,
|
||||
"features": ["all"]
|
||||
}
|
||||
elif license_key.startswith("TRIAL-"):
|
||||
# Trial-Lizenzen haben begrenzte Laufzeit
|
||||
expiry_date = (datetime.now() + timedelta(days=30)).isoformat()
|
||||
activation_response = {
|
||||
"status": "trial",
|
||||
"message": "Trial-Lizenz aktiviert (30 Tage)",
|
||||
"activation_date": datetime.now().isoformat(),
|
||||
"expiry_date": expiry_date,
|
||||
"features": ["basic"]
|
||||
}
|
||||
else:
|
||||
# Alle anderen Schlüssel simulieren eine Online-Aktivierung
|
||||
try:
|
||||
# Anfrage an den Lizenzserver senden
|
||||
response = requests.post(
|
||||
self.LICENSE_SERVER_URL + "/activate",
|
||||
json=activation_data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
activation_response = response.json()
|
||||
else:
|
||||
logger.warning(f"Fehler bei der Lizenzaktivierung: HTTP {response.status_code}")
|
||||
return False, f"Fehler bei der Lizenzaktivierung: HTTP {response.status_code}"
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Netzwerkfehler bei der Lizenzaktivierung: {e}")
|
||||
return False, f"Netzwerkfehler bei der Lizenzaktivierung: {e}"
|
||||
except Exception as e:
|
||||
logger.error(f"Unerwarteter Fehler bei der Lizenzaktivierung: {e}")
|
||||
return False, f"Unerwarteter Fehler bei der Lizenzaktivierung: {e}"
|
||||
|
||||
# Lizenzdaten aktualisieren
|
||||
self.license_data["key"] = license_key
|
||||
self.license_data["status"] = activation_response.get("status", "inactive")
|
||||
self.license_data["status_text"] = activation_response.get("message", "Unbekannter Status")
|
||||
self.license_data["activation_date"] = activation_response.get("activation_date", datetime.now().isoformat())
|
||||
self.license_data["expiry_date"] = activation_response.get("expiry_date", "")
|
||||
self.license_data["features"] = activation_response.get("features", [])
|
||||
self.license_data["last_online_check"] = datetime.now().isoformat()
|
||||
|
||||
# Signatur erstellen
|
||||
data_to_sign = f"{self.license_data['key']}|{self.machine_id}|{self.license_data['activation_date']}|{self.license_data['expiry_date']}"
|
||||
self.license_data["signature"] = self.create_signature(data_to_sign)
|
||||
|
||||
# Lizenzdaten speichern
|
||||
self.save_license_data()
|
||||
|
||||
logger.info(f"Lizenz '{license_key}' erfolgreich aktiviert: {self.license_data['status_text']}")
|
||||
|
||||
return True, self.license_data["status_text"]
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Fehler bei der Lizenzaktivierung: {e}"
|
||||
logger.error(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
def deactivate_license(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
Deaktiviert die aktuelle Lizenz.
|
||||
|
||||
Returns:
|
||||
(Erfolg, Nachricht)
|
||||
"""
|
||||
if not self.license_data["key"]:
|
||||
return False, "Keine Lizenz aktiviert"
|
||||
|
||||
old_key = self.license_data["key"]
|
||||
|
||||
try:
|
||||
# Online-Deaktivierung simulieren
|
||||
deactivation_data = {
|
||||
"license_key": self.license_data["key"],
|
||||
"machine_id": self.machine_id,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
# Anfrage für die Produktionsumgebung
|
||||
# response = requests.post(
|
||||
# self.LICENSE_SERVER_URL + "/deactivate",
|
||||
# json=deactivation_data,
|
||||
# timeout=10
|
||||
# )
|
||||
|
||||
# Lizenzdaten zurücksetzen
|
||||
self.license_data = {
|
||||
"key": "",
|
||||
"activation_date": "",
|
||||
"expiry_date": "",
|
||||
"status": "inactive",
|
||||
"status_text": "Keine Lizenz aktiviert",
|
||||
"features": [],
|
||||
"last_online_check": "",
|
||||
"signature": ""
|
||||
}
|
||||
|
||||
# Lizenzdaten speichern
|
||||
self.save_license_data()
|
||||
|
||||
logger.info(f"Lizenz '{old_key}' erfolgreich deaktiviert")
|
||||
|
||||
return True, "Lizenz erfolgreich deaktiviert"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Fehler bei der Lizenzdeaktivierung: {e}"
|
||||
logger.error(error_msg)
|
||||
return False, error_msg
|
||||
|
||||
def has_feature(self, feature_name: str) -> bool:
|
||||
"""
|
||||
Überprüft, ob die aktuelle Lizenz eine bestimmte Funktion unterstützt.
|
||||
|
||||
Args:
|
||||
feature_name: Name der zu überprüfenden Funktion
|
||||
|
||||
Returns:
|
||||
True, wenn die Funktion unterstützt wird, sonst False
|
||||
"""
|
||||
if not self.is_licensed():
|
||||
return False
|
||||
|
||||
# "all" bedeutet, dass alle Funktionen unterstützt werden
|
||||
if "all" in self.license_data["features"]:
|
||||
return True
|
||||
|
||||
return feature_name in self.license_data["features"]
|
||||
304
licensing/license_validator.py
Normale Datei
@ -0,0 +1,304 @@
|
||||
"""
|
||||
Lizenzvalidator - Validiert Lizenzschlüssel und enthält Sicherheitsalgorithmen
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional, Tuple, Any, List
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = logging.getLogger("license_validator")
|
||||
|
||||
class LicenseValidator:
|
||||
"""
|
||||
Validiert Lizenzschlüssel und führt kryptografische Operationen durch.
|
||||
Enthält Platzhaltercode für die Lizenzvalidierung.
|
||||
"""
|
||||
|
||||
# Sicherheitsschlüssel (würde in einer echten Implementierung nicht im Code stehen)
|
||||
SECRET_KEY = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialisiert den LicenseValidator."""
|
||||
logger.info("Lizenzvalidator initialisiert")
|
||||
|
||||
def validate_key_format(self, license_key: str) -> bool:
|
||||
"""
|
||||
Prüft, ob der Lizenzschlüssel das richtige Format hat.
|
||||
|
||||
Args:
|
||||
license_key: Der zu prüfende Lizenzschlüssel
|
||||
|
||||
Returns:
|
||||
bool: True, wenn das Format gültig ist, False sonst
|
||||
"""
|
||||
# Einfacher Formatcheck für XXXXX-XXXXX-XXXXX-XXXXX
|
||||
import re
|
||||
return bool(re.match(r'^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$', license_key))
|
||||
|
||||
def validate_key_checksum(self, license_key: str) -> bool:
|
||||
"""
|
||||
Prüft, ob die Prüfsumme des Lizenzschlüssels gültig ist.
|
||||
|
||||
Args:
|
||||
license_key: Der zu prüfende Lizenzschlüssel
|
||||
|
||||
Returns:
|
||||
bool: True, wenn die Prüfsumme gültig ist, False sonst
|
||||
"""
|
||||
# Platzhalterimplementierung - in einer echten Implementierung würde hier
|
||||
# eine Prüfsummenberechnung stehen
|
||||
|
||||
# Entferne Bindestriche für die Verarbeitung
|
||||
key_parts = license_key.split('-')
|
||||
if len(key_parts) != 4:
|
||||
return False
|
||||
|
||||
# Einfacher Check: Letzter Buchstabe des ersten Teils ist abhängig von den ersten Buchstaben
|
||||
# der anderen Teile (XOR der ASCII-Werte)
|
||||
try:
|
||||
check_char = key_parts[0][-1]
|
||||
calculated_char = chr(ord(key_parts[1][0]) ^ ord(key_parts[2][0]) ^ ord(key_parts[3][0]))
|
||||
|
||||
# In einer echten Implementierung wäre diese Prüfung viel stärker
|
||||
return check_char == calculated_char
|
||||
except IndexError:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Prüfsummenberechnung: {e}")
|
||||
return False
|
||||
|
||||
def decrypt_license_data(self, license_key: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Entschlüsselt Lizenzinformationen aus dem Schlüssel.
|
||||
|
||||
Args:
|
||||
license_key: Der zu entschlüsselnde Lizenzschlüssel
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: Entschlüsselte Lizenzdaten oder None bei Fehler
|
||||
"""
|
||||
# Platzhalterimplementierung - in einer echten Implementierung würde hier
|
||||
# eine Entschlüsselung stehen
|
||||
|
||||
if not self.validate_key_format(license_key):
|
||||
return None
|
||||
|
||||
# Mock-Daten generieren
|
||||
key_parts = license_key.split('-')
|
||||
|
||||
# Aus dem Schlüssel Informationen "ableiten"
|
||||
try:
|
||||
# Verwende den ersten Teil für die Lizenzart
|
||||
license_type_index = sum(ord(c) for c in key_parts[0]) % 3
|
||||
license_types = ["basic", "premium", "enterprise"]
|
||||
license_type = license_types[license_type_index]
|
||||
|
||||
# Verwende den zweiten Teil für die Gültigkeitsdauer
|
||||
validity_months = (sum(ord(c) for c in key_parts[1]) % 12) + 1
|
||||
|
||||
# Verwende den dritten Teil für die Funktionen
|
||||
features_count = (sum(ord(c) for c in key_parts[2]) % 5) + 1
|
||||
all_features = ["multi_account", "proxy_rotation", "advanced_analytics",
|
||||
"sms_verification", "captcha_solving", "phone_verification",
|
||||
"export", "scheduling"]
|
||||
features = all_features[:features_count]
|
||||
|
||||
# Generiere ein "verschlüsseltes" Token
|
||||
token = hashlib.sha256(license_key.encode()).hexdigest()
|
||||
|
||||
# Aktuelle Zeit für Aktivierung
|
||||
now = datetime.now()
|
||||
activation_date = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
expiry_date = (now + timedelta(days=30*validity_months)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Lizenzdaten zusammenstellen
|
||||
license_data = {
|
||||
"license_type": license_type,
|
||||
"features": features,
|
||||
"activation_date": activation_date,
|
||||
"expiry_date": expiry_date,
|
||||
"token": token
|
||||
}
|
||||
|
||||
return license_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Entschlüsselung des Lizenzschlüssels: {e}")
|
||||
return None
|
||||
|
||||
def generate_license_key(self, license_type: str = "basic", validity_months: int = 12,
|
||||
features: List[str] = None) -> str:
|
||||
"""
|
||||
Generiert einen Lizenzschlüssel.
|
||||
|
||||
Args:
|
||||
license_type: Art der Lizenz ("basic", "premium", "enterprise")
|
||||
validity_months: Gültigkeitsdauer in Monaten
|
||||
features: Liste der Funktionen
|
||||
|
||||
Returns:
|
||||
str: Generierter Lizenzschlüssel
|
||||
"""
|
||||
# Platzhalterimplementierung - in einer echten Implementierung würde hier
|
||||
# eine sichere Schlüsselgenerierung stehen
|
||||
|
||||
# Verwende die Eingabeparameter als Seed für die Generierung
|
||||
seed = f"{license_type}{validity_months}{','.join(features or [])}{time.time()}"
|
||||
random.seed(hashlib.md5(seed.encode()).hexdigest())
|
||||
|
||||
# Generiere 4 Teile mit jeweils 5 Zeichen (Großbuchstaben und Zahlen)
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
parts = []
|
||||
|
||||
for _ in range(4):
|
||||
part = ''.join(random.choice(chars) for _ in range(5))
|
||||
parts.append(part)
|
||||
|
||||
# Stelle sicher, dass der letzte Buchstabe des ersten Teils
|
||||
# ein XOR der ersten Buchstaben der anderen Teile ist
|
||||
# (für die einfache Prüfsumme)
|
||||
calc_char = chr(ord(parts[1][0]) ^ ord(parts[2][0]) ^ ord(parts[3][0]))
|
||||
parts[0] = parts[0][:-1] + calc_char
|
||||
|
||||
# Verbinde die Teile mit Bindestrichen
|
||||
license_key = '-'.join(parts)
|
||||
|
||||
return license_key
|
||||
|
||||
def sign_data(self, data: str) -> str:
|
||||
"""
|
||||
Signiert Daten mit dem geheimen Schlüssel.
|
||||
|
||||
Args:
|
||||
data: Zu signierende Daten
|
||||
|
||||
Returns:
|
||||
str: Signatur
|
||||
"""
|
||||
return hmac.new(
|
||||
self.SECRET_KEY.encode(),
|
||||
data.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
def verify_signature(self, data: str, signature: str) -> bool:
|
||||
"""
|
||||
Überprüft die Signatur von Daten.
|
||||
|
||||
Args:
|
||||
data: Signierte Daten
|
||||
signature: Zu überprüfende Signatur
|
||||
|
||||
Returns:
|
||||
bool: True, wenn die Signatur gültig ist, False sonst
|
||||
"""
|
||||
expected_signature = self.sign_data(data)
|
||||
return hmac.compare_digest(expected_signature, signature)
|
||||
|
||||
def encode_license_data(self, data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Kodiert Lizenzdaten zur sicheren Übertragung.
|
||||
|
||||
Args:
|
||||
data: Zu kodierende Lizenzdaten
|
||||
|
||||
Returns:
|
||||
str: Kodierte Lizenzdaten
|
||||
"""
|
||||
# Daten in JSON konvertieren
|
||||
json_data = json.dumps(data, sort_keys=True)
|
||||
|
||||
# Signatur hinzufügen
|
||||
signature = self.sign_data(json_data)
|
||||
|
||||
# Zusammen mit der Signatur kodieren
|
||||
combined = f"{json_data}|{signature}"
|
||||
encoded = base64.b64encode(combined.encode()).decode()
|
||||
|
||||
return encoded
|
||||
|
||||
def decode_license_data(self, encoded: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Dekodiert und überprüft kodierte Lizenzdaten.
|
||||
|
||||
Args:
|
||||
encoded: Kodierte Lizenzdaten
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: Dekodierte Lizenzdaten oder None bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Dekodieren
|
||||
decoded = base64.b64decode(encoded).decode()
|
||||
|
||||
# In Daten und Signatur aufteilen
|
||||
json_data, signature = decoded.split('|', 1)
|
||||
|
||||
# Signatur überprüfen
|
||||
if not self.verify_signature(json_data, signature):
|
||||
logger.warning("Ungültige Signatur in lizenzierten Daten")
|
||||
return None
|
||||
|
||||
# JSON parsen
|
||||
data = json.loads(json_data)
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Dekodieren der Lizenzdaten: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# Beispielnutzung, wenn direkt ausgeführt
|
||||
if __name__ == "__main__":
|
||||
# Konfiguriere Logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Beispiel für LicenseValidator
|
||||
validator = LicenseValidator()
|
||||
|
||||
# Generiere einen Lizenzschlüssel
|
||||
features = ["multi_account", "proxy_rotation", "advanced_analytics"]
|
||||
key = validator.generate_license_key("premium", 12, features)
|
||||
print(f"Generierter Lizenzschlüssel: {key}")
|
||||
|
||||
# Validiere den Schlüssel
|
||||
is_valid_format = validator.validate_key_format(key)
|
||||
is_valid_checksum = validator.validate_key_checksum(key)
|
||||
print(f"Format gültig: {is_valid_format}")
|
||||
print(f"Prüfsumme gültig: {is_valid_checksum}")
|
||||
|
||||
# Entschlüssele Lizenzdaten
|
||||
license_data = validator.decrypt_license_data(key)
|
||||
if license_data:
|
||||
print("\nEntschlüsselte Lizenzdaten:")
|
||||
for k, v in license_data.items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
# Beispiel für Kodierung und Dekodierung
|
||||
test_data = {
|
||||
"name": "Test License",
|
||||
"type": "premium",
|
||||
"expires": "2026-01-01"
|
||||
}
|
||||
|
||||
encoded = validator.encode_license_data(test_data)
|
||||
print(f"\nKodierte Daten: {encoded}")
|
||||
|
||||
decoded = validator.decode_license_data(encoded)
|
||||
if decoded:
|
||||
print("\nDekodierte Daten:")
|
||||
for k, v in decoded.items():
|
||||
print(f" {k}: {v}")
|
||||
0
localization/__init__.py
Normale Datei
BIN
localization/__pycache__/__init__.cpython-310.pyc
Normale Datei
BIN
localization/__pycache__/__init__.cpython-313.pyc
Normale Datei
BIN
localization/__pycache__/language_manager.cpython-310.pyc
Normale Datei
BIN
localization/__pycache__/language_manager.cpython-313.pyc
Normale Datei
272
localization/language_manager.py
Normale Datei
@ -0,0 +1,272 @@
|
||||
# Path: localization/language_manager.py
|
||||
|
||||
"""
|
||||
Sprachmanager für die Übersetzung der Benutzeroberfläche.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Any, Optional
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QSettings
|
||||
|
||||
logger = logging.getLogger("language_manager")
|
||||
|
||||
class LanguageManager(QObject):
|
||||
"""Verwaltet die Sprachen und Übersetzungen der Anwendung."""
|
||||
|
||||
# Signal, das ausgelöst wird, wenn sich die Sprache ändert
|
||||
language_changed = pyqtSignal(str)
|
||||
|
||||
# Signal, das den Status des Sprachwechsels anzeigt (True = läuft, False = abgeschlossen)
|
||||
language_change_status = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, app=None):
|
||||
"""
|
||||
Initialisiert den Sprachmanager.
|
||||
|
||||
Args:
|
||||
app: Die QApplication-Instanz
|
||||
"""
|
||||
super().__init__()
|
||||
self.app = app
|
||||
self.current_language = "de" # Standard ist Deutsch
|
||||
self.translations = {}
|
||||
self.available_languages = {}
|
||||
self.last_change_time = 0 # Zeitpunkt des letzten Sprachwechsels
|
||||
self.change_cooldown = 0.5 # Cooldown in Sekunden zwischen Sprachwechseln
|
||||
self.is_changing_language = False # Status-Variable für laufenden Sprachwechsel
|
||||
|
||||
# Basisverzeichnis für Sprachdateien ermitteln
|
||||
self.base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
self.languages_dir = os.path.join(self.base_dir, "languages")
|
||||
|
||||
# Verfügbare Sprachen ermitteln und laden
|
||||
self._discover_languages()
|
||||
|
||||
# Lade die gespeicherte Sprache, falls vorhanden
|
||||
self.settings = QSettings("Chimaira", "SocialMediaAccountGenerator")
|
||||
saved_language = self.settings.value("language", "de")
|
||||
|
||||
if saved_language in self.available_languages:
|
||||
self.current_language = saved_language
|
||||
|
||||
self._load_language(self.current_language)
|
||||
|
||||
logger.info(f"Sprachmanager initialisiert mit Sprache: {self.current_language}")
|
||||
|
||||
def _discover_languages(self):
|
||||
"""Ermittelt die verfügbaren Sprachen aus den Sprachdateien."""
|
||||
self.available_languages = {}
|
||||
|
||||
try:
|
||||
# Alle JSON-Dateien im Sprachverzeichnis suchen
|
||||
language_files = []
|
||||
for filename in os.listdir(self.languages_dir):
|
||||
if filename.endswith(".json"):
|
||||
language_code = filename.split(".")[0]
|
||||
language_path = os.path.join(self.languages_dir, filename)
|
||||
language_files.append((language_code, language_path))
|
||||
|
||||
# Definierte Reihenfolge der Sprachen
|
||||
ordered_codes = ["de", "en", "fr", "es", "ja"]
|
||||
|
||||
# Sortierte Liste erstellen
|
||||
for code in ordered_codes:
|
||||
for language_code, language_path in language_files:
|
||||
if language_code == code:
|
||||
# Sprachinformationen aus der Datei lesen
|
||||
try:
|
||||
with open(language_path, 'r', encoding='utf-8') as file:
|
||||
language_data = json.load(file)
|
||||
language_name = language_data.get("language_name", language_code)
|
||||
self.available_languages[language_code] = {
|
||||
"name": language_name,
|
||||
"path": language_path
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Sprachinformationen für {language_code}: {e}")
|
||||
|
||||
# Eventuelle restliche Sprachen hinzufügen
|
||||
for language_code, language_path in language_files:
|
||||
if language_code not in self.available_languages:
|
||||
try:
|
||||
with open(language_path, 'r', encoding='utf-8') as file:
|
||||
language_data = json.load(file)
|
||||
language_name = language_data.get("language_name", language_code)
|
||||
self.available_languages[language_code] = {
|
||||
"name": language_name,
|
||||
"path": language_path
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Sprachinformationen für {language_code}: {e}")
|
||||
|
||||
logger.info(f"Verfügbare Sprachen: {', '.join(self.available_languages.keys())}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ermitteln der verfügbaren Sprachen: {e}")
|
||||
|
||||
def _load_language(self, language_code: str) -> bool:
|
||||
"""
|
||||
Lädt die Übersetzungen für eine Sprache.
|
||||
|
||||
Args:
|
||||
language_code: Der Sprachcode (z.B. "de", "en")
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
if language_code not in self.available_languages:
|
||||
logger.error(f"Sprache {language_code} nicht verfügbar")
|
||||
return False
|
||||
|
||||
try:
|
||||
language_path = self.available_languages[language_code]["path"]
|
||||
|
||||
with open(language_path, 'r', encoding='utf-8') as file:
|
||||
self.translations = json.load(file)
|
||||
|
||||
self.current_language = language_code
|
||||
logger.info(f"Sprache {language_code} geladen")
|
||||
|
||||
# Signal auslösen, dass sich die Sprache geändert hat
|
||||
self.language_changed.emit(language_code)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Sprache {language_code}: {e}")
|
||||
return False
|
||||
|
||||
def get_text(self, key: str, default: str = None) -> str:
|
||||
"""
|
||||
Gibt den übersetzten Text für einen Schlüssel zurück.
|
||||
|
||||
Args:
|
||||
key: Der Schlüssel für den Text (z.B. "main.title")
|
||||
default: Der Standardtext, falls der Schlüssel nicht gefunden wurde
|
||||
|
||||
Returns:
|
||||
str: Der übersetzte Text oder der Standardtext
|
||||
"""
|
||||
# Wenn kein Standardtext angegeben wurde, verwende den Schlüssel
|
||||
if default is None:
|
||||
default = key
|
||||
|
||||
# Versuche, den Text aus den Übersetzungen zu holen
|
||||
try:
|
||||
# Unterstütze verschachtelte Schlüssel mit Punktnotation
|
||||
parts = key.split('.')
|
||||
result = self.translations
|
||||
|
||||
for part in parts:
|
||||
if part in result:
|
||||
result = result[part]
|
||||
else:
|
||||
return default
|
||||
|
||||
# Wenn das Ergebnis ein unterstützter Datentyp ist, gib es zurück
|
||||
if isinstance(result, (str, list, dict)):
|
||||
return result
|
||||
else:
|
||||
logger.warning(
|
||||
f"Schlüssel {key} hat unerwarteten Typ: {type(result)} - {result}"
|
||||
)
|
||||
return default
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Abrufen des Textes für Schlüssel {key}: {e}")
|
||||
return default
|
||||
|
||||
def change_language(self, language_code: str) -> bool:
|
||||
"""
|
||||
Ändert die aktive Sprache.
|
||||
|
||||
Args:
|
||||
language_code: Der Sprachcode (z.B. "de", "en")
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
# Wenn bereits ein Sprachwechsel läuft, blockiere weitere Wechsel
|
||||
if self.is_changing_language:
|
||||
logger.debug(f"Ein Sprachwechsel läuft bereits, ignoriere Wechsel zu {language_code}")
|
||||
return False
|
||||
|
||||
# Cooldown prüfen
|
||||
current_time = time.time()
|
||||
if current_time - self.last_change_time < self.change_cooldown:
|
||||
logger.debug("Sprachwechsel zu schnell hintereinander")
|
||||
return False
|
||||
|
||||
# Wenn es die gleiche Sprache ist, nichts tun
|
||||
if language_code == self.current_language:
|
||||
return True
|
||||
|
||||
# Status auf "Sprachwechsel läuft" setzen
|
||||
self.is_changing_language = True
|
||||
self.language_change_status.emit(True)
|
||||
|
||||
# Versuche die Sprache zu wechseln
|
||||
success = self._load_language(language_code)
|
||||
|
||||
if success:
|
||||
# Sprache in Einstellungen speichern
|
||||
self.settings.setValue("language", language_code)
|
||||
self.settings.sync()
|
||||
# Aktualisiere den Zeitpunkt des letzten Sprachwechsels
|
||||
self.last_change_time = time.time()
|
||||
|
||||
# Nach einer kurzen Verzögerung den Status zurücksetzen
|
||||
# um sicherzustellen, dass die UI-Aktualisierung abgeschlossen ist
|
||||
from PyQt5.QtCore import QTimer
|
||||
QTimer.singleShot(500, self._reset_change_status)
|
||||
|
||||
return success
|
||||
|
||||
def _reset_change_status(self):
|
||||
"""Setzt den Status des Sprachwechsels zurück."""
|
||||
self.is_changing_language = False
|
||||
self.language_change_status.emit(False)
|
||||
logger.debug("Sprachwechsel-Status zurückgesetzt")
|
||||
|
||||
def get_current_language(self) -> str:
|
||||
"""Gibt den Code der aktuellen Sprache zurück."""
|
||||
return self.current_language
|
||||
|
||||
def get_language_name(self, language_code: str = None) -> str:
|
||||
"""
|
||||
Gibt den Namen einer Sprache zurück.
|
||||
|
||||
Args:
|
||||
language_code: Der Sprachcode oder None für die aktuelle Sprache
|
||||
|
||||
Returns:
|
||||
str: Der Name der Sprache
|
||||
"""
|
||||
if language_code is None:
|
||||
language_code = self.current_language
|
||||
|
||||
if language_code in self.available_languages:
|
||||
return self.available_languages[language_code].get("name", language_code)
|
||||
else:
|
||||
return language_code
|
||||
|
||||
def get_available_languages(self) -> Dict[str, str]:
|
||||
"""
|
||||
Gibt eine Liste der verfügbaren Sprachen zurück.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Ein Dictionary mit Sprachcodes als Schlüssel und Sprachnamen als Werten
|
||||
"""
|
||||
return {code: info.get("name", code) for code, info in self.available_languages.items()}
|
||||
|
||||
def is_language_change_in_progress(self) -> bool:
|
||||
"""
|
||||
Gibt zurück, ob gerade ein Sprachwechsel im Gange ist.
|
||||
|
||||
Returns:
|
||||
bool: True wenn ein Sprachwechsel läuft, False sonst
|
||||
"""
|
||||
return self.is_changing_language
|
||||
102
localization/languages/de.json
Normale Datei
@ -0,0 +1,102 @@
|
||||
{
|
||||
"language_name": "Deutsch",
|
||||
"main": {
|
||||
"title": "Social Media Account Generator",
|
||||
"subtitle": "Wählen Sie eine Plattform",
|
||||
"version": "Version 1.0.0",
|
||||
"overview": "Übersicht"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "↩ Zurück",
|
||||
"create": "Account erstellen",
|
||||
"cancel": "Abbrechen",
|
||||
"refresh": "Aktualisieren",
|
||||
"export": "Exportieren",
|
||||
"delete": "Löschen",
|
||||
"test_proxy": "Proxy testen",
|
||||
"save_proxy": "Proxy-Einstellungen speichern",
|
||||
"test_email": "E-Mail testen",
|
||||
"save_email": "E-Mail-Einstellungen speichern",
|
||||
"activate_license": "Lizenz aktivieren",
|
||||
"check_updates": "Auf Updates prüfen",
|
||||
"ok": "OK"
|
||||
},
|
||||
"tabs": {
|
||||
"generator": "Account Generator",
|
||||
"accounts": "Konten",
|
||||
"settings": "Einstellungen",
|
||||
"about": "Über"
|
||||
},
|
||||
"menu": {
|
||||
"about": "Über",
|
||||
"about_app": "Über die Anwendung",
|
||||
"language": "Sprache"
|
||||
},
|
||||
"status": {
|
||||
"ready": "Bereit"
|
||||
},
|
||||
"generator_tab": {
|
||||
"form_title": "Account-Informationen",
|
||||
"first_name_label": "Vorname:",
|
||||
"first_name_placeholder": "z.B. Max",
|
||||
"last_name_label": "Nachname:",
|
||||
"last_name_placeholder": "z.B. Mustermann",
|
||||
"age_label": "Alter:",
|
||||
"age_placeholder": "Alter zwischen 13 und 99",
|
||||
"registration_method_label": "Registrierungsmethode:",
|
||||
"email_radio": "E-Mail",
|
||||
"phone_radio": "Telefon",
|
||||
"phone_label": "Telefonnummer:",
|
||||
"phone_placeholder": "z.B. +49123456789",
|
||||
"email_domain_label": "E-Mail-Domain:",
|
||||
"proxy_use": "Proxy verwenden",
|
||||
"proxy_label": "Proxy:",
|
||||
"proxy_type_label": "Typ:",
|
||||
"proxy_type_ipv4": "IPv4",
|
||||
"proxy_type_ipv6": "IPv6",
|
||||
"proxy_type_mobile": "Mobile",
|
||||
"headless": "Browser im Hintergrund ausführen",
|
||||
"debug": "Debug-Modus (detaillierte Protokollierung)",
|
||||
"log_title": "Log",
|
||||
"error_title": "Fehler",
|
||||
"first_name_error": "Bitte geben Sie einen Vornamen ein.",
|
||||
"last_name_error": "Bitte geben Sie einen Nachnamen ein.",
|
||||
"age_empty_error": "Bitte geben Sie ein Alter ein.",
|
||||
"age_int_error": "Das Alter muss eine ganze Zahl sein.",
|
||||
"age_range_error": "Das Alter muss zwischen 13 und 99 liegen.",
|
||||
"phone_error": "Bitte geben Sie eine Telefonnummer ein.",
|
||||
"tiktok_category_label": "Kategorie/Nische:",
|
||||
"tiktok_category_general": "Allgemein",
|
||||
"tiktok_category_gaming": "Gaming",
|
||||
"tiktok_category_fashion": "Mode",
|
||||
"tiktok_category_fitness": "Fitness",
|
||||
"tiktok_category_travel": "Reisen",
|
||||
"tiktok_category_cooking": "Kochen",
|
||||
"tiktok_category_technology": "Technologie",
|
||||
"tiktok_category_education": "Bildung"
|
||||
},
|
||||
"accounts_tab": {
|
||||
"headers": [
|
||||
"ID",
|
||||
"Benutzername",
|
||||
"Passwort",
|
||||
"E-Mail",
|
||||
"Handynummer",
|
||||
"Name",
|
||||
"Plattform",
|
||||
"Erstellt am"
|
||||
],
|
||||
"no_selection_title": "Kein Konto ausgewählt",
|
||||
"no_selection_text": "Bitte wählen Sie ein Konto zum Löschen aus.",
|
||||
"delete_title": "Konto löschen",
|
||||
"delete_text": "Möchten Sie das Konto '{username}' wirklich löschen?",
|
||||
"delete_success_title": "Erfolg",
|
||||
"delete_success_text": "Konto '{username}' wurde gelöscht.",
|
||||
"delete_error_title": "Fehler",
|
||||
"delete_error_text": "Konto '{username}' konnte nicht gelöscht werden."
|
||||
},
|
||||
"about_dialog": {
|
||||
"support": "Für Support kontaktieren Sie uns unter: support@example.com",
|
||||
"license": "Diese Software ist lizenzpflichtig und darf nur mit gültiger Lizenz verwendet werden."
|
||||
}
|
||||
}
|
||||
102
localization/languages/en.json
Normale Datei
@ -0,0 +1,102 @@
|
||||
{
|
||||
"language_name": "English",
|
||||
"main": {
|
||||
"title": "Social Media Account Generator",
|
||||
"subtitle": "Select a platform",
|
||||
"version": "Version 1.0.0",
|
||||
"overview": "Overview"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "↩ Back",
|
||||
"create": "Create Account",
|
||||
"cancel": "Cancel",
|
||||
"refresh": "Refresh",
|
||||
"export": "Export",
|
||||
"delete": "Delete",
|
||||
"test_proxy": "Test Proxy",
|
||||
"save_proxy": "Save Proxy Settings",
|
||||
"test_email": "Test Email",
|
||||
"save_email": "Save Email Settings",
|
||||
"activate_license": "Activate License",
|
||||
"check_updates": "Check for Updates",
|
||||
"ok": "OK"
|
||||
},
|
||||
"tabs": {
|
||||
"generator": "Account Generator",
|
||||
"accounts": "Accounts",
|
||||
"settings": "Settings",
|
||||
"about": "About"
|
||||
},
|
||||
"menu": {
|
||||
"about": "About",
|
||||
"about_app": "About the Application",
|
||||
"language": "Language"
|
||||
},
|
||||
"status": {
|
||||
"ready": "Ready"
|
||||
},
|
||||
"generator_tab": {
|
||||
"form_title": "Account Information",
|
||||
"first_name_label": "First Name:",
|
||||
"first_name_placeholder": "e.g. Max",
|
||||
"last_name_label": "Last Name:",
|
||||
"last_name_placeholder": "e.g. Mustermann",
|
||||
"age_label": "Age:",
|
||||
"age_placeholder": "Age between 13 and 99",
|
||||
"registration_method_label": "Registration Method:",
|
||||
"email_radio": "Email",
|
||||
"phone_radio": "Phone",
|
||||
"phone_label": "Phone Number:",
|
||||
"phone_placeholder": "e.g. +49123456789",
|
||||
"email_domain_label": "Email Domain:",
|
||||
"proxy_use": "Use Proxy",
|
||||
"proxy_label": "Proxy:",
|
||||
"proxy_type_label": "Type:",
|
||||
"proxy_type_ipv4": "IPv4",
|
||||
"proxy_type_ipv6": "IPv6",
|
||||
"proxy_type_mobile": "Mobile",
|
||||
"headless": "Run browser headless",
|
||||
"debug": "Debug mode (detailed logging)",
|
||||
"log_title": "Log",
|
||||
"error_title": "Error",
|
||||
"first_name_error": "Please enter a first name.",
|
||||
"last_name_error": "Please enter a last name.",
|
||||
"age_empty_error": "Please enter an age.",
|
||||
"age_int_error": "Age must be a whole number.",
|
||||
"age_range_error": "Age must be between 13 and 99.",
|
||||
"phone_error": "Please enter a phone number.",
|
||||
"tiktok_category_label": "Category/Niche:",
|
||||
"tiktok_category_general": "General",
|
||||
"tiktok_category_gaming": "Gaming",
|
||||
"tiktok_category_fashion": "Fashion",
|
||||
"tiktok_category_fitness": "Fitness",
|
||||
"tiktok_category_travel": "Travel",
|
||||
"tiktok_category_cooking": "Cooking",
|
||||
"tiktok_category_technology": "Technology",
|
||||
"tiktok_category_education": "Education"
|
||||
},
|
||||
"accounts_tab": {
|
||||
"headers": [
|
||||
"ID",
|
||||
"Username",
|
||||
"Password",
|
||||
"Email",
|
||||
"Phone",
|
||||
"Name",
|
||||
"Platform",
|
||||
"Created At"
|
||||
],
|
||||
"no_selection_title": "No Account Selected",
|
||||
"no_selection_text": "Please select an account to delete.",
|
||||
"delete_title": "Delete Account",
|
||||
"delete_text": "Do you really want to delete the account '{username}'?",
|
||||
"delete_success_title": "Success",
|
||||
"delete_success_text": "Account '{username}' was deleted.",
|
||||
"delete_error_title": "Error",
|
||||
"delete_error_text": "Account '{username}' could not be deleted."
|
||||
},
|
||||
"about_dialog": {
|
||||
"support": "For support contact us at: support@example.com",
|
||||
"license": "This software is licensed and may only be used with a valid license."
|
||||
}
|
||||
}
|
||||
102
localization/languages/es.json
Normale Datei
@ -0,0 +1,102 @@
|
||||
{
|
||||
"language_name": "Español",
|
||||
"main": {
|
||||
"title": "Generador de Cuentas de Redes Sociales",
|
||||
"subtitle": "Seleccione una plataforma",
|
||||
"version": "Versión 1.0.0",
|
||||
"overview": "Resumen"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "↩ Volver",
|
||||
"create": "Crear cuenta",
|
||||
"cancel": "Cancelar",
|
||||
"refresh": "Actualizar",
|
||||
"export": "Exportar",
|
||||
"delete": "Eliminar",
|
||||
"test_proxy": "Probar proxy",
|
||||
"save_proxy": "Guardar ajustes de proxy",
|
||||
"test_email": "Probar correo",
|
||||
"save_email": "Guardar ajustes de correo",
|
||||
"activate_license": "Activar licencia",
|
||||
"check_updates": "Buscar actualizaciones",
|
||||
"ok": "Aceptar"
|
||||
},
|
||||
"tabs": {
|
||||
"generator": "Generador de Cuentas",
|
||||
"accounts": "Cuentas",
|
||||
"settings": "Configuración",
|
||||
"about": "Acerca de"
|
||||
},
|
||||
"menu": {
|
||||
"about": "Acerca de",
|
||||
"about_app": "Acerca de la aplicación",
|
||||
"language": "Idioma"
|
||||
},
|
||||
"status": {
|
||||
"ready": "Listo"
|
||||
},
|
||||
"generator_tab": {
|
||||
"form_title": "Información de la cuenta",
|
||||
"first_name_label": "Nombre:",
|
||||
"first_name_placeholder": "p.ej. Max",
|
||||
"last_name_label": "Apellido:",
|
||||
"last_name_placeholder": "p.ej. Mustermann",
|
||||
"age_label": "Edad:",
|
||||
"age_placeholder": "Edad entre 13 y 99",
|
||||
"registration_method_label": "Método de registro:",
|
||||
"email_radio": "Correo electrónico",
|
||||
"phone_radio": "Teléfono",
|
||||
"phone_label": "Número de teléfono:",
|
||||
"phone_placeholder": "p.ej. +49123456789",
|
||||
"email_domain_label": "Dominio de correo:",
|
||||
"proxy_use": "Usar proxy",
|
||||
"proxy_label": "Proxy:",
|
||||
"proxy_type_label": "Tipo:",
|
||||
"proxy_type_ipv4": "IPv4",
|
||||
"proxy_type_ipv6": "IPv6",
|
||||
"proxy_type_mobile": "Móvil",
|
||||
"headless": "Ejecutar navegador en segundo plano",
|
||||
"debug": "Modo depuración (registro detallado)",
|
||||
"log_title": "Registro",
|
||||
"error_title": "Error",
|
||||
"first_name_error": "Por favor, introduzca un nombre.",
|
||||
"last_name_error": "Por favor, introduzca un apellido.",
|
||||
"age_empty_error": "Por favor, introduzca una edad.",
|
||||
"age_int_error": "La edad debe ser un número entero.",
|
||||
"age_range_error": "La edad debe estar entre 13 y 99.",
|
||||
"phone_error": "Por favor, introduzca un número de teléfono.",
|
||||
"tiktok_category_label": "Categoría/Nicho:",
|
||||
"tiktok_category_general": "General",
|
||||
"tiktok_category_gaming": "Juegos",
|
||||
"tiktok_category_fashion": "Moda",
|
||||
"tiktok_category_fitness": "Fitness",
|
||||
"tiktok_category_travel": "Viajes",
|
||||
"tiktok_category_cooking": "Cocina",
|
||||
"tiktok_category_technology": "Tecnología",
|
||||
"tiktok_category_education": "Educación"
|
||||
},
|
||||
"accounts_tab": {
|
||||
"headers": [
|
||||
"ID",
|
||||
"Nombre de usuario",
|
||||
"Contraseña",
|
||||
"Correo",
|
||||
"Teléfono",
|
||||
"Nombre",
|
||||
"Plataforma",
|
||||
"Creado el"
|
||||
],
|
||||
"no_selection_title": "Ninguna cuenta seleccionada",
|
||||
"no_selection_text": "Seleccione una cuenta para eliminar.",
|
||||
"delete_title": "Eliminar cuenta",
|
||||
"delete_text": "¿Realmente desea eliminar la cuenta '{username}'?",
|
||||
"delete_success_title": "Éxito",
|
||||
"delete_success_text": "La cuenta '{username}' fue eliminada.",
|
||||
"delete_error_title": "Error",
|
||||
"delete_error_text": "No se pudo eliminar la cuenta '{username}'."
|
||||
},
|
||||
"about_dialog": {
|
||||
"support": "Para soporte contáctenos en: support@example.com",
|
||||
"license": "Este software está sujeto a licencia y solo puede utilizarse con una licencia válida."
|
||||
}
|
||||
}
|
||||
102
localization/languages/fr.json
Normale Datei
@ -0,0 +1,102 @@
|
||||
{
|
||||
"language_name": "Français",
|
||||
"main": {
|
||||
"title": "Générateur de Comptes de Médias Sociaux",
|
||||
"subtitle": "Sélectionnez une plateforme",
|
||||
"version": "Version 1.0.0",
|
||||
"overview": "Aperçu"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "↩ Retour",
|
||||
"create": "Créer un compte",
|
||||
"cancel": "Annuler",
|
||||
"refresh": "Rafraîchir",
|
||||
"export": "Exporter",
|
||||
"delete": "Supprimer",
|
||||
"test_proxy": "Tester le proxy",
|
||||
"save_proxy": "Enregistrer le proxy",
|
||||
"test_email": "Tester l'e-mail",
|
||||
"save_email": "Enregistrer l'e-mail",
|
||||
"activate_license": "Activer la licence",
|
||||
"check_updates": "Vérifier les mises à jour",
|
||||
"ok": "OK"
|
||||
},
|
||||
"tabs": {
|
||||
"generator": "Générateur de Compte",
|
||||
"accounts": "Comptes",
|
||||
"settings": "Paramètres",
|
||||
"about": "À Propos"
|
||||
},
|
||||
"menu": {
|
||||
"about": "À propos",
|
||||
"about_app": "À propos de l'application",
|
||||
"language": "Langue"
|
||||
},
|
||||
"status": {
|
||||
"ready": "Prêt"
|
||||
},
|
||||
"generator_tab": {
|
||||
"form_title": "Informations du compte",
|
||||
"first_name_label": "Prénom:",
|
||||
"first_name_placeholder": "ex. Max",
|
||||
"last_name_label": "Nom:",
|
||||
"last_name_placeholder": "ex. Mustermann",
|
||||
"age_label": "Âge:",
|
||||
"age_placeholder": "Âge entre 13 et 99",
|
||||
"registration_method_label": "Méthode d'enregistrement:",
|
||||
"email_radio": "E-mail",
|
||||
"phone_radio": "Téléphone",
|
||||
"phone_label": "Numéro de téléphone:",
|
||||
"phone_placeholder": "ex. +49123456789",
|
||||
"email_domain_label": "Domaine e-mail:",
|
||||
"proxy_use": "Utiliser un proxy",
|
||||
"proxy_label": "Proxy:",
|
||||
"proxy_type_label": "Type:",
|
||||
"proxy_type_ipv4": "IPv4",
|
||||
"proxy_type_ipv6": "IPv6",
|
||||
"proxy_type_mobile": "Mobile",
|
||||
"headless": "Exécuter le navigateur en arrière-plan",
|
||||
"debug": "Mode débogage (journal détaillé)",
|
||||
"log_title": "Journal",
|
||||
"error_title": "Erreur",
|
||||
"first_name_error": "Veuillez saisir un prénom.",
|
||||
"last_name_error": "Veuillez saisir un nom.",
|
||||
"age_empty_error": "Veuillez saisir un âge.",
|
||||
"age_int_error": "L'âge doit être un nombre entier.",
|
||||
"age_range_error": "L'âge doit être compris entre 13 et 99.",
|
||||
"phone_error": "Veuillez saisir un numéro de téléphone.",
|
||||
"tiktok_category_label": "Catégorie/Niche:",
|
||||
"tiktok_category_general": "Général",
|
||||
"tiktok_category_gaming": "Jeux",
|
||||
"tiktok_category_fashion": "Mode",
|
||||
"tiktok_category_fitness": "Fitness",
|
||||
"tiktok_category_travel": "Voyage",
|
||||
"tiktok_category_cooking": "Cuisine",
|
||||
"tiktok_category_technology": "Technologie",
|
||||
"tiktok_category_education": "Éducation"
|
||||
},
|
||||
"accounts_tab": {
|
||||
"headers": [
|
||||
"ID",
|
||||
"Nom d'utilisateur",
|
||||
"Mot de passe",
|
||||
"E-mail",
|
||||
"Téléphone",
|
||||
"Nom",
|
||||
"Plateforme",
|
||||
"Créé le"
|
||||
],
|
||||
"no_selection_title": "Aucun compte sélectionné",
|
||||
"no_selection_text": "Veuillez sélectionner un compte à supprimer.",
|
||||
"delete_title": "Supprimer le compte",
|
||||
"delete_text": "Voulez-vous vraiment supprimer le compte '{username}' ?",
|
||||
"delete_success_title": "Succès",
|
||||
"delete_success_text": "Le compte '{username}' a été supprimé.",
|
||||
"delete_error_title": "Erreur",
|
||||
"delete_error_text": "Le compte '{username}' n'a pas pu être supprimé."
|
||||
},
|
||||
"about_dialog": {
|
||||
"support": "Pour toute assistance, contactez-nous à : support@example.com",
|
||||
"license": "Ce logiciel est soumis à licence et ne peut être utilisé qu'avec une licence valide."
|
||||
}
|
||||
}
|
||||
102
localization/languages/ja.json
Normale Datei
@ -0,0 +1,102 @@
|
||||
{
|
||||
"language_name": "日本語",
|
||||
"main": {
|
||||
"title": "ソーシャルメディアアカウントジェネレーター",
|
||||
"subtitle": "プラットフォームを選択",
|
||||
"version": "バージョン 1.0.0",
|
||||
"overview": "概要"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "↩ 戻る",
|
||||
"create": "アカウント作成",
|
||||
"cancel": "キャンセル",
|
||||
"refresh": "更新",
|
||||
"export": "エクスポート",
|
||||
"delete": "削除",
|
||||
"test_proxy": "プロキシテスト",
|
||||
"save_proxy": "プロキシ設定を保存",
|
||||
"test_email": "メールをテスト",
|
||||
"save_email": "メール設定を保存",
|
||||
"activate_license": "ライセンスを有効化",
|
||||
"check_updates": "アップデートを確認",
|
||||
"ok": "OK"
|
||||
},
|
||||
"tabs": {
|
||||
"generator": "アカウント生成",
|
||||
"accounts": "アカウント",
|
||||
"settings": "設定",
|
||||
"about": "情報"
|
||||
},
|
||||
"menu": {
|
||||
"about": "情報",
|
||||
"about_app": "アプリについて",
|
||||
"language": "言語"
|
||||
},
|
||||
"status": {
|
||||
"ready": "完了"
|
||||
},
|
||||
"generator_tab": {
|
||||
"form_title": "アカウント情報",
|
||||
"first_name_label": "名:",
|
||||
"first_name_placeholder": "例: Max",
|
||||
"last_name_label": "姓:",
|
||||
"last_name_placeholder": "例: Mustermann",
|
||||
"age_label": "年齢:",
|
||||
"age_placeholder": "13~99歳",
|
||||
"registration_method_label": "登録方法:",
|
||||
"email_radio": "メール",
|
||||
"phone_radio": "電話",
|
||||
"phone_label": "電話番号:",
|
||||
"phone_placeholder": "例: +49123456789",
|
||||
"email_domain_label": "メールドメイン:",
|
||||
"proxy_use": "プロキシを使用",
|
||||
"proxy_label": "プロキシ:",
|
||||
"proxy_type_label": "タイプ:",
|
||||
"proxy_type_ipv4": "IPv4",
|
||||
"proxy_type_ipv6": "IPv6",
|
||||
"proxy_type_mobile": "モバイル",
|
||||
"headless": "ブラウザをバックグラウンドで実行",
|
||||
"debug": "デバッグモード (詳細ログ)",
|
||||
"log_title": "ログ",
|
||||
"error_title": "エラー",
|
||||
"first_name_error": "名を入力してください。",
|
||||
"last_name_error": "姓を入力してください。",
|
||||
"age_empty_error": "年齢を入力してください。",
|
||||
"age_int_error": "年齢は整数である必要があります。",
|
||||
"age_range_error": "年齢は13から99の間である必要があります。",
|
||||
"phone_error": "電話番号を入力してください。",
|
||||
"tiktok_category_label": "カテゴリ/ニッチ:",
|
||||
"tiktok_category_general": "一般",
|
||||
"tiktok_category_gaming": "ゲーム",
|
||||
"tiktok_category_fashion": "ファッション",
|
||||
"tiktok_category_fitness": "フィットネス",
|
||||
"tiktok_category_travel": "旅行",
|
||||
"tiktok_category_cooking": "料理",
|
||||
"tiktok_category_technology": "テクノロジー",
|
||||
"tiktok_category_education": "教育"
|
||||
},
|
||||
"accounts_tab": {
|
||||
"headers": [
|
||||
"ID",
|
||||
"ユーザー名",
|
||||
"パスワード",
|
||||
"メール",
|
||||
"電話",
|
||||
"名前",
|
||||
"プラットフォーム",
|
||||
"作成日"
|
||||
],
|
||||
"no_selection_title": "アカウントが選択されていません",
|
||||
"no_selection_text": "削除するアカウントを選択してください。",
|
||||
"delete_title": "アカウントの削除",
|
||||
"delete_text": "アカウント '{username}' を本当に削除しますか?",
|
||||
"delete_success_title": "成功",
|
||||
"delete_success_text": "アカウント '{username}' は削除されました。",
|
||||
"delete_error_title": "エラー",
|
||||
"delete_error_text": "アカウント '{username}' を削除できませんでした。"
|
||||
},
|
||||
"about_dialog": {
|
||||
"support": "サポートが必要な場合は次へご連絡ください: support@example.com",
|
||||
"license": "このソフトウェアはライセンス制で、有効なライセンスでのみ使用できます。"
|
||||
}
|
||||
}
|
||||
0
logs/.gitkeep
Normale Datei
34
logs/main.log
Normale Datei
@ -0,0 +1,34 @@
|
||||
2025-05-05 21:38:41,856 - main - INFO - Anwendung wird gestartet...
|
||||
2025-05-05 21:57:27,120 - main - INFO - Anwendung wird gestartet...
|
||||
2025-05-11 14:34:47,588 - main - INFO - Anwendung wird gestartet...
|
||||
2025-05-11 14:35:09,257 - main - INFO - Plattform ausgew<65>hlt: instagram
|
||||
2025-05-11 22:14:11,965 - main - INFO - Anwendung wird gestartet...
|
||||
2025-05-11 22:14:15,499 - main - INFO - Plattform ausgew<65>hlt: tiktok
|
||||
2025-05-11 22:51:23,397 - main - INFO - Zur<75>ck zur Plattformauswahl
|
||||
2025-05-11 22:55:24,731 - main - INFO - Anwendung wird gestartet...
|
||||
2025-05-11 22:55:27,782 - main - INFO - Plattform ausgew<65>hlt: tiktok
|
||||
2025-05-21 00:35:56,633 - main - INFO - Anwendung wird gestartet...
|
||||
2025-05-21 00:36:03,187 - main - INFO - Plattform ausgew<65>hlt: instagram
|
||||
2025-05-21 00:36:04,994 - main - INFO - Zur<75>ck zur Plattformauswahl
|
||||
2025-05-21 00:36:07,722 - main - INFO - Plattform ausgew<65>hlt: vk
|
||||
2025-05-21 00:36:07,722 - main - ERROR - Plattform 'vk' wird nicht unterst<73>tzt
|
||||
2025-05-21 00:36:12,032 - main - INFO - Plattform ausgew<65>hlt: tiktok
|
||||
2025-05-21 02:02:11,243 - main - INFO - Anwendung wird gestartet...
|
||||
2025-06-16 19:57:45,252 - main - INFO - Anwendung wird gestartet...
|
||||
2025-06-16 19:57:54,936 - main - INFO - Plattform ausgew<65>hlt: instagram
|
||||
2025-06-16 19:57:56,286 - main - INFO - Zur<75>ck zur Plattformauswahl
|
||||
2025-06-16 19:58:06,367 - main - INFO - Plattform ausgew<65>hlt: instagram
|
||||
2025-06-16 19:58:07,276 - main - INFO - Zur<75>ck zur Plattformauswahl
|
||||
2025-06-16 19:58:07,706 - main - INFO - Plattform ausgew<65>hlt: facebook
|
||||
2025-06-16 19:58:07,706 - main - ERROR - Plattform 'facebook' wird nicht unterst<73>tzt
|
||||
2025-06-16 19:58:09,011 - main - INFO - Plattform ausgew<65>hlt: tiktok
|
||||
2025-06-16 19:58:10,828 - main - INFO - Zur<75>ck zur Plattformauswahl
|
||||
2025-06-16 19:58:11,603 - main - INFO - Plattform ausgew<65>hlt: twitter
|
||||
2025-06-16 19:58:11,603 - main - ERROR - Plattform 'twitter' wird nicht unterst<73>tzt
|
||||
2025-06-16 19:58:13,674 - main - INFO - Plattform ausgew<65>hlt: tiktok
|
||||
2025-06-16 19:58:14,871 - main - INFO - Zur<75>ck zur Plattformauswahl
|
||||
2025-06-22 17:43:11,967 - main - INFO - Anwendung wird gestartet...
|
||||
2025-06-22 17:52:27,938 - main - INFO - Plattform ausgew<65>hlt: instagram
|
||||
2025-06-22 17:52:35,074 - main - INFO - Zur<75>ck zur Plattformauswahl
|
||||
2025-06-22 17:52:48,316 - main - INFO - Plattform ausgew<65>hlt: instagram
|
||||
2025-06-22 17:52:56,088 - main - INFO - Zur<75>ck zur Plattformauswahl
|
||||
46
main.py
Normale Datei
@ -0,0 +1,46 @@
|
||||
"""
|
||||
Social Media Account Generator - Hauptanwendung (Einstiegspunkt)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# Stelle sicher, dass das Hauptverzeichnis im Pythonpfad ist
|
||||
if os.path.dirname(os.path.abspath(__file__)) not in sys.path:
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import der Hauptcontroller-Klasse
|
||||
from controllers.main_controller import MainController
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Stelle sicher, dass benötigte Verzeichnisse existieren
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
os.makedirs("config", exist_ok=True)
|
||||
os.makedirs(os.path.join("logs", "screenshots"), exist_ok=True)
|
||||
os.makedirs("resources", exist_ok=True)
|
||||
os.makedirs(os.path.join("resources", "themes"), exist_ok=True)
|
||||
os.makedirs(os.path.join("resources", "icons"), exist_ok=True)
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion für die Anwendung."""
|
||||
# Logger initialisieren
|
||||
logger = setup_logger()
|
||||
logger.info("Anwendung wird gestartet...")
|
||||
|
||||
# QApplication erstellen
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# High DPI Skalierung aktivieren
|
||||
app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
|
||||
app.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
|
||||
# Hauptcontroller initialisieren (mit QApplication-Instanz)
|
||||
controller = MainController(app)
|
||||
|
||||
# Anwendung starten
|
||||
sys.exit(app.exec_())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
27
requirements.txt
Normale Datei
@ -0,0 +1,27 @@
|
||||
# requirements.txt
|
||||
|
||||
# Core dependencies
|
||||
PyQt5>=5.15.0
|
||||
playwright>=1.20.0
|
||||
requests>=2.25.0
|
||||
|
||||
# Database
|
||||
SQLite3>=3.30.0
|
||||
|
||||
# Email handling
|
||||
IMAPClient>=2.1.0
|
||||
email>=6.0.0
|
||||
|
||||
# Utilities
|
||||
python-dateutil>=2.8.1
|
||||
difflib>=3.7.0
|
||||
|
||||
# Logging
|
||||
logging>=0.5.1
|
||||
|
||||
# Type hints
|
||||
typing>=3.7.4
|
||||
|
||||
# Web automation and anti-detection
|
||||
undetected-playwright>=0.1.0
|
||||
random-user-agent>=1.0.1
|
||||
2
resources/icons/de.svg
Normale Datei
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--emojione" preserveAspectRatio="xMidYMid meet"><path d="M31.9 2C18.8 2 7.7 10.4 3.6 22h56.6C56.1 10.4 45 2 31.9 2z" fill="#3e4347"></path><path d="M31.9 62c13.1 0 24.2-8.3 28.3-20H3.6c4.1 11.7 15.2 20 28.3 20z" fill="#ffe62e"></path><path d="M3.6 22c-1.1 3.1-1.7 6.5-1.7 10s.6 6.9 1.7 10h56.6c1.1-3.1 1.7-6.5 1.7-10s-.6-6.9-1.7-10H3.6" fill="#ed4c5c"></path></svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 668 B |
50
resources/icons/en.svg
Normale Datei
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--emojione" preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g fill="#2a5f9e">
|
||||
|
||||
<path d="M22 60.3V46.5l-10.3 7.6c2.9 2.7 6.4 4.8 10.3 6.2">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M42 60.3c3.9-1.4 7.4-3.5 10.3-6.2L42 46.4v13.9">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M3.7 42c.3 1 .7 1.9 1.2 2.9L8.8 42H3.7">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M55.2 42l3.9 2.9c.4-.9.8-1.9 1.2-2.9h-5.1">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
<g fill="#ffffff">
|
||||
|
||||
<path d="M23.5 38H2.6c.3 1.4.7 2.7 1.1 4h5.1l-3.9 2.9c.8 1.7 1.7 3.2 2.8 4.7L18 42h4v2l-11.7 8.6l1.4 1.4L22 46.5v13.8c1.3.5 2.6.8 4 1.1V38h-2.5">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M61.4 38H38v23.4c1.4-.3 2.7-.7 4-1.1V46.5L52.3 54c1.4-1.3 2.6-2.7 3.8-4.2L45.4 42h6.8l6.1 4.5c.3-.5.6-1.1.8-1.6L55.2 42h5.1c.4-1.3.8-2.6 1.1-4">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
<g fill="#ed4c5c">
|
||||
|
||||
<path d="M7.7 49.6c.8 1.1 1.6 2.1 2.5 3.1L22 44.1v-2h-4L7.7 49.6">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M45.5 42l10.7 7.8c.4-.5.7-1 1.1-1.5c.1-.1.1-.2.2-.2c.3-.5.7-1.1 1-1.6L52.2 42h-6.7">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
<g fill="#2a5f9e">
|
||||
|
Nachher Breite: | Höhe: | Größe: 2.2 KiB |
114
resources/icons/es.svg
Normale Datei
@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--emojione" preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<path d="M2 32c0 5.9 1.7 11.4 4.6 16h50.7c2.9-4.6 4.6-10.1 4.6-16s-1.7-11.4-4.6-16H6.6C3.7 20.6 2 26.1 2 32z" fill="#ffce31">
|
||||
|
||||
</path>
|
||||
|
||||
<g fill="#ed4c5c">
|
||||
|
||||
<path d="M57.4 16C52.1 7.6 42.7 2 32 2S11.9 7.6 6.6 16h50.8z">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M6.6 48c5.3 8.4 14.7 14 25.4 14s20.1-5.6 25.4-14H6.6z">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
<g fill="#c8b100">
|
||||
|
||||
<path d="M9.2 28.7h3.2v1.8H9.2z">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M9.2 41.9h3.3v1.7H9.2z">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
<path d="M8.9 39.1c-.3.2-.5.4-.5.5c0 .1.1.2.3.3c.2.1.4.3.3.5c.2-.2.3-.4.3-.6c0-.3-.2-.6-.4-.7" fill="#ed4c5c">
|
||||
|
||||
</path>
|
||||
|
||||
<path fill="#ffffff" d="M9.7 30.5H12v11.4H9.7z">
|
||||
|
||||
</path>
|
||||
|
||||
<g fill="#ed4c5c">
|
||||
|
||||
<path d="M14.4 34.7c-.5-.2-1.4-.4-2.4-.4c-.3 0-.7 0-1.1.1c-1.4.2-2.5.8-2.4 1.2L8 34.5c-.1-.5 1.1-1.1 2.6-1.4c.5-.1 1-.1 1.4-.1c1 0 1.9.1 2.4.3v1.4">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M9.7 36.2c-.6 0-1.1-.2-1.1-.5c0-.2.2-.5.6-.7h.6l-.1 1.2">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M12 35.3c.4.1.7.2.9.3c.1.1-.3.5-.9.8v-1.1">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M8.2 38.4c-.1-.2.6-.6 1.5-.9c.4-.1.7-.3 1.2-.5c1.2-.5 2.2-1.2 2-1.4l.2 1.2c.1.2-.7.8-1.9 1.4c-.4.2-1.1.5-1.5.6c-.7.2-1.3.6-1.3.7l-.2-1.1">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
<g fill="#c8b100">
|
||||
|
||||
<path d="M30.7 28.7h3.2v1.8h-3.2z">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M30.6 41.9h3.3v1.7h-3.3z">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
<path d="M34.2 39.1c.3.2.5.4.5.5c0 .1-.1.2-.3.3c-.2.2-.4.5-.3.6c-.2-.2-.3-.4-.3-.6c0-.4.2-.7.4-.8" fill="#ed4c5c">
|
||||
|
||||
</path>
|
||||
|
||||
<path fill="#ffffff" d="M31.1 30.5h2.3v11.4h-2.3z">
|
||||
|
||||
</path>
|
||||
|
||||
<g fill="#ed4c5c">
|
||||
|
||||
<path d="M28.7 34.7c.5-.2 1.4-.4 2.4-.4c.3 0 .7 0 1.1.1c1.4.2 2.5.8 2.4 1.2l.5-1.2c.1-.5-1.1-1.1-2.6-1.4h-1.4c-1 0-1.9.1-2.4.3v1.4">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M33.4 36.2c.6 0 1.1-.2 1.1-.5c0-.2-.2-.5-.6-.7h-.6l.1 1.2">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M31.1 35.3c-.4.1-.7.2-.9.3c-.1.1.3.5.9.8v-1.1">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M34.9 38.4c.1-.2-.6-.6-1.5-.9c-.4-.1-.7-.3-1.2-.5c-1.2-.5-2.2-1.2-2-1.4l-.2 1.2c-.1.2.7.8 1.9 1.4c.4.2 1.1.5 1.5.6c.7.2 1.3.7 1.2.8l.3-1.2">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M21.5 22.3c1.9 0 5.8.4 7.2 1.8c-1.5 3.6-3.9 2.1-7.2 2.1c-3.2 0-5.7 1.5-7.2-2.1c1.4-1.4 5.2-1.8 7.2-1.8">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
<g fill="#c8b100">
|
||||
|
||||
<path d="M26.4 26.3c-1.2-.7-3-.8-4.9-.8c-1.9 0-3.7.2-4.9.8L17 28c1.1.3 2.7.5 4.5.5c1.8 0 3.3-.2 4.5-.5l.4-1.7">
|
||||
|
||||
</path>
|
||||
|
||||
<path d="M28.1 22c-.4-.3-1.2-.6-1.9-.6c-.3 0-.6 0-.9.1c0 0-.6-.8-2-.8c-.5 0-.9.1-1.3.3v-.1c-.1-.2-.3-.4-.5-.4s-.5.3-.5.5v.1c-.4-.2-.8-.3-1.3-.3c-1.4 0-2 .9-2 .8c-.3-.1-.6-.1-.9-.1c-4.6 0-2.3 3.1-2.3 3.1l.5-.6c-1.1-1.4-.1-2.2 1.9-2.2c.3 0 .5 0 .7.1c-.7 1 .6 1.9.6 1.9l.3-.5c-.7-.5-.8-2.2 1.2-2.2c.5 0 .9.1 1.3.4c0 .1-.1 1.5-.2 1.7l.8.7l.8-.7c-.1-.3-.2-1.6-.2-1.7c.3-.2.8-.4 1.3-.4c2.1 0 2.1 1.7 1.2 2.2l.3.5s1.1-.9.6-1.9c.2 0 .5-.1.7-.1c2.4 0 2.5 1.8 1.9 2.2l.4.6c-.2 0 .9-1.4-.5-2.6">
|
||||
|
||||
</path>
|
||||
|
Nachher Breite: | Höhe: | Größe: 5.5 KiB |
17
resources/icons/facebook.svg
Normale Datei
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>Facebook-color</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Color-" transform="translate(-200.000000, -160.000000)" fill="#4460A0">
|
||||
<path d="M225.638355,208 L202.649232,208 C201.185673,208 200,206.813592 200,205.350603 L200,162.649211 C200,161.18585 201.185859,160 202.649232,160 L245.350955,160 C246.813955,160 248,161.18585 248,162.649211 L248,205.350603 C248,206.813778 246.813769,208 245.350955,208 L233.119305,208 L233.119305,189.411755 L239.358521,189.411755 L240.292755,182.167586 L233.119305,182.167586 L233.119305,177.542641 C233.119305,175.445287 233.701712,174.01601 236.70929,174.01601 L240.545311,174.014333 L240.545311,167.535091 C239.881886,167.446808 237.604784,167.24957 234.955552,167.24957 C229.424834,167.24957 225.638355,170.625526 225.638355,176.825209 L225.638355,182.167586 L219.383122,182.167586 L219.383122,189.411755 L225.638355,189.411755 L225.638355,208 L225.638355,208 Z" id="Facebook">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 1.4 KiB |
2
resources/icons/fr.svg
Normale Datei
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--emojione" preserveAspectRatio="xMidYMid meet"><path d="M1.9 32c0 13.1 8.4 24.2 20 28.3V3.7C10.3 7.8 1.9 18.9 1.9 32z" fill="#428bc1"></path><path d="M61.9 32c0-13.1-8.3-24.2-20-28.3v56.6c11.7-4.1 20-15.2 20-28.3" fill="#ed4c5c"></path><path d="M21.9 60.3c3.1 1.1 6.5 1.7 10 1.7s6.9-.6 10-1.7V3.7C38.8 2.6 35.5 2 31.9 2s-6.9.6-10 1.7v56.6" fill="#ffffff"></path></svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 672 B |
27
resources/icons/instagram.svg
Normale Datei
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint0_radial_87_7153)"/>
|
||||
<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint1_radial_87_7153)"/>
|
||||
<rect x="2" y="2" width="28" height="28" rx="6" fill="url(#paint2_radial_87_7153)"/>
|
||||
<path d="M23 10.5C23 11.3284 22.3284 12 21.5 12C20.6716 12 20 11.3284 20 10.5C20 9.67157 20.6716 9 21.5 9C22.3284 9 23 9.67157 23 10.5Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 21C18.7614 21 21 18.7614 21 16C21 13.2386 18.7614 11 16 11C13.2386 11 11 13.2386 11 16C11 18.7614 13.2386 21 16 21ZM16 19C17.6569 19 19 17.6569 19 16C19 14.3431 17.6569 13 16 13C14.3431 13 13 14.3431 13 16C13 17.6569 14.3431 19 16 19Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 15.6C6 12.2397 6 10.5595 6.65396 9.27606C7.2292 8.14708 8.14708 7.2292 9.27606 6.65396C10.5595 6 12.2397 6 15.6 6H16.4C19.7603 6 21.4405 6 22.7239 6.65396C23.8529 7.2292 24.7708 8.14708 25.346 9.27606C26 10.5595 26 12.2397 26 15.6V16.4C26 19.7603 26 21.4405 25.346 22.7239C24.7708 23.8529 23.8529 24.7708 22.7239 25.346C21.4405 26 19.7603 26 16.4 26H15.6C12.2397 26 10.5595 26 9.27606 25.346C8.14708 24.7708 7.2292 23.8529 6.65396 22.7239C6 21.4405 6 19.7603 6 16.4V15.6ZM15.6 8H16.4C18.1132 8 19.2777 8.00156 20.1779 8.0751C21.0548 8.14674 21.5032 8.27659 21.816 8.43597C22.5686 8.81947 23.1805 9.43139 23.564 10.184C23.7234 10.4968 23.8533 10.9452 23.9249 11.8221C23.9984 12.7223 24 13.8868 24 15.6V16.4C24 18.1132 23.9984 19.2777 23.9249 20.1779C23.8533 21.0548 23.7234 21.5032 23.564 21.816C23.1805 22.5686 22.5686 23.1805 21.816 23.564C21.5032 23.7234 21.0548 23.8533 20.1779 23.9249C19.2777 23.9984 18.1132 24 16.4 24H15.6C13.8868 24 12.7223 23.9984 11.8221 23.9249C10.9452 23.8533 10.4968 23.7234 10.184 23.564C9.43139 23.1805 8.81947 22.5686 8.43597 21.816C8.27659 21.5032 8.14674 21.0548 8.0751 20.1779C8.00156 19.2777 8 18.1132 8 16.4V15.6C8 13.8868 8.00156 12.7223 8.0751 11.8221C8.14674 10.9452 8.27659 10.4968 8.43597 10.184C8.81947 9.43139 9.43139 8.81947 10.184 8.43597C10.4968 8.27659 10.9452 8.14674 11.8221 8.0751C12.7223 8.00156 13.8868 8 15.6 8Z" fill="white"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_87_7153" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12 23) rotate(-55.3758) scale(25.5196)">
|
||||
<stop stop-color="#B13589"/>
|
||||
<stop offset="0.79309" stop-color="#C62F94"/>
|
||||
<stop offset="1" stop-color="#8A3AC8"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_87_7153" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11 31) rotate(-65.1363) scale(22.5942)">
|
||||
<stop stop-color="#E0E8B7"/>
|
||||
<stop offset="0.444662" stop-color="#FB8A2E"/>
|
||||
<stop offset="0.71474" stop-color="#E2425C"/>
|
||||
<stop offset="1" stop-color="#E2425C" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial_87_7153" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(0.500002 3) rotate(-8.1301) scale(38.8909 8.31836)">
|
||||
<stop offset="0.156701" stop-color="#406ADC"/>
|
||||
<stop offset="0.467799" stop-color="#6A45BE"/>
|
||||
<stop offset="1" stop-color="#6A45BE" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 3.3 KiB |
2
resources/icons/ja.svg
Normale Datei
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--emojione" preserveAspectRatio="xMidYMid meet"><circle cx="32" cy="32" r="30" fill="#f5f5f5"></circle><circle cx="32" cy="32" r="12" fill="#ed4c5c"></circle></svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 467 B |
4
resources/icons/moon.svg
Normale Datei
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.0672 11.8568L20.4253 11.469L21.0672 11.8568ZM12.1432 2.93276L11.7553 2.29085V2.29085L12.1432 2.93276ZM21.25 12C21.25 17.1086 17.1086 21.25 12 21.25V22.75C17.9371 22.75 22.75 17.9371 22.75 12H21.25ZM12 21.25C6.89137 21.25 2.75 17.1086 2.75 12H1.25C1.25 17.9371 6.06294 22.75 12 22.75V21.25ZM2.75 12C2.75 6.89137 6.89137 2.75 12 2.75V1.25C6.06294 1.25 1.25 6.06294 1.25 12H2.75ZM15.5 14.25C12.3244 14.25 9.75 11.6756 9.75 8.5H8.25C8.25 12.5041 11.4959 15.75 15.5 15.75V14.25ZM20.4253 11.469C19.4172 13.1373 17.5882 14.25 15.5 14.25V15.75C18.1349 15.75 20.4407 14.3439 21.7092 12.2447L20.4253 11.469ZM9.75 8.5C9.75 6.41182 10.8627 4.5828 12.531 3.57467L11.7553 2.29085C9.65609 3.5593 8.25 5.86509 8.25 8.5H9.75ZM12 2.75C11.9115 2.75 11.8077 2.71008 11.7324 2.63168C11.6686 2.56527 11.6538 2.50244 11.6503 2.47703C11.6461 2.44587 11.6482 2.35557 11.7553 2.29085L12.531 3.57467C13.0342 3.27065 13.196 2.71398 13.1368 2.27627C13.0754 1.82126 12.7166 1.25 12 1.25V2.75ZM21.7092 12.2447C21.6444 12.3518 21.5541 12.3539 21.523 12.3497C21.4976 12.3462 21.4347 12.3314 21.3683 12.2676C21.2899 12.1923 21.25 12.0885 21.25 12H22.75C22.75 11.2834 22.1787 10.9246 21.7237 10.8632C21.286 10.804 20.7293 10.9658 20.4253 11.469L21.7092 12.2447Z" fill="#1C274C"/>
|
||||
</svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 1.5 KiB |
7
resources/icons/sun.svg
Normale Datei
@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg fill="#FFFFFF" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 207.628 207.628" xml:space="preserve" stroke="#FFFFFF">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
|
Nachher Breite: | Höhe: | Größe: 2.4 KiB |
15
resources/icons/tiktok.svg
Normale Datei
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 250 250" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g clip-rule="evenodd" fill-rule="evenodd">
|
||||
|
||||
<path d="M25 0h200c13.808 0 25 11.192 25 25v200c0 13.808-11.192 25-25 25H25c-13.808 0-25-11.192-25-25V25C0 11.192 11.192 0 25 0z" fill="#010101"/>
|
||||
|
||||
<path d="M156.98 230c7.607 0 13.774-6.117 13.774-13.662s-6.167-13.663-13.774-13.663h-2.075c7.607 0 13.774 6.118 13.774 13.663S162.512 230 154.905 230z" fill="#ee1d51"/>
|
||||
|
||||
<path d="M154.717 202.675h-2.075c-7.607 0-13.775 6.118-13.775 13.663S145.035 230 152.642 230h2.075c-7.608 0-13.775-6.117-13.775-13.662s6.167-13.663 13.775-13.663z" fill="#66c8cf"/>
|
||||
|
||||
<ellipse cx="154.811" cy="216.338" fill="#010101" rx="6.699" ry="6.643"/>
|
||||
|
||||
|
Nachher Breite: | Höhe: | Größe: 3.3 KiB |
2
resources/icons/twitter.svg
Normale Datei
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="-1.5 0 19 19" xmlns="http://www.w3.org/2000/svg" class="cf-icon-svg"><path d="M15.917 1.666v15.833H.083V1.666zM13.743 6.02a4.702 4.702 0 0 1-1.353.37 2.365 2.365 0 0 0 1.036-1.303 4.725 4.725 0 0 1-1.496.572 2.359 2.359 0 0 0-4.017 2.149 6.685 6.685 0 0 1-4.856-2.462 2.357 2.357 0 0 0 .728 3.146 2.339 2.339 0 0 1-1.067-.295v.03a2.359 2.359 0 0 0 1.89 2.311 2.362 2.362 0 0 1-1.064.04 2.36 2.36 0 0 0 2.202 1.637 4.733 4.733 0 0 1-2.928 1.01 4.838 4.838 0 0 1-.562-.034 6.702 6.702 0 0 0 10.318-5.647c0-.102-.002-.203-.007-.304a4.785 4.785 0 0 0 1.176-1.22z"/></svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 744 B |
7
resources/icons/vk.svg
Normale Datei
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="VK" role="img"
|
||||
viewBox="0 0 512 512"><rect
|
||||
width="512" height="512"
|
||||
rx="15%"
|
||||
fill="#5281b8"/><path fill="#ffffff" d="M274 363c5-1 14-3 14-15 0 0-1-30 13-34s32 29 51 42c14 9 25 8 25 8l51-1s26-2 14-23c-1-2-9-15-39-42-31-30-26-25 11-76 23-31 33-50 30-57-4-7-20-6-20-6h-57c-6 0-9 1-12 6 0 0-9 25-21 45-25 43-35 45-40 42-9-5-7-24-7-37 0-45 7-61-13-65-13-2-59-4-73 3-7 4-11 11-8 12 3 0 12 1 17 7 8 13 9 75-2 81-15 11-53-62-62-86-2-6-5-7-12-9H79c-6 0-15 1-11 13 27 56 83 193 184 192z"/></svg>
|
||||
|
Nachher Breite: | Höhe: | Größe: 656 B |
1
resources/themes/dark.qss
Normale Datei
@ -0,0 +1 @@
|
||||
/* Auto-generated empty stylesheet */
|
||||
1
resources/themes/light.qss
Normale Datei
@ -0,0 +1 @@
|
||||
/* Auto-generated empty stylesheet */
|
||||