RollBack Punkt 2025-09-13
Dieser Commit ist enthalten in:
@ -13,9 +13,11 @@
|
||||
"Bash(claude config)",
|
||||
"Bash(claude config list:*)",
|
||||
"Bash(claude mcp)",
|
||||
"Bash(claude mcp:*)"
|
||||
"Bash(claude mcp:*)",
|
||||
"Bash(sqlite3:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"defaultMode": "acceptEdits",
|
||||
"additionalDirectories": [
|
||||
"/mnt/a/GiTea/Styleguide"
|
||||
]
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
## Project Overview
|
||||
|
||||
- **Path**: `A:\GiTea\AccountForger`
|
||||
- **Files**: 1039 files
|
||||
- **Size**: 380.8 MB
|
||||
- **Last Modified**: 2025-08-12 12:50
|
||||
- **Files**: 886 files
|
||||
- **Size**: 125.8 MB
|
||||
- **Last Modified**: 2025-09-10 21:18
|
||||
|
||||
## Technology Stack
|
||||
|
||||
@ -23,12 +23,11 @@
|
||||
check_rotation_system.py
|
||||
CLAUDE_PROJECT_README.md
|
||||
debug_video_issue.py
|
||||
gitea_push_debug.txt
|
||||
init
|
||||
install_requirements.py
|
||||
main.py
|
||||
package.json
|
||||
README.md
|
||||
requirements.txt
|
||||
application/
|
||||
│ ├── __init__.py
|
||||
│ ├── services/
|
||||
@ -77,7 +76,8 @@ controllers/
|
||||
│ ├── method_rotation_worker_mixin.py
|
||||
│ ├── ok_ru_controller.py
|
||||
│ ├── rotation_error_handler.py
|
||||
│ └── safe_imports.py
|
||||
│ ├── safe_imports.py
|
||||
│ └── mixins
|
||||
database/
|
||||
│ ├── accounts.db
|
||||
│ ├── account_repository.py
|
||||
@ -103,6 +103,7 @@ domain/
|
||||
│ │ ├── method_rotation.py
|
||||
│ │ ├── rate_limit_policy.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── enums
|
||||
│ ├── repositories/
|
||||
│ │ ├── analytics_repository.py
|
||||
│ │ ├── fingerprint_repository.py
|
||||
@ -169,29 +170,33 @@ localization/
|
||||
│ ├── fr.json
|
||||
│ └── ja.json
|
||||
logs/
|
||||
│ ├── instagram_automation.log
|
||||
│ ├── instagram_controller.log
|
||||
│ ├── instagram_login.log
|
||||
│ ├── instagram_registration.log
|
||||
│ ├── instagram_ui_helper.log
|
||||
│ ├── instagram_utils.log
|
||||
│ ├── instagram_verification.log
|
||||
│ ├── instagram_workflow.log
|
||||
│ ├── main.log
|
||||
│ ├── facebook_automation.log
|
||||
│ ├── facebook_controller.log
|
||||
│ ├── facebook_login.log
|
||||
│ ├── facebook_registration.log
|
||||
│ ├── facebook_ui_helper.log
|
||||
│ ├── facebook_utils.log
|
||||
│ ├── facebook_verification.log
|
||||
│ ├── facebook_workflow.log
|
||||
│ ├── gmail_controller.log
|
||||
│ ├── net/
|
||||
│ │ ├── gmail_landing_de.html
|
||||
│ │ └── google_signup_mail_de.html
|
||||
│ ├── proxyfather_tests/
|
||||
│ │ ├── free_test_20250814_134106.log
|
||||
│ │ ├── free_test_20250814_134123.log
|
||||
│ │ └── free_test_20250814_135000.log
|
||||
│ └── screenshots/
|
||||
│ ├── after_account_create_click_1753044575.png
|
||||
│ ├── after_account_create_click_1753044886.png
|
||||
│ ├── after_account_create_click_1753045178.png
|
||||
│ ├── after_account_create_click_1753045715.png
|
||||
│ ├── after_account_create_click_1753045915.png
|
||||
│ ├── after_account_create_click_1753046167.png
|
||||
│ ├── after_account_create_click_1753046976.png
|
||||
│ ├── after_account_create_click_1753047240.png
|
||||
│ ├── after_account_create_click_1753047386.png
|
||||
│ └── after_account_create_click_1753048280.png
|
||||
│ ├── after_account_create_click_1755281004.png
|
||||
│ ├── after_account_create_click_1757531758.png
|
||||
│ ├── after_code_retrieval_1757531778.png
|
||||
│ ├── after_login_button_click_1755168832.png
|
||||
│ ├── after_login_button_click_1755280227.png
|
||||
│ ├── after_login_button_click_1755280551.png
|
||||
│ ├── after_login_button_click_1755280826.png
|
||||
│ ├── after_login_button_click_1755282576.png
|
||||
│ ├── after_login_button_click_1755282842.png
|
||||
│ └── after_login_button_click_1755341790.png
|
||||
resources/
|
||||
│ ├── icons/
|
||||
│ │ ├── check-white.svg
|
||||
@ -254,23 +259,13 @@ social_networks/
|
||||
│ │ ├── tiktok_automation.py
|
||||
│ │ ├── tiktok_login.py
|
||||
│ │ ├── tiktok_registration.py
|
||||
│ │ ├── tiktok_registration_backup.py
|
||||
│ │ ├── tiktok_registration_clean.py
|
||||
│ │ ├── tiktok_registration_final.py
|
||||
│ │ ├── tiktok_registration_new.py
|
||||
│ │ ├── tiktok_selectors.py
|
||||
│ │ ├── tiktok_ui_helper.py
|
||||
│ │ └── tiktok_utils.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
|
||||
│ │ ├── tiktok_utils.py
|
||||
│ │ ├── tiktok_verification.py
|
||||
│ │ ├── tiktok_workflow.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── twitter
|
||||
│ ├── vk/
|
||||
│ │ ├── vk_automation.py
|
||||
│ │ ├── vk_login.py
|
||||
@ -295,7 +290,52 @@ styles/
|
||||
│ ├── modal_styles.py
|
||||
│ └── __init__.py
|
||||
tests/
|
||||
│ └── test_method_rotation.py
|
||||
│ ├── test_method_rotation.py
|
||||
│ ├── bright_data/
|
||||
│ │ └── logs/
|
||||
│ │ ├── proxy_test_20250813_172156.log
|
||||
│ │ ├── proxy_test_20250813_173258.log
|
||||
│ │ ├── proxy_test_20250813_173828.log
|
||||
│ │ ├── proxy_test_20250813_174940.log
|
||||
│ │ ├── proxy_test_20250813_175937.log
|
||||
│ │ ├── proxy_test_20250813_180630.log
|
||||
│ │ ├── proxy_test_20250813_181050.log
|
||||
│ │ ├── proxy_test_20250813_182854.log
|
||||
│ │ ├── proxy_test_20250813_185011.log
|
||||
│ │ └── proxy_test_20250813_185850.log
|
||||
│ ├── ProxyFather/
|
||||
│ │ └── logs/
|
||||
│ │ ├── connection_test_20250814_015554.log
|
||||
│ │ ├── connection_test_20250814_023651.log
|
||||
│ │ ├── instagram_test_20250814_015725.log
|
||||
│ │ ├── instagram_test_20250814_015846.log
|
||||
│ │ ├── instagram_test_20250814_020102.log
|
||||
│ │ ├── instagram_test_20250814_020429.log
|
||||
│ │ ├── instagram_test_20250814_021100.log
|
||||
│ │ ├── instagram_test_20250814_022223.log
|
||||
│ │ ├── instagram_test_20250814_023255.log
|
||||
│ │ └── instagram_test_20250814_024719.log
|
||||
│ └── proxy_evaluation/
|
||||
│ ├── test_results/
|
||||
│ │ ├── baseline_summary.json
|
||||
│ │ ├── baseline_test_20250813_143756.json
|
||||
│ │ ├── baseline_test_20250813_144600.json
|
||||
│ │ ├── browser_proxy_test_20250813_143241.json
|
||||
│ │ ├── browser_proxy_test_20250813_144227.json
|
||||
│ │ ├── browser_test_summary.json
|
||||
│ │ ├── facebook_isp_dedicated.png
|
||||
│ │ ├── facebook_isp_shared.png
|
||||
│ │ ├── facebook_test_20250813_150605.json
|
||||
│ │ └── ip_rotation_test_20250813_142319.json
|
||||
│ └── test_screenshots/
|
||||
│ ├── baseline_google.png
|
||||
│ ├── baseline_instagram.png
|
||||
│ ├── baseline_tiktok.png
|
||||
│ ├── basic_connection.png
|
||||
│ ├── gmail_loaded.png
|
||||
│ ├── instagram_loaded.png
|
||||
│ ├── tiktok_loaded.png
|
||||
│ └── twitter_x_loaded.png
|
||||
themes/
|
||||
│ ├── qss_generator.py
|
||||
│ ├── theme_config.py
|
||||
@ -337,19 +377,18 @@ views/
|
||||
│ ├── accounts_tab.py
|
||||
│ ├── facebook_generator_tab.py
|
||||
│ ├── generator_tab.py
|
||||
│ ├── generator_tab_modern.py
|
||||
│ └── settings_tab.py
|
||||
└── widgets/
|
||||
├── account_card.py
|
||||
├── account_creation_modal.py
|
||||
├── account_creation_modal_v2.py
|
||||
├── dark_mode_toggle.py
|
||||
├── forge_animation_widget.py
|
||||
├── forge_animation_widget_v2.py
|
||||
├── icon_factory.py
|
||||
├── language_dropdown.py
|
||||
├── login_process_modal.py
|
||||
└── modern_message_box.py
|
||||
├── modern_message_box.py
|
||||
├── platform_button.py
|
||||
└── progress_modal.py
|
||||
```
|
||||
|
||||
## Key Files
|
||||
@ -374,17 +413,5 @@ This project is managed with Claude Project Manager. To work with this project:
|
||||
|
||||
## Development Log
|
||||
|
||||
- README generated on 2025-07-27 11:07:01
|
||||
- README updated on 2025-07-28 18:14:33
|
||||
- README updated on 2025-07-29 19:24:34
|
||||
- README updated on 2025-07-31 00:00:41
|
||||
- README updated on 2025-08-01 19:02:35
|
||||
- README updated on 2025-08-01 20:50:22
|
||||
- README updated on 2025-08-01 20:51:41
|
||||
- README updated on 2025-08-01 21:06:44
|
||||
- README updated on 2025-08-09 01:31:28
|
||||
- README updated on 2025-08-10 00:03:51
|
||||
- README updated on 2025-08-10 12:55:25
|
||||
- README updated on 2025-08-11 19:49:09
|
||||
- README updated on 2025-08-12 12:50:03
|
||||
- README updated on 2025-08-12 13:20:51
|
||||
- README generated on 2025-09-06 21:49:36
|
||||
- README updated on 2025-09-13 20:45:18
|
||||
|
||||
114
README.md
114
README.md
@ -1,6 +1,6 @@
|
||||
# Social Media Account Generator
|
||||
# AccountForger – 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.
|
||||
Desktop‑Anwendung zur Erstellung und Verwaltung von Social‑Media‑Accounts. UI mit **PyQt5**, Automatisierung mit **Playwright**. Modularer Aufbau mit plattformspezifischen Controllern.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -22,92 +22,49 @@ Beim ersten Start werden benötigte Ordner wie `logs`, `config` und `resources`
|
||||
├── main.py
|
||||
├── browser/
|
||||
│ ├── playwright_manager.py
|
||||
│ └── stealth_config.py
|
||||
│ ├── stealth_config.py
|
||||
│ ├── fingerprint_protection.py
|
||||
│ └── cookie_consent_handler.py
|
||||
├── controllers/
|
||||
│ ├── main_controller.py
|
||||
│ ├── account_controller.py
|
||||
│ ├── session_controller.py
|
||||
│ ├── settings_controller.py
|
||||
│ └── platform_controllers/
|
||||
│ ├── base_controller.py
|
||||
│ ├── instagram_controller.py
|
||||
│ └── tiktok_controller.py
|
||||
│ ├── tiktok_controller.py
|
||||
│ ├── facebook_controller.py
|
||||
│ ├── gmail_controller.py
|
||||
│ ├── x_controller.py
|
||||
│ └── ok_ru_controller.py
|
||||
├── social_networks/
|
||||
│ ├── base_automation.py
|
||||
│ ├── instagram/ ...
|
||||
│ ├── tiktok/ ...
|
||||
│ ├── facebook/ ...
|
||||
│ ├── gmail/ ...
|
||||
│ ├── x/ ... # Ehemals „Twitter“
|
||||
│ └── vk/ ...
|
||||
├── 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/
|
||||
│ └── ...
|
||||
│ ├── tabs/ (generator_tab.py, settings_tab.py, accounts_tab.py)
|
||||
│ └── widgets/ (dark_mode_toggle.py, ...)
|
||||
├── 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
|
||||
│ └── languages/ (de.json, en.json, ...)
|
||||
├── utils/ (logger.py, proxy_rotator.py, email_handler.py, ...)
|
||||
├── database/ (db_manager.py, schema_v2.sql)
|
||||
├── licensing/ (license_manager.py, hardware_fingerprint.py, ...)
|
||||
├── updates/ (update_checker.py, downloader.py, version.py)
|
||||
├── config/ (app_version.json, theme.json, proxy_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
|
||||
│ ├── icons/ (instagram.svg, x.svg/twitter.svg, ...)
|
||||
│ └── themes/ (light.qss, dark.qss)
|
||||
├── tests/ (test_method_rotation.py, ...)
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
Weitere Ordner:
|
||||
@ -118,13 +75,16 @@ Weitere Ordner:
|
||||
|
||||
## 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.
|
||||
In `localization/languages` liegen Übersetzungen für Deutsch, Englisch, Spanisch, Französisch und Japanisch. Die 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.
|
||||
`licensing` enthält Lizenzprüfung und Sitzungsverwaltung; `updates` prüft und lädt Updates. Versionen stehen in `updates/version.py`.
|
||||
|
||||
## Tests
|
||||
|
||||
Im Ordner `testcases` liegt beispielhaft `imap_test.py`, mit dem die IMAP‑Konfiguration getestet werden kann.
|
||||
Tests liegen unter `tests/` und als einzelne `test_*.py` Dateien im Root. Umfang variiert je nach Komponente.
|
||||
|
||||
## Rechtliche Hinweise
|
||||
|
||||
Die Nutzung dieser Software muss im Einklang mit geltendem Recht und den Nutzungsbedingungen der jeweiligen Plattformen erfolgen. Umgehung von Sicherheits‑, Identitäts‑ oder Verifizierungsmechanismen ist unzulässig. Keine echten personenbezogenen Daten ohne Einwilligung verwenden.
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
{
|
||||
"ipv4": [
|
||||
"85.254.81.222:44444:14a38ed2efe94:04ed25fb1b"
|
||||
],
|
||||
"ipv6": [
|
||||
"92.119.89.251:30015:14a4622431481:a488401704"
|
||||
],
|
||||
"mobile": [
|
||||
"de1.4g.iproyal.com:7296:1rtSh0G:XswBCIqi1joy5dX"
|
||||
],
|
||||
"ipv4": [],
|
||||
"ipv6": [],
|
||||
"mobile": [],
|
||||
"mobile_api": {
|
||||
"marsproxies": "9zKXWpMEA1",
|
||||
"marsproxies": "",
|
||||
"iproyal": ""
|
||||
}
|
||||
}
|
||||
Binäre Datei nicht angezeigt.
@ -1,5 +1,7 @@
|
||||
# Clean Architecture Design - AccountForger
|
||||
|
||||
Status: Vorschlag/Entwurf. Diese Architektur ist im aktuellen Code nur teilweise umgesetzt und dient als Zielbild. Ordner‑/Dateinamen in Beispielen sind konzeptionell zu verstehen.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Diese Dokumentation beschreibt die saubere Architektur für das AccountForger-System mit Fokus auf das Fingerprint-Management und die Login-Funktionalität mit gespeicherten Fingerprints.
|
||||
@ -339,4 +341,4 @@ Diese Architektur stellt sicher, dass:
|
||||
- Fingerprints konsistent bleiben
|
||||
- Sessions zuverlässig wiederhergestellt werden
|
||||
- Der Code wartbar und erweiterbar bleibt
|
||||
- Keine zirkulären Abhängigkeiten entstehen
|
||||
- Keine zirkulären Abhängigkeiten entstehen
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
Push Debug Info - 2025-08-01 23:50:28.066003
|
||||
Repository: AccountForger-neuerUpload
|
||||
Owner: IntelSight
|
||||
Path: A:\GiTea\AccountForger
|
||||
Current branch: master
|
||||
Git remotes:
|
||||
origin https://StuXn3t:29aa2ffb5ef85bd4f56e2e7bd19098310a37f3bd@gitea-undso.intelsight.de/IntelSight/AccountForger-neuerUpload.git (fetch)
|
||||
origin https://StuXn3t:29aa2ffb5ef85bd4f56e2e7bd19098310a37f3bd@gitea-undso.intelsight.de/IntelSight/AccountForger-neuerUpload.git (push)
|
||||
Git status before push:
|
||||
Clean
|
||||
Push command: git push --set-upstream origin master:main -v
|
||||
Push result: Success
|
||||
Push stdout:
|
||||
branch 'master' set up to track 'origin/main'.
|
||||
Push stderr:
|
||||
POST git-receive-pack (623173 bytes)
|
||||
remote: . Processing 1 references
|
||||
remote: Processed 1 references in total
|
||||
Pushing to https://gitea-undso.intelsight.de/IntelSight/AccountForger-neuerUpload.git
|
||||
To https://gitea-undso.intelsight.de/IntelSight/AccountForger-neuerUpload.git
|
||||
* [new branch] master -> main
|
||||
updating local tracking ref 'refs/remotes/origin/main'
|
||||
8
init
Normale Datei
8
init
Normale Datei
@ -0,0 +1,8 @@
|
||||
- Rolle: Präziser Code-Experte mit hohem Qualitätsanspruch.
|
||||
- Prinzipien: Clean Code, Clean Architecture, sinnvolles Refactoring; kein Overengineering.
|
||||
- YAGNI: Nur das Nötige, keine Aufblähung.
|
||||
- Arbeitsmodus: Erst planen, dann nach ausdrücklicher Freigabe implementieren.
|
||||
- Fehler & Logging: Klare, typsichere Fehler; minimal strukturiertes Logging; keine internen Details leaken.
|
||||
- Security: Keine Secrets im Code; Eingaben validieren an Grenzen; Least-Privilege denken.
|
||||
- Schnittstellen: Kleine, stabile öffentliche APIs; interne Details kapseln; Breaking Changes vermeiden.
|
||||
- Performance: Kein Premature Optimization; einfache O(n)-Lösungen bevorzugen; Budget pro Feature definieren.
|
||||
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@ -1,672 +0,0 @@
|
||||
# social_networks/tiktok/tiktok_registration_clean.py
|
||||
|
||||
"""
|
||||
TikTok Registration Module - Clean Architecture Implementation
|
||||
Handles the complete TikTok account registration workflow.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from .tiktok_workflow import TikTokWorkflow
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger("tiktok_registration")
|
||||
|
||||
|
||||
class RegistrationStage(Enum):
|
||||
"""Enumeration of registration workflow stages."""
|
||||
NAVIGATION = "navigation"
|
||||
COOKIE_CONSENT = "cookie_consent"
|
||||
LOGIN_CLICK = "login_click"
|
||||
REGISTER_CLICK = "register_click"
|
||||
PHONE_EMAIL_SELECTION = "phone_email_selection"
|
||||
METHOD_SELECTION = "method_selection"
|
||||
BIRTHDAY_ENTRY = "birthday_entry"
|
||||
EMAIL_ENTRY = "email_entry"
|
||||
PASSWORD_ENTRY = "password_entry"
|
||||
CODE_SENDING = "code_sending"
|
||||
CODE_VERIFICATION = "code_verification"
|
||||
USERNAME_ENTRY = "username_entry"
|
||||
COMPLETION = "completion"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegistrationConfig:
|
||||
"""Configuration for registration process."""
|
||||
max_retries: int = 3
|
||||
retry_delay: float = 2.0
|
||||
timeout: int = 5000
|
||||
human_delay_min: float = 0.5
|
||||
human_delay_max: float = 2.0
|
||||
|
||||
|
||||
class TikTokRegistration:
|
||||
"""
|
||||
Clean implementation of TikTok account registration.
|
||||
Follows Single Responsibility Principle and Clean Architecture.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialize TikTok registration handler.
|
||||
|
||||
Args:
|
||||
automation: Parent automation instance with browser and utilities
|
||||
"""
|
||||
self.automation = automation
|
||||
self.selectors = TikTokSelectors()
|
||||
self.workflow = TikTokWorkflow.get_registration_workflow()
|
||||
self.config = RegistrationConfig()
|
||||
self._current_stage = None
|
||||
|
||||
logger.debug("TikTok registration handler initialized")
|
||||
|
||||
def register_account(
|
||||
self,
|
||||
full_name: str,
|
||||
age: int,
|
||||
registration_method: str = "email",
|
||||
phone_number: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute complete account registration workflow.
|
||||
|
||||
Args:
|
||||
full_name: User's full name
|
||||
age: User's age (must be >= 13)
|
||||
registration_method: Either "email" or "phone"
|
||||
phone_number: Phone number (required if method is "phone")
|
||||
**kwargs: Additional optional parameters
|
||||
|
||||
Returns:
|
||||
Registration result dictionary with status and account data
|
||||
"""
|
||||
try:
|
||||
# Validate inputs
|
||||
if not self._validate_inputs(full_name, age, registration_method, phone_number):
|
||||
return self._create_error_result("Invalid input parameters", RegistrationStage.NAVIGATION)
|
||||
|
||||
# Generate account data
|
||||
account_data = self._generate_account_data(full_name, age, registration_method, phone_number, **kwargs)
|
||||
logger.info(f"Starting TikTok registration for {account_data['username']} via {registration_method}")
|
||||
|
||||
# Execute registration workflow stages
|
||||
stages = [
|
||||
(RegistrationStage.NAVIGATION, self._navigate_to_tiktok),
|
||||
(RegistrationStage.COOKIE_CONSENT, self._handle_cookies),
|
||||
(RegistrationStage.LOGIN_CLICK, self._click_login),
|
||||
(RegistrationStage.REGISTER_CLICK, self._click_register),
|
||||
(RegistrationStage.PHONE_EMAIL_SELECTION, self._select_phone_email_option),
|
||||
(RegistrationStage.METHOD_SELECTION, lambda: self._select_method(registration_method)),
|
||||
(RegistrationStage.BIRTHDAY_ENTRY, lambda: self._enter_birthday(account_data["birthday"])),
|
||||
(RegistrationStage.EMAIL_ENTRY, lambda: self._enter_email(account_data["email"])),
|
||||
(RegistrationStage.PASSWORD_ENTRY, lambda: self._enter_password(account_data["password"])),
|
||||
(RegistrationStage.CODE_SENDING, self._send_verification_code),
|
||||
(RegistrationStage.CODE_VERIFICATION, lambda: self._verify_code(account_data["email"])),
|
||||
(RegistrationStage.USERNAME_ENTRY, lambda: self._handle_username(account_data)),
|
||||
(RegistrationStage.COMPLETION, self._complete_registration)
|
||||
]
|
||||
|
||||
for stage, handler in stages:
|
||||
self._current_stage = stage
|
||||
self._emit_progress(f"Processing: {stage.value}")
|
||||
|
||||
if not self._execute_with_retry(handler):
|
||||
return self._create_error_result(
|
||||
f"Failed at stage: {stage.value}",
|
||||
stage,
|
||||
account_data
|
||||
)
|
||||
|
||||
self._add_human_delay()
|
||||
|
||||
logger.info(f"Successfully created TikTok account: {account_data['username']}")
|
||||
return self._create_success_result(account_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during registration: {e}", exc_info=True)
|
||||
return self._create_error_result(str(e), self._current_stage)
|
||||
|
||||
# ========== VALIDATION METHODS ==========
|
||||
|
||||
def _validate_inputs(
|
||||
self,
|
||||
full_name: str,
|
||||
age: int,
|
||||
method: str,
|
||||
phone: Optional[str]
|
||||
) -> bool:
|
||||
"""Validate registration inputs."""
|
||||
if not full_name or len(full_name.strip()) < 2:
|
||||
logger.error("Invalid name provided")
|
||||
return False
|
||||
|
||||
if age < 13:
|
||||
logger.error("Age must be at least 13 for TikTok")
|
||||
return False
|
||||
|
||||
if method not in ["email", "phone"]:
|
||||
logger.error(f"Invalid registration method: {method}")
|
||||
return False
|
||||
|
||||
if method == "phone" and not phone:
|
||||
logger.error("Phone number required for phone registration")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# ========== DATA GENERATION ==========
|
||||
|
||||
def _generate_account_data(
|
||||
self,
|
||||
full_name: str,
|
||||
age: int,
|
||||
method: str,
|
||||
phone: Optional[str],
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate account data for registration."""
|
||||
birthday = self.automation.birthday_generator.generate_birthday_components("tiktok", age)
|
||||
password = kwargs.get("password") or self.automation.password_generator.generate_password()
|
||||
username = kwargs.get("username") or self.automation.username_generator.generate_username(full_name)
|
||||
email = kwargs.get("email") or self._generate_email(username)
|
||||
|
||||
return {
|
||||
"full_name": full_name,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"birthday": birthday,
|
||||
"age": age,
|
||||
"registration_method": method,
|
||||
"phone_number": phone
|
||||
}
|
||||
|
||||
def _generate_email(self, username: str) -> str:
|
||||
"""Generate email address for account."""
|
||||
return f"{username}@{self.automation.email_domain}"
|
||||
|
||||
# ========== NAVIGATION STAGES ==========
|
||||
|
||||
def _navigate_to_tiktok(self) -> bool:
|
||||
"""Navigate to TikTok homepage."""
|
||||
try:
|
||||
self._emit_progress("Navigating to TikTok...")
|
||||
page = self.automation.browser.page
|
||||
page.goto("https://www.tiktok.com/", wait_until="domcontentloaded", timeout=30000)
|
||||
|
||||
# Verify navigation success
|
||||
if not self._wait_for_element_any([
|
||||
"button#header-login-button",
|
||||
"button.TUXButton:has-text('Anmelden')",
|
||||
"button:has-text('Log in')"
|
||||
]):
|
||||
logger.error("Failed to load TikTok homepage")
|
||||
return False
|
||||
|
||||
logger.info("Successfully navigated to TikTok")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Navigation failed: {e}")
|
||||
return False
|
||||
|
||||
def _handle_cookies(self) -> bool:
|
||||
"""Handle cookie consent banner if present."""
|
||||
try:
|
||||
cookie_selectors = [
|
||||
"button:has-text('Alle Cookies akzeptieren')",
|
||||
"button:has-text('Accept all')",
|
||||
"button:has-text('Alle ablehnen')",
|
||||
"button:has-text('Reject all')"
|
||||
]
|
||||
|
||||
for selector in cookie_selectors:
|
||||
if self._click_if_visible(selector, timeout=2000):
|
||||
logger.info("Cookie banner handled")
|
||||
return True
|
||||
|
||||
logger.debug("No cookie banner found")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Cookie handling: {e}")
|
||||
return True
|
||||
|
||||
def _click_login(self) -> bool:
|
||||
"""Click the login button."""
|
||||
return self._click_element_with_fallback(
|
||||
primary_selectors=[
|
||||
"button#header-login-button",
|
||||
"button.TUXButton:has-text('Anmelden')",
|
||||
"button:has-text('Log in')"
|
||||
],
|
||||
fallback_text=["Anmelden", "Log in", "Sign in"],
|
||||
stage_name="login button"
|
||||
)
|
||||
|
||||
def _click_register(self) -> bool:
|
||||
"""Click the register link."""
|
||||
return self._click_element_with_fallback(
|
||||
primary_selectors=[
|
||||
"a:text('Registrieren')",
|
||||
"button:text('Registrieren')",
|
||||
"span:text('Registrieren')",
|
||||
"a:text('Sign up')",
|
||||
"span:text('Sign up')"
|
||||
],
|
||||
fallback_text=["Registrieren", "Sign up", "Register"],
|
||||
stage_name="register link"
|
||||
)
|
||||
|
||||
def _select_phone_email_option(self) -> bool:
|
||||
"""Select phone/email registration option."""
|
||||
return self._click_element_with_fallback(
|
||||
primary_selectors=[
|
||||
"div:has-text('Telefonnummer oder E-Mail')",
|
||||
"div:has-text('Use phone or email')",
|
||||
"button:has-text('Phone or email')"
|
||||
],
|
||||
fallback_text=["Telefonnummer oder E-Mail", "Use phone or email", "Phone or email"],
|
||||
stage_name="phone/email option",
|
||||
use_fuzzy=True
|
||||
)
|
||||
|
||||
def _select_method(self, method: str) -> bool:
|
||||
"""Select specific registration method (email or phone)."""
|
||||
if method == "email":
|
||||
# Check if already on email page
|
||||
if self._is_element_visible(self.selectors.EMAIL_FIELD, timeout=1000):
|
||||
logger.info("Already on email registration page")
|
||||
return True
|
||||
|
||||
return self._click_element_with_fallback(
|
||||
primary_selectors=[
|
||||
"a:has-text('Mit E-Mail-Adresse registrieren')",
|
||||
"a:has-text('Sign up with email')",
|
||||
"button:has-text('E-Mail')"
|
||||
],
|
||||
fallback_text=["E-Mail", "Email"],
|
||||
stage_name="email method"
|
||||
)
|
||||
|
||||
return False # Phone method not fully implemented
|
||||
|
||||
# ========== FORM FILLING STAGES ==========
|
||||
|
||||
def _enter_birthday(self, birthday: Dict[str, int]) -> bool:
|
||||
"""Enter birthday using dropdowns."""
|
||||
try:
|
||||
self._emit_progress("Entering birthday...")
|
||||
|
||||
# Month selection
|
||||
if not self._select_dropdown_option(
|
||||
dropdown_text="Monat",
|
||||
option_value=self._get_month_name(birthday["month"])
|
||||
):
|
||||
return False
|
||||
|
||||
# Day selection
|
||||
if not self._select_dropdown_option(
|
||||
dropdown_text="Tag",
|
||||
option_value=str(birthday["day"])
|
||||
):
|
||||
return False
|
||||
|
||||
# Year selection
|
||||
if not self._select_dropdown_option(
|
||||
dropdown_text="Jahr",
|
||||
option_value=str(birthday["year"])
|
||||
):
|
||||
return False
|
||||
|
||||
logger.info(f"Birthday entered: {birthday['month']}/{birthday['day']}/{birthday['year']}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Birthday entry failed: {e}")
|
||||
return False
|
||||
|
||||
def _enter_email(self, email: str) -> bool:
|
||||
"""Enter email address."""
|
||||
return self._fill_input_field(
|
||||
field_selectors=[
|
||||
"input[placeholder='E-Mail-Adresse']",
|
||||
"input[name='email']",
|
||||
"input[type='email']"
|
||||
],
|
||||
value=email,
|
||||
field_name="email"
|
||||
)
|
||||
|
||||
def _enter_password(self, password: str) -> bool:
|
||||
"""Enter password with proper selectors."""
|
||||
return self._fill_input_field(
|
||||
field_selectors=[
|
||||
"input.css-ujllvj-InputContainer[type='password']",
|
||||
"input[type='password'][autocomplete='new-password']",
|
||||
"input.etcs7ny1[type='password']",
|
||||
"input[type='password'][placeholder='Passwort']",
|
||||
"input[type='password']"
|
||||
],
|
||||
value=password,
|
||||
field_name="password",
|
||||
validate=True
|
||||
)
|
||||
|
||||
def _send_verification_code(self) -> bool:
|
||||
"""Click send code button."""
|
||||
self._emit_progress("Requesting verification code...")
|
||||
|
||||
return self._click_element_with_fallback(
|
||||
primary_selectors=[
|
||||
"button[data-e2e='send-code-button']",
|
||||
"button:has-text('Code senden')",
|
||||
"button:has-text('Send code')"
|
||||
],
|
||||
fallback_text=["Code senden", "Send code"],
|
||||
stage_name="send code button",
|
||||
wait_enabled=True
|
||||
)
|
||||
|
||||
def _verify_code(self, email: str) -> bool:
|
||||
"""Handle email verification code."""
|
||||
try:
|
||||
self._emit_progress("Waiting for verification code...")
|
||||
|
||||
# Get verification code from email
|
||||
code = self._get_verification_code(email)
|
||||
if not code:
|
||||
logger.error("Failed to retrieve verification code")
|
||||
return False
|
||||
|
||||
# Enter verification code
|
||||
return self._fill_input_field(
|
||||
field_selectors=[
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[placeholder*='6-digit code']",
|
||||
"input[maxlength='6']"
|
||||
],
|
||||
value=code,
|
||||
field_name="verification code"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Code verification failed: {e}")
|
||||
return False
|
||||
|
||||
def _get_verification_code(self, email: str) -> Optional[str]:
|
||||
"""Retrieve verification code from email."""
|
||||
max_attempts = 30
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
code = self.automation.email_handler.get_verification_code(
|
||||
target_email=email,
|
||||
platform="tiktok",
|
||||
max_attempts=1,
|
||||
delay_seconds=2
|
||||
)
|
||||
|
||||
if code and len(code) == 6 and code.isdigit():
|
||||
logger.info(f"Verification code retrieved: {code}")
|
||||
return code
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Attempt {attempt + 1} failed: {e}")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
return None
|
||||
|
||||
def _handle_username(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""Handle username step (usually skipped)."""
|
||||
try:
|
||||
# Try to skip username selection
|
||||
skip_selectors = [
|
||||
"button:has-text('Überspringen')",
|
||||
"button:has-text('Skip')",
|
||||
"a:has-text('Skip')"
|
||||
]
|
||||
|
||||
for selector in skip_selectors:
|
||||
if self._click_if_visible(selector, timeout=2000):
|
||||
logger.info("Username step skipped")
|
||||
return True
|
||||
|
||||
# If can't skip, try to continue
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Username handling: {e}")
|
||||
return True
|
||||
|
||||
def _complete_registration(self) -> bool:
|
||||
"""Complete registration and verify success."""
|
||||
try:
|
||||
self._emit_progress("Finalizing account...")
|
||||
|
||||
# Click any final continue buttons
|
||||
continue_selectors = [
|
||||
"button:has-text('Weiter')",
|
||||
"button:has-text('Continue')",
|
||||
"button:has-text('Next')"
|
||||
]
|
||||
|
||||
for selector in continue_selectors:
|
||||
self._click_if_visible(selector, timeout=1000)
|
||||
|
||||
# Check for success indicators
|
||||
success_indicators = [
|
||||
"a[href='/foryou']",
|
||||
"button[data-e2e='profile-icon']",
|
||||
"[aria-label='Profile']"
|
||||
]
|
||||
|
||||
for indicator in success_indicators:
|
||||
if self._is_element_visible(indicator, timeout=5000):
|
||||
logger.info("Registration completed successfully")
|
||||
return True
|
||||
|
||||
# Even if indicators not found, might still be successful
|
||||
logger.warning("Success indicators not found, assuming success")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Completion check failed: {e}")
|
||||
return False
|
||||
|
||||
# ========== HELPER METHODS ==========
|
||||
|
||||
def _execute_with_retry(self, handler, max_retries: Optional[int] = None) -> bool:
|
||||
"""Execute handler with retry logic."""
|
||||
retries = max_retries or self.config.max_retries
|
||||
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
if handler():
|
||||
return True
|
||||
|
||||
if attempt < retries - 1:
|
||||
logger.debug(f"Retrying... ({attempt + 2}/{retries})")
|
||||
time.sleep(self.config.retry_delay * (attempt + 1))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Handler error: {e}")
|
||||
if attempt == retries - 1:
|
||||
raise
|
||||
|
||||
return False
|
||||
|
||||
def _click_element_with_fallback(
|
||||
self,
|
||||
primary_selectors: List[str],
|
||||
fallback_text: List[str],
|
||||
stage_name: str,
|
||||
use_fuzzy: bool = False,
|
||||
wait_enabled: bool = False
|
||||
) -> bool:
|
||||
"""Click element with multiple fallback strategies."""
|
||||
# Try primary selectors
|
||||
for selector in primary_selectors:
|
||||
if self._click_if_visible(selector):
|
||||
logger.info(f"Clicked {stage_name} with selector: {selector}")
|
||||
return True
|
||||
|
||||
# Try fuzzy matching if enabled
|
||||
if use_fuzzy and hasattr(self.automation, 'ui_helper'):
|
||||
try:
|
||||
if self.automation.ui_helper.click_button_fuzzy(
|
||||
fallback_text,
|
||||
primary_selectors[0] if primary_selectors else None
|
||||
):
|
||||
logger.info(f"Clicked {stage_name} using fuzzy matching")
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.error(f"Failed to click {stage_name}")
|
||||
return False
|
||||
|
||||
def _fill_input_field(
|
||||
self,
|
||||
field_selectors: List[str],
|
||||
value: str,
|
||||
field_name: str,
|
||||
validate: bool = False
|
||||
) -> bool:
|
||||
"""Fill input field with value."""
|
||||
for selector in field_selectors:
|
||||
try:
|
||||
if self._is_element_visible(selector, timeout=2000):
|
||||
element = self.automation.browser.page.locator(selector).first
|
||||
element.click()
|
||||
element.fill("")
|
||||
element.type(value, delay=50)
|
||||
|
||||
if validate:
|
||||
actual = element.input_value()
|
||||
if actual != value:
|
||||
logger.warning(f"Validation failed for {field_name}")
|
||||
continue
|
||||
|
||||
logger.info(f"Filled {field_name} field")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed with selector {selector}: {e}")
|
||||
|
||||
logger.error(f"Failed to fill {field_name} field")
|
||||
return False
|
||||
|
||||
def _select_dropdown_option(self, dropdown_text: str, option_value: str) -> bool:
|
||||
"""Select option from dropdown."""
|
||||
try:
|
||||
# Click dropdown
|
||||
dropdown_selector = f"div:has-text('{dropdown_text}')"
|
||||
if not self._click_if_visible(dropdown_selector):
|
||||
return False
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Click option
|
||||
option_selector = f"div.css-vz5m7n-DivOption:has-text('{option_value}')"
|
||||
if not self._click_if_visible(option_selector):
|
||||
# Try alternative selector
|
||||
option_selector = f"div[role='option']:has-text('{option_value}')"
|
||||
if not self._click_if_visible(option_selector):
|
||||
return False
|
||||
|
||||
logger.info(f"Selected {option_value} from {dropdown_text} dropdown")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Dropdown selection failed: {e}")
|
||||
return False
|
||||
|
||||
def _click_if_visible(self, selector: str, timeout: int = None) -> bool:
|
||||
"""Click element if visible."""
|
||||
try:
|
||||
timeout = timeout or self.config.timeout
|
||||
if self.automation.browser.is_element_visible(selector, timeout=timeout):
|
||||
self.automation.browser.click_element(selector)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _is_element_visible(self, selector: str, timeout: int = None) -> bool:
|
||||
"""Check if element is visible."""
|
||||
try:
|
||||
timeout = timeout or self.config.timeout
|
||||
return self.automation.browser.is_element_visible(selector, timeout=timeout)
|
||||
except:
|
||||
return False
|
||||
|
||||
def _wait_for_element_any(self, selectors: List[str], timeout: int = None) -> bool:
|
||||
"""Wait for any of the selectors to be visible."""
|
||||
timeout = timeout or self.config.timeout
|
||||
end_time = time.time() + (timeout / 1000)
|
||||
|
||||
while time.time() < end_time:
|
||||
for selector in selectors:
|
||||
if self._is_element_visible(selector, timeout=500):
|
||||
return True
|
||||
time.sleep(0.5)
|
||||
|
||||
return False
|
||||
|
||||
def _add_human_delay(self):
|
||||
"""Add human-like delay between actions."""
|
||||
if hasattr(self.automation, 'human_behavior'):
|
||||
self.automation.human_behavior.random_delay(
|
||||
self.config.human_delay_min,
|
||||
self.config.human_delay_max
|
||||
)
|
||||
else:
|
||||
time.sleep(self.config.human_delay_min)
|
||||
|
||||
def _emit_progress(self, message: str):
|
||||
"""Emit progress message to UI."""
|
||||
if hasattr(self.automation, '_emit_customer_log'):
|
||||
self.automation._emit_customer_log(message)
|
||||
logger.info(message)
|
||||
|
||||
def _get_month_name(self, month: int) -> str:
|
||||
"""Convert month number to name."""
|
||||
months = {
|
||||
1: "Januar", 2: "Februar", 3: "März", 4: "April",
|
||||
5: "Mai", 6: "Juni", 7: "Juli", 8: "August",
|
||||
9: "September", 10: "Oktober", 11: "November", 12: "Dezember"
|
||||
}
|
||||
return months.get(month, str(month))
|
||||
|
||||
# ========== RESULT CREATION ==========
|
||||
|
||||
def _create_success_result(self, account_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create success result dictionary."""
|
||||
return {
|
||||
"success": True,
|
||||
"stage": RegistrationStage.COMPLETION.value,
|
||||
"account_data": account_data,
|
||||
"message": f"Account {account_data['username']} successfully created"
|
||||
}
|
||||
|
||||
def _create_error_result(
|
||||
self,
|
||||
error: str,
|
||||
stage: Optional[RegistrationStage] = None,
|
||||
account_data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create error result dictionary."""
|
||||
return {
|
||||
"success": False,
|
||||
"error": error,
|
||||
"stage": stage.value if stage else "unknown",
|
||||
"account_data": account_data or {}
|
||||
}
|
||||
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
@ -1,801 +0,0 @@
|
||||
# social_networks/tiktok/tiktok_registration_new.py
|
||||
|
||||
"""
|
||||
TikTok-Registrierung - Optimierte Klasse für die Kontoerstellung bei TikTok
|
||||
NEUE IMPLEMENTIERUNG mit korrekter Workflow-Reihenfolge für maximale Stabilität.
|
||||
|
||||
OPTIMIERTER WORKFLOW:
|
||||
1. E-Mail eingeben
|
||||
2. Passwort eingeben
|
||||
3. Code senden Button klicken
|
||||
4. Code empfangen und eingeben
|
||||
5. Weiter Button wird automatisch aktiviert
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
from .tiktok_selectors import TikTokSelectors
|
||||
from .tiktok_workflow import TikTokWorkflow
|
||||
from utils.logger import setup_logger
|
||||
|
||||
# Konfiguriere Logger
|
||||
logger = setup_logger("tiktok_registration")
|
||||
|
||||
class TikTokRegistration:
|
||||
"""
|
||||
Optimierte Klasse für die Registrierung von TikTok-Konten.
|
||||
Implementiert einen robusten, zukunftssicheren Workflow.
|
||||
"""
|
||||
|
||||
def __init__(self, automation):
|
||||
"""
|
||||
Initialisiert die TikTok-Registrierung.
|
||||
|
||||
Args:
|
||||
automation: Referenz auf die Hauptautomatisierungsklasse
|
||||
"""
|
||||
self.automation = automation
|
||||
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
|
||||
"""
|
||||
# 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 optimierten TikTok-Registrierungsprozess für {account_data['username']} via {registration_method}")
|
||||
|
||||
try:
|
||||
# 1. Zur Startseite navigieren
|
||||
self.automation._emit_customer_log("🌐 Mit TikTok verbinden...")
|
||||
if not self._navigate_to_homepage():
|
||||
return self._create_error_result("Konnte nicht zur TikTok-Startseite navigieren", "navigation", account_data)
|
||||
|
||||
# 2. Cookie-Banner behandeln
|
||||
self.automation._emit_customer_log("⚙️ Einstellungen werden vorbereitet...")
|
||||
self._handle_cookie_banner()
|
||||
|
||||
# 3. Anmelden-Button klicken
|
||||
self.automation._emit_customer_log("📋 Registrierungsformular wird geöffnet...")
|
||||
if not self._click_login_button():
|
||||
return self._create_error_result("Konnte nicht auf Anmelden-Button klicken", "login_button", account_data)
|
||||
|
||||
# 4. Registrieren-Link klicken
|
||||
if not self._click_register_link():
|
||||
return self._create_error_result("Konnte nicht auf Registrieren-Link klicken", "register_link", account_data)
|
||||
|
||||
# 5. Telefon/E-Mail-Option auswählen
|
||||
if not self._click_phone_email_option():
|
||||
return self._create_error_result("Konnte nicht auf Telefon/E-Mail-Option klicken", "phone_email_option", account_data)
|
||||
|
||||
# 6. E-Mail oder Telefon als Registrierungsmethode wählen
|
||||
if not self._select_registration_method(registration_method):
|
||||
return self._create_error_result(f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen", "registration_method", account_data)
|
||||
|
||||
# 7. Geburtsdatum eingeben
|
||||
self.automation._emit_customer_log("🎂 Geburtsdatum wird festgelegt...")
|
||||
if not self._enter_birthday(account_data["birthday"]):
|
||||
return self._create_error_result("Fehler beim Eingeben des Geburtsdatums", "birthday", account_data)
|
||||
|
||||
# 8. OPTIMIERTER REGISTRIERUNGSWORKFLOW
|
||||
self.automation._emit_customer_log("📝 Persönliche Daten werden übertragen...")
|
||||
if not self._execute_optimized_registration_workflow(account_data, registration_method):
|
||||
return self._create_error_result("Fehler im Registrierungsworkflow", "registration_workflow", account_data)
|
||||
|
||||
# 9. Benutzernamen erstellen
|
||||
self.automation._emit_customer_log("👤 Benutzername wird erstellt...")
|
||||
if not self._create_username(account_data):
|
||||
return self._create_error_result("Fehler beim Erstellen des Benutzernamens", "username", account_data)
|
||||
|
||||
# 10. Erfolgreiche Registrierung überprüfen
|
||||
self.automation._emit_customer_log("🔍 Account wird finalisiert...")
|
||||
if not self._check_registration_success():
|
||||
return self._create_error_result("Registrierung fehlgeschlagen oder konnte nicht verifiziert werden", "final_check", account_data)
|
||||
|
||||
# Registrierung erfolgreich abgeschlossen
|
||||
logger.info(f"TikTok-Account {account_data['username']} erfolgreich erstellt")
|
||||
self.automation._emit_customer_log("✅ Account 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 _execute_optimized_registration_workflow(self, account_data: Dict[str, Any], registration_method: str) -> bool:
|
||||
"""
|
||||
Führt den optimierten Registrierungsworkflow aus.
|
||||
|
||||
KORRIGIERTE REIHENFOLGE für E-Mail-Registrierung:
|
||||
1. E-Mail eingeben
|
||||
2. Code senden Button klicken
|
||||
3. Code empfangen und eingeben
|
||||
4. Passwort eingeben
|
||||
5. Dummy-Input-Trick anwenden
|
||||
6. Weiter Button klicken
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
registration_method: "email" oder "phone"
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
if registration_method == "email":
|
||||
return self._execute_email_workflow(account_data)
|
||||
elif registration_method == "phone":
|
||||
return self._execute_phone_workflow(account_data)
|
||||
else:
|
||||
logger.error(f"Unbekannte Registrierungsmethode: {registration_method}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im optimierten Registrierungsworkflow: {e}")
|
||||
return False
|
||||
|
||||
def _execute_email_workflow(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Führt den optimierten E-Mail-Registrierungsworkflow aus.
|
||||
|
||||
KORRIGIERTER WORKFLOW: E-Mail → Code senden → Code eingeben → Passwort → Dummy-Trick → Weiter
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("=== STARTE OPTIMIERTEN E-MAIL-WORKFLOW ===")
|
||||
|
||||
# SCHRITT 1: E-Mail-Feld ausfüllen
|
||||
logger.info("SCHRITT 1/6: E-Mail-Adresse eingeben")
|
||||
if not self._fill_email_field(account_data["email"]):
|
||||
logger.error("Fehler beim Ausfüllen des E-Mail-Feldes")
|
||||
return False
|
||||
|
||||
# SCHRITT 2: Code senden Button klicken (VOR Passwort!)
|
||||
logger.info("SCHRITT 2/6: Code senden Button klicken")
|
||||
if not self._click_send_code_button():
|
||||
logger.error("Fehler beim Klicken des Code-senden-Buttons")
|
||||
return False
|
||||
|
||||
# SCHRITT 3: Verifizierungscode empfangen und eingeben
|
||||
logger.info("SCHRITT 3/6: Auf Code warten und eingeben")
|
||||
if not self._handle_email_verification(account_data["email"]):
|
||||
logger.error("Fehler bei der E-Mail-Verifizierung")
|
||||
return False
|
||||
|
||||
# SCHRITT 4: Passwort-Feld ausfüllen (NACH Code-Eingabe!)
|
||||
logger.info("SCHRITT 4/6: Passwort eingeben (nach Code-Verifizierung)")
|
||||
if not self._fill_password_field(account_data["password"]):
|
||||
logger.error("Fehler beim Ausfüllen des Passwort-Feldes")
|
||||
return False
|
||||
|
||||
# SCHRITT 5: Dummy-Input-Trick anwenden
|
||||
logger.info("SCHRITT 5/6: Dummy-Input-Trick anwenden")
|
||||
if not self._apply_dummy_input_trick():
|
||||
logger.error("Fehler beim Dummy-Input-Trick")
|
||||
return False
|
||||
|
||||
# SCHRITT 6: Weiter Button klicken
|
||||
logger.info("SCHRITT 6/6: Weiter Button klicken")
|
||||
if not self._click_continue_button():
|
||||
logger.error("Fehler beim Klicken des Weiter-Buttons")
|
||||
return False
|
||||
|
||||
logger.info("=== E-MAIL-WORKFLOW ERFOLGREICH ABGESCHLOSSEN ===")
|
||||
|
||||
# Kurze Pause für UI-Updates - das Weiter-Button sollte jetzt aktiviert sein
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im E-Mail-Workflow: {e}")
|
||||
return False
|
||||
|
||||
def _fill_email_field(self, email: str) -> bool:
|
||||
"""
|
||||
Füllt das E-Mail-Feld mit robusten Selektoren aus.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste E-Mail-Feld-Selektoren (in Prioritätsreihenfolge)
|
||||
email_selectors = [
|
||||
"input[placeholder*='E-Mail']",
|
||||
"input[placeholder*='Email']",
|
||||
"input[type='email']",
|
||||
"input[name='email']",
|
||||
"input[aria-label*='Email']",
|
||||
"input[aria-label*='E-Mail']",
|
||||
self.selectors.EMAIL_FIELD,
|
||||
self.selectors.EMAIL_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(email_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
success = self.automation.browser.fill_form_field(selector, email, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"E-Mail-Feld erfolgreich ausgefüllt mit Selektor {i+1}: {email}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"E-Mail-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["E-Mail-Adresse", "Email", "E-Mail"],
|
||||
email,
|
||||
email_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"E-Mail-Feld über Fuzzy-Matching ausgefüllt: {email}")
|
||||
return True
|
||||
|
||||
logger.error("Konnte E-Mail-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des E-Mail-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _fill_password_field(self, password: str) -> bool:
|
||||
"""
|
||||
Füllt das Passwort-Feld mit robusten Selektoren aus.
|
||||
|
||||
Args:
|
||||
password: Passwort
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste Passwort-Feld-Selektoren (in Prioritätsreihenfolge)
|
||||
password_selectors = [
|
||||
"input[type='password'][placeholder*='Passwort']",
|
||||
"input[type='password'][placeholder*='Password']",
|
||||
"input[type='password']",
|
||||
"input[name='password']",
|
||||
"input[aria-label*='Password']",
|
||||
"input[aria-label*='Passwort']",
|
||||
self.selectors.PASSWORD_FIELD,
|
||||
self.selectors.PASSWORD_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(password_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
success = self.automation.browser.fill_form_field(selector, password, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"Passwort-Feld erfolgreich ausgefüllt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Passwort-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Passwort", "Password"],
|
||||
password,
|
||||
password_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Passwort-Feld über Fuzzy-Matching ausgefüllt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte Passwort-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Passwort-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _click_send_code_button(self) -> bool:
|
||||
"""
|
||||
Klickt den 'Code senden'-Button mit robusten Selektoren.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Kurze Pause vor dem Klicken
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
|
||||
# Robuste Send-Code-Button-Selektoren
|
||||
send_code_selectors = [
|
||||
"button[data-e2e='send-code-button']",
|
||||
"button:has-text('Code senden')",
|
||||
"button:has-text('Send code')",
|
||||
"button[type='submit']",
|
||||
"button.css-10nhlj9-Button-StyledButton",
|
||||
self.selectors.SEND_CODE_BUTTON
|
||||
]
|
||||
|
||||
for i, selector in enumerate(send_code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Prüfe, ob Button enabled ist
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
||||
if element:
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
if is_disabled:
|
||||
logger.debug(f"Send-Code-Button {i+1} ist disabled, versuche nächsten")
|
||||
continue
|
||||
|
||||
success = self.automation.browser.click_element(selector)
|
||||
if success:
|
||||
logger.info(f"'Code senden'-Button erfolgreich geklickt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Send-Code-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Button-Matching
|
||||
success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Code senden", "Send code", "Senden"],
|
||||
send_code_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("'Code senden'-Button über Fuzzy-Matching geklickt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte 'Code senden'-Button mit keinem Selektor klicken")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Klicken des 'Code senden'-Buttons: {e}")
|
||||
return False
|
||||
|
||||
def _handle_email_verification(self, email: str) -> bool:
|
||||
"""
|
||||
Behandelt die E-Mail-Verifizierung mit verbessertem Timing.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("Warte auf E-Mail-Verifizierungscode...")
|
||||
|
||||
# Warte auf den Code mit exponential backoff
|
||||
verification_code = self._get_email_verification_code_with_retry(email)
|
||||
|
||||
if not verification_code:
|
||||
logger.error("Konnte keinen Verifizierungscode empfangen")
|
||||
return False
|
||||
|
||||
logger.info(f"Verifizierungscode empfangen: {verification_code}")
|
||||
|
||||
# Code-Feld ausfüllen
|
||||
if not self._fill_verification_code_field(verification_code):
|
||||
logger.error("Konnte Verifizierungscode-Feld nicht ausfüllen")
|
||||
return False
|
||||
|
||||
logger.info("Verifizierungscode erfolgreich eingegeben")
|
||||
|
||||
# Kurze Pause nach Code-Eingabe
|
||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der E-Mail-Verifizierung: {e}")
|
||||
return False
|
||||
|
||||
def _get_email_verification_code_with_retry(self, email: str, max_attempts: int = 30) -> Optional[str]:
|
||||
"""
|
||||
Ruft den E-Mail-Verifizierungscode mit Retry-Logik ab.
|
||||
|
||||
Args:
|
||||
email: E-Mail-Adresse
|
||||
max_attempts: Maximale Anzahl Versuche
|
||||
|
||||
Returns:
|
||||
Optional[str]: Verifizierungscode oder None
|
||||
"""
|
||||
try:
|
||||
for attempt in range(max_attempts):
|
||||
# Exponential backoff: 2s, 3s, 4.5s, 6.75s, ... (max 30s)
|
||||
delay = min(2 * (1.5 ** attempt), 30)
|
||||
|
||||
logger.debug(f"E-Mail-Abruf Versuch {attempt + 1}/{max_attempts} (Wartezeit: {delay:.1f}s)")
|
||||
|
||||
# Versuche Code abzurufen
|
||||
code = self.automation.email_handler.get_verification_code(
|
||||
target_email=email,
|
||||
platform="tiktok",
|
||||
max_attempts=1, # Nur ein Versuch pro Iteration
|
||||
delay_seconds=1
|
||||
)
|
||||
|
||||
if code:
|
||||
logger.info(f"E-Mail-Code nach {attempt + 1} Versuchen empfangen")
|
||||
return code
|
||||
|
||||
# Warte vor nächstem Versuch
|
||||
time.sleep(delay)
|
||||
|
||||
logger.warning(f"Kein E-Mail-Code nach {max_attempts} Versuchen empfangen")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim E-Mail-Code-Abruf: {e}")
|
||||
return None
|
||||
|
||||
def _fill_verification_code_field(self, code: str) -> bool:
|
||||
"""
|
||||
Füllt das Verifizierungscode-Feld aus.
|
||||
|
||||
Args:
|
||||
code: Verifizierungscode
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# Robuste Verifizierungscode-Feld-Selektoren
|
||||
code_selectors = [
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[placeholder*='verification code']",
|
||||
"input[placeholder*='Code']",
|
||||
"input[name='verificationCode']",
|
||||
"input[type='text'][maxlength='6']",
|
||||
self.selectors.VERIFICATION_CODE_FIELD,
|
||||
self.selectors.VERIFICATION_CODE_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
# Normale Code-Eingabe (Dummy-Trick wird separat angewendet)
|
||||
success = self.automation.browser.fill_form_field(selector, code, human_typing=True)
|
||||
if success:
|
||||
logger.info(f"Verifizierungscode-Feld erfolgreich ausgefüllt mit Selektor {i+1}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Code-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Matching
|
||||
success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Gib den sechsstelligen Code ein", "Enter verification code", "Verification code"],
|
||||
code,
|
||||
code_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Verifizierungscode-Feld über Fuzzy-Matching ausgefüllt")
|
||||
return True
|
||||
|
||||
logger.error("Konnte Verifizierungscode-Feld mit keinem Selektor ausfüllen")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Ausfüllen des Verifizierungscode-Feldes: {e}")
|
||||
return False
|
||||
|
||||
def _execute_phone_workflow(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Führt den Telefon-Registrierungsworkflow aus.
|
||||
|
||||
Args:
|
||||
account_data: Account-Daten
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.info("=== STARTE TELEFON-WORKFLOW ===")
|
||||
|
||||
# Telefonnummer aufbereiten
|
||||
phone_number = account_data["phone"]
|
||||
if phone_number.startswith("+"):
|
||||
parts = phone_number.split(" ", 1)
|
||||
if len(parts) > 1:
|
||||
phone_number = parts[1]
|
||||
|
||||
# Telefonnummer eingeben
|
||||
phone_success = self.automation.ui_helper.fill_field_fuzzy(
|
||||
["Telefonnummer", "Phone number", "Phone"],
|
||||
phone_number,
|
||||
self.selectors.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)
|
||||
|
||||
# Code senden
|
||||
if not self._click_send_code_button():
|
||||
return False
|
||||
|
||||
# SMS-Code behandeln (Platzhalter)
|
||||
logger.warning("SMS-Verifizierung ist noch nicht vollständig implementiert")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler im Telefon-Workflow: {e}")
|
||||
return False
|
||||
|
||||
# Hilfsmethoden für die Basis-Funktionalität
|
||||
def _validate_registration_inputs(self, full_name: str, age: int,
|
||||
registration_method: str, phone_number: str) -> bool:
|
||||
"""Validiert die Eingaben für die Registrierung."""
|
||||
if not full_name or len(full_name) < 3:
|
||||
logger.error("Ungültiger vollständiger Name")
|
||||
return False
|
||||
|
||||
if age < 13:
|
||||
logger.error("Benutzer muss mindestens 13 Jahre alt sein")
|
||||
return False
|
||||
|
||||
if registration_method not in ["email", "phone"]:
|
||||
logger.error(f"Ungültige Registrierungsmethode: {registration_method}")
|
||||
return False
|
||||
|
||||
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."""
|
||||
# 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 _create_error_result(self, error_msg: str, stage: str, account_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Erstellt ein standardisiertes Fehler-Result."""
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"stage": stage,
|
||||
"account_data": account_data
|
||||
}
|
||||
|
||||
# Platzhalter für weitere Methoden (Navigation, etc.)
|
||||
def _navigate_to_homepage(self) -> bool:
|
||||
"""Navigiert zur TikTok-Startseite."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _handle_cookie_banner(self) -> bool:
|
||||
"""Behandelt den Cookie-Banner."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_login_button(self) -> bool:
|
||||
"""Klickt auf den Anmelden-Button."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_register_link(self) -> bool:
|
||||
"""Klickt auf den Registrieren-Link."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _click_phone_email_option(self) -> bool:
|
||||
"""Klickt auf die Telefon/E-Mail-Option."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _select_registration_method(self, method: str) -> bool:
|
||||
"""Wählt die Registrierungsmethode aus."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _enter_birthday(self, birthday: Dict[str, Any]) -> bool:
|
||||
"""Gibt das Geburtsdatum ein."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _create_username(self, account_data: Dict[str, Any]) -> bool:
|
||||
"""Erstellt einen Benutzernamen."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _check_registration_success(self) -> bool:
|
||||
"""Überprüft, ob die Registrierung erfolgreich war."""
|
||||
# Diese Methode würde aus der ursprünglichen Implementierung übernommen
|
||||
return True
|
||||
|
||||
def _apply_dummy_input_trick(self) -> bool:
|
||||
"""
|
||||
Wendet den Dummy-Input-Trick auf das Code-Feld an.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.debug("Wende Dummy-Input-Trick an")
|
||||
|
||||
# Code-Feld-Selektoren
|
||||
code_selectors = [
|
||||
"input[placeholder*='sechsstelligen Code']",
|
||||
"input[placeholder*='verification code']",
|
||||
"input[placeholder*='Code']",
|
||||
"input[name='verificationCode']",
|
||||
"input[type='text'][maxlength='6']",
|
||||
self.selectors.VERIFICATION_CODE_FIELD,
|
||||
self.selectors.VERIFICATION_CODE_FIELD_ALT
|
||||
]
|
||||
|
||||
for i, selector in enumerate(code_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=2000):
|
||||
# Dummy-Input-Trick anwenden
|
||||
success = self.automation.browser.fill_form_field_with_dummy_trick(
|
||||
selector, "123456", timeout=3000
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"Dummy-Input-Trick erfolgreich angewendet mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Dummy-Input-Trick Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
logger.warning("Dummy-Input-Trick konnte nicht angewendet werden")
|
||||
return True # Nicht kritisch - fortfahren
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Kritischer Fehler beim Dummy-Input-Trick: {e}")
|
||||
return True # Nicht kritisch - fortfahren
|
||||
|
||||
def _click_continue_button(self) -> bool:
|
||||
"""
|
||||
Klickt den Weiter/Continue-Button mit robusten Selektoren.
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
logger.debug("Klicke Weiter-Button")
|
||||
|
||||
# Robuste Continue-Button-Selektoren
|
||||
continue_selectors = [
|
||||
"button[data-e2e='continue-button']",
|
||||
"button:has-text('Weiter')",
|
||||
"button:has-text('Continue')",
|
||||
"button:has-text('Fortfahren')",
|
||||
"button[type='submit']",
|
||||
"button.css-10nhlj9-Button-StyledButton:not([disabled])"
|
||||
]
|
||||
|
||||
for i, selector in enumerate(continue_selectors):
|
||||
try:
|
||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||
# Prüfe, ob Button enabled ist
|
||||
element = self.automation.browser.wait_for_selector(selector, timeout=1000)
|
||||
if element:
|
||||
is_disabled = element.get_attribute("disabled")
|
||||
aria_disabled = element.get_attribute("aria-disabled")
|
||||
|
||||
if is_disabled or aria_disabled == "true":
|
||||
logger.debug(f"Continue-Button {i+1} ist disabled, versuche nächsten")
|
||||
continue
|
||||
|
||||
# Button klicken
|
||||
success = self.automation.browser.click_element(selector)
|
||||
if success:
|
||||
logger.info(f"Weiter-Button erfolgreich geklickt mit Selektor {i+1}")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Continue-Selektor {i+1} fehlgeschlagen: {e}")
|
||||
continue
|
||||
|
||||
# Fallback: Fuzzy-Button-Matching
|
||||
try:
|
||||
success = self.automation.ui_helper.click_button_fuzzy(
|
||||
["Weiter", "Continue", "Fortfahren", "Next"],
|
||||
continue_selectors[0]
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Weiter-Button über Fuzzy-Matching geklickt")
|
||||
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Continue Fuzzy-Matching fehlgeschlagen: {e}")
|
||||
|
||||
logger.error("Weiter-Button konnte nicht geklickt werden")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Kritischer Fehler beim Weiter-Button: {e}")
|
||||
return False
|
||||
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
@ -1,508 +0,0 @@
|
||||
"""
|
||||
Moderner Account Generator Tab - Ohne Log-Widget, mit besserem Layout
|
||||
"""
|
||||
|
||||
import logging
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QGridLayout,
|
||||
QGroupBox, QLabel, QLineEdit, QSpinBox, QRadioButton,
|
||||
QCheckBox, QComboBox, QPushButton, QProgressBar,
|
||||
QMessageBox, QFrame, QScrollArea
|
||||
)
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve
|
||||
from PyQt5.QtGui import QFont, QPalette
|
||||
|
||||
logger = logging.getLogger("generator_tab")
|
||||
|
||||
class ModernGeneratorTab(QWidget):
|
||||
"""Modernes Widget für Account-Generierung ohne Log."""
|
||||
|
||||
# Signale
|
||||
start_requested = pyqtSignal(dict)
|
||||
stop_requested = pyqtSignal()
|
||||
account_created = pyqtSignal(str, dict)
|
||||
|
||||
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 moderne Benutzeroberfläche."""
|
||||
# Haupt-Layout mit Scroll-Bereich
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Scroll-Bereich für bessere Anpassung
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.NoFrame)
|
||||
scroll_widget = QWidget()
|
||||
scroll_layout = QVBoxLayout(scroll_widget)
|
||||
scroll_layout.setContentsMargins(40, 30, 40, 30)
|
||||
scroll_layout.setSpacing(30)
|
||||
|
||||
# Header mit Titel und Status
|
||||
header_widget = self.create_header()
|
||||
scroll_layout.addWidget(header_widget)
|
||||
|
||||
# Haupt-Formular als Card
|
||||
form_card = self.create_form_card()
|
||||
scroll_layout.addWidget(form_card)
|
||||
|
||||
# Erweiterte Optionen
|
||||
options_card = self.create_options_card()
|
||||
scroll_layout.addWidget(options_card)
|
||||
|
||||
# Action-Bereich mit Buttons
|
||||
action_widget = self.create_action_area()
|
||||
scroll_layout.addWidget(action_widget)
|
||||
|
||||
scroll_layout.addStretch()
|
||||
|
||||
scroll.setWidget(scroll_widget)
|
||||
main_layout.addWidget(scroll)
|
||||
|
||||
# Event-Verbindungen
|
||||
self.email_radio.toggled.connect(self.toggle_phone_input)
|
||||
self.phone_radio.toggled.connect(self.toggle_phone_input)
|
||||
|
||||
# Initialer Status
|
||||
self.toggle_phone_input()
|
||||
|
||||
def create_header(self):
|
||||
"""Erstellt den Header-Bereich mit Titel und Status."""
|
||||
header = QWidget()
|
||||
layout = QHBoxLayout(header)
|
||||
layout.setContentsMargins(0, 0, 0, 20)
|
||||
|
||||
# Titel
|
||||
title = QLabel("Account erstellen")
|
||||
title_font = QFont()
|
||||
title_font.setPointSize(24)
|
||||
title_font.setWeight(QFont.Bold)
|
||||
title.setFont(title_font)
|
||||
layout.addWidget(title)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# Status-Widget entfernt für cleanes Design
|
||||
|
||||
return header
|
||||
|
||||
def create_form_card(self):
|
||||
"""Erstellt die Haupt-Formular-Card."""
|
||||
card = QFrame()
|
||||
card.setObjectName("formCard")
|
||||
card.setStyleSheet("""
|
||||
#formCard {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(card)
|
||||
|
||||
# Section Title
|
||||
section_title = QLabel("Account-Informationen")
|
||||
section_font = QFont()
|
||||
section_font.setPointSize(16)
|
||||
section_font.setWeight(QFont.DemiBold)
|
||||
section_title.setFont(section_font)
|
||||
layout.addWidget(section_title)
|
||||
layout.addSpacing(20)
|
||||
|
||||
# Grid-Layout für bessere Anordnung
|
||||
grid = QGridLayout()
|
||||
grid.setHorizontalSpacing(20)
|
||||
grid.setVerticalSpacing(20)
|
||||
|
||||
# Erste Zeile: Vorname und Nachname
|
||||
self.first_name_label = self.create_label("Vorname")
|
||||
self.first_name_input = self.create_input("z.B. Max")
|
||||
grid.addWidget(self.first_name_label, 0, 0)
|
||||
grid.addWidget(self.first_name_input, 1, 0)
|
||||
|
||||
self.last_name_label = self.create_label("Nachname")
|
||||
self.last_name_input = self.create_input("z.B. Mustermann")
|
||||
grid.addWidget(self.last_name_label, 0, 1)
|
||||
grid.addWidget(self.last_name_input, 1, 1)
|
||||
|
||||
# Zweite Zeile: Alter
|
||||
self.age_label = self.create_label("Alter")
|
||||
self.age_input = self.create_input("z.B. 25")
|
||||
grid.addWidget(self.age_label, 2, 0)
|
||||
grid.addWidget(self.age_input, 3, 0)
|
||||
|
||||
# Dritte Zeile: Registrierungsmethode
|
||||
self.reg_method_label = self.create_label("Registrierungsmethode")
|
||||
grid.addWidget(self.reg_method_label, 4, 0, 1, 2)
|
||||
|
||||
# Radio Buttons in horizontaler Anordnung
|
||||
radio_widget = QWidget()
|
||||
radio_layout = QHBoxLayout(radio_widget)
|
||||
radio_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.email_radio = QRadioButton("E-Mail")
|
||||
self.phone_radio = QRadioButton("Telefon")
|
||||
self.email_radio.setChecked(True)
|
||||
|
||||
# Styling für Radio Buttons
|
||||
radio_style = """
|
||||
QRadioButton {
|
||||
font-size: 14px;
|
||||
spacing: 8px;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
"""
|
||||
self.email_radio.setStyleSheet(radio_style)
|
||||
self.phone_radio.setStyleSheet(radio_style)
|
||||
|
||||
radio_layout.addWidget(self.email_radio)
|
||||
radio_layout.addWidget(self.phone_radio)
|
||||
radio_layout.addStretch()
|
||||
|
||||
grid.addWidget(radio_widget, 5, 0, 1, 2)
|
||||
|
||||
# Vierte Zeile: Kontaktfeld (E-Mail Domain oder Telefon)
|
||||
self.email_domain_label = self.create_label("E-Mail Domain")
|
||||
self.email_domain_input = self.create_input("")
|
||||
self.email_domain_input.setText("z5m7q9dk3ah2v1plx6ju.com")
|
||||
grid.addWidget(self.email_domain_label, 6, 0)
|
||||
grid.addWidget(self.email_domain_input, 7, 0)
|
||||
|
||||
self.phone_label = self.create_label("Telefonnummer")
|
||||
self.phone_input = self.create_input("z.B. +49123456789")
|
||||
self.phone_label.hide()
|
||||
self.phone_input.hide()
|
||||
grid.addWidget(self.phone_label, 6, 1)
|
||||
grid.addWidget(self.phone_input, 7, 1)
|
||||
|
||||
layout.addLayout(grid)
|
||||
|
||||
# Plattform-spezifische Felder
|
||||
self.add_platform_specific_fields(layout)
|
||||
|
||||
return card
|
||||
|
||||
def create_options_card(self):
|
||||
"""Erstellt die Card für erweiterte Optionen."""
|
||||
card = QFrame()
|
||||
card.setObjectName("optionsCard")
|
||||
card.setStyleSheet("""
|
||||
#optionsCard {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(card)
|
||||
|
||||
# Section Title
|
||||
section_title = QLabel("Erweiterte Optionen")
|
||||
section_font = QFont()
|
||||
section_font.setPointSize(16)
|
||||
section_font.setWeight(QFont.DemiBold)
|
||||
section_title.setFont(section_font)
|
||||
layout.addWidget(section_title)
|
||||
layout.addSpacing(15)
|
||||
|
||||
# Optionen als moderne Checkboxen
|
||||
self.headless_check = self.create_checkbox("Browser im Hintergrund ausführen")
|
||||
self.headless_check.setToolTip("Der Browser wird nicht sichtbar sein")
|
||||
layout.addWidget(self.headless_check)
|
||||
|
||||
return card
|
||||
|
||||
def create_action_area(self):
|
||||
"""Erstellt den Bereich mit Aktions-Buttons und Progress."""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Progress Bar (initial versteckt)
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setMinimumHeight(6)
|
||||
self.progress_bar.setTextVisible(False)
|
||||
self.progress_bar.setStyleSheet("""
|
||||
QProgressBar {
|
||||
background-color: #f0f0f0;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #2196F3;
|
||||
border-radius: 3px;
|
||||
}
|
||||
""")
|
||||
self.progress_bar.hide()
|
||||
layout.addWidget(self.progress_bar)
|
||||
layout.addSpacing(20)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch()
|
||||
|
||||
self.stop_button = self.create_button("Abbrechen", primary=False)
|
||||
self.stop_button.clicked.connect(self.on_stop_clicked)
|
||||
self.stop_button.setEnabled(False)
|
||||
self.stop_button.hide()
|
||||
button_layout.addWidget(self.stop_button)
|
||||
|
||||
self.start_button = self.create_button("Account erstellen", primary=True)
|
||||
self.start_button.clicked.connect(self.on_start_clicked)
|
||||
button_layout.addWidget(self.start_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
return widget
|
||||
|
||||
def create_label(self, text):
|
||||
"""Erstellt ein modernes Label."""
|
||||
label = QLabel(text)
|
||||
label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
""")
|
||||
return label
|
||||
|
||||
def create_input(self, placeholder=""):
|
||||
"""Erstellt ein modernes Input-Feld."""
|
||||
input_field = QLineEdit()
|
||||
input_field.setPlaceholderText(placeholder)
|
||||
input_field.setMinimumHeight(42)
|
||||
input_field.setStyleSheet("""
|
||||
QLineEdit {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border-color: #2196F3;
|
||||
background-color: white;
|
||||
}
|
||||
QLineEdit:hover {
|
||||
border-color: #bdbdbd;
|
||||
}
|
||||
""")
|
||||
return input_field
|
||||
|
||||
def create_checkbox(self, text):
|
||||
"""Erstellt eine moderne Checkbox."""
|
||||
checkbox = QCheckBox(text)
|
||||
checkbox.setStyleSheet("""
|
||||
QCheckBox {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
spacing: 10px;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #e0e0e0;
|
||||
background-color: white;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
background-color: #2196F3;
|
||||
border-color: #2196F3;
|
||||
image: url(check_white.png);
|
||||
}
|
||||
QCheckBox::indicator:hover {
|
||||
border-color: #2196F3;
|
||||
}
|
||||
""")
|
||||
return checkbox
|
||||
|
||||
def create_button(self, text, primary=False):
|
||||
"""Erstellt einen modernen Button."""
|
||||
button = QPushButton(text)
|
||||
button.setMinimumHeight(45)
|
||||
button.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
if primary:
|
||||
button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #0D47A1;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #BBBBBB;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: white;
|
||||
color: #666;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 12px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:hover {
|
||||
border-color: #bdbdbd;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
color: #BBBBBB;
|
||||
border-color: #eeeeee;
|
||||
}
|
||||
""")
|
||||
|
||||
return button
|
||||
|
||||
def add_platform_specific_fields(self, parent_layout):
|
||||
"""Fügt plattformspezifische Felder hinzu."""
|
||||
platform = self.platform_name.lower()
|
||||
|
||||
|
||||
def toggle_phone_input(self):
|
||||
"""Wechselt zwischen E-Mail und Telefon-Eingabe."""
|
||||
if self.email_radio.isChecked():
|
||||
self.email_domain_label.show()
|
||||
self.email_domain_input.show()
|
||||
self.phone_label.hide()
|
||||
self.phone_input.hide()
|
||||
else:
|
||||
self.email_domain_label.hide()
|
||||
self.email_domain_input.hide()
|
||||
self.phone_label.show()
|
||||
self.phone_input.show()
|
||||
|
||||
def on_start_clicked(self):
|
||||
"""Wird aufgerufen, wenn der Start-Button geklickt wird."""
|
||||
params = self.get_params()
|
||||
|
||||
valid, error_msg = self.validate_inputs(params)
|
||||
if not valid:
|
||||
self.show_error(error_msg)
|
||||
return
|
||||
|
||||
# UI für laufenden Prozess anpassen
|
||||
self.set_running(True)
|
||||
self.progress_bar.show()
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
# 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."""
|
||||
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(),
|
||||
"registration_method": "email" if self.email_radio.isChecked() else "phone",
|
||||
"headless": self.headless_check.isChecked(),
|
||||
"debug": True,
|
||||
"email_domain": self.email_domain_input.text().strip(),
|
||||
"use_proxy": False
|
||||
}
|
||||
|
||||
if self.phone_radio.isChecked():
|
||||
params["phone_number"] = self.phone_input.text().strip()
|
||||
|
||||
|
||||
return params
|
||||
|
||||
def validate_inputs(self, params):
|
||||
"""Validiert die Eingaben."""
|
||||
if not params.get("first_name"):
|
||||
return False, "Bitte geben Sie einen Vornamen ein."
|
||||
|
||||
if not params.get("last_name"):
|
||||
return False, "Bitte geben Sie einen Nachnamen ein."
|
||||
|
||||
age_text = params.get("age_text", "")
|
||||
if not age_text:
|
||||
return False, "Bitte geben Sie ein Alter ein."
|
||||
|
||||
try:
|
||||
age = int(age_text)
|
||||
params["age"] = age
|
||||
except ValueError:
|
||||
return False, "Das Alter muss eine ganze Zahl sein."
|
||||
|
||||
if age < 13 or age > 99:
|
||||
return False, "Das Alter muss zwischen 13 und 99 liegen."
|
||||
|
||||
if params.get("registration_method") == "phone" and not params.get("phone_number"):
|
||||
return False, "Bitte geben Sie eine Telefonnummer ein."
|
||||
|
||||
return True, ""
|
||||
|
||||
def set_running(self, running: bool):
|
||||
"""Setzt den Status auf 'Wird ausgeführt' oder 'Bereit'."""
|
||||
if running:
|
||||
self.start_button.hide()
|
||||
self.stop_button.show()
|
||||
self.stop_button.setEnabled(True)
|
||||
# Status-Dot entfernt
|
||||
# Status-Text entfernt
|
||||
else:
|
||||
self.start_button.show()
|
||||
self.stop_button.hide()
|
||||
self.stop_button.setEnabled(False)
|
||||
self.progress_bar.hide()
|
||||
# Status-Dot entfernt
|
||||
# Status-Text entfernt
|
||||
|
||||
def set_progress(self, value: int):
|
||||
"""Setzt den Fortschritt."""
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
def set_status(self, message: str):
|
||||
"""Setzt die Statusnachricht."""
|
||||
# Status-Text entfernt
|
||||
logger.info(f"Status: {message}")
|
||||
|
||||
def show_error(self, message: str):
|
||||
"""Zeigt eine moderne Fehlermeldung an."""
|
||||
from views.widgets.modern_message_box import show_error
|
||||
show_error(self, "Fehler", message)
|
||||
|
||||
def update_texts(self):
|
||||
"""Aktualisiert UI-Texte gemäß der aktuellen Sprache."""
|
||||
# Hier würden die Übersetzungen implementiert
|
||||
pass
|
||||
@ -1,625 +0,0 @@
|
||||
"""
|
||||
Account Creation Modal V2 - Verbessertes Design mit eindringlicher Warnung
|
||||
Basierend auf dem Corporate Design Styleguide
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime, timedelta
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QPushButton, QFrame, QWidget, QScrollArea,
|
||||
QProgressBar, QGraphicsOpacityEffect
|
||||
)
|
||||
from PyQt5.QtCore import (
|
||||
Qt, QTimer, pyqtSignal, QPropertyAnimation,
|
||||
QEasingCurve, QRect, QSize
|
||||
)
|
||||
from PyQt5.QtGui import QFont, QPainter, QBrush, QColor, QPen, QPixmap
|
||||
|
||||
logger = logging.getLogger("account_creation_modal_v2")
|
||||
|
||||
|
||||
class AccountCreationModalV2(QDialog):
|
||||
"""
|
||||
Verbessertes Modal für Account-Erstellung mit:
|
||||
- Prominenter Warnung
|
||||
- Besserer Step-Visualisierung
|
||||
- Größerem Fenster
|
||||
- Klarerer Kommunikation
|
||||
"""
|
||||
|
||||
# Signale
|
||||
cancel_clicked = pyqtSignal()
|
||||
process_completed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None, platform: str = "Social Media"):
|
||||
super().__init__(parent)
|
||||
self.platform = platform
|
||||
self.current_step = 0
|
||||
self.total_steps = 0
|
||||
self.steps = []
|
||||
self.start_time = None
|
||||
self.is_process_running = False
|
||||
|
||||
# Timer für Updates
|
||||
self.update_timer = QTimer()
|
||||
self.update_timer.timeout.connect(self.update_time_display)
|
||||
self.update_timer.setInterval(1000) # Jede Sekunde
|
||||
|
||||
# Animation Timer für Warning
|
||||
self.warning_animation_timer = QTimer()
|
||||
self.warning_animation_timer.timeout.connect(self.animate_warning)
|
||||
self.warning_animation_timer.setInterval(100) # Smooth animation
|
||||
self.warning_opacity = 1.0
|
||||
self.warning_direction = -0.02
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialisiert die UI mit verbessertem Design"""
|
||||
# Modal-Eigenschaften
|
||||
self.setModal(True)
|
||||
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||
|
||||
# Größeres Fenster
|
||||
self.setFixedSize(650, 700)
|
||||
|
||||
# Zentriere auf Parent oder Bildschirm
|
||||
if self.parent():
|
||||
parent_rect = self.parent().geometry()
|
||||
x = parent_rect.x() + (parent_rect.width() - self.width()) // 2
|
||||
y = parent_rect.y() + (parent_rect.height() - self.height()) // 2
|
||||
self.move(x, y)
|
||||
|
||||
# Hauptlayout
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
# Container mit weißem Hintergrund
|
||||
self.container = QFrame()
|
||||
self.container.setObjectName("mainContainer")
|
||||
self.container.setStyleSheet("""
|
||||
QFrame#mainContainer {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
|
||||
container_layout = QVBoxLayout(self.container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
container_layout.setSpacing(0)
|
||||
|
||||
# 1. WARNING BANNER (Sehr auffällig!)
|
||||
self.warning_banner = self.create_warning_banner()
|
||||
container_layout.addWidget(self.warning_banner)
|
||||
|
||||
# Content Area mit Padding
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(40, 30, 40, 40)
|
||||
content_layout.setSpacing(24)
|
||||
|
||||
# 2. Titel-Bereich
|
||||
self.title_label = QLabel(f"Account wird erstellt für {self.platform}")
|
||||
self.title_label.setAlignment(Qt.AlignCenter)
|
||||
self.title_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #1A365D;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
""")
|
||||
content_layout.addWidget(self.title_label)
|
||||
|
||||
# 3. Progress-Bereich
|
||||
progress_widget = self.create_progress_section()
|
||||
content_layout.addWidget(progress_widget)
|
||||
|
||||
# 4. Steps-Liste
|
||||
self.steps_widget = self.create_steps_list()
|
||||
content_layout.addWidget(self.steps_widget)
|
||||
|
||||
# 5. Live-Status
|
||||
self.status_widget = self.create_status_section()
|
||||
content_layout.addWidget(self.status_widget)
|
||||
|
||||
# 6. Info-Box
|
||||
info_box = self.create_info_box()
|
||||
content_layout.addWidget(info_box)
|
||||
|
||||
# Spacer
|
||||
content_layout.addStretch()
|
||||
|
||||
# Container zum Hauptlayout hinzufügen
|
||||
container_layout.addWidget(content_widget)
|
||||
main_layout.addWidget(self.container)
|
||||
|
||||
def create_warning_banner(self) -> QFrame:
|
||||
"""Erstellt den auffälligen Warning Banner"""
|
||||
banner = QFrame()
|
||||
banner.setObjectName("warningBanner")
|
||||
banner.setFixedHeight(100)
|
||||
banner.setStyleSheet("""
|
||||
QFrame#warningBanner {
|
||||
background-color: #DC2626;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
border-bottom: none;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(banner)
|
||||
layout.setContentsMargins(40, 20, 40, 20)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# Warning Icon + Haupttext
|
||||
main_warning = QLabel("⚠️ NICHT DEN BROWSER BERÜHREN!")
|
||||
main_warning.setAlignment(Qt.AlignCenter)
|
||||
main_warning.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #FFFFFF;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
""")
|
||||
|
||||
# Untertitel
|
||||
subtitle = QLabel("Die Automatisierung läuft. Jede Interaktion kann den Prozess unterbrechen.")
|
||||
subtitle.setAlignment(Qt.AlignCenter)
|
||||
subtitle.setWordWrap(True)
|
||||
subtitle.setStyleSheet("""
|
||||
QLabel {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
""")
|
||||
|
||||
layout.addWidget(main_warning)
|
||||
layout.addWidget(subtitle)
|
||||
|
||||
return banner
|
||||
|
||||
def create_progress_section(self) -> QWidget:
|
||||
"""Erstellt den Progress-Bereich"""
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(24)
|
||||
|
||||
# Progress Bar
|
||||
progress_container = QWidget()
|
||||
progress_layout = QVBoxLayout(progress_container)
|
||||
progress_layout.setContentsMargins(0, 0, 0, 0)
|
||||
progress_layout.setSpacing(8)
|
||||
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setFixedHeight(12)
|
||||
self.progress_bar.setStyleSheet("""
|
||||
QProgressBar {
|
||||
background-color: #E2E8F0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #3182CE;
|
||||
border-radius: 6px;
|
||||
}
|
||||
""")
|
||||
|
||||
self.progress_label = QLabel("Schritt 0 von 0")
|
||||
self.progress_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #4A5568;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
""")
|
||||
|
||||
progress_layout.addWidget(self.progress_bar)
|
||||
progress_layout.addWidget(self.progress_label)
|
||||
|
||||
# Zeit-Anzeige
|
||||
time_container = QWidget()
|
||||
time_container.setFixedWidth(180)
|
||||
time_layout = QVBoxLayout(time_container)
|
||||
time_layout.setContentsMargins(0, 0, 0, 0)
|
||||
time_layout.setSpacing(4)
|
||||
|
||||
self.time_label = QLabel("~2 Min verbleibend")
|
||||
self.time_label.setAlignment(Qt.AlignRight)
|
||||
self.time_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #1A365D;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-family: 'Poppins', -apple-system, sans-serif;
|
||||
}
|
||||
""")
|
||||
|
||||
self.elapsed_label = QLabel("Verstrichene Zeit: 0:00")
|
||||
self.elapsed_label.setAlignment(Qt.AlignRight)
|
||||
self.elapsed_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #718096;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
""")
|
||||
|
||||
time_layout.addWidget(self.time_label)
|
||||
time_layout.addWidget(self.elapsed_label)
|
||||
|
||||
layout.addWidget(progress_container, 1)
|
||||
layout.addWidget(time_container)
|
||||
|
||||
return widget
|
||||
|
||||
def create_steps_list(self) -> QWidget:
|
||||
"""Erstellt die visuelle Steps-Liste"""
|
||||
container = QFrame()
|
||||
container.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #F8FAFC;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
""")
|
||||
|
||||
self.steps_layout = QVBoxLayout(container)
|
||||
self.steps_layout.setSpacing(12)
|
||||
|
||||
# Wird dynamisch befüllt
|
||||
return container
|
||||
|
||||
def create_status_section(self) -> QWidget:
|
||||
"""Erstellt den Live-Status Bereich"""
|
||||
container = QFrame()
|
||||
container.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #1A365D;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
min-height: 80px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(container)
|
||||
layout.setSpacing(8)
|
||||
|
||||
status_title = QLabel("Aktueller Status:")
|
||||
status_title.setStyleSheet("""
|
||||
QLabel {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
""")
|
||||
|
||||
self.current_status_label = QLabel("Initialisiere Browser...")
|
||||
self.current_status_label.setWordWrap(True)
|
||||
self.current_status_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
""")
|
||||
|
||||
layout.addWidget(status_title)
|
||||
layout.addWidget(self.current_status_label)
|
||||
|
||||
return container
|
||||
|
||||
def create_info_box(self) -> QWidget:
|
||||
"""Erstellt die Info-Box"""
|
||||
container = QFrame()
|
||||
container.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #DBEAFE;
|
||||
border: 1px solid #2563EB;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Info Icon
|
||||
icon_label = QLabel("ℹ️")
|
||||
icon_label.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 24px;
|
||||
color: #2563EB;
|
||||
}
|
||||
""")
|
||||
|
||||
# Info Text
|
||||
info_layout = QVBoxLayout()
|
||||
info_layout.setSpacing(4)
|
||||
|
||||
info_title = QLabel("Was passiert gerade?")
|
||||
info_title.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #1E40AF;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
""")
|
||||
|
||||
self.info_text = QLabel("Der Browser simuliert menschliches Verhalten beim Ausfüllen des Formulars.")
|
||||
self.info_text.setWordWrap(True)
|
||||
self.info_text.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #1E40AF;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
""")
|
||||
|
||||
info_layout.addWidget(info_title)
|
||||
info_layout.addWidget(self.info_text)
|
||||
|
||||
layout.addWidget(icon_label)
|
||||
layout.addWidget(info_layout, 1)
|
||||
|
||||
return container
|
||||
|
||||
def set_steps(self, steps: List[str]):
|
||||
"""Setzt die Steps und erstellt die visuelle Liste"""
|
||||
self.steps = steps
|
||||
self.total_steps = len(steps)
|
||||
self.current_step = 0
|
||||
|
||||
# Progress Bar Setup
|
||||
self.progress_bar.setMaximum(self.total_steps)
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
# Clear existing steps
|
||||
for i in reversed(range(self.steps_layout.count())):
|
||||
self.steps_layout.itemAt(i).widget().setParent(None)
|
||||
|
||||
# Create step items
|
||||
self.step_widgets = []
|
||||
for i, step in enumerate(steps):
|
||||
step_widget = self.create_step_item(i, step)
|
||||
self.steps_layout.addWidget(step_widget)
|
||||
self.step_widgets.append(step_widget)
|
||||
|
||||
def create_step_item(self, index: int, text: str) -> QWidget:
|
||||
"""Erstellt ein einzelnes Step-Item"""
|
||||
widget = QWidget()
|
||||
layout = QHBoxLayout(widget)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# Status Icon
|
||||
icon_label = QLabel()
|
||||
icon_label.setFixedSize(24, 24)
|
||||
icon_label.setAlignment(Qt.AlignCenter)
|
||||
icon_label.setObjectName(f"stepIcon_{index}")
|
||||
|
||||
# Text
|
||||
text_label = QLabel(text)
|
||||
text_label.setObjectName(f"stepText_{index}")
|
||||
|
||||
# Initial state (pending)
|
||||
self.update_step_appearance(widget, 'pending')
|
||||
|
||||
layout.addWidget(icon_label)
|
||||
layout.addWidget(text_label, 1)
|
||||
|
||||
return widget
|
||||
|
||||
def update_step_appearance(self, widget: QWidget, status: str):
|
||||
"""Aktualisiert das Aussehen eines Steps basierend auf Status"""
|
||||
icon_label = widget.findChild(QLabel, QRegExp("stepIcon_*"))
|
||||
text_label = widget.findChild(QLabel, QRegExp("stepText_*"))
|
||||
|
||||
if status == 'completed':
|
||||
icon_label.setText("✅")
|
||||
text_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #059669;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
""")
|
||||
elif status == 'active':
|
||||
icon_label.setText("🔄")
|
||||
text_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #2563EB;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
""")
|
||||
# Rotation animation würde hier hinzugefügt
|
||||
else: # pending
|
||||
icon_label.setText("⏳")
|
||||
text_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
""")
|
||||
|
||||
def start_process(self):
|
||||
"""Startet den Account-Erstellungsprozess"""
|
||||
self.is_process_running = True
|
||||
self.start_time = datetime.now()
|
||||
self.update_timer.start()
|
||||
self.warning_animation_timer.start()
|
||||
self.show()
|
||||
|
||||
def next_step(self, status_text: str = None, info_text: str = None):
|
||||
"""Geht zum nächsten Schritt"""
|
||||
if self.current_step < self.total_steps:
|
||||
# Aktuellen Step als aktiv markieren
|
||||
if self.current_step > 0:
|
||||
self.update_step_appearance(self.step_widgets[self.current_step - 1], 'completed')
|
||||
|
||||
if self.current_step < len(self.step_widgets):
|
||||
self.update_step_appearance(self.step_widgets[self.current_step], 'active')
|
||||
|
||||
self.current_step += 1
|
||||
self.progress_bar.setValue(self.current_step)
|
||||
self.progress_label.setText(f"Schritt {self.current_step} von {self.total_steps}")
|
||||
|
||||
# Update status
|
||||
if status_text:
|
||||
self.current_status_label.setText(status_text)
|
||||
|
||||
# Update info
|
||||
if info_text:
|
||||
self.info_text.setText(info_text)
|
||||
|
||||
# Update time estimate
|
||||
self.update_time_estimate()
|
||||
|
||||
def update_time_estimate(self):
|
||||
"""Aktualisiert die Zeitschätzung"""
|
||||
if self.total_steps > 0 and self.current_step > 0:
|
||||
elapsed = (datetime.now() - self.start_time).total_seconds()
|
||||
avg_time_per_step = elapsed / self.current_step
|
||||
remaining_steps = self.total_steps - self.current_step
|
||||
estimated_remaining = remaining_steps * avg_time_per_step
|
||||
|
||||
if estimated_remaining < 60:
|
||||
self.time_label.setText(f"~{int(estimated_remaining)}s verbleibend")
|
||||
else:
|
||||
minutes = int(estimated_remaining / 60)
|
||||
self.time_label.setText(f"~{minutes} Min verbleibend")
|
||||
|
||||
def update_time_display(self):
|
||||
"""Aktualisiert die verstrichene Zeit"""
|
||||
if self.start_time:
|
||||
elapsed = datetime.now() - self.start_time
|
||||
minutes = int(elapsed.total_seconds() / 60)
|
||||
seconds = int(elapsed.total_seconds() % 60)
|
||||
self.elapsed_label.setText(f"Verstrichene Zeit: {minutes}:{seconds:02d}")
|
||||
|
||||
def animate_warning(self):
|
||||
"""Animiert den Warning Banner (Pulseffekt)"""
|
||||
self.warning_opacity += self.warning_direction
|
||||
|
||||
if self.warning_opacity <= 0.7:
|
||||
self.warning_opacity = 0.7
|
||||
self.warning_direction = 0.02
|
||||
elif self.warning_opacity >= 1.0:
|
||||
self.warning_opacity = 1.0
|
||||
self.warning_direction = -0.02
|
||||
|
||||
# Opacity-Effekt anwenden
|
||||
effect = QGraphicsOpacityEffect()
|
||||
effect.setOpacity(self.warning_opacity)
|
||||
self.warning_banner.setGraphicsEffect(effect)
|
||||
|
||||
def set_status(self, status: str, info: str = None):
|
||||
"""Setzt den aktuellen Status"""
|
||||
self.current_status_label.setText(status)
|
||||
if info:
|
||||
self.info_text.setText(info)
|
||||
|
||||
def complete_process(self):
|
||||
"""Beendet den Prozess erfolgreich"""
|
||||
self.is_process_running = False
|
||||
self.update_timer.stop()
|
||||
self.warning_animation_timer.stop()
|
||||
|
||||
# Letzten Step als completed markieren
|
||||
if self.current_step > 0 and self.current_step <= len(self.step_widgets):
|
||||
self.update_step_appearance(self.step_widgets[self.current_step - 1], 'completed')
|
||||
|
||||
# Success message
|
||||
self.current_status_label.setText("✅ Account erfolgreich erstellt!")
|
||||
self.info_text.setText("Der Prozess wurde erfolgreich abgeschlossen. Das Fenster schließt sich in wenigen Sekunden.")
|
||||
|
||||
# Auto-close nach 3 Sekunden
|
||||
QTimer.singleShot(3000, self.close)
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Custom paint event für Transparenz"""
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
# Semi-transparenter Hintergrund
|
||||
painter.fillRect(self.rect(), QColor(0, 0, 0, 120))
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Verhindert das Schließen mit ESC"""
|
||||
if event.key() == Qt.Key_Escape:
|
||||
event.ignore()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Verhindert das Schließen während der Prozess läuft"""
|
||||
if self.is_process_running:
|
||||
event.ignore()
|
||||
else:
|
||||
self.update_timer.stop()
|
||||
self.warning_animation_timer.stop()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
# Beispiel-Verwendung
|
||||
if __name__ == "__main__":
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Test window
|
||||
main_window = QMainWindow()
|
||||
main_window.setGeometry(100, 100, 800, 600)
|
||||
main_window.show()
|
||||
|
||||
# Create and show modal
|
||||
modal = AccountCreationModalV2(main_window, "Instagram")
|
||||
modal.set_steps([
|
||||
"Browser vorbereiten",
|
||||
"Instagram-Seite laden",
|
||||
"Registrierungsformular öffnen",
|
||||
"Benutzerdaten eingeben",
|
||||
"Account erstellen",
|
||||
"E-Mail-Verifizierung",
|
||||
"Profil einrichten"
|
||||
])
|
||||
|
||||
# Simulate process
|
||||
modal.start_process()
|
||||
|
||||
# Simulate step progression
|
||||
def next_step():
|
||||
modal.next_step(
|
||||
f"Führe Schritt {modal.current_step + 1} aus...",
|
||||
"Der Browser füllt automatisch die erforderlichen Felder aus."
|
||||
)
|
||||
|
||||
if modal.current_step < modal.total_steps:
|
||||
QTimer.singleShot(2000, next_step)
|
||||
else:
|
||||
modal.complete_process()
|
||||
|
||||
QTimer.singleShot(1000, next_step)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@ -1,370 +0,0 @@
|
||||
"""
|
||||
Forge Animation Widget V2 - Verbessertes Design mit prominenter Warnung
|
||||
Angepasst an den bestehenden Code mit minimalen Änderungen
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import QDialog, QWidget, QVBoxLayout, QLabel, QTextEdit, QPushButton, QHBoxLayout, QFrame
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
|
||||
from PyQt5.QtGui import QFont, QMovie, QPixmap
|
||||
|
||||
class ForgeAnimationDialog(QDialog):
|
||||
"""Modal-Dialog für die Account-Erstellung mit verbessertem Design"""
|
||||
|
||||
# Signal wenn Abbrechen geklickt wird
|
||||
cancel_clicked = pyqtSignal()
|
||||
# Signal wenn Dialog geschlossen wird
|
||||
closed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.init_ui()
|
||||
|
||||
# Timer für das regelmäßige Nach-vorne-Holen
|
||||
self.raise_timer = QTimer()
|
||||
self.raise_timer.timeout.connect(self._raise_to_front)
|
||||
self.raise_timer.setInterval(500) # Alle 500ms
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialisiert die UI mit verbessertem Design"""
|
||||
# Nur Dialog im Vordergrund, nicht das ganze Hauptfenster
|
||||
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.setModal(False) # Nicht modal - blockiert nicht das Hauptfenster
|
||||
self.setFixedSize(650, 600) # Größer für bessere Sichtbarkeit
|
||||
|
||||
# Styling für Light Theme
|
||||
self.setStyleSheet("""
|
||||
ForgeAnimationDialog {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
""")
|
||||
|
||||
# Hauptlayout
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# NEUE WARNUNG OBEN - Sehr auffällig!
|
||||
warning_banner = QFrame()
|
||||
warning_banner.setFixedHeight(80)
|
||||
warning_banner.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #DC2626;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
padding: 15px 30px;
|
||||
}
|
||||
""")
|
||||
|
||||
warning_layout = QVBoxLayout(warning_banner)
|
||||
warning_layout.setContentsMargins(0, 10, 0, 10)
|
||||
|
||||
# Großer Warning Text
|
||||
warning_text = QLabel("⚠️ BROWSER NICHT BERÜHREN!")
|
||||
warning_text.setAlignment(Qt.AlignCenter)
|
||||
warning_text.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #FFFFFF;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
""")
|
||||
|
||||
warning_subtext = QLabel("Jede Interaktion kann den Prozess unterbrechen")
|
||||
warning_subtext.setAlignment(Qt.AlignCenter)
|
||||
warning_subtext.setStyleSheet("""
|
||||
QLabel {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
""")
|
||||
|
||||
warning_layout.addWidget(warning_text)
|
||||
warning_layout.addWidget(warning_subtext)
|
||||
|
||||
layout.addWidget(warning_banner)
|
||||
|
||||
# Content Container
|
||||
content_container = QWidget()
|
||||
content_layout = QVBoxLayout(content_container)
|
||||
content_layout.setContentsMargins(30, 25, 30, 30)
|
||||
content_layout.setSpacing(20)
|
||||
|
||||
# Titel (etwas größer)
|
||||
title_label = QLabel("Account wird erstellt")
|
||||
title_label.setObjectName("titleLabel")
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet("""
|
||||
QLabel#titleLabel {
|
||||
color: #1A365D;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
padding-bottom: 10px;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
content_layout.addWidget(title_label)
|
||||
|
||||
# Moderne Info-Karte (angepasst)
|
||||
info_frame = QFrame()
|
||||
info_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background: #DBEAFE;
|
||||
border: 1px solid #2563EB;
|
||||
border-radius: 12px;
|
||||
min-height: 100px;
|
||||
}
|
||||
""")
|
||||
|
||||
info_layout = QHBoxLayout(info_frame)
|
||||
info_layout.setContentsMargins(20, 15, 20, 15)
|
||||
info_layout.setSpacing(15)
|
||||
|
||||
# Icon Container
|
||||
icon_container = QFrame()
|
||||
icon_container.setFixedSize(50, 50)
|
||||
icon_container.setStyleSheet("""
|
||||
QFrame {
|
||||
background: #2563EB;
|
||||
border-radius: 25px;
|
||||
}
|
||||
""")
|
||||
|
||||
icon_layout = QVBoxLayout(icon_container)
|
||||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
icon_label = QLabel("🤖")
|
||||
icon_label.setAlignment(Qt.AlignCenter)
|
||||
icon_label.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
icon_layout.addWidget(icon_label)
|
||||
|
||||
# Text Container
|
||||
text_container = QFrame()
|
||||
text_layout = QVBoxLayout(text_container)
|
||||
text_layout.setContentsMargins(0, 0, 0, 0)
|
||||
text_layout.setSpacing(5)
|
||||
|
||||
# Titel
|
||||
info_title = QLabel("Automatisierung läuft")
|
||||
info_title.setObjectName("infoTitle")
|
||||
info_title.setStyleSheet("""
|
||||
QLabel#infoTitle {
|
||||
color: #1E40AF;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
font-family: 'Segoe UI', -apple-system, sans-serif;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
|
||||
# Beschreibung (deutlicher)
|
||||
info_desc = QLabel("Der Browser arbeitet automatisch. Bitte warten Sie, bis der Vorgang abgeschlossen ist.")
|
||||
info_desc.setObjectName("infoDesc")
|
||||
info_desc.setWordWrap(True)
|
||||
info_desc.setStyleSheet("""
|
||||
QLabel#infoDesc {
|
||||
color: #1E40AF;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: 'Segoe UI', -apple-system, sans-serif;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
line-height: 20px;
|
||||
}
|
||||
""")
|
||||
|
||||
text_layout.addWidget(info_title)
|
||||
text_layout.addWidget(info_desc)
|
||||
text_layout.addStretch()
|
||||
|
||||
info_layout.addWidget(icon_container)
|
||||
info_layout.addWidget(text_container, 1)
|
||||
|
||||
content_layout.addWidget(info_frame)
|
||||
|
||||
# Status Container
|
||||
status_container = QFrame()
|
||||
status_container.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #1A365D;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
|
||||
status_layout = QVBoxLayout(status_container)
|
||||
|
||||
# Status-Label (größer)
|
||||
self.status_label = QLabel("Initialisiere...")
|
||||
self.status_label.setObjectName("statusLabel")
|
||||
self.status_label.setAlignment(Qt.AlignLeft)
|
||||
self.status_label.setStyleSheet("""
|
||||
QLabel#statusLabel {
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
}
|
||||
""")
|
||||
status_layout.addWidget(self.status_label)
|
||||
|
||||
content_layout.addWidget(status_container)
|
||||
|
||||
# Log-Ausgabe (größer)
|
||||
self.log_output = QTextEdit()
|
||||
self.log_output.setReadOnly(True)
|
||||
self.log_output.setMaximumHeight(180) # Etwas größer
|
||||
self.log_output.setStyleSheet("""
|
||||
QTextEdit {
|
||||
background-color: #F8FAFC;
|
||||
color: #2D3748;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
|
||||
font-size: 12px;
|
||||
border: 1px solid #CBD5E0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
""")
|
||||
content_layout.addWidget(self.log_output)
|
||||
|
||||
# Button-Container
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch()
|
||||
|
||||
# Abbrechen-Button (weniger prominent)
|
||||
self.cancel_button = QPushButton("Abbrechen")
|
||||
self.cancel_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #F0F4F8;
|
||||
color: #4A5568;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 8px 24px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid #E2E8F0;
|
||||
min-height: 40px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #FEE2E2;
|
||||
color: #DC2626;
|
||||
border-color: #DC2626;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #DC2626;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
""")
|
||||
self.cancel_button.clicked.connect(self.cancel_clicked.emit)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
|
||||
button_layout.addStretch()
|
||||
content_layout.addLayout(button_layout)
|
||||
|
||||
layout.addWidget(content_container)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Volle Sichtbarkeit
|
||||
self.setWindowOpacity(1.0)
|
||||
|
||||
def start_animation(self):
|
||||
"""Zeigt den Dialog an"""
|
||||
self.status_label.setText("Initialisiere...")
|
||||
self.raise_timer.start() # Starte Timer für Always-on-Top
|
||||
|
||||
def stop_animation(self):
|
||||
"""Stoppt die Animation und den Timer"""
|
||||
self.raise_timer.stop()
|
||||
|
||||
def set_status(self, status: str):
|
||||
"""Aktualisiert den Status-Text"""
|
||||
self.status_label.setText(status)
|
||||
|
||||
def add_log(self, message: str):
|
||||
"""Fügt eine Log-Nachricht hinzu"""
|
||||
self.log_output.append(message)
|
||||
# Auto-scroll zum Ende
|
||||
scrollbar = self.log_output.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
def clear_log(self):
|
||||
"""Löscht alle Log-Nachrichten"""
|
||||
self.log_output.clear()
|
||||
|
||||
def set_progress(self, value: int):
|
||||
"""Setzt den Fortschritt (0-100) - wird ignoriert da wir Spinner nutzen"""
|
||||
pass
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Wird aufgerufen wenn der Dialog geschlossen wird"""
|
||||
self.stop_animation()
|
||||
self.closed.emit()
|
||||
event.accept()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Verhindert das Schließen mit ESC"""
|
||||
if event.key() == Qt.Key_Escape:
|
||||
event.ignore()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def _raise_to_front(self):
|
||||
"""Holt den Dialog in den Vordergrund"""
|
||||
self.raise_()
|
||||
|
||||
def show(self):
|
||||
"""Überschreibt show() um den Dialog richtig zu positionieren"""
|
||||
super().show()
|
||||
self._raise_to_front() # Initial in den Vordergrund holen
|
||||
|
||||
|
||||
# Zusätzliche Widget-Klasse für den AccountForger
|
||||
class ForgeAnimationWidget(QLabel):
|
||||
"""
|
||||
Einfaches Animation Widget für den Progress Modal
|
||||
Kann einen Spinner oder andere Animation anzeigen
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setText("⚙️") # Placeholder Icon
|
||||
self.setAlignment(Qt.AlignCenter)
|
||||
self.setStyleSheet("""
|
||||
QLabel {
|
||||
font-size: 48px;
|
||||
color: #3182CE;
|
||||
}
|
||||
""")
|
||||
|
||||
# Animation Timer
|
||||
self.animation_timer = QTimer()
|
||||
self.animation_timer.timeout.connect(self.rotate_icon)
|
||||
self.rotation_state = 0
|
||||
|
||||
def start_animation(self):
|
||||
"""Startet die Animation"""
|
||||
self.animation_timer.start(100)
|
||||
|
||||
def stop_animation(self):
|
||||
"""Stoppt die Animation"""
|
||||
self.animation_timer.stop()
|
||||
|
||||
def rotate_icon(self):
|
||||
"""Einfache Rotation Animation"""
|
||||
icons = ["⚙️", "🔧", "🔨", "⚒️"]
|
||||
self.setText(icons[self.rotation_state % len(icons)])
|
||||
self.rotation_state += 1
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren