commit 08ed93810528d5be1497960dd51af43f20dba262 Author: Claude Project Manager Date: Thu Jul 3 21:11:05 2025 +0200 Initial commit diff --git a/CLAUDE_PROJECT_README.md b/CLAUDE_PROJECT_README.md new file mode 100644 index 0000000..32e2ded --- /dev/null +++ b/CLAUDE_PROJECT_README.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c459dfa --- /dev/null +++ b/README.md @@ -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. + diff --git a/browser/__init__.py b/browser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/browser/__pycache__/__init__.cpython-310.pyc b/browser/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..4b5a7f9 Binary files /dev/null and b/browser/__pycache__/__init__.cpython-310.pyc differ diff --git a/browser/__pycache__/__init__.cpython-313.pyc b/browser/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..487ccab Binary files /dev/null and b/browser/__pycache__/__init__.cpython-313.pyc differ diff --git a/browser/__pycache__/fingerprint_protection.cpython-310.pyc b/browser/__pycache__/fingerprint_protection.cpython-310.pyc new file mode 100644 index 0000000..23383da Binary files /dev/null and b/browser/__pycache__/fingerprint_protection.cpython-310.pyc differ diff --git a/browser/__pycache__/fingerprint_protection.cpython-313.pyc b/browser/__pycache__/fingerprint_protection.cpython-313.pyc new file mode 100644 index 0000000..dc9fe53 Binary files /dev/null and b/browser/__pycache__/fingerprint_protection.cpython-313.pyc differ diff --git a/browser/__pycache__/playwright_extensions.cpython-310.pyc b/browser/__pycache__/playwright_extensions.cpython-310.pyc new file mode 100644 index 0000000..e6ee01a Binary files /dev/null and b/browser/__pycache__/playwright_extensions.cpython-310.pyc differ diff --git a/browser/__pycache__/playwright_extensions.cpython-313.pyc b/browser/__pycache__/playwright_extensions.cpython-313.pyc new file mode 100644 index 0000000..0e97392 Binary files /dev/null and b/browser/__pycache__/playwright_extensions.cpython-313.pyc differ diff --git a/browser/__pycache__/playwright_manager.cpython-310.pyc b/browser/__pycache__/playwright_manager.cpython-310.pyc new file mode 100644 index 0000000..05786d2 Binary files /dev/null and b/browser/__pycache__/playwright_manager.cpython-310.pyc differ diff --git a/browser/__pycache__/playwright_manager.cpython-313.pyc b/browser/__pycache__/playwright_manager.cpython-313.pyc new file mode 100644 index 0000000..559bd90 Binary files /dev/null and b/browser/__pycache__/playwright_manager.cpython-313.pyc differ diff --git a/browser/fingerprint_protection.py b/browser/fingerprint_protection.py new file mode 100644 index 0000000..22b5b92 --- /dev/null +++ b/browser/fingerprint_protection.py @@ -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 \ No newline at end of file diff --git a/browser/playwright_extensions.py b/browser/playwright_extensions.py new file mode 100644 index 0000000..8758d0d --- /dev/null +++ b/browser/playwright_extensions.py @@ -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 diff --git a/browser/playwright_manager.py b/browser/playwright_manager.py new file mode 100644 index 0000000..3dbe4d6 --- /dev/null +++ b/browser/playwright_manager.py @@ -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 \ No newline at end of file diff --git a/browser/stealth_config.py b/browser/stealth_config.py new file mode 100644 index 0000000..2e2dee4 --- /dev/null +++ b/browser/stealth_config.py @@ -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)) \ No newline at end of file diff --git a/config/.machine_id b/config/.machine_id new file mode 100644 index 0000000..b9be037 --- /dev/null +++ b/config/.machine_id @@ -0,0 +1 @@ +ae30d891-0b45-408e-8f47-75fada7cb094 \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/app_version.json b/config/app_version.json new file mode 100644 index 0000000..375a42e --- /dev/null +++ b/config/app_version.json @@ -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 +} \ No newline at end of file diff --git a/config/browser_config.json b/config/browser_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/email_config.json b/config/email_config.json new file mode 100644 index 0000000..3aa5d81 --- /dev/null +++ b/config/email_config.json @@ -0,0 +1,6 @@ +{ + "imap_server": "imap.ionos.de", + "imap_port": 993, + "imap_user": "info@z5m7q9dk3ah2v1plx6ju.com", + "imap_pass": "cz&ie.O9$!:!tYY@" + } \ No newline at end of file diff --git a/config/facebook_config.json b/config/facebook_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/instagram_config.json b/config/instagram_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/license.json b/config/license.json new file mode 100644 index 0000000..fe8c4b9 --- /dev/null +++ b/config/license.json @@ -0,0 +1,10 @@ +{ + "key": "", + "activation_date": "", + "expiry_date": "", + "status": "inactive", + "status_text": "Keine Lizenz aktiviert", + "features": [], + "last_online_check": "", + "signature": "" +} \ No newline at end of file diff --git a/config/license_config.json b/config/license_config.json new file mode 100644 index 0000000..87abb36 --- /dev/null +++ b/config/license_config.json @@ -0,0 +1,9 @@ +{ + "key": "", + "status": "inactive", + "hardware_id": "", + "activation_date": null, + "expiry_date": null, + "features": [], + "last_check": null + } \ No newline at end of file diff --git a/config/proxy_config.json b/config/proxy_config.json new file mode 100644 index 0000000..7b8b830 --- /dev/null +++ b/config/proxy_config.json @@ -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": "" + } + } \ No newline at end of file diff --git a/config/stealth_config.json b/config/stealth_config.json new file mode 100644 index 0000000..51e99dd --- /dev/null +++ b/config/stealth_config.json @@ -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" + } \ No newline at end of file diff --git a/config/theme.json b/config/theme.json new file mode 100644 index 0000000..016f40f --- /dev/null +++ b/config/theme.json @@ -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" + } + } +} diff --git a/config/tiktok_config.json b/config/tiktok_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/twitter_config.json b/config/twitter_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/update_config.json b/config/update_config.json new file mode 100644 index 0000000..c7fb841 --- /dev/null +++ b/config/update_config.json @@ -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": [] + } \ No newline at end of file diff --git a/config/user_agents.json b/config/user_agents.json new file mode 100644 index 0000000..26d2967 --- /dev/null +++ b/config/user_agents.json @@ -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" + ] + } \ No newline at end of file diff --git a/controllers/__pycache__/account_controller.cpython-310.pyc b/controllers/__pycache__/account_controller.cpython-310.pyc new file mode 100644 index 0000000..3bdb12a Binary files /dev/null and b/controllers/__pycache__/account_controller.cpython-310.pyc differ diff --git a/controllers/__pycache__/account_controller.cpython-313.pyc b/controllers/__pycache__/account_controller.cpython-313.pyc new file mode 100644 index 0000000..b09f4d0 Binary files /dev/null and b/controllers/__pycache__/account_controller.cpython-313.pyc differ diff --git a/controllers/__pycache__/main_controller.cpython-310.pyc b/controllers/__pycache__/main_controller.cpython-310.pyc new file mode 100644 index 0000000..8ef2372 Binary files /dev/null and b/controllers/__pycache__/main_controller.cpython-310.pyc differ diff --git a/controllers/__pycache__/main_controller.cpython-313.pyc b/controllers/__pycache__/main_controller.cpython-313.pyc new file mode 100644 index 0000000..d58bbbb Binary files /dev/null and b/controllers/__pycache__/main_controller.cpython-313.pyc differ diff --git a/controllers/__pycache__/settings_controller.cpython-310.pyc b/controllers/__pycache__/settings_controller.cpython-310.pyc new file mode 100644 index 0000000..df33c6a Binary files /dev/null and b/controllers/__pycache__/settings_controller.cpython-310.pyc differ diff --git a/controllers/__pycache__/settings_controller.cpython-313.pyc b/controllers/__pycache__/settings_controller.cpython-313.pyc new file mode 100644 index 0000000..aa43e80 Binary files /dev/null and b/controllers/__pycache__/settings_controller.cpython-313.pyc differ diff --git a/controllers/account_controller.py b/controllers/account_controller.py new file mode 100644 index 0000000..2543b25 --- /dev/null +++ b/controllers/account_controller.py @@ -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 diff --git a/controllers/main_controller.py b/controllers/main_controller.py new file mode 100644 index 0000000..6f70165 --- /dev/null +++ b/controllers/main_controller.py @@ -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)}" + ) \ No newline at end of file diff --git a/controllers/platform_controllers/__pycache__/base_controller.cpython-310.pyc b/controllers/platform_controllers/__pycache__/base_controller.cpython-310.pyc new file mode 100644 index 0000000..39655b5 Binary files /dev/null and b/controllers/platform_controllers/__pycache__/base_controller.cpython-310.pyc differ diff --git a/controllers/platform_controllers/__pycache__/base_controller.cpython-313.pyc b/controllers/platform_controllers/__pycache__/base_controller.cpython-313.pyc new file mode 100644 index 0000000..4d0e9f6 Binary files /dev/null and b/controllers/platform_controllers/__pycache__/base_controller.cpython-313.pyc differ diff --git a/controllers/platform_controllers/__pycache__/instagram_controller.cpython-310.pyc b/controllers/platform_controllers/__pycache__/instagram_controller.cpython-310.pyc new file mode 100644 index 0000000..b79dd21 Binary files /dev/null and b/controllers/platform_controllers/__pycache__/instagram_controller.cpython-310.pyc differ diff --git a/controllers/platform_controllers/__pycache__/instagram_controller.cpython-313.pyc b/controllers/platform_controllers/__pycache__/instagram_controller.cpython-313.pyc new file mode 100644 index 0000000..7a661ba Binary files /dev/null and b/controllers/platform_controllers/__pycache__/instagram_controller.cpython-313.pyc differ diff --git a/controllers/platform_controllers/__pycache__/tiktok_controller.cpython-310.pyc b/controllers/platform_controllers/__pycache__/tiktok_controller.cpython-310.pyc new file mode 100644 index 0000000..ad2a470 Binary files /dev/null and b/controllers/platform_controllers/__pycache__/tiktok_controller.cpython-310.pyc differ diff --git a/controllers/platform_controllers/__pycache__/tiktok_controller.cpython-313.pyc b/controllers/platform_controllers/__pycache__/tiktok_controller.cpython-313.pyc new file mode 100644 index 0000000..4398731 Binary files /dev/null and b/controllers/platform_controllers/__pycache__/tiktok_controller.cpython-313.pyc differ diff --git a/controllers/platform_controllers/base_controller.py b/controllers/platform_controllers/base_controller.py new file mode 100644 index 0000000..ef5a8c7 --- /dev/null +++ b/controllers/platform_controllers/base_controller.py @@ -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, "" \ No newline at end of file diff --git a/controllers/platform_controllers/instagram_controller.py b/controllers/platform_controllers/instagram_controller.py new file mode 100644 index 0000000..eed13b2 --- /dev/null +++ b/controllers/platform_controllers/instagram_controller.py @@ -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()) \ No newline at end of file diff --git a/controllers/platform_controllers/tiktok_controller.py b/controllers/platform_controllers/tiktok_controller.py new file mode 100644 index 0000000..429e074 --- /dev/null +++ b/controllers/platform_controllers/tiktok_controller.py @@ -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()) \ No newline at end of file diff --git a/controllers/settings_controller.py b/controllers/settings_controller.py new file mode 100644 index 0000000..bc443f9 --- /dev/null +++ b/controllers/settings_controller.py @@ -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 diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/database/__pycache__/__init__.cpython-310.pyc b/database/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..34bdd01 Binary files /dev/null and b/database/__pycache__/__init__.cpython-310.pyc differ diff --git a/database/__pycache__/__init__.cpython-313.pyc b/database/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..2f0ddea Binary files /dev/null and b/database/__pycache__/__init__.cpython-313.pyc differ diff --git a/database/__pycache__/db_manager.cpython-310.pyc b/database/__pycache__/db_manager.cpython-310.pyc new file mode 100644 index 0000000..6dc85ed Binary files /dev/null and b/database/__pycache__/db_manager.cpython-310.pyc differ diff --git a/database/__pycache__/db_manager.cpython-313.pyc b/database/__pycache__/db_manager.cpython-313.pyc new file mode 100644 index 0000000..02a1c01 Binary files /dev/null and b/database/__pycache__/db_manager.cpython-313.pyc differ diff --git a/database/account_repository.py b/database/account_repository.py new file mode 100644 index 0000000..e69de29 diff --git a/database/accounts.db b/database/accounts.db new file mode 100644 index 0000000..0af961b Binary files /dev/null and b/database/accounts.db differ diff --git a/database/db_manager.py b/database/db_manager.py new file mode 100644 index 0000000..e80cafd --- /dev/null +++ b/database/db_manager.py @@ -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 \ No newline at end of file diff --git a/database/instagram_accounts.db b/database/instagram_accounts.db new file mode 100644 index 0000000..fe2cd7f Binary files /dev/null and b/database/instagram_accounts.db differ diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..27002d0 --- /dev/null +++ b/database/schema.sql @@ -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')); diff --git a/licensing/__init__.py b/licensing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/licensing/__pycache__/__init__.cpython-310.pyc b/licensing/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..b39c2b7 Binary files /dev/null and b/licensing/__pycache__/__init__.cpython-310.pyc differ diff --git a/licensing/__pycache__/__init__.cpython-313.pyc b/licensing/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..e579a4b Binary files /dev/null and b/licensing/__pycache__/__init__.cpython-313.pyc differ diff --git a/licensing/__pycache__/license_manager.cpython-310.pyc b/licensing/__pycache__/license_manager.cpython-310.pyc new file mode 100644 index 0000000..e8088df Binary files /dev/null and b/licensing/__pycache__/license_manager.cpython-310.pyc differ diff --git a/licensing/__pycache__/license_manager.cpython-313.pyc b/licensing/__pycache__/license_manager.cpython-313.pyc new file mode 100644 index 0000000..b7140b0 Binary files /dev/null and b/licensing/__pycache__/license_manager.cpython-313.pyc differ diff --git a/licensing/hardware_fingerprint.py b/licensing/hardware_fingerprint.py new file mode 100644 index 0000000..e69de29 diff --git a/licensing/license_manager.py b/licensing/license_manager.py new file mode 100644 index 0000000..bfa3804 --- /dev/null +++ b/licensing/license_manager.py @@ -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"] \ No newline at end of file diff --git a/licensing/license_validator.py b/licensing/license_validator.py new file mode 100644 index 0000000..51db972 --- /dev/null +++ b/licensing/license_validator.py @@ -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}") \ No newline at end of file diff --git a/localization/__init__.py b/localization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/localization/__pycache__/__init__.cpython-310.pyc b/localization/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..6735da7 Binary files /dev/null and b/localization/__pycache__/__init__.cpython-310.pyc differ diff --git a/localization/__pycache__/__init__.cpython-313.pyc b/localization/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..3d3d0da Binary files /dev/null and b/localization/__pycache__/__init__.cpython-313.pyc differ diff --git a/localization/__pycache__/language_manager.cpython-310.pyc b/localization/__pycache__/language_manager.cpython-310.pyc new file mode 100644 index 0000000..dbfa951 Binary files /dev/null and b/localization/__pycache__/language_manager.cpython-310.pyc differ diff --git a/localization/__pycache__/language_manager.cpython-313.pyc b/localization/__pycache__/language_manager.cpython-313.pyc new file mode 100644 index 0000000..3bd77d9 Binary files /dev/null and b/localization/__pycache__/language_manager.cpython-313.pyc differ diff --git a/localization/language_manager.py b/localization/language_manager.py new file mode 100644 index 0000000..a5073d7 --- /dev/null +++ b/localization/language_manager.py @@ -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 \ No newline at end of file diff --git a/localization/languages/de.json b/localization/languages/de.json new file mode 100644 index 0000000..151b547 --- /dev/null +++ b/localization/languages/de.json @@ -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." + } +} \ No newline at end of file diff --git a/localization/languages/en.json b/localization/languages/en.json new file mode 100644 index 0000000..282d5bf --- /dev/null +++ b/localization/languages/en.json @@ -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." + } +} \ No newline at end of file diff --git a/localization/languages/es.json b/localization/languages/es.json new file mode 100644 index 0000000..4c403f5 --- /dev/null +++ b/localization/languages/es.json @@ -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." + } +} \ No newline at end of file diff --git a/localization/languages/fr.json b/localization/languages/fr.json new file mode 100644 index 0000000..6081651 --- /dev/null +++ b/localization/languages/fr.json @@ -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." + } +} \ No newline at end of file diff --git a/localization/languages/ja.json b/localization/languages/ja.json new file mode 100644 index 0000000..5ecbc9f --- /dev/null +++ b/localization/languages/ja.json @@ -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": "このソフトウェアはライセンス制で、有効なライセンスでのみ使用できます。" + } +} \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/logs/main.log b/logs/main.log new file mode 100644 index 0000000..ebe0e40 --- /dev/null +++ b/logs/main.log @@ -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 ausgewhlt: instagram +2025-05-11 22:14:11,965 - main - INFO - Anwendung wird gestartet... +2025-05-11 22:14:15,499 - main - INFO - Plattform ausgewhlt: tiktok +2025-05-11 22:51:23,397 - main - INFO - Zurck zur Plattformauswahl +2025-05-11 22:55:24,731 - main - INFO - Anwendung wird gestartet... +2025-05-11 22:55:27,782 - main - INFO - Plattform ausgewhlt: tiktok +2025-05-21 00:35:56,633 - main - INFO - Anwendung wird gestartet... +2025-05-21 00:36:03,187 - main - INFO - Plattform ausgewhlt: instagram +2025-05-21 00:36:04,994 - main - INFO - Zurck zur Plattformauswahl +2025-05-21 00:36:07,722 - main - INFO - Plattform ausgewhlt: vk +2025-05-21 00:36:07,722 - main - ERROR - Plattform 'vk' wird nicht untersttzt +2025-05-21 00:36:12,032 - main - INFO - Plattform ausgewhlt: 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 ausgewhlt: instagram +2025-06-16 19:57:56,286 - main - INFO - Zurck zur Plattformauswahl +2025-06-16 19:58:06,367 - main - INFO - Plattform ausgewhlt: instagram +2025-06-16 19:58:07,276 - main - INFO - Zurck zur Plattformauswahl +2025-06-16 19:58:07,706 - main - INFO - Plattform ausgewhlt: facebook +2025-06-16 19:58:07,706 - main - ERROR - Plattform 'facebook' wird nicht untersttzt +2025-06-16 19:58:09,011 - main - INFO - Plattform ausgewhlt: tiktok +2025-06-16 19:58:10,828 - main - INFO - Zurck zur Plattformauswahl +2025-06-16 19:58:11,603 - main - INFO - Plattform ausgewhlt: twitter +2025-06-16 19:58:11,603 - main - ERROR - Plattform 'twitter' wird nicht untersttzt +2025-06-16 19:58:13,674 - main - INFO - Plattform ausgewhlt: tiktok +2025-06-16 19:58:14,871 - main - INFO - Zurck zur Plattformauswahl +2025-06-22 17:43:11,967 - main - INFO - Anwendung wird gestartet... +2025-06-22 17:52:27,938 - main - INFO - Plattform ausgewhlt: instagram +2025-06-22 17:52:35,074 - main - INFO - Zurck zur Plattformauswahl +2025-06-22 17:52:48,316 - main - INFO - Plattform ausgewhlt: instagram +2025-06-22 17:52:56,088 - main - INFO - Zurck zur Plattformauswahl diff --git a/main.py b/main.py new file mode 100644 index 0000000..ccc3312 --- /dev/null +++ b/main.py @@ -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() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e8aa591 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/resources/icons/de.svg b/resources/icons/de.svg new file mode 100644 index 0000000..20a017e --- /dev/null +++ b/resources/icons/de.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/en.svg b/resources/icons/en.svg new file mode 100644 index 0000000..016c075 --- /dev/null +++ b/resources/icons/en.svg @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/resources/icons/es.svg b/resources/icons/es.svg new file mode 100644 index 0000000..7570f40 --- /dev/null +++ b/resources/icons/es.svg @@ -0,0 +1,114 @@ + + + \ No newline at end of file diff --git a/resources/icons/facebook.svg b/resources/icons/facebook.svg new file mode 100644 index 0000000..88c6485 --- /dev/null +++ b/resources/icons/facebook.svg @@ -0,0 +1,17 @@ + + + + + Facebook-color + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/fr.svg b/resources/icons/fr.svg new file mode 100644 index 0000000..a035bda --- /dev/null +++ b/resources/icons/fr.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/instagram.svg b/resources/icons/instagram.svg new file mode 100644 index 0000000..b7b9792 --- /dev/null +++ b/resources/icons/instagram.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/ja.svg b/resources/icons/ja.svg new file mode 100644 index 0000000..73fe223 --- /dev/null +++ b/resources/icons/ja.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/moon.svg b/resources/icons/moon.svg new file mode 100644 index 0000000..8dbdf3a --- /dev/null +++ b/resources/icons/moon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/icons/sun.svg b/resources/icons/sun.svg new file mode 100644 index 0000000..1c0898f --- /dev/null +++ b/resources/icons/sun.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/icons/tiktok.svg b/resources/icons/tiktok.svg new file mode 100644 index 0000000..c1b2ac6 --- /dev/null +++ b/resources/icons/tiktok.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/twitter.svg b/resources/icons/twitter.svg new file mode 100644 index 0000000..781cbc6 --- /dev/null +++ b/resources/icons/twitter.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/vk.svg b/resources/icons/vk.svg new file mode 100644 index 0000000..0b9bd6c --- /dev/null +++ b/resources/icons/vk.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/resources/themes/dark.qss b/resources/themes/dark.qss new file mode 100644 index 0000000..cff011d --- /dev/null +++ b/resources/themes/dark.qss @@ -0,0 +1 @@ +/* Auto-generated empty stylesheet */ diff --git a/resources/themes/light.qss b/resources/themes/light.qss new file mode 100644 index 0000000..cff011d --- /dev/null +++ b/resources/themes/light.qss @@ -0,0 +1 @@ +/* Auto-generated empty stylesheet */ diff --git a/social_networks/__init__.py b/social_networks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/__pycache__/__init__.cpython-310.pyc b/social_networks/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..7b8cc97 Binary files /dev/null and b/social_networks/__pycache__/__init__.cpython-310.pyc differ diff --git a/social_networks/__pycache__/__init__.cpython-313.pyc b/social_networks/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..1c7d7f6 Binary files /dev/null and b/social_networks/__pycache__/__init__.cpython-313.pyc differ diff --git a/social_networks/__pycache__/base_automation.cpython-310.pyc b/social_networks/__pycache__/base_automation.cpython-310.pyc new file mode 100644 index 0000000..8cb43d5 Binary files /dev/null and b/social_networks/__pycache__/base_automation.cpython-310.pyc differ diff --git a/social_networks/__pycache__/base_automation.cpython-313.pyc b/social_networks/__pycache__/base_automation.cpython-313.pyc new file mode 100644 index 0000000..7f003dd Binary files /dev/null and b/social_networks/__pycache__/base_automation.cpython-313.pyc differ diff --git a/social_networks/base_automation.py b/social_networks/base_automation.py new file mode 100644 index 0000000..dac597e --- /dev/null +++ b/social_networks/base_automation.py @@ -0,0 +1,487 @@ +""" +Basis-Automatisierungsklasse für soziale Netzwerke +""" + +import os +import logging +import time +import random +from typing import Dict, List, Optional, Any, Tuple +from abc import ABC, abstractmethod + +from browser.playwright_manager import PlaywrightManager +from utils.proxy_rotator import ProxyRotator +from utils.email_handler import EmailHandler +from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button + +# Konfiguriere Logger +logger = logging.getLogger("base_automation") + +class BaseAutomation(ABC): + """ + Abstrakte Basisklasse für die Automatisierung von sozialen Netzwerken. + Definiert die gemeinsame Schnittstelle für alle Implementierungen. + """ + + def __init__(self, + headless: bool = False, + use_proxy: bool = False, + proxy_type: str = None, + save_screenshots: bool = True, + screenshots_dir: str = None, + slowmo: int = 0, + debug: bool = False, + email_domain: str = "z5m7q9dk3ah2v1plx6ju.com"): + """ + Initialisiert die Basis-Automatisierung. + + Args: + headless: Ob der Browser im Headless-Modus ausgeführt werden soll + use_proxy: Ob ein Proxy verwendet werden soll + proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ + save_screenshots: Ob Screenshots gespeichert werden sollen + screenshots_dir: Verzeichnis für Screenshots + slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging) + debug: Ob Debug-Informationen angezeigt werden sollen + email_domain: Domain für generierte E-Mail-Adressen + """ + self.headless = headless + self.use_proxy = use_proxy + self.proxy_type = proxy_type + self.save_screenshots = save_screenshots + self.slowmo = slowmo + self.debug = debug + self.email_domain = email_domain + + # Verzeichnis für Screenshots + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.screenshots_dir = screenshots_dir or os.path.join(base_dir, "logs", "screenshots") + os.makedirs(self.screenshots_dir, exist_ok=True) + + # Initialisiere Hilfsklassen + self.proxy_rotator = ProxyRotator() + self.email_handler = EmailHandler() + + # Initialisiere TextSimilarity für robustes UI-Element-Matching + self.text_similarity = TextSimilarity(default_threshold=0.75) + + # Playwright-Manager wird bei Bedarf initialisiert + self.browser = None + + # Status und Ergebnis der Automatisierung + self.status = { + "success": False, + "stage": "initialized", + "error": None, + "account_data": {} + } + + # Debug-Logging + if self.debug: + logging.getLogger().setLevel(logging.DEBUG) + + logger.info(f"Basis-Automatisierung initialisiert (Proxy: {use_proxy}, Typ: {proxy_type})") + + def _initialize_browser(self) -> bool: + """ + Initialisiert den Browser mit den entsprechenden Einstellungen. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Proxy-Konfiguration, falls aktiviert + proxy_config = None + if self.use_proxy: + proxy_config = self.proxy_rotator.get_proxy(self.proxy_type) + if not proxy_config: + logger.warning(f"Kein Proxy vom Typ '{self.proxy_type}' verfügbar, verwende direkten Zugriff") + + # Browser initialisieren + self.browser = PlaywrightManager( + headless=self.headless, + proxy=proxy_config, + browser_type="chromium", + screenshots_dir=self.screenshots_dir, + slowmo=self.slowmo + ) + + # Browser starten + self.browser.start() + + logger.info("Browser erfolgreich initialisiert") + return True + + except Exception as e: + logger.error(f"Fehler bei der Browser-Initialisierung: {e}") + self.status["error"] = f"Browser-Initialisierungsfehler: {str(e)}" + return False + + def _close_browser(self) -> None: + """Schließt den Browser und gibt Ressourcen frei.""" + if self.browser: + self.browser.close() + self.browser = None + logger.info("Browser geschlossen") + + def _take_screenshot(self, name: str) -> Optional[str]: + """ + Erstellt einen Screenshot der aktuellen Seite. + + Args: + name: Name für den Screenshot (ohne Dateierweiterung) + + Returns: + Optional[str]: Pfad zum erstellten Screenshot oder None bei Fehler + """ + if not self.save_screenshots: + return None + + try: + if self.browser and hasattr(self.browser, 'take_screenshot'): + return self.browser.take_screenshot(name) + + except Exception as e: + logger.warning(f"Fehler beim Erstellen eines Screenshots: {e}") + + return None + + def _random_delay(self, min_seconds: float = 1.0, max_seconds: float = 3.0) -> None: + """ + Führt eine zufällige Wartezeit aus, um menschliches Verhalten zu simulieren. + + Args: + min_seconds: Minimale Wartezeit in Sekunden + max_seconds: Maximale Wartezeit in Sekunden + """ + delay = random.uniform(min_seconds, max_seconds) + logger.debug(f"Zufällige Wartezeit: {delay:.2f} Sekunden") + time.sleep(delay) + + def _fill_field_fuzzy(self, field_labels: List[str], value: str, fallback_selector: str = None) -> bool: + """ + Füllt ein Formularfeld mit Fuzzy-Text-Matching aus. + + Args: + field_labels: Liste mit möglichen Bezeichnungen des Feldes + value: Einzugebender Wert + fallback_selector: CSS-Selektor für Fallback + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + # Versuche, das Feld mit Fuzzy-Matching zu finden + field = fuzzy_find_element(self.browser.page, field_labels, selector_type="input", threshold=0.6, wait_time=3000) + + if field: + try: + field.fill(value) + return True + except Exception as e: + logger.warning(f"Fehler beim Ausfüllen des Feldes mit Fuzzy-Match: {e}") + # Fallback auf normales Ausfüllen + + # Fallback: Versuche mit dem angegebenen Selektor + if fallback_selector: + field_success = self.browser.fill_form_field(fallback_selector, value) + if field_success: + return True + + return False + + def _click_button_fuzzy(self, button_texts: List[str], fallback_selector: str = None) -> bool: + """ + Klickt einen Button mit Fuzzy-Text-Matching. + + Args: + button_texts: Liste mit möglichen Button-Texten + fallback_selector: CSS-Selektor für Fallback + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + # Versuche, den Button mit Fuzzy-Matching zu finden + success = click_fuzzy_button(self.browser.page, button_texts, threshold=0.6, timeout=3000) + + if success: + return True + + # Fallback: Versuche mit dem angegebenen Selektor + if fallback_selector: + return self.browser.click_element(fallback_selector) + + return False + + def _find_element_by_text(self, texts: List[str], selector_type: str = "any", threshold: float = 0.7) -> Optional[Any]: + """ + Findet ein Element basierend auf Textähnlichkeit. + + Args: + texts: Liste mit möglichen Texten + selector_type: Art des Elements ("button", "link", "input", "any") + threshold: Ähnlichkeitsschwellenwert + + Returns: + Das gefundene Element oder None + """ + return fuzzy_find_element(self.browser.page, texts, selector_type, threshold, wait_time=3000) + + def _check_for_text_on_page(self, texts: List[str], threshold: float = 0.7) -> bool: + """ + Prüft, ob ein Text auf der Seite vorhanden ist. + + Args: + texts: Liste mit zu suchenden Texten + threshold: Ähnlichkeitsschwellenwert + + Returns: + True wenn einer der Texte gefunden wurde, False sonst + """ + # Hole den gesamten Seiteninhalt + try: + page_content = self.browser.page.content() + if not page_content: + return False + + # Versuche, Text im HTML zu finden (einfache Suche) + for text in texts: + if text.lower() in page_content.lower(): + return True + + # Wenn nicht gefunden, versuche über alle sichtbaren Textelemente + elements = self.browser.page.query_selector_all("p, h1, h2, h3, h4, h5, h6, span, div, button, a, label") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip() + + # Prüfe die Textähnlichkeit mit jedem der gesuchten Texte + for text in texts: + if self.text_similarity.is_similar(text, element_text, threshold=threshold): + return True + + return False + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Text auf der Seite: {e}") + return False + + def _check_for_error(self, error_selectors: List[str], error_texts: List[str]) -> Optional[str]: + """ + Prüft, ob Fehlermeldungen angezeigt werden. + + Args: + error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen + error_texts: Liste mit typischen Fehlertexten + + Returns: + Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden + """ + try: + # Prüfe selektoren + for selector in error_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + error_text = element.text_content() + if error_text: + return error_text.strip() + + # Fuzzy-Suche nach Fehlermeldungen + elements = self.browser.page.query_selector_all("p, div[role='alert'], span.error, .error-message") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip() + + # Prüfe, ob der Text einem Fehlermuster ähnelt + for error_text in error_texts: + if self.text_similarity.is_similar(error_text, element_text, threshold=0.6): + return element_text + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}") + return None + + def _attempt_ocr_fallback(self, action_name: str, target_text: str = None, value: str = None) -> bool: + """ + Versucht, eine Aktion mit OCR-Fallback durchzuführen, wenn Playwright fehlschlägt. + + Args: + action_name: Name der Aktion ("click", "type", "select") + target_text: Text, nach dem gesucht werden soll + value: Wert, der eingegeben werden soll (bei "type" oder "select") + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + # Diese Methode wird in abgeleiteten Klassen implementiert + logger.warning(f"OCR-Fallback für '{action_name}' wurde aufgerufen, aber nicht implementiert") + return False + + def _rotate_proxy(self) -> bool: + """ + Rotiert den Proxy und aktualisiert die Browser-Sitzung. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self.use_proxy: + return False + + try: + # Browser schließen + self._close_browser() + + # Proxy rotieren + new_proxy = self.proxy_rotator.rotate_proxy(self.proxy_type) + if not new_proxy: + logger.warning("Konnte Proxy nicht rotieren") + return False + + # Browser neu initialisieren + success = self._initialize_browser() + + logger.info(f"Proxy rotiert zu: {new_proxy['server']}") + return success + + except Exception as e: + logger.error(f"Fehler bei der Proxy-Rotation: {e}") + return False + + def _generate_random_email(self, length: int = 10) -> str: + """ + Generiert eine zufällige E-Mail-Adresse. + + Args: + length: Länge des lokalen Teils der E-Mail + + Returns: + str: Die generierte E-Mail-Adresse + """ + import string + local_chars = string.ascii_lowercase + string.digits + local_part = ''.join(random.choice(local_chars) for _ in range(length)) + return f"{local_part}@{self.email_domain}" + + def _get_confirmation_code(self, email_address: str, search_criteria: str, + max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]: + """ + Ruft einen Bestätigungscode aus einer E-Mail ab. + + Args: + email_address: E-Mail-Adresse, an die der Code gesendet wurde + search_criteria: Suchkriterium für die E-Mail + max_attempts: Maximale Anzahl an Versuchen + delay_seconds: Verzögerung zwischen Versuchen in Sekunden + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + logger.info(f"Suche nach Bestätigungscode für {email_address}") + + code = self.email_handler.get_confirmation_code( + expected_email=email_address, + search_criteria=search_criteria, + max_attempts=max_attempts, + delay_seconds=delay_seconds + ) + + if code: + logger.info(f"Bestätigungscode gefunden: {code}") + else: + logger.warning(f"Kein Bestätigungscode für {email_address} gefunden") + + return code + + def _is_text_similar(self, text1: str, text2: str, threshold: float = None) -> bool: + """ + Prüft, ob zwei Texte ähnlich sind. + + Args: + text1: Erster Text + text2: Zweiter Text + threshold: Ähnlichkeitsschwellenwert (None für Standardwert) + + Returns: + True wenn die Texte ähnlich sind, False sonst + """ + return self.text_similarity.is_similar(text1, text2, threshold) + + @abstractmethod + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Registriert einen neuen Account im sozialen Netzwerk. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + pass + + @abstractmethod + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Meldet sich bei einem bestehenden Account an. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Anmeldung mit Status + """ + pass + + @abstractmethod + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Verifiziert einen Account mit einem Bestätigungscode. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + pass + + def get_status(self) -> Dict[str, Any]: + """ + Gibt den aktuellen Status der Automatisierung zurück. + + Returns: + Dict[str, Any]: Aktueller Status + """ + return self.status + + def __enter__(self): + """Kontext-Manager-Eintritt.""" + self._initialize_browser() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Kontext-Manager-Austritt.""" + self._close_browser() + + +# Wenn direkt ausgeführt, zeige Informationen zur Klasse +if __name__ == "__main__": + print("Dies ist eine abstrakte Basisklasse und kann nicht direkt instanziiert werden.") + print("Bitte verwende eine konkrete Implementierung wie InstagramAutomation.") \ No newline at end of file diff --git a/social_networks/facebook/__init__.py b/social_networks/facebook/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_automation.py b/social_networks/facebook/facebook_automation.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_login.py b/social_networks/facebook/facebook_login.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_registration.py b/social_networks/facebook/facebook_registration.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_selectors.py b/social_networks/facebook/facebook_selectors.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_ui_helper.py b/social_networks/facebook/facebook_ui_helper.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_utils.py b/social_networks/facebook/facebook_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_verification.py b/social_networks/facebook/facebook_verification.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_workflow.py b/social_networks/facebook/facebook_workflow.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/instagram/__init__.py b/social_networks/instagram/__init__.py new file mode 100644 index 0000000..893b1a7 --- /dev/null +++ b/social_networks/instagram/__init__.py @@ -0,0 +1,4 @@ +# social_networks/instagram/__init__.py +from .instagram_automation import InstagramAutomation + +__all__ = ['InstagramAutomation'] \ No newline at end of file diff --git a/social_networks/instagram/__pycache__/__init__.cpython-310.pyc b/social_networks/instagram/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..c16a8b6 Binary files /dev/null and b/social_networks/instagram/__pycache__/__init__.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/__init__.cpython-313.pyc b/social_networks/instagram/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..59e7d12 Binary files /dev/null and b/social_networks/instagram/__pycache__/__init__.cpython-313.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_automation.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_automation.cpython-310.pyc new file mode 100644 index 0000000..60ae880 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_automation.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_automation.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_automation.cpython-313.pyc new file mode 100644 index 0000000..3217d88 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_automation.cpython-313.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_login.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_login.cpython-310.pyc new file mode 100644 index 0000000..9cabc5a Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_login.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_login.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_login.cpython-313.pyc new file mode 100644 index 0000000..53ba629 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_login.cpython-313.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_registration.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_registration.cpython-310.pyc new file mode 100644 index 0000000..8973269 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_registration.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_registration.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_registration.cpython-313.pyc new file mode 100644 index 0000000..27b5ec9 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_registration.cpython-313.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_selectors.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_selectors.cpython-310.pyc new file mode 100644 index 0000000..2fae7e3 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_selectors.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_selectors.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_selectors.cpython-313.pyc new file mode 100644 index 0000000..7658ee4 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_selectors.cpython-313.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_ui_helper.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_ui_helper.cpython-310.pyc new file mode 100644 index 0000000..526ff87 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_ui_helper.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_ui_helper.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_ui_helper.cpython-313.pyc new file mode 100644 index 0000000..77d7337 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_ui_helper.cpython-313.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_utils.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_utils.cpython-310.pyc new file mode 100644 index 0000000..9f849a8 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_utils.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_utils.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_utils.cpython-313.pyc new file mode 100644 index 0000000..d81d6bb Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_utils.cpython-313.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_verification.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_verification.cpython-310.pyc new file mode 100644 index 0000000..273d8ec Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_verification.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_verification.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_verification.cpython-313.pyc new file mode 100644 index 0000000..90692b6 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_verification.cpython-313.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_workflow.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_workflow.cpython-310.pyc new file mode 100644 index 0000000..ba32623 Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_workflow.cpython-310.pyc differ diff --git a/social_networks/instagram/__pycache__/instagram_workflow.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_workflow.cpython-313.pyc new file mode 100644 index 0000000..469ed8c Binary files /dev/null and b/social_networks/instagram/__pycache__/instagram_workflow.cpython-313.pyc differ diff --git a/social_networks/instagram/instagram_automation.py b/social_networks/instagram/instagram_automation.py new file mode 100644 index 0000000..5476585 --- /dev/null +++ b/social_networks/instagram/instagram_automation.py @@ -0,0 +1,330 @@ +# social_networks/instagram/instagram_automation.py + +""" +Instagram-Automatisierung - Hauptklasse für Instagram-Automatisierungsfunktionalität +""" + +import logging +import time +import random +from typing import Dict, List, Any, Optional, Tuple + +from browser.playwright_manager import PlaywrightManager +from browser.playwright_extensions import PlaywrightExtensions +from social_networks.base_automation import BaseAutomation +from utils.password_generator import PasswordGenerator +from utils.username_generator import UsernameGenerator +from utils.birthday_generator import BirthdayGenerator +from utils.human_behavior import HumanBehavior + +# Importiere Helferklassen +from .instagram_registration import InstagramRegistration +from .instagram_login import InstagramLogin +from .instagram_verification import InstagramVerification +from .instagram_ui_helper import InstagramUIHelper +from .instagram_utils import InstagramUtils + +# Konfiguriere Logger +logger = logging.getLogger("instagram_automation") + +class InstagramAutomation(BaseAutomation): + """ + Hauptklasse für die Instagram-Automatisierung. + Implementiert die Registrierung und Anmeldung bei Instagram. + """ + + def __init__(self, + headless: bool = False, + use_proxy: bool = False, + proxy_type: str = None, + save_screenshots: bool = True, + screenshots_dir: str = None, + slowmo: int = 0, + debug: bool = False, + email_domain: str = "z5m7q9dk3ah2v1plx6ju.com", + enhanced_stealth: bool = True, + fingerprint_noise: float = 0.5): + """ + Initialisiert die Instagram-Automatisierung. + + Args: + headless: Ob der Browser im Headless-Modus ausgeführt werden soll + use_proxy: Ob ein Proxy verwendet werden soll + proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ + save_screenshots: Ob Screenshots gespeichert werden sollen + screenshots_dir: Verzeichnis für Screenshots + slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging) + debug: Ob Debug-Informationen angezeigt werden sollen + email_domain: Domain für generierte E-Mail-Adressen + enhanced_stealth: Ob erweiterter Stealth-Modus aktiviert werden soll + fingerprint_noise: Menge an Rauschen für Fingerprint-Verschleierung (0.0-1.0) + """ + # Initialisiere die Basisklasse + super().__init__( + headless=headless, + use_proxy=use_proxy, + proxy_type=proxy_type, + save_screenshots=save_screenshots, + screenshots_dir=screenshots_dir, + slowmo=slowmo, + debug=debug, + email_domain=email_domain + ) + + # Stealth-Modus-Einstellungen + self.enhanced_stealth = enhanced_stealth + self.fingerprint_noise = max(0.0, min(1.0, fingerprint_noise)) + + # Initialisiere Helferklassen + self.registration = InstagramRegistration(self) + self.login = InstagramLogin(self) + self.verification = InstagramVerification(self) + self.ui_helper = InstagramUIHelper(self) + self.utils = InstagramUtils(self) + + # Zusätzliche Hilfsklassen + self.password_generator = PasswordGenerator() + self.username_generator = UsernameGenerator() + self.birthday_generator = BirthdayGenerator() + self.human_behavior = HumanBehavior(speed_factor=0.8, randomness=0.6) + + logger.info("Instagram-Automatisierung initialisiert") + + def _initialize_browser(self) -> bool: + """ + Initialisiert den Browser mit den entsprechenden Einstellungen. + Diese Methode überschreibt die Methode der Basisklasse, um den erweiterten + Fingerprint-Schutz zu aktivieren. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Proxy-Konfiguration, falls aktiviert + proxy_config = None + if self.use_proxy: + proxy_config = self.proxy_rotator.get_proxy(self.proxy_type) + if not proxy_config: + logger.warning(f"Kein Proxy vom Typ '{self.proxy_type}' verfügbar, verwende direkten Zugriff") + + # Browser initialisieren + self.browser = PlaywrightManager( + headless=self.headless, + proxy=proxy_config, + browser_type="chromium", + screenshots_dir=self.screenshots_dir, + slowmo=self.slowmo + ) + + # Browser starten + self.browser.start() + + # Erweiterten Fingerprint-Schutz aktivieren, wenn gewünscht + if self.enhanced_stealth: + # Erstelle Extensions-Objekt + extensions = PlaywrightExtensions(self.browser) + + # Methoden anhängen + extensions.hook_into_playwright_manager() + + # Fingerprint-Schutz aktivieren mit angepasster Konfiguration + fingerprint_config = { + "noise_level": self.fingerprint_noise, + "canvas_noise": True, + "audio_noise": True, + "webgl_noise": True, + "hardware_concurrency": random.choice([4, 6, 8]), + "device_memory": random.choice([4, 8]), + "timezone_id": "Europe/Berlin" + } + + success = self.browser.enable_enhanced_fingerprint_protection(fingerprint_config) + if success: + logger.info("Erweiterter Fingerprint-Schutz erfolgreich aktiviert") + else: + logger.warning("Erweiterter Fingerprint-Schutz konnte nicht aktiviert werden") + + logger.info("Browser erfolgreich initialisiert") + return True + + except Exception as e: + logger.error(f"Fehler bei der Browser-Initialisierung: {e}") + self.status["error"] = f"Browser-Initialisierungsfehler: {str(e)}" + return False + + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Registriert einen neuen Instagram-Account. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + logger.info(f"Starte Instagram-Account-Registrierung für '{full_name}' via {registration_method}") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Rotiere Fingerprint vor der Hauptaktivität, um Erkennung weiter zu erschweren + if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'): + self.browser.rotate_fingerprint() + logger.info("Browser-Fingerprint vor der Registrierung rotiert") + + # Delegiere die Hauptregistrierungslogik an die Registration-Klasse + result = self.registration.register_account(full_name, age, registration_method, phone_number, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"registration_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Account-Registrierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"registration_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Meldet sich bei einem bestehenden Instagram-Account an. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Anmeldung mit Status + """ + logger.info(f"Starte Instagram-Login für '{username_or_email}'") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Rotiere Fingerprint vor dem Login + if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'): + self.browser.rotate_fingerprint() + logger.info("Browser-Fingerprint vor dem Login rotiert") + + # Delegiere die Hauptlogin-Logik an die Login-Klasse + result = self.login.login_account(username_or_email, password, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"login_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Login: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"login_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Verifiziert einen Instagram-Account mit einem Bestätigungscode. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + logger.info(f"Starte Instagram-Account-Verifizierung mit Code: {verification_code}") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Delegiere die Hauptverifizierungslogik an die Verification-Klasse + result = self.verification.verify_account(verification_code, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"verification_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Account-Verifizierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"verification_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + 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 or not hasattr(self.browser, 'get_fingerprint_status'): + return { + "active": False, + "message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert" + } + + return self.browser.get_fingerprint_status() \ No newline at end of file diff --git a/social_networks/instagram/instagram_login.py b/social_networks/instagram/instagram_login.py new file mode 100644 index 0000000..f28c926 --- /dev/null +++ b/social_networks/instagram/instagram_login.py @@ -0,0 +1,576 @@ +# social_networks/instagram/instagram_login.py + +""" +Instagram-Login - Klasse für die Anmeldefunktionalität bei Instagram +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple + +from .instagram_selectors import InstagramSelectors +from .instagram_workflow import InstagramWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("instagram_login") + +class InstagramLogin: + """ + Klasse für die Anmeldung bei Instagram-Konten. + Enthält alle Methoden für den Login-Prozess. + """ + + def __init__(self, automation): + """ + Initialisiert die Instagram-Login-Funktionalität. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + self.workflow = InstagramWorkflow.get_login_workflow() + + logger.debug("Instagram-Login initialisiert") + + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Führt den Login-Prozess für ein Instagram-Konto durch. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis des Logins mit Status + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere die Eingaben + if not self._validate_login_inputs(username_or_email, password): + return { + "success": False, + "error": "Ungültige Login-Eingaben", + "stage": "input_validation" + } + + # Account-Daten für die Anmeldung + account_data = { + "username": username_or_email, + "password": password, + "handle_2fa": kwargs.get("handle_2fa", False), + "two_factor_code": kwargs.get("two_factor_code"), + "skip_save_login": kwargs.get("skip_save_login", True) + } + + logger.info(f"Starte Instagram-Login für {username_or_email}") + + try: + # 1. Zur Login-Seite navigieren + if not self._navigate_to_login_page(): + return { + "success": False, + "error": "Konnte nicht zur Login-Seite navigieren", + "stage": "navigation" + } + + # 2. Cookie-Banner behandeln + self._handle_cookie_banner() + + # 3. Login-Formular ausfüllen + if not self._fill_login_form(account_data): + return { + "success": False, + "error": "Fehler beim Ausfüllen des Login-Formulars", + "stage": "login_form" + } + + # 4. Auf 2FA prüfen und behandeln, falls nötig + needs_2fa, two_fa_error = self._check_needs_two_factor_auth() + + if needs_2fa: + if not account_data["handle_2fa"]: + return { + "success": False, + "error": "Zwei-Faktor-Authentifizierung erforderlich, aber nicht aktiviert", + "stage": "two_factor_required" + } + + # 2FA behandeln + if not self._handle_two_factor_auth(account_data["two_factor_code"]): + return { + "success": False, + "error": "Fehler bei der Zwei-Faktor-Authentifizierung", + "stage": "two_factor_auth" + } + + # 5. "Anmeldedaten speichern"-Dialog behandeln + if account_data["skip_save_login"]: + self._handle_save_login_prompt() + + # 6. Benachrichtigungsdialog behandeln + self._handle_notifications_prompt() + + # 7. Erfolgreichen Login überprüfen + if not self._check_login_success(): + error_message = self._get_login_error() + return { + "success": False, + "error": f"Login fehlgeschlagen: {error_message or 'Unbekannter Fehler'}", + "stage": "login_check" + } + + # Login erfolgreich + logger.info(f"Instagram-Login für {username_or_email} erfolgreich") + + return { + "success": True, + "stage": "completed", + "username": username_or_email + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Instagram-Login: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception" + } + + def _validate_login_inputs(self, username_or_email: str, password: str) -> bool: + """ + Validiert die Eingaben für den Login. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + + Returns: + bool: True wenn alle Eingaben gültig sind, False sonst + """ + if not username_or_email or len(username_or_email) < 3: + logger.error("Ungültiger Benutzername oder E-Mail") + return False + + if not password or len(password) < 6: + logger.error("Ungültiges Passwort") + return False + + return True + + def _navigate_to_login_page(self) -> bool: + """ + Navigiert zur Instagram-Login-Seite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Zur Login-Seite navigieren + self.browser.navigate_to(InstagramSelectors.LOGIN_URL) + + # Warten, bis die Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Screenshot erstellen + self.automation._take_screenshot("login_page") + + # Prüfen, ob Login-Formular sichtbar ist + if not self.browser.is_element_visible(InstagramSelectors.LOGIN_USERNAME_FIELD, timeout=5000): + logger.warning("Login-Formular nicht sichtbar") + return False + + logger.info("Erfolgreich zur Login-Seite navigiert") + return True + + except Exception as e: + logger.error(f"Fehler beim Navigieren zur Login-Seite: {e}") + return False + + def _handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("reject_cookies"), + InstagramSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(InstagramSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + def _fill_login_form(self, account_data: Dict[str, Any]) -> bool: + """ + Füllt das Login-Formular aus und sendet es ab. + + Args: + account_data: Account-Daten für den Login + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Benutzername/E-Mail eingeben + username_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("username"), + account_data["username"], + InstagramSelectors.LOGIN_USERNAME_FIELD + ) + + if not username_success: + logger.error("Konnte Benutzername-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Passwort eingeben + password_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("password"), + account_data["password"], + InstagramSelectors.LOGIN_PASSWORD_FIELD + ) + + if not password_success: + logger.error("Konnte Passwort-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot vorm Absenden + self.automation._take_screenshot("login_form_filled") + + # Formular absenden + submit_success = self.automation.ui_helper.click_button_fuzzy( + ["Anmelden", "Log in", "Login"], + InstagramSelectors.SUBMIT_BUTTON + ) + + if not submit_success: + logger.error("Konnte Login-Formular nicht absenden") + return False + + # Nach dem Absenden warten + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Überprüfen, ob es eine Fehlermeldung gab + error_message = self._get_login_error() + if error_message: + logger.error(f"Login-Fehler erkannt: {error_message}") + return False + + logger.info("Login-Formular erfolgreich ausgefüllt und abgesendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen des Login-Formulars: {e}") + return False + + def _get_login_error(self) -> Optional[str]: + """ + Überprüft, ob eine Login-Fehlermeldung angezeigt wird. + + Returns: + Optional[str]: Fehlermeldung oder None, wenn keine gefunden wurde + """ + try: + # Auf Fehlermeldungen prüfen + error_selectors = [ + InstagramSelectors.ERROR_MESSAGE, + "p[class*='error']", + "div[role='alert']", + "p[data-testid='login-error-message']" + ] + + for selector in error_selectors: + error_element = self.browser.wait_for_selector(selector, timeout=2000) + if error_element: + error_text = error_element.text_content() + if error_text and len(error_text.strip()) > 0: + return error_text.strip() + + # Wenn keine spezifische Fehlermeldung gefunden wurde, nach bekannten Fehlermustern suchen + error_texts = [ + "Falsches Passwort", + "Benutzername nicht gefunden", + "incorrect password", + "username you entered doesn't belong", + "please wait a few minutes", + "try again later", + "Bitte warte einige Minuten", + "versuche es später noch einmal" + ] + + page_content = self.browser.page.content() + for error_text in error_texts: + if error_text.lower() in page_content.lower(): + return f"Erkannter Fehler: {error_text}" + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Login-Fehler: {e}") + return None + + def _check_needs_two_factor_auth(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob eine Zwei-Faktor-Authentifizierung erforderlich ist. + + Returns: + Tuple[bool, Optional[str]]: (2FA erforderlich, Fehlermeldung falls vorhanden) + """ + try: + # Nach 2FA-Indikatoren suchen + two_fa_selectors = [ + "input[name='verificationCode']", + "input[aria-label='Sicherheitscode']", + "input[aria-label='Security code']", + "input[placeholder*='code']" + ] + + for selector in two_fa_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Zwei-Faktor-Authentifizierung erforderlich") + return True, None + + # Texte, die auf 2FA hinweisen + two_fa_indicators = InstagramSelectors.get_two_fa_indicators() + + # Seiteninhalt durchsuchen + page_content = self.browser.page.content() + + for indicator in two_fa_indicators: + if indicator.lower() in page_content.lower(): + logger.info(f"Zwei-Faktor-Authentifizierung erkannt durch Text: {indicator}") + return True, None + + return False, None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf 2FA: {e}") + return False, f"Fehler bei der 2FA-Erkennung: {str(e)}" + + def _handle_two_factor_auth(self, two_factor_code: Optional[str] = None) -> bool: + """ + Behandelt die Zwei-Faktor-Authentifizierung. + + Args: + two_factor_code: Optional vorhandener 2FA-Code + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Screenshot erstellen + self.automation._take_screenshot("two_factor_auth") + + # 2FA-Eingabefeld finden + two_fa_selectors = [ + "input[name='verificationCode']", + "input[aria-label='Sicherheitscode']", + "input[aria-label='Security code']", + "input[placeholder*='code']" + ] + + two_fa_field = None + for selector in two_fa_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + two_fa_field = selector + break + + if not two_fa_field: + logger.error("Konnte 2FA-Eingabefeld nicht finden") + return False + + # Wenn kein Code bereitgestellt wurde, Benutzer auffordern + if not two_factor_code: + logger.warning("Kein 2FA-Code bereitgestellt, kann nicht fortfahren") + return False + + # 2FA-Code eingeben + code_success = self.browser.fill_form_field(two_fa_field, two_factor_code) + + if not code_success: + logger.error("Konnte 2FA-Code nicht eingeben") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Bestätigen-Button finden und klicken + confirm_button_selectors = [ + "button[type='submit']", + "//button[contains(text(), 'Bestätigen')]", + "//button[contains(text(), 'Confirm')]", + "//button[contains(text(), 'Verify')]" + ] + + confirm_clicked = False + for selector in confirm_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + confirm_clicked = True + break + + if not confirm_clicked: + # Alternative: Mit Tastendruck bestätigen + self.browser.page.keyboard.press("Enter") + logger.info("Enter-Taste gedrückt, um 2FA zu bestätigen") + + # Warten nach der Bestätigung + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Überprüfen, ob 2FA erfolgreich war + still_on_2fa = self._check_needs_two_factor_auth()[0] + + if still_on_2fa: + # Prüfen, ob Fehlermeldung angezeigt wird + error_message = self._get_login_error() + if error_message: + logger.error(f"2FA-Fehler: {error_message}") + else: + logger.error("2FA fehlgeschlagen, immer noch auf 2FA-Seite") + return False + + logger.info("Zwei-Faktor-Authentifizierung erfolgreich") + return True + + except Exception as e: + logger.error(f"Fehler bei der Zwei-Faktor-Authentifizierung: {e}") + return False + + def _handle_save_login_prompt(self) -> bool: + """ + Behandelt den "Anmeldedaten speichern"-Dialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Nach "Nicht jetzt"-Button suchen + not_now_selectors = [ + "//button[contains(text(), 'Nicht jetzt')]", + "//button[contains(text(), 'Not now')]", + "//button[contains(text(), 'Skip')]", + "//button[contains(text(), 'Überspringen')]" + ] + + for selector in not_now_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + if self.browser.click_element(selector): + logger.info("'Anmeldedaten speichern'-Dialog übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden + logger.debug("Kein 'Anmeldedaten speichern'-Dialog erkannt") + return True + + except Exception as e: + logger.warning(f"Fehler beim Behandeln des 'Anmeldedaten speichern'-Dialogs: {e}") + # Dies ist nicht kritisch, daher geben wir trotzdem True zurück + return True + + def _handle_notifications_prompt(self) -> bool: + """ + Behandelt den Benachrichtigungen-Dialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Nach "Nicht jetzt"-Button suchen + not_now_selectors = [ + "//button[contains(text(), 'Nicht jetzt')]", + "//button[contains(text(), 'Not now')]", + "//button[contains(text(), 'Skip')]", + "//button[contains(text(), 'Später')]", + "//button[contains(text(), 'Later')]" + ] + + for selector in not_now_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + if self.browser.click_element(selector): + logger.info("Benachrichtigungen-Dialog übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden + logger.debug("Kein Benachrichtigungen-Dialog erkannt") + return True + + except Exception as e: + logger.warning(f"Fehler beim Behandeln des Benachrichtigungen-Dialogs: {e}") + # Dies ist nicht kritisch, daher geben wir trotzdem True zurück + return True + + def _check_login_success(self) -> bool: + """ + Überprüft, ob der Login erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Warten nach dem Login + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Screenshot erstellen + self.automation._take_screenshot("login_final") + + # Erfolg anhand verschiedener Indikatoren prüfen + success_indicators = InstagramSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=2000): + logger.info(f"Login-Erfolgsindikator gefunden: {indicator}") + return True + + # Alternativ prüfen, ob wir auf der Instagram-Startseite sind + current_url = self.browser.page.url + if "instagram.com" in current_url and "/accounts/login" not in current_url: + logger.info(f"Login-Erfolg basierend auf URL: {current_url}") + return True + + # Prüfen, ob immer noch auf der Login-Seite + if "/accounts/login" in current_url or self.browser.is_element_visible(InstagramSelectors.LOGIN_USERNAME_FIELD, timeout=1000): + logger.warning("Immer noch auf der Login-Seite, Login fehlgeschlagen") + return False + + logger.warning("Keine Login-Erfolgsindikatoren gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Login-Erfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/instagram/instagram_registration.py b/social_networks/instagram/instagram_registration.py new file mode 100644 index 0000000..39ca88d --- /dev/null +++ b/social_networks/instagram/instagram_registration.py @@ -0,0 +1,735 @@ +# social_networks/instagram/instagram_registration.py + +""" +Instagram-Registrierung - Klasse für die Kontoerstellung bei Instagram +""" + +import logging +import time +import random +import re +from typing import Dict, List, Any, Optional, Tuple + +from .instagram_selectors import InstagramSelectors +from .instagram_workflow import InstagramWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("instagram_registration") + +class InstagramRegistration: + """ + Klasse für die Registrierung von Instagram-Konten. + Enthält alle Methoden zur Kontoerstellung. + """ + + def __init__(self, automation): + """ + Initialisiert die Instagram-Registrierung. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + self.workflow = InstagramWorkflow.get_registration_workflow() + + logger.debug("Instagram-Registrierung initialisiert") + + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Führt den vollständigen Registrierungsprozess für einen Instagram-Account durch. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere die Eingaben + if not self._validate_registration_inputs(full_name, age, registration_method, phone_number): + return { + "success": False, + "error": "Ungültige Eingabeparameter", + "stage": "input_validation" + } + + # Account-Daten generieren + account_data = self._generate_account_data(full_name, age, registration_method, phone_number, **kwargs) + + # Starte den Registrierungsprozess + logger.info(f"Starte Instagram-Registrierung für {account_data['username']} via {registration_method}") + + try: + # 1. Zur Registrierungsseite navigieren + if not self._navigate_to_signup_page(): + return { + "success": False, + "error": "Konnte nicht zur Registrierungsseite navigieren", + "stage": "navigation", + "account_data": account_data + } + + # 2. Cookie-Banner behandeln + self._handle_cookie_banner() + + # 3. Registrierungsmethode wählen + if not self._select_registration_method(registration_method): + return { + "success": False, + "error": f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen", + "stage": "registration_method", + "account_data": account_data + } + + # 4. Registrierungsformular ausfüllen + if not self._fill_registration_form(account_data): + return { + "success": False, + "error": "Fehler beim Ausfüllen des Registrierungsformulars", + "stage": "registration_form", + "account_data": account_data + } + + # 5. Geburtsdatum eingeben + if not self._select_birthday(account_data["birthday"]): + return { + "success": False, + "error": "Fehler beim Eingeben des Geburtsdatums", + "stage": "birthday", + "account_data": account_data + } + + # 6. Bestätigungscode abrufen und eingeben + if not self._handle_verification(account_data, registration_method): + return { + "success": False, + "error": "Fehler bei der Verifizierung", + "stage": "verification", + "account_data": account_data + } + + # 7. Erfolgreiche Registrierung überprüfen + if not self._check_registration_success(): + return { + "success": False, + "error": "Registrierung fehlgeschlagen oder konnte nicht verifiziert werden", + "stage": "final_check", + "account_data": account_data + } + + # Registrierung erfolgreich abgeschlossen + logger.info(f"Instagram-Account {account_data['username']} erfolgreich erstellt") + + return { + "success": True, + "stage": "completed", + "account_data": account_data + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Instagram-Registrierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception", + "account_data": account_data + } + + def _validate_registration_inputs(self, full_name: str, age: int, + registration_method: str, phone_number: str) -> bool: + """ + Validiert die Eingaben für die Registrierung. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + + Returns: + bool: True wenn alle Eingaben gültig sind, False sonst + """ + # Vollständiger Name prüfen + if not full_name or len(full_name) < 3: + logger.error("Ungültiger vollständiger Name") + return False + + # Alter prüfen + if age < 13: + logger.error("Benutzer muss mindestens 13 Jahre alt sein") + return False + + # Registrierungsmethode prüfen + if registration_method not in ["email", "phone"]: + logger.error(f"Ungültige Registrierungsmethode: {registration_method}") + return False + + # Telefonnummer prüfen, falls erforderlich + if registration_method == "phone" and not phone_number: + logger.error("Telefonnummer erforderlich für Registrierung via Telefon") + return False + + return True + + def _generate_account_data(self, full_name: str, age: int, registration_method: str, + phone_number: str, **kwargs) -> Dict[str, Any]: + """ + Generiert Account-Daten für die Registrierung. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Generierte Account-Daten + """ + # Benutzername generieren + username = kwargs.get("username") + if not username: + username = self.automation.username_generator.generate_username("instagram", full_name) + + # Passwort generieren + password = kwargs.get("password") + if not password: + password = self.automation.password_generator.generate_password("instagram") + + # E-Mail generieren (falls nötig) + email = None + if registration_method == "email": + email_prefix = username.lower().replace(".", "").replace("_", "") + email = f"{email_prefix}@{self.automation.email_domain}" + + # Geburtsdatum generieren + birthday = self.automation.birthday_generator.generate_birthday_components("instagram", age) + + # Account-Daten zusammenstellen + account_data = { + "username": username, + "password": password, + "full_name": full_name, + "email": email, + "phone": phone_number, + "birthday": birthday, + "age": age, + "registration_method": registration_method + } + + logger.debug(f"Account-Daten generiert: {account_data['username']}") + + return account_data + + def _navigate_to_signup_page(self) -> bool: + """ + Navigiert zur Instagram-Registrierungsseite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Zur Registrierungsseite navigieren + self.browser.navigate_to(InstagramSelectors.SIGNUP_URL) + + # Warten, bis die Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Screenshot erstellen + self.automation._take_screenshot("signup_page") + + # Prüfen, ob Registrierungsformular sichtbar ist + if not self.browser.is_element_visible(InstagramSelectors.EMAIL_PHONE_FIELD, timeout=5000): + logger.warning("Registrierungsformular nicht sichtbar") + return False + + logger.info("Erfolgreich zur Registrierungsseite navigiert") + return True + + except Exception as e: + logger.error(f"Fehler beim Navigieren zur Registrierungsseite: {e}") + return False + + def _handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("reject_cookies"), + InstagramSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(InstagramSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + def _select_registration_method(self, method: str) -> bool: + """ + Wählt die Registrierungsmethode (E-Mail oder Telefon). + + Args: + method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + if method == "email": + # Prüfen, ob der E-Mail-Tab ausgewählt werden muss + if self.browser.is_element_visible("//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]"): + success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_tab_texts("email"), + InstagramSelectors.EMAIL_TAB + ) + + if success: + logger.info("E-Mail-Registrierungsmethode ausgewählt") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + else: + logger.warning("Konnte E-Mail-Tab nicht auswählen") + return False + + # E-Mail ist vermutlich bereits ausgewählt + logger.debug("E-Mail-Registrierung erscheint bereits ausgewählt") + return True + + elif method == "phone": + # Prüfen, ob der Telefon-Tab sichtbar ist und klicken + if self.browser.is_element_visible("//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]"): + success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_tab_texts("phone"), + InstagramSelectors.PHONE_TAB + ) + + if success: + logger.info("Telefon-Registrierungsmethode ausgewählt") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + else: + logger.warning("Konnte Telefon-Tab nicht auswählen") + return False + + # Telefon ist möglicherweise bereits ausgewählt + logger.debug("Telefon-Registrierung erscheint bereits ausgewählt") + return True + + logger.error(f"Ungültige Registrierungsmethode: {method}") + return False + + except Exception as e: + logger.error(f"Fehler beim Auswählen der Registrierungsmethode: {e}") + return False + + def _fill_registration_form(self, account_data: Dict[str, Any]) -> bool: + """ + Füllt das Registrierungsformular aus und sendet es ab. + + Args: + account_data: Account-Daten für die Registrierung + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # E-Mail/Telefon eingeben + if account_data["registration_method"] == "email": + email_phone_value = account_data["email"] + else: + email_phone_value = account_data["phone"] + + # E-Mail-/Telefon-Feld ausfüllen + email_phone_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("email_phone"), + email_phone_value, + InstagramSelectors.EMAIL_PHONE_FIELD + ) + + if not email_phone_success: + logger.error("Konnte E-Mail/Telefon-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Vollständigen Namen eingeben + name_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("full_name"), + account_data["full_name"], + InstagramSelectors.FULLNAME_FIELD + ) + + if not name_success: + logger.error("Konnte Vollständiger-Name-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Benutzernamen eingeben + username_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("username"), + account_data["username"], + InstagramSelectors.USERNAME_FIELD + ) + + if not username_success: + logger.error("Konnte Benutzername-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Passwort eingeben + password_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("password"), + account_data["password"], + InstagramSelectors.PASSWORD_FIELD + ) + + if not password_success: + logger.error("Konnte Passwort-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot vorm Absenden + self.automation._take_screenshot("registration_form_filled") + + # Formular absenden + submit_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("submit"), + InstagramSelectors.SUBMIT_BUTTON + ) + + if not submit_success: + logger.error("Konnte Registrierungsformular nicht absenden") + return False + + # Prüfen, ob es Fehler gab + self.automation.human_behavior.wait_for_page_load() + + # Nach dem Absenden prüfen, ob das Formular für das Geburtsdatum erscheint + birthday_visible = self.browser.is_element_visible(InstagramSelectors.BIRTHDAY_MONTH_SELECT, timeout=10000) + + if not birthday_visible: + # Auf mögliche Fehlermeldung prüfen + error_message = self.automation.ui_helper.check_for_error( + error_selectors=[InstagramSelectors.ERROR_MESSAGE], + error_texts=InstagramSelectors.get_error_indicators() + ) + + if error_message: + logger.error(f"Fehler beim Absenden des Formulars: {error_message}") + return False + + logger.error("Geburtstagsformular nicht sichtbar nach Absenden") + return False + + logger.info("Registrierungsformular erfolgreich ausgefüllt und abgesendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen des Registrierungsformulars: {e}") + return False + + def _select_birthday(self, birthday: Dict[str, int]) -> bool: + """ + Wählt das Geburtsdatum aus den Dropdown-Menüs aus. + + Args: + birthday: Geburtsdatum als Dictionary mit 'year', 'month', 'day' Keys + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Dropdowns sichtbar sind + if not self.browser.is_element_visible(InstagramSelectors.BIRTHDAY_MONTH_SELECT, timeout=5000): + logger.error("Geburtstags-Dropdowns nicht sichtbar") + return False + + # Screenshot zu Beginn + self.automation._take_screenshot("birthday_form") + + # Monat auswählen + month_success = self.browser.select_option( + InstagramSelectors.BIRTHDAY_MONTH_SELECT, + str(birthday["month"]) + ) + + if not month_success: + logger.error(f"Konnte Monat nicht auswählen: {birthday['month']}") + return False + + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Tag auswählen + day_success = self.browser.select_option( + InstagramSelectors.BIRTHDAY_DAY_SELECT, + str(birthday["day"]) + ) + + if not day_success: + logger.error(f"Konnte Tag nicht auswählen: {birthday['day']}") + return False + + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Jahr auswählen + year_success = self.browser.select_option( + InstagramSelectors.BIRTHDAY_YEAR_SELECT, + str(birthday["year"]) + ) + + if not year_success: + logger.error(f"Konnte Jahr nicht auswählen: {birthday['year']}") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot vor dem Absenden + self.automation._take_screenshot("birthday_selected") + + # Weiter-Button klicken + next_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("next"), + InstagramSelectors.NEXT_BUTTON + ) + + if not next_success: + logger.error("Konnte Weiter-Button nicht klicken") + return False + + # Prüfen, ob es zum Bestätigungscode-Formular weitergeht + self.automation.human_behavior.wait_for_page_load() + + # Nach dem Absenden auf den Bestätigungscode-Bildschirm warten + confirmation_visible = self.browser.is_element_visible( + InstagramSelectors.CONFIRMATION_CODE_FIELD, timeout=10000 + ) or self.browser.is_element_visible( + InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, timeout=2000 + ) + + if not confirmation_visible: + # Auf mögliche Fehlermeldung prüfen + error_message = self.automation.ui_helper.check_for_error( + error_selectors=[InstagramSelectors.ERROR_MESSAGE], + error_texts=InstagramSelectors.get_error_indicators() + ) + + if error_message: + logger.error(f"Fehler nach dem Absenden des Geburtsdatums: {error_message}") + return False + + logger.error("Bestätigungscode-Formular nicht sichtbar nach Absenden des Geburtsdatums") + return False + + logger.info("Geburtsdatum erfolgreich ausgewählt und abgesendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Auswählen des Geburtsdatums: {e}") + return False + + def _handle_verification(self, account_data: Dict[str, Any], registration_method: str) -> bool: + """ + Behandelt den Verifizierungsprozess (E-Mail/SMS). + + Args: + account_data: Account-Daten mit E-Mail/Telefon + registration_method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Bestätigungscode-Eingabe sichtbar ist + if not self.browser.is_element_visible(InstagramSelectors.CONFIRMATION_CODE_FIELD, timeout=5000) and not \ + self.browser.is_element_visible(InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, timeout=1000): + logger.error("Bestätigungscode-Eingabe nicht sichtbar") + return False + + # Screenshot erstellen + self.automation._take_screenshot("verification_page") + + # Verifizierungscode je nach Methode abrufen + if registration_method == "email": + # Verifizierungscode von E-Mail abrufen + verification_code = self._get_email_confirmation_code(account_data["email"]) + else: + # Verifizierungscode von SMS abrufen + verification_code = self._get_sms_confirmation_code(account_data["phone"]) + + if not verification_code: + logger.error("Konnte keinen Verifizierungscode abrufen") + return False + + logger.info(f"Verifizierungscode erhalten: {verification_code}") + + # Verifizierungscode eingeben und absenden + return self.automation.verification.enter_and_submit_verification_code(verification_code) + + except Exception as e: + logger.error(f"Fehler bei der Verifizierung: {e}") + return False + + def _get_email_confirmation_code(self, email: str) -> Optional[str]: + """ + Ruft den Bestätigungscode von einer E-Mail ab. + + Args: + email: E-Mail-Adresse, an die der Code gesendet wurde + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + try: + # Warte auf die E-Mail + verification_code = self.automation.email_handler.get_verification_code( + email_domain=self.automation.email_domain, + platform="instagram", + timeout=120 # Warte bis zu 2 Minuten auf den Code + ) + + if verification_code: + return verification_code + + # Wenn kein Code gefunden wurde, prüfen, ob der Code vielleicht direkt angezeigt wird + verification_code = self._extract_code_from_page() + + if verification_code: + logger.info(f"Verifizierungscode direkt von der Seite extrahiert: {verification_code}") + return verification_code + + logger.warning(f"Konnte keinen Verifizierungscode für {email} finden") + return None + + except Exception as e: + logger.error(f"Fehler beim Abrufen des E-Mail-Bestätigungscodes: {e}") + return None + + def _get_sms_confirmation_code(self, phone: str) -> Optional[str]: + """ + Ruft den Bestätigungscode aus einer SMS ab. + Hier müsste ein SMS-Empfangs-Service eingebunden werden. + + Args: + phone: Telefonnummer, an die der Code gesendet wurde + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + # Diese Implementierung ist ein Platzhalter + # In einer echten Implementierung würde hier ein SMS-Empfangs-Service verwendet + logger.warning("SMS-Verifizierung ist noch nicht implementiert") + + # Versuche, den Code trotzdem zu extrahieren, falls er auf der Seite angezeigt wird + return self._extract_code_from_page() + + def _extract_code_from_page(self) -> Optional[str]: + """ + Versucht, einen Bestätigungscode direkt von der Seite zu extrahieren. + + Returns: + Optional[str]: Der extrahierte Code oder None, wenn nicht gefunden + """ + try: + # Gesamten Seiteninhalt abrufen + page_content = self.browser.page.content() + + # Mögliche Regex-Muster für Bestätigungscodes + patterns = [ + r"Dein Code ist (\d{6})", + r"Your code is (\d{6})", + r"Bestätigungscode: (\d{6})", + r"Confirmation code: (\d{6})", + r"(\d{6}) ist dein Instagram-Code", + r"(\d{6}) is your Instagram code" + ] + + for pattern in patterns: + match = re.search(pattern, page_content) + if match: + return match.group(1) + + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Codes von der Seite: {e}") + return None + + def _check_registration_success(self) -> bool: + """ + Überprüft, ob die Registrierung erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Warten nach der Verifizierung + self.automation.human_behavior.wait_for_page_load(multiplier=2.0) + + # Screenshot erstellen + self.automation._take_screenshot("registration_final") + + # Erfolg anhand verschiedener Indikatoren prüfen + success_indicators = InstagramSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=2000): + logger.info(f"Erfolgsindikator gefunden: {indicator}") + return True + + # Alternativ prüfen, ob wir auf der Instagram-Startseite sind + current_url = self.browser.page.url + if "instagram.com" in current_url and "/accounts/" not in current_url: + logger.info(f"Erfolg basierend auf URL: {current_url}") + return True + + # Prüfen, ob wir auf weitere Einrichtungsschritte weitergeleitet wurden + # (z.B. "Folge anderen Benutzern" oder "Füge ein Profilbild hinzu") + # Diese sind optional und gelten als erfolgreiche Registrierung + if self.automation.ui_helper.check_for_next_steps(): + logger.info("Auf weitere Einrichtungsschritte weitergeleitet, Registrierung erfolgreich") + return True + + logger.warning("Keine Erfolgsindikatoren gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Registrierungserfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/instagram/instagram_selectors.py b/social_networks/instagram/instagram_selectors.py new file mode 100644 index 0000000..0c64e61 --- /dev/null +++ b/social_networks/instagram/instagram_selectors.py @@ -0,0 +1,242 @@ +""" +Instagram-Selektoren - CSS-Selektoren und XPath-Ausdrücke für die Instagram-Automatisierung +Mit zusätzlichen Text-Matching-Funktionen für robustere Element-Erkennung +""" + +from typing import List, Dict, Optional, Any + +class InstagramSelectors: + """ + Zentrale Sammlung aller Selektoren für die Instagram-Automatisierung. + Bei Änderungen der Instagram-Webseite müssen nur hier Anpassungen vorgenommen werden. + Enthält auch Fuzzy-Text-Matching-Daten für robustere Element-Erkennung. + """ + + # URL-Konstanten + BASE_URL = "https://www.instagram.com" + SIGNUP_URL = "https://www.instagram.com/accounts/emailsignup/" + LOGIN_URL = "https://www.instagram.com/accounts/login/" + + # Cookie-Banner + COOKIE_DIALOG = "div[role='dialog']" + COOKIE_REJECT_BUTTON = "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]" + COOKIE_ACCEPT_BUTTON = "//button[contains(text(), 'Akzeptieren') or contains(text(), 'Accept') or contains(text(), 'Zulassen')]" + + # Registrierungsformular - Hauptfelder + EMAIL_PHONE_FIELD = "input[name='emailOrPhone']" + FULLNAME_FIELD = "input[name='fullName']" + USERNAME_FIELD = "input[name='username']" + PASSWORD_FIELD = "input[name='password']" + + # Registrierungsformular - Alternative Selektoren + ALT_EMAIL_FIELD = "//input[@aria-label='Handynummer oder E-Mail-Adresse']" + ALT_FULLNAME_FIELD = "//input[@aria-label='Vollständiger Name']" + ALT_USERNAME_FIELD = "//input[@aria-label='Benutzername']" + ALT_PASSWORD_FIELD = "//input[@aria-label='Passwort']" + + # Geburtsdatum-Selektoren + BIRTHDAY_MONTH_SELECT = "//select[@title='Monat:']" + BIRTHDAY_DAY_SELECT = "//select[@title='Tag:']" + BIRTHDAY_YEAR_SELECT = "//select[@title='Jahr:']" + + # Buttons + SUBMIT_BUTTON = "button[type='submit']" + NEXT_BUTTON = "//button[contains(text(), 'Weiter') or contains(text(), 'Next')]" + CONFIRMATION_BUTTON = "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]" + + # Bestätigungscode + CONFIRMATION_CODE_FIELD = "input[name='confirmationCode']" + ALT_CONFIRMATION_CODE_FIELD = "//input[@aria-label='Bestätigungscode']" + RESEND_CODE_BUTTON = "//button[contains(text(), 'Code erneut senden') or contains(text(), 'Resend code')]" + + # Fehlermeldungen + ERROR_MESSAGE = "p[class*='error'], div[role='alert']" + CAPTCHA_CONTAINER = "div[class*='captcha']" + + # Registrierungsmethode umschalten + EMAIL_TAB = "//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]" + PHONE_TAB = "//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]" + + # Login-Felder + LOGIN_USERNAME_FIELD = "input[name='username']" + LOGIN_PASSWORD_FIELD = "input[name='password']" + + # Erfolgs-Indikatoren für Login/Registrierung + SUCCESS_INDICATORS = [ + "svg[aria-label='Home']", + "img[alt='Instagram']", + "a[href='/direct/inbox/']", + "a[href='/explore/']" + ] + + # OCR-Fallback-Texte + OCR_TEXTS = { + "email_field": ["Handynummer oder E-Mail", "Mobile number or email"], + "fullname_field": ["Vollständiger Name", "Full name"], + "username_field": ["Benutzername", "Username"], + "password_field": ["Passwort", "Password"], + "birthday_day": ["Tag", "Day"], + "birthday_month": ["Monat", "Month"], + "birthday_year": ["Jahr", "Year"], + "confirmation_code": ["Bestätigungscode", "Confirmation code"], + "next_button": ["Weiter", "Next"], + "submit_button": ["Registrieren", "Sign up"], + "confirm_button": ["Bestätigen", "Confirm"], + "cookie_reject": ["Nur erforderliche Cookies erlauben", "Optionale Cookies ablehnen", "Reject"] + } + + # Text-Matching-Parameter für Fuzzy-Matching + TEXT_MATCH = { + # Formularfelder + "form_fields": { + "email_phone": ["Handynummer oder E-Mail-Adresse", "Mobile Number or Email", + "Phone number or email", "E-Mail-Adresse oder Telefonnummer"], + "full_name": ["Vollständiger Name", "Full Name", "Name"], + "username": ["Benutzername", "Username"], + "password": ["Passwort", "Password"], + "confirmation_code": ["Bestätigungscode", "Verification code", "Confirmation code", "Code"], + }, + + # Buttons + "buttons": { + "submit": ["Weiter", "Registrieren", "Next", "Sign up", "Continue", "Anmelden"], + "confirm": ["Bestätigen", "Confirm", "Verify", "Weiter", "Next"], + "next": ["Weiter", "Next", "Continue", "Fortfahren"], + "reject_cookies": ["Ablehnen", "Nur erforderliche", "Reject", "Not now", "Decline", + "Optionale Cookies ablehnen", "Nur notwendige Cookies"], + "accept_cookies": ["Akzeptieren", "Accept", "Allow", "Zulassen", "Alle Cookies akzeptieren"], + "skip": ["Überspringen", "Skip", "Später", "Later", "Nicht jetzt", "Not now"], + }, + + # Tabs + "tabs": { + "email": ["E-Mail", "Email", "E-mail", "Mail"], + "phone": ["Telefon", "Telefonnummer", "Phone", "Mobile"], + }, + + # Erfolgs-Indikatoren + "success_indicators": [ + "Home", "Startseite", "Feed", "Timeline", "Nachrichten", + "Profil", "Suche", "Entdecken", "Explore" + ], + + # Fehler-Indikatoren + "error_indicators": [ + "Fehler", "Error", "Leider", "Ungültig", "Invalid", "Nicht verfügbar", + "Fehlgeschlagen", "Problem", "Failed", "Nicht möglich", "Bereits verwendet" + ], + + # 2FA-Indikatoren + "two_fa_indicators": [ + "Bestätigungscode", "Verifizierungscode", "Sicherheitscode", + "2-Faktor", "Verification code", "Two-factor", "2FA" + ] + } + + @classmethod + def birthday_month_option(cls, month_num: int) -> str: + """ + Erstellt einen Selektor für eine bestimmte Monatsoption. + + Args: + month_num: Monatsnummer (1-12) + + Returns: + str: XPath-Selektor für die Monatsoption + """ + return f"//select[@title='Monat:']/option[@value='{month_num}']" + + @classmethod + def birthday_day_option(cls, day_num: int) -> str: + """ + Erstellt einen Selektor für eine bestimmte Tagesoption. + + Args: + day_num: Tagesnummer (1-31) + + Returns: + str: XPath-Selektor für die Tagesoption + """ + return f"//select[@title='Tag:']/option[@value='{day_num}']" + + @classmethod + def birthday_year_option(cls, year: int) -> str: + """ + Erstellt einen Selektor für eine bestimmte Jahresoption. + + Args: + year: Jahr (z.B. 1990) + + Returns: + str: XPath-Selektor für die Jahresoption + """ + return f"//select[@title='Jahr:']/option[@value='{year}']" + + @classmethod + def get_field_labels(cls, field_type: str) -> List[str]: + """ + Gibt die möglichen Bezeichnungen für ein Formularfeld zurück. + + Args: + field_type: Typ des Formularfelds (z.B. "email_phone", "full_name") + + Returns: + List[str]: Liste mit möglichen Bezeichnungen + """ + return cls.TEXT_MATCH["form_fields"].get(field_type, []) + + @classmethod + def get_button_texts(cls, button_type: str) -> List[str]: + """ + Gibt die möglichen Texte für einen Button zurück. + + Args: + button_type: Typ des Buttons (z.B. "submit", "confirm") + + Returns: + List[str]: Liste mit möglichen Button-Texten + """ + return cls.TEXT_MATCH["buttons"].get(button_type, []) + + @classmethod + def get_tab_texts(cls, tab_type: str) -> List[str]: + """ + Gibt die möglichen Texte für einen Tab zurück. + + Args: + tab_type: Typ des Tabs (z.B. "email", "phone") + + Returns: + List[str]: Liste mit möglichen Tab-Texten + """ + return cls.TEXT_MATCH["tabs"].get(tab_type, []) + + @classmethod + def get_success_indicators(cls) -> List[str]: + """ + Gibt die möglichen Texte für Erfolgsindikatoren zurück. + + Returns: + List[str]: Liste mit möglichen Erfolgsindikator-Texten + """ + return cls.TEXT_MATCH["success_indicators"] + + @classmethod + def get_error_indicators(cls) -> List[str]: + """ + Gibt die möglichen Texte für Fehlerindikatoren zurück. + + Returns: + List[str]: Liste mit möglichen Fehlerindikator-Texten + """ + return cls.TEXT_MATCH["error_indicators"] + + @classmethod + def get_two_fa_indicators(cls) -> List[str]: + """ + Gibt die möglichen Texte für 2FA-Indikatoren zurück. + + Returns: + List[str]: Liste mit möglichen 2FA-Indikator-Texten + """ + return cls.TEXT_MATCH["two_fa_indicators"] \ No newline at end of file diff --git a/social_networks/instagram/instagram_ui_helper.py b/social_networks/instagram/instagram_ui_helper.py new file mode 100644 index 0000000..d115490 --- /dev/null +++ b/social_networks/instagram/instagram_ui_helper.py @@ -0,0 +1,668 @@ +# social_networks/instagram/instagram_ui_helper.py + +""" +Instagram-UI-Helper - Hilfsmethoden für die Interaktion mit der Instagram-UI +""" + +import logging +import re +import time +from typing import Dict, List, Any, Optional, Tuple, Union, Callable + +from .instagram_selectors import InstagramSelectors +from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button + +# Konfiguriere Logger +logger = logging.getLogger("instagram_ui_helper") + +class InstagramUIHelper: + """ + Hilfsmethoden für die Interaktion mit der Instagram-Benutzeroberfläche. + Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen. + """ + + def __init__(self, automation): + """ + Initialisiert den Instagram-UI-Helper. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + + # Initialisiere TextSimilarity für Fuzzy-Matching + self.text_similarity = TextSimilarity(default_threshold=0.7) + + logger.debug("Instagram-UI-Helper initialisiert") + + def _ensure_browser(self) -> bool: + """ + Stellt sicher, dass die Browser-Referenz verfügbar ist. + + Returns: + bool: True wenn Browser verfügbar, False sonst + """ + if self.browser is None: + self.browser = self.automation.browser + + if self.browser is None: + logger.error("Browser-Referenz nicht verfügbar") + return False + + return True + + def fill_field_fuzzy(self, field_labels: Union[str, List[str]], + value: str, fallback_selector: str = None, + threshold: float = 0.7, timeout: int = 5000) -> bool: + """ + Füllt ein Formularfeld mit Fuzzy-Text-Matching aus. + + Args: + field_labels: Bezeichner oder Liste von Bezeichnern des Feldes + value: Einzugebender Wert + fallback_selector: CSS-Selektor für Fallback + threshold: Schwellenwert für die Textähnlichkeit (0-1) + timeout: Zeitlimit für die Suche in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere field_labels zu einer Liste + if isinstance(field_labels, str): + field_labels = [field_labels] + + # Versuche, das Feld mit Fuzzy-Matching zu finden + element = fuzzy_find_element( + self.browser.page, + field_labels, + selector_type="input", + threshold=threshold, + wait_time=timeout + ) + + if element: + # Versuche, das Feld zu fokussieren und den Wert einzugeben + element.focus() + time.sleep(0.1) + element.fill("") # Leere das Feld zuerst + time.sleep(0.2) + + # Text menschenähnlich eingeben + for char in value: + element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000) + time.sleep(0.01) + + logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}") + return True + + # Fuzzy-Matching fehlgeschlagen, versuche über Attribute + if fallback_selector: + field_success = self.browser.fill_form_field(fallback_selector, value) + if field_success: + logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}") + return True + + # Versuche noch alternative Selektoren basierend auf field_labels + for label in field_labels: + # Versuche aria-label Attribut + aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']" + if self.browser.is_element_visible(aria_selector, timeout=1000): + if self.browser.fill_form_field(aria_selector, value): + logger.info(f"Feld über aria-label gefüllt: {label}") + return True + + # Versuche placeholder Attribut + placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']" + if self.browser.is_element_visible(placeholder_selector, timeout=1000): + if self.browser.fill_form_field(placeholder_selector, value): + logger.info(f"Feld über placeholder gefüllt: {label}") + return True + + # Versuche name Attribut + name_selector = f"input[name='{label.lower().replace(' ', '')}']" + if self.browser.is_element_visible(name_selector, timeout=1000): + if self.browser.fill_form_field(name_selector, value): + logger.info(f"Feld über name-Attribut gefüllt: {label}") + return True + + logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen") + return False + + except Exception as e: + logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {e}") + return False + + def click_button_fuzzy(self, button_texts: Union[str, List[str]], + fallback_selector: str = None, threshold: float = 0.7, + timeout: int = 5000) -> bool: + """ + Klickt einen Button mit Fuzzy-Text-Matching. + + Args: + button_texts: Text oder Liste von Texten des Buttons + fallback_selector: CSS-Selektor für Fallback + threshold: Schwellenwert für die Textähnlichkeit (0-1) + timeout: Zeitlimit für die Suche in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere button_texts zu einer Liste + if isinstance(button_texts, str): + button_texts = [button_texts] + + # Logging der Suche + logger.info(f"Suche nach Button mit Texten: {button_texts}") + + if not button_texts or button_texts == [[]]: + logger.warning("Leere Button-Text-Liste angegeben!") + return False + + # Versuche, den Button mit Fuzzy-Matching zu finden und zu klicken + try: + # Direktes Matching für alle angegebenen Texte + for text in button_texts: + if not text or text == "[]": + continue + + # Direkte Selektoren versuchen + button_selector = f"button:has-text('{text}')" + button = self.browser.page.query_selector(button_selector) + if button: + logger.info(f"Button mit exaktem Text gefunden: {text}") + button.click() + return True + + # Alternativer Selektor für Links und andere klickbare Elemente + link_selector = f"a:has-text('{text}')" + link = self.browser.page.query_selector(link_selector) + if link: + logger.info(f"Link mit exaktem Text gefunden: {text}") + link.click() + return True + + # Versuche alle Buttons auf der Seite zu finden und zu prüfen + all_buttons = self.browser.page.query_selector_all("button, [role='button'], a.button, input[type='submit']") + logger.info(f"Gefundene Button-ähnliche Elemente: {len(all_buttons)}") + + for button in all_buttons: + button_text = button.inner_text() + if not button_text: + continue + + button_text = button_text.strip() + logger.debug(f"Button-Text: '{button_text}'") + + # Prüfe auf Textähnlichkeit + for search_text in button_texts: + if not search_text: + continue + + if self.text_similarity.is_similar(search_text, button_text, threshold=threshold): + logger.info(f"Button mit ähnlichem Text gefunden: '{button_text}' (ähnlich zu '{search_text}')") + button.click() + return True + + logger.warning(f"Kein Button mit Text ähnlich zu '{button_texts}' gefunden") + except Exception as inner_e: + logger.error(f"Innerer Fehler beim Fuzzy-Klicken: {inner_e}") + + # Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector + if fallback_selector: + logger.info(f"Versuche Fallback-Selektor: {fallback_selector}") + if self.browser.click_element(fallback_selector): + logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}") + return True + + # Versuche alternative Methoden + + # 1. Versuche über aria-label + for text in button_texts: + if not text: + continue + + aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']" + if self.browser.is_element_visible(aria_selector, timeout=1000): + if self.browser.click_element(aria_selector): + logger.info(f"Button über aria-label geklickt: {text}") + return True + + # 2. Versuche über role='button' mit Text + for text in button_texts: + if not text: + continue + + xpath_selector = f"//div[@role='button' and contains(., '{text}')]" + if self.browser.is_element_visible(xpath_selector, timeout=1000): + if self.browser.click_element(xpath_selector): + logger.info(f"Button über role+text geklickt: {text}") + return True + + # 3. Versuche über Link-Text + for text in button_texts: + if not text: + continue + + link_selector = f"//a[contains(text(), '{text}')]" + if self.browser.is_element_visible(link_selector, timeout=1000): + if self.browser.click_element(link_selector): + logger.info(f"Link mit passendem Text geklickt: {text}") + return True + + # 4. Als letzten Versuch, klicke auf einen beliebigen Button + logger.warning("Kein spezifischer Button gefunden, versuche beliebigen Button zu klicken") + buttons = self.browser.page.query_selector_all("button") + if buttons and len(buttons) > 0: + for button in buttons: + visible = button.is_visible() + if visible: + logger.info("Klicke auf beliebigen sichtbaren Button") + button.click() + return True + + logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken") + return False + + except Exception as e: + logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}") + return False + + def check_for_error(self, error_selectors: List[str] = None, + error_texts: List[str] = None) -> Optional[str]: + """ + Überprüft, ob Fehlermeldungen angezeigt werden. + + Args: + error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen + error_texts: Liste mit typischen Fehlertexten + + Returns: + Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden + """ + if not self._ensure_browser(): + return None + + try: + # Standardselektoren verwenden, wenn keine angegeben sind + if error_selectors is None: + error_selectors = [ + InstagramSelectors.ERROR_MESSAGE, + "p[class*='error']", + "div[role='alert']", + "span[class*='error']", + ".error-message" + ] + + # Standardfehlertexte verwenden, wenn keine angegeben sind + if error_texts is None: + error_texts = InstagramSelectors.get_error_indicators() + + # 1. Nach Fehlerselektoren suchen + for selector in error_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + error_text = element.text_content() + if error_text and len(error_text.strip()) > 0: + logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}") + return error_text.strip() + + # 2. Alle Texte auf der Seite durchsuchen + page_content = self.browser.page.content() + + for error_text in error_texts: + if error_text.lower() in page_content.lower(): + # Versuche, den genauen Fehlertext zu extrahieren + matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower()) + if matches: + full_error = matches[0].strip() + logger.info(f"Fehlermeldung gefunden (Text): {full_error}") + return full_error + else: + logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}") + return error_text + + # 3. Nach weiteren Fehlerelementen suchen + elements = self.browser.page.query_selector_all("p, div, span") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip() + + # Prüfe Textähnlichkeit mit Fehlertexten + for error_text in error_texts: + if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \ + self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7): + logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}") + return element_text + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}") + return None + + def check_for_captcha(self) -> bool: + """ + Überprüft, ob ein Captcha angezeigt wird. + + Returns: + bool: True wenn Captcha erkannt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Selektoren für Captcha-Erkennung + captcha_selectors = [ + InstagramSelectors.CAPTCHA_CONTAINER, + "div[class*='captcha']", + "iframe[src*='captcha']", + "iframe[title*='captcha']", + "iframe[title*='reCAPTCHA']" + ] + + # Captcha-Texte für textbasierte Erkennung + captcha_texts = [ + "captcha", "recaptcha", "sicherheitsüberprüfung", "security check", + "i'm not a robot", "ich bin kein roboter", "verify you're human", + "bestätige, dass du ein mensch bist" + ] + + # Nach Selektoren suchen + for selector in captcha_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.warning(f"Captcha erkannt (Selektor): {selector}") + return True + + # Nach Texten suchen + page_content = self.browser.page.content().lower() + + for text in captcha_texts: + if text in page_content: + logger.warning(f"Captcha erkannt (Text): {text}") + return True + + return False + + except Exception as e: + logger.error(f"Fehler bei der Captcha-Erkennung: {e}") + return False + + def check_for_next_steps(self) -> bool: + """ + Überprüft, ob Elemente für weitere Einrichtungsschritte angezeigt werden. + + Returns: + bool: True wenn weitere Schritte erkannt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Texte, die auf weitere Einrichtungsschritte hinweisen + next_step_texts = [ + "profilbild hinzufügen", "add profile photo", + "freunde finden", "find friends", + "konten folgen", "follow accounts", + "profilbild hochladen", "upload profile picture", + "profil einrichten", "set up your profile", + "willkommen bei instagram", "welcome to instagram", + "personalisieren", "personalize", + "für dich empfohlen", "recommended for you" + ] + + # Seiteninhalt durchsuchen + page_content = self.browser.page.content().lower() + + for text in next_step_texts: + if text in page_content: + logger.info(f"Weitere Einrichtungsschritte erkannt (Text): {text}") + return True + + # Nach typischen Buttons für weitere Schritte suchen + next_step_button_texts = [ + "überspringen", "skip", + "später", "later", + "folgen", "follow", + "hinzufügen", "add", + "hochladen", "upload", + "weiter", "next", + "fertig", "done" + ] + + # Textähnlichkeit mit Button-Texten prüfen + elements = self.browser.page.query_selector_all("button, [role='button'], a") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip().lower() + + for button_text in next_step_button_texts: + if button_text in element_text or \ + self.text_similarity.is_similar(button_text, element_text, threshold=0.8): + logger.info(f"Button für weitere Einrichtungsschritte erkannt: {element_text}") + return True + + return False + + except Exception as e: + logger.error(f"Fehler bei der Erkennung weiterer Einrichtungsschritte: {e}") + return False + + def find_element_by_text(self, text: Union[str, List[str]], + element_type: str = "any", threshold: float = 0.7) -> Optional[Any]: + """ + Findet ein Element basierend auf Textähnlichkeit. + + Args: + text: Zu suchender Text oder Liste von Texten + element_type: Art des Elements ("button", "link", "input", "any") + threshold: Schwellenwert für die Textähnlichkeit (0-1) + + Returns: + Optional[Any]: Das gefundene Element oder None, wenn keines gefunden wurde + """ + if not self._ensure_browser(): + return None + + try: + # Normalisiere text zu einer Liste + if isinstance(text, str): + text = [text] + + # Verwende die Fuzzy-Find-Funktion + element = fuzzy_find_element( + self.browser.page, + text, + selector_type=element_type, + threshold=threshold, + wait_time=5000 + ) + + if element: + logger.info(f"Element mit Text '{text}' gefunden") + return element + + logger.warning(f"Kein Element mit Text '{text}' gefunden") + return None + + except Exception as e: + logger.error(f"Fehler beim Suchen nach Element mit Text '{text}': {e}") + return None + + def is_text_on_page(self, text: Union[str, List[str]], threshold: float = 0.7) -> bool: + """ + Überprüft, ob ein Text auf der Seite vorhanden ist. + + Args: + text: Zu suchender Text oder Liste von Texten + threshold: Schwellenwert für die Textähnlichkeit (0-1) + + Returns: + bool: True wenn der Text gefunden wurde, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere text zu einer Liste + if isinstance(text, str): + text = [text] + + # Hole den gesamten Seiteninhalt + page_content = self.browser.page.content().lower() + + # Direkte Textsuche + for t in text: + if t.lower() in page_content: + logger.info(f"Text '{t}' auf der Seite gefunden (exakte Übereinstimmung)") + return True + + # Suche in allen sichtbaren Textelementen mit Ähnlichkeitsvergleich + elements = self.browser.page.query_selector_all("p, h1, h2, h3, h4, h5, h6, span, div, button, a, label") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip().lower() + + for t in text: + if self.text_similarity.is_similar(t.lower(), element_text, threshold=threshold) or \ + self.text_similarity.contains_similar_text(element_text, [t.lower()], threshold=threshold): + logger.info(f"Text '{t}' auf der Seite gefunden (Ähnlichkeit)") + return True + + logger.info(f"Text '{text}' nicht auf der Seite gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Suchen nach Text auf der Seite: {e}") + return False + + def wait_for_element(self, selectors: Union[str, List[str]], + timeout: int = 10000, check_interval: int = 500) -> Optional[Any]: + """ + Wartet auf das Erscheinen eines Elements. + + Args: + selectors: CSS-Selektor oder Liste von Selektoren + timeout: Zeitlimit in Millisekunden + check_interval: Intervall zwischen den Prüfungen in Millisekunden + + Returns: + Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist + """ + if not self._ensure_browser(): + return None + + try: + # Normalisiere selectors zu einer Liste + if isinstance(selectors, str): + selectors = [selectors] + + start_time = time.time() + end_time = start_time + (timeout / 1000) + + while time.time() < end_time: + for selector in selectors: + element = self.browser.wait_for_selector(selector, timeout=check_interval) + if element: + logger.info(f"Element mit Selektor '{selector}' gefunden") + return element + + # Kurze Pause vor der nächsten Prüfung + time.sleep(check_interval / 1000) + + logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'") + return None + + except Exception as e: + logger.error(f"Fehler beim Warten auf Element: {e}") + return None + + def is_page_loading(self) -> bool: + """ + Überprüft, ob die Seite noch lädt. + + Returns: + bool: True wenn die Seite lädt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Lade-Indikatoren auf Instagram + loading_selectors = [ + "div[class*='spinner']", + "div[class*='loading']", + "div[role='progressbar']", + "svg[aria-label='Loading...']", + "svg[aria-label='Wird geladen...']" + ] + + for selector in loading_selectors: + if self.browser.is_element_visible(selector, timeout=500): + return True + + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Ladestatus: {e}") + return False + + def wait_for_page_load(self, timeout: int = 30000, check_interval: int = 500) -> bool: + """ + Wartet, bis die Seite vollständig geladen ist. + + Args: + timeout: Zeitlimit in Millisekunden + check_interval: Intervall zwischen den Prüfungen in Millisekunden + + Returns: + bool: True wenn die Seite geladen wurde, False bei Zeitüberschreitung + """ + if not self._ensure_browser(): + return False + + try: + # Warten auf Netzwerk-Idle + self.browser.page.wait_for_load_state("networkidle", timeout=timeout) + + # Zusätzlich auf das Verschwinden der Ladeindikatoren warten + start_time = time.time() + end_time = start_time + (timeout / 1000) + + while time.time() < end_time: + if not self.is_page_loading(): + # Noch eine kurze Pause für Animationen + time.sleep(0.5) + logger.info("Seite vollständig geladen") + return True + + # Kurze Pause vor der nächsten Prüfung + time.sleep(check_interval / 1000) + + logger.warning("Zeitüberschreitung beim Warten auf das Laden der Seite") + return False + + except Exception as e: + logger.error(f"Fehler beim Warten auf das Laden der Seite: {e}") + return False \ No newline at end of file diff --git a/social_networks/instagram/instagram_utils.py b/social_networks/instagram/instagram_utils.py new file mode 100644 index 0000000..50a2289 --- /dev/null +++ b/social_networks/instagram/instagram_utils.py @@ -0,0 +1,549 @@ +# social_networks/instagram/instagram_utils.py + +""" +Instagram-Utils - Hilfsfunktionen für die Instagram-Automatisierung +""" + +import logging +import time +import re +import random +from typing import Dict, List, Any, Optional, Tuple, Union + +from .instagram_selectors import InstagramSelectors + +# Konfiguriere Logger +logger = logging.getLogger("instagram_utils") + +class InstagramUtils: + """ + Hilfsfunktionen für die Instagram-Automatisierung. + Enthält allgemeine Hilfsmethoden und kleinere Funktionen. + """ + + def __init__(self, automation): + """ + Initialisiert die Instagram-Utils. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + + logger.debug("Instagram-Utils initialisiert") + + def _ensure_browser(self) -> bool: + """ + Stellt sicher, dass die Browser-Referenz verfügbar ist. + + Returns: + bool: True wenn Browser verfügbar, False sonst + """ + if self.browser is None: + self.browser = self.automation.browser + + if self.browser is None: + logger.error("Browser-Referenz nicht verfügbar") + return False + + return True + + def handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("reject_cookies"), + InstagramSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(InstagramSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + except Exception as e: + logger.error(f"Fehler beim Behandeln des Cookie-Banners: {e}") + return False + + def is_logged_in(self) -> bool: + """ + Überprüft, ob der Benutzer bei Instagram angemeldet ist. + + Returns: + bool: True wenn angemeldet, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Login-Status über verschiedene Indikatoren prüfen + + # 1. URL prüfen + current_url = self.browser.page.url + if "/accounts/login" in current_url: + logger.debug("Nicht angemeldet (Auf Login-Seite)") + return False + + # 2. Erfolgs-Indikatoren prüfen + success_indicators = InstagramSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=1000): + logger.debug(f"Angemeldet (Indikator: {indicator})") + return True + + # 3. Profil-Icon prüfen + profile_selectors = [ + "img[data-testid='user-avatar']", + "span[role='link'][aria-label*='profil']", + "span[role='link'][aria-label*='profile']" + ] + + for selector in profile_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + logger.debug(f"Angemeldet (Profil-Icon sichtbar: {selector})") + return True + + # 4. Login-Formular prüfen (umgekehrte Logik) + login_selectors = [ + InstagramSelectors.LOGIN_USERNAME_FIELD, + InstagramSelectors.LOGIN_PASSWORD_FIELD + ] + + for selector in login_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + logger.debug(f"Nicht angemeldet (Login-Formular sichtbar: {selector})") + return False + + # Wenn wir hier ankommen, können wir den Status nicht eindeutig bestimmen + # Wir gehen davon aus, dass wir nicht angemeldet sind + logger.debug("Login-Status konnte nicht eindeutig bestimmt werden, nehme 'nicht angemeldet' an") + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Login-Status: {e}") + return False + + def check_for_captcha(self) -> bool: + """ + Überprüft, ob ein Captcha angezeigt wird. + + Returns: + bool: True wenn Captcha erkannt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Delegiere an UI-Helper + return self.automation.ui_helper.check_for_captcha() + + except Exception as e: + logger.error(f"Fehler bei der Captcha-Erkennung: {e}") + return False + + def handle_rate_limiting(self, rotate_proxy: bool = True) -> bool: + """ + Behandelt eine Rate-Limiting-Situation. + + Args: + rotate_proxy: Ob der Proxy rotiert werden soll + + Returns: + bool: True wenn erfolgreich behandelt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + logger.warning("Rate-Limiting erkannt, warte und versuche es erneut") + + # Screenshot erstellen + self.automation._take_screenshot("rate_limit_detected") + + # Proxy rotieren, falls gewünscht + if rotate_proxy and self.automation.use_proxy: + success = self.automation._rotate_proxy() + if not success: + logger.warning("Konnte Proxy nicht rotieren") + + # Längere Wartezeit + wait_time = random.uniform(120, 300) # 2-5 Minuten + logger.info(f"Warte {wait_time:.1f} Sekunden vor dem nächsten Versuch") + time.sleep(wait_time) + + # Seite neuladen + self.browser.page.reload() + self.automation.human_behavior.wait_for_page_load() + + # Prüfen, ob Rate-Limiting noch aktiv ist + rate_limit_texts = [ + "bitte warte einige minuten", + "please wait a few minutes", + "try again later", + "versuche es später erneut", + "zu viele anfragen", + "too many requests" + ] + + page_content = self.browser.page.content().lower() + + still_rate_limited = False + for text in rate_limit_texts: + if text in page_content: + still_rate_limited = True + break + + if still_rate_limited: + logger.warning("Immer noch Rate-Limited nach dem Warten") + return False + else: + logger.info("Rate-Limiting scheint aufgehoben zu sein") + return True + + except Exception as e: + logger.error(f"Fehler bei der Behandlung des Rate-Limitings: {e}") + return False + + def scroll_page(self, direction: str = "down", amount: int = 3) -> bool: + """ + Scrollt die Seite. + + Args: + direction: "up" oder "down" + amount: Scroll-Menge (1=wenig, 5=viel) + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Scroll-Faktor je nach Richtung + scroll_factor = 1 if direction == "down" else -1 + + # Scroll-Menge in Pixeln + pixel_amount = amount * 300 * scroll_factor + + # Scroll ausführen + self.browser.page.evaluate(f"window.scrollBy(0, {pixel_amount})") + + # Menschliche Verzögerung + self.automation.human_behavior.random_delay(0.5, 1.0) + + logger.debug(f"Seite ge{direction}scrollt um {abs(pixel_amount)} Pixel") + return True + + except Exception as e: + logger.error(f"Fehler beim Scrollen der Seite: {e}") + return False + + def extract_username_from_url(self, url: str) -> Optional[str]: + """ + Extrahiert den Benutzernamen aus einer Instagram-URL. + + Args: + url: Die Instagram-URL + + Returns: + Optional[str]: Der extrahierte Benutzername oder None + """ + try: + # Muster für Profil-URLs + patterns = [ + r'instagram\.com/([a-zA-Z0-9._]+)/?(?:$|\?|#)', + r'instagram\.com/([a-zA-Z0-9._]+)/(?:saved|tagged|reels)/?', + r'instagram\.com/p/[^/]+/(?:by|from)/([a-zA-Z0-9._]+)/?' + ] + + for pattern in patterns: + match = re.search(pattern, url) + if match: + username = match.group(1) + # Einige Ausnahmen filtern + if username not in ["explore", "accounts", "p", "reel", "stories", "direct"]: + return username + + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Benutzernamens aus der URL: {e}") + return None + + def get_current_username(self) -> Optional[str]: + """ + Versucht, den Benutzernamen des aktuell angemeldeten Kontos zu ermitteln. + + Returns: + Optional[str]: Der Benutzername oder None, wenn nicht gefunden + """ + if not self._ensure_browser(): + return None + + try: + # Verschiedene Methoden zur Erkennung des Benutzernamens + + # 1. Benutzername aus URL des Profils + profile_link_selectors = [ + "a[href*='/profile/']", + "a[href*='instagram.com/'][role='link']", + "a[href*='instagram.com/']:not([href*='/explore/']):not([href*='/direct/'])" + ] + + for selector in profile_link_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + href = element.get_attribute("href") + if href: + username = self.extract_username_from_url(href) + if username: + logger.info(f"Benutzername aus Profil-Link ermittelt: {username}") + return username + + # 2. Benutzername aus aria-label Attributen + profile_aria_selectors = [ + "img[data-testid='user-avatar']", + "span[role='link'][aria-label*='profil']", + "span[role='link'][aria-label*='profile']" + ] + + for selector in profile_aria_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + aria_label = element.get_attribute("aria-label") + if aria_label: + # Verschiedene Formate: "profil von username", "username's profile" + for pattern in [r'profil von ([a-zA-Z0-9._]+)', r"([a-zA-Z0-9._]+)'s profile"]: + match = re.search(pattern, aria_label, re.IGNORECASE) + if match: + username = match.group(1) + logger.info(f"Benutzername aus aria-label ermittelt: {username}") + return username + + # 3. Fallback: Zur Profilseite navigieren und aus URL extrahieren + # Dies sollte nur gemacht werden, wenn ein wichtiger Grund dafür besteht + # und kein anderer Weg verfügbar ist, den Benutzernamen zu ermitteln + + logger.warning("Konnte Benutzernamen nicht ermitteln") + return None + + except Exception as e: + logger.error(f"Fehler bei der Ermittlung des Benutzernamens: {e}") + return None + + def is_account_private(self) -> Optional[bool]: + """ + Überprüft, ob das aktuelle Konto privat ist. + + Returns: + Optional[bool]: True wenn privat, False wenn öffentlich, None wenn nicht erkennbar + """ + if not self._ensure_browser(): + return None + + try: + # Texte, die auf ein privates Konto hinweisen + private_indicators = [ + "this account is private", + "dieses konto ist privat", + "must be following", + "musst diesem konto folgen" + ] + + # Privat-Icon suchen + private_icon_selectors = [ + "svg[aria-label='Private Account']", + "svg[aria-label='Privates Konto']" + ] + + for selector in private_icon_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + logger.info("Konto ist privat (Icon gefunden)") + return True + + # Texte prüfen + page_content = self.browser.page.content().lower() + + for indicator in private_indicators: + if indicator in page_content: + logger.info(f"Konto ist privat (Text gefunden: {indicator})") + return True + + # Wenn keine Privat-Indikatoren gefunden wurden, gehen wir davon aus, dass das Konto öffentlich ist + # Dies ist jedoch nicht 100% sicher + logger.debug("Konto scheint öffentlich zu sein") + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Privat-Status: {e}") + return None + + def count_followers(self) -> Optional[int]: + """ + Versucht, die Anzahl der Follower des aktuellen Kontos zu ermitteln. + + Returns: + Optional[int]: Die Anzahl der Follower oder None, wenn nicht ermittelbar + """ + if not self._ensure_browser(): + return None + + try: + # Selektoren für Follower-Zähler + follower_selectors = [ + "a[href*='/followers/'] span", + "a[href*='/followers/']", + "ul li:nth-child(2) span" # Typisches Layout auf Profilseiten + ] + + for selector in follower_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + follower_text = element.inner_text() + if follower_text: + # Zahlen extrahieren (möglicherweise mit K, M, B für Tausend, Million, Milliarde) + follower_text = follower_text.strip().replace(',', '').replace('.', '') + + # Direkter Zahlenwert + if follower_text.isdigit(): + return int(follower_text) + + # Wert mit K (Tausend) + if 'k' in follower_text.lower(): + value = float(follower_text.lower().replace('k', '')) * 1000 + return int(value) + + # Wert mit M (Million) + if 'm' in follower_text.lower(): + value = float(follower_text.lower().replace('m', '')) * 1000000 + return int(value) + + # Wert mit B (Milliarde) + if 'b' in follower_text.lower(): + value = float(follower_text.lower().replace('b', '')) * 1000000000 + return int(value) + + logger.warning("Konnte Follower-Anzahl nicht ermitteln") + return None + + except Exception as e: + logger.error(f"Fehler bei der Ermittlung der Follower-Anzahl: {e}") + return None + + def get_account_stats(self) -> Dict[str, Any]: + """ + Sammelt verfügbare Statistiken zum aktuellen Konto. + + Returns: + Dict[str, Any]: Konto-Statistiken + """ + if not self._ensure_browser(): + return {} + + try: + stats = {} + + # Benutzername ermitteln + username = self.get_current_username() + if username: + stats["username"] = username + + # Privat-Status prüfen + is_private = self.is_account_private() + if is_private is not None: + stats["is_private"] = is_private + + # Follower-Anzahl + followers = self.count_followers() + if followers is not None: + stats["followers"] = followers + + # Optional: Weitere Statistiken sammeln + # ... + + return stats + + except Exception as e: + logger.error(f"Fehler beim Sammeln der Konto-Statistiken: {e}") + return {} + + def check_login_banned(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob der Login gesperrt wurde. + + Returns: + Tuple[bool, Optional[str]]: (Gesperrt, Fehlermeldung falls vorhanden) + """ + if not self._ensure_browser(): + return False, None + + try: + # Texte, die auf eine Sperrung hinweisen + ban_indicators = [ + "your account has been disabled", + "dein konto wurde deaktiviert", + "your account has been locked", + "dein konto wurde gesperrt", + "suspicious activity", + "verdächtige aktivität", + "we've detected suspicious activity", + "wir haben verdächtige aktivitäten festgestellt", + "your account has been temporarily locked", + "dein konto wurde vorübergehend gesperrt" + ] + + # Seiteninhalt durchsuchen + page_content = self.browser.page.content().lower() + + for indicator in ban_indicators: + if indicator in page_content: + # Vollständigen Text der Fehlermeldung extrahieren + error_element = self.browser.wait_for_selector("p[data-testid='login-error-message'], div[role='alert'], p[class*='error']", timeout=2000) + error_message = None + + if error_element: + error_message = error_element.inner_text().strip() + + if not error_message: + error_message = f"Konto gesperrt oder eingeschränkt (Indikator: {indicator})" + + logger.warning(f"Konto-Sperrung erkannt: {error_message}") + return True, error_message + + return False, None + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung auf Konto-Sperrung: {e}") + return False, None \ No newline at end of file diff --git a/social_networks/instagram/instagram_verification.py b/social_networks/instagram/instagram_verification.py new file mode 100644 index 0000000..c403afb --- /dev/null +++ b/social_networks/instagram/instagram_verification.py @@ -0,0 +1,491 @@ +# social_networks/instagram/instagram_verification.py + +""" +Instagram-Verifizierung - Klasse für die Verifizierungsfunktionalität bei Instagram +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple + +from .instagram_selectors import InstagramSelectors +from .instagram_workflow import InstagramWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("instagram_verification") + +class InstagramVerification: + """ + Klasse für die Verifizierung von Instagram-Konten. + Enthält alle Methoden für den Verifizierungsprozess. + """ + + def __init__(self, automation): + """ + Initialisiert die Instagram-Verifizierung. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + self.workflow = InstagramWorkflow.get_verification_workflow() + + logger.debug("Instagram-Verifizierung initialisiert") + + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Führt den Verifizierungsprozess für ein Instagram-Konto durch. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere den Verifizierungscode + if not self._validate_verification_code(verification_code): + return { + "success": False, + "error": "Ungültiger Verifizierungscode", + "stage": "code_validation" + } + + try: + # 1. Überprüfen, ob wir auf der Verifizierungsseite sind + if not self._is_on_verification_page(): + # Versuche, zur Verifizierungsseite zu navigieren, falls möglich + # Direktnavigation ist jedoch normalerweise nicht möglich + return { + "success": False, + "error": "Nicht auf der Verifizierungsseite", + "stage": "page_check" + } + + # 2. Verifizierungscode eingeben und absenden + if not self.enter_and_submit_verification_code(verification_code): + return { + "success": False, + "error": "Fehler beim Eingeben oder Absenden des Verifizierungscodes", + "stage": "code_entry" + } + + # 3. Überprüfen, ob die Verifizierung erfolgreich war + success, error_message = self._check_verification_success() + + if not success: + return { + "success": False, + "error": f"Verifizierung fehlgeschlagen: {error_message or 'Unbekannter Fehler'}", + "stage": "verification_check" + } + + # 4. Zusätzliche Dialoge behandeln + self._handle_post_verification_dialogs() + + # Verifizierung erfolgreich + logger.info("Instagram-Verifizierung erfolgreich abgeschlossen") + + return { + "success": True, + "stage": "completed" + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Instagram-Verifizierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception" + } + + def _validate_verification_code(self, verification_code: str) -> bool: + """ + Validiert den Verifizierungscode. + + Args: + verification_code: Der zu validierende Code + + Returns: + bool: True wenn der Code gültig ist, False sonst + """ + # Leerer Code + if not verification_code: + logger.error("Verifizierungscode ist leer") + return False + + # Code-Format prüfen (normalerweise 6-stellige Zahl) + if not re.match(r"^\d{6}$", verification_code): + logger.warning(f"Verifizierungscode hat unerwartetes Format: {verification_code}") + # Wir geben trotzdem True zurück, da einige Codes andere Formate haben könnten + return True + + return True + + def _is_on_verification_page(self) -> bool: + """ + Überprüft, ob wir auf der Verifizierungsseite sind. + + Returns: + bool: True wenn auf der Verifizierungsseite, False sonst + """ + try: + # Screenshot erstellen + self.automation._take_screenshot("verification_page_check") + + # Nach Verifizierungsfeld suchen + verification_selectors = [ + InstagramSelectors.CONFIRMATION_CODE_FIELD, + InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, + "input[name='confirmationCode']", + "input[aria-label='Bestätigungscode']", + "input[aria-label='Confirmation code']", + "input[aria-label='Verification code']" + ] + + for selector in verification_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + logger.info("Auf Verifizierungsseite") + return True + + # Textbasierte Erkennung + verification_texts = [ + "Bestätigungscode", + "Confirmation code", + "Verification code", + "Sicherheitscode", + "Security code", + "Enter the code we sent to" + ] + + page_content = self.browser.page.content().lower() + + for text in verification_texts: + if text.lower() in page_content: + logger.info(f"Auf Verifizierungsseite (erkannt durch Text: {text})") + return True + + logger.warning("Nicht auf der Verifizierungsseite") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen der Verifizierungsseite: {e}") + return False + + def enter_and_submit_verification_code(self, verification_code: str) -> bool: + """ + Gibt den Verifizierungscode ein und sendet ihn ab. + + Args: + verification_code: Der einzugebende Verifizierungscode + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + logger.info(f"Versuche Verifizierungscode einzugeben: {verification_code}") + + # Mögliche Selektoren für das Verifizierungscode-Feld + code_field_selectors = [ + InstagramSelectors.CONFIRMATION_CODE_FIELD, + InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, + "input[name='email_confirmation_code']", + "input[name='confirmationCode']", + "input[aria-label='Bestätigungscode']", + "input[aria-label='Confirmation code']", + "input[placeholder*='code']", + "input[placeholder='Bestätigungscode']" + ] + + # Versuche, das Feld zu finden und auszufüllen + code_field_found = False + + for selector in code_field_selectors: + logger.debug(f"Versuche Selektor: {selector}") + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Codefeld gefunden mit Selektor: {selector}") + if self.browser.fill_form_field(selector, verification_code): + code_field_found = True + logger.info(f"Verifizierungscode eingegeben: {verification_code}") + break + + # Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen + if not code_field_found: + logger.info("Versuche Fuzzy-Matching für Codefeld") + code_field_found = self.automation.ui_helper.fill_field_fuzzy( + ["Bestätigungscode", "Confirmation code", "Verification code", "Code"], + verification_code + ) + + if not code_field_found: + logger.error("Konnte Verifizierungscode-Feld nicht finden oder ausfüllen") + + # Erstelle einen Screenshot zum Debuggen + self.automation._take_screenshot("code_field_not_found") + return False + + # Menschliche Verzögerung vor dem Absenden + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot erstellen + self.automation._take_screenshot("verification_code_entered") + + # Absenden-Button finden und klicken + submit_button_selectors = [ + "button[type='submit']", + InstagramSelectors.CONFIRMATION_BUTTON, + "//button[contains(text(), 'Bestätigen')]", + "//button[contains(text(), 'Confirm')]", + "//button[contains(text(), 'Verify')]", + "//button[contains(text(), 'Next')]", + "//button[contains(text(), 'Weiter')]", + "button" # Falls alle anderen fehlschlagen, probiere jeden Button + ] + + submit_button_found = False + + logger.info("Suche nach Submit-Button") + for selector in submit_button_selectors: + logger.debug(f"Versuche Submit-Button-Selektor: {selector}") + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Submit-Button gefunden mit Selektor: {selector}") + if self.browser.click_element(selector): + submit_button_found = True + logger.info("Verifizierungscode-Formular abgesendet") + break + + # Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen + if not submit_button_found: + logger.info("Versuche Fuzzy-Matching für Submit-Button") + weiter_buttons = ["Weiter", "Next", "Continue", "Bestätigen", "Confirm", "Submit", "Verify", "Senden"] + submit_button_found = self.automation.ui_helper.click_button_fuzzy( + weiter_buttons + ) + + if not submit_button_found: + # Erstelle einen Screenshot zum Debuggen + self.automation._take_screenshot("submit_button_not_found") + + # Versuche es mit Enter-Taste als letzten Ausweg + logger.info("Konnte Submit-Button nicht finden, versuche Enter-Taste") + self.browser.page.keyboard.press("Enter") + logger.info("Enter-Taste zur Bestätigung des Verifizierungscodes gedrückt") + submit_button_found = True + + # Warten nach dem Absenden + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + return submit_button_found + + except Exception as e: + logger.error(f"Fehler beim Eingeben und Absenden des Verifizierungscodes: {e}") + return False + + def _check_verification_success(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob die Verifizierung erfolgreich war. + + Returns: + Tuple[bool, Optional[str]]: (Erfolg, Fehlermeldung falls vorhanden) + """ + try: + # Warten nach der Verifizierung + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Screenshot erstellen + self.automation._take_screenshot("verification_result") + + # Immer noch auf der Verifizierungsseite? + still_on_verification = self._is_on_verification_page() + + if still_on_verification: + # Fehlermeldung suchen + error_message = self.automation.ui_helper.check_for_error( + error_selectors=[InstagramSelectors.ERROR_MESSAGE], + error_texts=InstagramSelectors.get_error_indicators() + ) + + if error_message: + logger.error(f"Verifizierungsfehler: {error_message}") + return False, error_message + else: + logger.error("Verifizierung fehlgeschlagen, immer noch auf der Verifizierungsseite") + return False, "Immer noch auf der Verifizierungsseite" + + # Erfolg anhand verschiedener Indikatoren prüfen + current_url = self.browser.page.url + + # Wenn wir auf der Startseite sind + if "instagram.com" in current_url and "/accounts/" not in current_url: + logger.info("Verifizierung erfolgreich, jetzt auf der Startseite") + return True, None + + # Prüfe auf weitere Einrichtungsschritte (auch ein Erfolgszeichen) + setup_indicators = [ + "add profile photo", + "profilbild hinzufügen", + "find friends", + "freunde finden", + "follow accounts", + "konten folgen", + "set up your profile", + "einrichten deines profils" + ] + + page_content = self.browser.page.content().lower() + + for indicator in setup_indicators: + if indicator in page_content: + logger.info(f"Verifizierung erfolgreich, jetzt bei weiteren Einrichtungsschritten: {indicator}") + return True, None + + # Erfolg anhand von UI-Elementen prüfen + success_indicators = InstagramSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=2000): + logger.info(f"Verifizierung erfolgreich, Erfolgsindikator gefunden: {indicator}") + return True, None + + # Wenn keine eindeutigen Indikatoren gefunden wurden, aber auch keine Fehler + logger.warning("Keine eindeutigen Erfolgsindikatoren für die Verifizierung gefunden") + return True, None # Wir gehen davon aus, dass es erfolgreich war + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Verifizierungserfolgs: {e}") + return False, f"Fehler bei der Erfolgsprüfung: {str(e)}" + + def _handle_post_verification_dialogs(self) -> None: + """ + Behandelt Dialoge, die nach erfolgreicher Verifizierung erscheinen können. + """ + try: + # Liste der möglichen Dialoge und wie man sie überspringt + dialogs_to_handle = [ + { + "name": "profile_photo", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "find_friends", + "skip_texts": ["Nicht jetzt", "Später", "Not now", "Later"], + "skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"] + }, + { + "name": "follow_accounts", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "save_login_info", + "skip_texts": ["Nicht jetzt", "Not now"], + "skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"] + }, + { + "name": "notifications", + "skip_texts": ["Nicht jetzt", "Not now"], + "skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"] + } + ] + + # Versuche, jeden möglichen Dialog zu behandeln + for dialog in dialogs_to_handle: + self._try_skip_dialog(dialog) + + logger.info("Nachverifikations-Dialoge behandelt") + + except Exception as e: + logger.warning(f"Fehler beim Behandeln der Nachverifikations-Dialoge: {e}") + # Nicht kritisch, daher keine Fehlerbehandlung + + def _try_skip_dialog(self, dialog: Dict[str, Any]) -> bool: + """ + Versucht, einen bestimmten Dialog zu überspringen. + + Args: + dialog: Informationen zum Dialog + + Returns: + bool: True wenn Dialog gefunden und übersprungen, False sonst + """ + try: + # Zuerst mit Fuzzy-Matching versuchen + skip_clicked = self.automation.ui_helper.click_button_fuzzy( + dialog["skip_texts"], + threshold=0.7, + timeout=3000 + ) + + if skip_clicked: + logger.info(f"Dialog '{dialog['name']}' mit Fuzzy-Matching übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn Fuzzy-Matching fehlschlägt, direkte Selektoren versuchen + for selector in dialog["skip_selectors"]: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info(f"Dialog '{dialog['name']}' mit direktem Selektor übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + return False + + except Exception as e: + logger.warning(f"Fehler beim Versuch, Dialog '{dialog['name']}' zu überspringen: {e}") + return False + + def resend_verification_code(self) -> bool: + """ + Versucht, den Verifizierungscode erneut senden zu lassen. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Resend-Button suchen und klicken + resend_selectors = [ + InstagramSelectors.RESEND_CODE_BUTTON, + "//button[contains(text(), 'Code erneut senden')]", + "//button[contains(text(), 'Resend code')]", + "//button[contains(text(), 'Send again')]", + "//button[contains(text(), 'Noch einmal senden')]", + "a[href*='resend']" + ] + + for selector in resend_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + if self.browser.click_element(selector): + logger.info("Code erneut angefordert") + self.automation.human_behavior.wait_between_actions("decision", 1.5) + return True + + # Fuzzy-Matching versuchen + resend_texts = ["Code erneut senden", "Resend code", "Send again", "Noch einmal senden"] + + resend_clicked = self.automation.ui_helper.click_button_fuzzy( + resend_texts, + threshold=0.7, + timeout=3000 + ) + + if resend_clicked: + logger.info("Code erneut angefordert (über Fuzzy-Matching)") + self.automation.human_behavior.wait_between_actions("decision", 1.5) + return True + + logger.warning("Konnte keinen 'Code erneut senden'-Button finden") + return False + + except Exception as e: + logger.error(f"Fehler beim erneuten Anfordern des Codes: {e}") + return False \ No newline at end of file diff --git a/social_networks/instagram/instagram_workflow.py b/social_networks/instagram/instagram_workflow.py new file mode 100644 index 0000000..eb7e7bf --- /dev/null +++ b/social_networks/instagram/instagram_workflow.py @@ -0,0 +1,454 @@ +""" +Instagram-Workflow - Definiert die Schritte für die Instagram-Anmeldung und -Registrierung +Mit TextSimilarity-Integration für robusteres Element-Matching +""" + +import logging +from typing import Dict, List, Any, Optional, Tuple +import re + +from utils.text_similarity import TextSimilarity + +# Konfiguriere Logger +logger = logging.getLogger("instagram_workflow") + +class InstagramWorkflow: + """ + Definiert die Workflow-Schritte für verschiedene Instagram-Aktionen + wie Registrierung, Anmeldung und Verifizierung. + Enthält TextSimilarity-Integration für robusteres Element-Matching. + """ + + # Text-Ähnlichkeits-Threshold für Fuzzy-Matching + SIMILARITY_THRESHOLD = 0.7 + + # Initialisiere TextSimilarity für Matching + text_similarity = TextSimilarity(default_threshold=SIMILARITY_THRESHOLD) + + # Mögliche alternative Texte für verschiedene UI-Elemente + TEXT_ALTERNATIVES = { + "email": ["E-Mail", "Email", "E-mail", "Mail", "email"], + "phone": ["Telefon", "Telefonnummer", "Phone", "Mobile", "mobile"], + "fullname": ["Vollständiger Name", "Full Name", "Name", "full name"], + "username": ["Benutzername", "Username", "user name"], + "password": ["Passwort", "Password", "pass"], + "submit": ["Registrieren", "Sign up", "Anmelden", "Login", "Log in", "Submit"], + "next": ["Weiter", "Next", "Continue", "Fortfahren"], + "confirm": ["Bestätigen", "Confirm", "Verify", "Verifizieren"], + "reject_cookies": ["Ablehnen", "Nur erforderliche", "Decline", "Reject", "Only necessary"], + "skip": ["Überspringen", "Skip", "Later", "Später", "Not now", "Nicht jetzt"] + } + + @staticmethod + def get_registration_workflow(registration_method: str = "email") -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die Instagram-Registrierung zurück. + + Args: + registration_method: "email" oder "phone" + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + # Basisschritte für beide Methoden + common_steps = [ + { + "name": "navigate_to_signup", + "description": "Zur Registrierungsseite navigieren", + "url": "https://www.instagram.com/accounts/emailsignup/", + "wait_for": ["input[name='emailOrPhone']", "div[role='dialog']"], + "fuzzy_match": None # Kein Fuzzy-Matching für die Navigation + }, + { + "name": "handle_cookie_banner", + "description": "Cookie-Banner behandeln", + "action": "click", + "target": "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]", + "optional": True, + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["reject_cookies"] # Fuzzy-Matching für Cookie-Ablehnung + } + ] + + # Registrierungsmethode wechseln, falls nötig + method_steps = [] + if registration_method == "phone": + method_steps.append({ + "name": "switch_to_phone", + "description": "Zur Telefon-Registrierungsmethode wechseln", + "action": "click", + "target": "//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]", + "wait_for": ["input[name='emailOrPhone']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["phone"] # Fuzzy-Matching für Telefon-Tab + }) + elif registration_method == "email": + method_steps.append({ + "name": "switch_to_email", + "description": "Zur E-Mail-Registrierungsmethode wechseln", + "action": "click", + "target": "//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]", + "wait_for": ["input[name='emailOrPhone']"], + "optional": True, # Meist Standard + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["email"] # Fuzzy-Matching für E-Mail-Tab + }) + + # Formularausfüllung für E-Mail/Telefon + form_steps = [ + { + "name": "fill_email_or_phone", + "description": f"E-Mail/Telefon eingeben ({registration_method})", + "action": "fill", + "target": "input[name='emailOrPhone']", + "value": "{EMAIL_OR_PHONE}", + "fuzzy_match": ["Handynummer oder E-Mail-Adresse", "Mobile Number or Email", + "Phone number or email", "E-Mail-Adresse oder Telefonnummer"] + }, + { + "name": "fill_fullname", + "description": "Vollständigen Namen eingeben", + "action": "fill", + "target": "input[name='fullName']", + "value": "{FULL_NAME}", + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["fullname"] + }, + { + "name": "fill_username", + "description": "Benutzernamen eingeben", + "action": "fill", + "target": "input[name='username']", + "value": "{USERNAME}", + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["username"] + }, + { + "name": "fill_password", + "description": "Passwort eingeben", + "action": "fill", + "target": "input[name='password']", + "value": "{PASSWORD}", + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["password"] + }, + { + "name": "submit_form", + "description": "Formular absenden", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["select[title='Monat:']", "select[title='Tag:']", "select[title='Jahr:']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["submit"] + } + ] + + # Geburtsdatumschritte + birthday_steps = [ + { + "name": "select_month", + "description": "Monat auswählen", + "action": "select", + "target": "select[title='Monat:']", + "value": "{MONTH}", + "fuzzy_match": ["Monat", "Month"] + }, + { + "name": "select_day", + "description": "Tag auswählen", + "action": "select", + "target": "select[title='Tag:']", + "value": "{DAY}", + "fuzzy_match": ["Tag", "Day"] + }, + { + "name": "select_year", + "description": "Jahr auswählen", + "action": "select", + "target": "select[title='Jahr:']", + "value": "{YEAR}", + "fuzzy_match": ["Jahr", "Year"] + }, + { + "name": "submit_birthday", + "description": "Geburtsdatum bestätigen", + "action": "click", + "target": "//button[contains(text(), 'Weiter') or contains(text(), 'Next')]", + "wait_for": ["input[name='confirmationCode']", "input[aria-label='Bestätigungscode']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["next"] + } + ] + + # Bestätigungscodeschritte + verification_steps = [ + { + "name": "enter_confirmation_code", + "description": "Bestätigungscode eingeben", + "action": "fill", + "target": "input[name='confirmationCode']", + "alternative_target": "input[aria-label='Bestätigungscode']", + "value": "{CONFIRMATION_CODE}", + "fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"] + }, + { + "name": "submit_verification", + "description": "Bestätigungscode absenden", + "action": "click", + "target": "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]", + "wait_for": ["img[alt='Instagram']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["confirm"] + } + ] + + # Vollständigen Workflow zusammenstellen + workflow = common_steps + method_steps + form_steps + birthday_steps + verification_steps + + return workflow + + @staticmethod + def get_login_workflow() -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die Instagram-Anmeldung zurück. + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + login_steps = [ + { + "name": "navigate_to_login", + "description": "Zur Anmeldeseite navigieren", + "url": "https://www.instagram.com/accounts/login/", + "wait_for": ["input[name='username']", "div[role='dialog']"], + "fuzzy_match": None # Kein Fuzzy-Matching für die Navigation + }, + { + "name": "handle_cookie_banner", + "description": "Cookie-Banner behandeln", + "action": "click", + "target": "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]", + "optional": True, + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["reject_cookies"] + }, + { + "name": "fill_username_or_email", + "description": "Benutzername oder E-Mail eingeben", + "action": "fill", + "target": "input[name='username']", + "value": "{USERNAME_OR_EMAIL}", + "fuzzy_match": ["Benutzername", "Username", "E-Mail", "Email", "Telefonnummer", "Phone number"] + }, + { + "name": "fill_password", + "description": "Passwort eingeben", + "action": "fill", + "target": "input[name='password']", + "value": "{PASSWORD}", + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["password"] + }, + { + "name": "submit_login", + "description": "Anmeldung absenden", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["svg[aria-label='Home']", "img[alt='Instagram']"], + "fuzzy_match": ["Anmelden", "Log in", "Einloggen", "Login"] + }, + { + "name": "handle_save_info_prompt", + "description": "Optional: 'Anmeldedaten speichern'-Prompt ablehnen", + "action": "click", + "target": "//button[contains(text(), 'Nicht jetzt') or contains(text(), 'Not now')]", + "optional": True, + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["skip"] + }, + { + "name": "handle_notifications_prompt", + "description": "Optional: Benachrichtigungen-Prompt ablehnen", + "action": "click", + "target": "//button[contains(text(), 'Nicht jetzt') or contains(text(), 'Not now')]", + "optional": True, + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["skip"] + } + ] + + return login_steps + + @staticmethod + def get_verification_workflow(verification_method: str = "email") -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die Instagram-Verifizierung zurück. + + Args: + verification_method: "email" oder "phone" + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + verification_steps = [ + { + "name": "wait_for_verification_page", + "description": "Auf Verifizierungsseite warten", + "wait_for": ["input[name='confirmationCode']", "input[aria-label='Bestätigungscode']"], + "fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"] + }, + { + "name": "enter_verification_code", + "description": f"Verifizierungscode eingeben ({verification_method})", + "action": "fill", + "target": "input[name='confirmationCode']", + "alternative_target": "input[aria-label='Bestätigungscode']", + "value": "{VERIFICATION_CODE}", + "fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"] + }, + { + "name": "submit_verification", + "description": "Verifizierungscode absenden", + "action": "click", + "target": "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]", + "wait_for": ["img[alt='Instagram']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["confirm"] + } + ] + + return verification_steps + + @staticmethod + def identify_current_step(page_title: str, page_url: str, visible_elements: List[str]) -> str: + """ + Identifiziert den aktuellen Schritt basierend auf dem Seitentitel, der URL und sichtbaren Elementen. + + Args: + page_title: Titel der Seite + page_url: URL der Seite + visible_elements: Liste sichtbarer Elemente (Selektoren) + + Returns: + str: Name des identifizierten Schritts + """ + # Registrierungsseite + if "signup" in page_url: + if "input[name='emailOrPhone']" in visible_elements: + return "fill_registration_form" + elif "select[title='Monat:']" in visible_elements: + return "select_birthday" + elif "input[name='confirmationCode']" in visible_elements: + return "enter_confirmation_code" + + # Anmeldeseite + elif "login" in page_url: + return "fill_login_form" + + # Verifizierungsseite - robuste Erkennung mit Text-Matching + elif any(element for element in visible_elements if + InstagramWorkflow.text_similarity.contains_similar_text(element, + ["Bestätigungscode", "Verification", "Code"], + threshold=InstagramWorkflow.SIMILARITY_THRESHOLD)): + return "enter_verification_code" + + # Verifizierungsseite - Fallback mit Selektoren + elif "input[name='confirmationCode']" in visible_elements: + return "enter_verification_code" + + # Startseite / Dashboard - robuste Erkennung mit Text-Matching + elif "instagram.com/" in page_url and any(element for element in visible_elements if + InstagramWorkflow.text_similarity.contains_similar_text(element, + ["Home", "Feed", "Startseite"], + threshold=InstagramWorkflow.SIMILARITY_THRESHOLD)): + return "logged_in" + + # Startseite / Dashboard - Fallback mit Selektoren + elif "instagram.com/" in page_url and ("svg[aria-label='Home']" in visible_elements or "img[alt='Instagram']" in visible_elements): + return "logged_in" + + # Nicht identifizierbar + return "unknown" + + @staticmethod + def find_similar_element(elements: List[Dict[str, str]], target_text: str, threshold: float = None) -> Optional[Dict[str, str]]: + """ + Findet ein Element, das dem Zieltext ähnlich ist. + + Args: + elements: Liste von Elementen mit Text-Eigenschaft + target_text: Zu suchender Text + threshold: Ähnlichkeitsschwellenwert (None für Standardwert) + + Returns: + Element oder None, wenn keines gefunden wurde + """ + if threshold is None: + threshold = InstagramWorkflow.SIMILARITY_THRESHOLD + + for element in elements: + element_text = element.get("text", "") + if not element_text: + continue + + if InstagramWorkflow.text_similarity.is_similar(target_text, element_text, threshold=threshold): + return element + + return None + + @staticmethod + def get_alternative_texts(element_type: str) -> List[str]: + """ + Gibt alternative Texte für einen Elementtyp zurück. + + Args: + element_type: Typ des Elements (z.B. "email", "submit") + + Returns: + Liste mit alternativen Texten + """ + return InstagramWorkflow.TEXT_ALTERNATIVES.get(element_type, []) + + @staticmethod + def fuzzy_find_step_by_name(workflow: List[Dict[str, Any]], step_name_pattern: str) -> Optional[Dict[str, Any]]: + """ + Findet einen Workflow-Schritt anhand eines Namensmusters. + + Args: + workflow: Workflow-Schritte + step_name_pattern: Name oder Muster für den gesuchten Schritt + + Returns: + Der gefundene Schritt oder None + """ + # Exakte Übereinstimmung prüfen + for step in workflow: + if step["name"] == step_name_pattern: + return step + + # Mustersuche mit regulären Ausdrücken + pattern = re.compile(step_name_pattern, re.IGNORECASE) + for step in workflow: + if pattern.search(step["name"]) or pattern.search(step.get("description", "")): + return step + + # Fuzzy-Matching als letzter Ausweg + for step in workflow: + name_similarity = InstagramWorkflow.text_similarity.similarity_ratio(step_name_pattern, step["name"]) + desc_similarity = InstagramWorkflow.text_similarity.similarity_ratio(step_name_pattern, step.get("description", "")) + + if name_similarity > 0.8 or desc_similarity > 0.8: + return step + + 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 Workflow-Generierung + email_workflow = InstagramWorkflow.get_registration_workflow("email") + phone_workflow = InstagramWorkflow.get_registration_workflow("phone") + login_workflow = InstagramWorkflow.get_login_workflow() + + print(f"E-Mail-Registrierung: {len(email_workflow)} Schritte") + print(f"Telefon-Registrierung: {len(phone_workflow)} Schritte") + print(f"Anmeldung: {len(login_workflow)} Schritte") + + # Beispiel für Workflow-Details + print("\nDetails zum E-Mail-Registrierungs-Workflow:") + for i, step in enumerate(email_workflow): + print(f"{i+1}. {step['name']}: {step['description']}") + if 'fuzzy_match' in step and step['fuzzy_match']: + print(f" Fuzzy-Match-Texte: {step['fuzzy_match']}") \ No newline at end of file diff --git a/social_networks/tiktok/__init__.py b/social_networks/tiktok/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/tiktok/__pycache__/__init__.cpython-310.pyc b/social_networks/tiktok/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..6e046d6 Binary files /dev/null and b/social_networks/tiktok/__pycache__/__init__.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/__init__.cpython-313.pyc b/social_networks/tiktok/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4086955 Binary files /dev/null and b/social_networks/tiktok/__pycache__/__init__.cpython-313.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_automation.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_automation.cpython-310.pyc new file mode 100644 index 0000000..414deb5 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_automation.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_automation.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_automation.cpython-313.pyc new file mode 100644 index 0000000..f4476f2 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_automation.cpython-313.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_login.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_login.cpython-310.pyc new file mode 100644 index 0000000..a09cb75 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_login.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_login.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_login.cpython-313.pyc new file mode 100644 index 0000000..075a4dc Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_login.cpython-313.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_registration.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_registration.cpython-310.pyc new file mode 100644 index 0000000..cfafe7a Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_registration.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_registration.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_registration.cpython-313.pyc new file mode 100644 index 0000000..75c2374 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_registration.cpython-313.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_selectors.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_selectors.cpython-310.pyc new file mode 100644 index 0000000..aaf6e72 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_selectors.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_selectors.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_selectors.cpython-313.pyc new file mode 100644 index 0000000..7908e92 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_selectors.cpython-313.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-310.pyc new file mode 100644 index 0000000..66fd7b3 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-313.pyc new file mode 100644 index 0000000..8c71915 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-313.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_utils.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_utils.cpython-310.pyc new file mode 100644 index 0000000..28ba7ce Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_utils.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_utils.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_utils.cpython-313.pyc new file mode 100644 index 0000000..6ed5bb0 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_utils.cpython-313.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_verification.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_verification.cpython-310.pyc new file mode 100644 index 0000000..f8104d5 Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_verification.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_verification.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_verification.cpython-313.pyc new file mode 100644 index 0000000..2fe482f Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_verification.cpython-313.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_workflow.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_workflow.cpython-310.pyc new file mode 100644 index 0000000..7e71fac Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_workflow.cpython-310.pyc differ diff --git a/social_networks/tiktok/__pycache__/tiktok_workflow.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_workflow.cpython-313.pyc new file mode 100644 index 0000000..acfc7ed Binary files /dev/null and b/social_networks/tiktok/__pycache__/tiktok_workflow.cpython-313.pyc differ diff --git a/social_networks/tiktok/tiktok_automation.py b/social_networks/tiktok/tiktok_automation.py new file mode 100644 index 0000000..760af17 --- /dev/null +++ b/social_networks/tiktok/tiktok_automation.py @@ -0,0 +1,328 @@ +""" +TikTok-Automatisierung - Hauptklasse für TikTok-Automatisierungsfunktionalität +""" + +import logging +import time +import random +from typing import Dict, List, Any, Optional, Tuple + +from browser.playwright_manager import PlaywrightManager +from browser.playwright_extensions import PlaywrightExtensions +from social_networks.base_automation import BaseAutomation +from utils.password_generator import PasswordGenerator +from utils.username_generator import UsernameGenerator +from utils.birthday_generator import BirthdayGenerator +from utils.human_behavior import HumanBehavior + +# Importiere Helferklassen +from .tiktok_registration import TikTokRegistration +from .tiktok_login import TikTokLogin +from .tiktok_verification import TikTokVerification +from .tiktok_ui_helper import TikTokUIHelper +from .tiktok_utils import TikTokUtils + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_automation") + +class TikTokAutomation(BaseAutomation): + """ + Hauptklasse für die TikTok-Automatisierung. + Implementiert die Registrierung und Anmeldung bei TikTok. + """ + + def __init__(self, + headless: bool = False, + use_proxy: bool = False, + proxy_type: str = None, + save_screenshots: bool = True, + screenshots_dir: str = None, + slowmo: int = 0, + debug: bool = False, + email_domain: str = "z5m7q9dk3ah2v1plx6ju.com", + enhanced_stealth: bool = True, + fingerprint_noise: float = 0.5): + """ + Initialisiert die TikTok-Automatisierung. + + Args: + headless: Ob der Browser im Headless-Modus ausgeführt werden soll + use_proxy: Ob ein Proxy verwendet werden soll + proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ + save_screenshots: Ob Screenshots gespeichert werden sollen + screenshots_dir: Verzeichnis für Screenshots + slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging) + debug: Ob Debug-Informationen angezeigt werden sollen + email_domain: Domain für generierte E-Mail-Adressen + enhanced_stealth: Ob erweiterter Stealth-Modus aktiviert werden soll + fingerprint_noise: Menge an Rauschen für Fingerprint-Verschleierung (0.0-1.0) + """ + # Initialisiere die Basisklasse + super().__init__( + headless=headless, + use_proxy=use_proxy, + proxy_type=proxy_type, + save_screenshots=save_screenshots, + screenshots_dir=screenshots_dir, + slowmo=slowmo, + debug=debug, + email_domain=email_domain + ) + + # Stealth-Modus-Einstellungen + self.enhanced_stealth = enhanced_stealth + self.fingerprint_noise = max(0.0, min(1.0, fingerprint_noise)) + + # Initialisiere Helferklassen + self.registration = TikTokRegistration(self) + self.login = TikTokLogin(self) + self.verification = TikTokVerification(self) + self.ui_helper = TikTokUIHelper(self) + self.utils = TikTokUtils(self) + + # Zusätzliche Hilfsklassen + self.password_generator = PasswordGenerator() + self.username_generator = UsernameGenerator() + self.birthday_generator = BirthdayGenerator() + self.human_behavior = HumanBehavior(speed_factor=0.8, randomness=0.6) + + logger.info("TikTok-Automatisierung initialisiert") + + def _initialize_browser(self) -> bool: + """ + Initialisiert den Browser mit den entsprechenden Einstellungen. + Diese Methode überschreibt die Methode der Basisklasse, um den erweiterten + Fingerprint-Schutz zu aktivieren. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Proxy-Konfiguration, falls aktiviert + proxy_config = None + if self.use_proxy: + proxy_config = self.proxy_rotator.get_proxy(self.proxy_type) + if not proxy_config: + logger.warning(f"Kein Proxy vom Typ '{self.proxy_type}' verfügbar, verwende direkten Zugriff") + + # Browser initialisieren + self.browser = PlaywrightManager( + headless=self.headless, + proxy=proxy_config, + browser_type="chromium", + screenshots_dir=self.screenshots_dir, + slowmo=self.slowmo + ) + + # Browser starten + self.browser.start() + + # Erweiterten Fingerprint-Schutz aktivieren, wenn gewünscht + if self.enhanced_stealth: + # Erstelle Extensions-Objekt + extensions = PlaywrightExtensions(self.browser) + + # Methoden anhängen + extensions.hook_into_playwright_manager() + + # Fingerprint-Schutz aktivieren mit angepasster Konfiguration + fingerprint_config = { + "noise_level": self.fingerprint_noise, + "canvas_noise": True, + "audio_noise": True, + "webgl_noise": True, + "hardware_concurrency": random.choice([4, 6, 8]), + "device_memory": random.choice([4, 8]), + "timezone_id": "Europe/Berlin" + } + + success = self.browser.enable_enhanced_fingerprint_protection(fingerprint_config) + if success: + logger.info("Erweiterter Fingerprint-Schutz erfolgreich aktiviert") + else: + logger.warning("Erweiterter Fingerprint-Schutz konnte nicht aktiviert werden") + + logger.info("Browser erfolgreich initialisiert") + return True + + except Exception as e: + logger.error(f"Fehler bei der Browser-Initialisierung: {e}") + self.status["error"] = f"Browser-Initialisierungsfehler: {str(e)}" + return False + + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Registriert einen neuen TikTok-Account. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + logger.info(f"Starte TikTok-Account-Registrierung für '{full_name}' via {registration_method}") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Rotiere Fingerprint vor der Hauptaktivität, um Erkennung weiter zu erschweren + if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'): + self.browser.rotate_fingerprint() + logger.info("Browser-Fingerprint vor der Registrierung rotiert") + + # Delegiere die Hauptregistrierungslogik an die Registration-Klasse + result = self.registration.register_account(full_name, age, registration_method, phone_number, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"registration_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Account-Registrierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"registration_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Meldet sich bei einem bestehenden TikTok-Account an. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Anmeldung mit Status + """ + logger.info(f"Starte TikTok-Login für '{username_or_email}'") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Rotiere Fingerprint vor dem Login + if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'): + self.browser.rotate_fingerprint() + logger.info("Browser-Fingerprint vor dem Login rotiert") + + # Delegiere die Hauptlogin-Logik an die Login-Klasse + result = self.login.login_account(username_or_email, password, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"login_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Login: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"login_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Verifiziert einen TikTok-Account mit einem Bestätigungscode. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + logger.info(f"Starte TikTok-Account-Verifizierung mit Code: {verification_code}") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Delegiere die Hauptverifizierungslogik an die Verification-Klasse + result = self.verification.verify_account(verification_code, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"verification_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Account-Verifizierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"verification_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + 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 or not hasattr(self.browser, 'get_fingerprint_status'): + return { + "active": False, + "message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert" + } + + return self.browser.get_fingerprint_status() \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_login.py b/social_networks/tiktok/tiktok_login.py new file mode 100644 index 0000000..0d80e5e --- /dev/null +++ b/social_networks/tiktok/tiktok_login.py @@ -0,0 +1,582 @@ +""" +TikTok-Login - Klasse für die Anmeldefunktionalität bei TikTok +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple + +from .tiktok_selectors import TikTokSelectors +from .tiktok_workflow import TikTokWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_login") + +class TikTokLogin: + """ + Klasse für die Anmeldung bei TikTok-Konten. + Enthält alle Methoden für den Login-Prozess. + """ + + def __init__(self, automation): + """ + Initialisiert die TikTok-Login-Funktionalität. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + self.workflow = TikTokWorkflow.get_login_workflow() + + logger.debug("TikTok-Login initialisiert") + + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Führt den Login-Prozess für ein TikTok-Konto durch. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis des Logins mit Status + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere die Eingaben + if not self._validate_login_inputs(username_or_email, password): + return { + "success": False, + "error": "Ungültige Login-Eingaben", + "stage": "input_validation" + } + + # Account-Daten für die Anmeldung + account_data = { + "username": username_or_email, + "password": password, + "handle_2fa": kwargs.get("handle_2fa", False), + "two_factor_code": kwargs.get("two_factor_code"), + "skip_save_login": kwargs.get("skip_save_login", True) + } + + logger.info(f"Starte TikTok-Login für {username_or_email}") + + try: + # 1. Zur Login-Seite navigieren + if not self._navigate_to_login_page(): + return { + "success": False, + "error": "Konnte nicht zur Login-Seite navigieren", + "stage": "navigation" + } + + # 2. Cookie-Banner behandeln + self._handle_cookie_banner() + + # 3. Login-Formular ausfüllen + if not self._fill_login_form(account_data): + return { + "success": False, + "error": "Fehler beim Ausfüllen des Login-Formulars", + "stage": "login_form" + } + + # 4. Auf 2FA prüfen und behandeln, falls nötig + needs_2fa, two_fa_error = self._check_needs_two_factor_auth() + + if needs_2fa: + if not account_data["handle_2fa"]: + return { + "success": False, + "error": "Zwei-Faktor-Authentifizierung erforderlich, aber nicht aktiviert", + "stage": "two_factor_required" + } + + # 2FA behandeln + if not self._handle_two_factor_auth(account_data["two_factor_code"]): + return { + "success": False, + "error": "Fehler bei der Zwei-Faktor-Authentifizierung", + "stage": "two_factor_auth" + } + + # 5. Benachrichtigungserlaubnis-Dialog behandeln + self._handle_notifications_prompt() + + # 6. Erfolgreichen Login überprüfen + if not self._check_login_success(): + error_message = self._get_login_error() + return { + "success": False, + "error": f"Login fehlgeschlagen: {error_message or 'Unbekannter Fehler'}", + "stage": "login_check" + } + + # Login erfolgreich + logger.info(f"TikTok-Login für {username_or_email} erfolgreich") + + return { + "success": True, + "stage": "completed", + "username": username_or_email + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler beim TikTok-Login: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception" + } + + def _validate_login_inputs(self, username_or_email: str, password: str) -> bool: + """ + Validiert die Eingaben für den Login. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + + Returns: + bool: True wenn alle Eingaben gültig sind, False sonst + """ + if not username_or_email or len(username_or_email) < 3: + logger.error("Ungültiger Benutzername oder E-Mail") + return False + + if not password or len(password) < 6: + logger.error("Ungültiges Passwort") + return False + + return True + + def _navigate_to_login_page(self) -> bool: + """ + Navigiert zur TikTok-Login-Seite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Zur Login-Seite navigieren + self.browser.navigate_to(TikTokSelectors.LOGIN_URL) + + # Warten, bis die Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Screenshot erstellen + self.automation._take_screenshot("login_page") + + # Prüfen, ob Login-Dialog sichtbar ist + if not self.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=5000): + logger.warning("Login-Dialog nicht sichtbar, versuche Login-Button zu klicken") + + # Versuche, den Login-Button zu klicken, um das Login-Modal zu öffnen + login_buttons = [ + TikTokSelectors.LOGIN_BUTTON_TOP, + TikTokSelectors.LOGIN_BUTTON_SIDEBAR + ] + + button_clicked = False + for button in login_buttons: + if self.browser.is_element_visible(button, timeout=2000): + self.browser.click_element(button) + button_clicked = True + break + + if not button_clicked: + logger.warning("Keine Login-Buttons gefunden") + return False + + # Warten, bis der Login-Dialog erscheint + self.automation.human_behavior.wait_between_actions("decision", 1.5) + + # Erneut prüfen, ob der Login-Dialog sichtbar ist + if not self.browser.is_element_visible(TikTokSelectors.LOGIN_DIALOG, timeout=5000): + logger.warning("Login-Dialog nach dem Klicken auf den Login-Button nicht sichtbar") + return False + + logger.info("Erfolgreich zur Login-Seite navigiert") + return True + + except Exception as e: + logger.error(f"Fehler beim Navigieren zur Login-Seite: {e}") + return False + + def _handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(TikTokSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + TikTokSelectors.get_button_texts("reject_cookies"), + TikTokSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(TikTokSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + def _fill_login_form(self, account_data: Dict[str, Any]) -> bool: + """ + Füllt das Login-Formular aus und sendet es ab. + + Args: + account_data: Account-Daten für den Login + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # E-Mail/Telefon-Login-Option auswählen + email_phone_option = self.automation.ui_helper.click_button_fuzzy( + ["Telefon-Nr./E-Mail/Anmeldename nutzen", "Use phone / email / username"], + TikTokSelectors.LOGIN_EMAIL_PHONE_OPTION + ) + + if not email_phone_option: + logger.warning("Konnte die E-Mail/Telefon-Option nicht auswählen, versuche direkt das Formular auszufüllen") + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # E-Mail/Benutzername eingeben + username_success = self.automation.ui_helper.fill_field_fuzzy( + TikTokSelectors.get_field_labels("email_username"), + account_data["username"], + TikTokSelectors.LOGIN_EMAIL_FIELD + ) + + if not username_success: + logger.error("Konnte Benutzername-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Passwort eingeben + password_success = self.automation.ui_helper.fill_field_fuzzy( + TikTokSelectors.get_field_labels("password"), + account_data["password"], + TikTokSelectors.LOGIN_PASSWORD_FIELD + ) + + if not password_success: + logger.error("Konnte Passwort-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot vorm Absenden + self.automation._take_screenshot("login_form_filled") + + # Formular absenden + submit_success = self.automation.ui_helper.click_button_fuzzy( + ["Anmelden", "Log in", "Login"], + TikTokSelectors.LOGIN_SUBMIT_BUTTON + ) + + if not submit_success: + logger.error("Konnte Login-Formular nicht absenden") + return False + + # Nach dem Absenden warten + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Überprüfen, ob es eine Fehlermeldung gab + error_message = self._get_login_error() + if error_message: + logger.error(f"Login-Fehler erkannt: {error_message}") + return False + + logger.info("Login-Formular erfolgreich ausgefüllt und abgesendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen des Login-Formulars: {e}") + return False + + def _get_login_error(self) -> Optional[str]: + """ + Überprüft, ob eine Login-Fehlermeldung angezeigt wird. + + Returns: + Optional[str]: Fehlermeldung oder None, wenn keine gefunden wurde + """ + try: + # Auf Fehlermeldungen prüfen + error_selectors = [ + TikTokSelectors.ERROR_MESSAGE, + "p[class*='error']", + "div[role='alert']", + "div[class*='error']", + "div[class*='Error']" + ] + + for selector in error_selectors: + error_element = self.browser.wait_for_selector(selector, timeout=2000) + if error_element: + error_text = error_element.text_content() + if error_text and len(error_text.strip()) > 0: + return error_text.strip() + + # Wenn keine spezifische Fehlermeldung gefunden wurde, nach bekannten Fehlermustern suchen + error_texts = [ + "Falsches Passwort", + "Benutzername nicht gefunden", + "incorrect password", + "username you entered doesn't belong", + "please wait a few minutes", + "try again later", + "Bitte warte einige Minuten", + "versuche es später noch einmal" + ] + + page_content = self.browser.page.content() + for error_text in error_texts: + if error_text.lower() in page_content.lower(): + return f"Erkannter Fehler: {error_text}" + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Login-Fehler: {e}") + return None + + def _check_needs_two_factor_auth(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob eine Zwei-Faktor-Authentifizierung erforderlich ist. + + Returns: + Tuple[bool, Optional[str]]: (2FA erforderlich, Fehlermeldung falls vorhanden) + """ + try: + # Nach 2FA-Indikatoren suchen + two_fa_selectors = [ + "input[name='verificationCode']", + "input[placeholder*='code']", + "input[placeholder*='Code']", + "div[class*='verification-code']", + "div[class*='two-factor']" + ] + + for selector in two_fa_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Zwei-Faktor-Authentifizierung erforderlich") + return True, None + + # Texte, die auf 2FA hinweisen + two_fa_indicators = [ + "Verifizierungscode", + "Verification code", + "Sicherheitscode", + "Security code", + "zwei-faktor", + "two-factor", + "2FA" + ] + + # Seiteninhalt durchsuchen + page_content = self.browser.page.content().lower() + + for indicator in two_fa_indicators: + if indicator.lower() in page_content: + logger.info(f"Zwei-Faktor-Authentifizierung erkannt durch Text: {indicator}") + return True, None + + return False, None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf 2FA: {e}") + return False, f"Fehler bei der 2FA-Erkennung: {str(e)}" + + def _handle_two_factor_auth(self, two_factor_code: Optional[str] = None) -> bool: + """ + Behandelt die Zwei-Faktor-Authentifizierung. + + Args: + two_factor_code: Optional vorhandener 2FA-Code + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Screenshot erstellen + self.automation._take_screenshot("two_factor_auth") + + # 2FA-Eingabefeld finden + two_fa_selectors = [ + "input[name='verificationCode']", + "input[placeholder*='code']", + "input[placeholder*='Code']" + ] + + two_fa_field = None + for selector in two_fa_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + two_fa_field = selector + break + + if not two_fa_field: + logger.error("Konnte 2FA-Eingabefeld nicht finden") + return False + + # Wenn kein Code bereitgestellt wurde, Benutzer auffordern + if not two_factor_code: + logger.warning("Kein 2FA-Code bereitgestellt, kann nicht fortfahren") + return False + + # 2FA-Code eingeben + code_success = self.browser.fill_form_field(two_fa_field, two_factor_code) + + if not code_success: + logger.error("Konnte 2FA-Code nicht eingeben") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Bestätigen-Button finden und klicken + confirm_button_selectors = [ + "button[type='submit']", + "//button[contains(text(), 'Bestätigen')]", + "//button[contains(text(), 'Confirm')]", + "//button[contains(text(), 'Verify')]" + ] + + confirm_clicked = False + for selector in confirm_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + confirm_clicked = True + break + + if not confirm_clicked: + # Alternative: Mit Tastendruck bestätigen + self.browser.page.keyboard.press("Enter") + logger.info("Enter-Taste gedrückt, um 2FA zu bestätigen") + + # Warten nach der Bestätigung + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Überprüfen, ob 2FA erfolgreich war + still_on_2fa = self._check_needs_two_factor_auth()[0] + + if still_on_2fa: + # Prüfen, ob Fehlermeldung angezeigt wird + error_message = self._get_login_error() + if error_message: + logger.error(f"2FA-Fehler: {error_message}") + else: + logger.error("2FA fehlgeschlagen, immer noch auf 2FA-Seite") + return False + + logger.info("Zwei-Faktor-Authentifizierung erfolgreich") + return True + + except Exception as e: + logger.error(f"Fehler bei der Zwei-Faktor-Authentifizierung: {e}") + return False + + def _handle_notifications_prompt(self) -> bool: + """ + Behandelt den Benachrichtigungen-Dialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Nach "Nicht jetzt"-Button suchen + not_now_selectors = [ + "//button[contains(text(), 'Nicht jetzt')]", + "//button[contains(text(), 'Not now')]", + "//button[contains(text(), 'Skip')]", + "//button[contains(text(), 'Später')]", + "//button[contains(text(), 'Later')]" + ] + + for selector in not_now_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + if self.browser.click_element(selector): + logger.info("Benachrichtigungen-Dialog übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden + logger.debug("Kein Benachrichtigungen-Dialog erkannt") + return True + + except Exception as e: + logger.warning(f"Fehler beim Behandeln des Benachrichtigungen-Dialogs: {e}") + # Dies ist nicht kritisch, daher geben wir trotzdem True zurück + return True + + def _check_login_success(self) -> bool: + """ + Überprüft, ob der Login erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Warten nach dem Login + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Screenshot erstellen + self.automation._take_screenshot("login_final") + + # Erfolg anhand verschiedener Indikatoren prüfen + success_indicators = TikTokSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=2000): + logger.info(f"Login-Erfolgsindikator gefunden: {indicator}") + return True + + # Alternativ prüfen, ob wir auf der TikTok-Startseite sind + current_url = self.browser.page.url + if "tiktok.com" in current_url and "/login" not in current_url: + logger.info(f"Login-Erfolg basierend auf URL: {current_url}") + return True + + # Prüfen, ob immer noch auf der Login-Seite + if "/login" in current_url or self.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=1000): + logger.warning("Immer noch auf der Login-Seite, Login fehlgeschlagen") + return False + + logger.warning("Keine Login-Erfolgsindikatoren gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Login-Erfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_registration.py b/social_networks/tiktok/tiktok_registration.py new file mode 100644 index 0000000..4468424 --- /dev/null +++ b/social_networks/tiktok/tiktok_registration.py @@ -0,0 +1,987 @@ +# social_networks/tiktok/tiktok_registration.py + +""" +TikTok-Registrierung - Klasse für die Kontoerstellung bei TikTok +""" + +import logging +import time +import random +import re +from typing import Dict, List, Any, Optional, Tuple + +from .tiktok_selectors import TikTokSelectors +from .tiktok_workflow import TikTokWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_registration") + +class TikTokRegistration: + """ + Klasse für die Registrierung von TikTok-Konten. + Enthält alle Methoden zur Kontoerstellung. + """ + + def __init__(self, automation): + """ + Initialisiert die TikTok-Registrierung. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + self.workflow = TikTokWorkflow.get_registration_workflow() + + logger.debug("TikTok-Registrierung initialisiert") + + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Führt den vollständigen Registrierungsprozess für einen TikTok-Account durch. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere die Eingaben + if not self._validate_registration_inputs(full_name, age, registration_method, phone_number): + return { + "success": False, + "error": "Ungültige Eingabeparameter", + "stage": "input_validation" + } + + # Account-Daten generieren + account_data = self._generate_account_data(full_name, age, registration_method, phone_number, **kwargs) + + # Starte den Registrierungsprozess + logger.info(f"Starte TikTok-Registrierung für {account_data['username']} via {registration_method}") + + try: + # 1. Zur Startseite navigieren + if not self._navigate_to_homepage(): + return { + "success": False, + "error": "Konnte nicht zur TikTok-Startseite navigieren", + "stage": "navigation", + "account_data": account_data + } + + # 2. Cookie-Banner behandeln + self._handle_cookie_banner() + + # 3. Anmelden-Button klicken + if not self._click_login_button(): + return { + "success": False, + "error": "Konnte nicht auf Anmelden-Button klicken", + "stage": "login_button", + "account_data": account_data + } + + # 4. Registrieren-Link klicken + if not self._click_register_link(): + return { + "success": False, + "error": "Konnte nicht auf Registrieren-Link klicken", + "stage": "register_link", + "account_data": account_data + } + + # 5. Telefon/E-Mail-Option auswählen + if not self._click_phone_email_option(): + return { + "success": False, + "error": "Konnte nicht auf Telefon/E-Mail-Option klicken", + "stage": "phone_email_option", + "account_data": account_data + } + + # 6. E-Mail oder Telefon als Registrierungsmethode wählen + if not self._select_registration_method(registration_method): + return { + "success": False, + "error": f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen", + "stage": "registration_method", + "account_data": account_data + } + + # 7. Geburtsdatum eingeben + if not self._enter_birthday(account_data["birthday"]): + return { + "success": False, + "error": "Fehler beim Eingeben des Geburtsdatums", + "stage": "birthday", + "account_data": account_data + } + + # 8. Registrierungsformular ausfüllen + if not self._fill_registration_form(account_data, registration_method): + return { + "success": False, + "error": "Fehler beim Ausfüllen des Registrierungsformulars", + "stage": "registration_form", + "account_data": account_data + } + + # 9. Bestätigungscode abrufen und eingeben + if not self._handle_verification(account_data, registration_method): + return { + "success": False, + "error": "Fehler bei der Verifizierung", + "stage": "verification", + "account_data": account_data + } + + # 10. Benutzernamen erstellen + if not self._create_username(account_data): + return { + "success": False, + "error": "Fehler beim Erstellen des Benutzernamens", + "stage": "username", + "account_data": account_data + } + + # 11. Erfolgreiche Registrierung überprüfen + if not self._check_registration_success(): + return { + "success": False, + "error": "Registrierung fehlgeschlagen oder konnte nicht verifiziert werden", + "stage": "final_check", + "account_data": account_data + } + + # Registrierung erfolgreich abgeschlossen + logger.info(f"TikTok-Account {account_data['username']} erfolgreich erstellt") + + return { + "success": True, + "stage": "completed", + "account_data": account_data + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der TikTok-Registrierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception", + "account_data": account_data + } + + def _validate_registration_inputs(self, full_name: str, age: int, + registration_method: str, phone_number: str) -> bool: + """ + Validiert die Eingaben für die Registrierung. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + + Returns: + bool: True wenn alle Eingaben gültig sind, False sonst + """ + # Vollständiger Name prüfen + if not full_name or len(full_name) < 3: + logger.error("Ungültiger vollständiger Name") + return False + + # Alter prüfen + if age < 13: + logger.error("Benutzer muss mindestens 13 Jahre alt sein") + return False + + # Registrierungsmethode prüfen + if registration_method not in ["email", "phone"]: + logger.error(f"Ungültige Registrierungsmethode: {registration_method}") + return False + + # Telefonnummer prüfen, falls erforderlich + if registration_method == "phone" and not phone_number: + logger.error("Telefonnummer erforderlich für Registrierung via Telefon") + return False + + return True + + def _generate_account_data(self, full_name: str, age: int, registration_method: str, + phone_number: str, **kwargs) -> Dict[str, Any]: + """ + Generiert Account-Daten für die Registrierung. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Generierte Account-Daten + """ + # Benutzername generieren + username = kwargs.get("username") + if not username: + username = self.automation.username_generator.generate_username("tiktok", full_name) + + # Passwort generieren + password = kwargs.get("password") + if not password: + password = self.automation.password_generator.generate_password("tiktok") + + # E-Mail generieren (falls nötig) + email = None + if registration_method == "email": + email_prefix = username.lower().replace(".", "").replace("_", "") + email = f"{email_prefix}@{self.automation.email_domain}" + + # Geburtsdatum generieren + birthday = self.automation.birthday_generator.generate_birthday_components("tiktok", age) + + # Account-Daten zusammenstellen + account_data = { + "username": username, + "password": password, + "full_name": full_name, + "email": email, + "phone": phone_number, + "birthday": birthday, + "age": age, + "registration_method": registration_method + } + + logger.debug(f"Account-Daten generiert: {account_data['username']}") + + return account_data + + def _navigate_to_homepage(self) -> bool: + """ + Navigiert zur TikTok-Startseite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Zur Startseite navigieren + self.browser.navigate_to(TikTokSelectors.BASE_URL) + + # Warten, bis die Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Screenshot erstellen + self.automation._take_screenshot("tiktok_homepage") + + # Prüfen, ob die Seite korrekt geladen wurde + if not self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON, timeout=5000): + logger.warning("TikTok-Startseite nicht korrekt geladen") + return False + + logger.info("Erfolgreich zur TikTok-Startseite navigiert") + return True + + except Exception as e: + logger.error(f"Fehler beim Navigieren zur TikTok-Startseite: {e}") + return False + + def _handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(TikTokSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + TikTokSelectors.get_button_texts("reject_cookies"), + TikTokSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(TikTokSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + def _click_login_button(self) -> bool: + """ + Klickt auf den Anmelden-Button auf der Startseite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Versuche zuerst den Hauptbutton + if self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON, timeout=2000): + result = self.browser.click_element(TikTokSelectors.LOGIN_BUTTON) + if result: + logger.info("Anmelden-Button erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche alternativ den Button in der oberen rechten Ecke + if self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON_TOP_RIGHT, timeout=2000): + result = self.browser.click_element(TikTokSelectors.LOGIN_BUTTON_TOP_RIGHT) + if result: + logger.info("Anmelden-Button (oben rechts) erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Anmelden", "Log in", "Login"], + TikTokSelectors.LOGIN_BUTTON_FALLBACK + ) + + if result: + logger.info("Anmelden-Button über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + logger.error("Konnte keinen Anmelden-Button finden") + return False + + except Exception as e: + logger.error(f"Fehler beim Klicken auf den Anmelden-Button: {e}") + return False + + def _click_register_link(self) -> bool: + """ + Klickt auf den Registrieren-Link im Login-Dialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis der Login-Dialog angezeigt wird + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Prüfen, ob wir bereits im Registrierungsdialog sind + if self.browser.is_element_visible(TikTokSelectors.REGISTER_DIALOG_TITLE, timeout=2000): + logger.info("Bereits im Registrierungsdialog") + return True + + # Versuche, den Registrieren-Link zu finden und zu klicken + if self.browser.is_element_visible(TikTokSelectors.REGISTER_LINK, timeout=2000): + result = self.browser.click_element(TikTokSelectors.REGISTER_LINK) + if result: + logger.info("Registrieren-Link erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Registrieren", "Sign up", "Konto erstellen", "Register"], + TikTokSelectors.REGISTER_LINK_FALLBACK + ) + + if result: + logger.info("Registrieren-Link über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Prüfe, ob der Text "Du hast noch kein Konto? Registrieren" vorhanden ist + register_link_text = "Du hast noch kein Konto? Registrieren" + elements = self.browser.page.query_selector_all("*") + for element in elements: + if register_link_text in element.inner_text(): + # Finde das "Registrieren"-Wort und klicke darauf + matches = re.search(r"(.*?)(Registrieren)$", element.inner_text()) + if matches: + # Versuche, nur auf das Wort "Registrieren" zu klicken + element.click() + logger.info("Auf 'Registrieren' Text geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + logger.error("Konnte keinen Registrieren-Link finden") + return False + + except Exception as e: + logger.error(f"Fehler beim Klicken auf den Registrieren-Link: {e}") + return False + + def _click_phone_email_option(self) -> bool: + """ + Klickt auf die Telefon/E-Mail-Option im Registrierungsdialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis der Registrierungsdialog angezeigt wird + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Prüfen, ob wir bereits die Optionen für Telefon/E-Mail sehen + if self.browser.is_element_visible(TikTokSelectors.EMAIL_FIELD, timeout=2000) or \ + self.browser.is_element_visible(TikTokSelectors.PHONE_FIELD, timeout=2000): + logger.info("Bereits auf der Telefon/E-Mail-Registrierungsseite") + return True + + # Versuche, die Telefon/E-Mail-Option zu finden und zu klicken + if self.browser.is_element_visible(TikTokSelectors.PHONE_EMAIL_OPTION, timeout=2000): + result = self.browser.click_element(TikTokSelectors.PHONE_EMAIL_OPTION) + if result: + logger.info("Telefon/E-Mail-Option erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Telefonnummer oder E-Mail-Adresse nutzen", "Use phone or email", "Phone or email"], + TikTokSelectors.PHONE_EMAIL_OPTION_FALLBACK + ) + + if result: + logger.info("Telefon/E-Mail-Option über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + logger.error("Konnte keine Telefon/E-Mail-Option finden") + return False + + except Exception as e: + logger.error(f"Fehler beim Klicken auf die Telefon/E-Mail-Option: {e}") + return False + + def _select_registration_method(self, registration_method: str) -> bool: + """ + Wählt die Registrierungsmethode (E-Mail oder Telefon). + + Args: + registration_method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Registrierungsmethoden-Seite geladen ist + self.automation.human_behavior.random_delay(1.0, 2.0) + + if registration_method == "email": + # Wenn bereits das E-Mail-Feld sichtbar ist, sind wir schon auf der richtigen Seite + if self.browser.is_element_visible(TikTokSelectors.EMAIL_FIELD, timeout=1000): + logger.info("Bereits auf der E-Mail-Registrierungsseite") + return True + + # Suche nach dem "Mit E-Mail-Adresse registrieren" Link + if self.browser.is_element_visible(TikTokSelectors.EMAIL_OPTION, timeout=2000): + result = self.browser.click_element(TikTokSelectors.EMAIL_OPTION) + if result: + logger.info("E-Mail-Registrierungsmethode erfolgreich ausgewählt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Mit E-Mail-Adresse registrieren", "Register with email", "E-Mail-Adresse"], + TikTokSelectors.EMAIL_OPTION_FALLBACK + ) + + if result: + logger.info("E-Mail-Option über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + elif registration_method == "phone": + # Wenn bereits das Telefon-Feld sichtbar ist, sind wir schon auf der richtigen Seite + if self.browser.is_element_visible(TikTokSelectors.PHONE_FIELD, timeout=1000): + logger.info("Bereits auf der Telefon-Registrierungsseite") + return True + + # Suche nach dem "Mit Telefonnummer registrieren" Link + if self.browser.is_element_visible(TikTokSelectors.PHONE_OPTION, timeout=2000): + result = self.browser.click_element(TikTokSelectors.PHONE_OPTION) + if result: + logger.info("Telefon-Registrierungsmethode erfolgreich ausgewählt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Mit Telefonnummer registrieren", "Register with phone", "Telefonnummer"], + TikTokSelectors.PHONE_OPTION_FALLBACK + ) + + if result: + logger.info("Telefon-Option über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + logger.error(f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen") + return False + + except Exception as e: + logger.error(f"Fehler beim Auswählen der Registrierungsmethode: {e}") + return False + + def _enter_birthday(self, birthday: Dict[str, int]) -> bool: + """ + Gibt das Geburtsdatum ein. + + Args: + birthday: Dictionary mit 'year', 'month', 'day' Schlüsseln + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Geburtstagsauswahl angezeigt wird + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Monat auswählen + month_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_MONTH_DROPDOWN, timeout=5000) + if not month_dropdown: + logger.error("Monat-Dropdown nicht gefunden") + return False + + month_dropdown.click() + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Monat-Option auswählen + month_option = self.browser.wait_for_selector( + TikTokSelectors.get_month_option_selector(birthday["month"]), + timeout=3000 + ) + if month_option: + month_option.click() + logger.info(f"Monat {birthday['month']} ausgewählt") + else: + # Fallback: Monat über Select-Funktion auswählen + month_success = self.browser.select_option( + TikTokSelectors.BIRTHDAY_MONTH_DROPDOWN, + str(birthday["month"]) + ) + if not month_success: + logger.error(f"Konnte Monat {birthday['month']} nicht auswählen") + return False + + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Tag auswählen + day_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_DAY_DROPDOWN, timeout=3000) + if not day_dropdown: + logger.error("Tag-Dropdown nicht gefunden") + return False + + day_dropdown.click() + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Tag-Option auswählen + day_option = self.browser.wait_for_selector( + TikTokSelectors.get_day_option_selector(birthday["day"]), + timeout=3000 + ) + if day_option: + day_option.click() + logger.info(f"Tag {birthday['day']} ausgewählt") + else: + # Fallback: Tag über Select-Funktion auswählen + day_success = self.browser.select_option( + TikTokSelectors.BIRTHDAY_DAY_DROPDOWN, + str(birthday["day"]) + ) + if not day_success: + logger.error(f"Konnte Tag {birthday['day']} nicht auswählen") + return False + + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Jahr auswählen + year_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_YEAR_DROPDOWN, timeout=3000) + if not year_dropdown: + logger.error("Jahr-Dropdown nicht gefunden") + return False + + year_dropdown.click() + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Jahr-Option auswählen + year_option = self.browser.wait_for_selector( + TikTokSelectors.get_year_option_selector(birthday["year"]), + timeout=3000 + ) + if year_option: + year_option.click() + logger.info(f"Jahr {birthday['year']} ausgewählt") + else: + # Fallback: Jahr über Select-Funktion auswählen + year_success = self.browser.select_option( + TikTokSelectors.BIRTHDAY_YEAR_DROPDOWN, + str(birthday["year"]) + ) + if not year_success: + logger.error(f"Konnte Jahr {birthday['year']} nicht auswählen") + return False + + logger.info(f"Geburtsdatum {birthday['month']}/{birthday['day']}/{birthday['year']} erfolgreich eingegeben") + return True + + except Exception as e: + logger.error(f"Fehler beim Eingeben des Geburtsdatums: {e}") + return False + + def _fill_registration_form(self, account_data: Dict[str, Any], registration_method: str) -> bool: + """ + Füllt das Registrierungsformular aus. + + Args: + account_data: Account-Daten für die Registrierung + registration_method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Je nach Registrierungsmethode das entsprechende Feld ausfüllen + if registration_method == "email": + # E-Mail-Feld ausfüllen + email_success = self.automation.ui_helper.fill_field_fuzzy( + ["E-Mail-Adresse", "Email", "E-Mail"], + account_data["email"], + TikTokSelectors.EMAIL_FIELD + ) + + if not email_success: + logger.error("Konnte E-Mail-Feld nicht ausfüllen") + return False + + logger.info(f"E-Mail-Feld ausgefüllt: {account_data['email']}") + + elif registration_method == "phone": + # Telefonnummer-Feld ausfüllen (ohne Ländervorwahl) + phone_number = account_data["phone"] + if phone_number.startswith("+"): + # Entferne Ländervorwahl, wenn vorhanden + parts = phone_number.split(" ", 1) + if len(parts) > 1: + phone_number = parts[1] + + phone_success = self.automation.ui_helper.fill_field_fuzzy( + ["Telefonnummer", "Phone number", "Phone"], + phone_number, + TikTokSelectors.PHONE_FIELD + ) + + if not phone_success: + logger.error("Konnte Telefonnummer-Feld nicht ausfüllen") + return False + + logger.info(f"Telefonnummer-Feld ausgefüllt: {phone_number}") + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Bei E-Mail-Registrierung auch das Passwort-Feld ausfüllen + if registration_method == "email": + password_success = self.automation.ui_helper.fill_field_fuzzy( + ["Passwort", "Password"], + account_data["password"], + TikTokSelectors.PASSWORD_FIELD + ) + + if not password_success: + logger.error("Konnte Passwort-Feld nicht ausfüllen") + return False + + logger.info("Passwort-Feld ausgefüllt") + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Code senden Button klicken + send_code_success = self.automation.ui_helper.click_button_fuzzy( + ["Code senden", "Send code", "Send verification code"], + TikTokSelectors.SEND_CODE_BUTTON + ) + + if not send_code_success: + logger.error("Konnte 'Code senden'-Button nicht klicken") + return False + + logger.info("'Code senden'-Button erfolgreich geklickt") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen des Registrierungsformulars: {e}") + return False + + def _handle_verification(self, account_data: Dict[str, Any], registration_method: str) -> bool: + """ + Behandelt den Verifizierungsprozess (E-Mail/SMS). + + Args: + account_data: Account-Daten mit E-Mail/Telefon + registration_method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis der Bestätigungscode gesendet wurde + self.automation.human_behavior.wait_for_page_load() + self.automation.human_behavior.random_delay(2.0, 4.0) + + # Verifizierungscode je nach Methode abrufen + if registration_method == "email": + # Verifizierungscode von E-Mail abrufen + verification_code = self._get_email_confirmation_code(account_data["email"]) + else: + # Verifizierungscode von SMS abrufen + verification_code = self._get_sms_confirmation_code(account_data["phone"]) + + if not verification_code: + logger.error("Konnte keinen Verifizierungscode abrufen") + return False + + logger.info(f"Verifizierungscode erhalten: {verification_code}") + + # Verifizierungscode-Feld ausfüllen + code_success = self.automation.ui_helper.fill_field_fuzzy( + ["Gib den sechsstelligen Code ein", "Enter verification code", "Verification code"], + verification_code, + TikTokSelectors.VERIFICATION_CODE_FIELD + ) + + if not code_success: + logger.error("Konnte Verifizierungscode-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Weiter-Button klicken + continue_success = self.automation.ui_helper.click_button_fuzzy( + ["Weiter", "Continue", "Next", "Submit"], + TikTokSelectors.CONTINUE_BUTTON + ) + + if not continue_success: + logger.error("Konnte 'Weiter'-Button nicht klicken") + return False + + logger.info("Verifizierungscode eingegeben und 'Weiter' geklickt") + + # Warten nach der Verifizierung + self.automation.human_behavior.wait_for_page_load() + self.automation.human_behavior.random_delay(1.0, 2.0) + + return True + + except Exception as e: + logger.error(f"Fehler bei der Verifizierung: {e}") + return False + + def _get_email_confirmation_code(self, email: str) -> Optional[str]: + """ + Ruft den Bestätigungscode von einer E-Mail ab. + + Args: + email: E-Mail-Adresse, an die der Code gesendet wurde + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + try: + # Warte auf die E-Mail + verification_code = self.automation.email_handler.get_verification_code( + email_domain=self.automation.email_domain, + platform="tiktok", + timeout=120 # Warte bis zu 2 Minuten auf den Code + ) + + if verification_code: + return verification_code + + # Wenn kein Code gefunden wurde, prüfen, ob der Code vielleicht direkt angezeigt wird + verification_code = self._extract_code_from_page() + + if verification_code: + logger.info(f"Verifizierungscode direkt von der Seite extrahiert: {verification_code}") + return verification_code + + logger.warning(f"Konnte keinen Verifizierungscode für {email} finden") + return None + + except Exception as e: + logger.error(f"Fehler beim Abrufen des E-Mail-Bestätigungscodes: {e}") + return None + + def _get_sms_confirmation_code(self, phone: str) -> Optional[str]: + """ + Ruft den Bestätigungscode aus einer SMS ab. + Hier müsste ein SMS-Empfangs-Service eingebunden werden. + + Args: + phone: Telefonnummer, an die der Code gesendet wurde + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + # Diese Implementierung ist ein Platzhalter + # In einer echten Implementierung würde hier ein SMS-Empfangs-Service verwendet + logger.warning("SMS-Verifizierung ist noch nicht implementiert") + + # Versuche, den Code trotzdem zu extrahieren, falls er auf der Seite angezeigt wird + return self._extract_code_from_page() + + def _extract_code_from_page(self) -> Optional[str]: + """ + Versucht, einen Bestätigungscode direkt von der Seite zu extrahieren. + + Returns: + Optional[str]: Der extrahierte Code oder None, wenn nicht gefunden + """ + try: + # Gesamten Seiteninhalt abrufen + page_content = self.browser.page.content() + + # Mögliche Regex-Muster für Bestätigungscodes + patterns = [ + r"Dein Code ist (\d{6})", + r"Your code is (\d{6})", + r"Bestätigungscode: (\d{6})", + r"Confirmation code: (\d{6})", + r"(\d{6}) ist dein TikTok-Code", + r"(\d{6}) is your TikTok code" + ] + + for pattern in patterns: + match = re.search(pattern, page_content) + if match: + return match.group(1) + + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Codes von der Seite: {e}") + return None + + def _create_username(self, account_data: Dict[str, Any]) -> bool: + """ + Erstellt einen Benutzernamen. + + Args: + account_data: Account-Daten + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Benutzernamen-Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Prüfen, ob wir auf der Benutzernamen-Seite sind + if not self.browser.is_element_visible(TikTokSelectors.USERNAME_FIELD, timeout=5000): + logger.warning("Benutzernamen-Feld nicht gefunden, möglicherweise ist dieser Schritt übersprungen worden") + + # Versuche, den "Überspringen"-Button zu klicken, falls vorhanden + skip_visible = self.browser.is_element_visible(TikTokSelectors.SKIP_USERNAME_BUTTON, timeout=2000) + if skip_visible: + self.browser.click_element(TikTokSelectors.SKIP_USERNAME_BUTTON) + logger.info("Benutzernamen-Schritt übersprungen") + return True + + # Möglicherweise wurde der Benutzername automatisch erstellt + logger.info("Benutzernamen-Schritt möglicherweise automatisch abgeschlossen") + return True + + # Benutzernamen eingeben + username_success = self.automation.ui_helper.fill_field_fuzzy( + ["Benutzername", "Username"], + account_data["username"], + TikTokSelectors.USERNAME_FIELD + ) + + if not username_success: + logger.error("Konnte Benutzernamen-Feld nicht ausfüllen") + return False + + logger.info(f"Benutzernamen-Feld ausgefüllt: {account_data['username']}") + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Registrieren-Button klicken + register_success = self.automation.ui_helper.click_button_fuzzy( + ["Registrieren", "Register", "Sign up", "Submit"], + TikTokSelectors.REGISTER_BUTTON + ) + + if not register_success: + logger.error("Konnte 'Registrieren'-Button nicht klicken") + return False + + logger.info("'Registrieren'-Button erfolgreich geklickt") + + # Warten nach der Registrierung + self.automation.human_behavior.wait_for_page_load() + + return True + + except Exception as e: + logger.error(f"Fehler beim Erstellen des Benutzernamens: {e}") + return False + + def _check_registration_success(self) -> bool: + """ + Überprüft, ob die Registrierung erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Warten nach der Registrierung + self.automation.human_behavior.wait_for_page_load(multiplier=2.0) + + # Screenshot erstellen + self.automation._take_screenshot("registration_final") + + # Erfolg anhand verschiedener Indikatoren prüfen + success_indicators = TikTokSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=3000): + logger.info(f"Erfolgsindikator gefunden: {indicator}") + return True + + # Alternativ prüfen, ob wir auf der TikTok-Startseite sind + current_url = self.browser.page.url + if "tiktok.com" in current_url and "/signup" not in current_url and "/login" not in current_url: + logger.info(f"Erfolg basierend auf URL: {current_url}") + return True + + logger.warning("Keine Erfolgsindikatoren gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Registrierungserfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_selectors.py b/social_networks/tiktok/tiktok_selectors.py new file mode 100644 index 0000000..2606e8f --- /dev/null +++ b/social_networks/tiktok/tiktok_selectors.py @@ -0,0 +1,150 @@ +""" +TikTok-Selektoren - CSS-Selektoren und XPath-Ausdrücke für die TikTok-Automatisierung +Mit Text-Matching-Funktionen für robuste Element-Erkennung +""" + +from typing import List, Dict, Optional, Any + +class TikTokSelectors: + """ + Zentrale Sammlung aller Selektoren für die TikTok-Automatisierung. + Bei Änderungen der TikTok-Webseite müssen nur hier Anpassungen vorgenommen werden. + Enthält auch Fuzzy-Text-Matching-Daten für robustere Element-Erkennung. + """ + + # URL-Konstanten + BASE_URL = "https://www.tiktok.com" + SIGNUP_URL = "https://www.tiktok.com/signup" + LOGIN_URL = "https://www.tiktok.com/login" + + # Anmelden/Registrieren-Buttons Hauptseite + LOGIN_BUTTON_LEFT = "button#header-login-button" + LOGIN_BUTTON_RIGHT = "button#top-right-action-bar-login-button" + SIGNUP_LINK = "a[href*='/signup']" + + # Registrierungsdialog - Methoden + REGISTRATION_DIALOG = "div[role='dialog']" + PHONE_EMAIL_BUTTON = "div[data-e2e='channel-item']" + REGISTER_WITH_EMAIL = "a[href*='/signup/phone-or-email/email']" + REGISTER_WITH_PHONE = "a[href*='/signup/phone-or-email/phone']" + + # Geburtsdatum-Selektoren + BIRTHDAY_MONTH_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Monat')" + BIRTHDAY_DAY_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Tag')" + BIRTHDAY_YEAR_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Jahr')" + BIRTHDAY_DROPDOWN_OPTION = "div[role='option']" + + # Formularfelder - E-Mail-Registrierung + EMAIL_FIELD = "input[placeholder='E-Mail-Adresse']" + PASSWORD_FIELD = "input[placeholder='Passwort']" + VERIFICATION_CODE_FIELD = "input[placeholder*='sechsstelligen Code']" + USERNAME_FIELD = "input[placeholder='Benutzername']" + + # Formularfelder - Telefon-Registrierung + COUNTRY_CODE_SELECT = "div[role='combobox']" + PHONE_FIELD = "input[placeholder='Telefonnummer']" + + # Buttons + SEND_CODE_BUTTON = "button[data-e2e='send-code-button']" + RESEND_CODE_BUTTON = "button:contains('Code erneut senden')" + CONTINUE_BUTTON = "button[type='submit']" + REGISTER_BUTTON = "button:contains('Registrieren')" + SKIP_BUTTON = "button:contains('Überspringen')" + + # Checkbox + NEWSLETTER_CHECKBOX = "input[type='checkbox']" + + # Erfolgs-Indikatoren für Registrierung + SUCCESS_INDICATORS = [ + "a[href='/foryou']", + "a[href='/explore']", + "button[data-e2e='profile-icon']", + "svg[data-e2e='profile-icon']" + ] + + # Links für Nutzungsbedingungen und Datenschutz + TERMS_LINK = "a:contains('Nutzungsbedingungen')" + PRIVACY_LINK = "a:contains('Datenschutzerklärung')" + + # Text-Matching-Parameter für Fuzzy-Matching + TEXT_MATCH = { + # Formularfelder + "form_fields": { + "email": ["E-Mail-Adresse", "E-Mail", "Email", "Mail"], + "phone": ["Telefonnummer", "Telefon", "Phone", "Mobile"], + "password": ["Passwort", "Password"], + "verification_code": ["Bestätigungscode", "Code", "Verifizierungscode", "Sicherheitscode"], + "username": ["Benutzername", "Username", "Name"] + }, + + # Buttons + "buttons": { + "send_code": ["Code senden", "Senden", "Send code", "Verification code", "Send"], + "continue": ["Weiter", "Continue", "Next", "Fortfahren"], + "register": ["Registrieren", "Register", "Sign up", "Konto erstellen"], + "skip": ["Überspringen", "Skip", "Later", "Später", "Nicht jetzt"], + }, + + # Fehler-Indikatoren + "error_indicators": [ + "Fehler", "Error", "Leider", "Ungültig", "Invalid", "Nicht verfügbar", + "Fehlgeschlagen", "Problem", "Failed", "Nicht möglich", "Bereits verwendet", + "Too many attempts", "Zu viele Versuche", "Rate limit", "Bitte warte" + ], + + # Bestätigungscode-Texte in E-Mails + "email_verification_patterns": [ + "ist dein Bestätigungscode", + "ist dein TikTok-Code", + "is your TikTok code", + "is your verification code", + "Dein Bestätigungscode lautet", + "Your verification code is" + ] + } + + @classmethod + def get_field_labels(cls, field_type: str) -> List[str]: + """ + Gibt die möglichen Bezeichnungen für ein Formularfeld zurück. + + Args: + field_type: Typ des Formularfelds (z.B. "email", "phone") + + Returns: + List[str]: Liste mit möglichen Bezeichnungen + """ + return cls.TEXT_MATCH["form_fields"].get(field_type, []) + + @classmethod + def get_button_texts(cls, button_type: str) -> List[str]: + """ + Gibt die möglichen Texte für einen Button zurück. + + Args: + button_type: Typ des Buttons (z.B. "send_code", "continue") + + Returns: + List[str]: Liste mit möglichen Button-Texten + """ + return cls.TEXT_MATCH["buttons"].get(button_type, []) + + @classmethod + def get_error_indicators(cls) -> List[str]: + """ + Gibt die möglichen Texte für Fehlerindikatoren zurück. + + Returns: + List[str]: Liste mit möglichen Fehlerindikator-Texten + """ + return cls.TEXT_MATCH["error_indicators"] + + @classmethod + def get_email_verification_patterns(cls) -> List[str]: + """ + Gibt die möglichen Texte für Bestätigungscodes in E-Mails zurück. + + Returns: + List[str]: Liste mit möglichen E-Mail-Bestätigungscode-Texten + """ + return cls.TEXT_MATCH["email_verification_patterns"] \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_ui_helper.py b/social_networks/tiktok/tiktok_ui_helper.py new file mode 100644 index 0000000..767f16e --- /dev/null +++ b/social_networks/tiktok/tiktok_ui_helper.py @@ -0,0 +1,523 @@ +""" +TikTok-UI-Helper - Hilfsmethoden für die Interaktion mit der TikTok-UI +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple, Union, Callable + +from .tiktok_selectors import TikTokSelectors +from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_ui_helper") + +class TikTokUIHelper: + """ + Hilfsmethoden für die Interaktion mit der TikTok-Benutzeroberfläche. + Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen. + """ + + def __init__(self, automation): + """ + Initialisiert den TikTok-UI-Helper. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + + # Initialisiere TextSimilarity für Fuzzy-Matching + self.text_similarity = TextSimilarity(default_threshold=0.7) + + logger.debug("TikTok-UI-Helper initialisiert") + + def _ensure_browser(self) -> bool: + """ + Stellt sicher, dass die Browser-Referenz verfügbar ist. + + Returns: + bool: True wenn Browser verfügbar, False sonst + """ + if self.browser is None: + self.browser = self.automation.browser + + if self.browser is None: + logger.error("Browser-Referenz nicht verfügbar") + return False + + return True + + def fill_field_fuzzy(self, field_labels: Union[str, List[str]], + value: str, fallback_selector: str = None, + threshold: float = 0.7, timeout: int = 5000) -> bool: + """ + Füllt ein Formularfeld mit Fuzzy-Text-Matching aus. + + Args: + field_labels: Bezeichner oder Liste von Bezeichnern des Feldes + value: Einzugebender Wert + fallback_selector: CSS-Selektor für Fallback + threshold: Schwellenwert für die Textähnlichkeit (0-1) + timeout: Zeitlimit für die Suche in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere field_labels zu einer Liste + if isinstance(field_labels, str): + field_labels = [field_labels] + + # Versuche, das Feld mit Fuzzy-Matching zu finden + element = fuzzy_find_element( + self.browser.page, + field_labels, + selector_type="input", + threshold=threshold, + wait_time=timeout + ) + + if element: + # Versuche, das Feld zu fokussieren und den Wert einzugeben + element.focus() + time.sleep(0.1) + element.fill("") # Leere das Feld zuerst + time.sleep(0.2) + + # Text menschenähnlich eingeben + for char in value: + element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000) + time.sleep(0.01) + + logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}") + return True + + # Fuzzy-Matching fehlgeschlagen, versuche über Attribute + if fallback_selector: + field_success = self.browser.fill_form_field(fallback_selector, value) + if field_success: + logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}") + return True + + # Versuche noch alternative Selektoren basierend auf field_labels + for label in field_labels: + # Versuche aria-label Attribut + aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']" + if self.browser.is_element_visible(aria_selector, timeout=1000): + if self.browser.fill_form_field(aria_selector, value): + logger.info(f"Feld über aria-label gefüllt: {label}") + return True + + # Versuche placeholder Attribut + placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']" + if self.browser.is_element_visible(placeholder_selector, timeout=1000): + if self.browser.fill_form_field(placeholder_selector, value): + logger.info(f"Feld über placeholder gefüllt: {label}") + return True + + # Versuche name Attribut + name_selector = f"input[name='{label.lower().replace(' ', '')}']" + if self.browser.is_element_visible(name_selector, timeout=1000): + if self.browser.fill_form_field(name_selector, value): + logger.info(f"Feld über name-Attribut gefüllt: {label}") + return True + + logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen") + return False + + except Exception as e: + logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {e}") + return False + + def click_button_fuzzy(self, button_texts: Union[str, List[str]], + fallback_selector: str = None, threshold: float = 0.7, + timeout: int = 5000) -> bool: + """ + Klickt einen Button mit Fuzzy-Text-Matching. + + Args: + button_texts: Text oder Liste von Texten des Buttons + fallback_selector: CSS-Selektor für Fallback + threshold: Schwellenwert für die Textähnlichkeit (0-1) + timeout: Zeitlimit für die Suche in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere button_texts zu einer Liste + if isinstance(button_texts, str): + button_texts = [button_texts] + + # Logging der Suche + logger.info(f"Suche nach Button mit Texten: {button_texts}") + + if not button_texts or button_texts == [[]]: + logger.warning("Leere Button-Text-Liste angegeben!") + return False + + # TikTok-spezifische Selektoren zuerst prüfen + # Diese Selektoren sind häufig in TikTok's UI zu finden + tiktok_button_selectors = [ + "button[type='submit']", + "button[data-e2e='send-code-button']", + "button.e1w6iovg0", + "button.css-10nhlj9-Button-StyledButton" + ] + + for selector in tiktok_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + button_element = self.browser.wait_for_selector(selector, timeout=1000) + if button_element: + button_text = button_element.inner_text().strip() + + # Überprüfe, ob der Button-Text mit einem der gesuchten Texte übereinstimmt + for text in button_texts: + if self.text_similarity.is_similar(text, button_text, threshold=threshold): + logger.info(f"Button mit passendem Text gefunden: '{button_text}'") + button_element.click() + return True + + # Die allgemeine fuzzy_click_button-Funktion verwenden + result = click_fuzzy_button( + self.browser.page, + button_texts, + threshold=threshold, + timeout=timeout + ) + + if result: + logger.info(f"Button mit Fuzzy-Matching geklickt") + return True + + # Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector + if fallback_selector: + logger.info(f"Versuche Fallback-Selektor: {fallback_selector}") + if self.browser.click_element(fallback_selector): + logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}") + return True + + # Versuche alternative Methoden + + # 1. Versuche über aria-label + for text in button_texts: + if not text: + continue + + aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']" + if self.browser.is_element_visible(aria_selector, timeout=1000): + if self.browser.click_element(aria_selector): + logger.info(f"Button über aria-label geklickt: {text}") + return True + + # 2. Versuche über role='button' mit Text + for text in button_texts: + if not text: + continue + + xpath_selector = f"//div[@role='button' and contains(., '{text}')]" + if self.browser.is_element_visible(xpath_selector, timeout=1000): + if self.browser.click_element(xpath_selector): + logger.info(f"Button über role+text geklickt: {text}") + return True + + # 3. Versuche über Link-Text + for text in button_texts: + if not text: + continue + + link_selector = f"//a[contains(text(), '{text}')]" + if self.browser.is_element_visible(link_selector, timeout=1000): + if self.browser.click_element(link_selector): + logger.info(f"Link mit passendem Text geklickt: {text}") + return True + + # 4. Als letzten Versuch, klicke auf einen beliebigen Button + logger.warning("Kein spezifischer Button gefunden, versuche beliebigen Button zu klicken") + buttons = self.browser.page.query_selector_all("button") + if buttons and len(buttons) > 0: + for button in buttons: + visible = button.is_visible() + if visible: + logger.info("Klicke auf beliebigen sichtbaren Button") + button.click() + return True + + logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken") + return False + + except Exception as e: + logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}") + return False + + def select_dropdown_option(self, dropdown_selector: str, option_value: str, + option_type: str = "text", timeout: int = 5000) -> bool: + """ + Wählt eine Option aus einer Dropdown-Liste aus. + + Args: + dropdown_selector: Selektor für das Dropdown-Element + option_value: Wert oder Text der auszuwählenden Option + option_type: "text" für Text-Matching, "value" für Wert-Matching + timeout: Zeitlimit in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Auf Dropdown-Element klicken, um die Optionen anzuzeigen + dropdown_element = self.browser.wait_for_selector(dropdown_selector, timeout=timeout) + if not dropdown_element: + logger.warning(f"Dropdown-Element nicht gefunden: {dropdown_selector}") + return False + + # Dropdown öffnen + dropdown_element.click() + time.sleep(0.5) # Kurz warten, damit die Optionen angezeigt werden + + # Optionen suchen + option_selector = "div[role='option']" + options = self.browser.page.query_selector_all(option_selector) + + if not options or len(options) == 0: + logger.warning(f"Keine Optionen gefunden für Dropdown: {dropdown_selector}") + return False + + # Option nach Text oder Wert suchen + selected = False + for option in options: + option_text = option.inner_text().strip() + + if option_type == "text": + if option_text == option_value or self.text_similarity.is_similar(option_text, option_value, threshold=0.9): + option.click() + selected = True + break + elif option_type == "value": + option_val = option.get_attribute("value") or "" + if option_val == option_value: + option.click() + selected = True + break + + if not selected: + logger.warning(f"Keine passende Option für '{option_value}' gefunden") + return False + + logger.info(f"Option '{option_value}' im Dropdown ausgewählt") + return True + + except Exception as e: + logger.error(f"Fehler bei der Auswahl der Dropdown-Option: {e}") + return False + + def check_for_error(self, error_selectors: List[str] = None, + error_texts: List[str] = None) -> Optional[str]: + """ + Überprüft, ob Fehlermeldungen angezeigt werden. + + Args: + error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen + error_texts: Liste mit typischen Fehlertexten + + Returns: + Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden + """ + if not self._ensure_browser(): + return None + + try: + # Standardselektoren verwenden, wenn keine angegeben sind + if error_selectors is None: + error_selectors = [ + "div[role='alert']", + "p[class*='error']", + "span[class*='error']", + ".error-message" + ] + + # Standardfehlertexte verwenden, wenn keine angegeben sind + if error_texts is None: + error_texts = TikTokSelectors.get_error_indicators() + + # 1. Nach Fehlerselektoren suchen + for selector in error_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + error_text = element.text_content() + if error_text and len(error_text.strip()) > 0: + logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}") + return error_text.strip() + + # 2. Alle Texte auf der Seite durchsuchen + page_content = self.browser.page.content() + + for error_text in error_texts: + if error_text.lower() in page_content.lower(): + # Versuche, den genauen Fehlertext zu extrahieren + matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower()) + if matches: + full_error = matches[0].strip() + logger.info(f"Fehlermeldung gefunden (Text): {full_error}") + return full_error + else: + logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}") + return error_text + + # 3. Nach weiteren Fehlerelementen suchen + elements = self.browser.page.query_selector_all("p, div, span") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip() + + # Prüfe Textähnlichkeit mit Fehlertexten + for error_text in error_texts: + if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \ + self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7): + logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}") + return element_text + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}") + return None + + def check_for_captcha(self) -> bool: + """ + Überprüft, ob ein Captcha angezeigt wird. + + Returns: + bool: True wenn Captcha erkannt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Selektoren für Captcha-Erkennung + captcha_selectors = [ + "div[data-testid='captcha']", + "iframe[src*='captcha']", + "iframe[title*='captcha']", + "iframe[title*='reCAPTCHA']" + ] + + # Captcha-Texte für textbasierte Erkennung + captcha_texts = [ + "captcha", "recaptcha", "sicherheitsüberprüfung", "security check", + "i'm not a robot", "ich bin kein roboter", "verify you're human", + "bestätige, dass du ein mensch bist" + ] + + # Nach Selektoren suchen + for selector in captcha_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.warning(f"Captcha erkannt (Selektor): {selector}") + return True + + # Nach Texten suchen + page_content = self.browser.page.content().lower() + + for text in captcha_texts: + if text in page_content: + logger.warning(f"Captcha erkannt (Text): {text}") + return True + + return False + + except Exception as e: + logger.error(f"Fehler bei der Captcha-Erkennung: {e}") + return False + + def wait_for_element(self, selectors: Union[str, List[str]], + timeout: int = 10000, check_interval: int = 500) -> Optional[Any]: + """ + Wartet auf das Erscheinen eines Elements. + + Args: + selectors: CSS-Selektor oder Liste von Selektoren + timeout: Zeitlimit in Millisekunden + check_interval: Intervall zwischen den Prüfungen in Millisekunden + + Returns: + Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist + """ + if not self._ensure_browser(): + return None + + try: + # Normalisiere selectors zu einer Liste + if isinstance(selectors, str): + selectors = [selectors] + + start_time = time.time() + end_time = start_time + (timeout / 1000) + + while time.time() < end_time: + for selector in selectors: + element = self.browser.wait_for_selector(selector, timeout=check_interval) + if element: + logger.info(f"Element mit Selektor '{selector}' gefunden") + return element + + # Kurze Pause vor der nächsten Prüfung + time.sleep(check_interval / 1000) + + logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'") + return None + + except Exception as e: + logger.error(f"Fehler beim Warten auf Element: {e}") + return None + + def is_registration_successful(self) -> bool: + """ + Überprüft, ob die Registrierung erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Erfolgsindikatoren überprüfen + success_indicators = TikTokSelectors.SUCCESS_INDICATORS + + for selector in success_indicators: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Registrierung erfolgreich (Indikator gefunden: {selector})") + return True + + # URL überprüfen + current_url = self.browser.page.url + if "/foryou" in current_url or "tiktok.com/explore" in current_url: + logger.info("Registrierung erfolgreich (Erfolgreiche Navigation erkannt)") + return True + + # Überprüfen, ob Fehler angezeigt werden + error_message = self.check_for_error() + if error_message: + logger.warning(f"Registrierung nicht erfolgreich: {error_message}") + return False + + logger.warning("Konnte Registrierungserfolg nicht bestätigen") + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Registrierungserfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_utils.py b/social_networks/tiktok/tiktok_utils.py new file mode 100644 index 0000000..2e2b3b6 --- /dev/null +++ b/social_networks/tiktok/tiktok_utils.py @@ -0,0 +1,495 @@ +""" +TikTok-Utils - Hilfsfunktionen für die TikTok-Automatisierung. +""" + +import logging +import re +import time +import random +from typing import Dict, List, Any, Optional, Tuple, Union + +from .tiktok_selectors import TikTokSelectors + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_utils") + +class TikTokUtils: + """ + Hilfsfunktionen für die TikTok-Automatisierung. + Enthält allgemeine Hilfsmethoden und kleinere Funktionen. + """ + + def __init__(self, automation): + """ + Initialisiert die TikTok-Utils. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + + logger.debug("TikTok-Utils initialisiert") + + def _ensure_browser(self) -> bool: + """ + Stellt sicher, dass die Browser-Referenz verfügbar ist. + + Returns: + bool: True wenn Browser verfügbar, False sonst + """ + if self.browser is None: + self.browser = self.automation.browser + + if self.browser is None: + logger.error("Browser-Referenz nicht verfügbar") + return False + + return True + + def handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Cookie-Dialoge in TikTok prüfen + cookie_selectors = [ + "button[data-e2e='cookie-banner-reject']", + "button:contains('Ablehnen')", + "button:contains('Nur erforderliche')", + "button:contains('Reject')", + "button[data-e2e='cookie-banner-accept']" + ] + + for selector in cookie_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Cookie-Banner erkannt: {selector}") + + # Versuche, den Ablehnen-Button zu klicken + if "reject" in selector.lower() or "ablehnen" in selector.lower() or "erforderliche" in selector.lower(): + if self.browser.click_element(selector): + logger.info("Cookie-Banner erfolgreich abgelehnt") + time.sleep(random.uniform(0.5, 1.5)) + return True + + # Fallback: Akzeptieren-Button klicken, wenn Ablehnen nicht funktioniert + else: + if self.browser.click_element(selector): + logger.info("Cookie-Banner erfolgreich akzeptiert") + time.sleep(random.uniform(0.5, 1.5)) + return True + + # Wenn kein Cookie-Banner gefunden wurde + logger.debug("Kein Cookie-Banner erkannt") + return True + + except Exception as e: + logger.error(f"Fehler beim Behandeln des Cookie-Banners: {e}") + return False + + def extract_username_from_url(self, url: str) -> Optional[str]: + """ + Extrahiert den Benutzernamen aus einer TikTok-URL. + + Args: + url: Die TikTok-URL + + Returns: + Optional[str]: Der extrahierte Benutzername oder None + """ + try: + # Muster für Profil-URLs + patterns = [ + r'tiktok\.com/@([a-zA-Z0-9._]+)/?(?:$|\?|#)', + r'tiktok\.com/user/([a-zA-Z0-9._]+)/?', + r'tiktok\.com/video/[^/]+/by/([a-zA-Z0-9._]+)/?' + ] + + for pattern in patterns: + match = re.search(pattern, url) + if match: + username = match.group(1) + # Einige Ausnahmen filtern + if username not in ["explore", "accounts", "video", "foryou", "trending"]: + return username + + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Benutzernamens aus der URL: {e}") + return None + + def get_current_username(self) -> Optional[str]: + """ + Versucht, den Benutzernamen des aktuell angemeldeten Kontos zu ermitteln. + + Returns: + Optional[str]: Der Benutzername oder None, wenn nicht gefunden + """ + if not self._ensure_browser(): + return None + + try: + # Verschiedene Methoden zur Erkennung des Benutzernamens + + # 1. Benutzername aus URL des Profils + profile_link_selectors = [ + "a[href*='/@']", + "a[href*='/user/']" + ] + + for selector in profile_link_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + href = element.get_attribute("href") + if href: + username = self.extract_username_from_url(href) + if username: + logger.info(f"Benutzername aus Profil-Link ermittelt: {username}") + return username + + # 2. Profilicon prüfen auf data-e2e-Attribut + profile_icon_selectors = [ + "button[data-e2e='profile-icon']", + "svg[data-e2e='profile-icon']" + ] + + for selector in profile_icon_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + # Prüfen, ob ein Elternelement möglicherweise ein data-e2e-Attribut mit dem Benutzernamen hat + parent = element.evaluate("node => node.parentElement") + if parent: + data_e2e = parent.get_attribute("data-e2e") + if data_e2e and "profile" in data_e2e: + username_match = re.search(r'profile-([a-zA-Z0-9._]+)', data_e2e) + if username_match: + username = username_match.group(1) + logger.info(f"Benutzername aus data-e2e-Attribut ermittelt: {username}") + return username + + # 3. TikTok-spezifisches Element mit Benutzername suchen + username_element = self.browser.wait_for_selector("h1[data-e2e='user-title']", timeout=2000) + if username_element: + username = username_element.inner_text().strip() + if username: + logger.info(f"Benutzername aus user-title-Element ermittelt: {username}") + return username + + logger.warning("Konnte Benutzernamen nicht ermitteln") + return None + + except Exception as e: + logger.error(f"Fehler bei der Ermittlung des Benutzernamens: {e}") + return None + + def wait_for_navigation(self, expected_url_pattern: str = None, + timeout: int = 30000, check_interval: int = 500) -> bool: + """ + Wartet, bis die Seite zu einer URL mit einem bestimmten Muster navigiert. + + Args: + expected_url_pattern: Erwartetes Muster der URL (Regex) + timeout: Zeitlimit in Millisekunden + check_interval: Intervall zwischen den Prüfungen in Millisekunden + + Returns: + bool: True wenn die Navigation erfolgreich war, False sonst + """ + if not self._ensure_browser(): + return False + + try: + start_time = time.time() + end_time = start_time + (timeout / 1000) + + while time.time() < end_time: + current_url = self.browser.page.url + + if expected_url_pattern and re.search(expected_url_pattern, current_url): + logger.info(f"Navigation zu URL mit Muster '{expected_url_pattern}' erfolgreich") + return True + + # Kurze Pause vor der nächsten Prüfung + time.sleep(check_interval / 1000) + + logger.warning(f"Zeitüberschreitung bei Navigation zu URL mit Muster '{expected_url_pattern}'") + return False + + except Exception as e: + logger.error(f"Fehler beim Warten auf Navigation: {e}") + return False + + def handle_dialog_or_popup(self, expected_text: Union[str, List[str]] = None, + action: str = "close", timeout: int = 5000) -> bool: + """ + Behandelt einen Dialog oder Popup. + + Args: + expected_text: Erwarteter Text im Dialog oder Liste von Texten + action: Aktion ("close", "confirm", "cancel") + timeout: Zeitlimit in Millisekunden + + Returns: + bool: True wenn der Dialog erfolgreich behandelt wurde, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Dialog-Element suchen + dialog_selector = "div[role='dialog']" + dialog_element = self.browser.wait_for_selector(dialog_selector, timeout=timeout) + + if not dialog_element: + logger.debug("Kein Dialog gefunden") + return False + + logger.info("Dialog gefunden") + + # Text im Dialog prüfen, falls angegeben + if expected_text: + if isinstance(expected_text, str): + expected_text = [expected_text] + + dialog_text = dialog_element.inner_text() + text_found = False + + for text in expected_text: + if text in dialog_text: + logger.info(f"Erwarteter Text im Dialog gefunden: '{text}'") + text_found = True + break + + if not text_found: + logger.warning(f"Erwarteter Text nicht im Dialog gefunden: {expected_text}") + return False + + # Aktion ausführen + if action == "close": + # Schließen-Button suchen und klicken + close_button_selectors = [ + "button[data-e2e='modal-close']", + "svg[data-e2e='modal-close']", + "button.css-1afoydx-StyledCloseButton", + "div[role='dialog'] button:first-child" + ] + + for selector in close_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info("Dialog geschlossen") + return True + + # Wenn kein Schließen-Button gefunden wurde, Escape-Taste drücken + self.browser.page.keyboard.press("Escape") + logger.info("Dialog mit Escape-Taste geschlossen") + + elif action == "confirm": + # Bestätigen-Button suchen und klicken + confirm_button_selectors = [ + "button[type='submit']", + "button:contains('OK')", + "button:contains('Ja')", + "button:contains('Yes')", + "button:contains('Bestätigen')", + "button:contains('Confirm')" + ] + + for selector in confirm_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info("Dialog bestätigt") + return True + + elif action == "cancel": + # Abbrechen-Button suchen und klicken + cancel_button_selectors = [ + "button:contains('Abbrechen')", + "button:contains('Cancel')", + "button:contains('Nein')", + "button:contains('No')" + ] + + for selector in cancel_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info("Dialog abgebrochen") + return True + + logger.warning(f"Konnte keine {action}-Aktion für den Dialog ausführen") + return False + + except Exception as e: + logger.error(f"Fehler bei der Dialog-Behandlung: {e}") + return False + + def handle_rate_limiting(self, rotate_proxy: bool = True) -> bool: + """ + Behandelt eine Rate-Limiting-Situation. + + Args: + rotate_proxy: Ob der Proxy rotiert werden soll + + Returns: + bool: True wenn erfolgreich behandelt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + logger.warning("Rate-Limiting erkannt, warte und versuche es erneut") + + # Screenshot erstellen + self.automation._take_screenshot("rate_limit_detected") + + # Proxy rotieren, falls gewünscht + if rotate_proxy and self.automation.use_proxy: + success = self.automation._rotate_proxy() + if not success: + logger.warning("Konnte Proxy nicht rotieren") + + # Längere Wartezeit + wait_time = random.uniform(120, 300) # 2-5 Minuten + logger.info(f"Warte {wait_time:.1f} Sekunden vor dem nächsten Versuch") + time.sleep(wait_time) + + # Seite neuladen + self.browser.page.reload() + self.automation.human_behavior.wait_for_page_load() + + # Prüfen, ob Rate-Limiting noch aktiv ist + rate_limit_texts = [ + "bitte warte einige minuten", + "please wait a few minutes", + "try again later", + "versuche es später erneut", + "zu viele anfragen", + "too many requests" + ] + + page_content = self.browser.page.content().lower() + + still_rate_limited = False + for text in rate_limit_texts: + if text in page_content: + still_rate_limited = True + break + + if still_rate_limited: + logger.warning("Immer noch Rate-Limited nach dem Warten") + return False + else: + logger.info("Rate-Limiting scheint aufgehoben zu sein") + return True + + except Exception as e: + logger.error(f"Fehler bei der Behandlung des Rate-Limitings: {e}") + return False + + def is_logged_in(self) -> bool: + """ + Überprüft, ob der Benutzer bei TikTok angemeldet ist. + + Returns: + bool: True wenn angemeldet, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Erfolgsindikatoren überprüfen + success_indicators = TikTokSelectors.SUCCESS_INDICATORS + + for selector in success_indicators: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Benutzer ist angemeldet (Indikator: {selector})") + return True + + # URL überprüfen + current_url = self.browser.page.url + if "/foryou" in current_url or "tiktok.com/explore" in current_url: + logger.info("Benutzer ist angemeldet (URL-Check)") + return True + + # Anmelden-Button prüfen - wenn sichtbar, dann nicht angemeldet + login_button_selectors = [ + TikTokSelectors.LOGIN_BUTTON_LEFT, + TikTokSelectors.LOGIN_BUTTON_RIGHT + ] + + for selector in login_button_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Benutzer ist nicht angemeldet (Anmelde-Button sichtbar)") + return False + + # Profilicon checken - wenn sichtbar, dann angemeldet + profile_selectors = [ + "button[data-e2e='profile-icon']", + "svg[data-e2e='profile-icon']" + ] + + for selector in profile_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Benutzer ist angemeldet (Profilicon sichtbar)") + return True + + logger.warning("Konnte Login-Status nicht eindeutig bestimmen") + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Login-Status: {e}") + return False + + def extract_verification_code_from_email(self, email_body: str) -> Optional[str]: + """ + Extrahiert den Verifizierungscode aus einer E-Mail. + + Args: + email_body: Der E-Mail-Text + + Returns: + Optional[str]: Der Verifizierungscode oder None, wenn nicht gefunden + """ + try: + # Muster für TikTok-Verifizierungscodes + patterns = [ + r'(\d{6}) ist dein Bestätigungscode', + r'(\d{6}) ist dein TikTok-Code', + r'(\d{6}) is your TikTok code', + r'(\d{6}) is your verification code', + r'Dein Bestätigungscode lautet (\d{6})', + r'Your verification code is (\d{6})', + r'Verification code: (\d{6})', + r'Bestätigungscode: (\d{6})', + r'TikTok code: (\d{6})', + r'TikTok-Code: (\d{6})' + ] + + for pattern in patterns: + match = re.search(pattern, email_body) + if match: + code = match.group(1) + logger.info(f"Verifizierungscode aus E-Mail extrahiert: {code}") + return code + + # Allgemeine Suche nach 6-stelligen Zahlen, wenn keine spezifischen Muster passen + general_match = re.search(r'[^\d](\d{6})[^\d]', email_body) + if general_match: + code = general_match.group(1) + logger.info(f"6-stelliger Code aus E-Mail extrahiert: {code}") + return code + + logger.warning("Kein Verifizierungscode in der E-Mail gefunden") + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Verifizierungscodes aus der E-Mail: {e}") + return None \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_verification.py b/social_networks/tiktok/tiktok_verification.py new file mode 100644 index 0000000..c3519b5 --- /dev/null +++ b/social_networks/tiktok/tiktok_verification.py @@ -0,0 +1,457 @@ +# social_networks/tiktok/tiktok_verification.py + +""" +TikTok-Verifizierung - Klasse für die Verifizierungsfunktionalität bei TikTok +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple + +from .tiktok_selectors import TikTokSelectors +from .tiktok_workflow import TikTokWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_verification") + +class TikTokVerification: + """ + Klasse für die Verifizierung von TikTok-Konten. + Enthält alle Methoden für den Verifizierungsprozess. + """ + + def __init__(self, automation): + """ + Initialisiert die TikTok-Verifizierung. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + self.workflow = TikTokWorkflow.get_verification_workflow() + + logger.debug("TikTok-Verifizierung initialisiert") + + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Führt den Verifizierungsprozess für ein TikTok-Konto durch. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere den Verifizierungscode + if not self._validate_verification_code(verification_code): + return { + "success": False, + "error": "Ungültiger Verifizierungscode", + "stage": "code_validation" + } + + try: + # 1. Überprüfen, ob wir auf der Verifizierungsseite sind + if not self._is_on_verification_page(): + # Versuche, zur Verifizierungsseite zu navigieren, falls möglich + # Direktnavigation ist jedoch normalerweise nicht möglich + return { + "success": False, + "error": "Nicht auf der Verifizierungsseite", + "stage": "page_check" + } + + # 2. Verifizierungscode eingeben und absenden + if not self.enter_and_submit_verification_code(verification_code): + return { + "success": False, + "error": "Fehler beim Eingeben oder Absenden des Verifizierungscodes", + "stage": "code_entry" + } + + # 3. Überprüfen, ob die Verifizierung erfolgreich war + success, error_message = self._check_verification_success() + + if not success: + return { + "success": False, + "error": f"Verifizierung fehlgeschlagen: {error_message or 'Unbekannter Fehler'}", + "stage": "verification_check" + } + + # 4. Zusätzliche Dialoge behandeln + self._handle_post_verification_dialogs() + + # Verifizierung erfolgreich + logger.info("TikTok-Verifizierung erfolgreich abgeschlossen") + + return { + "success": True, + "stage": "completed" + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der TikTok-Verifizierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception" + } + + def _validate_verification_code(self, verification_code: str) -> bool: + """ + Validiert den Verifizierungscode. + + Args: + verification_code: Der zu validierende Code + + Returns: + bool: True wenn der Code gültig ist, False sonst + """ + # Leerer Code + if not verification_code: + logger.error("Verifizierungscode ist leer") + return False + + # Code-Format prüfen (normalerweise 6-stellige Zahl) + if not re.match(r"^\d{6}$", verification_code): + logger.warning(f"Verifizierungscode hat unerwartetes Format: {verification_code}") + # Wir geben trotzdem True zurück, da einige Codes andere Formate haben könnten + return True + + return True + + def _is_on_verification_page(self) -> bool: + """ + Überprüft, ob wir auf der Verifizierungsseite sind. + + Returns: + bool: True wenn auf der Verifizierungsseite, False sonst + """ + try: + # Screenshot erstellen + self.automation._take_screenshot("verification_page_check") + + # Nach Verifizierungsfeld suchen + verification_selectors = [ + TikTokSelectors.VERIFICATION_CODE_FIELD, + "input[placeholder*='Code']", + "input[placeholder*='code']", + "input[placeholder*='sechsstelligen']", + "input[data-e2e='verification-code-input']" + ] + + for selector in verification_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + logger.info("Auf Verifizierungsseite") + return True + + # Textbasierte Erkennung + verification_texts = [ + "Bestätigungscode", + "Verification code", + "sechsstelligen Code", + "6-digit code", + "Code senden", + "Send code" + ] + + page_content = self.browser.page.content().lower() + + for text in verification_texts: + if text.lower() in page_content: + logger.info(f"Auf Verifizierungsseite (erkannt durch Text: {text})") + return True + + logger.warning("Nicht auf der Verifizierungsseite") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen der Verifizierungsseite: {e}") + return False + + def enter_and_submit_verification_code(self, verification_code: str) -> bool: + """ + Gibt den Verifizierungscode ein und sendet ihn ab. + + Args: + verification_code: Der einzugebende Verifizierungscode + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + logger.info(f"Versuche Verifizierungscode einzugeben: {verification_code}") + + # Mögliche Selektoren für das Verifizierungscode-Feld + code_field_selectors = [ + TikTokSelectors.VERIFICATION_CODE_FIELD, + "input[placeholder*='Code']", + "input[placeholder*='sechsstelligen Code']", + "input[data-e2e='verification-code-input']" + ] + + # Versuche, das Feld zu finden und auszufüllen + code_field_found = False + + for selector in code_field_selectors: + logger.debug(f"Versuche Selektor: {selector}") + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Codefeld gefunden mit Selektor: {selector}") + if self.browser.fill_form_field(selector, verification_code): + code_field_found = True + logger.info(f"Verifizierungscode eingegeben: {verification_code}") + break + + # Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen + if not code_field_found: + logger.info("Versuche Fuzzy-Matching für Codefeld") + code_field_found = self.automation.ui_helper.fill_field_fuzzy( + ["Bestätigungscode", "Code eingeben", "Verification code", "6-digit code"], + verification_code + ) + + if not code_field_found: + logger.error("Konnte Verifizierungscode-Feld nicht finden oder ausfüllen") + + # Erstelle einen Screenshot zum Debuggen + self.automation._take_screenshot("code_field_not_found") + return False + + # Menschliche Verzögerung vor dem Absenden + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot erstellen + self.automation._take_screenshot("verification_code_entered") + + # "Weiter"-Button finden und klicken + weiter_button_selectors = [ + TikTokSelectors.WEITER_BUTTON, + "button[type='submit']", + "button.e1w6iovg0", + "button[data-e2e='next-button']", + "//button[contains(text(), 'Weiter')]" + ] + + weiter_button_found = False + + logger.info("Suche nach Weiter-Button") + for selector in weiter_button_selectors: + logger.debug(f"Versuche Weiter-Button-Selektor: {selector}") + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Weiter-Button gefunden mit Selektor: {selector}") + if self.browser.click_element(selector): + weiter_button_found = True + logger.info("Verifizierungscode-Formular abgesendet") + break + + # Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen + if not weiter_button_found: + logger.info("Versuche Fuzzy-Matching für Weiter-Button") + weiter_buttons = ["Weiter", "Next", "Continue", "Fertig", "Submit", "Verify", "Senden"] + weiter_button_found = self.automation.ui_helper.click_button_fuzzy( + weiter_buttons + ) + + if not weiter_button_found: + # Erstelle einen Screenshot zum Debuggen + self.automation._take_screenshot("weiter_button_not_found") + + # Versuche es mit Enter-Taste als letzten Ausweg + logger.info("Konnte Weiter-Button nicht finden, versuche Enter-Taste") + self.browser.page.keyboard.press("Enter") + logger.info("Enter-Taste zur Bestätigung des Verifizierungscodes gedrückt") + weiter_button_found = True + + # Warten nach dem Absenden + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + return weiter_button_found + + except Exception as e: + logger.error(f"Fehler beim Eingeben und Absenden des Verifizierungscodes: {e}") + return False + + def _check_verification_success(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob die Verifizierung erfolgreich war. + + Returns: + Tuple[bool, Optional[str]]: (Erfolg, Fehlermeldung falls vorhanden) + """ + try: + # Warten nach der Verifizierung + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Screenshot erstellen + self.automation._take_screenshot("verification_result") + + # Immer noch auf der Verifizierungsseite? + still_on_verification = self._is_on_verification_page() + + if still_on_verification: + # Fehlermeldung suchen + error_message = self.automation.ui_helper.check_for_error() + + if error_message: + logger.error(f"Verifizierungsfehler: {error_message}") + return False, error_message + else: + logger.error("Verifizierung fehlgeschlagen, immer noch auf der Verifizierungsseite") + return False, "Immer noch auf der Verifizierungsseite" + + # Prüfe, ob wir zur Benutzernamen-Erstellung weitergeleitet wurden + username_selectors = [ + "input[placeholder='Benutzername']", + "input[name='new-username']", + "//input[@placeholder='Benutzername']" + ] + + for selector in username_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Verifizierung erfolgreich, zur Benutzernamenauswahl weitergeleitet") + return True, None + + # Prüfe auf TikTok-Startseite + current_url = self.browser.page.url + if "tiktok.com" in current_url and "/login" not in current_url and "/signup" not in current_url: + logger.info("Verifizierung erfolgreich, jetzt auf der Startseite") + return True, None + + # Wenn keine eindeutigen Indikatoren gefunden wurden, aber auch keine Fehler + logger.warning("Keine eindeutigen Erfolgsindikatoren für die Verifizierung gefunden") + return True, None # Wir gehen davon aus, dass es erfolgreich war + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Verifizierungserfolgs: {e}") + return False, f"Fehler bei der Erfolgsprüfung: {str(e)}" + + def _handle_post_verification_dialogs(self) -> None: + """ + Behandelt Dialoge, die nach erfolgreicher Verifizierung erscheinen können. + """ + try: + # Liste der möglichen Dialoge und wie man sie überspringt + dialogs_to_handle = [ + { + "name": "username_setup", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "interests", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "follow_accounts", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "notifications", + "skip_texts": ["Später", "Nein", "Nicht jetzt", "Later", "No", "Not now"], + "skip_selectors": ["//button[contains(text(), 'Später')]", "//button[contains(text(), 'Not now')]"] + } + ] + + # Versuche, jeden möglichen Dialog zu behandeln + for dialog in dialogs_to_handle: + self._try_skip_dialog(dialog) + + logger.info("Nachverifizierungs-Dialoge behandelt") + + except Exception as e: + logger.warning(f"Fehler beim Behandeln der Nachverifizierungs-Dialoge: {e}") + # Nicht kritisch, daher keine Fehlerbehandlung + + def _try_skip_dialog(self, dialog: Dict[str, Any]) -> bool: + """ + Versucht, einen bestimmten Dialog zu überspringen. + + Args: + dialog: Informationen zum Dialog + + Returns: + bool: True wenn Dialog gefunden und übersprungen, False sonst + """ + try: + # Zuerst mit Fuzzy-Matching versuchen + skip_clicked = self.automation.ui_helper.click_button_fuzzy( + dialog["skip_texts"], + threshold=0.7, + timeout=3000 + ) + + if skip_clicked: + logger.info(f"Dialog '{dialog['name']}' mit Fuzzy-Matching übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn Fuzzy-Matching fehlschlägt, direkte Selektoren versuchen + for selector in dialog["skip_selectors"]: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info(f"Dialog '{dialog['name']}' mit direktem Selektor übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + return False + + except Exception as e: + logger.warning(f"Fehler beim Versuch, Dialog '{dialog['name']}' zu überspringen: {e}") + return False + + def resend_verification_code(self) -> bool: + """ + Versucht, den Verifizierungscode erneut senden zu lassen. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Resend-Button suchen und klicken + resend_selectors = [ + "button[data-e2e='send-code-button']", + "//button[contains(text(), 'Code senden')]", + "//button[contains(text(), 'Code erneut senden')]", + "//button[contains(text(), 'Erneut senden')]", + "a[data-e2e='resend-code-link']" + ] + + for selector in resend_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + if self.browser.click_element(selector): + logger.info("Code erneut angefordert") + self.automation.human_behavior.random_delay(1.0, 2.0) + return True + + # Fuzzy-Matching versuchen + resend_texts = ["Code senden", "Code erneut senden", "Erneut senden", "Resend code", "Send again"] + + resend_clicked = self.automation.ui_helper.click_button_fuzzy( + resend_texts, + threshold=0.7, + timeout=3000 + ) + + if resend_clicked: + logger.info("Code erneut angefordert (über Fuzzy-Matching)") + self.automation.human_behavior.random_delay(1.0, 2.0) + return True + + logger.warning("Konnte keinen 'Code erneut senden'-Button finden") + return False + + except Exception as e: + logger.error(f"Fehler beim erneuten Anfordern des Codes: {e}") + return False \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_workflow.py b/social_networks/tiktok/tiktok_workflow.py new file mode 100644 index 0000000..37a62a0 --- /dev/null +++ b/social_networks/tiktok/tiktok_workflow.py @@ -0,0 +1,427 @@ +""" +TikTok-Workflow - Definiert die Schritte für die TikTok-Anmeldung und -Registrierung +""" + +import logging +from typing import Dict, List, Any, Optional, Tuple +import re + +from utils.text_similarity import TextSimilarity + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_workflow") + +class TikTokWorkflow: + """ + Definiert die Workflow-Schritte für verschiedene TikTok-Aktionen + wie Registrierung, Anmeldung und Verifizierung. + """ + + # Text-Ähnlichkeits-Threshold für Fuzzy-Matching + SIMILARITY_THRESHOLD = 0.7 + + # Initialisiere TextSimilarity für Matching + text_similarity = TextSimilarity(default_threshold=SIMILARITY_THRESHOLD) + + # Mögliche alternative Texte für verschiedene UI-Elemente + TEXT_ALTERNATIVES = { + "email": ["E-Mail", "Email", "E-mail", "Mail", "email"], + "phone": ["Telefon", "Telefonnummer", "Phone", "Mobile", "mobile"], + "password": ["Passwort", "Password", "pass"], + "code": ["Code", "Bestätigungscode", "Verification code", "Sicherheitscode"], + "username": ["Benutzername", "Username", "user name"], + "submit": ["Registrieren", "Sign up", "Anmelden", "Login", "Log in", "Submit"], + "next": ["Weiter", "Next", "Continue", "Fortfahren"], + "skip": ["Überspringen", "Skip", "Later", "Später", "Not now", "Nicht jetzt"] + } + + @staticmethod + def get_registration_workflow(registration_method: str = "email") -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die TikTok-Registrierung zurück. + + Args: + registration_method: "email" oder "phone" + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + # Basisschritte für beide Methoden + common_steps = [ + { + "name": "navigate_to_signup", + "description": "Zur TikTok-Startseite navigieren", + "url": "https://www.tiktok.com", + "wait_for": ["button#header-login-button", "button#top-right-action-bar-login-button"], + "fuzzy_match": None + }, + { + "name": "click_login_button", + "description": "Anmelden-Button klicken", + "action": "click", + "target": "button#top-right-action-bar-login-button", + "wait_for": ["a[href*='/signup']", "div[role='dialog']"], + "fuzzy_match": ["Anmelden", "Sign in", "Log in"] + }, + { + "name": "click_register_link", + "description": "Registrieren-Link klicken", + "action": "click", + "target": "a[href*='/signup']", + "wait_for": ["div[data-e2e='channel-item']"], + "fuzzy_match": ["Registrieren", "Sign up", "Register"] + }, + { + "name": "click_phone_email_button", + "description": "Telefon/E-Mail-Option auswählen", + "action": "click", + "target": "div[data-e2e='channel-item']", + "wait_for": ["a[href*='/signup/phone-or-email/email']", "a[href*='/signup/phone-or-email/phone']"], + "fuzzy_match": ["Telefonnummer oder E-Mail-Adresse", "Phone or Email"] + } + ] + + # Spezifische Schritte je nach Registrierungsmethode + method_steps = [] + if registration_method == "email": + method_steps.append({ + "name": "click_email_registration", + "description": "Mit E-Mail registrieren auswählen", + "action": "click", + "target": "a[href*='/signup/phone-or-email/email']", + "wait_for": ["input[placeholder='E-Mail-Adresse']"], + "fuzzy_match": ["Mit E-Mail-Adresse registrieren", "Email", "E-Mail"] + }) + else: # phone + method_steps.append({ + "name": "click_phone_registration", + "description": "Mit Telefonnummer registrieren auswählen", + "action": "click", + "target": "a[href*='/signup/phone-or-email/phone']", + "wait_for": ["input[placeholder='Telefonnummer']"], + "fuzzy_match": ["Mit Telefonnummer registrieren", "Phone", "Telefon"] + }) + + # Geburtsdatum-Schritte + birthday_steps = [ + { + "name": "select_birth_month", + "description": "Geburtsmonat auswählen", + "action": "click", + "target": "div.css-1fi2hzv-DivSelectLabel:contains('Monat')", + "wait_for": ["div[role='option']"], + "fuzzy_match": ["Monat", "Month"] + }, + { + "name": "select_month_option", + "description": "Monats-Option auswählen", + "action": "select_option", + "target": "div[role='option']", + "value": "{MONTH_NAME}", + "wait_for": [], + "fuzzy_match": None + }, + { + "name": "select_birth_day", + "description": "Geburtstag auswählen", + "action": "click", + "target": "div.css-1fi2hzv-DivSelectLabel:contains('Tag')", + "wait_for": ["div[role='option']"], + "fuzzy_match": ["Tag", "Day"] + }, + { + "name": "select_day_option", + "description": "Tags-Option auswählen", + "action": "select_option", + "target": "div[role='option']", + "value": "{DAY}", + "wait_for": [], + "fuzzy_match": None + }, + { + "name": "select_birth_year", + "description": "Geburtsjahr auswählen", + "action": "click", + "target": "div.css-1fi2hzv-DivSelectLabel:contains('Jahr')", + "wait_for": ["div[role='option']"], + "fuzzy_match": ["Jahr", "Year"] + }, + { + "name": "select_year_option", + "description": "Jahres-Option auswählen", + "action": "select_option", + "target": "div[role='option']", + "value": "{YEAR}", + "wait_for": [], + "fuzzy_match": None + } + ] + + # Formularschritte für E-Mail + email_form_steps = [ + { + "name": "fill_email", + "description": "E-Mail-Adresse eingeben", + "action": "fill", + "target": "input[placeholder='E-Mail-Adresse']", + "value": "{EMAIL}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["email"] + }, + { + "name": "fill_password", + "description": "Passwort eingeben", + "action": "fill", + "target": "input[placeholder='Passwort']", + "value": "{PASSWORD}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["password"] + }, + { + "name": "click_send_code", + "description": "Code senden klicken", + "action": "click", + "target": "button[data-e2e='send-code-button']", + "wait_for": ["input[placeholder*='sechsstelligen Code']"], + "fuzzy_match": ["Code senden", "Send code", "Senden"] + }, + { + "name": "fill_verification_code", + "description": "Bestätigungscode eingeben", + "action": "fill", + "target": "input[placeholder*='sechsstelligen Code']", + "value": "{VERIFICATION_CODE}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"] + }, + { + "name": "click_continue", + "description": "Weiter klicken", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["input[placeholder='Benutzername']"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"] + } + ] + + # Formularschritte für Telefon + phone_form_steps = [ + { + "name": "select_country_code", + "description": "Ländervorwahl auswählen", + "action": "click", + "target": "div[role='combobox']", + "wait_for": ["div[role='option']"], + "fuzzy_match": None + }, + { + "name": "select_country_option", + "description": "Land auswählen", + "action": "select_option", + "target": "div[role='option']", + "value": "{COUNTRY_NAME}", + "wait_for": [], + "fuzzy_match": None + }, + { + "name": "fill_phone", + "description": "Telefonnummer eingeben", + "action": "fill", + "target": "input[placeholder='Telefonnummer']", + "value": "{PHONE}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["phone"] + }, + { + "name": "click_send_code", + "description": "Code senden klicken", + "action": "click", + "target": "button[data-e2e='send-code-button']", + "wait_for": ["input[placeholder*='sechsstelligen Code']"], + "fuzzy_match": ["Code senden", "Send code", "Senden"] + }, + { + "name": "fill_verification_code", + "description": "Bestätigungscode eingeben", + "action": "fill", + "target": "input[placeholder*='sechsstelligen Code']", + "value": "{VERIFICATION_CODE}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"] + }, + { + "name": "click_continue", + "description": "Weiter klicken", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["input[placeholder='Benutzername']"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"] + } + ] + + # Benutzername-Schritte + username_steps = [ + { + "name": "fill_username", + "description": "Benutzernamen eingeben", + "action": "fill", + "target": "input[placeholder='Benutzername']", + "value": "{USERNAME}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["username"] + }, + { + "name": "click_register", + "description": "Registrieren klicken", + "action": "click", + "target": "button:contains('Registrieren')", + "wait_for": ["a[href='/foryou']", "button:contains('Überspringen')"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["submit"] + }, + { + "name": "handle_skip_option", + "description": "Optional: Überspringen klicken", + "action": "click", + "target": "button:contains('Überspringen')", + "optional": True, + "wait_for": ["a[href='/foryou']"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["skip"] + } + ] + + # Vollständigen Workflow zusammenstellen + if registration_method == "email": + workflow = common_steps + method_steps + birthday_steps + email_form_steps + username_steps + else: # phone + workflow = common_steps + method_steps + birthday_steps + phone_form_steps + username_steps + + return workflow + + @staticmethod + def get_login_workflow() -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die TikTok-Anmeldung zurück. + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + login_steps = [ + { + "name": "navigate_to_login", + "description": "Zur TikTok-Startseite navigieren", + "url": "https://www.tiktok.com", + "wait_for": ["button#header-login-button", "button#top-right-action-bar-login-button"], + "fuzzy_match": None + }, + { + "name": "click_login_button", + "description": "Anmelden-Button klicken", + "action": "click", + "target": "button#top-right-action-bar-login-button", + "wait_for": ["div[role='dialog']"], + "fuzzy_match": ["Anmelden", "Sign in", "Log in"] + }, + { + "name": "click_phone_email_button", + "description": "Telefon/E-Mail-Option auswählen", + "action": "click", + "target": "div[data-e2e='channel-item']", + "wait_for": ["input[type='text']"], + "fuzzy_match": ["Telefon-Nr./E-Mail/Anmeldename", "Phone or Email"] + }, + { + "name": "fill_login_field", + "description": "Benutzername/E-Mail/Telefon eingeben", + "action": "fill", + "target": "input[type='text']", + "value": "{USERNAME_OR_EMAIL}", + "fuzzy_match": ["Email", "Benutzername", "Telefon"] + }, + { + "name": "fill_password", + "description": "Passwort eingeben", + "action": "fill", + "target": "input[type='password']", + "value": "{PASSWORD}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["password"] + }, + { + "name": "click_login", + "description": "Anmelden klicken", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["a[href='/foryou']"], + "fuzzy_match": ["Anmelden", "Log in", "Login"] + } + ] + + return login_steps + + @staticmethod + def get_verification_workflow() -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die TikTok-Verifizierung zurück. + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + verification_steps = [ + { + "name": "fill_verification_code", + "description": "Bestätigungscode eingeben", + "action": "fill", + "target": "input[placeholder*='sechsstelligen Code']", + "value": "{VERIFICATION_CODE}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"] + }, + { + "name": "click_continue", + "description": "Weiter klicken", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["input[placeholder='Benutzername']", "a[href='/foryou']"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"] + } + ] + + return verification_steps + + @staticmethod + def identify_current_step(page_title: str, page_url: str, visible_elements: List[str]) -> str: + """ + Identifiziert den aktuellen Schritt basierend auf dem Seitentitel, der URL und sichtbaren Elementen. + + Args: + page_title: Titel der Seite + page_url: URL der Seite + visible_elements: Liste sichtbarer Elemente (Selektoren) + + Returns: + str: Name des identifizierten Schritts + """ + # Auf der Startseite + if "tiktok.com" in page_url and not "/signup" in page_url and not "/login" in page_url: + return "navigate_to_signup" + + # Anmelde-/Registrierungsauswahl + if "signup" in page_url or "login" in page_url: + if any("channel-item" in element for element in visible_elements): + return "click_phone_email_button" + + # Geburtsdatum + if "Monat" in page_title or "Month" in page_title or any("Geburtsdatum" in element for element in visible_elements): + return "select_birth_month" + + # E-Mail-/Telefon-Eingabe + if any("E-Mail-Adresse" in element for element in visible_elements): + return "fill_email" + if any("Telefonnummer" in element for element in visible_elements): + return "fill_phone" + + # Bestätigungscode + if any("sechsstelligen Code" in element for element in visible_elements): + return "fill_verification_code" + + # Benutzernamen-Erstellung + if any("Benutzername" in element for element in visible_elements): + return "fill_username" + + # Erfolgreiche Anmeldung + if "foryou" in page_url or any("Für dich" in element for element in visible_elements): + return "logged_in" + + return "unknown" \ No newline at end of file diff --git a/social_networks/twitter/__init__.py b/social_networks/twitter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_automation.py b/social_networks/twitter/twitter_automation.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_login.py b/social_networks/twitter/twitter_login.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_registration.py b/social_networks/twitter/twitter_registration.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_selectors.py b/social_networks/twitter/twitter_selectors.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_ui_helper.py b/social_networks/twitter/twitter_ui_helper.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_utils.py b/social_networks/twitter/twitter_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_verification.py b/social_networks/twitter/twitter_verification.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_workflow.py b/social_networks/twitter/twitter_workflow.py new file mode 100644 index 0000000..e69de29 diff --git a/testcases/imap_test.py b/testcases/imap_test.py new file mode 100644 index 0000000..2137f70 --- /dev/null +++ b/testcases/imap_test.py @@ -0,0 +1,679 @@ +#!/usr/bin/env python3 +# Path: testcase/imap_test.py + +""" +IMAP-Verbindungstest für Social Media Account Generator. +Dieses eigenständige Skript testet die IMAP-Verbindungsdaten aus der Konfigurationsdatei +unter Verwendung der Logik aus email_handler.py. +""" + +import imaplib +import email +import json +import os +import sys +import time +import re +from email.header import decode_header +from datetime import datetime, timedelta +from typing import Dict, List, Any, Optional, Tuple, Union + +# Füge das Hauptverzeichnis zum Pythonpfad hinzu, um Zugriff auf die Konfigurationsdateien zu erhalten +# Der Pfad wird relativ zum Skriptverzeichnis aufgelöst +script_dir = os.path.dirname(os.path.abspath(__file__)) +base_dir = os.path.dirname(script_dir) # Gehe eine Ebene hoch zum Hauptverzeichnis +sys.path.insert(0, base_dir) + + +class EmailTester: + """ + Tester für IMAP-Email-Funktionalität unter Verwendung der Logik aus email_handler.py. + """ + + CONFIG_FILE = os.path.join(base_dir, "config", "email_config.json") + + def __init__(self): + """Initialisiert den EmailTester und lädt die Konfiguration.""" + self.config = self.load_config() + + # Typische Betreffzeilen für Verifizierungs-E-Mails nach Plattform + self.verification_subjects = { + "instagram": [ + "Bestätige deine E-Mail-Adresse", + "Bestätigungscode für Instagram", + "Dein Instagram-Code", + "Bestätige deinen Instagram-Account", + "Verify your email address", + "Instagram Verification Code", + "Your Instagram Code", + "Verify your Instagram account", + "Instagram-Bestätigungscode", + "Instagram security code" + ], + "facebook": [ + "Bestätigungscode für Facebook", + "Facebook-Bestätigungscode", + "Dein Facebook-Code", + "Facebook Verification Code", + "Your Facebook Code" + ], + "twitter": [ + "Bestätige dein Twitter-Konto", + "Twitter-Bestätigungscode", + "Verify your Twitter account", + "Twitter Verification Code" + ], + "tiktok": [ + "TikTok-Bestätigungscode", + "Bestätige dein TikTok-Konto", + "TikTok Verification Code", + "Verify your TikTok account" + ], + "default": [ + "Bestätigungscode", + "Verification Code", + "Account Verification", + "Konto-Bestätigung", + "Security Code", + "Sicherheitscode" + ] + } + + print(f"EmailTester initialisiert, suche nach Konfiguration in: {self.CONFIG_FILE}") + + def load_config(self) -> Dict[str, Any]: + """ + Lädt die E-Mail-Konfiguration aus der Konfigurationsdatei. + + Returns: + Dict[str, Any]: Die geladene Konfiguration oder Standardwerte + """ + default_config = { + "imap_server": "imap.ionos.de", + "imap_port": 993, + "imap_user": "info@z5m7q9dk3ah2v1plx6ju.com", + "imap_pass": "cz&ie.O9$!:!tYY@" + } + + try: + if os.path.exists(self.CONFIG_FILE): + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + config = json.load(f) + + print(f"E-Mail-Konfiguration geladen aus: {self.CONFIG_FILE}") + return config + else: + print(f"Konfigurationsdatei nicht gefunden: {self.CONFIG_FILE}") + print("Verwende Standardkonfiguration:") + print(json.dumps(default_config, indent=2)) + return default_config + + except Exception as e: + print(f"Fehler beim Laden der E-Mail-Konfiguration: {e}") + print("Verwende Standardkonfiguration.") + return default_config + + def test_connection(self) -> Dict[str, Any]: + """ + Testet die Verbindung zum IMAP-Server. + + Returns: + Dict[str, Any]: Ergebnis des Tests + """ + try: + print(f"Teste Verbindung zu {self.config['imap_server']}:{self.config['imap_port']}") + print(f"Benutzer: {self.config['imap_user']}") + + # SSL-Verbindung zum IMAP-Server herstellen + mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) + + # Anmelden + mail.login(self.config["imap_user"], self.config["imap_pass"]) + + # Verfügbare Postfächer auflisten + status, mailboxes = mail.list() + + if status == 'OK': + print("\nVerfügbare Postfächer:") + mailbox_count = 0 + + for mailbox in mailboxes: + if isinstance(mailbox, bytes): + try: + # Decode von bytes zu string + decoded_mailbox = mailbox.decode('utf-8') + print(f" - {decoded_mailbox}") + mailbox_count += 1 + except: + print(f" - [Nicht decodierbar: {mailbox}]") + else: + print(f" - {mailbox}") + mailbox_count += 1 + + # INBOX auswählen + mail.select("INBOX") + + # Abmelden + mail.logout() + + print(f"\nVerbindungstest erfolgreich! {mailbox_count} Postfächer gefunden.") + return { + "success": True, + "server": self.config["imap_server"], + "port": self.config["imap_port"], + "mailbox_count": mailbox_count + } + else: + print(f"Fehler beim Abrufen der Postfächer: {status}") + mail.logout() + return { + "success": False, + "error": f"Fehler beim Abrufen der Postfächer: {status}" + } + except imaplib.IMAP4.error as e: + print(f"IMAP-Fehler: {e}") + return { + "success": False, + "error": f"IMAP-Fehler: {e}" + } + except Exception as e: + print(f"Allgemeiner Fehler: {e}") + return { + "success": False, + "error": f"Allgemeiner Fehler: {e}" + } + + def _extract_email_from_addr(self, addr_str: str) -> str: + """ + Extrahiert die E-Mail-Adresse aus einem Adressstring im Format 'Name '. + + Args: + addr_str: Adressstring + + Returns: + str: Die extrahierte E-Mail-Adresse oder der ursprüngliche String + """ + # Regulärer Ausdruck für die Extraktion der E-Mail-Adresse + email_pattern = r'?' + match = re.search(email_pattern, addr_str) + + if match: + return match.group(1).lower() + + return addr_str.lower() + + def search_emails(self, search_criteria: str = "ALL", max_emails: int = 5) -> List[Dict[str, Any]]: + """ + Sucht nach E-Mails mit den angegebenen Kriterien. + + Args: + search_criteria: IMAP-Suchkriterien + max_emails: Maximale Anzahl der abzurufenden E-Mails + + Returns: + List[Dict[str, Any]]: Liste der gefundenen E-Mails + """ + try: + print(f"Suche E-Mails mit Kriterien: {search_criteria}") + print(f"Maximale Anzahl: {max_emails}") + + # Verbindung zum IMAP-Server herstellen + mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) + + # Anmelden + mail.login(self.config["imap_user"], self.config["imap_pass"]) + + # INBOX auswählen + mail.select("INBOX") + + # Nach E-Mails suchen + status, data = mail.search(None, search_criteria) + + emails = [] + + if status == 'OK': + # E-Mail-IDs abrufen + email_ids = data[0].split() + + if not email_ids: + print("Keine E-Mails gefunden.") + mail.logout() + return [] + + # Newest emails first + email_ids = list(reversed(email_ids)) + + # Begrenze die Anzahl der abzurufenden E-Mails + if max_emails > 0: + email_ids = email_ids[:max_emails] + + print(f"Gefunden: {len(email_ids)} E-Mails. Abrufen der Details...") + + for i, email_id in enumerate(email_ids): + # E-Mail abrufen + status, data = mail.fetch(email_id, '(RFC822)') + + if status == 'OK': + print(f"Verarbeite E-Mail {i+1}/{len(email_ids)}...") + + # E-Mail-Inhalt parsen + raw_email = data[0][1] + msg = email.message_from_bytes(raw_email) + + # Betreff decodieren + subject = decode_header(msg.get("Subject", ""))[0] + if isinstance(subject[0], bytes): + subject = subject[0].decode(subject[1] or 'utf-8', errors='replace') + else: + subject = subject[0] + + # Absender decodieren + from_addr = decode_header(msg.get("From", ""))[0] + if isinstance(from_addr[0], bytes): + from_addr = from_addr[0].decode(from_addr[1] or 'utf-8', errors='replace') + else: + from_addr = from_addr[0] + + # Empfänger decodieren + to_addr = decode_header(msg.get("To", ""))[0] + if isinstance(to_addr[0], bytes): + to_addr = to_addr[0].decode(to_addr[1] or 'utf-8', errors='replace') + else: + to_addr = to_addr[0] + + # Extrahiere E-Mail-Adresse aus dem To-Feld + to_email = self._extract_email_from_addr(to_addr) + + # Datum decodieren + date = msg.get("Date", "") + + # E-Mail-Text extrahieren + body = "" + + if msg.is_multipart(): + for part in msg.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + + if "attachment" not in content_disposition: + if content_type == "text/plain": + try: + # Textinhalt decodieren + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='replace') + break + except: + body = "[Fehler beim Decodieren des Inhalts]" + elif content_type == "text/html" and not body: + try: + # HTML-Inhalt decodieren + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='replace') + except: + body = "[Fehler beim Decodieren des HTML-Inhalts]" + else: + try: + # Einzel-Teil-E-Mail decodieren + charset = msg.get_content_charset() or 'utf-8' + body = msg.get_payload(decode=True).decode(charset, errors='replace') + except: + body = "[Fehler beim Decodieren des Inhalts]" + + # E-Mail-Informationen speichern + email_info = { + "id": email_id.decode(), + "subject": subject, + "from": from_addr, + "to": to_addr, + "to_email": to_email, + "date": date, + "body": body + } + + # Nach Bestätigungscode im Inhalt suchen + verification_code = self._extract_verification_code(body) + if verification_code: + email_info["verification_code"] = verification_code + + emails.append(email_info) + + # Abmelden + mail.logout() + + print(f"Insgesamt {len(emails)} E-Mails verarbeitet") + return emails + + except Exception as e: + print(f"Fehler beim Suchen nach E-Mails: {e}") + return [] + + def _is_subject_relevant(self, subject: str, platform: str) -> bool: + """ + Prüft, ob der Betreff relevant für eine Verifizierungs-E-Mail der angegebenen Plattform ist. + + Args: + subject: Betreff der E-Mail + platform: Plattform (instagram, facebook, twitter, etc.) + + Returns: + bool: True, wenn der Betreff relevant ist, False sonst + """ + # Betreffzeilen für die angegebene Plattform und Standard + subject_patterns = self.verification_subjects.get(platform.lower(), []) + subject_patterns += self.verification_subjects["default"] + + # Prüfe auf exakte Übereinstimmung (schneller) + for pattern in subject_patterns: + if pattern.lower() in subject.lower(): + print(f"Relevanter Betreff gefunden (exakte Übereinstimmung): {subject}") + return True + + return False + + def _extract_verification_code(self, text: str, platform: str = "instagram") -> Optional[str]: + """ + Extrahiert einen Bestätigungscode aus einem Text. + + Args: + text: Zu durchsuchender Text + platform: Plattform (instagram, facebook, twitter, etc.) + + Returns: + Optional[str]: Der gefundene Bestätigungscode oder None + """ + # Plattformspezifische Muster für Bestätigungscodes + patterns = { + "instagram": [ + r"Dein Code ist (\d{6})", + r"Your code is (\d{6})", + r"Bestätigungscode: (\d{6})", + r"Confirmation code: (\d{6})", + r"(\d{6}) ist dein Instagram-Code", + r"(\d{6}) is your Instagram code", + r"Instagram-Code: (\d{6})", + r"Instagram code: (\d{6})", + r"Instagram: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "facebook": [ + r"Dein Code ist (\d{5})", + r"Your code is (\d{5})", + r"Bestätigungscode: (\d{5})", + r"Confirmation code: (\d{5})", + r"Facebook-Code: (\d{5})", + r"Facebook code: (\d{5})", + r"Facebook: (\d{5})", + r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern + ], + "twitter": [ + r"Code: (\d{6})", + r"Verification code: (\d{6})", + r"Twitter-Code: (\d{6})", + r"Twitter code: (\d{6})", + r"Twitter: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "tiktok": [ + r"TikTok-Code: (\d{6})", + r"TikTok code: (\d{6})", + r"TikTok: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "default": [ + r"Code[:\s]*(\d{4,8})", + r"[Vv]erification [Cc]ode[:\s]*(\d{4,8})", + r"[Bb]estätigungscode[:\s]*(\d{4,8})", + r"(\d{4,8}) is your code", + r"(\d{4,8}) ist dein Code", + r"[^\d](\d{6})[^\d]", # 6-stellige Zahl umgeben von Nicht-Ziffern + r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern + ] + } + + # Plattformspezifische Muster verwenden + platform_patterns = patterns.get(platform.lower(), []) + + # Alle Muster dieser Plattform durchsuchen + for pattern in platform_patterns: + match = re.search(pattern, text) + if match: + code = match.group(1) + print(f"Code gefunden mit Muster '{pattern}': {code}") + return code + + # Wenn keine plattformspezifischen Muster gefunden wurden, Default-Muster verwenden + for pattern in patterns["default"]: + match = re.search(pattern, text) + if match: + code = match.group(1) + print(f"Code gefunden mit Default-Muster '{pattern}': {code}") + return code + + # Generische Suche nach Zahlen (für die jeweilige Plattform typische Länge) + code_length = 6 # Standard + if platform.lower() == "facebook": + code_length = 5 + + # Suche nach alleinstehenden Zahlen der richtigen Länge + generic_pattern = r"\b(\d{" + str(code_length) + r"})\b" + matches = re.findall(generic_pattern, text) + + if matches: + # Nehme die erste gefundene Zahl + code = matches[0] + print(f"Code gefunden mit generischem Muster: {code}") + return code + + return None + + def find_verification_codes(self, target_email: Optional[str] = None, platform: str = "instagram", + max_emails: int = 10) -> List[Dict[str, Any]]: + """ + Sucht nach Bestätigungscodes in E-Mails. + + Args: + target_email: Ziel-E-Mail-Adresse oder None für alle + platform: Plattform (instagram, facebook, twitter, etc.) + max_emails: Maximale Anzahl der zu durchsuchenden E-Mails + + Returns: + List[Dict[str, Any]]: Liste der gefundenen E-Mails mit Bestätigungscodes + """ + # Letzter Tag als Suchkriterium + today = datetime.now() + yesterday = today - timedelta(days=1) + date_str = yesterday.strftime("%d-%b-%Y") + + search_criteria = f'(SINCE "{date_str}")' + + if target_email: + search_criteria = f'(SINCE "{date_str}" TO "{target_email}")' + print(f"Suche nach E-Mails für {target_email} seit {date_str}") + else: + print(f"Suche nach allen E-Mails seit {date_str}") + + # Alle E-Mails abrufen + emails = self.search_emails(search_criteria, max_emails=max_emails) + + # Relevante E-Mails filtern + verification_emails = [] + + for email_info in emails: + # Extrahierte E-Mail-Adresse des Empfängers + to_email = email_info.get("to_email", "").lower() + + # Wenn eine bestimmte Ziel-E-Mail angegeben ist, prüfe auf Übereinstimmung + if target_email and target_email.lower() != to_email: + # Wenn Domain angegeben wurde, prüfe auf Domain-Übereinstimmung + if '@' in target_email and '@' in to_email: + target_domain = target_email.split('@')[1] + email_domain = to_email.split('@')[1] + if target_domain != email_domain: + continue + else: + continue + + # Betreff auf Relevanz prüfen + subject = email_info.get("subject", "") + if self._is_subject_relevant(subject, platform): + verification_emails.append(email_info) + + # Alternativ: In der E-Mail nach Bestätigungscodes suchen + elif "verification_code" in email_info or self._extract_verification_code(email_info.get("body", ""), platform): + verification_emails.append(email_info) + + print(f"Gefunden: {len(verification_emails)} E-Mails mit Bestätigungscodes oder relevanten Betreffs") + return verification_emails + + +def display_email(email_info, truncate_body=True): + """ + Zeigt eine E-Mail formatiert an. + + Args: + email_info: E-Mail-Informationen + truncate_body: Wenn True, wird der Body gekürzt + """ + print("\n" + "="*80) + print(f"ID: {email_info.get('id', 'N/A')}") + print(f"Von: {email_info.get('from', 'N/A')}") + print(f"An: {email_info.get('to', 'N/A')}") + print(f"Datum: {email_info.get('date', 'N/A')}") + print(f"Betreff: {email_info.get('subject', 'N/A')}") + + # Bestätigungscode anzeigen, falls vorhanden + verification_code = email_info.get("verification_code") + if verification_code: + print(f"\n>>> BESTÄTIGUNGSCODE GEFUNDEN: {verification_code} <<<\n") + + # Body anzeigen + print("\nInhalt:") + print("-"*80) + body = email_info.get("body", "") + if truncate_body and len(body) > 500: + print(body[:500] + "...\n[Gekürzt - Vollständigen Inhalt mit Option 2 anzeigen]") + else: + print(body) + print("="*80) + + +def main(): + """Hauptfunktion des Skripts.""" + print("="*80) + print("IMAP-Verbindungs- und E-Mail-Test") + print("="*80) + + # EmailTester initialisieren + tester = EmailTester() + + # Verbindung testen + connection_result = tester.test_connection() + + if not connection_result["success"]: + print("\nVerbindungstest fehlgeschlagen!") + print(f"Fehler: {connection_result.get('error', 'Unbekannter Fehler')}") + sys.exit(1) + + # Interaktives Menü + while True: + print("\n" + "="*80) + print("MENU") + print("="*80) + print("1. Letzte E-Mails anzeigen") + print("2. Vollständigen Inhalt einer E-Mail anzeigen") + print("3. Nach Bestätigungscodes suchen") + print("4. Bestätigungscodes für eine bestimmte E-Mail-Adresse suchen") + print("5. Verbindungstest erneut durchführen") + print("0. Beenden") + print("="*80) + + choice = input("Wählen Sie eine Option: ") + + if choice == "1": + # Letzte E-Mails anzeigen + max_emails = int(input("Anzahl der anzuzeigenden E-Mails (Standard: 5): ") or "5") + emails = tester.search_emails(max_emails=max_emails) + + if emails: + print(f"\n{len(emails)} E-Mails gefunden:") + for email_info in emails: + display_email(email_info) + else: + print("Keine E-Mails gefunden.") + + elif choice == "2": + # Vollständigen Inhalt einer E-Mail anzeigen + email_id = input("Geben Sie die E-Mail-ID ein: ") + if not email_id: + print("Keine ID angegeben.") + continue + + # Suche nach der E-Mail mit der angegebenen ID + emails = tester.search_emails(search_criteria=f"(UID {email_id})", max_emails=1) + + if emails: + display_email(emails[0], truncate_body=False) + else: + print(f"Keine E-Mail mit ID {email_id} gefunden.") + + elif choice == "3": + # Nach Bestätigungscodes in allen E-Mails suchen + platform = input("Plattform (instagram, facebook, twitter, tiktok, default): ") or "instagram" + max_emails = int(input("Maximale Anzahl zu durchsuchender E-Mails (Standard: 10): ") or "10") + + verification_emails = tester.find_verification_codes(platform=platform, max_emails=max_emails) + + if verification_emails: + print(f"\n{len(verification_emails)} E-Mails mit Bestätigungscodes oder relevanten Betreffs gefunden:") + for email_info in verification_emails: + display_email(email_info) + else: + print("Keine relevanten E-Mails mit Bestätigungscodes gefunden.") + + elif choice == "4": + # Nach Bestätigungscodes für eine bestimmte E-Mail-Adresse suchen + email_address = input("E-Mail-Adresse: ") + if not email_address: + print("Keine E-Mail-Adresse angegeben.") + continue + + platform = input("Plattform (instagram, facebook, twitter, tiktok, default): ") or "instagram" + max_emails = int(input("Maximale Anzahl zu durchsuchender E-Mails (Standard: 10): ") or "10") + + verification_emails = tester.find_verification_codes( + target_email=email_address, + platform=platform, + max_emails=max_emails + ) + + if verification_emails: + print(f"\n{len(verification_emails)} E-Mails mit Bestätigungscodes für {email_address} gefunden:") + for email_info in verification_emails: + display_email(email_info) + else: + print(f"Keine relevanten E-Mails mit Bestätigungscodes für {email_address} gefunden.") + + elif choice == "5": + # Verbindungstest erneut durchführen + connection_result = tester.test_connection() + + if not connection_result["success"]: + print("\nVerbindungstest fehlgeschlagen!") + print(f"Fehler: {connection_result.get('error', 'Unbekannter Fehler')}") + + elif choice == "0": + # Beenden + print("Programm wird beendet.") + break + + else: + print("Ungültige Eingabe. Bitte versuchen Sie es erneut.") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nProgramm durch Benutzer abgebrochen. Auf Wiedersehen!") + except Exception as e: + print(f"\nUnerwarteter Fehler: {e}") \ No newline at end of file diff --git a/updates/__init__.py b/updates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/updates/__pycache__/__init__.cpython-310.pyc b/updates/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..b1e1cea Binary files /dev/null and b/updates/__pycache__/__init__.cpython-310.pyc differ diff --git a/updates/__pycache__/__init__.cpython-313.pyc b/updates/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..8feca49 Binary files /dev/null and b/updates/__pycache__/__init__.cpython-313.pyc differ diff --git a/updates/__pycache__/update_checker.cpython-310.pyc b/updates/__pycache__/update_checker.cpython-310.pyc new file mode 100644 index 0000000..f72afb7 Binary files /dev/null and b/updates/__pycache__/update_checker.cpython-310.pyc differ diff --git a/updates/__pycache__/update_checker.cpython-313.pyc b/updates/__pycache__/update_checker.cpython-313.pyc new file mode 100644 index 0000000..9a0ba52 Binary files /dev/null and b/updates/__pycache__/update_checker.cpython-313.pyc differ diff --git a/updates/__pycache__/version.cpython-310.pyc b/updates/__pycache__/version.cpython-310.pyc new file mode 100644 index 0000000..2276b23 Binary files /dev/null and b/updates/__pycache__/version.cpython-310.pyc differ diff --git a/updates/downloader.py b/updates/downloader.py new file mode 100644 index 0000000..e69de29 diff --git a/updates/update_checker.py b/updates/update_checker.py new file mode 100644 index 0000000..0534223 --- /dev/null +++ b/updates/update_checker.py @@ -0,0 +1,411 @@ +""" +Update-Checking-Funktionalität für den Social Media Account Generator. +""" + +import os +import json +import logging +import requests +import shutil +from datetime import datetime +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("update_checker") + +class UpdateChecker: + """Klasse zum Überprüfen und Herunterladen von Updates.""" + + CONFIG_FILE = os.path.join("config", "app_version.json") + UPDATE_SERVER_URL = "https://api.example.com/updates" # Platzhalter - in der Produktion anpassen + + def __init__(self): + """Initialisiert den UpdateChecker und lädt die Konfiguration.""" + self.version_info = self.load_version_info() + + # Stelle sicher, dass das Konfigurationsverzeichnis existiert + os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True) + + # Updates-Verzeichnis für Downloads + os.makedirs("updates", exist_ok=True) + + def load_version_info(self) -> Dict[str, Any]: + """Lädt die Versionsinformationen aus der Konfigurationsdatei.""" + if not os.path.exists(self.CONFIG_FILE): + default_info = { + "current_version": "1.0.0", + "last_check": "", + "channel": "stable", + "auto_check": True, + "auto_download": False + } + + # Standardwerte speichern + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(default_info, f, indent=2) + except Exception as e: + logger.error(f"Fehler beim Speichern der Standardversionsinformationen: {e}") + + return default_info + + try: + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + version_info = json.load(f) + + logger.info(f"Versionsinformationen geladen: {version_info.get('current_version', 'unbekannt')}") + + return version_info + except Exception as e: + logger.error(f"Fehler beim Laden der Versionsinformationen: {e}") + return { + "current_version": "1.0.0", + "last_check": "", + "channel": "stable", + "auto_check": True, + "auto_download": False + } + + def save_version_info(self) -> bool: + """Speichert die Versionsinformationen in die Konfigurationsdatei.""" + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.version_info, f, indent=2) + + logger.info("Versionsinformationen gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Versionsinformationen: {e}") + return False + + def compare_versions(self, version1: str, version2: str) -> int: + """ + Vergleicht zwei Versionsstrings (semver). + + Args: + version1: Erste Version + version2: Zweite Version + + Returns: + -1, wenn version1 < version2 + 0, wenn version1 == version2 + 1, wenn version1 > version2 + """ + v1_parts = [int(part) for part in version1.split(".")] + v2_parts = [int(part) for part in version2.split(".")] + + # Fülle fehlende Teile mit Nullen auf + while len(v1_parts) < 3: + v1_parts.append(0) + while len(v2_parts) < 3: + v2_parts.append(0) + + # Vergleiche die Teile + for i in range(3): + if v1_parts[i] < v2_parts[i]: + return -1 + elif v1_parts[i] > v2_parts[i]: + return 1 + + return 0 + + def check_for_updates(self, force: bool = False) -> Dict[str, Any]: + """ + Überprüft, ob Updates verfügbar sind. + + Args: + force: Erzwingt eine Überprüfung, auch wenn erst kürzlich geprüft wurde + + Returns: + Dictionary mit Update-Informationen + """ + result = { + "has_update": False, + "current_version": self.version_info["current_version"], + "latest_version": self.version_info["current_version"], + "release_date": "", + "release_notes": "", + "download_url": "", + "error": "" + } + + # Prüfe, ob seit der letzten Überprüfung genügend Zeit vergangen ist (24 Stunden) + if not force and self.version_info.get("last_check"): + try: + last_check = datetime.fromisoformat(self.version_info["last_check"]) + now = datetime.now() + + # Wenn weniger als 24 Stunden seit der letzten Überprüfung vergangen sind + if (now - last_check).total_seconds() < 86400: + logger.info("Update-Überprüfung übersprungen (letzte Überprüfung vor weniger als 24 Stunden)") + return result + except Exception as e: + logger.warning(f"Fehler beim Parsen des letzten Überprüfungsdatums: {e}") + + try: + # Simuliere eine Online-Überprüfung für Entwicklungszwecke + # In der Produktion sollte eine echte API-Anfrage implementiert werden + # response = requests.get( + # f"{self.UPDATE_SERVER_URL}/check", + # params={ + # "version": self.version_info["current_version"], + # "channel": self.version_info["channel"] + # }, + # timeout=10 + # ) + + # For demonstration purposes only + latest_version = "1.1.0" + has_update = self.compare_versions(self.version_info["current_version"], latest_version) < 0 + + if has_update: + result["has_update"] = True + result["latest_version"] = latest_version + result["release_date"] = "2025-05-01" + result["release_notes"] = ( + "Version 1.1.0:\n" + "- Unterstützung für Facebook-Accounts hinzugefügt\n" + "- Verbesserte Proxy-Rotation\n" + "- Bessere Fehlerbehandlung bei der Account-Erstellung\n" + "- Verschiedene Bugfixes und Leistungsverbesserungen" + ) + result["download_url"] = f"{self.UPDATE_SERVER_URL}/download/v1.1.0" + + # Update der letzten Überprüfung + self.version_info["last_check"] = datetime.now().isoformat() + self.save_version_info() + + logger.info(f"Update-Überprüfung abgeschlossen: {result['latest_version']} verfügbar") + + return result + + except requests.RequestException as e: + error_msg = f"Netzwerkfehler bei der Update-Überprüfung: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Update-Überprüfung: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + + def download_update(self, download_url: str, version: str) -> Dict[str, Any]: + """ + Lädt ein Update herunter. + + Args: + download_url: URL zum Herunterladen des Updates + version: Version des Updates + + Returns: + Dictionary mit Download-Informationen + """ + result = { + "success": False, + "file_path": "", + "version": version, + "error": "" + } + + try: + # Zieldateiname erstellen + file_name = f"update_v{version}.zip" + file_path = os.path.join("updates", file_name) + + # Simuliere einen Download für Entwicklungszwecke + # In der Produktion sollte ein echter Download implementiert werden + + # response = requests.get(download_url, stream=True, timeout=60) + # if response.status_code == 200: + # with open(file_path, "wb") as f: + # shutil.copyfileobj(response.raw, f) + + # Simulierter Download (erstelle eine leere Datei) + with open(file_path, "w") as f: + f.write(f"Placeholder for version {version} update") + + result["success"] = True + result["file_path"] = file_path + + logger.info(f"Update v{version} heruntergeladen: {file_path}") + + return result + + except requests.RequestException as e: + error_msg = f"Netzwerkfehler beim Herunterladen des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Herunterladen des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + + def is_update_available(self) -> bool: + """ + Überprüft, ob ein Update verfügbar ist. + + Returns: + True, wenn ein Update verfügbar ist, sonst False + """ + update_info = self.check_for_updates() + return update_info["has_update"] + + def get_current_version(self) -> str: + """ + Gibt die aktuelle Version zurück. + + Returns: + Aktuelle Version + """ + return self.version_info["current_version"] + + def set_current_version(self, version: str) -> bool: + """ + Setzt die aktuelle Version. + + Args: + version: Neue Version + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["current_version"] = version + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen der aktuellen Version: {e}") + return False + + def set_update_channel(self, channel: str) -> bool: + """ + Setzt den Update-Kanal (stable, beta, dev). + + Args: + channel: Update-Kanal + + Returns: + True bei Erfolg, False im Fehlerfall + """ + if channel not in ["stable", "beta", "dev"]: + logger.warning(f"Ungültiger Update-Kanal: {channel}") + return False + + try: + self.version_info["channel"] = channel + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen des Update-Kanals: {e}") + return False + + def get_update_channel(self) -> str: + """ + Gibt den aktuellen Update-Kanal zurück. + + Returns: + Update-Kanal (stable, beta, dev) + """ + return self.version_info.get("channel", "stable") + + def set_auto_check(self, auto_check: bool) -> bool: + """ + Aktiviert oder deaktiviert die automatische Update-Überprüfung. + + Args: + auto_check: True, um automatische Updates zu aktivieren, False zum Deaktivieren + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["auto_check"] = bool(auto_check) + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen der automatischen Update-Überprüfung: {e}") + return False + + def is_auto_check_enabled(self) -> bool: + """ + Überprüft, ob die automatische Update-Überprüfung aktiviert ist. + + Returns: + True, wenn die automatische Update-Überprüfung aktiviert ist, sonst False + """ + return self.version_info.get("auto_check", True) + + def set_auto_download(self, auto_download: bool) -> bool: + """ + Aktiviert oder deaktiviert den automatischen Download von Updates. + + Args: + auto_download: True, um automatische Downloads zu aktivieren, False zum Deaktivieren + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["auto_download"] = bool(auto_download) + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen des automatischen Downloads: {e}") + return False + + def is_auto_download_enabled(self) -> bool: + """ + Überprüft, ob der automatische Download von Updates aktiviert ist. + + Returns: + True, wenn der automatische Download aktiviert ist, sonst False + """ + return self.version_info.get("auto_download", False) + + def apply_update(self, update_file: str) -> Dict[str, Any]: + """ + Wendet ein heruntergeladenes Update an. + + Args: + update_file: Pfad zur Update-Datei + + Returns: + Dictionary mit Informationen über die Anwendung des Updates + """ + result = { + "success": False, + "version": "", + "error": "" + } + + if not os.path.exists(update_file): + result["error"] = f"Update-Datei nicht gefunden: {update_file}" + logger.error(result["error"]) + return result + + try: + # In der Produktion sollte hier die tatsächliche Update-Logik implementiert werden + # 1. Extrahieren des Updates + # 2. Sichern der aktuellen Version + # 3. Anwenden der Änderungen + # 4. Aktualisieren der Versionsinformationen + + # Simuliere ein erfolgreiches Update + logger.info(f"Update aus {update_file} erfolgreich angewendet (simuliert)") + + # Extrahiere Version aus dem Dateinamen + file_name = os.path.basename(update_file) + version_match = re.search(r"v([0-9.]+)", file_name) + + if version_match: + new_version = version_match.group(1) + self.set_current_version(new_version) + result["version"] = new_version + + result["success"] = True + + return result + + except Exception as e: + error_msg = f"Fehler beim Anwenden des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result \ No newline at end of file diff --git a/updates/update_v1.1.0.zip b/updates/update_v1.1.0.zip new file mode 100644 index 0000000..446d6b6 --- /dev/null +++ b/updates/update_v1.1.0.zip @@ -0,0 +1 @@ +Placeholder for version 1.1.0 update \ No newline at end of file diff --git a/updates/version.py b/updates/version.py new file mode 100644 index 0000000..fe3516c --- /dev/null +++ b/updates/version.py @@ -0,0 +1,193 @@ +""" +Version-Verwaltung - Enthält Versionsinformationen und Hilfsfunktionen +""" + +import os +import logging +import json +from typing import Dict, Any, Tuple + +# Konfiguriere Logger +logger = logging.getLogger("version") + +# Aktuelle Version der Software +CURRENT_VERSION = "1.0.0" + +# Build-Informationen +BUILD_DATE = "2025-04-30" +BUILD_NUMBER = "1001" + +# Versionsinformationen +VERSION_INFO = { + "version": CURRENT_VERSION, + "build_date": BUILD_DATE, + "build_number": BUILD_NUMBER, + "channel": "stable", + "min_platform_version": "10.0.0", + "compatible_versions": ["0.9.0", "0.9.1", "0.9.2"] +} + +def get_version() -> str: + """ + Gibt die aktuelle Version der Software zurück. + + Returns: + str: Aktuelle Version + """ + return CURRENT_VERSION + +def get_version_info() -> Dict[str, Any]: + """ + Gibt detaillierte Versionsinformationen zurück. + + Returns: + Dict[str, Any]: Versionsinformationen + """ + return VERSION_INFO.copy() + +def parse_version(version_str: str) -> Tuple[int, ...]: + """ + Parst eine Versionszeichenfolge in ein vergleichbares Tupel. + + Args: + version_str: Versionszeichenfolge im Format x.y.z + + Returns: + Tuple[int, ...]: Geparste Version als Tupel + """ + try: + return tuple(map(int, version_str.split('.'))) + except ValueError: + # Fallback bei ungültigem Format + logger.warning(f"Ungültiges Versionsformat: {version_str}") + return (0, 0, 0) + +def is_newer_version(version_a: str, version_b: str) -> bool: + """ + Prüft, ob Version A neuer ist als Version B. + + Args: + version_a: Erste Version + version_b: Zweite Version + + Returns: + bool: True, wenn Version A neuer ist als Version B, False sonst + """ + version_a_tuple = parse_version(version_a) + version_b_tuple = parse_version(version_b) + + return version_a_tuple > version_b_tuple + +def is_compatible_version(version: str) -> bool: + """ + Prüft, ob die angegebene Version mit der aktuellen Version kompatibel ist. + + Args: + version: Zu prüfende Version + + Returns: + bool: True, wenn die Version kompatibel ist, False sonst + """ + # Wenn es die gleiche Version ist, ist sie kompatibel + if version == CURRENT_VERSION: + return True + + # Prüfe, ob die Version in der Liste der kompatiblen Versionen ist + if version in VERSION_INFO.get("compatible_versions", []): + return True + + # Wenn es eine neuere Version ist, nehmen wir an, sie ist kompatibel + if is_newer_version(version, CURRENT_VERSION): + return True + + # Ansonsten ist die Version nicht kompatibel + return False + +def get_version_description() -> str: + """ + Gibt eine menschenlesbare Beschreibung der Versionsinfos zurück. + + Returns: + str: Versionsbeschreibung + """ + info = get_version_info() + + description = [ + f"Version: {info['version']}", + f"Build: {info['build_number']} ({info['build_date']})", + f"Kanal: {info['channel']}" + ] + + return "\n".join(description) + +def save_version_info(file_path: str = "version_info.json") -> bool: + """ + Speichert die Versionsinformationen in einer Datei. + + Args: + file_path: Pfad zur Datei + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + with open(file_path, 'w') as f: + json.dump(VERSION_INFO, f, indent=2) + logger.info(f"Versionsinformationen gespeichert in {file_path}") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Versionsinformationen: {e}") + return False + +def load_version_info(file_path: str = "version_info.json") -> Dict[str, Any]: + """ + Lädt Versionsinformationen aus einer Datei. + + Args: + file_path: Pfad zur Datei + + Returns: + Dict[str, Any]: Geladene Versionsinformationen oder Standardwerte + """ + try: + if os.path.exists(file_path): + with open(file_path, 'r') as f: + return json.load(f) + else: + logger.warning(f"Versionsinformationsdatei nicht gefunden: {file_path}") + return VERSION_INFO.copy() + except Exception as e: + logger.error(f"Fehler beim Laden der Versionsinformationen: {e}") + return VERSION_INFO.copy() + + +# Beispielnutzung, wenn direkt ausgeführt +if __name__ == "__main__": + # Konfiguriere Logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Versionsinformationen anzeigen + print("Aktuelle Version:", get_version()) + print("\nVersionsinformationen:") + info = get_version_info() + for key, value in info.items(): + print(f" {key}: {value}") + + # Versionsbeschreibung + print("\nVersionsbeschreibung:") + print(get_version_description()) + + # Versionsvergleich + test_versions = ["0.9.0", "1.0.0", "1.0.1", "1.1.0", "2.0.0"] + print("\nVersionsvergleiche:") + for version in test_versions: + is_newer = is_newer_version(version, CURRENT_VERSION) + is_compat = is_compatible_version(version) + print(f" {version}: Neuer als aktuell: {is_newer}, Kompatibel: {is_compat}") + + # Versionsinformationen speichern + save_version_info("test_version_info.json") + print("\nVersionsinformationen gespeichert in test_version_info.json") \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..8a16522 Binary files /dev/null and b/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/utils/__pycache__/__init__.cpython-313.pyc b/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..d8ff459 Binary files /dev/null and b/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/utils/__pycache__/birthday_generator.cpython-310.pyc b/utils/__pycache__/birthday_generator.cpython-310.pyc new file mode 100644 index 0000000..236f15c Binary files /dev/null and b/utils/__pycache__/birthday_generator.cpython-310.pyc differ diff --git a/utils/__pycache__/birthday_generator.cpython-313.pyc b/utils/__pycache__/birthday_generator.cpython-313.pyc new file mode 100644 index 0000000..98db6d3 Binary files /dev/null and b/utils/__pycache__/birthday_generator.cpython-313.pyc differ diff --git a/utils/__pycache__/email_handler.cpython-310.pyc b/utils/__pycache__/email_handler.cpython-310.pyc new file mode 100644 index 0000000..9d023b5 Binary files /dev/null and b/utils/__pycache__/email_handler.cpython-310.pyc differ diff --git a/utils/__pycache__/email_handler.cpython-313.pyc b/utils/__pycache__/email_handler.cpython-313.pyc new file mode 100644 index 0000000..2ba3ee1 Binary files /dev/null and b/utils/__pycache__/email_handler.cpython-313.pyc differ diff --git a/utils/__pycache__/human_behavior.cpython-310.pyc b/utils/__pycache__/human_behavior.cpython-310.pyc new file mode 100644 index 0000000..795a74b Binary files /dev/null and b/utils/__pycache__/human_behavior.cpython-310.pyc differ diff --git a/utils/__pycache__/human_behavior.cpython-313.pyc b/utils/__pycache__/human_behavior.cpython-313.pyc new file mode 100644 index 0000000..7f6c04c Binary files /dev/null and b/utils/__pycache__/human_behavior.cpython-313.pyc differ diff --git a/utils/__pycache__/logger.cpython-310.pyc b/utils/__pycache__/logger.cpython-310.pyc new file mode 100644 index 0000000..c6f2d40 Binary files /dev/null and b/utils/__pycache__/logger.cpython-310.pyc differ diff --git a/utils/__pycache__/logger.cpython-313.pyc b/utils/__pycache__/logger.cpython-313.pyc new file mode 100644 index 0000000..183fb6e Binary files /dev/null and b/utils/__pycache__/logger.cpython-313.pyc differ diff --git a/utils/__pycache__/password_generator.cpython-310.pyc b/utils/__pycache__/password_generator.cpython-310.pyc new file mode 100644 index 0000000..f85b7e2 Binary files /dev/null and b/utils/__pycache__/password_generator.cpython-310.pyc differ diff --git a/utils/__pycache__/password_generator.cpython-313.pyc b/utils/__pycache__/password_generator.cpython-313.pyc new file mode 100644 index 0000000..52f3fbd Binary files /dev/null and b/utils/__pycache__/password_generator.cpython-313.pyc differ diff --git a/utils/__pycache__/proxy_rotator.cpython-310.pyc b/utils/__pycache__/proxy_rotator.cpython-310.pyc new file mode 100644 index 0000000..1357791 Binary files /dev/null and b/utils/__pycache__/proxy_rotator.cpython-310.pyc differ diff --git a/utils/__pycache__/proxy_rotator.cpython-313.pyc b/utils/__pycache__/proxy_rotator.cpython-313.pyc new file mode 100644 index 0000000..5888bf0 Binary files /dev/null and b/utils/__pycache__/proxy_rotator.cpython-313.pyc differ diff --git a/utils/__pycache__/text_similarity.cpython-310.pyc b/utils/__pycache__/text_similarity.cpython-310.pyc new file mode 100644 index 0000000..bdb0546 Binary files /dev/null and b/utils/__pycache__/text_similarity.cpython-310.pyc differ diff --git a/utils/__pycache__/text_similarity.cpython-313.pyc b/utils/__pycache__/text_similarity.cpython-313.pyc new file mode 100644 index 0000000..5ec2297 Binary files /dev/null and b/utils/__pycache__/text_similarity.cpython-313.pyc differ diff --git a/utils/__pycache__/theme_manager.cpython-310.pyc b/utils/__pycache__/theme_manager.cpython-310.pyc new file mode 100644 index 0000000..678c461 Binary files /dev/null and b/utils/__pycache__/theme_manager.cpython-310.pyc differ diff --git a/utils/__pycache__/theme_manager.cpython-313.pyc b/utils/__pycache__/theme_manager.cpython-313.pyc new file mode 100644 index 0000000..f8824bc Binary files /dev/null and b/utils/__pycache__/theme_manager.cpython-313.pyc differ diff --git a/utils/__pycache__/username_generator.cpython-310.pyc b/utils/__pycache__/username_generator.cpython-310.pyc new file mode 100644 index 0000000..70a51cf Binary files /dev/null and b/utils/__pycache__/username_generator.cpython-310.pyc differ diff --git a/utils/__pycache__/username_generator.cpython-313.pyc b/utils/__pycache__/username_generator.cpython-313.pyc new file mode 100644 index 0000000..25506a6 Binary files /dev/null and b/utils/__pycache__/username_generator.cpython-313.pyc differ diff --git a/utils/birthday_generator.py b/utils/birthday_generator.py new file mode 100644 index 0000000..20a08dd --- /dev/null +++ b/utils/birthday_generator.py @@ -0,0 +1,299 @@ +""" +Geburtsdatumsgenerator für den Social Media Account Generator. +""" + +import random +import datetime +import logging +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("birthday_generator") + +class BirthdayGenerator: + """Klasse zur Generierung von realistischen Geburtsdaten für Social-Media-Accounts.""" + + def __init__(self): + """Initialisiert den BirthdayGenerator.""" + # Plattformspezifische Richtlinien + self.platform_policies = { + "instagram": { + "min_age": 13, + "max_age": 100, + "date_format": "%Y-%m-%d" # ISO-Format + }, + "facebook": { + "min_age": 13, + "max_age": 100, + "date_format": "%m/%d/%Y" # US-Format + }, + "twitter": { + "min_age": 13, + "max_age": 100, + "date_format": "%Y-%m-%d" # ISO-Format + }, + "tiktok": { + "min_age": 13, + "max_age": 100, + "date_format": "%Y-%m-%d" # ISO-Format + }, + "default": { + "min_age": 18, + "max_age": 80, + "date_format": "%Y-%m-%d" # ISO-Format + } + } + + def get_platform_policy(self, platform: str) -> Dict[str, Any]: + """ + Gibt die Altersrichtlinie für eine bestimmte Plattform zurück. + + Args: + platform: Name der Plattform + + Returns: + Dictionary mit der Altersrichtlinie + """ + platform = platform.lower() + return self.platform_policies.get(platform, self.platform_policies["default"]) + + def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None: + """ + Setzt oder aktualisiert die Altersrichtlinie für eine Plattform. + + Args: + platform: Name der Plattform + policy: Dictionary mit der Altersrichtlinie + """ + platform = platform.lower() + self.platform_policies[platform] = policy + logger.info(f"Altersrichtlinie für '{platform}' aktualisiert") + + def generate_birthday(self, platform: str = "default", age: Optional[int] = None) -> Tuple[datetime.date, str]: + """ + Generiert ein Geburtsdatum gemäß den Plattformrichtlinien. + + Args: + platform: Name der Plattform + age: Optionales spezifisches Alter + + Returns: + (Geburtsdatum als datetime.date, Formatiertes Geburtsdatum als String) + """ + policy = self.get_platform_policy(platform) + + # Aktuelles Datum + today = datetime.date.today() + + # Altersbereich bestimmen + min_age = policy["min_age"] + max_age = policy["max_age"] + + # Wenn ein spezifisches Alter angegeben ist, dieses verwenden + if age is not None: + if age < min_age: + logger.warning(f"Angegebenes Alter ({age}) ist kleiner als das Mindestalter " + f"({min_age}). Verwende Mindestalter.") + age = min_age + elif age > max_age: + logger.warning(f"Angegebenes Alter ({age}) ist größer als das Höchstalter " + f"({max_age}). Verwende Höchstalter.") + age = max_age + else: + # Zufälliges Alter im erlaubten Bereich + age = random.randint(min_age, max_age) + + # Berechne das Geburtsjahr + birth_year = today.year - age + + # Berücksichtige, ob der Geburtstag in diesem Jahr bereits stattgefunden hat + has_had_birthday_this_year = random.choice([True, False]) + + if not has_had_birthday_this_year: + birth_year -= 1 + + # Generiere Monat und Tag + if has_had_birthday_this_year: + # Geburtstag war bereits in diesem Jahr + birth_month = random.randint(1, today.month) + + if birth_month == today.month: + # Wenn gleicher Monat, Tag muss vor oder gleich dem heutigen sein + birth_day = random.randint(1, today.day) + else: + # Wenn anderer Monat, beliebiger Tag + birth_day = random.randint(1, self._days_in_month(birth_month, birth_year)) + else: + # Geburtstag ist noch in diesem Jahr + birth_month = random.randint(today.month, 12) + + if birth_month == today.month: + # Wenn gleicher Monat, Tag muss nach dem heutigen sein + birth_day = random.randint(today.day + 1, self._days_in_month(birth_month, birth_year)) + else: + # Wenn anderer Monat, beliebiger Tag + birth_day = random.randint(1, self._days_in_month(birth_month, birth_year)) + + # Erstelle und formatiere das Geburtsdatum + birth_date = datetime.date(birth_year, birth_month, birth_day) + formatted_date = birth_date.strftime(policy["date_format"]) + + logger.info(f"Geburtsdatum generiert: {formatted_date} (Alter: {age})") + + return birth_date, formatted_date + + def _days_in_month(self, month: int, year: int) -> int: + """ + Gibt die Anzahl der Tage in einem Monat zurück. + + Args: + month: Monat (1-12) + year: Jahr + + Returns: + Anzahl der Tage im angegebenen Monat + """ + if month in [4, 6, 9, 11]: + return 30 + elif month == 2: + # Schaltjahr prüfen + if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0): + return 29 + else: + return 28 + else: + return 31 + + def generate_birthday_components(self, platform: str = "default", age: Optional[int] = None) -> Dict[str, int]: + """ + Generiert die Komponenten eines Geburtsdatums (Tag, Monat, Jahr). + + Args: + platform: Name der Plattform + age: Optionales spezifisches Alter + + Returns: + Dictionary mit den Komponenten des Geburtsdatums (year, month, day) + """ + birth_date, _ = self.generate_birthday(platform, age) + + return { + "year": birth_date.year, + "month": birth_date.month, + "day": birth_date.day + } + + def is_valid_age(self, birth_date: datetime.date, platform: str = "default") -> bool: + """ + Überprüft, ob ein Geburtsdatum für eine Plattform gültig ist. + + Args: + birth_date: Geburtsdatum + platform: Name der Plattform + + Returns: + True, wenn das Alter gültig ist, sonst False + """ + policy = self.get_platform_policy(platform) + + # Aktuelles Datum + today = datetime.date.today() + + # Alter berechnen + age = today.year - birth_date.year + + # Berücksichtigen, ob der Geburtstag in diesem Jahr bereits stattgefunden hat + if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day): + age -= 1 + + return policy["min_age"] <= age <= policy["max_age"] + + def generate_age_from_date(self, birth_date: datetime.date) -> int: + """ + Berechnet das Alter basierend auf einem Geburtsdatum. + + Args: + birth_date: Geburtsdatum + + Returns: + Berechnetes Alter + """ + # Aktuelles Datum + today = datetime.date.today() + + # Alter berechnen + age = today.year - birth_date.year + + # Berücksichtigen, ob der Geburtstag in diesem Jahr bereits stattgefunden hat + if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day): + age -= 1 + + return age + + def generate_date_from_components(self, year: int, month: int, day: int, platform: str = "default") -> str: + """ + Formatiert ein Datum aus seinen Komponenten gemäß dem Plattformformat. + + Args: + year: Jahr + month: Monat + day: Tag + platform: Name der Plattform + + Returns: + Formatiertes Geburtsdatum als String + """ + policy = self.get_platform_policy(platform) + + try: + birth_date = datetime.date(year, month, day) + formatted_date = birth_date.strftime(policy["date_format"]) + return formatted_date + except ValueError as e: + logger.error(f"Ungültiges Datum: {year}-{month}-{day}, Fehler: {e}") + # Fallback: Gültiges Datum zurückgeben + return self.generate_birthday(platform)[1] + + def generate_random_date(self, start_year: int, end_year: int, platform: str = "default") -> str: + """ + Generiert ein zufälliges Datum innerhalb eines Jahresbereichs. + + Args: + start_year: Startjahr + end_year: Endjahr + platform: Name der Plattform + + Returns: + Formatiertes Datum als String + """ + policy = self.get_platform_policy(platform) + + year = random.randint(start_year, end_year) + month = random.randint(1, 12) + day = random.randint(1, self._days_in_month(month, year)) + + date = datetime.date(year, month, day) + + return date.strftime(policy["date_format"]) + +def generate_birthday(age: int = None, platform: str = "default") -> str: + """ + Kompatibilitätsfunktion für ältere Codeversionen. + Generiert ein Geburtsdatum basierend auf einem Alter. + + Args: + age: Alter in Jahren (optional) + platform: Name der Plattform + + Returns: + Generiertes Geburtsdatum im Format "TT.MM.JJJJ" + """ + # Logger-Warnung für Legacy-Funktion + logger.warning("Die Funktion generate_birthday() ist für Kompatibilität, bitte verwende stattdessen die BirthdayGenerator-Klasse.") + + # Eine Instanz der Generator-Klasse erstellen und die Methode aufrufen + generator = BirthdayGenerator() + + # Geburtsdatum generieren + _, formatted_date = generator.generate_birthday(platform, age) + + return formatted_date \ No newline at end of file diff --git a/utils/email_handler.py b/utils/email_handler.py new file mode 100644 index 0000000..31512fc --- /dev/null +++ b/utils/email_handler.py @@ -0,0 +1,646 @@ +""" +E-Mail-Handler für den Social Media Account Generator. +Verwaltet den Abruf von Bestätigungscodes und E-Mail-Verifizierungen. +""" + +import os +import json +import logging +import time +import imaplib +import email +import re +from typing import Dict, List, Any, Optional, Tuple, Union +from email.header import decode_header +from datetime import datetime, timedelta + +from utils.text_similarity import TextSimilarity + +logger = logging.getLogger("email_handler") + +class EmailHandler: + """ + Handler für den Zugriff auf E-Mail-Dienste und den Abruf von Bestätigungscodes. + """ + + CONFIG_FILE = os.path.join("config", "email_config.json") + + def __init__(self): + """Initialisiert den EmailHandler und lädt die Konfiguration.""" + self.config = self.load_config() + + # Stelle sicher, dass das Konfigurationsverzeichnis existiert + os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True) + + # TextSimilarity-Instanz für Fuzzy-Matching + self.text_similarity = TextSimilarity(default_threshold=0.75) + + # Cache für die letzten erfolgreichen Verbindungsdaten + self.last_connection = None + + # Typische Betreffzeilen für Verifizierungs-E-Mails nach Plattform + self.verification_subjects = { + "instagram": [ + "Bestätige deine E-Mail-Adresse", + "Bestätigungscode für Instagram", + "Dein Instagram-Code", + "Bestätige deinen Instagram-Account", + "Verify your email address", + "Instagram Verification Code", + "Your Instagram Code", + "Verify your Instagram account", + "Instagram-Bestätigungscode", + "Instagram security code" + ], + "facebook": [ + "Bestätigungscode für Facebook", + "Facebook-Bestätigungscode", + "Dein Facebook-Code", + "Facebook Verification Code", + "Your Facebook Code" + ], + "twitter": [ + "Bestätige dein Twitter-Konto", + "Twitter-Bestätigungscode", + "Verify your Twitter account", + "Twitter Verification Code" + ], + "tiktok": [ + "TikTok-Bestätigungscode", + "Bestätige dein TikTok-Konto", + "TikTok Verification Code", + "Verify your TikTok account" + ], + "default": [ + "Bestätigungscode", + "Verification Code", + "Account Verification", + "Konto-Bestätigung", + "Security Code", + "Sicherheitscode" + ] + } + + logger.info("E-Mail-Handler initialisiert") + + def load_config(self) -> Dict[str, Any]: + """ + Lädt die E-Mail-Konfiguration aus der Konfigurationsdatei. + + Returns: + Dict[str, Any]: Die geladene Konfiguration oder Standardwerte + """ + default_config = { + "imap_server": "imap.ionos.de", + "imap_port": 993, + "imap_user": "info@z5m7q9dk3ah2v1plx6ju.com", + "imap_pass": "cz&ie.O9$!:!tYY@" + } + + try: + if os.path.exists(self.CONFIG_FILE): + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + config = json.load(f) + + logger.info("E-Mail-Konfiguration geladen") + return config + else: + # Standardwerte speichern + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(default_config, f, indent=2) + + logger.info("Standard-E-Mail-Konfiguration erstellt") + return default_config + + except Exception as e: + logger.error(f"Fehler beim Laden der E-Mail-Konfiguration: {e}") + return default_config + + def save_config(self) -> bool: + """ + Speichert die aktuelle Konfiguration in die Konfigurationsdatei. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.config, f, indent=2) + + logger.info("E-Mail-Konfiguration gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der E-Mail-Konfiguration: {e}") + return False + + def get_config(self) -> Dict[str, Any]: + """ + Gibt die aktuelle Konfiguration zurück. + + Returns: + Dict[str, Any]: Die aktuelle Konfiguration + """ + return self.config + + def update_config(self, new_config: Dict[str, Any]) -> bool: + """ + Aktualisiert die Konfiguration mit den neuen Werten. + + Args: + new_config: Neue Konfiguration + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Aktuelle Konfiguration speichern + old_config = self.config.copy() + + # Neue Werte übernehmen + self.config.update(new_config) + + # Konfiguration speichern + success = self.save_config() + + if not success: + # Bei Speicherfehler zur alten Konfiguration zurückkehren + self.config = old_config + return False + + return True + except Exception as e: + logger.error(f"Fehler beim Aktualisieren der E-Mail-Konfiguration: {e}") + return False + + def update_credentials(self, username: str, password: str) -> bool: + """ + Aktualisiert nur die Anmeldeinformationen. + + Args: + username: Benutzername + password: Passwort + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + return self.update_config({ + "imap_user": username, + "imap_pass": password + }) + + def update_server(self, server: str, port: int) -> bool: + """ + Aktualisiert nur die Serverinformationen. + + Args: + server: IMAP-Server + port: IMAP-Port + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + return self.update_config({ + "imap_server": server, + "imap_port": port + }) + + def test_connection(self) -> Dict[str, Any]: + """ + Testet die Verbindung zum IMAP-Server. + + Returns: + Dict[str, Any]: Ergebnis des Tests + """ + try: + logger.info(f"Teste Verbindung zu {self.config['imap_server']}:{self.config['imap_port']}") + + # SSL-Verbindung zum IMAP-Server herstellen + mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) + + # Anmelden + mail.login(self.config["imap_user"], self.config["imap_pass"]) + + # Verfügbare Postfächer auflisten + status, mailboxes = mail.list() + + if status == 'OK': + mailbox_count = len(mailboxes) + + # INBOX auswählen + mail.select("INBOX") + + # Abmelden + mail.logout() + + # Verbindungsdaten im Cache speichern + self.last_connection = { + "server": self.config["imap_server"], + "port": self.config["imap_port"], + "username": self.config["imap_user"], + "password": self.config["imap_pass"] + } + + logger.info(f"Verbindungstest erfolgreich: {mailbox_count} Postfächer gefunden") + + return { + "success": True, + "server": self.config["imap_server"], + "port": self.config["imap_port"], + "mailbox_count": mailbox_count + } + else: + logger.error(f"Fehler beim Abrufen der Postfächer: {status}") + mail.logout() + return { + "success": False, + "error": f"Fehler beim Abrufen der Postfächer: {status}" + } + except imaplib.IMAP4.error as e: + logger.error(f"IMAP-Fehler: {e}") + return { + "success": False, + "error": f"IMAP-Fehler: {e}" + } + except Exception as e: + logger.error(f"Allgemeiner Fehler: {e}") + return { + "success": False, + "error": f"Allgemeiner Fehler: {e}" + } + + def search_emails(self, search_criteria: str = "ALL", max_emails: int = 5) -> List[Dict[str, Any]]: + """ + Sucht nach E-Mails mit den angegebenen Kriterien. + + Args: + search_criteria: IMAP-Suchkriterien + max_emails: Maximale Anzahl der abzurufenden E-Mails + + Returns: + List[Dict[str, Any]]: Liste der gefundenen E-Mails + """ + try: + # Verbindung zum IMAP-Server herstellen + mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) + + # Anmelden + mail.login(self.config["imap_user"], self.config["imap_pass"]) + + # INBOX auswählen + mail.select("INBOX") + + # Nach E-Mails suchen + status, data = mail.search(None, search_criteria) + + emails = [] + + if status == 'OK': + # E-Mail-IDs abrufen + email_ids = data[0].split() + + # Newest emails first + email_ids = list(reversed(email_ids)) + + # Begrenze die Anzahl der abzurufenden E-Mails + if max_emails > 0: + email_ids = email_ids[:max_emails] + + for email_id in email_ids: + # E-Mail abrufen + status, data = mail.fetch(email_id, '(RFC822)') + + if status == 'OK': + # E-Mail-Inhalt parsen + raw_email = data[0][1] + msg = email.message_from_bytes(raw_email) + + # Betreff decodieren + subject = decode_header(msg.get("Subject", ""))[0] + if isinstance(subject[0], bytes): + subject = subject[0].decode(subject[1] or 'utf-8', errors='replace') + else: + subject = subject[0] + + # Absender decodieren + from_addr = decode_header(msg.get("From", ""))[0] + if isinstance(from_addr[0], bytes): + from_addr = from_addr[0].decode(from_addr[1] or 'utf-8', errors='replace') + else: + from_addr = from_addr[0] + + # Empfänger decodieren + to_addr = decode_header(msg.get("To", ""))[0] + if isinstance(to_addr[0], bytes): + to_addr = to_addr[0].decode(to_addr[1] or 'utf-8', errors='replace') + else: + to_addr = to_addr[0] + + # Extrahiere E-Mail-Adresse aus dem To-Feld + to_email = self._extract_email_from_addr(to_addr) + + # Datum decodieren + date = msg.get("Date", "") + + # E-Mail-Text extrahieren + body = "" + + if msg.is_multipart(): + for part in msg.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + + if "attachment" not in content_disposition: + if content_type == "text/plain": + try: + # Textinhalt decodieren + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='replace') + break + except: + body = "[Fehler beim Decodieren des Inhalts]" + elif content_type == "text/html" and not body: + try: + # HTML-Inhalt decodieren + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='replace') + except: + body = "[Fehler beim Decodieren des HTML-Inhalts]" + else: + try: + # Einzel-Teil-E-Mail decodieren + charset = msg.get_content_charset() or 'utf-8' + body = msg.get_payload(decode=True).decode(charset, errors='replace') + except: + body = "[Fehler beim Decodieren des Inhalts]" + + # E-Mail-Informationen speichern + email_info = { + "id": email_id.decode(), + "subject": subject, + "from": from_addr, + "to": to_addr, + "to_email": to_email, + "date": date, + "body": body + } + + emails.append(email_info) + + # Abmelden + mail.logout() + + logger.info(f"{len(emails)} E-Mails gefunden") + return emails + + except Exception as e: + logger.error(f"Fehler beim Suchen nach E-Mails: {e}") + return [] + + def _extract_email_from_addr(self, addr_str: str) -> str: + """ + Extrahiert die E-Mail-Adresse aus einem Adressstring im Format 'Name '. + + Args: + addr_str: Adressstring + + Returns: + str: Die extrahierte E-Mail-Adresse oder der ursprüngliche String + """ + # Regulärer Ausdruck für die Extraktion der E-Mail-Adresse + email_pattern = r'?' + match = re.search(email_pattern, addr_str) + + if match: + return match.group(1).lower() + + return addr_str.lower() + + def _is_subject_relevant(self, subject: str, platform: str) -> bool: + """ + Prüft, ob der Betreff relevant für eine Verifizierungs-E-Mail der angegebenen Plattform ist. + Verwendet Fuzzy-Matching für die Erkennung. + + Args: + subject: Betreff der E-Mail + platform: Plattform (instagram, facebook, twitter, etc.) + + Returns: + bool: True, wenn der Betreff relevant ist, False sonst + """ + # Standardschwellenwert für Fuzzy-Matching + threshold = 0.75 + + # Betreffzeilen für die angegebene Plattform und Standard + subject_patterns = self.verification_subjects.get(platform.lower(), []) + subject_patterns += self.verification_subjects["default"] + + # Prüfe auf exakte Übereinstimmung (schneller) + for pattern in subject_patterns: + if pattern.lower() in subject.lower(): + logger.debug(f"Relevanter Betreff gefunden (exakte Übereinstimmung): {subject}") + return True + + # Wenn keine exakte Übereinstimmung, Fuzzy-Matching verwenden + for pattern in subject_patterns: + similarity = self.text_similarity.similarity_ratio(pattern.lower(), subject.lower()) + if similarity >= threshold: + logger.debug(f"Relevanter Betreff gefunden (Fuzzy-Matching, {similarity:.2f}): {subject}") + return True + + # Alternativ: Prüfe, ob der Betreff den Pattern enthält (mit Fuzzy-Matching) + if self.text_similarity.contains_similar_text(subject.lower(), [pattern.lower()], threshold=threshold): + logger.debug(f"Relevanter Betreff gefunden (Fuzzy-Contains): {subject}") + return True + + return False + + def get_verification_code(self, target_email: Optional[str] = None, platform: str = "instagram", + max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]: + """ + Ruft einen Bestätigungscode von einer E-Mail ab. + + Args: + target_email: Ziel-E-Mail-Adresse oder None für alle + platform: Plattform (instagram, facebook, twitter, etc.) + max_attempts: Maximale Anzahl an Versuchen + delay_seconds: Verzögerung zwischen Versuchen in Sekunden + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + logger.info(f"Suche nach Bestätigungscode für {platform} mit E-Mail {target_email or 'alle'}") + + # Domain aus der Ziel-E-Mail-Adresse extrahieren, falls vorhanden + target_domain = None + if target_email and "@" in target_email: + target_domain = target_email.split("@")[-1].lower() + logger.debug(f"Ziel-Domain: {target_domain}") + + # Letzter Tag als Suchkriterium + today = datetime.now() + yesterday = today - timedelta(days=1) + date_str = yesterday.strftime("%d-%b-%Y") + + search_criteria = f'(SINCE "{date_str}")' + + # E-Mail-Abruf mit Wiederholungsversuch + for attempt in range(max_attempts): + logger.debug(f"Versuch {attempt + 1}/{max_attempts}, Abruf neuer E-Mails...") + + # Alle neuen E-Mails abrufen + emails = self.search_emails(search_criteria, max_emails=10) + + # E-Mails filtern und nach Bestätigungscode suchen + for email_info in emails: + # Extrahierte E-Mail-Adresse des Empfängers + to_email = email_info.get("to_email", "").lower() + + # Wenn eine bestimmte Ziel-E-Mail angegeben ist, prüfe auf exakte Übereinstimmung + if target_email and target_email.lower() != to_email: + # Wenn keine exakte Übereinstimmung, prüfe auf Domain-Übereinstimmung (für Catch-All) + if not target_domain or target_domain not in to_email: + logger.debug(f"E-Mail übersprungen: Empfänger {to_email} stimmt nicht mit Ziel {target_email} überein") + continue + + # Betreff auf Relevanz prüfen (mit Fuzzy-Matching) + subject = email_info.get("subject", "") + if not subject or not self._is_subject_relevant(subject, platform): + logger.debug(f"E-Mail übersprungen: Betreff '{subject}' ist nicht relevant") + continue + + # Nach Bestätigungscode im Text suchen + body = email_info.get("body", "") + code = self._extract_verification_code(body, platform) + + if code: + logger.info(f"Bestätigungscode gefunden: {code} (E-Mail an {to_email})") + return code + else: + logger.debug(f"Kein Code in relevanter E-Mail gefunden (Betreff: {subject})") + + # Wenn kein Code gefunden wurde und noch Versuche übrig sind, warten und erneut versuchen + if attempt < max_attempts - 1: + logger.debug(f"Kein Code gefunden, warte {delay_seconds} Sekunden...") + time.sleep(delay_seconds) + + logger.warning("Kein Bestätigungscode gefunden nach allen Versuchen") + return None + + def _extract_verification_code(self, text: str, platform: str = "instagram") -> Optional[str]: + """ + Extrahiert einen Bestätigungscode aus einem Text. + + Args: + text: Zu durchsuchender Text + platform: Plattform (instagram, facebook, twitter, etc.) + + Returns: + Optional[str]: Der gefundene Bestätigungscode oder None + """ + # Plattformspezifische Muster für Bestätigungscodes + patterns = { + "instagram": [ + r"Dein Code ist (\d{6})", + r"Your code is (\d{6})", + r"Bestätigungscode: (\d{6})", + r"Confirmation code: (\d{6})", + r"(\d{6}) ist dein Instagram-Code", + r"(\d{6}) is your Instagram code", + r"Instagram-Code: (\d{6})", + r"Instagram code: (\d{6})", + r"Instagram: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "facebook": [ + r"Dein Code ist (\d{5})", + r"Your code is (\d{5})", + r"Bestätigungscode: (\d{5})", + r"Confirmation code: (\d{5})", + r"Facebook-Code: (\d{5})", + r"Facebook code: (\d{5})", + r"Facebook: (\d{5})", + r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern + ], + "twitter": [ + r"Code: (\d{6})", + r"Verification code: (\d{6})", + r"Twitter-Code: (\d{6})", + r"Twitter code: (\d{6})", + r"Twitter: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "tiktok": [ + r"TikTok-Code: (\d{6})", + r"TikTok code: (\d{6})", + r"TikTok: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "default": [ + r"Code[:\s]*(\d{4,8})", + r"[Vv]erification [Cc]ode[:\s]*(\d{4,8})", + r"[Bb]estätigungscode[:\s]*(\d{4,8})", + r"(\d{4,8}) is your code", + r"(\d{4,8}) ist dein Code", + r"[^\d](\d{6})[^\d]", # 6-stellige Zahl umgeben von Nicht-Ziffern + r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern + ] + } + + # Plattformspezifische Muster verwenden + platform_patterns = patterns.get(platform.lower(), []) + + # Alle Muster dieser Plattform durchsuchen + for pattern in platform_patterns: + match = re.search(pattern, text) + if match: + code = match.group(1) + logger.debug(f"Code gefunden mit Muster '{pattern}': {code}") + return code + + # Wenn keine plattformspezifischen Muster gefunden wurden, Default-Muster verwenden + for pattern in patterns["default"]: + match = re.search(pattern, text) + if match: + code = match.group(1) + logger.debug(f"Code gefunden mit Default-Muster '{pattern}': {code}") + return code + + # Generische Suche nach Zahlen (für die jeweilige Plattform typische Länge) + code_length = 6 # Standard + if platform.lower() == "facebook": + code_length = 5 + + # Suche nach alleinstehenden Zahlen der richtigen Länge + generic_pattern = r"\b(\d{" + str(code_length) + r"})\b" + matches = re.findall(generic_pattern, text) + + if matches: + # Nehme die erste gefundene Zahl + code = matches[0] + logger.debug(f"Code gefunden mit generischem Muster: {code}") + return code + + logger.debug("Kein Code gefunden") + return None + + def get_confirmation_code(self, expected_email: str, search_criteria: str = "ALL", + max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]: + """ + Ruft einen Bestätigungscode von einer E-Mail ab (Kompatibilitätsmethode). + + Args: + expected_email: E-Mail-Adresse, von der der Code erwartet wird + search_criteria: IMAP-Suchkriterien + max_attempts: Maximale Anzahl an Versuchen + delay_seconds: Verzögerung zwischen Versuchen in Sekunden + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + logger.info(f"Suche nach Bestätigungscode für {expected_email}") + + # Vermutete Plattform basierend auf der E-Mail-Adresse oder dem Inhalt + platform = "instagram" # Standard + + # Bestätigungscode abrufen + return self.get_verification_code(expected_email, platform, max_attempts, delay_seconds) \ No newline at end of file diff --git a/utils/human_behavior.py b/utils/human_behavior.py new file mode 100644 index 0000000..af4eb7c --- /dev/null +++ b/utils/human_behavior.py @@ -0,0 +1,488 @@ +""" +Menschliches Verhalten für den Social Media Account Generator. +""" + +import random +import time +import logging +from typing import Optional, Tuple, List, Dict, Any, Callable + +logger = logging.getLogger("human_behavior") + +class HumanBehavior: + """Klasse zur Simulation von menschlichem Verhalten für die Automatisierung.""" + + def __init__(self, speed_factor: float = 1.0, randomness: float = 0.5): + """ + Initialisiert die HumanBehavior-Klasse. + + Args: + speed_factor: Faktormultiplikator für die Geschwindigkeit (höher = schneller) + randomness: Faktor für die Zufälligkeit (0-1, höher = zufälliger) + """ + self.speed_factor = max(0.1, min(10.0, speed_factor)) # Begrenzung auf 0.1-10.0 + self.randomness = max(0.0, min(1.0, randomness)) # Begrenzung auf 0.0-1.0 + + # Typische Verzögerungen (in Sekunden) + self.delays = { + "typing_per_char": 0.05, # Verzögerung pro Zeichen beim Tippen + "mouse_movement": 0.5, # Verzögerung für Mausbewegung + "click": 0.1, # Verzögerung für Mausklick + "page_load": 2.0, # Verzögerung für das Laden einer Seite + "form_fill": 1.0, # Verzögerung zwischen Formularfeldern + "decision": 1.5, # Verzögerung für Entscheidungen + "scroll": 0.3, # Verzögerung für Scrollbewegungen + "verification": 5.0, # Verzögerung für Verifizierungsprozesse + "image_upload": 3.0, # Verzögerung für Bildupload + "navigation": 1.0 # Verzögerung für Navigation + } + + def sleep(self, delay_type: str, multiplier: float = 1.0) -> None: + """ + Pausiert die Ausführung für eine bestimmte Zeit, basierend auf dem Verzögerungstyp. + + Args: + delay_type: Typ der Verzögerung (aus self.delays) + multiplier: Zusätzlicher Multiplikator für die Verzögerung + """ + base_delay = self.delays.get(delay_type, 0.5) # Standardverzögerung, wenn der Typ nicht bekannt ist + + # Berechne die effektive Verzögerung + delay = base_delay * multiplier / self.speed_factor + + # Füge Zufälligkeit hinzu + if self.randomness > 0: + # Variiere die Verzögerung um ±randomness% + variation = 1.0 + (random.random() * 2 - 1) * self.randomness + delay *= variation + + # Stelle sicher, dass die Verzögerung nicht negativ ist + delay = max(0, delay) + + logger.debug(f"Schlafe für {delay:.2f}s ({delay_type})") + time.sleep(delay) + + def random_delay(self, min_seconds: float = 1.0, max_seconds: float = 3.0) -> None: + """ + Führt eine zufällige Wartezeit aus, um menschliches Verhalten zu simulieren. + + Args: + min_seconds: Minimale Wartezeit in Sekunden + max_seconds: Maximale Wartezeit in Sekunden + """ + return self._random_delay(min_seconds, max_seconds) + + def _random_delay(self, min_seconds: float = 1.0, max_seconds: float = 3.0) -> None: + """ + Führt eine zufällige Wartezeit aus, um menschliches Verhalten zu simulieren. + + Args: + min_seconds: Minimale Wartezeit in Sekunden + max_seconds: Maximale Wartezeit in Sekunden + """ + delay = random.uniform(min_seconds, max_seconds) + logger.debug(f"Zufällige Wartezeit: {delay:.2f} Sekunden") + time.sleep(delay) + + def type_text(self, text: str, on_char_typed: Optional[Callable[[str], None]] = None, + error_probability: float = 0.05, correction_probability: float = 0.9) -> str: + """ + Simuliert menschliches Tippen mit möglichen Tippfehlern und Korrekturen. + + Args: + text: Zu tippender Text + on_char_typed: Optionale Funktion, die für jedes getippte Zeichen aufgerufen wird + error_probability: Wahrscheinlichkeit für Tippfehler (0-1) + correction_probability: Wahrscheinlichkeit, Tippfehler zu korrigieren (0-1) + + Returns: + Der tatsächlich getippte Text (mit oder ohne Fehler) + """ + # Anpassen der Fehlerwahrscheinlichkeit basierend auf Zufälligkeit + adjusted_error_prob = error_probability * self.randomness + + result = "" + i = 0 + + while i < len(text): + char = text[i] + + # Potentieller Tippfehler + if random.random() < adjusted_error_prob: + # Auswahl eines Fehlertyps: + # - Falsches Zeichen (Tastatur-Nachbarn) + # - Ausgelassenes Zeichen + # - Doppeltes Zeichen + error_type = random.choices( + ["wrong", "skip", "double"], + weights=[0.6, 0.2, 0.2], + k=1 + )[0] + + if error_type == "wrong": + # Falsches Zeichen tippen (Tastatur-Nachbarn) + keyboard_neighbors = self.get_keyboard_neighbors(char) + if keyboard_neighbors: + wrong_char = random.choice(keyboard_neighbors) + result += wrong_char + if on_char_typed: + on_char_typed(wrong_char) + self.sleep("typing_per_char") + + # Entscheiden, ob der Fehler korrigiert wird + if random.random() < correction_probability: + # Löschen des falschen Zeichens + result = result[:-1] + if on_char_typed: + on_char_typed("\b") # Backspace + self.sleep("typing_per_char", 1.5) # Längere Pause für Korrektur + + # Korrektes Zeichen tippen + result += char + if on_char_typed: + on_char_typed(char) + self.sleep("typing_per_char") + else: + # Wenn keine Nachbarn gefunden werden, normales Zeichen tippen + result += char + if on_char_typed: + on_char_typed(char) + self.sleep("typing_per_char") + + elif error_type == "skip": + # Zeichen auslassen (nichts tun) + pass + + elif error_type == "double": + # Zeichen doppelt tippen + result += char + char + if on_char_typed: + on_char_typed(char) + on_char_typed(char) + self.sleep("typing_per_char") + + # Entscheiden, ob der Fehler korrigiert wird + if random.random() < correction_probability: + # Löschen des doppelten Zeichens + result = result[:-1] + if on_char_typed: + on_char_typed("\b") # Backspace + self.sleep("typing_per_char", 1.2) + else: + # Normales Tippen ohne Fehler + result += char + if on_char_typed: + on_char_typed(char) + self.sleep("typing_per_char") + + i += 1 + + return result + + def get_keyboard_neighbors(self, char: str) -> List[str]: + """ + Gibt die Tastatur-Nachbarn eines Zeichens zurück. + + Args: + char: Das Zeichen, für das Nachbarn gefunden werden sollen + + Returns: + Liste von benachbarten Zeichen + """ + # QWERTY-Tastaturlayout + keyboard_layout = { + "1": ["2", "q"], + "2": ["1", "3", "q", "w"], + "3": ["2", "4", "w", "e"], + "4": ["3", "5", "e", "r"], + "5": ["4", "6", "r", "t"], + "6": ["5", "7", "t", "y"], + "7": ["6", "8", "y", "u"], + "8": ["7", "9", "u", "i"], + "9": ["8", "0", "i", "o"], + "0": ["9", "-", "o", "p"], + "-": ["0", "=", "p", "["], + "=": ["-", "[", "]"], + "q": ["1", "2", "w", "a"], + "w": ["2", "3", "q", "e", "a", "s"], + "e": ["3", "4", "w", "r", "s", "d"], + "r": ["4", "5", "e", "t", "d", "f"], + "t": ["5", "6", "r", "y", "f", "g"], + "y": ["6", "7", "t", "u", "g", "h"], + "u": ["7", "8", "y", "i", "h", "j"], + "i": ["8", "9", "u", "o", "j", "k"], + "o": ["9", "0", "i", "p", "k", "l"], + "p": ["0", "-", "o", "[", "l", ";"], + "[": ["-", "=", "p", "]", ";", "'"], + "]": ["=", "[", "'", "\\"], + "a": ["q", "w", "s", "z"], + "s": ["w", "e", "a", "d", "z", "x"], + "d": ["e", "r", "s", "f", "x", "c"], + "f": ["r", "t", "d", "g", "c", "v"], + "g": ["t", "y", "f", "h", "v", "b"], + "h": ["y", "u", "g", "j", "b", "n"], + "j": ["u", "i", "h", "k", "n", "m"], + "k": ["i", "o", "j", "l", "m", ","], + "l": ["o", "p", "k", ";", ",", "."], + ";": ["p", "[", "l", "'", ".", "/"], + "'": ["[", "]", ";", "/"], + "z": ["a", "s", "x"], + "x": ["s", "d", "z", "c"], + "c": ["d", "f", "x", "v"], + "v": ["f", "g", "c", "b"], + "b": ["g", "h", "v", "n"], + "n": ["h", "j", "b", "m"], + "m": ["j", "k", "n", ","], + ",": ["k", "l", "m", "."], + ".": ["l", ";", ",", "/"], + "/": [";", "'", "."], + " ": ["c", "v", "b", "n", "m"] # Leertaste hat viele benachbarte Tasten + } + + # Für Großbuchstaben die Nachbarn der Kleinbuchstaben verwenden + if char.lower() != char and char.lower() in keyboard_layout: + return [neighbor.upper() if random.choice([True, False]) else neighbor + for neighbor in keyboard_layout[char.lower()]] + + return keyboard_layout.get(char, []) + + def mouse_move(self, from_point: Optional[Tuple[int, int]] = None, + to_point: Tuple[int, int] = (0, 0), + on_move: Optional[Callable[[Tuple[int, int]], None]] = None) -> None: + """ + Simuliert eine menschliche Mausbewegung mit natürlicher Beschleunigung und Verzögerung. + + Args: + from_point: Startpunkt der Bewegung (oder None für aktuelle Position) + to_point: Zielpunkt der Bewegung + on_move: Optionale Funktion, die für jede Zwischenposition aufgerufen wird + """ + # Wenn kein Startpunkt angegeben ist, einen zufälligen verwenden + if from_point is None: + from_point = (random.randint(0, 1000), random.randint(0, 800)) + + # Berechne die Entfernung + dx = to_point[0] - from_point[0] + dy = to_point[1] - from_point[1] + distance = (dx**2 + dy**2)**0.5 + + # Anzahl der Zwischenschritte basierend auf der Entfernung + steps = max(10, int(distance / 10)) + + # Berechne die Bewegungskurve (Bézierkurve) + # Zufällige Kontrollpunkte für eine natürliche Bewegung + control_point_1 = ( + from_point[0] + dx * 0.3 + random.randint(-int(distance/10), int(distance/10)), + from_point[1] + dy * 0.1 + random.randint(-int(distance/10), int(distance/10)) + ) + control_point_2 = ( + from_point[0] + dx * 0.7 + random.randint(-int(distance/10), int(distance/10)), + from_point[1] + dy * 0.9 + random.randint(-int(distance/10), int(distance/10)) + ) + + # Bewegung durchführen + for i in range(steps + 1): + t = i / steps + + # Kubische Bézierkurve + x = (1-t)**3 * from_point[0] + 3*(1-t)**2*t * control_point_1[0] + 3*(1-t)*t**2 * control_point_2[0] + t**3 * to_point[0] + y = (1-t)**3 * from_point[1] + 3*(1-t)**2*t * control_point_1[1] + 3*(1-t)*t**2 * control_point_2[1] + t**3 * to_point[1] + + # Runde auf ganze Zahlen + curr_point = (int(x), int(y)) + + # Callback aufrufen, wenn vorhanden + if on_move: + on_move(curr_point) + + # Verzögerung basierend auf der Position in der Bewegung + # Am Anfang und Ende langsamer, in der Mitte schneller + if i < 0.2 * steps or i > 0.8 * steps: + self.sleep("mouse_movement", 1.5 / steps) + else: + self.sleep("mouse_movement", 1.0 / steps) + + def click(self, double: bool = False, right: bool = False) -> None: + """ + Simuliert einen Mausklick mit menschlicher Verzögerung. + + Args: + double: True für Doppelklick, False für Einzelklick + right: True für Rechtsklick, False für Linksklick + """ + click_type = "right" if right else "left" + click_count = 2 if double else 1 + + for _ in range(click_count): + logger.debug(f"{click_type.capitalize()}-Klick") + self.sleep("click") + + if double and _ == 0: + # Kürzere Pause zwischen Doppelklicks + self.sleep("click", 0.3) + + def scroll(self, direction: str = "down", amount: int = 5, + on_scroll: Optional[Callable[[int], None]] = None) -> None: + """ + Simuliert Scrollen mit menschlicher Verzögerung. + + Args: + direction: "up" oder "down" + amount: Anzahl der Scroll-Ereignisse + on_scroll: Optionale Funktion, die für jedes Scroll-Ereignis aufgerufen wird + """ + if direction not in ["up", "down"]: + logger.warning(f"Ungültige Scrollrichtung: {direction}") + return + + # Vorzeichenwechsel für die Richtung + scroll_factor = -1 if direction == "up" else 1 + + for i in range(amount): + # Zufällige Variation der Scrollmenge + scroll_amount = scroll_factor * (random.randint(1, 3) if self.randomness > 0.5 else 1) + + logger.debug(f"Scrolle {direction} ({scroll_amount})") + + if on_scroll: + on_scroll(scroll_amount) + + # Verzögerung zwischen Scroll-Ereignissen + if i < amount - 1: # Keine Verzögerung nach dem letzten Scroll + self.sleep("scroll") + + def wait_for_page_load(self, multiplier: float = 1.0) -> None: + """ + Wartet eine angemessene Zeit auf das Laden einer Seite. + + Args: + multiplier: Multiplikator für die Standardwartezeit + """ + self.sleep("page_load", multiplier) + + def wait_between_actions(self, action_type: str = "decision", multiplier: float = 1.0) -> None: + """ + Wartet zwischen Aktionen, um menschliches Verhalten zu simulieren. + + Args: + action_type: Art der Aktion + multiplier: Multiplikator für die Standardwartezeit + """ + self.sleep(action_type, multiplier) + + def navigate_sequence(self, steps: int, min_delay: float = 0.5, max_delay: float = 2.0) -> None: + """ + Simuliert eine Sequenz von Navigationsschritten mit variierenden Verzögerungen. + + Args: + steps: Anzahl der Navigationsschritte + min_delay: Minimale Verzögerung zwischen Schritten + max_delay: Maximale Verzögerung zwischen Schritten + """ + for i in range(steps): + # Zufällige Verzögerung zwischen Navigationsschritten + delay = random.uniform(min_delay, max_delay) + + logger.debug(f"Navigationsschritt {i+1}/{steps}, Verzögerung: {delay:.2f}s") + time.sleep(delay / self.speed_factor) + + def human_delay_pattern(self, action_type: str = "default", intensity: str = "medium") -> None: + """ + Erzeugt ein komplexes, menschliches Verzögerungsmuster. + + Args: + action_type: Art der Aktion (entscheidet über Basismuster) + intensity: Intensität des Musters ("low", "medium", "high") + """ + # Verzögerungsmuster basierend auf Aktionstyp und Intensität + patterns = { + "default": { + "low": (0.2, 0.5), + "medium": (0.5, 1.0), + "high": (1.0, 2.0) + }, + "reading": { + "low": (1.0, 2.0), + "medium": (2.0, 4.0), + "high": (3.0, 6.0) + }, + "thinking": { + "low": (1.5, 3.0), + "medium": (3.0, 5.0), + "high": (5.0, 8.0) + }, + "verification": { + "low": (3.0, 5.0), + "medium": (5.0, 8.0), + "high": (8.0, 12.0) + } + } + + # Standardmuster verwenden, wenn nicht bekannt + pattern = patterns.get(action_type, patterns["default"]) + delay_range = pattern.get(intensity, pattern["medium"]) + + # Zufällige Verzögerung im angegebenen Bereich + delay = random.uniform(delay_range[0], delay_range[1]) + + # Anpassung basierend auf Geschwindigkeit und Zufälligkeit + delay = delay / self.speed_factor + + if self.randomness > 0: + # Füge ein zufälliges "Zittern" hinzu + jitter = random.uniform(-0.2, 0.2) * self.randomness * delay + delay += jitter + + logger.debug(f"Menschliche Verzögerung ({action_type}, {intensity}): {delay:.2f}s") + time.sleep(max(0, delay)) + + def simulate_form_filling(self, fields: int, field_callback: Optional[Callable[[int], None]] = None) -> None: + """ + Simuliert das Ausfüllen eines Formulars mit menschlichem Verhalten. + + Args: + fields: Anzahl der auszufüllenden Felder + field_callback: Optionale Funktion, die für jedes Feld aufgerufen wird + """ + for i in range(fields): + logger.debug(f"Fülle Formularfeld {i+1}/{fields} aus") + + if field_callback: + field_callback(i) + + # Verzögerung zwischen Feldern + if i < fields - 1: # Keine Verzögerung nach dem letzten Feld + # Gelegentlich längere Pausen einbauen + if random.random() < 0.2 * self.randomness: + self.human_delay_pattern("thinking", "low") + else: + self.sleep("form_fill") + + def simulate_captcha_solving(self, on_progress: Optional[Callable[[float], None]] = None) -> None: + """ + Simuliert das Lösen eines CAPTCHAs mit menschlichem Verhalten. + + Args: + on_progress: Optionale Funktion, die mit dem Fortschritt (0-1) aufgerufen wird + """ + # Simuliere einen komplexen Prozess mit mehreren Schritten + steps = random.randint(4, 8) + + for i in range(steps): + progress = (i + 1) / steps + + logger.debug(f"CAPTCHA-Lösung Fortschritt: {progress:.0%}") + + if on_progress: + on_progress(progress) + + # Verschiedene Verzögerungsmuster für die einzelnen Schritte + if i == 0: + # Anfängliches Lesen/Verstehen + self.human_delay_pattern("reading", "medium") + elif i == steps - 1: + # Abschließende Überprüfung/Bestätigung + self.human_delay_pattern("verification", "low") + else: + # Auswahl/Interaktion + self.human_delay_pattern("thinking", "medium") \ No newline at end of file diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..d02aefd --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,69 @@ +""" +Logger-Konfiguration für die Social Media Account Generator Anwendung. +""" + +import os +import logging +import sys +from PyQt5.QtWidgets import QTextEdit +from PyQt5.QtGui import QTextCursor + +class LogHandler(logging.Handler): + """Handler, der Logs an ein QTextEdit-Widget sendet.""" + + def __init__(self, text_widget=None): + super().__init__() + self.text_widget = text_widget + if self.text_widget: + self.text_widget.setReadOnly(True) + + def emit(self, record): + msg = self.format(record) + if self.text_widget: + self.text_widget.append(msg) + # Scrolle nach unten + self.text_widget.moveCursor(QTextCursor.End) + +def setup_logger(name="main", level=logging.INFO): + """ + Konfiguriert und gibt einen Logger zurück. + + Args: + name: Name des Loggers + level: Logging-Level + + Returns: + Konfigurierter Logger + """ + logger = logging.getLogger(name) + + # Verhindere doppelte Handler + if logger.handlers: + return logger + + logger.setLevel(level) + + # Datehandler + log_file = os.path.join("logs", f"{name}.log") + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + logger.addHandler(file_handler) + + # Konsolen-Handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + logger.addHandler(console_handler) + + return logger + +def add_gui_handler(logger, text_widget): + """ + Fügt einem Logger einen GUI-Handler hinzu. + + Args: + logger: Logger, dem der Handler hinzugefügt werden soll + text_widget: QTextEdit-Widget für die Ausgabe + """ + gui_handler = LogHandler(text_widget) + gui_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + logger.addHandler(gui_handler) diff --git a/utils/password_generator.py b/utils/password_generator.py new file mode 100644 index 0000000..32cf02f --- /dev/null +++ b/utils/password_generator.py @@ -0,0 +1,338 @@ +""" +Passwortgenerator für den Social Media Account Generator. +""" + +import random +import string +import logging +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("password_generator") + +class PasswordGenerator: + """Klasse zur Generierung sicherer und plattformkonformer Passwörter.""" + + def __init__(self): + """Initialisiert den PasswordGenerator.""" + # Passwort-Richtlinien für verschiedene Plattformen + self.platform_policies = { + "instagram": { + "min_length": 6, + "max_length": 20, + "require_uppercase": False, + "require_lowercase": True, + "require_digits": False, + "require_special": False, + "allowed_special": "", + "disallowed_chars": "" + }, + "facebook": { + "min_length": 8, + "max_length": 20, + "require_uppercase": False, + "require_lowercase": True, + "require_digits": False, + "require_special": False, + "allowed_special": "!@#$%^&*()", + "disallowed_chars": "" + }, + "twitter": { + "min_length": 8, + "max_length": 20, + "require_uppercase": False, + "require_lowercase": True, + "require_digits": False, + "require_special": False, + "allowed_special": "!@#$%^&*()", + "disallowed_chars": "" + }, + "tiktok": { + "min_length": 8, + "max_length": 20, + "require_uppercase": False, + "require_lowercase": True, + "require_digits": True, + "require_special": False, + "allowed_special": "!@#$%^&*()", + "disallowed_chars": "" + }, + "default": { + "min_length": 8, + "max_length": 16, + "require_uppercase": True, + "require_lowercase": True, + "require_digits": True, + "require_special": True, + "allowed_special": "!@#$%^&*()", + "disallowed_chars": "" + } + } + + def get_platform_policy(self, platform: str) -> Dict[str, Any]: + """ + Gibt die Passwort-Richtlinie für eine bestimmte Plattform zurück. + + Args: + platform: Name der Plattform + + Returns: + Dictionary mit der Passwort-Richtlinie + """ + platform = platform.lower() + return self.platform_policies.get(platform, self.platform_policies["default"]) + + def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None: + """ + Setzt oder aktualisiert die Passwort-Richtlinie für eine Plattform. + + Args: + platform: Name der Plattform + policy: Dictionary mit der Passwort-Richtlinie + """ + platform = platform.lower() + self.platform_policies[platform] = policy + logger.info(f"Passwort-Richtlinie für '{platform}' aktualisiert") + + def generate_password(self, platform: str = "default", length: Optional[int] = None, + custom_policy: Optional[Dict[str, Any]] = None) -> str: + """ + Generiert ein Passwort gemäß den Richtlinien. + + Args: + platform: Name der Plattform + length: Optionale Länge des Passworts (überschreibt die Plattformrichtlinie) + custom_policy: Optionale benutzerdefinierte Richtlinie + + Returns: + Generiertes Passwort + """ + # Richtlinie bestimmen + if custom_policy: + policy = custom_policy + else: + policy = self.get_platform_policy(platform) + + # Länge bestimmen + if length: + if length < policy["min_length"]: + logger.warning(f"Angeforderte Länge ({length}) ist kleiner als das Minimum " + f"({policy['min_length']}). Verwende Minimum.") + length = policy["min_length"] + elif length > policy["max_length"]: + logger.warning(f"Angeforderte Länge ({length}) ist größer als das Maximum " + f"({policy['max_length']}). Verwende Maximum.") + length = policy["max_length"] + else: + # Zufällige Länge im erlaubten Bereich + length = random.randint(policy["min_length"], policy["max_length"]) + + # Verfügbare Zeichen bestimmen + available_chars = "" + + if policy["require_lowercase"] or not (policy["require_uppercase"] or + policy["require_digits"] or + policy["require_special"]): + available_chars += string.ascii_lowercase + + if policy["require_uppercase"]: + available_chars += string.ascii_uppercase + + if policy["require_digits"]: + available_chars += string.digits + + if policy["require_special"] and policy["allowed_special"]: + available_chars += policy["allowed_special"] + + # Entferne nicht erlaubte Zeichen + if policy["disallowed_chars"]: + available_chars = "".join(char for char in available_chars + if char not in policy["disallowed_chars"]) + + # Sicherstellen, dass keine leere Zeichenmenge vorliegt + if not available_chars: + logger.error("Keine Zeichen für die Passwortgenerierung verfügbar") + available_chars = string.ascii_lowercase + + # Passwort generieren + password = "".join(random.choice(available_chars) for _ in range(length)) + + # Überprüfen, ob die Anforderungen erfüllt sind + meets_requirements = True + + if policy["require_lowercase"] and not any(char.islower() for char in password): + meets_requirements = False + + if policy["require_uppercase"] and not any(char.isupper() for char in password): + meets_requirements = False + + if policy["require_digits"] and not any(char.isdigit() for char in password): + meets_requirements = False + + if policy["require_special"] and not any(char in policy["allowed_special"] for char in password): + meets_requirements = False + + # Falls die Anforderungen nicht erfüllt sind, erneut generieren + if not meets_requirements: + logger.debug("Generiertes Passwort erfüllt nicht alle Anforderungen, generiere neu") + return self.generate_password(platform, length, custom_policy) + + logger.info(f"Passwort für '{platform}' generiert (Länge: {length})") + + return password + + def generate_platform_password(self, platform: str) -> str: + """ + Generiert ein Passwort für eine bestimmte Plattform. + + Args: + platform: Name der Plattform + + Returns: + Generiertes Passwort + """ + return self.generate_password(platform) + + def generate_strong_password(self, length: int = 16) -> str: + """ + Generiert ein starkes Passwort. + + Args: + length: Länge des Passworts + + Returns: + Generiertes Passwort + """ + custom_policy = { + "min_length": length, + "max_length": length, + "require_uppercase": True, + "require_lowercase": True, + "require_digits": True, + "require_special": True, + "allowed_special": "!@#$%^&*()-_=+[]{}<>,.;:/?|", + "disallowed_chars": "" + } + + return self.generate_password(custom_policy=custom_policy) + + def generate_memorable_password(self, num_words: int = 3, separator: str = "-") -> str: + """ + Generiert ein einprägsames Passwort aus Wörtern und Zahlen. + + Args: + num_words: Anzahl der Wörter + separator: Trennzeichen zwischen den Wörtern + + Returns: + Generiertes Passwort + """ + # Liste von einfachen Wörtern (kann erweitert/angepasst werden) + words = [ + "time", "year", "people", "way", "day", "man", "thing", "woman", "life", "child", + "world", "school", "state", "family", "student", "group", "country", "problem", + "hand", "part", "place", "case", "week", "company", "system", "program", "question", + "work", "government", "number", "night", "point", "home", "water", "room", "mother", + "area", "money", "story", "fact", "month", "lot", "right", "study", "book", "eye", + "job", "word", "business", "issue", "side", "kind", "head", "house", "service", + "friend", "father", "power", "hour", "game", "line", "end", "member", "law", "car", + "city", "name", "team", "minute", "idea", "kid", "body", "back", "parent", "face", + "level", "office", "door", "health", "person", "art", "war", "history", "party", + "result", "change", "morning", "reason", "research", "girl", "guy", "moment", "air", + "teacher", "force", "education" + ] + + # Zufällige Wörter auswählen + selected_words = random.sample(words, num_words) + + # Groß- und Kleinschreibung variieren und Zahlen hinzufügen + for i in range(len(selected_words)): + if random.choice([True, False]): + selected_words[i] = selected_words[i].capitalize() + + # Mit 50% Wahrscheinlichkeit eine Zahl anhängen + if random.choice([True, False]): + selected_words[i] += str(random.randint(0, 9)) + + # Passwort zusammensetzen + password = separator.join(selected_words) + + logger.info(f"Einprägsames Passwort generiert (Länge: {len(password)})") + + return password + + def validate_password(self, password: str, platform: str = "default", + custom_policy: Optional[Dict[str, Any]] = None) -> Tuple[bool, str]: + """ + Überprüft, ob ein Passwort den Richtlinien entspricht. + + Args: + password: Zu überprüfendes Passwort + platform: Name der Plattform + custom_policy: Optionale benutzerdefinierte Richtlinie + + Returns: + (Gültigkeit, Fehlermeldung) + """ + # Richtlinie bestimmen + if custom_policy: + policy = custom_policy + else: + policy = self.get_platform_policy(platform) + + # Prüfungen durchführen + if len(password) < policy["min_length"]: + return False, f"Passwort ist zu kurz (mindestens {policy['min_length']} Zeichen erforderlich)" + + if len(password) > policy["max_length"]: + return False, f"Passwort ist zu lang (maximal {policy['max_length']} Zeichen erlaubt)" + + if policy["require_lowercase"] and not any(char.islower() for char in password): + return False, "Passwort muss mindestens einen Kleinbuchstaben enthalten" + + if policy["require_uppercase"] and not any(char.isupper() for char in password): + return False, "Passwort muss mindestens einen Großbuchstaben enthalten" + + if policy["require_digits"] and not any(char.isdigit() for char in password): + return False, "Passwort muss mindestens eine Ziffer enthalten" + + if policy["require_special"] and not any(char in policy["allowed_special"] for char in password): + return False, f"Passwort muss mindestens ein Sonderzeichen enthalten ({policy['allowed_special']})" + + if policy["disallowed_chars"] and any(char in policy["disallowed_chars"] for char in password): + return False, f"Passwort enthält nicht erlaubte Zeichen ({policy['disallowed_chars']})" + + return True, "Passwort ist gültig" + + +# Kompatibilitätsfunktion für Legacy-Code, der direkt generate_password() importiert +def generate_password(platform: str = "instagram", length: Optional[int] = None) -> str: + """ + Kompatibilitätsfunktion für ältere Codeversionen. + Generiert ein Passwort für die angegebene Plattform. + + Args: + platform: Name der Plattform + length: Optionale Länge des Passworts + + Returns: + Generiertes Passwort + """ + # Einmalige Logger-Warnung, wenn die Legacy-Funktion verwendet wird + logger.warning("Die Funktion generate_password() ist veraltet, bitte verwende stattdessen die PasswordGenerator-Klasse.") + + # Eine Instanz der Generator-Klasse erstellen und die Methode aufrufen + generator = PasswordGenerator() + return generator.generate_password(platform, length) + + +# Weitere Legacy-Funktionen für Kompatibilität +def generate_strong_password(length: int = 16) -> str: + """Legacy-Funktion für Kompatibilität.""" + generator = PasswordGenerator() + return generator.generate_strong_password(length) + + +def generate_memorable_password(num_words: int = 3, separator: str = "-") -> str: + """Legacy-Funktion für Kompatibilität.""" + generator = PasswordGenerator() + return generator.generate_memorable_password(num_words, separator) \ No newline at end of file diff --git a/utils/proxy_rotator.py b/utils/proxy_rotator.py new file mode 100644 index 0000000..40ef930 --- /dev/null +++ b/utils/proxy_rotator.py @@ -0,0 +1,413 @@ +# Path: p:/Chimaira/Code-Playwright/utils/proxy_rotator.py + +""" +Proxy-Rotations- und Verwaltungsfunktionalität. +""" + +import os +import json +import random +import logging +import requests +import time +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("proxy_rotator") + +class ProxyRotator: + """Klasse zur Verwaltung und Rotation von Proxies.""" + + CONFIG_FILE = os.path.join("config", "proxies.json") + + def __init__(self): + """Initialisiert den ProxyRotator und lädt die Konfiguration.""" + self.config = self.load_config() + self.current_proxy = None + self.last_rotation_time = 0 + + # Stelle sicher, dass das Konfigurationsverzeichnis existiert + os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True) + + def load_config(self) -> Dict[str, Any]: + """Lädt die Proxy-Konfiguration aus der Konfigurationsdatei.""" + if not os.path.exists(self.CONFIG_FILE): + return { + "ipv4": [], + "ipv6": [], + "mobile": [], + "mobile_api": { + "marsproxies": "", + "iproyal": "" + }, + "rotation_interval": 300 # 5 Minuten + } + + try: + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + config = json.load(f) + + logger.info(f"Proxy-Konfiguration geladen: {len(config.get('ipv4', []))} IPv4, " + f"{len(config.get('ipv6', []))} IPv6, {len(config.get('mobile', []))} Mobile") + + return config + except Exception as e: + logger.error(f"Fehler beim Laden der Proxy-Konfiguration: {e}") + return { + "ipv4": [], + "ipv6": [], + "mobile": [], + "mobile_api": { + "marsproxies": "", + "iproyal": "" + }, + "rotation_interval": 300 + } + + def save_config(self) -> bool: + """Speichert die Proxy-Konfiguration in die Konfigurationsdatei.""" + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.config, f, indent=2) + + logger.info("Proxy-Konfiguration gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Proxy-Konfiguration: {e}") + return False + + def get_config(self) -> Dict[str, Any]: + """Gibt die aktuelle Konfiguration zurück.""" + return self.config + + def update_config(self, new_config: Dict[str, Any]) -> bool: + """Aktualisiert die Konfiguration mit den neuen Werten.""" + try: + # Aktualisiere nur die bereitgestellten Schlüssel + for key, value in new_config.items(): + self.config[key] = value + + # Konfiguration speichern + return self.save_config() + except Exception as e: + logger.error(f"Fehler beim Aktualisieren der Proxy-Konfiguration: {e}") + return False + + def get_proxies_by_type(self, proxy_type: str) -> List[str]: + """Gibt eine Liste von Proxies des angegebenen Typs zurück.""" + if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]: + logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}") + return [] + + return self.config.get(proxy_type.lower(), []) + + def get_random_proxy(self, proxy_type: str) -> Optional[str]: + """Gibt einen zufälligen Proxy des angegebenen Typs zurück.""" + proxies = self.get_proxies_by_type(proxy_type) + + if not proxies: + logger.warning(f"Keine Proxies vom Typ '{proxy_type}' verfügbar") + return None + + return random.choice(proxies) + + def get_proxy(self, proxy_type=None): + """ + Gibt eine Proxy-Konfiguration für den angegebenen Typ zurück. + + Args: + proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ + + Returns: + Dict mit Proxy-Konfiguration oder None, wenn kein Proxy verfügbar ist + """ + try: + # Wenn kein Proxy-Typ angegeben ist, einen zufälligen verwenden + if proxy_type is None: + available_types = [] + if self.config.get("ipv4"): + available_types.append("ipv4") + if self.config.get("ipv6"): + available_types.append("ipv6") + if self.config.get("mobile"): + available_types.append("mobile") + + if not available_types: + logger.warning("Keine Proxies verfügbar") + return None + + proxy_type = random.choice(available_types) + + # Proxy vom angegebenen Typ holen + proxy_list = self.get_proxies_by_type(proxy_type) + + if not proxy_list: + logger.warning(f"Keine Proxies vom Typ '{proxy_type}' verfügbar") + return None + + # Zufälligen Proxy aus der Liste auswählen + proxy = random.choice(proxy_list) + + # Proxy-URL parsen + parts = proxy.split(":") + + if len(parts) >= 4: + # Format: host:port:username:password + host, port, username, password = parts[:4] + + return { + "server": f"http://{host}:{port}", + "username": username, + "password": password + } + elif len(parts) >= 2: + # Format: host:port + host, port = parts[:2] + + return { + "server": f"http://{host}:{port}" + } + else: + logger.warning(f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}") + return None + + except Exception as e: + logger.error(f"Fehler beim Abrufen des Proxys: {e}") + return None + + def get_next_proxy(self, proxy_type: str, force_rotation: bool = False) -> Optional[str]: + """ + Gibt den nächsten zu verwendenden Proxy zurück, unter Berücksichtigung des Rotationsintervalls. + + Args: + proxy_type: Typ des Proxys (ipv4, ipv6, mobile) + force_rotation: Erzwingt eine Rotation, unabhängig vom Zeitintervall + + Returns: + Proxy-String oder None, wenn kein Proxy verfügbar ist + """ + current_time = time.time() + interval = self.config.get("rotation_interval", 300) # Standardintervall: 5 Minuten + + # Rotation durchführen, wenn das Intervall abgelaufen ist oder erzwungen wird + if force_rotation or self.current_proxy is None or (current_time - self.last_rotation_time) > interval: + self.current_proxy = self.get_random_proxy(proxy_type) + self.last_rotation_time = current_time + + if self.current_proxy: + logger.info(f"Proxy rotiert zu: {self.mask_proxy_credentials(self.current_proxy)}") + + return self.current_proxy + + def test_proxy(self, proxy_type: str) -> Dict[str, Any]: + """ + Testet einen Proxy des angegebenen Typs. + + Args: + proxy_type: Typ des zu testenden Proxys + + Returns: + Dictionary mit Testergebnissen + """ + proxy = self.get_random_proxy(proxy_type) + + if not proxy: + return { + "success": False, + "error": f"Keine Proxies vom Typ '{proxy_type}' verfügbar" + } + + try: + # Proxy-URL parsen + parts = proxy.split(":") + + if len(parts) >= 4: + # Format: host:port:username:password + host, port, username, password = parts[:4] + proxy_url = f"http://{username}:{password}@{host}:{port}" + elif len(parts) >= 2: + # Format: host:port + host, port = parts[:2] + proxy_url = f"http://{host}:{port}" + else: + return { + "success": False, + "error": f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}" + } + + # Proxy-Konfiguration für requests + proxies = { + "http": proxy_url, + "https": proxy_url + } + + # Startzeit für Antwortzeit-Messung + start_time = time.time() + + # Test-Anfrage über den Proxy + response = requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=10) + + # Antwortzeit berechnen + response_time = time.time() - start_time + + if response.status_code == 200: + data = response.json() + ip = data.get("ip", "Unbekannt") + + # Länderinformationen abrufen (optional) + country = self.get_country_for_ip(ip) + + return { + "success": True, + "ip": ip, + "country": country, + "response_time": response_time, + "proxy_type": proxy_type + } + else: + return { + "success": False, + "error": f"Ungültige Antwort: HTTP {response.status_code}" + } + + except requests.exceptions.Timeout: + return { + "success": False, + "error": "Zeitüberschreitung bei der Verbindung" + } + except requests.exceptions.ProxyError: + return { + "success": False, + "error": "Proxy-Fehler: Verbindung abgelehnt oder fehlgeschlagen" + } + except Exception as e: + logger.error(f"Fehler beim Testen des Proxys: {e}") + return { + "success": False, + "error": str(e) + } + + def get_country_for_ip(self, ip: str) -> Optional[str]: + """ + Ermittelt das Land für eine IP-Adresse. + + Args: + ip: IP-Adresse + + Returns: + Ländername oder None im Fehlerfall + """ + try: + response = requests.get(f"https://ipapi.co/{ip}/json/", timeout=5) + + if response.status_code == 200: + data = response.json() + return data.get("country_name") + + return None + except Exception: + return None + + def mask_proxy_credentials(self, proxy: str) -> str: + """ + Maskiert die Anmeldeinformationen in einem Proxy-String für die Protokollierung. + + Args: + proxy: Original-Proxy-String + + Returns: + Maskierter Proxy-String + """ + parts = proxy.split(":") + + if len(parts) >= 4: + # Format: host:port:username:password + host, port = parts[0], parts[1] + return f"{host}:{port}:***:***" + + return proxy + + def add_proxy(self, proxy: str, proxy_type: str) -> bool: + """ + Fügt einen neuen Proxy zur Konfiguration hinzu. + + Args: + proxy: Proxy-String im Format host:port:username:password + proxy_type: Typ des Proxys (ipv4, ipv6, mobile) + + Returns: + True bei Erfolg, False bei Fehler + """ + if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]: + logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}") + return False + + proxy_list = self.config.get(proxy_type.lower(), []) + + if proxy not in proxy_list: + proxy_list.append(proxy) + self.config[proxy_type.lower()] = proxy_list + self.save_config() + + logger.info(f"Proxy hinzugefügt: {self.mask_proxy_credentials(proxy)} (Typ: {proxy_type})") + return True + + return False + + def remove_proxy(self, proxy: str, proxy_type: str) -> bool: + """ + Entfernt einen Proxy aus der Konfiguration. + + Args: + proxy: Proxy-String + proxy_type: Typ des Proxys (ipv4, ipv6, mobile) + + Returns: + True bei Erfolg, False bei Fehler + """ + if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]: + logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}") + return False + + proxy_list = self.config.get(proxy_type.lower(), []) + + if proxy in proxy_list: + proxy_list.remove(proxy) + self.config[proxy_type.lower()] = proxy_list + self.save_config() + + logger.info(f"Proxy entfernt: {self.mask_proxy_credentials(proxy)} (Typ: {proxy_type})") + return True + + return False + + def format_proxy_for_playwright(self, proxy: str) -> Dict[str, str]: + """ + Formatiert einen Proxy-String für die Verwendung mit Playwright. + + Args: + proxy: Proxy-String im Format host:port:username:password + + Returns: + Dictionary mit Playwright-Proxy-Konfiguration + """ + parts = proxy.split(":") + + if len(parts) >= 4: + # Format: host:port:username:password + host, port, username, password = parts[:4] + + return { + "server": f"{host}:{port}", + "username": username, + "password": password + } + elif len(parts) >= 2: + # Format: host:port + host, port = parts[:2] + + return { + "server": f"{host}:{port}" + } + else: + logger.warning(f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}") + return {} \ No newline at end of file diff --git a/utils/text_similarity.py b/utils/text_similarity.py new file mode 100644 index 0000000..d509551 --- /dev/null +++ b/utils/text_similarity.py @@ -0,0 +1,558 @@ +""" +Textähnlichkeits-Funktionen für robustes UI-Element-Matching. +Ermöglicht flexibles Auffinden von UI-Elementen auch bei leichten Textänderungen. +""" + +import re +import logging +from typing import List, Dict, Any, Optional, Tuple, Union, Callable +from difflib import SequenceMatcher + +logger = logging.getLogger("text_similarity") + +class TextSimilarity: + """Klasse für Textähnlichkeitsfunktionen zum robusten UI-Element-Matching.""" + + def __init__(self, default_threshold: float = 0.8): + """ + Initialisiert die TextSimilarity-Klasse. + + Args: + default_threshold: Standardschwellenwert für Ähnlichkeitsprüfungen (0-1) + """ + self.default_threshold = max(0.0, min(1.0, default_threshold)) + + def levenshtein_distance(self, s1: str, s2: str) -> int: + """ + Berechnet die Levenshtein-Distanz zwischen zwei Strings. + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Die Levenshtein-Distanz (kleinere Werte = ähnlichere Strings) + """ + if s1 == s2: + return 0 + + # Strings für die Berechnung vorbereiten + s1 = s1.lower().strip() + s2 = s2.lower().strip() + + # Spezialfall: leere Strings + if len(s1) == 0: return len(s2) + if len(s2) == 0: return len(s1) + + # Initialisiere die Distanzmatrix + matrix = [[0 for x in range(len(s2) + 1)] for x in range(len(s1) + 1)] + + # Fülle die erste Zeile und Spalte + for i in range(len(s1) + 1): + matrix[i][0] = i + for j in range(len(s2) + 1): + matrix[0][j] = j + + # Fülle die Matrix + for i in range(1, len(s1) + 1): + for j in range(1, len(s2) + 1): + cost = 0 if s1[i-1] == s2[j-1] else 1 + matrix[i][j] = min( + matrix[i-1][j] + 1, # Löschen + matrix[i][j-1] + 1, # Einfügen + matrix[i-1][j-1] + cost # Ersetzen + ) + + return matrix[len(s1)][len(s2)] + + def similarity_ratio(self, s1: str, s2: str) -> float: + """ + Berechnet das Ähnlichkeitsverhältnis zwischen zwei Strings (0-1). + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Ähnlichkeitsverhältnis zwischen 0 (unähnlich) und 1 (identisch) + """ + # Strings für Vergleich normalisieren + s1 = s1.lower().strip() + s2 = s2.lower().strip() + + # Leere Strings behandeln + if len(s1) == 0 and len(s2) == 0: + return 1.0 + + # Maximale mögliche Distanz = Summe der Längen beider Strings + max_distance = max(len(s1), len(s2)) + if max_distance == 0: + return 1.0 + + # Levenshtein-Distanz berechnen + distance = self.levenshtein_distance(s1, s2) + + # Ähnlichkeitsverhältnis berechnen + similarity = 1.0 - (distance / max_distance) + + return similarity + + def sequence_matcher_ratio(self, s1: str, s2: str) -> float: + """ + Berechnet das Ähnlichkeitsverhältnis mit Pythons SequenceMatcher. + Oft genauer als einfaches Levenshtein für längere Texte. + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Ähnlichkeitsverhältnis zwischen 0 (unähnlich) und 1 (identisch) + """ + # Strings für Vergleich normalisieren + s1 = s1.lower().strip() + s2 = s2.lower().strip() + + # SequenceMatcher verwenden + return SequenceMatcher(None, s1, s2).ratio() + + def jaro_winkler_similarity(self, s1: str, s2: str) -> float: + """ + Berechnet die Jaro-Winkler-Ähnlichkeit, die Präfixübereinstimmungen berücksichtigt. + Gut für kurze Strings wie Namen oder IDs. + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Ähnlichkeitswert zwischen 0 (unähnlich) und 1 (identisch) + """ + # Strings für Vergleich normalisieren + s1 = s1.lower().strip() + s2 = s2.lower().strip() + + # Identische Strings + if s1 == s2: + return 1.0 + + # Leere Strings behandeln + if len(s1) == 0 or len(s2) == 0: + return 0.0 + + # Berechne die Jaro-Ähnlichkeit + + # Suche nach übereinstimmenden Zeichen innerhalb des Suchradius + search_range = max(len(s1), len(s2)) // 2 - 1 + search_range = max(0, search_range) + + # Initialisiere Übereinstimmungs- und Transpositionszähler + matches = 0 + transpositions = 0 + + # Markiere übereinstimmende Zeichen + s1_matches = [False] * len(s1) + s2_matches = [False] * len(s2) + + # Finde Übereinstimmungen + for i in range(len(s1)): + start = max(0, i - search_range) + end = min(i + search_range + 1, len(s2)) + + for j in range(start, end): + if not s2_matches[j] and s1[i] == s2[j]: + s1_matches[i] = True + s2_matches[j] = True + matches += 1 + break + + # Wenn keine Übereinstimmungen gefunden wurden + if matches == 0: + return 0.0 + + # Zähle Transpositionszeichen + k = 0 + for i in range(len(s1)): + if s1_matches[i]: + while not s2_matches[k]: + k += 1 + if s1[i] != s2[k]: + transpositions += 1 + k += 1 + + # Berechne Jaro-Ähnlichkeit + jaro = ( + matches / len(s1) + + matches / len(s2) + + (matches - transpositions // 2) / matches + ) / 3.0 + + # Berechne Jaro-Winkler-Ähnlichkeit mit Präfixbonus + prefix_len = 0 + max_prefix_len = min(4, min(len(s1), len(s2))) + + # Zähle übereinstimmende Präfixzeichen + while prefix_len < max_prefix_len and s1[prefix_len] == s2[prefix_len]: + prefix_len += 1 + + # Skalierungsfaktor für Präfixanpassung (Standard: 0.1) + scaling_factor = 0.1 + + # Berechne Jaro-Winkler-Ähnlichkeit + jaro_winkler = jaro + prefix_len * scaling_factor * (1 - jaro) + + return jaro_winkler + + def is_similar(self, s1: str, s2: str, threshold: float = None, method: str = "sequence") -> bool: + """ + Prüft, ob zwei Strings ähnlich genug sind, basierend auf einem Schwellenwert. + + Args: + s1: Erster String + s2: Zweiter String + threshold: Ähnlichkeitsschwellenwert (0-1), oder None für Standardwert + method: Ähnlichkeitsmethode ("levenshtein", "sequence", "jaro_winkler") + + Returns: + True, wenn die Strings ähnlich genug sind, False sonst + """ + if threshold is None: + threshold = self.default_threshold + + # Leere oder None-Strings behandeln + s1 = "" if s1 is None else str(s1) + s2 = "" if s2 is None else str(s2) + + # Wenn beide Strings identisch sind + if s1 == s2: + return True + + # Ähnlichkeitsmethode auswählen + if method == "levenshtein": + similarity = self.similarity_ratio(s1, s2) + elif method == "jaro_winkler": + similarity = self.jaro_winkler_similarity(s1, s2) + else: # "sequence" oder andere + similarity = self.sequence_matcher_ratio(s1, s2) + + return similarity >= threshold + + def find_most_similar(self, target: str, candidates: List[str], + method: str = "sequence") -> Tuple[str, float]: + """ + Findet den ähnlichsten String in einer Liste von Kandidaten. + + Args: + target: Zieltext, zu dem der ähnlichste String gefunden werden soll + candidates: Liste von Kandidatenstrings + method: Ähnlichkeitsmethode ("levenshtein", "sequence", "jaro_winkler") + + Returns: + Tuple (ähnlichster String, Ähnlichkeitswert) + """ + if not candidates: + return "", 0.0 + + # Ähnlichkeitsfunktion auswählen + if method == "levenshtein": + similarity_func = self.similarity_ratio + elif method == "jaro_winkler": + similarity_func = self.jaro_winkler_similarity + else: # "sequence" oder andere + similarity_func = self.sequence_matcher_ratio + + # Finde den ähnlichsten Kandidaten + similarities = [(candidate, similarity_func(target, candidate)) for candidate in candidates] + most_similar = max(similarities, key=lambda x: x[1]) + + return most_similar + + def get_similarity_scores(self, target: str, candidates: List[str], + method: str = "sequence") -> Dict[str, float]: + """ + Berechnet Ähnlichkeitswerte für alle Kandidaten. + + Args: + target: Zieltext + candidates: Liste von Kandidatenstrings + method: Ähnlichkeitsmethode + + Returns: + Dictionary mit {Kandidat: Ähnlichkeitswert} + """ + # Ähnlichkeitsfunktion auswählen + if method == "levenshtein": + similarity_func = self.similarity_ratio + elif method == "jaro_winkler": + similarity_func = self.jaro_winkler_similarity + else: # "sequence" oder andere + similarity_func = self.sequence_matcher_ratio + + # Berechne Ähnlichkeiten für alle Kandidaten + return {candidate: similarity_func(target, candidate) for candidate in candidates} + + def words_similarity(self, s1: str, s2: str) -> float: + """ + Berechnet die Ähnlichkeit basierend auf gemeinsamen Wörtern. + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Ähnlichkeitswert zwischen 0 (unähnlich) und 1 (identisch) + """ + # Strings in Wörter zerlegen + words1 = set(re.findall(r'\w+', s1.lower())) + words2 = set(re.findall(r'\w+', s2.lower())) + + # Leere Wortmengen behandeln + if not words1 and not words2: + return 1.0 + if not words1 or not words2: + return 0.0 + + # Berechne Jaccard-Ähnlichkeit + intersection = len(words1.intersection(words2)) + union = len(words1.union(words2)) + + return intersection / union + + def contains_similar_text(self, text: str, patterns: List[str], + threshold: float = None, method: str = "sequence") -> bool: + """ + Prüft, ob ein Text einen der Muster ähnlich enthält. + + Args: + text: Zu durchsuchender Text + patterns: Liste von zu suchenden Mustern + threshold: Ähnlichkeitsschwellenwert + method: Ähnlichkeitsmethode + + Returns: + True, wenn mindestens ein Muster ähnlich genug ist + """ + if threshold is None: + threshold = self.default_threshold + + # Wenn patterns leer ist oder Text None ist + if not patterns or text is None: + return False + + text = str(text).lower() + + for pattern in patterns: + pattern = str(pattern).lower() + + # Prüfe, ob der Text das Muster enthält + if pattern in text: + return True + + # Prüfe Ähnlichkeit mit Wörtern im Text + words = re.findall(r'\w+', text) + for word in words: + if self.is_similar(word, pattern, threshold, method): + return True + + return False + +def fuzzy_find_element(page, text_or_patterns, selector_type="button", threshold=0.8, + method="sequence", wait_time=5000) -> Optional[Any]: + """ + Findet ein Element basierend auf Textähnlichkeit. + + Args: + page: Playwright Page-Objekt + text_or_patterns: Zieltext oder Liste von Texten + selector_type: Art des Elements ("button", "link", "input", "any") + threshold: Ähnlichkeitsschwellenwert + method: Ähnlichkeitsmethode + wait_time: Wartezeit in Millisekunden + + Returns: + Gefundenes Element oder None + """ + similarity = TextSimilarity(threshold) + patterns = [text_or_patterns] if isinstance(text_or_patterns, str) else text_or_patterns + + try: + # Warte, bis die Seite geladen ist + page.wait_for_load_state("domcontentloaded", timeout=wait_time) + + # Selektoren basierend auf dem Element-Typ + if selector_type == "button": + elements = page.query_selector_all("button, input[type='button'], input[type='submit'], [role='button']") + elif selector_type == "link": + elements = page.query_selector_all("a, [role='link']") + elif selector_type == "input": + elements = page.query_selector_all("input, textarea, select") + else: # "any" + elements = page.query_selector_all("*") + + # Keine Elemente gefunden + if not elements: + logger.debug(f"Keine {selector_type}-Elemente auf der Seite gefunden") + return None + + # Für jedes Element den Text und die Ähnlichkeit prüfen + best_match = None + best_similarity = -1 + + for element in elements: + # Text aus verschiedenen Attributen extrahieren + element_text = "" + + # Inneren Text prüfen + inner_text = element.inner_text() + if inner_text and inner_text.strip(): + element_text = inner_text.strip() + + # Value-Attribut prüfen (für Eingabefelder) + if not element_text: + try: + value = element.get_attribute("value") + if value and value.strip(): + element_text = value.strip() + except: + pass + + # Placeholder prüfen + if not element_text: + try: + placeholder = element.get_attribute("placeholder") + if placeholder and placeholder.strip(): + element_text = placeholder.strip() + except: + pass + + # Aria-Label prüfen + if not element_text: + try: + aria_label = element.get_attribute("aria-label") + if aria_label and aria_label.strip(): + element_text = aria_label.strip() + except: + pass + + # Title-Attribut prüfen + if not element_text: + try: + title = element.get_attribute("title") + if title and title.strip(): + element_text = title.strip() + except: + pass + + # Wenn immer noch kein Text gefunden wurde, überspringen + if not element_text: + continue + + # Ähnlichkeit für jeden Pattern prüfen + for pattern in patterns: + sim_score = similarity.sequence_matcher_ratio(pattern, element_text) + + # Ist dieser Match besser als der bisherige beste? + if sim_score > best_similarity and sim_score >= threshold: + best_similarity = sim_score + best_match = element + + # Bei perfekter Übereinstimmung sofort zurückgeben + if sim_score >= 0.99: + logger.info(f"Element mit perfekter Übereinstimmung gefunden: '{element_text}'") + return element + + # Bestes Ergebnis zurückgeben, wenn es über dem Schwellenwert liegt + if best_match: + try: + match_text = best_match.inner_text() or best_match.get_attribute("value") or best_match.get_attribute("placeholder") + logger.info(f"Element mit Ähnlichkeit {best_similarity:.2f} gefunden: '{match_text}'") + except: + logger.info(f"Element mit Ähnlichkeit {best_similarity:.2f} gefunden") + + return best_match + + logger.debug(f"Kein passendes Element für die angegebenen Muster gefunden: {patterns}") + return None + + except Exception as e: + logger.error(f"Fehler beim Suchen nach ähnlichem Element: {e}") + return None + +def find_element_by_text(page, text, exact=False, selector="*", timeout=5000) -> Optional[Any]: + """ + Findet ein Element, das den angegebenen Text enthält oder ihm ähnlich ist. + + Args: + page: Playwright Page-Objekt + text: Zu suchender Text + exact: Ob exakte Übereinstimmung erforderlich ist + selector: CSS-Selektor zum Einschränken der Suche + timeout: Timeout in Millisekunden + + Returns: + Gefundenes Element oder None + """ + try: + if exact: + # Bei exakter Suche XPath verwenden + xpath = f"//{selector}[contains(text(), '{text}') or contains(@value, '{text}') or contains(@placeholder, '{text}')]" + return page.wait_for_selector(xpath, timeout=timeout) + else: + # Bei Ähnlichkeitssuche alle passenden Elemente finden + similarity = TextSimilarity(0.8) # 80% Schwellenwert + + # Warten auf DOM-Bereitschaft + page.wait_for_load_state("domcontentloaded", timeout=timeout) + + # Alle Elemente mit dem angegebenen Selektor finden + elements = page.query_selector_all(selector) + + for element in elements: + # Verschiedene Textattribute prüfen + element_text = element.inner_text() + if not element_text: + element_text = element.get_attribute("value") or "" + if not element_text: + element_text = element.get_attribute("placeholder") or "" + + # Ähnlichkeit prüfen + if similarity.is_similar(text, element_text): + return element + + return None + + except Exception as e: + logger.error(f"Fehler beim Suchen nach Element mit Text '{text}': {e}") + return None + +def click_fuzzy_button(page, button_text, threshold=0.7, timeout=5000) -> bool: + """ + Klickt auf einen Button basierend auf Textähnlichkeit. + + Args: + page: Playwright Page-Objekt + button_text: Text oder Textmuster des Buttons + threshold: Ähnlichkeitsschwellenwert + timeout: Timeout in Millisekunden + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + # Versuche, das Element zu finden + button = fuzzy_find_element(page, button_text, selector_type="button", + threshold=threshold, wait_time=timeout) + + if button: + # Scrolle zum Button und klicke + button.scroll_into_view_if_needed() + button.click() + logger.info(f"Auf Button mit Text ähnlich zu '{button_text}' geklickt") + return True + else: + logger.warning(f"Kein Button mit Text ähnlich zu '{button_text}' gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Klicken auf Button mit Text '{button_text}': {e}") + return False \ No newline at end of file diff --git a/utils/theme_manager.py b/utils/theme_manager.py new file mode 100644 index 0000000..38cbe74 --- /dev/null +++ b/utils/theme_manager.py @@ -0,0 +1,133 @@ +""" +Theme Manager - Verwaltet das Erscheinungsbild der Anwendung (nur Light Mode) +""" + +import os +import json +import logging +from typing import Dict, Any, Optional +from PyQt5.QtWidgets import QApplication +from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtCore import Qt, QSettings + +logger = logging.getLogger("theme_manager") + +class ThemeManager: + """ + Verwaltet das Erscheinungsbild der Anwendung. + """ + + # Themennamen + LIGHT_THEME = "light" + + def __init__(self, app: QApplication): + """ + Initialisiert den ThemeManager. + + Args: + app: Die QApplication-Instanz + """ + self.app = app + self.settings = QSettings("Chimaira", "SocialMediaAccountGenerator") + self.current_theme = self.LIGHT_THEME + + # Basisverzeichnis ermitteln + self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # Stelle sicher, dass die Verzeichnisse existieren + os.makedirs(os.path.join(self.base_dir, "resources", "themes"), exist_ok=True) + os.makedirs(os.path.join(self.base_dir, "resources", "icons"), exist_ok=True) + + # Lade QSS-Dateien für Themes + self.theme_stylesheets = { + self.LIGHT_THEME: self._load_stylesheet("light.qss") + } + + # Wende das Light Theme an + self.apply_theme(self.LIGHT_THEME) + + logger.info(f"ThemeManager initialisiert mit Theme: {self.current_theme}") + + def _load_stylesheet(self, filename: str) -> str: + """Lädt ein QSS-Stylesheet aus einer Datei.""" + try: + stylesheet_path = os.path.join(self.base_dir, "resources", "themes", filename) + if os.path.exists(stylesheet_path): + with open(stylesheet_path, 'r', encoding='utf-8') as f: + return f.read() + else: + logger.warning(f"Stylesheet-Datei nicht gefunden: {stylesheet_path}") + # Erzeuge eine leere Stylesheet-Datei, wenn sie nicht existiert + with open(stylesheet_path, 'w', encoding='utf-8') as f: + f.write("/* Auto-generated empty stylesheet */\n") + return "" + except Exception as e: + logger.error(f"Fehler beim Laden des Stylesheets {filename}: {e}") + return "" + + def apply_theme(self, theme_name: str) -> bool: + """ + Wendet das Light Theme auf die Anwendung an. + + Args: + theme_name: Wird ignoriert, immer Light Theme verwendet + + Returns: + bool: True, wenn das Theme erfolgreich angewendet wurde, sonst False + """ + try: + # Palette für das Light Theme erstellen + palette = QPalette() + + # Light Theme Palette + palette.setColor(QPalette.Window, QColor(240, 240, 240)) + palette.setColor(QPalette.WindowText, Qt.black) + palette.setColor(QPalette.Base, Qt.white) + palette.setColor(QPalette.AlternateBase, QColor(245, 245, 245)) + palette.setColor(QPalette.ToolTipBase, Qt.white) + palette.setColor(QPalette.ToolTipText, Qt.black) + palette.setColor(QPalette.Text, Qt.black) + palette.setColor(QPalette.Button, QColor(240, 240, 240)) + palette.setColor(QPalette.ButtonText, Qt.black) + palette.setColor(QPalette.BrightText, Qt.red) + palette.setColor(QPalette.Link, QColor(0, 0, 255)) + palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) + palette.setColor(QPalette.HighlightedText, Qt.white) + + # Palette auf die Anwendung anwenden + self.app.setPalette(palette) + + # Stylesheet anwenden + self.app.setStyleSheet(self.theme_stylesheets.get(self.LIGHT_THEME, "")) + + # Aktuelles Theme speichern + self.current_theme = self.LIGHT_THEME + self.settings.setValue("theme", self.LIGHT_THEME) + + logger.info(f"Theme '{self.LIGHT_THEME}' erfolgreich angewendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Anwenden des Themes '{self.LIGHT_THEME}': {e}") + return False + + def get_current_theme(self) -> str: + """Gibt den Namen des aktuellen Themes zurück.""" + return self.LIGHT_THEME + + def get_icon_path(self, icon_name: str) -> str: + """ + Gibt den Pfad zum Icon zurück. + + Args: + icon_name: Name des Icons (ohne Dateierweiterung) + + Returns: + str: Pfad zum Icon + """ + # Social Media Icons bleiben unverändert (immer farbig) + if icon_name in ["instagram", "facebook", "twitter", "tiktok", "vk"]: + return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg") + + # Für andere Icons, die möglicherweise Theme-spezifisch sind + return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg") \ No newline at end of file diff --git a/utils/update_checker.py b/utils/update_checker.py new file mode 100644 index 0000000..8ace01a --- /dev/null +++ b/utils/update_checker.py @@ -0,0 +1,733 @@ + +Alle Projekte +Chimaira +Privat +Projektziel Das Hauptziel des Projekts ist die Entwicklung einer benutzerfreundlichen Software zur automatisierten Erstellung von Social-Media-Accounts. Die Anwendung ermöglicht es Benutzern, Konten für verschiedene Plattformen (Instagram, Facebook, Twitter, TikTok) mit minimaler manueller Intervention zu erstellen, zu verwalten und zu exportieren. Kernfunktionalitäten Automatisierte Account-Erstellung: Erstellen von Benutzerkonten für verschiedene Social-Media-Plattformen Proxy-Unterstützung: Verwendung von Proxies für anonyme Verbindungen und zur Umgehung von Einschränkungen E-Mail-Integration: Automatische Verarbeitung von Bestätigungscodes Datenbankintegration: Speichern und Verwalten erstellter Konten Benutzerfreundliche GUI: Intuitive Benutzeroberfläche mit Dark Mode Robuste Automatisierung: OCR-Fallback-Mechanismen für UI-Änderungen Code-Playwright/ # Social-Media-Account-Generator/ ├── config/ # Konfigurationsverzeichnis │ ├── browser_config.json │ ├── email_config.json │ ├── facebook_config.json │ ├── instagram_config.json │ ├── license_config.json │ ├── proxy_config.json │ ├── stealth_config.json │ ├── tiktok_config.json │ ├── twitter_config.json │ ├── update_config.json │ └── user_agents.json ├── controllers/ # Controller-Logik │ ├── account_controller.py │ ├── main_controller.py │ ├── settings_controller.py │ └── platform_controllers/ │ ├── base_controller.py │ ├── instagram_controller.py │ ├── facebook_controller.py (nicht implementiert) │ ├── twitter_controller.py (nicht implementiert) │ └── tiktok_controller.py (nicht implementiert) ├── database/ # Datenbankfunktionalität │ └── db_manager.py ├── licensing/ # Lizenzverwaltung │ ├── license_manager.py │ └── license_validator.py ├── logs/ # Log-Verzeichnis │ └── screenshots/ # Screenshots für OCR-Fallbacks ├── ocr/ # OCR-Funktionalität │ ├── fallback_actions.py │ ├── screenshot.py │ └── text_detector.py ├── resources/ # Ressourcen │ ├── icons/ # Icons für die UI │ │ ├── instagram.svg │ │ ├── facebook.svg │ │ ├── twitter.svg │ │ ├── tiktok.svg │ │ └── [andere Icons] │ └── themes/ # Theme-Ressourcen │ ├── dark.qss │ └── light.qss ├── social_networks/ # Social-Media-Automatisierung │ ├── base_automation.py │ ├── instagram/ │ │ ├── instagram_automation.py │ │ ├── instagram_selectors.py │ │ └── instagram_workflow.py │ ├── facebook/ # Noch nicht implementiert │ ├── twitter/ # Noch nicht implementiert │ └── tiktok/ # Noch nicht implementiert ├── updates/ # Update-Funktionalität │ └── update_checker.py ├── utils/ # Hilfsfunktionen │ ├── birthday_generator.py │ ├── email_handler.py │ ├── human_behavior.py │ ├── logger.py │ ├── password_generator.py │ ├── proxy_rotator.py │ ├── theme_manager.py │ └── username_generator.py ├── views/ # UI-Komponenten │ ├── main_window.py │ ├── platform_selector.py │ └── tabs/ │ ├── about_tab.py │ ├── accounts_tab.py │ ├── generator_tab.py │ └── settings_tab.py ├── browser/ # Browser-Automatisierung │ ├── playwright_manager.py │ └── stealth_config.py └── main.py # Haupteinstiegspunkt Kernstruktur und MVC-Framework: Grundlegendes MVC-Muster mit klarer Trennung von Daten, Ansicht und Logik Signale und Slots für die Kommunikation zwischen Komponenten Zentrale Logging-Funktionalität Benutzeroberfläche: Hauptfenster mit Plattformauswahl Plattformspezifische Tabs (Generator, Konten, Einstellungen, Über) Dark Mode für alle UI-Komponenten Utility-Klassen: Logger mit GUI-Integration Proxy-Rotator mit Testoption E-Mail-Handler für Verifizierungscodes Passwort-, Benutzernamen- und Geburtsdatumsgeneratoren Human-Behavior-Simulation für natürliche Verzögerungen Lizenzmanager Update-Checker Datenbankintegration: SQLite-Datenbankmanager für Account-Speicherung Import- und Exportfunktionen Suchfunktionen Instagram-Integration: Basis-Automation und Instagram-spezifische Logik Account-Generator-Workflow Stealth-Funktionalität zur Umgehung von Bot-Erkennung Noch ausstehende Aufgaben Plattform-Integration: Implementierung der Facebook-Automatisierung Implementierung der Twitter-Automatisierung Implementierung der TikTok-Automatisierung Model-Klassen: Entwicklung der Datenmodelle für Accounts und Plattformen Integration in die Controller-Logik OCR-Integration: Anpassung der OCR-Komponenten an die neue MVC-Struktur Verbesserung der Fallback-Mechanismen für UI-Änderungen Plattformspezifische Controller: Implementierung der Facebook-, Twitter- und TikTok-Controller Anpassung an spezifische Anforderungen jeder Plattform Erweiterte Funktionen: CAPTCHA-Behandlung mit externen Diensten oder manueller Eingabe SMS-Verifizierung mit SMS-Empfangsdiensten Verbesserte Fehlerbehandlung und Wiederherstellung Tests: Entwicklung von Unit-Tests für die Kernkomponenten End-to-End-Tests für den gesamten Workflow Performance-Optimierungen: Multi-Threading für parallele Account-Erstellung Optimierte Ressourcennutzung bei längeren Automatisierungen Zusammenfassung der Refaktorierung Das ursprüngliche, monolithische Design wurde zu einer modularen MVC-Architektur umgestaltet, die folgende Vorteile bietet: Verbesserte Wartbarkeit: Kleinere, spezialisierte Dateien statt einer großen main.py Einfachere Erweiterbarkeit: Neue Plattformen können durch Ableitung von Basisklassen hinzugefügt werden Bessere Testbarkeit: Komponenten können isoliert getestet werden Wiederverwendbarkeit: Gemeinsame Funktionalität in Basisklassen extrahiert Klare Verantwortlichkeiten: Jede Komponente hat eine spezifische Aufgabe Die größte Verbesserung ist die klare Trennung von Benutzeroberfläche, Geschäftslogik und Datenmanagement, was die Wartung und Erweiterung erheblich erleichtert und einen strukturierten Rahmen für die Implementierung weiterer Plattformen schafft. Nächste Schritte Die nächsten unmittelbaren Schritte sind: Implementierung der Datenmodelle zur Vervollständigung der MVC-Struktur Entwicklung der weiteren plattformspezifischen Controller Anpassung der bestehenden Automatisierungslogik an die neue Struktur Erstellung von Grundtests für die Kernfunktionalität. Nimm für die Pfade NIEMALS absolute Pfade, sondern IMMER relative Pfade + + + + +Unbenannt +Letzte Nachricht vor 21 Sekunden +Deprecation of generate_birthday() function +Letzte Nachricht vor 10 Minuten +Playwright Cookie Banner and Date Parsing Issues +Letzte Nachricht vor 30 Minuten +Troubleshooting Python script error with HumanBehavior class +Letzte Nachricht vor 2 Stunden +Troubleshooting Python code error with Instagram automation +Letzte Nachricht vor 2 Stunden +Troubleshooting Instagram Automation Error +Letzte Nachricht vor 3 Stunden +Modular Localization System for Multilingual App +Letzte Nachricht vor 4 Stunden +Instagram Account Creation Error +Letzte Nachricht vor 1 Tag +Code and Icon Structure Review for Social Media Account Generator +Letzte Nachricht vor 1 Tag +Projektwissen +57 % der Kapazität der Wissensdatenbank genutzt + +instagram_automation.py +1.080 Zeilen + +py + + + +instagram_automation.py +1.075 Zeilen + +py + + + +human_behavior.py +488 Zeilen + +py + + + +main_window.py +168 Zeilen + +py + + + +platform_selector.py +96 Zeilen + +py + + + +platform_button.py +77 Zeilen + +py + + + +theme_manager.py +133 Zeilen + +py + + + +main.py +49 Zeilen + +py + + + +main_controller.py +226 Zeilen + +py + + + +light.qss +255 Zeilen + +text + + + +dark.qss +190 Zeilen + +text + + + +theme.json +47 Zeilen + +json + + + +birthday_generator.py +299 Zeilen + +py + + + +username_generator.py +426 Zeilen + +py + + + +stealth_config.py +216 Zeilen + +py + + + +playwright_manager.py +517 Zeilen + +py + + + +user_agents.json +31 Zeilen + +json + + + +update_config.json +9 Zeilen + +json + + + +stealth_config.json +14 Zeilen + +json + + + +proxy_config.json +15 Zeilen + +json + + + +license_config.json +9 Zeilen + +json + + + +email_config.json +6 Zeilen + +json + + + +instagram_controller.py +186 Zeilen + +py + + + +base_controller.py +130 Zeilen + +py + + + +settings_controller.py +295 Zeilen + +py + + + +account_controller.py +150 Zeilen + +py + + + +license_validator.py +304 Zeilen + +py + + + +license_manager.py +450 Zeilen + +py + + + +instagram_workflow.py +315 Zeilen + +py + + + +instagram_selectors.py +113 Zeilen + +py + + + +base_automation.py +329 Zeilen + +py + + + +version.py +193 Zeilen + +py + + + +update_checker.py +411 Zeilen + +py + + + +proxy_rotator.py +347 Zeilen + +py + + + +password_generator.py +338 Zeilen + +py + + + +logger.py +70 Zeilen + +py + + + +email_handler.py +410 Zeilen + +py + + + +about_tab.py +64 Zeilen + +py + + + +settings_tab.py +302 Zeilen + +py + + + +generator_tab.py +278 Zeilen + +py + + + +accounts_tab.py +138 Zeilen + +py + + +update_checker.py + +14.92 KB •411 Zeilen +• +Die Formatierung kann von der Quelle abweichen + +""" +Update-Checking-Funktionalität für den Social Media Account Generator. +""" + +import os +import json +import logging +import requests +import shutil +from datetime import datetime +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("update_checker") + +class UpdateChecker: + """Klasse zum Überprüfen und Herunterladen von Updates.""" + + CONFIG_FILE = os.path.join("config", "app_version.json") + UPDATE_SERVER_URL = "https://api.example.com/updates" # Platzhalter - in der Produktion anpassen + + def __init__(self): + """Initialisiert den UpdateChecker und lädt die Konfiguration.""" + self.version_info = self.load_version_info() + + # Stelle sicher, dass das Konfigurationsverzeichnis existiert + os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True) + + # Updates-Verzeichnis für Downloads + os.makedirs("updates", exist_ok=True) + + def load_version_info(self) -> Dict[str, Any]: + """Lädt die Versionsinformationen aus der Konfigurationsdatei.""" + if not os.path.exists(self.CONFIG_FILE): + default_info = { + "current_version": "1.0.0", + "last_check": "", + "channel": "stable", + "auto_check": True, + "auto_download": False + } + + # Standardwerte speichern + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(default_info, f, indent=2) + except Exception as e: + logger.error(f"Fehler beim Speichern der Standardversionsinformationen: {e}") + + return default_info + + try: + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + version_info = json.load(f) + + logger.info(f"Versionsinformationen geladen: {version_info.get('current_version', 'unbekannt')}") + + return version_info + except Exception as e: + logger.error(f"Fehler beim Laden der Versionsinformationen: {e}") + return { + "current_version": "1.0.0", + "last_check": "", + "channel": "stable", + "auto_check": True, + "auto_download": False + } + + def save_version_info(self) -> bool: + """Speichert die Versionsinformationen in die Konfigurationsdatei.""" + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.version_info, f, indent=2) + + logger.info("Versionsinformationen gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Versionsinformationen: {e}") + return False + + def compare_versions(self, version1: str, version2: str) -> int: + """ + Vergleicht zwei Versionsstrings (semver). + + Args: + version1: Erste Version + version2: Zweite Version + + Returns: + -1, wenn version1 < version2 + 0, wenn version1 == version2 + 1, wenn version1 > version2 + """ + v1_parts = [int(part) for part in version1.split(".")] + v2_parts = [int(part) for part in version2.split(".")] + + # Fülle fehlende Teile mit Nullen auf + while len(v1_parts) < 3: + v1_parts.append(0) + while len(v2_parts) < 3: + v2_parts.append(0) + + # Vergleiche die Teile + for i in range(3): + if v1_parts[i] < v2_parts[i]: + return -1 + elif v1_parts[i] > v2_parts[i]: + return 1 + + return 0 + + def check_for_updates(self, force: bool = False) -> Dict[str, Any]: + """ + Überprüft, ob Updates verfügbar sind. + + Args: + force: Erzwingt eine Überprüfung, auch wenn erst kürzlich geprüft wurde + + Returns: + Dictionary mit Update-Informationen + """ + result = { + "has_update": False, + "current_version": self.version_info["current_version"], + "latest_version": self.version_info["current_version"], + "release_date": "", + "release_notes": "", + "download_url": "", + "error": "" + } + + # Prüfe, ob seit der letzten Überprüfung genügend Zeit vergangen ist (24 Stunden) + if not force and self.version_info.get("last_check"): + try: + last_check = datetime.fromisoformat(self.version_info["last_check"]) + now = datetime.now() + + # Wenn weniger als 24 Stunden seit der letzten Überprüfung vergangen sind + if (now - last_check).total_seconds() < 86400: + logger.info("Update-Überprüfung übersprungen (letzte Überprüfung vor weniger als 24 Stunden)") + return result + except Exception as e: + logger.warning(f"Fehler beim Parsen des letzten Überprüfungsdatums: {e}") + + try: + # Simuliere eine Online-Überprüfung für Entwicklungszwecke + # In der Produktion sollte eine echte API-Anfrage implementiert werden + # response = requests.get( + # f"{self.UPDATE_SERVER_URL}/check", + # params={ + # "version": self.version_info["current_version"], + # "channel": self.version_info["channel"] + # }, + # timeout=10 + # ) + + # For demonstration purposes only + latest_version = "1.1.0" + has_update = self.compare_versions(self.version_info["current_version"], latest_version) < 0 + + if has_update: + result["has_update"] = True + result["latest_version"] = latest_version + result["release_date"] = "2025-05-01" + result["release_notes"] = ( + "Version 1.1.0:\n" + "- Unterstützung für Facebook-Accounts hinzugefügt\n" + "- Verbesserte Proxy-Rotation\n" + "- Bessere Fehlerbehandlung bei der Account-Erstellung\n" + "- Verschiedene Bugfixes und Leistungsverbesserungen" + ) + result["download_url"] = f"{self.UPDATE_SERVER_URL}/download/v1.1.0" + + # Update der letzten Überprüfung + self.version_info["last_check"] = datetime.now().isoformat() + self.save_version_info() + + logger.info(f"Update-Überprüfung abgeschlossen: {result['latest_version']} verfügbar") + + return result + + except requests.RequestException as e: + error_msg = f"Netzwerkfehler bei der Update-Überprüfung: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Update-Überprüfung: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + + def download_update(self, download_url: str, version: str) -> Dict[str, Any]: + """ + Lädt ein Update herunter. + + Args: + download_url: URL zum Herunterladen des Updates + version: Version des Updates + + Returns: + Dictionary mit Download-Informationen + """ + result = { + "success": False, + "file_path": "", + "version": version, + "error": "" + } + + try: + # Zieldateiname erstellen + file_name = f"update_v{version}.zip" + file_path = os.path.join("updates", file_name) + + # Simuliere einen Download für Entwicklungszwecke + # In der Produktion sollte ein echter Download implementiert werden + + # response = requests.get(download_url, stream=True, timeout=60) + # if response.status_code == 200: + # with open(file_path, "wb") as f: + # shutil.copyfileobj(response.raw, f) + + # Simulierter Download (erstelle eine leere Datei) + with open(file_path, "w") as f: + f.write(f"Placeholder for version {version} update") + + result["success"] = True + result["file_path"] = file_path + + logger.info(f"Update v{version} heruntergeladen: {file_path}") + + return result + + except requests.RequestException as e: + error_msg = f"Netzwerkfehler beim Herunterladen des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Herunterladen des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + + def is_update_available(self) -> bool: + """ + Überprüft, ob ein Update verfügbar ist. + + Returns: + True, wenn ein Update verfügbar ist, sonst False + """ + update_info = self.check_for_updates() + return update_info["has_update"] + + def get_current_version(self) -> str: + """ + Gibt die aktuelle Version zurück. + + Returns: + Aktuelle Version + """ + return self.version_info["current_version"] + + def set_current_version(self, version: str) -> bool: + """ + Setzt die aktuelle Version. + + Args: + version: Neue Version + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["current_version"] = version + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen der aktuellen Version: {e}") + return False + + def set_update_channel(self, channel: str) -> bool: + """ + Setzt den Update-Kanal (stable, beta, dev). + + Args: + channel: Update-Kanal + + Returns: + True bei Erfolg, False im Fehlerfall + """ + if channel not in ["stable", "beta", "dev"]: + logger.warning(f"Ungültiger Update-Kanal: {channel}") + return False + + try: + self.version_info["channel"] = channel + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen des Update-Kanals: {e}") + return False + + def get_update_channel(self) -> str: + """ + Gibt den aktuellen Update-Kanal zurück. + + Returns: + Update-Kanal (stable, beta, dev) + """ + return self.version_info.get("channel", "stable") + + def set_auto_check(self, auto_check: bool) -> bool: + """ + Aktiviert oder deaktiviert die automatische Update-Überprüfung. + + Args: + auto_check: True, um automatische Updates zu aktivieren, False zum Deaktivieren + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["auto_check"] = bool(auto_check) + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen der automatischen Update-Überprüfung: {e}") + return False + + def is_auto_check_enabled(self) -> bool: + """ + Überprüft, ob die automatische Update-Überprüfung aktiviert ist. + + Returns: + True, wenn die automatische Update-Überprüfung aktiviert ist, sonst False + """ + return self.version_info.get("auto_check", True) + + def set_auto_download(self, auto_download: bool) -> bool: + """ + Aktiviert oder deaktiviert den automatischen Download von Updates. + + Args: + auto_download: True, um automatische Downloads zu aktivieren, False zum Deaktivieren + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["auto_download"] = bool(auto_download) + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen des automatischen Downloads: {e}") + return False + + def is_auto_download_enabled(self) -> bool: + """ + Überprüft, ob der automatische Download von Updates aktiviert ist. + + Returns: + True, wenn der automatische Download aktiviert ist, sonst False + """ + return self.version_info.get("auto_download", False) + + def apply_update(self, update_file: str) -> Dict[str, Any]: + """ + Wendet ein heruntergeladenes Update an. + + Args: + update_file: Pfad zur Update-Datei + + Returns: + Dictionary mit Informationen über die Anwendung des Updates + """ + result = { + "success": False, + "version": "", + "error": "" + } + + if not os.path.exists(update_file): + result["error"] = f"Update-Datei nicht gefunden: {update_file}" + logger.error(result["error"]) + return result + + try: + # In der Produktion sollte hier die tatsächliche Update-Logik implementiert werden + # 1. Extrahieren des Updates + # 2. Sichern der aktuellen Version + # 3. Anwenden der Änderungen + # 4. Aktualisieren der Versionsinformationen + + # Simuliere ein erfolgreiches Update + logger.info(f"Update aus {update_file} erfolgreich angewendet (simuliert)") + + # Extrahiere Version aus dem Dateinamen + file_name = os.path.basename(update_file) + version_match = re.search(r"v([0-9.]+)", file_name) + + if version_match: + new_version = version_match.group(1) + self.set_current_version(new_version) + result["version"] = new_version + + result["success"] = True + + return result + + except Exception as e: + error_msg = f"Fehler beim Anwenden des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result diff --git a/utils/username_generator.py b/utils/username_generator.py new file mode 100644 index 0000000..bba87ac --- /dev/null +++ b/utils/username_generator.py @@ -0,0 +1,465 @@ +# utils/username_generator.py + +""" +Benutzernamen-Generator für den Social Media Account Generator. +""" + +import random +import string +import re +import logging +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("username_generator") + +class UsernameGenerator: + """Klasse zur Generierung von Benutzernamen für verschiedene Plattformen.""" + + def __init__(self): + """Initialisiert den UsernameGenerator.""" + # Plattformspezifische Richtlinien + self.platform_policies = { + "instagram": { + "min_length": 1, + "max_length": 30, + "allowed_chars": string.ascii_letters + string.digits + "._", + "allowed_start_chars": string.ascii_letters + string.digits, + "allowed_end_chars": string.ascii_letters + string.digits + ".", + "allowed_consecutive_special": False, + "disallowed_words": ["instagram", "admin", "official"], + "auto_suggestions": True + }, + "facebook": { + "min_length": 5, + "max_length": 50, + "allowed_chars": string.ascii_letters + string.digits + ".-", + "allowed_start_chars": string.ascii_letters, + "allowed_end_chars": string.ascii_letters + string.digits, + "allowed_consecutive_special": True, + "disallowed_words": ["facebook", "meta", "admin"], + "auto_suggestions": False + }, + "twitter": { + "min_length": 4, + "max_length": 15, + "allowed_chars": string.ascii_letters + string.digits + "_", + "allowed_start_chars": string.ascii_letters + string.digits, + "allowed_end_chars": string.ascii_letters + string.digits + "_", + "allowed_consecutive_special": True, + "disallowed_words": ["twitter", "admin", "official"], + "auto_suggestions": True + }, + "tiktok": { + "min_length": 2, + "max_length": 24, + "allowed_chars": string.ascii_letters + string.digits + "._", + "allowed_start_chars": string.ascii_letters + string.digits, + "allowed_end_chars": string.ascii_letters + string.digits, + "allowed_consecutive_special": False, + "disallowed_words": ["tiktok", "admin", "official"], + "auto_suggestions": True + }, + "default": { + "min_length": 3, + "max_length": 20, + "allowed_chars": string.ascii_letters + string.digits + "._-", + "allowed_start_chars": string.ascii_letters, + "allowed_end_chars": string.ascii_letters + string.digits, + "allowed_consecutive_special": False, + "disallowed_words": ["admin", "root", "system"], + "auto_suggestions": True + } + } + + # Liste von Adjektiven und Substantiven für zufällige Benutzernamen + self.adjectives = [ + "happy", "sunny", "clever", "brave", "mighty", "gentle", "wild", "calm", "bright", + "quiet", "swift", "bold", "wise", "fancy", "little", "big", "smart", "cool", "hot", + "super", "mega", "epic", "magic", "golden", "silver", "bronze", "shiny", "dark", + "light", "fast", "slow", "strong", "soft", "hard", "sweet", "sour", "tasty", "fresh", + "green", "blue", "red", "purple", "yellow", "orange", "pink", "white", "black" + ] + + self.nouns = [ + "tiger", "eagle", "lion", "wolf", "bear", "fox", "owl", "hawk", "falcon", "dolphin", + "shark", "whale", "turtle", "panda", "koala", "monkey", "cat", "dog", "horse", "pony", + "unicorn", "dragon", "phoenix", "wizard", "knight", "warrior", "ninja", "samurai", + "queen", "king", "prince", "princess", "hero", "legend", "star", "moon", "sun", "sky", + "ocean", "river", "mountain", "forest", "tree", "flower", "rose", "tulip", "daisy" + ] + + # Internationaler Wortschatz (nur mit ASCII-Zeichen) für verschiedene Sprachen + # Jeweils 100 kurze, neutrale Begriffe pro Sprache + self.international_words = { + # Deutsch - 100 kurze, neutrale Begriffe ohne Umlaute oder Sonderzeichen + "de": [ + "wald", "berg", "fluss", "tal", "see", "meer", "boot", "schiff", "haus", "dach", + "tuer", "fenster", "glas", "holz", "stein", "sand", "erde", "weg", "pfad", "strasse", + "auto", "rad", "ball", "spiel", "tisch", "stuhl", "bett", "kissen", "lampe", "licht", + "tag", "nacht", "sonne", "mond", "stern", "himmel", "wolke", "regen", "schnee", "wind", + "baum", "blume", "gras", "blatt", "frucht", "apfel", "brot", "wasser", "milch", "kaffee", + "buch", "brief", "stift", "musik", "lied", "tanz", "film", "bild", "farbe", "kunst", + "hand", "fuss", "kopf", "auge", "ohr", "nase", "mund", "zahn", "haar", "herz", + "zeit", "jahr", "monat", "woche", "tag", "stunde", "minute", "uhr", "zahl", "wort", + "name", "freund", "kind", "tier", "vogel", "fisch", "stadt", "land", "dorf", "garten", + "feld", "werk", "kraft", "geld", "gold", "bank", "markt", "preis", "karte", "punkt" + ], + + # Englisch - 100 kurze, neutrale Begriffe + "en": [ + "wood", "hill", "river", "valley", "lake", "sea", "boat", "ship", "house", "roof", + "door", "window", "glass", "wood", "stone", "sand", "earth", "way", "path", "road", + "car", "wheel", "ball", "game", "table", "chair", "bed", "pillow", "lamp", "light", + "day", "night", "sun", "moon", "star", "sky", "cloud", "rain", "snow", "wind", + "tree", "flower", "grass", "leaf", "fruit", "apple", "bread", "water", "milk", "coffee", + "book", "letter", "pen", "music", "song", "dance", "film", "image", "color", "art", + "hand", "foot", "head", "eye", "ear", "nose", "mouth", "tooth", "hair", "heart", + "time", "year", "month", "week", "day", "hour", "minute", "clock", "number", "word", + "name", "friend", "child", "animal", "bird", "fish", "city", "country", "village", "garden", + "field", "work", "power", "money", "gold", "bank", "market", "price", "card", "point" + ], + + # Französisch - 100 kurze, neutrale Begriffe (ohne Akzente oder Sonderzeichen) + "fr": [ + "bois", "mont", "fleuve", "vallee", "lac", "mer", "bateau", "navire", "maison", "toit", + "porte", "fenetre", "verre", "bois", "pierre", "sable", "terre", "voie", "sentier", "route", + "auto", "roue", "balle", "jeu", "table", "chaise", "lit", "coussin", "lampe", "lumiere", + "jour", "nuit", "soleil", "lune", "etoile", "ciel", "nuage", "pluie", "neige", "vent", + "arbre", "fleur", "herbe", "feuille", "fruit", "pomme", "pain", "eau", "lait", "cafe", + "livre", "lettre", "stylo", "musique", "chanson", "danse", "film", "image", "couleur", "art", + "main", "pied", "tete", "oeil", "oreille", "nez", "bouche", "dent", "cheveu", "coeur", + "temps", "annee", "mois", "semaine", "jour", "heure", "minute", "horloge", "nombre", "mot", + "nom", "ami", "enfant", "animal", "oiseau", "poisson", "ville", "pays", "village", "jardin", + "champ", "travail", "force", "argent", "or", "banque", "marche", "prix", "carte", "point" + ], + + # Spanisch - 100 kurze, neutrale Begriffe (ohne Akzente oder Sonderzeichen) + "es": [ + "bosque", "monte", "rio", "valle", "lago", "mar", "barco", "nave", "casa", "techo", + "puerta", "ventana", "vidrio", "madera", "piedra", "arena", "tierra", "via", "ruta", "calle", + "coche", "rueda", "bola", "juego", "mesa", "silla", "cama", "cojin", "lampara", "luz", + "dia", "noche", "sol", "luna", "estrella", "cielo", "nube", "lluvia", "nieve", "viento", + "arbol", "flor", "hierba", "hoja", "fruta", "manzana", "pan", "agua", "leche", "cafe", + "libro", "carta", "pluma", "musica", "cancion", "baile", "pelicula", "imagen", "color", "arte", + "mano", "pie", "cabeza", "ojo", "oreja", "nariz", "boca", "diente", "pelo", "corazon", + "tiempo", "ano", "mes", "semana", "dia", "hora", "minuto", "reloj", "numero", "palabra", + "nombre", "amigo", "nino", "animal", "ave", "pez", "ciudad", "pais", "pueblo", "jardin", + "campo", "trabajo", "fuerza", "dinero", "oro", "banco", "mercado", "precio", "carta", "punto" + ], + + # Japanisch - 100 kurze, neutrale Begriffe (in romanisierter Form) + "ja": [ + "ki", "yama", "kawa", "tani", "mizu", "umi", "fune", "ie", "yane", "kado", + "mado", "garasu", "ki", "ishi", "suna", "tsuchi", "michi", "kuruma", "wa", "tama", + "asobi", "tsukue", "isu", "neru", "makura", "akari", "hikari", "hi", "yoru", "taiyou", + "tsuki", "hoshi", "sora", "kumo", "ame", "yuki", "kaze", "ki", "hana", "kusa", + "ha", "kudamono", "ringo", "pan", "mizu", "gyunyu", "kohi", "hon", "tegami", "pen", + "ongaku", "uta", "odori", "eiga", "e", "iro", "geijutsu", "te", "ashi", "atama", + "me", "mimi", "hana", "kuchi", "ha", "kami", "kokoro", "jikan", "toshi", "tsuki", + "shukan", "hi", "jikan", "fun", "tokei", "kazu", "kotoba", "namae", "tomodachi", "kodomo", + "doubutsu", "tori", "sakana", "machi", "kuni", "mura", "niwa", "hatake", "shigoto", "chikara", + "okane", "kin", "ginko", "ichiba", "nedan", "kado", "ten", "ai", "heiwa", "yume" + ] + } + + def get_platform_policy(self, platform: str) -> Dict[str, Any]: + """ + Gibt die Benutzernamen-Richtlinie für eine bestimmte Plattform zurück. + + Args: + platform: Name der Plattform + + Returns: + Dictionary mit der Benutzernamen-Richtlinie + """ + platform = platform.lower() + return self.platform_policies.get(platform, self.platform_policies["default"]) + + def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None: + """ + Setzt oder aktualisiert die Benutzernamen-Richtlinie für eine Plattform. + + Args: + platform: Name der Plattform + policy: Dictionary mit der Benutzernamen-Richtlinie + """ + platform = platform.lower() + self.platform_policies[platform] = policy + logger.info(f"Benutzernamen-Richtlinie für '{platform}' aktualisiert") + + def generate_username(self, platform: str = "default", name: Optional[str] = None, + custom_policy: Optional[Dict[str, Any]] = None) -> str: + """ + Generiert einen Benutzernamen gemäß den Richtlinien. + + Args: + platform: Name der Plattform + name: Optionaler vollständiger Name für die Generierung + custom_policy: Optionale benutzerdefinierte Richtlinie + + Returns: + Generierter Benutzername + """ + # Richtlinie bestimmen + if custom_policy: + policy = custom_policy + else: + policy = self.get_platform_policy(platform) + + # Wenn ein Name angegeben ist, versuche einen darauf basierenden Benutzernamen zu erstellen + if name: + return self.generate_from_name(name, policy) + else: + # Zufälligen Benutzernamen erstellen + return self.generate_random_username(policy) + + def generate_from_name(self, name: str, policy: Dict[str, Any]) -> str: + """ + Generiert einen Benutzernamen aus einem vollständigen Namen im Format + Vorname_RandomBegriffAusDerAusgewähltenSprache_GeburtsjahrXX. + + Args: + name: Vollständiger Name + policy: Benutzernamen-Richtlinie + + Returns: + Generierter Benutzername + """ + # Name in Teile zerlegen + parts = name.lower().split() + + # Sonderzeichen und Leerzeichen entfernen + parts = [re.sub(r'[^a-z0-9]', '', part) for part in parts] + parts = [part for part in parts if part] + + if not parts: + # Falls keine gültigen Teile, zufälligen Benutzernamen generieren + return self.generate_random_username(policy) + + # Vorname nehmen + firstname = parts[0] + + # Zufällige Sprache auswählen + available_languages = list(self.international_words.keys()) + chosen_language = random.choice(available_languages) + + # Zufälliges Wort aus der gewählten Sprache wählen + random_word = random.choice(self.international_words[chosen_language]) + + # Geburtsjahr simulieren (zwischen 18 und 40 Jahre alt) + current_year = 2025 # Aktuelle Jahresangabe im Code + birth_year = current_year - random.randint(18, 40) + + # Letzte zwei Ziffern vom Geburtsjahr plus eine Zufallszahl + year_suffix = str(birth_year)[-2:] + str(random.randint(0, 9)) + + # Benutzernamen im neuen Format zusammensetzen + username = f"{firstname}_{random_word}_{year_suffix}" + + # Länge prüfen und anpassen + if len(username) > policy["max_length"]: + # Bei Überlänge, kürze den Mittelteil + max_word_length = policy["max_length"] - len(firstname) - len(year_suffix) - 2 # 2 für die Unterstriche + if max_word_length < 3: # Zu kurz für ein sinnvolles Wort + # Fallback: Nur Vorname + Jahreszahl + username = f"{firstname}_{year_suffix}" + else: + random_word = random_word[:max_word_length] + username = f"{firstname}_{random_word}_{year_suffix}" + + # Überprüfen, ob die Richtlinien erfüllt sind + valid, error_msg = self.validate_username(username, policy=policy) + if not valid: + # Wenn nicht gültig, generiere einen alternativen Namen + logger.debug(f"Generierter Name '{username}' nicht gültig: {error_msg}") + + # Einfachere Variante versuchen + username = f"{firstname}{year_suffix}" + + valid, _ = self.validate_username(username, policy=policy) + if not valid: + # Wenn immer noch nicht gültig, Fallback auf Standard-Generator + return self.generate_random_username(policy) + + logger.info(f"Aus Name generierter Benutzername: {username}") + return username + + def generate_random_username(self, policy: Dict[str, Any]) -> str: + """ + Generiert einen zufälligen Benutzernamen. + + Args: + policy: Benutzernamen-Richtlinie + + Returns: + Generierter Benutzername + """ + # Verschiedene Muster für zufällige Benutzernamen + patterns = [ + # Adjektiv + Substantiv + lambda: random.choice(self.adjectives) + random.choice(self.nouns), + + # Substantiv + Zahlen + lambda: random.choice(self.nouns) + "".join(random.choices(string.digits, k=random.randint(1, 4))), + + # Adjektiv + Substantiv + Zahlen + lambda: random.choice(self.adjectives) + random.choice(self.nouns) + "".join(random.choices(string.digits, k=random.randint(1, 3))), + + # Substantiv + Unterstrich + Substantiv + lambda: random.choice(self.nouns) + ("_" if "_" in policy["allowed_chars"] else "") + random.choice(self.nouns), + + # Benutzer + Zahlen + lambda: "user" + "".join(random.choices(string.digits, k=random.randint(3, 6))) + ] + + # Zufälliges Muster auswählen und Benutzernamen generieren + max_attempts = 10 + for _ in range(max_attempts): + pattern_func = random.choice(patterns) + username = pattern_func() + + # Zu lange Benutzernamen kürzen + if len(username) > policy["max_length"]: + username = username[:policy["max_length"]] + + # Zu kurze Benutzernamen verlängern + if len(username) < policy["min_length"]: + username += "".join(random.choices(string.digits, k=policy["min_length"] - len(username))) + + # Überprüfen, ob der Benutzername den Richtlinien entspricht + valid, _ = self.validate_username(username, policy=policy) + if valid: + logger.info(f"Zufälliger Benutzername generiert: {username}") + return username + + # Fallback: Einfachen Benutzernamen mit Zufallsbuchstaben und Zahlen generieren + length = random.randint(policy["min_length"], min(policy["max_length"], policy["min_length"] + 5)) + username = random.choice(string.ascii_lowercase) # Erster Buchstabe + + allowed_chars = [c for c in policy["allowed_chars"] if c in (string.ascii_lowercase + string.digits)] + username += "".join(random.choice(allowed_chars) for _ in range(length - 1)) + + logger.info(f"Fallback-Benutzername generiert: {username}") + return username + + def suggest_alternatives(self, username: str, platform: str = "default") -> List[str]: + """ + Schlägt alternative Benutzernamen vor, wenn der gewünschte bereits vergeben ist. + + Args: + username: Gewünschter Benutzername + platform: Name der Plattform + + Returns: + Liste mit alternativen Benutzernamen + """ + policy = self.get_platform_policy(platform) + + # Wenn Auto-Suggestions deaktiviert sind, leere Liste zurückgeben + if not policy.get("auto_suggestions", True): + return [] + + alternatives = [] + base_username = username + + # Verschiedene Modifikationen ausprobieren + + # Anhängen von Zahlen + for i in range(5): + suffix = str(random.randint(1, 999)) + alt = base_username + suffix + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Sonderzeichen einfügen + for special in ["_", ".", "-"]: + if special in policy["allowed_chars"]: + alt = base_username + special + str(random.randint(1, 99)) + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Adjektiv voranstellen + for _ in range(2): + prefix = random.choice(self.adjectives) + alt = prefix + base_username + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Buchstaben ersetzen (z.B. 'o' durch '0') + if "0" in policy["allowed_chars"] and "o" in base_username.lower(): + alt = base_username.lower().replace("o", "0") + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Zufällige Buchstaben voranstellen + for _ in range(2): + prefix = "".join(random.choices(string.ascii_lowercase, k=random.randint(1, 3))) + alt = prefix + base_username + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Validiere die alternativen Benutzernamen + valid_alternatives = [] + for alt in alternatives: + valid, _ = self.validate_username(alt, policy=policy) + if valid: + valid_alternatives.append(alt) + + # Zufällige Auswahl aus den gültigen Alternativen (maximal 5) + if len(valid_alternatives) > 5: + valid_alternatives = random.sample(valid_alternatives, 5) + + logger.info(f"{len(valid_alternatives)} alternative Benutzernamen generiert für '{username}'") + + return valid_alternatives + + def validate_username(self, username: str, platform: str = "default", + policy: Optional[Dict[str, Any]] = None) -> Tuple[bool, str]: + """ + Überprüft, ob ein Benutzername den Richtlinien entspricht. + + Args: + username: Zu überprüfender Benutzername + platform: Name der Plattform + policy: Optionale Richtlinie (sonst wird die der Plattform verwendet) + + Returns: + (Gültigkeit, Fehlermeldung) + """ + # Richtlinie bestimmen + if not policy: + policy = self.get_platform_policy(platform) + + # Länge prüfen + if len(username) < policy["min_length"]: + return False, f"Benutzername ist zu kurz (mindestens {policy['min_length']} Zeichen erforderlich)" + + if len(username) > policy["max_length"]: + return False, f"Benutzername ist zu lang (maximal {policy['max_length']} Zeichen erlaubt)" + + # Erlaubte Zeichen prüfen + for char in username: + if char not in policy["allowed_chars"]: + return False, f"Unerlaubtes Zeichen: '{char}'" + + # Anfangszeichen prüfen + if username[0] not in policy["allowed_start_chars"]: + return False, f"Benutzername darf nicht mit '{username[0]}' beginnen" + + # Endzeichen prüfen + if username[-1] not in policy["allowed_end_chars"]: + return False, f"Benutzername darf nicht mit '{username[-1]}' enden" + + # Aufeinanderfolgende Sonderzeichen prüfen + if not policy["allowed_consecutive_special"]: + special_chars = set(policy["allowed_chars"]) - set(string.ascii_letters + string.digits) + for i in range(len(username) - 1): + if username[i] in special_chars and username[i+1] in special_chars: + return False, "Keine aufeinanderfolgenden Sonderzeichen erlaubt" + + # Disallowed words + for word in policy["disallowed_words"]: + if word.lower() in username.lower(): + return False, f"Der Benutzername darf '{word}' nicht enthalten" + + return True, "Benutzername ist gültig" \ No newline at end of file diff --git a/views/__pycache__/about_dialog.cpython-310.pyc b/views/__pycache__/about_dialog.cpython-310.pyc new file mode 100644 index 0000000..a729309 Binary files /dev/null and b/views/__pycache__/about_dialog.cpython-310.pyc differ diff --git a/views/__pycache__/main_window.cpython-310.pyc b/views/__pycache__/main_window.cpython-310.pyc new file mode 100644 index 0000000..8c1155e Binary files /dev/null and b/views/__pycache__/main_window.cpython-310.pyc differ diff --git a/views/__pycache__/main_window.cpython-313.pyc b/views/__pycache__/main_window.cpython-313.pyc new file mode 100644 index 0000000..37dd944 Binary files /dev/null and b/views/__pycache__/main_window.cpython-313.pyc differ diff --git a/views/__pycache__/platform_selector.cpython-310.pyc b/views/__pycache__/platform_selector.cpython-310.pyc new file mode 100644 index 0000000..d321ba0 Binary files /dev/null and b/views/__pycache__/platform_selector.cpython-310.pyc differ diff --git a/views/__pycache__/platform_selector.cpython-313.pyc b/views/__pycache__/platform_selector.cpython-313.pyc new file mode 100644 index 0000000..29a6a05 Binary files /dev/null and b/views/__pycache__/platform_selector.cpython-313.pyc differ diff --git a/views/about_dialog.py b/views/about_dialog.py new file mode 100644 index 0000000..740b4df --- /dev/null +++ b/views/about_dialog.py @@ -0,0 +1,68 @@ +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton +from PyQt5.QtCore import Qt + +from updates.version import get_version + + +class AboutDialog(QDialog): + """Dialog that shows information about the application.""" + + def __init__(self, language_manager=None, parent=None): + super().__init__(parent) + # Remove the standard "?" help button that appears on some platforms + self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + self.language_manager = language_manager + self._setup_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + def _setup_ui(self): + self.setWindowTitle("About") + layout = QVBoxLayout(self) + + self.info_label = QLabel() + self.info_label.setAlignment(Qt.AlignCenter) + self.info_label.setWordWrap(True) + layout.addWidget(self.info_label) + + self.close_button = QPushButton("OK") + self.close_button.clicked.connect(self.accept) + layout.addWidget(self.close_button, alignment=Qt.AlignCenter) + + def update_texts(self): + version_text = ( + self.language_manager.get_text("main.version", f"Version {get_version()}") + if self.language_manager + else f"Version {get_version()}" + ) + lm = self.language_manager + title = "Social Media Account Generator" if not lm else lm.get_text("main.title", "Social Media Account Generator") + support = ( + lm.get_text( + "about_dialog.support", + "Für Support kontaktieren Sie uns unter: support@example.com", + ) + if lm + else "Für Support kontaktieren Sie uns unter: support@example.com" + ) + license_text = ( + lm.get_text( + "about_dialog.license", + "Diese Software ist lizenzpflichtig und darf nur mit gültiger Lizenz verwendet werden.", + ) + if lm + else "Diese Software ist lizenzpflichtig und darf nur mit gültiger Lizenz verwendet werden." + ) + lines = [ + f"

{title}

", + f"

{version_text}

", + "

© 2025 Chimaira

", + f"

{support}

", + f"

{license_text}

", + ] + self.info_label.setText("".join(lines)) + if lm: + self.setWindowTitle(lm.get_text("menu.about", "Über")) + self.close_button.setText(lm.get_text("buttons.ok", "OK")) + diff --git a/views/main_window.py b/views/main_window.py new file mode 100644 index 0000000..9036dc1 --- /dev/null +++ b/views/main_window.py @@ -0,0 +1,235 @@ +# Path: views/main_window.py + +""" +Hauptfenster der Social Media Account Generator Anwendung. +""" + +import os +import logging +from PyQt5.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QStackedWidget, QTabWidget, + QAction, QMessageBox +) +from PyQt5.QtCore import Qt, pyqtSignal, QSize, QFile +from PyQt5.QtGui import QIcon, QFont +from localization.language_manager import LanguageManager + +from views.platform_selector import PlatformSelector +from views.about_dialog import AboutDialog +from utils.logger import add_gui_handler + +logger = logging.getLogger("main") + +class MainWindow(QMainWindow): + """Hauptfenster der Anwendung.""" + + # Signale + platform_selected = pyqtSignal(str) + back_to_selector_requested = pyqtSignal() + theme_toggled = pyqtSignal() + + def __init__(self, theme_manager=None, language_manager=None, db_manager=None): + super().__init__() + + # Theme Manager + self.theme_manager = theme_manager + + # Language Manager + self.language_manager = language_manager + self.db_manager = db_manager + + # Fenstereigenschaften setzen + self.setWindowTitle("Social Media Account Generator") + # Größere Mindest- und Startgröße, damit Plattformnamen + # (z.B. "Twitter" und "VK") nicht abgeschnitten werden und + # Tabelleninhalte genügend Platz haben + self.setMinimumSize(1200, 700) + self.resize(1200, 700) + + # Hauptwidget und Layout + self.central_widget = QWidget() + self.setCentralWidget(self.central_widget) + + # Haupt-Layout + self.main_layout = QVBoxLayout(self.central_widget) + + # Gestapeltes Widget für Anzeige von Plattformwahl und Hauptfunktionen + self.stacked_widget = QStackedWidget() + self.main_layout.addWidget(self.stacked_widget) + + # Plattform-Auswahl-Widget + self.platform_selector = PlatformSelector(self.language_manager, self.db_manager) + self.stacked_widget.addWidget(self.platform_selector) + + # Container für Plattform-spezifische Tabs + self.platform_container = QWidget() + self.platform_layout = QVBoxLayout(self.platform_container) + + # Header-Bereich mit Titel und Zurück-Button + self.header_widget = QWidget() + self.header_layout = QHBoxLayout(self.header_widget) + self.header_layout.setContentsMargins(0, 0, 0, 0) + + # Zurück-Button + self.back_button = QPushButton("↩ Zurück") + self.back_button.setFixedWidth(100) + self.header_layout.addWidget(self.back_button) + + # Plattform-Titel + self.platform_title = QLabel() + title_font = QFont() + title_font.setPointSize(14) + title_font.setBold(True) + self.platform_title.setFont(title_font) + self.platform_title.setAlignment(Qt.AlignCenter) + self.header_layout.addWidget(self.platform_title) + + # Platzhalter für die rechte Seite, um die Zentrierung zu erhalten + spacer = QLabel() + spacer.setFixedWidth(100) + self.header_layout.addWidget(spacer) + + self.platform_layout.addWidget(self.header_widget) + + # Tabs für die Plattform + self.tabs = QTabWidget() + self.platform_layout.addWidget(self.tabs) + + # Stacked Widget hinzufügen + self.stacked_widget.addWidget(self.platform_container) + + # Anfänglich Platform-Selektor anzeigen + self.stacked_widget.setCurrentWidget(self.platform_selector) + + # Statusleiste + self.statusBar().showMessage("Bereit") + + # "Über"-Menü erstellen + if self.language_manager: + self._create_menus() + # Verbinde das Sprachänderungssignal mit der UI-Aktualisierung + self.language_manager.language_changed.connect(self.refresh_language_ui) + + # Verbinde Signale + self.connect_signals() + + def connect_signals(self): + """Verbindet die internen Signale.""" + # Platform-Selector-Signal verbinden + self.platform_selector.platform_selected.connect(self.platform_selected) + + # Zurück-Button-Signal verbinden + self.back_button.clicked.connect(self.back_to_selector_requested) + + def init_platform_ui(self, platform: str, platform_controller): + """Initialisiert die plattformspezifische UI.""" + # Tabs entfernen (falls vorhanden) + while self.tabs.count() > 0: + self.tabs.removeTab(0) + + # Plattform-Titel setzen + gen_text = self.language_manager.get_text("tabs.generator", "Account Generator") if self.language_manager else "Account Generator" + self.platform_title.setText(f"{platform.title()} {gen_text}") + + # Icon laden und anzeigen + if self.theme_manager: + icon_path = self.theme_manager.get_icon_path(platform.lower()) + if os.path.exists(icon_path): + self.setWindowTitle(f"{platform.title()} {gen_text}") + self.setWindowIcon(QIcon(icon_path)) + + # Tabs von den Plattform-Controllern holen und hinzufügen + self.add_platform_tabs(platform_controller) + + def _create_menus(self): + """Erstellt die Menüeinträge für "Über" und Sprachen.""" + + # "Über"-Aktion + self.about_action = QAction(self.language_manager.get_text("menu.about", "Über"), self) + self.menuBar().addAction(self.about_action) + self.about_action.triggered.connect(self._show_about_dialog) + + + def _show_about_dialog(self): + """Öffnet den Über-Dialog.""" + dialog = AboutDialog(self.language_manager, self) + dialog.exec_() + + + def refresh_language_ui(self): + """ + Aktualisiert alle UI-Texte nach einem Sprachwechsel. + Diese Methode wird beim Language-Changed-Signal aufgerufen. + """ + if not self.language_manager: + return + + # Fenstername aktualisieren + self.setWindowTitle(self.language_manager.get_text("main.title", "Social Media Account Generator")) + + # Status-Nachricht aktualisieren + self.statusBar().showMessage(self.language_manager.get_text("status.ready", "Bereit")) + + # Den Zurück-Button aktualisieren + self.back_button.setText(self.language_manager.get_text("buttons.back", "↩ Zurück")) + + # Menüs aktualisieren + self.about_action.setText(self.language_manager.get_text("menu.about", "Über")) + + # Die Platform Selector-View aktualisieren + if hasattr(self.platform_selector, "update_texts"): + self.platform_selector.update_texts() + + # Die aktuelle Plattform-UI aktualisieren, falls vorhanden + current_platform = self.platform_title.text().split()[0].lower() if self.platform_title.text() else None + if current_platform: + gen_text = self.language_manager.get_text("tabs.generator", "Account Generator") + self.platform_title.setText(f"{current_platform.title()} {gen_text}") + + # Tabs aktualisieren + tab_names = { + 0: self.language_manager.get_text("tabs.generator", "Account Generator"), + 1: self.language_manager.get_text("tabs.settings", "Einstellungen") + } + + for i in range(self.tabs.count()): + self.tabs.setTabText(i, tab_names.get(i, self.tabs.tabText(i))) + + # Aktualisierung erzwingen + self.repaint() + + def add_platform_tabs(self, platform_controller): + """Fügt die Tabs vom Plattform-Controller hinzu.""" + # Generator-Tab + if hasattr(platform_controller, "get_generator_tab"): + generator_tab = platform_controller.get_generator_tab() + gen_text = self.language_manager.get_text("tabs.generator", "Account Generator") if self.language_manager else "Account Generator" + self.tabs.addTab(generator_tab, gen_text) + + # Einstellungen-Tab + if hasattr(platform_controller, "get_settings_tab"): + settings_tab = platform_controller.get_settings_tab() + settings_text = self.language_manager.get_text("tabs.settings", "Einstellungen") if self.language_manager else "Einstellungen" + self.tabs.addTab(settings_tab, settings_text) + + + def show_platform_ui(self): + """Zeigt die plattformspezifische UI an.""" + self.stacked_widget.setCurrentWidget(self.platform_container) + + def show_platform_selector(self): + """Zeigt den Plattform-Selektor an.""" + self.stacked_widget.setCurrentWidget(self.platform_selector) + self.setWindowTitle("Social Media Account Generator") + + # Standard-Icon zurücksetzen + self.setWindowIcon(QIcon()) + + def set_status_message(self, message: str): + """Setzt eine Nachricht in der Statusleiste.""" + self.statusBar().showMessage(message) + + def add_log_widget(self, text_widget): + """Fügt einen GUI-Handler zum Logger hinzu.""" + add_gui_handler(logger, text_widget) \ No newline at end of file diff --git a/views/platform_selector.py b/views/platform_selector.py new file mode 100644 index 0000000..be2395e --- /dev/null +++ b/views/platform_selector.py @@ -0,0 +1,148 @@ +# Path: views/platform_selector.py + +""" +Plattformauswahl-Widget für die Social Media Account Generator Anwendung. +""" + +import os +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QGridLayout, QLabel, + QHBoxLayout +) +from PyQt5.QtCore import pyqtSignal, Qt, QSize +from PyQt5.QtGui import QFont + +from views.widgets.platform_button import PlatformButton +from views.widgets.language_dropdown import LanguageDropdown +from views.tabs.accounts_tab import AccountsTab + +class PlatformSelector(QWidget): + """Widget zur Auswahl der Plattform.""" + + # Signal wird ausgelöst, wenn eine Plattform ausgewählt wird + platform_selected = pyqtSignal(str) + + def __init__(self, language_manager=None, db_manager=None): + super().__init__() + self.language_manager = language_manager + self.db_manager = db_manager + self.init_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + main_layout = QHBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + + # ----------------------------- + # Linke Seite: Plattformwahl + # ----------------------------- + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + + self.title_label = QLabel("Social Media Account Generator") + self.title_label.setAlignment(Qt.AlignCenter) + title_font = QFont() + title_font.setPointSize(18) + title_font.setBold(True) + self.title_label.setFont(title_font) + left_layout.addWidget(self.title_label) + + self.subtitle_label = QLabel("Wählen Sie eine Plattform") + self.subtitle_label.setAlignment(Qt.AlignCenter) + subtitle_font = QFont() + subtitle_font.setPointSize(12) + self.subtitle_label.setFont(subtitle_font) + left_layout.addWidget(self.subtitle_label) + + platforms_container = QWidget() + grid_layout = QGridLayout(platforms_container) + grid_layout.setSpacing(40) + + # Definiere verfügbare Plattformen + platforms = [ + {"name": "Instagram", "enabled": True}, + {"name": "Facebook", "enabled": True}, + {"name": "TikTok", "enabled": True}, + {"name": "Twitter", "enabled": True}, + {"name": "VK", "enabled": True} + ] + + # Relativer Pfad zu den Icons - vom aktuellen Verzeichnis aus + current_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(current_dir) + icons_dir = os.path.join(parent_dir, "resources", "icons") + + # Platziere Buttons in einem 2x3 Grid + for i, platform in enumerate(platforms): + row = i // 3 + col = i % 3 + + # Icon-Pfad erstellen + icon_path = os.path.join(icons_dir, f"{platform['name'].lower()}.svg") + + # Verwende das Icon nur, wenn die Datei existiert + if not os.path.exists(icon_path): + icon_path = None + + button = PlatformButton( + platform["name"], + icon_path, + platform["enabled"] + ) + button.clicked.connect(lambda checked=False, p=platform["name"]: self.platform_selected.emit(p.lower())) + grid_layout.addWidget(button, row, col, Qt.AlignCenter) + + left_layout.addWidget(platforms_container) + left_layout.addStretch() + + + # ----------------------------- + # Rechte Seite: Übersicht + # ----------------------------- + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + header_right_layout = QHBoxLayout() + header_right_layout.addStretch() + if self.language_manager: + self.language_dropdown = LanguageDropdown(self.language_manager) + header_right_layout.addWidget(self.language_dropdown) + right_layout.addLayout(header_right_layout) + + self.overview_label = QLabel("Übersicht") + self.overview_label.setAlignment(Qt.AlignCenter) + ov_font = QFont() + ov_font.setPointSize(14) + ov_font.setBold(True) + self.overview_label.setFont(ov_font) + right_layout.addWidget(self.overview_label) + + self.accounts_tab = AccountsTab(None, self.db_manager, self.language_manager) + right_layout.addWidget(self.accounts_tab) + + main_layout.addWidget(left_widget) + main_layout.addWidget(right_widget) + + def load_accounts(self): + """Lädt die Konten in der Übersicht neu.""" + if hasattr(self, "accounts_tab"): + self.accounts_tab.load_accounts() + + def update_texts(self): + """Aktualisiert die Texte gemäß der aktuellen Sprache.""" + if not self.language_manager: + return + + self.title_label.setText( + self.language_manager.get_text("main.title", "Social Media Account Generator") + ) + self.subtitle_label.setText( + self.language_manager.get_text("main.subtitle", "Wählen Sie eine Plattform") + ) + self.overview_label.setText( + self.language_manager.get_text("main.overview", "Übersicht") + ) diff --git a/views/tabs/__pycache__/accounts_tab.cpython-310.pyc b/views/tabs/__pycache__/accounts_tab.cpython-310.pyc new file mode 100644 index 0000000..ab2d08b Binary files /dev/null and b/views/tabs/__pycache__/accounts_tab.cpython-310.pyc differ diff --git a/views/tabs/__pycache__/accounts_tab.cpython-313.pyc b/views/tabs/__pycache__/accounts_tab.cpython-313.pyc new file mode 100644 index 0000000..5375ab5 Binary files /dev/null and b/views/tabs/__pycache__/accounts_tab.cpython-313.pyc differ diff --git a/views/tabs/__pycache__/generator_tab.cpython-310.pyc b/views/tabs/__pycache__/generator_tab.cpython-310.pyc new file mode 100644 index 0000000..a95208f Binary files /dev/null and b/views/tabs/__pycache__/generator_tab.cpython-310.pyc differ diff --git a/views/tabs/__pycache__/generator_tab.cpython-313.pyc b/views/tabs/__pycache__/generator_tab.cpython-313.pyc new file mode 100644 index 0000000..ba31e57 Binary files /dev/null and b/views/tabs/__pycache__/generator_tab.cpython-313.pyc differ diff --git a/views/tabs/__pycache__/settings_tab.cpython-310.pyc b/views/tabs/__pycache__/settings_tab.cpython-310.pyc new file mode 100644 index 0000000..8650488 Binary files /dev/null and b/views/tabs/__pycache__/settings_tab.cpython-310.pyc differ diff --git a/views/tabs/__pycache__/settings_tab.cpython-313.pyc b/views/tabs/__pycache__/settings_tab.cpython-313.pyc new file mode 100644 index 0000000..75d5838 Binary files /dev/null and b/views/tabs/__pycache__/settings_tab.cpython-313.pyc differ diff --git a/views/tabs/accounts_tab.py b/views/tabs/accounts_tab.py new file mode 100644 index 0000000..e49c169 --- /dev/null +++ b/views/tabs/accounts_tab.py @@ -0,0 +1,207 @@ +""" +Tab zur Verwaltung der erstellten Social-Media-Accounts. +""" + +import logging +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, + QTableWidgetItem, QPushButton, QHeaderView, QMessageBox +) +from PyQt5.QtCore import pyqtSignal, Qt + +logger = logging.getLogger("accounts_tab") + +class AccountsTab(QWidget): + """Widget für den Konten-Tab.""" + + # Signale + refresh_requested = pyqtSignal() + export_requested = pyqtSignal() + delete_requested = pyqtSignal(int) # account_id + + def __init__(self, platform_name=None, db_manager=None, language_manager=None): + super().__init__() + self.platform_name = platform_name + self.db_manager = db_manager + self.language_manager = language_manager + self.init_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + # Konten laden, falls db_manager vorhanden + if self.db_manager: + self.load_accounts() + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + layout = QVBoxLayout(self) + + # Konten-Tabelle + self.accounts_table = QTableWidget() + self.accounts_table.setColumnCount(8) + self.accounts_table.setHorizontalHeaderLabels([ + "ID", + "Benutzername", + "Passwort", + "E-Mail", + "Handynummer", + "Name", + "Plattform", + "Erstellt am", + ]) + self.accounts_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + # ID-Spalte verstecken + self.accounts_table.setColumnHidden(0, True) + + layout.addWidget(self.accounts_table) + + # Button-Leiste + button_layout = QHBoxLayout() + + self.refresh_button = QPushButton("Aktualisieren") + self.refresh_button.clicked.connect(self.on_refresh_clicked) + + self.export_button = QPushButton("Exportieren") + self.export_button.clicked.connect(self.on_export_clicked) + + self.delete_button = QPushButton("Löschen") + self.delete_button.clicked.connect(self.on_delete_clicked) + + button_layout.addWidget(self.refresh_button) + button_layout.addWidget(self.export_button) + button_layout.addWidget(self.delete_button) + + layout.addLayout(button_layout) + + def load_accounts(self): + """Lädt Konten aus der Datenbank und zeigt sie in der Tabelle an.""" + try: + if (self.platform_name and str(self.platform_name).lower() not in ["all", ""] + and hasattr(self.db_manager, "get_accounts_by_platform")): + accounts = self.db_manager.get_accounts_by_platform(self.platform_name.lower()) + else: + accounts = self.db_manager.get_all_accounts() + if self.platform_name and str(self.platform_name).lower() not in ["all", ""]: + accounts = [ + acc for acc in accounts + if acc.get("platform", "").lower() == str(self.platform_name).lower() + ] + + self.display_accounts(accounts) + except Exception as e: + logger.error(f"Fehler beim Laden der Konten: {e}") + QMessageBox.critical(self, "Fehler", f"Fehler beim Laden der Konten:\n{str(e)}") + + def display_accounts(self, accounts): + """Zeigt die Konten in der Tabelle an.""" + self.accounts_table.setRowCount(len(accounts)) + + for row, account in enumerate(accounts): + self.accounts_table.setItem(row, 0, QTableWidgetItem(str(account.get("id", "")))) + self.accounts_table.setItem(row, 1, QTableWidgetItem(account.get("username", ""))) + self.accounts_table.setItem(row, 2, QTableWidgetItem(account.get("password", ""))) + self.accounts_table.setItem(row, 3, QTableWidgetItem(account.get("email", ""))) + self.accounts_table.setItem(row, 4, QTableWidgetItem(account.get("phone", ""))) + self.accounts_table.setItem(row, 5, QTableWidgetItem(account.get("full_name", ""))) + self.accounts_table.setItem(row, 6, QTableWidgetItem(account.get("platform", ""))) + self.accounts_table.setItem(row, 7, QTableWidgetItem(account.get("created_at", ""))) + + def on_refresh_clicked(self): + """Wird aufgerufen, wenn der Aktualisieren-Button geklickt wird.""" + self.refresh_requested.emit() + + # Direkt aktualisieren, falls db_manager vorhanden + if self.db_manager: + self.load_accounts() + + def on_export_clicked(self): + """Wird aufgerufen, wenn der Exportieren-Button geklickt wird.""" + self.export_requested.emit() + + def on_delete_clicked(self): + """Wird aufgerufen, wenn der Löschen-Button geklickt wird.""" + selected_rows = self.accounts_table.selectionModel().selectedRows() + if not selected_rows: + title = "Kein Konto ausgewählt" + text = "Bitte wählen Sie ein Konto zum Löschen aus." + if self.language_manager: + title = self.language_manager.get_text( + "accounts_tab.no_selection_title", title + ) + text = self.language_manager.get_text( + "accounts_tab.no_selection_text", text + ) + QMessageBox.warning(self, title, text) + return + + account_id = int(self.accounts_table.item(selected_rows[0].row(), 0).text()) + username = self.accounts_table.item(selected_rows[0].row(), 1).text() + + q_title = "Konto löschen" + q_text = f"Möchten Sie das Konto '{username}' wirklich löschen?" + if self.language_manager: + q_title = self.language_manager.get_text("accounts_tab.delete_title", q_title) + q_text = self.language_manager.get_text( + "accounts_tab.delete_text", q_text + ).format(username=username) + reply = QMessageBox.question( + self, + q_title, + q_text, + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + + if reply == QMessageBox.Yes: + self.delete_requested.emit(account_id) + + # Direkt aktualisieren, falls db_manager vorhanden + if self.db_manager: + success = self.db_manager.delete_account(account_id) + if success: + self.load_accounts() + suc_title = "Erfolg" + suc_text = f"Konto '{username}' wurde gelöscht." + if self.language_manager: + suc_title = self.language_manager.get_text( + "accounts_tab.delete_success_title", suc_title + ) + suc_text = self.language_manager.get_text( + "accounts_tab.delete_success_text", suc_text + ).format(username=username) + QMessageBox.information(self, suc_title, suc_text) + else: + err_title = "Fehler" + err_text = f"Konto '{username}' konnte nicht gelöscht werden." + if self.language_manager: + err_title = self.language_manager.get_text( + "accounts_tab.delete_error_title", err_title + ) + err_text = self.language_manager.get_text( + "accounts_tab.delete_error_text", err_text + ).format(username=username) + QMessageBox.critical(self, err_title, err_text) + + def update_texts(self): + """Aktualisiert UI-Texte gemäß aktueller Sprache.""" + if not self.language_manager: + return + lm = self.language_manager + self.refresh_button.setText(lm.get_text("buttons.refresh", "Aktualisieren")) + self.export_button.setText(lm.get_text("buttons.export", "Exportieren")) + self.delete_button.setText(lm.get_text("buttons.delete", "Löschen")) + headers = lm.get_text( + "accounts_tab.headers", + [ + "ID", + "Benutzername", + "Passwort", + "E-Mail", + "Handynummer", + "Name", + "Plattform", + "Erstellt am", + ], + ) + self.accounts_table.setHorizontalHeaderLabels(headers) diff --git a/views/tabs/generator_tab.py b/views/tabs/generator_tab.py new file mode 100644 index 0000000..bdce6b8 --- /dev/null +++ b/views/tabs/generator_tab.py @@ -0,0 +1,500 @@ +# Pfad: views/tabs/generator_tab.py + +""" +Tab zur Erstellung von Social-Media-Accounts. +""" + +import logging +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, + QGroupBox, QLabel, QLineEdit, QSpinBox, QRadioButton, + QCheckBox, QComboBox, QPushButton, QTextEdit, QProgressBar, + QMessageBox +) +from PyQt5.QtCore import Qt, pyqtSignal + +from utils.logger import add_gui_handler + +logger = logging.getLogger("generator_tab") + +class GeneratorTab(QWidget): + """Widget für den Account-Generator-Tab.""" + + # Signale + start_requested = pyqtSignal(dict) + stop_requested = pyqtSignal() + account_created = pyqtSignal(str, dict) # (platform, account_data) + + def __init__(self, platform_name, language_manager=None): + super().__init__() + self.platform_name = platform_name + self.language_manager = language_manager + self.init_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + layout = QVBoxLayout(self) + + # Formularbereich + self.form_group = QGroupBox() + form_layout = QFormLayout() + self.form_group.setLayout(form_layout) + + # Vorname und Nachname statt vollständigem Namen + self.first_name_label = QLabel() + self.first_name_input = QLineEdit() + form_layout.addRow(self.first_name_label, self.first_name_input) + + self.last_name_label = QLabel() + self.last_name_input = QLineEdit() + form_layout.addRow(self.last_name_label, self.last_name_input) + + # Alter (jetzt mit QLineEdit statt QSpinBox) + self.age_label = QLabel() + self.age_input = QLineEdit() + form_layout.addRow(self.age_label, self.age_input) + + # Registrierungsmethode + self.reg_method_group = QWidget() + reg_method_layout = QHBoxLayout(self.reg_method_group) + reg_method_layout.setContentsMargins(0, 0, 0, 0) + + self.email_radio = QRadioButton() + self.phone_radio = QRadioButton() + self.email_radio.setChecked(True) + + reg_method_layout.addWidget(self.email_radio) + reg_method_layout.addWidget(self.phone_radio) + + self.reg_method_label = QLabel() + form_layout.addRow(self.reg_method_label, self.reg_method_group) + + # Telefonnummer (nur sichtbar, wenn Telefon ausgewählt) + self.phone_label = QLabel() + self.phone_input = QLineEdit() + self.phone_input.setEnabled(False) + form_layout.addRow(self.phone_label, self.phone_input) + + # E-Mail-Domain + self.email_domain_label = QLabel() + self.email_domain_input = QLineEdit("z5m7q9dk3ah2v1plx6ju.com") + form_layout.addRow(self.email_domain_label, self.email_domain_input) + + # Proxy verwenden + self.use_proxy_check = QCheckBox() + self.use_proxy_check.setChecked(True) + + # Proxy-Typ + self.proxy_type_combo = QComboBox() + + proxy_widget = QWidget() + proxy_layout = QHBoxLayout(proxy_widget) + proxy_layout.setContentsMargins(0, 0, 0, 0) + proxy_layout.addWidget(self.use_proxy_check) + self.proxy_type_label = QLabel() + proxy_layout.addWidget(self.proxy_type_label) + proxy_layout.addWidget(self.proxy_type_combo) + proxy_layout.addStretch() + + self.proxy_label = QLabel() + form_layout.addRow(self.proxy_label, proxy_widget) + + # Headless-Modus + self.headless_check = QCheckBox() + form_layout.addRow("", self.headless_check) + + # Debug-Modus + self.debug_check = QCheckBox() + form_layout.addRow("", self.debug_check) + + # Plattformspezifische Parameter hinzufügen + self.add_platform_specific_fields(form_layout) + + # Formular zum Layout hinzufügen + layout.addWidget(self.form_group) + + # Fortschrittsanzeige + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + layout.addWidget(self.progress_bar) + + # Log-Bereich + self.log_group = QGroupBox() + log_layout = QVBoxLayout(self.log_group) + + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + log_layout.addWidget(self.log_text) + + # Log-Handler für das TextEdit + add_gui_handler(logger, self.log_text) + + layout.addWidget(self.log_group) + + # Buttons + button_layout = QHBoxLayout() + + self.start_button = QPushButton() + self.start_button.clicked.connect(self.on_start_clicked) + + self.stop_button = QPushButton() + self.stop_button.clicked.connect(self.on_stop_clicked) + self.stop_button.setEnabled(False) + + button_layout.addWidget(self.start_button) + button_layout.addWidget(self.stop_button) + + layout.addLayout(button_layout) + + # Event-Verbindungen + self.email_radio.toggled.connect(self.toggle_phone_input) + self.phone_radio.toggled.connect(self.toggle_phone_input) + self.use_proxy_check.toggled.connect(self.toggle_proxy_combo) + + def add_platform_specific_fields(self, form_layout): + """ + Fügt plattformspezifische Felder hinzu. + Diese Methode kann in abgeleiteten Klassen überschrieben werden. + + Args: + form_layout: Das Formular-Layout, zu dem die Felder hinzugefügt werden sollen + """ + # In plattformspezifischen Unterklassen überschreiben + platform = self.platform_name.lower() + + if platform == "tiktok": + # Beispiel: Kategorie/Nische für TikTok + self.category_label = QLabel() + self.category_combo = QComboBox() + + categories = [ + "Allgemein", + "Gaming", + "Mode", + "Fitness", + "Reisen", + "Kochen", + "Technologie", + "Bildung", + ] + + if self.language_manager: + categories = [ + self.language_manager.get_text( + "generator_tab.tiktok_category_general", "Allgemein" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_gaming", "Gaming" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_fashion", "Mode" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_fitness", "Fitness" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_travel", "Reisen" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_cooking", "Kochen" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_technology", "Technologie" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_education", "Bildung" + ), + ] + + self.category_label.setText( + self.language_manager.get_text( + "generator_tab.tiktok_category_label", "Kategorie/Nische:" + ) + ) + else: + self.category_label.setText("Kategorie/Nische:") + + self.category_combo.addItems(categories) + form_layout.addRow(self.category_label, self.category_combo) + + elif platform == "twitter": + # Beispiel: Interessen für Twitter + self.interests_combo = QComboBox() + interests = ["Allgemein", "Politik", "Technologie", "Wirtschaft", "Sport", "Unterhaltung", "Wissenschaft"] + self.interests_combo.addItems(interests) + form_layout.addRow("Interessen:", self.interests_combo) + + elif platform == "facebook": + # Beispiel: Datenschutzeinstellungen für Facebook + self.privacy_combo = QComboBox() + privacy_options = ["Öffentlich", "Nur Freunde", "Freunde außer...", "Nur ich"] + self.privacy_combo.addItems(privacy_options) + form_layout.addRow("Datenschutz:", self.privacy_combo) + + def toggle_phone_input(self): + """Aktiviert/Deaktiviert das Telefoneingabefeld basierend auf der Radiobutton-Auswahl.""" + self.phone_input.setEnabled(self.phone_radio.isChecked()) + + def toggle_proxy_combo(self): + """Aktiviert/Deaktiviert die Proxy-Typ-Combobox basierend auf der Checkbox.""" + self.proxy_type_combo.setEnabled(self.use_proxy_check.isChecked()) + + def on_start_clicked(self): + """Wird aufgerufen, wenn der Start-Button geklickt wird.""" + # Parameter sammeln + params = self.get_params() + + # Eingaben validieren + valid, error_msg = self.validate_inputs(params) + if not valid: + self.show_error(error_msg) + return + + # Signal auslösen + self.start_requested.emit(params) + + def on_stop_clicked(self): + """Wird aufgerufen, wenn der Stop-Button geklickt wird.""" + self.stop_requested.emit() + + def get_params(self): + """Sammelt alle Parameter für die Account-Erstellung.""" + # Vorname und Nachname kombinieren für den vollständigen Namen + first_name = self.first_name_input.text().strip() + last_name = self.last_name_input.text().strip() + full_name = f"{first_name} {last_name}" + + params = { + "first_name": first_name, + "last_name": last_name, + "full_name": full_name, + "age_text": self.age_input.text().strip(), # Speichere den Rohtext + "registration_method": "email" if self.email_radio.isChecked() else "phone", + "headless": self.headless_check.isChecked(), + "debug": self.debug_check.isChecked(), + "email_domain": self.email_domain_input.text().strip() + } + + # Telefonnummer (wenn ausgewählt) + if self.phone_radio.isChecked(): + params["phone_number"] = self.phone_input.text().strip() + + # Proxy (wenn aktiviert) + if self.use_proxy_check.isChecked(): + proxy_type = self.proxy_type_combo.currentText().lower() + params["use_proxy"] = True + params["proxy_type"] = proxy_type + else: + params["use_proxy"] = False + + # Plattformspezifische Parameter + additional_params = self.get_platform_specific_params() + if additional_params: + params["additional_params"] = additional_params + + return params + + def validate_inputs(self, params): + """ + Validiert die Eingaben für die Account-Erstellung. + + Args: + params: Die gesammelten Parameter + + Returns: + tuple: (gültig, Fehlermeldung) + """ + # Namen prüfen + if not params.get("first_name"): + msg = "Bitte geben Sie einen Vornamen ein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.first_name_error", + "Bitte geben Sie einen Vornamen ein.", + ) + return False, msg + + if not params.get("last_name"): + msg = "Bitte geben Sie einen Nachnamen ein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.last_name_error", + "Bitte geben Sie einen Nachnamen ein.", + ) + return False, msg + + # Alter prüfen + age_text = params.get("age_text", "") + if not age_text: + msg = "Bitte geben Sie ein Alter ein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.age_empty_error", + "Bitte geben Sie ein Alter ein.", + ) + return False, msg + + # Alter muss eine Zahl sein + try: + age = int(age_text) + params["age"] = age # Füge das konvertierte Alter zu den Parametern hinzu + except ValueError: + msg = "Das Alter muss eine ganze Zahl sein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.age_int_error", + "Das Alter muss eine ganze Zahl sein.", + ) + return False, msg + + # Alter-Bereich prüfen - hier ist die allgemeine Prüfung für 13-99 + # Die plattformspezifische Validierung kann später in den Controllern erfolgen + if age < 13 or age > 99: + msg = "Das Alter muss zwischen 13 und 99 liegen." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.age_range_error", + "Das Alter muss zwischen 13 und 99 liegen.", + ) + return False, msg + + # Telefonnummer prüfen, falls erforderlich + if params.get("registration_method") == "phone" and not params.get("phone_number"): + msg = "Bitte geben Sie eine Telefonnummer ein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.phone_error", + "Bitte geben Sie eine Telefonnummer ein.", + ) + return False, msg + + return True, "" + + def get_platform_specific_params(self): + """ + Gibt plattformspezifische Parameter zurück. + Diese Methode kann in abgeleiteten Klassen überschrieben werden. + + Returns: + dict: Plattformspezifische Parameter + """ + # In plattformspezifischen Unterklassen überschreiben + platform = self.platform_name.lower() + additional_params = {} + + if platform == "tiktok" and hasattr(self, "category_combo"): + additional_params["category"] = self.category_combo.currentText() + + elif platform == "twitter" and hasattr(self, "interests_combo"): + additional_params["interests"] = self.interests_combo.currentText() + + elif platform == "facebook" and hasattr(self, "privacy_combo"): + additional_params["privacy"] = self.privacy_combo.currentText() + + return additional_params + + def set_running(self, running: bool): + """Setzt den Status auf 'Wird ausgeführt' oder 'Bereit'.""" + self.start_button.setEnabled(not running) + self.stop_button.setEnabled(running) + + def clear_log(self): + """Löscht den Log-Bereich.""" + self.log_text.clear() + + def add_log(self, message: str): + """Fügt eine Nachricht zum Log-Bereich hinzu.""" + self.log_text.append(message) + # Scrolle nach unten + # Korrigierter Code für die Qt-Version + try: + self.log_text.moveCursor(Qt.MoveOperation.MoveEnd) + except AttributeError: + # Fallback für ältere Qt-Versionen + try: + self.log_text.moveCursor(Qt.MoveEnd) + except AttributeError: + # Weitere Fallbacks + try: + from PyQt5.QtGui import QTextCursor + self.log_text.moveCursor(QTextCursor.End) + except: + pass # Im Notfall einfach ignorieren + + def set_progress(self, value: int): + """Setzt den Fortschritt der Fortschrittsanzeige.""" + self.progress_bar.setValue(value) + + def set_status(self, message: str): + """Setzt die Statusnachricht.""" + # Diese Methode könnte später um eine Statusleiste erweitert werden + self.add_log(message) + + def show_error(self, message: str): + """Zeigt eine Fehlermeldung an.""" + title = "Fehler" + if self.language_manager: + title = self.language_manager.get_text( + "generator_tab.error_title", "Fehler" + ) + QMessageBox.critical(self, title, message) + + def update_texts(self): + """Aktualisiert UI-Texte gemäß der aktuellen Sprache.""" + if not self.language_manager: + return + + lm = self.language_manager + + self.form_group.setTitle(lm.get_text("generator_tab.form_title", "Account-Informationen")) + self.first_name_label.setText(lm.get_text("generator_tab.first_name_label", "Vorname:")) + self.first_name_input.setPlaceholderText(lm.get_text("generator_tab.first_name_placeholder", "z.B. Max")) + self.last_name_label.setText(lm.get_text("generator_tab.last_name_label", "Nachname:")) + self.last_name_input.setPlaceholderText(lm.get_text("generator_tab.last_name_placeholder", "z.B. Mustermann")) + self.age_label.setText(lm.get_text("generator_tab.age_label", "Alter:")) + self.age_input.setPlaceholderText(lm.get_text("generator_tab.age_placeholder", "Alter zwischen 13 und 99")) + self.reg_method_label.setText(lm.get_text("generator_tab.registration_method_label", "Registrierungsmethode:")) + self.email_radio.setText(lm.get_text("generator_tab.email_radio", "E-Mail")) + self.phone_radio.setText(lm.get_text("generator_tab.phone_radio", "Telefon")) + self.phone_label.setText(lm.get_text("generator_tab.phone_label", "Telefonnummer:")) + self.phone_input.setPlaceholderText(lm.get_text("generator_tab.phone_placeholder", "z.B. +49123456789")) + self.email_domain_label.setText(lm.get_text("generator_tab.email_domain_label", "E-Mail-Domain:")) + self.use_proxy_check.setText(lm.get_text("generator_tab.proxy_use", "Proxy verwenden")) + self.proxy_type_label.setText(lm.get_text("generator_tab.proxy_type_label", "Typ:")) + self.proxy_type_combo.clear() + self.proxy_type_combo.addItems([ + lm.get_text("generator_tab.proxy_type_ipv4", "IPv4"), + lm.get_text("generator_tab.proxy_type_ipv6", "IPv6"), + lm.get_text("generator_tab.proxy_type_mobile", "Mobile"), + ]) + self.proxy_label.setText(lm.get_text("generator_tab.proxy_label", "Proxy:")) + self.headless_check.setText(lm.get_text("generator_tab.headless", "Browser im Hintergrund ausführen")) + self.debug_check.setText(lm.get_text("generator_tab.debug", "Debug-Modus (detaillierte Protokollierung)")) + + platform = self.platform_name.lower() + if platform == "tiktok" and hasattr(self, "category_combo"): + self.category_label.setText( + lm.get_text("generator_tab.tiktok_category_label", "Kategorie/Nische:") + ) + categories = [ + lm.get_text("generator_tab.tiktok_category_general", "Allgemein"), + lm.get_text("generator_tab.tiktok_category_gaming", "Gaming"), + lm.get_text("generator_tab.tiktok_category_fashion", "Mode"), + lm.get_text("generator_tab.tiktok_category_fitness", "Fitness"), + lm.get_text("generator_tab.tiktok_category_travel", "Reisen"), + lm.get_text("generator_tab.tiktok_category_cooking", "Kochen"), + lm.get_text("generator_tab.tiktok_category_technology", "Technologie"), + lm.get_text("generator_tab.tiktok_category_education", "Bildung"), + ] + current = self.category_combo.currentText() + self.category_combo.clear() + self.category_combo.addItems(categories) + if current in categories: + self.category_combo.setCurrentIndex(categories.index(current)) + self.log_group.setTitle(lm.get_text("generator_tab.log_title", "Log")) + self.start_button.setText(lm.get_text("buttons.create", "Account erstellen")) + self.stop_button.setText(lm.get_text("buttons.cancel", "Abbrechen")) diff --git a/views/tabs/settings_tab.py b/views/tabs/settings_tab.py new file mode 100644 index 0000000..479e433 --- /dev/null +++ b/views/tabs/settings_tab.py @@ -0,0 +1,315 @@ +""" +Tab für die Einstellungen der Anwendung. +""" + +import logging +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, + QGroupBox, QLabel, QLineEdit, QSpinBox, QTextEdit, + QPushButton, QCheckBox, QComboBox +) +from PyQt5.QtCore import pyqtSignal, Qt + +logger = logging.getLogger("settings_tab") + +class SettingsTab(QWidget): + """Widget für den Einstellungen-Tab.""" + + # Signale + proxy_settings_saved = pyqtSignal(dict) + proxy_tested = pyqtSignal(str) # proxy_type + email_settings_saved = pyqtSignal(dict) + email_tested = pyqtSignal(dict) # email_settings + license_activated = pyqtSignal(str) # license_key + + def __init__(self, platform_name, proxy_rotator=None, email_handler=None, license_manager=None, language_manager=None): + super().__init__() + self.platform_name = platform_name + self.proxy_rotator = proxy_rotator + self.email_handler = email_handler + self.license_manager = license_manager + self.language_manager = language_manager + self.init_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + # Einstellungen laden, falls Handler vorhanden + self.load_settings() + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + layout = QVBoxLayout(self) + + # Proxy-Einstellungen + proxy_group = QGroupBox("Proxy-Einstellungen") + proxy_layout = QVBoxLayout(proxy_group) + + # IPv4 Proxies + ipv4_form = QFormLayout() + self.ipv4_proxy_input = QTextEdit() + self.ipv4_proxy_input.setPlaceholderText("Ein Proxy pro Zeile im Format: host:port:username:password") + ipv4_form.addRow("IPv4 Proxies:", self.ipv4_proxy_input) + proxy_layout.addLayout(ipv4_form) + + # IPv6 Proxies + ipv6_form = QFormLayout() + self.ipv6_proxy_input = QTextEdit() + self.ipv6_proxy_input.setPlaceholderText("Ein Proxy pro Zeile im Format: host:port:username:password") + ipv6_form.addRow("IPv6 Proxies:", self.ipv6_proxy_input) + proxy_layout.addLayout(ipv6_form) + + # Mobile Proxies + mobile_form = QFormLayout() + self.mobile_proxy_input = QTextEdit() + self.mobile_proxy_input.setPlaceholderText("Ein Proxy pro Zeile im Format: host:port:username:password") + mobile_form.addRow("Mobile Proxies:", self.mobile_proxy_input) + proxy_layout.addLayout(mobile_form) + + # Mobile Proxy API Keys + api_key_layout = QFormLayout() + self.marsproxy_api_input = QLineEdit() + api_key_layout.addRow("MarsProxies API Key:", self.marsproxy_api_input) + + self.iproyal_api_input = QLineEdit() + api_key_layout.addRow("IPRoyal API Key:", self.iproyal_api_input) + + proxy_layout.addLayout(api_key_layout) + + # Test-Button + proxy_button_layout = QHBoxLayout() + + self.test_proxy_button = QPushButton("Proxy testen") + self.test_proxy_button.clicked.connect(self.on_test_proxy_clicked) + + self.save_proxy_button = QPushButton("Proxy-Einstellungen speichern") + self.save_proxy_button.clicked.connect(self.on_save_proxy_clicked) + + proxy_button_layout.addWidget(self.test_proxy_button) + proxy_button_layout.addWidget(self.save_proxy_button) + + proxy_layout.addLayout(proxy_button_layout) + + layout.addWidget(proxy_group) + + # E-Mail-Einstellungen + email_group = QGroupBox("E-Mail-Einstellungen") + email_layout = QFormLayout(email_group) + + self.imap_server_input = QLineEdit("imap.ionos.de") + email_layout.addRow("IMAP-Server:", self.imap_server_input) + + self.imap_port_input = QSpinBox() + self.imap_port_input.setRange(1, 65535) + self.imap_port_input.setValue(993) + email_layout.addRow("IMAP-Port:", self.imap_port_input) + + self.imap_user_input = QLineEdit() + email_layout.addRow("IMAP-Benutzername:", self.imap_user_input) + + self.imap_pass_input = QLineEdit() + self.imap_pass_input.setEchoMode(QLineEdit.Password) + email_layout.addRow("IMAP-Passwort:", self.imap_pass_input) + + email_button_layout = QHBoxLayout() + + self.test_email_button = QPushButton("E-Mail testen") + self.test_email_button.clicked.connect(self.on_test_email_clicked) + + self.save_email_button = QPushButton("E-Mail-Einstellungen speichern") + self.save_email_button.clicked.connect(self.on_save_email_clicked) + + email_button_layout.addWidget(self.test_email_button) + email_button_layout.addWidget(self.save_email_button) + + email_layout.addRow("", email_button_layout) + + layout.addWidget(email_group) + + # Plattformspezifische Einstellungen + if self.platform_name.lower() != "instagram": + self.add_platform_specific_settings(layout) + + # Lizenz-Gruppe + license_group = QGroupBox("Lizenz") + license_layout = QFormLayout(license_group) + + self.license_key_input = QLineEdit() + license_layout.addRow("Lizenzschlüssel:", self.license_key_input) + + self.activate_license_button = QPushButton("Lizenz aktivieren") + self.activate_license_button.clicked.connect(self.on_activate_license_clicked) + license_layout.addRow("", self.activate_license_button) + + layout.addWidget(license_group) + + # Stretch am Ende hinzufügen + layout.addStretch(1) + + def add_platform_specific_settings(self, layout): + """ + Fügt plattformspezifische Einstellungen hinzu. + Diese Methode kann in abgeleiteten Klassen überschrieben werden. + + Args: + layout: Das Layout, zu dem die Einstellungen hinzugefügt werden sollen + """ + platform = self.platform_name.lower() + + platform_settings_group = QGroupBox(f"{self.platform_name}-spezifische Einstellungen") + platform_settings_layout = QFormLayout(platform_settings_group) + + # Je nach Plattform unterschiedliche Einstellungen + if platform == "twitter": + self.twitter_api_key = QLineEdit() + platform_settings_layout.addRow("Twitter API Key:", self.twitter_api_key) + + self.twitter_api_secret = QLineEdit() + platform_settings_layout.addRow("Twitter API Secret:", self.twitter_api_secret) + + elif platform == "tiktok": + self.video_upload = QCheckBox("Video automatisch hochladen") + platform_settings_layout.addRow("", self.video_upload) + + self.follower_action = QCheckBox("Automatisch anderen Nutzern folgen") + platform_settings_layout.addRow("", self.follower_action) + + elif platform == "facebook": + self.page_creation = QCheckBox("Seite automatisch erstellen") + platform_settings_layout.addRow("", self.page_creation) + + self.privacy_level = QComboBox() + self.privacy_level.addItems(["Öffentlich", "Freunde", "Nur ich"]) + platform_settings_layout.addRow("Datenschutzeinstellung:", self.privacy_level) + + # Speichern-Button für plattformspezifische Einstellungen + self.platform_save_button = QPushButton(f"{self.platform_name}-Einstellungen speichern") + self.platform_save_button.clicked.connect(self.on_save_platform_settings_clicked) + platform_settings_layout.addRow("", self.platform_save_button) + + layout.addWidget(platform_settings_group) + + def load_settings(self): + """Lädt die Einstellungen aus den Handlern.""" + # Proxy-Einstellungen laden + if self.proxy_rotator: + try: + proxy_config = self.proxy_rotator.get_config() or {} + + # IPv4 Proxies + ipv4_proxies = proxy_config.get("ipv4", []) + self.ipv4_proxy_input.setPlainText("\n".join(ipv4_proxies)) + + # IPv6 Proxies + ipv6_proxies = proxy_config.get("ipv6", []) + self.ipv6_proxy_input.setPlainText("\n".join(ipv6_proxies)) + + # Mobile Proxies + mobile_proxies = proxy_config.get("mobile", []) + self.mobile_proxy_input.setPlainText("\n".join(mobile_proxies)) + + # API Keys + mobile_api = proxy_config.get("mobile_api", {}) + self.marsproxy_api_input.setText(mobile_api.get("marsproxies", "")) + self.iproyal_api_input.setText(mobile_api.get("iproyal", "")) + + except Exception as e: + logger.error(f"Fehler beim Laden der Proxy-Einstellungen: {e}") + + # E-Mail-Einstellungen laden + if self.email_handler: + try: + email_config = self.email_handler.get_config() or {} + + self.imap_server_input.setText(email_config.get("imap_server", "imap.ionos.de")) + self.imap_port_input.setValue(email_config.get("imap_port", 993)) + self.imap_user_input.setText(email_config.get("imap_user", "")) + self.imap_pass_input.setText(email_config.get("imap_pass", "")) + + except Exception as e: + logger.error(f"Fehler beim Laden der E-Mail-Einstellungen: {e}") + + # Lizenzeinstellungen laden + if self.license_manager: + try: + license_info = self.license_manager.get_license_info() + self.license_key_input.setText(license_info.get("key", "")) + + except Exception as e: + logger.error(f"Fehler beim Laden der Lizenzeinstellungen: {e}") + + def on_save_proxy_clicked(self): + """Wird aufgerufen, wenn der Proxy-Speichern-Button geklickt wird.""" + # Proxy-Einstellungen sammeln + settings = { + "ipv4_proxies": self.ipv4_proxy_input.toPlainText(), + "ipv6_proxies": self.ipv6_proxy_input.toPlainText(), + "mobile_proxies": self.mobile_proxy_input.toPlainText(), + "mobile_api": { + "marsproxies": self.marsproxy_api_input.text().strip(), + "iproyal": self.iproyal_api_input.text().strip() + } + } + + # Signal auslösen + self.proxy_settings_saved.emit(settings) + + def on_test_proxy_clicked(self): + """Wird aufgerufen, wenn der Proxy-Test-Button geklickt wird.""" + # Proxy-Typ aus der Combobox in der Generator-Tab holen + # Da wir keine direkte Referenz haben, nehmen wir den ersten Eintrag + proxy_type = "ipv4" + + # Signal auslösen + self.proxy_tested.emit(proxy_type) + + def on_save_email_clicked(self): + """Wird aufgerufen, wenn der E-Mail-Speichern-Button geklickt wird.""" + # E-Mail-Einstellungen sammeln + settings = { + "imap_server": self.imap_server_input.text().strip(), + "imap_port": self.imap_port_input.value(), + "imap_user": self.imap_user_input.text().strip(), + "imap_pass": self.imap_pass_input.text() + } + + # Signal auslösen + self.email_settings_saved.emit(settings) + + def on_test_email_clicked(self): + """Wird aufgerufen, wenn der E-Mail-Test-Button geklickt wird.""" + # E-Mail-Einstellungen sammeln + settings = { + "imap_server": self.imap_server_input.text().strip(), + "imap_port": self.imap_port_input.value(), + "imap_user": self.imap_user_input.text().strip(), + "imap_pass": self.imap_pass_input.text() + } + + # Signal auslösen + self.email_tested.emit(settings) + + def on_save_platform_settings_clicked(self): + """Wird aufgerufen, wenn der Plattform-Einstellungen-Speichern-Button geklickt wird.""" + # Hier könnte ein plattformspezifisches Signal ausgelöst werden + logger.info(f"{self.platform_name}-Einstellungen gespeichert") + + def on_activate_license_clicked(self): + """Wird aufgerufen, wenn der Lizenz-Aktivieren-Button geklickt wird.""" + license_key = self.license_key_input.text().strip() + + if not license_key: + return + + # Signal auslösen + self.license_activated.emit(license_key) + + def update_texts(self): + """Aktualisiert UI-Texte gemäß aktueller Sprache.""" + if not self.language_manager: + return + self.test_proxy_button.setText(self.language_manager.get_text("buttons.test_proxy", "Proxy testen")) + self.save_proxy_button.setText(self.language_manager.get_text("buttons.save_proxy", "Proxy-Einstellungen speichern")) + self.test_email_button.setText(self.language_manager.get_text("buttons.test_email", "E-Mail testen")) + self.save_email_button.setText(self.language_manager.get_text("buttons.save_email", "E-Mail-Einstellungen speichern")) + self.activate_license_button.setText(self.language_manager.get_text("buttons.activate_license", "Lizenz aktivieren")) diff --git a/views/widgets/__pycache__/language_dropdown.cpython-310.pyc b/views/widgets/__pycache__/language_dropdown.cpython-310.pyc new file mode 100644 index 0000000..c795a99 Binary files /dev/null and b/views/widgets/__pycache__/language_dropdown.cpython-310.pyc differ diff --git a/views/widgets/__pycache__/language_dropdown.cpython-313.pyc b/views/widgets/__pycache__/language_dropdown.cpython-313.pyc new file mode 100644 index 0000000..fa5d32b Binary files /dev/null and b/views/widgets/__pycache__/language_dropdown.cpython-313.pyc differ diff --git a/views/widgets/__pycache__/platform_button.cpython-310.pyc b/views/widgets/__pycache__/platform_button.cpython-310.pyc new file mode 100644 index 0000000..1d105f1 Binary files /dev/null and b/views/widgets/__pycache__/platform_button.cpython-310.pyc differ diff --git a/views/widgets/__pycache__/platform_button.cpython-313.pyc b/views/widgets/__pycache__/platform_button.cpython-313.pyc new file mode 100644 index 0000000..20eec51 Binary files /dev/null and b/views/widgets/__pycache__/platform_button.cpython-313.pyc differ diff --git a/views/widgets/language_dropdown.py b/views/widgets/language_dropdown.py new file mode 100644 index 0000000..c484b2f --- /dev/null +++ b/views/widgets/language_dropdown.py @@ -0,0 +1,203 @@ +# Path: views/widgets/language_dropdown.py + +""" +Benutzerdefiniertes Dropdown-Widget für die Sprachauswahl mit Flaggen-Icons. +""" + +import os +from PyQt5.QtWidgets import (QWidget, QComboBox, QLabel, QHBoxLayout, + QVBoxLayout, QFrame, QListWidget, QListWidgetItem, + QAbstractItemView, QApplication) +from PyQt5.QtCore import Qt, QSize, QEvent, pyqtSignal +from PyQt5.QtGui import QIcon, QPainter, QPen, QColor, QCursor + +class LanguageDropdown(QWidget): + """Benutzerdefiniertes Dropdown für die Sprachauswahl mit Flaggen.""" + + def __init__(self, language_manager): + super().__init__() + self.language_manager = language_manager + self.is_open = False + self.languages = {} + self.current_language = self.language_manager.get_current_language() + + # QApplication-Instanz merken, um einen Event-Filter installieren zu können + self.app = QApplication.instance() + + # Verfügbare Sprachen aus dem Manager holen + self.available_languages = self.language_manager.get_available_languages() + + self.init_ui() + + # Verbinde Signal des Language Managers + self.language_manager.language_changed.connect(self.on_language_changed) + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + # Container für die aktuelle Sprachauswahl + self.current_language_container = QFrame() + self.current_language_container.setObjectName("languageSelector") + self.current_language_container.setCursor(Qt.PointingHandCursor) + self.current_language_container.setStyleSheet(""" + QFrame#languageSelector { + background-color: transparent; + border-radius: 15px; + } + QFrame#languageSelector:hover { + background-color: rgba(200, 200, 200, 30); + } + """) + + current_layout = QHBoxLayout(self.current_language_container) + current_layout.setContentsMargins(5, 5, 5, 5) + + # Icon der aktuellen Sprache + self.current_flag = QLabel() + self.current_flag.setFixedSize(24, 24) + + # Pfad zum Icon + icon_path = self.get_language_icon_path(self.current_language) + if icon_path: + self.current_flag.setPixmap(QIcon(icon_path).pixmap(QSize(24, 24))) + + current_layout.addWidget(self.current_flag) + + # Kleiner Pfeil nach unten + arrow_label = QLabel("▼") + arrow_label.setStyleSheet("font-size: 8px; color: #888888;") + current_layout.addWidget(arrow_label) + + layout.addWidget(self.current_language_container) + + # Dropdown-Liste (anfangs versteckt) + self.dropdown_list = QListWidget() + self.dropdown_list.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint) + self.dropdown_list.setFocusPolicy(Qt.NoFocus) + self.dropdown_list.setMouseTracking(True) + self.dropdown_list.setFrameShape(QFrame.NoFrame) + self.dropdown_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.dropdown_list.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.dropdown_list.setSelectionMode(QAbstractItemView.NoSelection) + self.dropdown_list.setStyleSheet(""" + QListWidget { + background-color: white; + border: 1px solid #CCCCCC; + border-radius: 5px; + padding: 5px; + } + QListWidget::item { + padding: 4px; + border-radius: 3px; + } + QListWidget::item:hover { + background-color: #F0F0F0; + } + """) + + # Sprachen zum Dropdown hinzufügen + self.populate_dropdown() + + # Event-Verbindungen + self.current_language_container.mousePressEvent = self.toggle_dropdown + self.dropdown_list.itemClicked.connect(self.on_language_selected) + + # Zugänglichkeit mit Tastaturfokus + self.setFocusPolicy(Qt.StrongFocus) + self.current_language_container.setFocusPolicy(Qt.StrongFocus) + + def get_language_icon_path(self, language_code): + """Gibt den Pfad zum Icon für den angegebenen Sprachcode zurück.""" + # Projektbasis-Verzeichnis ermitteln + base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + icon_path = os.path.join(base_dir, "resources", "icons", f"{language_code}.svg") + + if os.path.exists(icon_path): + return icon_path + return None + + def populate_dropdown(self): + """Füllt das Dropdown mit den verfügbaren Sprachen.""" + self.dropdown_list.clear() + self.languages = {} + + for code, name in self.available_languages.items(): + item = QListWidgetItem(name) + + # Icon erstellen + icon_path = self.get_language_icon_path(code) + if icon_path: + item.setIcon(QIcon(icon_path)) + + # Sprach-Code speichern + item.setData(Qt.UserRole, code) + + # Zum Dropdown hinzufügen + self.dropdown_list.addItem(item) + self.languages[code] = item + + def toggle_dropdown(self, event): + """Öffnet oder schließt das Dropdown-Menü.""" + if not self.is_open: + # Position des Dropdowns unter dem Button berechnen + pos = self.current_language_container.mapToGlobal(self.current_language_container.rect().bottomLeft()) + self.dropdown_list.setGeometry(pos.x(), pos.y(), 120, 120) # Größe anpassen + self.dropdown_list.show() + self.is_open = True + if self.app: + self.app.installEventFilter(self) + else: + self.dropdown_list.hide() + self.is_open = False + if self.app: + self.app.removeEventFilter(self) + + def on_language_selected(self, item): + """Wird aufgerufen, wenn eine Sprache im Dropdown ausgewählt wird.""" + language_code = item.data(Qt.UserRole) + + # Sprache wechseln + if language_code != self.current_language: + self.language_manager.change_language(language_code) + + # Dropdown schließen + self.dropdown_list.hide() + self.is_open = False + if self.app: + self.app.removeEventFilter(self) + + def on_language_changed(self, language_code): + """Wird aufgerufen, wenn sich die Sprache im LanguageManager ändert.""" + self.current_language = language_code + + # Icon aktualisieren + icon_path = self.get_language_icon_path(language_code) + if icon_path: + self.current_flag.setPixmap(QIcon(icon_path).pixmap(QSize(24, 24))) + + # Texte aktualisieren (falls vorhanden) + if hasattr(self.parent(), "update_texts"): + self.parent().update_texts() + + def keyPressEvent(self, event): + """Behandelt Tastatureingaben für verbesserte Zugänglichkeit.""" + if event.key() == Qt.Key_Space or event.key() == Qt.Key_Return: + self.toggle_dropdown(None) + else: + super().keyPressEvent(event) + + def eventFilter(self, obj, event): + """Schließt das Dropdown, wenn außerhalb geklickt wird.""" + if self.is_open and event.type() == QEvent.MouseButtonPress: + # Klickposition relativ zum Dropdown ermitteln + if not self.dropdown_list.geometry().contains(event.globalPos()) and \ + not self.current_language_container.geometry().contains( + self.mapFromGlobal(event.globalPos())): + self.dropdown_list.hide() + self.is_open = False + if self.app: + self.app.removeEventFilter(self) + return super().eventFilter(obj, event) diff --git a/views/widgets/platform_button.py b/views/widgets/platform_button.py new file mode 100644 index 0000000..d887bb4 --- /dev/null +++ b/views/widgets/platform_button.py @@ -0,0 +1,76 @@ +# Path: views/widgets/platform_button.py + +""" +Benutzerdefinierter Button für die Plattformauswahl. +""" + +import os +from PyQt5.QtWidgets import QPushButton, QVBoxLayout, QLabel, QWidget +from PyQt5.QtCore import QSize, Qt, pyqtSignal +from PyQt5.QtGui import QIcon, QFont + +class PlatformButton(QWidget): + """Angepasster Button-Widget für Plattformauswahl mit Icon.""" + + # Signal wenn geklickt + clicked = pyqtSignal() + + def __init__(self, platform_name, icon_path=None, enabled=True): + super().__init__() + + self.platform = platform_name.lower() + self.setMinimumSize(200, 200) + self.setEnabled(enabled) + + # Layout für den Container + layout = QVBoxLayout(self) + layout.setAlignment(Qt.AlignCenter) + layout.setContentsMargins(10, 10, 10, 10) + + # Icon-Button + self.icon_button = QPushButton() + self.icon_button.setFlat(True) + self.icon_button.setCursor(Qt.PointingHandCursor) + + # Icon setzen, falls vorhanden + if icon_path and os.path.exists(icon_path): + self.icon_button.setIcon(QIcon(icon_path)) + self.icon_button.setIconSize(QSize(120, 120)) # Größeres Icon + + self.icon_button.setMinimumSize(150, 150) + self.icon_button.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + } + QPushButton:hover { + background-color: rgba(200, 200, 200, 50); + border-radius: 10px; + } + QPushButton:pressed { + background-color: rgba(150, 150, 150, 80); + } + QPushButton:disabled { + opacity: 0.5; + } + """) + + # Button-Signal verbinden + self.icon_button.clicked.connect(self.clicked) + + # Name-Label + self.name_label = QLabel(platform_name) + self.name_label.setAlignment(Qt.AlignCenter) + name_font = QFont() + name_font.setPointSize(12) + name_font.setBold(True) + self.name_label.setFont(name_font) + self.name_label.setStyleSheet("color: black;") # Schwarzer Text + + # Widgets zum Layout hinzufügen + layout.addWidget(self.icon_button, 0, Qt.AlignCenter) + layout.addWidget(self.name_label, 0, Qt.AlignCenter) + + # Styling für den deaktivierten Zustand + if not enabled: + self.setStyleSheet("opacity: 0.5;") \ No newline at end of file