From 08ed93810528d5be1497960dd51af43f20dba262 Mon Sep 17 00:00:00 2001 From: Claude Project Manager Date: Thu, 3 Jul 2025 21:11:05 +0200 Subject: [PATCH] Initial commit --- CLAUDE_PROJECT_README.md | 188 ++++ README.md | 130 +++ browser/__init__.py | 0 browser/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 145 bytes browser/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 144 bytes .../fingerprint_protection.cpython-310.pyc | Bin 0 -> 31787 bytes .../fingerprint_protection.cpython-313.pyc | Bin 0 -> 34746 bytes .../playwright_extensions.cpython-310.pyc | Bin 0 -> 3882 bytes .../playwright_extensions.cpython-313.pyc | Bin 0 -> 5485 bytes .../playwright_manager.cpython-310.pyc | Bin 0 -> 15864 bytes .../playwright_manager.cpython-313.pyc | Bin 0 -> 22064 bytes browser/fingerprint_protection.py | 721 +++++++++++++ browser/playwright_extensions.py | 127 +++ browser/playwright_manager.py | 517 +++++++++ browser/stealth_config.py | 216 ++++ config/.machine_id | 1 + config/__init__.py | 0 config/app_version.json | 7 + config/browser_config.json | 0 config/email_config.json | 6 + config/facebook_config.json | 0 config/instagram_config.json | 0 config/license.json | 10 + config/license_config.json | 9 + config/proxy_config.json | 15 + config/stealth_config.json | 14 + config/theme.json | 46 + config/tiktok_config.json | 0 config/twitter_config.json | 0 config/update_config.json | 9 + config/user_agents.json | 31 + .../account_controller.cpython-310.pyc | Bin 0 -> 4459 bytes .../account_controller.cpython-313.pyc | Bin 0 -> 7269 bytes .../main_controller.cpython-310.pyc | Bin 0 -> 6398 bytes .../main_controller.cpython-313.pyc | Bin 0 -> 11618 bytes .../settings_controller.cpython-310.pyc | Bin 0 -> 7469 bytes .../settings_controller.cpython-313.pyc | Bin 0 -> 12821 bytes controllers/account_controller.py | 149 +++ controllers/main_controller.py | 231 ++++ .../base_controller.cpython-310.pyc | Bin 0 -> 4435 bytes .../base_controller.cpython-313.pyc | Bin 0 -> 6495 bytes .../instagram_controller.cpython-310.pyc | Bin 0 -> 9508 bytes .../instagram_controller.cpython-313.pyc | Bin 0 -> 14705 bytes .../tiktok_controller.cpython-310.pyc | Bin 0 -> 9603 bytes .../tiktok_controller.cpython-313.pyc | Bin 0 -> 14802 bytes .../platform_controllers/base_controller.py | 132 +++ .../instagram_controller.py | 275 +++++ .../platform_controllers/tiktok_controller.py | 277 +++++ controllers/settings_controller.py | 294 ++++++ database/__init__.py | 0 database/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 146 bytes database/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 145 bytes .../__pycache__/db_manager.cpython-310.pyc | Bin 0 -> 13791 bytes .../__pycache__/db_manager.cpython-313.pyc | Bin 0 -> 20134 bytes database/account_repository.py | 0 database/accounts.db | Bin 0 -> 20480 bytes database/db_manager.py | 480 +++++++++ database/instagram_accounts.db | Bin 0 -> 28672 bytes database/schema.sql | 112 ++ licensing/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 147 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 146 bytes .../license_manager.cpython-310.pyc | Bin 0 -> 10657 bytes .../license_manager.cpython-313.pyc | Bin 0 -> 18107 bytes licensing/hardware_fingerprint.py | 0 licensing/license_manager.py | 450 ++++++++ licensing/license_validator.py | 304 ++++++ localization/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 150 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 149 bytes .../language_manager.cpython-310.pyc | Bin 0 -> 7411 bytes .../language_manager.cpython-313.pyc | Bin 0 -> 11230 bytes localization/language_manager.py | 272 +++++ localization/languages/de.json | 102 ++ localization/languages/en.json | 102 ++ localization/languages/es.json | 102 ++ localization/languages/fr.json | 102 ++ localization/languages/ja.json | 102 ++ logs/.gitkeep | 0 logs/main.log | 34 + main.py | 46 + requirements.txt | 27 + resources/icons/de.svg | 2 + resources/icons/en.svg | 50 + resources/icons/es.svg | 114 ++ resources/icons/facebook.svg | 17 + resources/icons/fr.svg | 2 + resources/icons/instagram.svg | 27 + resources/icons/ja.svg | 2 + resources/icons/moon.svg | 4 + resources/icons/sun.svg | 7 + resources/icons/tiktok.svg | 15 + resources/icons/twitter.svg | 2 + resources/icons/vk.svg | 7 + resources/themes/dark.qss | 1 + resources/themes/light.qss | 1 + social_networks/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 153 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 152 bytes .../base_automation.cpython-310.pyc | Bin 0 -> 15815 bytes .../base_automation.cpython-313.pyc | Bin 0 -> 20193 bytes social_networks/base_automation.py | 487 +++++++++ social_networks/facebook/__init__.py | 0 .../facebook/facebook_automation.py | 0 social_networks/facebook/facebook_login.py | 0 .../facebook/facebook_registration.py | 0 .../facebook/facebook_selectors.py | 0 .../facebook/facebook_ui_helper.py | 0 social_networks/facebook/facebook_utils.py | 0 .../facebook/facebook_verification.py | 0 social_networks/facebook/facebook_workflow.py | 0 social_networks/instagram/__init__.py | 4 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 249 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 257 bytes .../instagram_automation.cpython-310.pyc | Bin 0 -> 9483 bytes .../instagram_automation.cpython-313.pyc | Bin 0 -> 14214 bytes .../instagram_login.cpython-310.pyc | Bin 0 -> 13361 bytes .../instagram_login.cpython-313.pyc | Bin 0 -> 21283 bytes .../instagram_registration.cpython-310.pyc | Bin 0 -> 17419 bytes .../instagram_registration.cpython-313.pyc | Bin 0 -> 27936 bytes .../instagram_selectors.cpython-310.pyc | Bin 0 -> 8602 bytes .../instagram_selectors.cpython-313.pyc | Bin 0 -> 9228 bytes .../instagram_ui_helper.cpython-310.pyc | Bin 0 -> 16751 bytes .../instagram_ui_helper.cpython-313.pyc | Bin 0 -> 26787 bytes .../instagram_utils.cpython-310.pyc | Bin 0 -> 13237 bytes .../instagram_utils.cpython-313.pyc | Bin 0 -> 20308 bytes .../instagram_verification.cpython-310.pyc | Bin 0 -> 12360 bytes .../instagram_verification.cpython-313.pyc | Bin 0 -> 18599 bytes .../instagram_workflow.cpython-310.pyc | Bin 0 -> 11211 bytes .../instagram_workflow.cpython-313.pyc | Bin 0 -> 14346 bytes .../instagram/instagram_automation.py | 330 ++++++ social_networks/instagram/instagram_login.py | 576 ++++++++++ .../instagram/instagram_registration.py | 735 +++++++++++++ .../instagram/instagram_selectors.py | 242 +++++ .../instagram/instagram_ui_helper.py | 668 ++++++++++++ social_networks/instagram/instagram_utils.py | 549 ++++++++++ .../instagram/instagram_verification.py | 491 +++++++++ .../instagram/instagram_workflow.py | 454 ++++++++ social_networks/tiktok/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 160 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 176 bytes .../tiktok_automation.cpython-310.pyc | Bin 0 -> 9387 bytes .../tiktok_automation.cpython-313.pyc | Bin 0 -> 14135 bytes .../__pycache__/tiktok_login.cpython-310.pyc | Bin 0 -> 13499 bytes .../__pycache__/tiktok_login.cpython-313.pyc | Bin 0 -> 21589 bytes .../tiktok_registration.cpython-310.pyc | Bin 0 -> 22761 bytes .../tiktok_registration.cpython-313.pyc | Bin 0 -> 38900 bytes .../tiktok_selectors.cpython-310.pyc | Bin 0 -> 5312 bytes .../tiktok_selectors.cpython-313.pyc | Bin 0 -> 5767 bytes .../tiktok_ui_helper.cpython-310.pyc | Bin 0 -> 13341 bytes .../tiktok_ui_helper.cpython-313.pyc | Bin 0 -> 21463 bytes .../__pycache__/tiktok_utils.cpython-310.pyc | Bin 0 -> 12561 bytes .../__pycache__/tiktok_utils.cpython-313.pyc | Bin 0 -> 19318 bytes .../tiktok_verification.cpython-310.pyc | Bin 0 -> 11527 bytes .../tiktok_verification.cpython-313.pyc | Bin 0 -> 17330 bytes .../tiktok_workflow.cpython-310.pyc | Bin 0 -> 8736 bytes .../tiktok_workflow.cpython-313.pyc | Bin 0 -> 10985 bytes social_networks/tiktok/tiktok_automation.py | 328 ++++++ social_networks/tiktok/tiktok_login.py | 582 +++++++++++ social_networks/tiktok/tiktok_registration.py | 987 ++++++++++++++++++ social_networks/tiktok/tiktok_selectors.py | 150 +++ social_networks/tiktok/tiktok_ui_helper.py | 523 ++++++++++ social_networks/tiktok/tiktok_utils.py | 495 +++++++++ social_networks/tiktok/tiktok_verification.py | 457 ++++++++ social_networks/tiktok/tiktok_workflow.py | 427 ++++++++ social_networks/twitter/__init__.py | 0 social_networks/twitter/twitter_automation.py | 0 social_networks/twitter/twitter_login.py | 0 .../twitter/twitter_registration.py | 0 social_networks/twitter/twitter_selectors.py | 0 social_networks/twitter/twitter_ui_helper.py | 0 social_networks/twitter/twitter_utils.py | 0 .../twitter/twitter_verification.py | 0 social_networks/twitter/twitter_workflow.py | 0 testcases/imap_test.py | 679 ++++++++++++ updates/__init__.py | 0 updates/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 145 bytes updates/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 144 bytes .../update_checker.cpython-310.pyc | Bin 0 -> 11499 bytes .../update_checker.cpython-313.pyc | Bin 0 -> 16077 bytes updates/__pycache__/version.cpython-310.pyc | Bin 0 -> 4993 bytes updates/downloader.py | 0 updates/update_checker.py | 411 ++++++++ updates/update_v1.1.0.zip | 1 + updates/version.py | 193 ++++ utils/__init__.py | 0 utils/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 143 bytes utils/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 142 bytes .../birthday_generator.cpython-310.pyc | Bin 0 -> 7838 bytes .../birthday_generator.cpython-313.pyc | Bin 0 -> 10814 bytes .../__pycache__/email_handler.cpython-310.pyc | Bin 0 -> 15599 bytes .../__pycache__/email_handler.cpython-313.pyc | Bin 0 -> 23277 bytes .../human_behavior.cpython-310.pyc | Bin 0 -> 14017 bytes .../human_behavior.cpython-313.pyc | Bin 0 -> 19424 bytes utils/__pycache__/logger.cpython-310.pyc | Bin 0 -> 2197 bytes utils/__pycache__/logger.cpython-313.pyc | Bin 0 -> 3389 bytes .../password_generator.cpython-310.pyc | Bin 0 -> 9816 bytes .../password_generator.cpython-313.pyc | Bin 0 -> 13532 bytes .../__pycache__/proxy_rotator.cpython-310.pyc | Bin 0 -> 10229 bytes .../__pycache__/proxy_rotator.cpython-313.pyc | Bin 0 -> 15674 bytes .../text_similarity.cpython-310.pyc | Bin 0 -> 13628 bytes .../text_similarity.cpython-313.pyc | Bin 0 -> 19243 bytes .../__pycache__/theme_manager.cpython-310.pyc | Bin 0 -> 4254 bytes .../__pycache__/theme_manager.cpython-313.pyc | Bin 0 -> 7731 bytes .../username_generator.cpython-310.pyc | Bin 0 -> 14099 bytes .../username_generator.cpython-313.pyc | Bin 0 -> 20546 bytes utils/birthday_generator.py | 299 ++++++ utils/email_handler.py | 646 ++++++++++++ utils/human_behavior.py | 488 +++++++++ utils/logger.py | 69 ++ utils/password_generator.py | 338 ++++++ utils/proxy_rotator.py | 413 ++++++++ utils/text_similarity.py | 558 ++++++++++ utils/theme_manager.py | 133 +++ utils/update_checker.py | 733 +++++++++++++ utils/username_generator.py | 465 +++++++++ .../__pycache__/about_dialog.cpython-310.pyc | Bin 0 -> 2252 bytes views/__pycache__/main_window.cpython-310.pyc | Bin 0 -> 6216 bytes views/__pycache__/main_window.cpython-313.pyc | Bin 0 -> 15448 bytes .../platform_selector.cpython-310.pyc | Bin 0 -> 3797 bytes .../platform_selector.cpython-313.pyc | Bin 0 -> 6025 bytes views/about_dialog.py | 68 ++ views/main_window.py | 235 +++++ views/platform_selector.py | 148 +++ .../__pycache__/accounts_tab.cpython-310.pyc | Bin 0 -> 6010 bytes .../__pycache__/accounts_tab.cpython-313.pyc | Bin 0 -> 9556 bytes .../__pycache__/generator_tab.cpython-310.pyc | Bin 0 -> 12462 bytes .../__pycache__/generator_tab.cpython-313.pyc | Bin 0 -> 17410 bytes .../__pycache__/settings_tab.cpython-310.pyc | Bin 0 -> 8352 bytes .../__pycache__/settings_tab.cpython-313.pyc | Bin 0 -> 17205 bytes views/tabs/accounts_tab.py | 207 ++++ views/tabs/generator_tab.py | 500 +++++++++ views/tabs/settings_tab.py | 315 ++++++ .../language_dropdown.cpython-310.pyc | Bin 0 -> 6259 bytes .../language_dropdown.cpython-313.pyc | Bin 0 -> 10328 bytes .../platform_button.cpython-310.pyc | Bin 0 -> 2184 bytes .../platform_button.cpython-313.pyc | Bin 0 -> 3950 bytes views/widgets/language_dropdown.py | 203 ++++ views/widgets/platform_button.py | 76 ++ 239 files changed, 21554 insertions(+) create mode 100644 CLAUDE_PROJECT_README.md create mode 100644 README.md create mode 100644 browser/__init__.py create mode 100644 browser/__pycache__/__init__.cpython-310.pyc create mode 100644 browser/__pycache__/__init__.cpython-313.pyc create mode 100644 browser/__pycache__/fingerprint_protection.cpython-310.pyc create mode 100644 browser/__pycache__/fingerprint_protection.cpython-313.pyc create mode 100644 browser/__pycache__/playwright_extensions.cpython-310.pyc create mode 100644 browser/__pycache__/playwright_extensions.cpython-313.pyc create mode 100644 browser/__pycache__/playwright_manager.cpython-310.pyc create mode 100644 browser/__pycache__/playwright_manager.cpython-313.pyc create mode 100644 browser/fingerprint_protection.py create mode 100644 browser/playwright_extensions.py create mode 100644 browser/playwright_manager.py create mode 100644 browser/stealth_config.py create mode 100644 config/.machine_id create mode 100644 config/__init__.py create mode 100644 config/app_version.json create mode 100644 config/browser_config.json create mode 100644 config/email_config.json create mode 100644 config/facebook_config.json create mode 100644 config/instagram_config.json create mode 100644 config/license.json create mode 100644 config/license_config.json create mode 100644 config/proxy_config.json create mode 100644 config/stealth_config.json create mode 100644 config/theme.json create mode 100644 config/tiktok_config.json create mode 100644 config/twitter_config.json create mode 100644 config/update_config.json create mode 100644 config/user_agents.json create mode 100644 controllers/__pycache__/account_controller.cpython-310.pyc create mode 100644 controllers/__pycache__/account_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/main_controller.cpython-310.pyc create mode 100644 controllers/__pycache__/main_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/settings_controller.cpython-310.pyc create mode 100644 controllers/__pycache__/settings_controller.cpython-313.pyc create mode 100644 controllers/account_controller.py create mode 100644 controllers/main_controller.py create mode 100644 controllers/platform_controllers/__pycache__/base_controller.cpython-310.pyc create mode 100644 controllers/platform_controllers/__pycache__/base_controller.cpython-313.pyc create mode 100644 controllers/platform_controllers/__pycache__/instagram_controller.cpython-310.pyc create mode 100644 controllers/platform_controllers/__pycache__/instagram_controller.cpython-313.pyc create mode 100644 controllers/platform_controllers/__pycache__/tiktok_controller.cpython-310.pyc create mode 100644 controllers/platform_controllers/__pycache__/tiktok_controller.cpython-313.pyc create mode 100644 controllers/platform_controllers/base_controller.py create mode 100644 controllers/platform_controllers/instagram_controller.py create mode 100644 controllers/platform_controllers/tiktok_controller.py create mode 100644 controllers/settings_controller.py create mode 100644 database/__init__.py create mode 100644 database/__pycache__/__init__.cpython-310.pyc create mode 100644 database/__pycache__/__init__.cpython-313.pyc create mode 100644 database/__pycache__/db_manager.cpython-310.pyc create mode 100644 database/__pycache__/db_manager.cpython-313.pyc create mode 100644 database/account_repository.py create mode 100644 database/accounts.db create mode 100644 database/db_manager.py create mode 100644 database/instagram_accounts.db create mode 100644 database/schema.sql create mode 100644 licensing/__init__.py create mode 100644 licensing/__pycache__/__init__.cpython-310.pyc create mode 100644 licensing/__pycache__/__init__.cpython-313.pyc create mode 100644 licensing/__pycache__/license_manager.cpython-310.pyc create mode 100644 licensing/__pycache__/license_manager.cpython-313.pyc create mode 100644 licensing/hardware_fingerprint.py create mode 100644 licensing/license_manager.py create mode 100644 licensing/license_validator.py create mode 100644 localization/__init__.py create mode 100644 localization/__pycache__/__init__.cpython-310.pyc create mode 100644 localization/__pycache__/__init__.cpython-313.pyc create mode 100644 localization/__pycache__/language_manager.cpython-310.pyc create mode 100644 localization/__pycache__/language_manager.cpython-313.pyc create mode 100644 localization/language_manager.py create mode 100644 localization/languages/de.json create mode 100644 localization/languages/en.json create mode 100644 localization/languages/es.json create mode 100644 localization/languages/fr.json create mode 100644 localization/languages/ja.json create mode 100644 logs/.gitkeep create mode 100644 logs/main.log create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 resources/icons/de.svg create mode 100644 resources/icons/en.svg create mode 100644 resources/icons/es.svg create mode 100644 resources/icons/facebook.svg create mode 100644 resources/icons/fr.svg create mode 100644 resources/icons/instagram.svg create mode 100644 resources/icons/ja.svg create mode 100644 resources/icons/moon.svg create mode 100644 resources/icons/sun.svg create mode 100644 resources/icons/tiktok.svg create mode 100644 resources/icons/twitter.svg create mode 100644 resources/icons/vk.svg create mode 100644 resources/themes/dark.qss create mode 100644 resources/themes/light.qss create mode 100644 social_networks/__init__.py create mode 100644 social_networks/__pycache__/__init__.cpython-310.pyc create mode 100644 social_networks/__pycache__/__init__.cpython-313.pyc create mode 100644 social_networks/__pycache__/base_automation.cpython-310.pyc create mode 100644 social_networks/__pycache__/base_automation.cpython-313.pyc create mode 100644 social_networks/base_automation.py create mode 100644 social_networks/facebook/__init__.py create mode 100644 social_networks/facebook/facebook_automation.py create mode 100644 social_networks/facebook/facebook_login.py create mode 100644 social_networks/facebook/facebook_registration.py create mode 100644 social_networks/facebook/facebook_selectors.py create mode 100644 social_networks/facebook/facebook_ui_helper.py create mode 100644 social_networks/facebook/facebook_utils.py create mode 100644 social_networks/facebook/facebook_verification.py create mode 100644 social_networks/facebook/facebook_workflow.py create mode 100644 social_networks/instagram/__init__.py create mode 100644 social_networks/instagram/__pycache__/__init__.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/__init__.cpython-313.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_automation.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_automation.cpython-313.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_login.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_login.cpython-313.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_registration.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_registration.cpython-313.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_selectors.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_selectors.cpython-313.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_ui_helper.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_ui_helper.cpython-313.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_utils.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_utils.cpython-313.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_verification.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_verification.cpython-313.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_workflow.cpython-310.pyc create mode 100644 social_networks/instagram/__pycache__/instagram_workflow.cpython-313.pyc create mode 100644 social_networks/instagram/instagram_automation.py create mode 100644 social_networks/instagram/instagram_login.py create mode 100644 social_networks/instagram/instagram_registration.py create mode 100644 social_networks/instagram/instagram_selectors.py create mode 100644 social_networks/instagram/instagram_ui_helper.py create mode 100644 social_networks/instagram/instagram_utils.py create mode 100644 social_networks/instagram/instagram_verification.py create mode 100644 social_networks/instagram/instagram_workflow.py create mode 100644 social_networks/tiktok/__init__.py create mode 100644 social_networks/tiktok/__pycache__/__init__.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/__init__.cpython-313.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_automation.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_automation.cpython-313.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_login.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_login.cpython-313.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_registration.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_registration.cpython-313.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_selectors.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_selectors.cpython-313.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-313.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_utils.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_utils.cpython-313.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_verification.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_verification.cpython-313.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_workflow.cpython-310.pyc create mode 100644 social_networks/tiktok/__pycache__/tiktok_workflow.cpython-313.pyc create mode 100644 social_networks/tiktok/tiktok_automation.py create mode 100644 social_networks/tiktok/tiktok_login.py create mode 100644 social_networks/tiktok/tiktok_registration.py create mode 100644 social_networks/tiktok/tiktok_selectors.py create mode 100644 social_networks/tiktok/tiktok_ui_helper.py create mode 100644 social_networks/tiktok/tiktok_utils.py create mode 100644 social_networks/tiktok/tiktok_verification.py create mode 100644 social_networks/tiktok/tiktok_workflow.py create mode 100644 social_networks/twitter/__init__.py create mode 100644 social_networks/twitter/twitter_automation.py create mode 100644 social_networks/twitter/twitter_login.py create mode 100644 social_networks/twitter/twitter_registration.py create mode 100644 social_networks/twitter/twitter_selectors.py create mode 100644 social_networks/twitter/twitter_ui_helper.py create mode 100644 social_networks/twitter/twitter_utils.py create mode 100644 social_networks/twitter/twitter_verification.py create mode 100644 social_networks/twitter/twitter_workflow.py create mode 100644 testcases/imap_test.py create mode 100644 updates/__init__.py create mode 100644 updates/__pycache__/__init__.cpython-310.pyc create mode 100644 updates/__pycache__/__init__.cpython-313.pyc create mode 100644 updates/__pycache__/update_checker.cpython-310.pyc create mode 100644 updates/__pycache__/update_checker.cpython-313.pyc create mode 100644 updates/__pycache__/version.cpython-310.pyc create mode 100644 updates/downloader.py create mode 100644 updates/update_checker.py create mode 100644 updates/update_v1.1.0.zip create mode 100644 updates/version.py create mode 100644 utils/__init__.py create mode 100644 utils/__pycache__/__init__.cpython-310.pyc create mode 100644 utils/__pycache__/__init__.cpython-313.pyc create mode 100644 utils/__pycache__/birthday_generator.cpython-310.pyc create mode 100644 utils/__pycache__/birthday_generator.cpython-313.pyc create mode 100644 utils/__pycache__/email_handler.cpython-310.pyc create mode 100644 utils/__pycache__/email_handler.cpython-313.pyc create mode 100644 utils/__pycache__/human_behavior.cpython-310.pyc create mode 100644 utils/__pycache__/human_behavior.cpython-313.pyc create mode 100644 utils/__pycache__/logger.cpython-310.pyc create mode 100644 utils/__pycache__/logger.cpython-313.pyc create mode 100644 utils/__pycache__/password_generator.cpython-310.pyc create mode 100644 utils/__pycache__/password_generator.cpython-313.pyc create mode 100644 utils/__pycache__/proxy_rotator.cpython-310.pyc create mode 100644 utils/__pycache__/proxy_rotator.cpython-313.pyc create mode 100644 utils/__pycache__/text_similarity.cpython-310.pyc create mode 100644 utils/__pycache__/text_similarity.cpython-313.pyc create mode 100644 utils/__pycache__/theme_manager.cpython-310.pyc create mode 100644 utils/__pycache__/theme_manager.cpython-313.pyc create mode 100644 utils/__pycache__/username_generator.cpython-310.pyc create mode 100644 utils/__pycache__/username_generator.cpython-313.pyc create mode 100644 utils/birthday_generator.py create mode 100644 utils/email_handler.py create mode 100644 utils/human_behavior.py create mode 100644 utils/logger.py create mode 100644 utils/password_generator.py create mode 100644 utils/proxy_rotator.py create mode 100644 utils/text_similarity.py create mode 100644 utils/theme_manager.py create mode 100644 utils/update_checker.py create mode 100644 utils/username_generator.py create mode 100644 views/__pycache__/about_dialog.cpython-310.pyc create mode 100644 views/__pycache__/main_window.cpython-310.pyc create mode 100644 views/__pycache__/main_window.cpython-313.pyc create mode 100644 views/__pycache__/platform_selector.cpython-310.pyc create mode 100644 views/__pycache__/platform_selector.cpython-313.pyc create mode 100644 views/about_dialog.py create mode 100644 views/main_window.py create mode 100644 views/platform_selector.py create mode 100644 views/tabs/__pycache__/accounts_tab.cpython-310.pyc create mode 100644 views/tabs/__pycache__/accounts_tab.cpython-313.pyc create mode 100644 views/tabs/__pycache__/generator_tab.cpython-310.pyc create mode 100644 views/tabs/__pycache__/generator_tab.cpython-313.pyc create mode 100644 views/tabs/__pycache__/settings_tab.cpython-310.pyc create mode 100644 views/tabs/__pycache__/settings_tab.cpython-313.pyc create mode 100644 views/tabs/accounts_tab.py create mode 100644 views/tabs/generator_tab.py create mode 100644 views/tabs/settings_tab.py create mode 100644 views/widgets/__pycache__/language_dropdown.cpython-310.pyc create mode 100644 views/widgets/__pycache__/language_dropdown.cpython-313.pyc create mode 100644 views/widgets/__pycache__/platform_button.cpython-310.pyc create mode 100644 views/widgets/__pycache__/platform_button.cpython-313.pyc create mode 100644 views/widgets/language_dropdown.py create mode 100644 views/widgets/platform_button.py diff --git a/CLAUDE_PROJECT_README.md b/CLAUDE_PROJECT_README.md new file mode 100644 index 0000000..32e2ded --- /dev/null +++ b/CLAUDE_PROJECT_README.md @@ -0,0 +1,188 @@ +# test-main + +*This README was automatically generated by Claude Project Manager* + +## Project Overview + +- **Path**: `C:/Users/hendr/Desktop/IntelSight/test-main` +- **Files**: 125 files +- **Size**: 884.4 KB +- **Last Modified**: 2025-07-01 22:08 + +## Technology Stack + +### Languages +- Python + +## Project Structure + +``` +CLAUDE_PROJECT_README.md +main.py +README.md +requirements.txt +browser/ +│ ├── fingerprint_protection.py +│ ├── playwright_extensions.py +│ ├── playwright_manager.py +│ ├── stealth_config.py +│ └── __init__.py +config/ +│ ├── app_version.json +│ ├── browser_config.json +│ ├── email_config.json +│ ├── facebook_config.json +│ ├── instagram_config.json +│ ├── license.json +│ ├── license_config.json +│ ├── proxy_config.json +│ └── stealth_config.json +controllers/ +│ ├── account_controller.py +│ ├── main_controller.py +│ ├── settings_controller.py +│ └── platform_controllers/ +│ ├── base_controller.py +│ ├── instagram_controller.py +│ └── tiktok_controller.py +database/ +│ ├── accounts.db +│ ├── account_repository.py +│ ├── db_manager.py +│ ├── instagram_accounts.db +│ ├── schema.sql +│ └── __init__.py +licensing/ +│ ├── hardware_fingerprint.py +│ ├── license_manager.py +│ ├── license_validator.py +│ └── __init__.py +localization/ +│ ├── language_manager.py +│ ├── __init__.py +│ └── languages/ +│ ├── de.json +│ ├── en.json +│ ├── es.json +│ ├── fr.json +│ └── ja.json +logs/ +│ ├── main.log +│ └── screenshots +resources/ +│ ├── icons/ +│ │ ├── de.svg +│ │ ├── en.svg +│ │ ├── es.svg +│ │ ├── facebook.svg +│ │ ├── fr.svg +│ │ ├── instagram.svg +│ │ ├── ja.svg +│ │ ├── moon.svg +│ │ ├── sun.svg +│ │ └── tiktok.svg +│ └── themes/ +│ ├── dark.qss +│ └── light.qss +social_networks/ +│ ├── base_automation.py +│ ├── __init__.py +│ ├── facebook/ +│ │ ├── facebook_automation.py +│ │ ├── facebook_login.py +│ │ ├── facebook_registration.py +│ │ ├── facebook_selectors.py +│ │ ├── facebook_ui_helper.py +│ │ ├── facebook_utils.py +│ │ ├── facebook_verification.py +│ │ ├── facebook_workflow.py +│ │ └── __init__.py +│ ├── instagram/ +│ │ ├── instagram_automation.py +│ │ ├── instagram_login.py +│ │ ├── instagram_registration.py +│ │ ├── instagram_selectors.py +│ │ ├── instagram_ui_helper.py +│ │ ├── instagram_utils.py +│ │ ├── instagram_verification.py +│ │ ├── instagram_workflow.py +│ │ └── __init__.py +│ ├── tiktok/ +│ │ ├── tiktok_automation.py +│ │ ├── tiktok_login.py +│ │ ├── tiktok_registration.py +│ │ ├── tiktok_selectors.py +│ │ ├── tiktok_ui_helper.py +│ │ ├── tiktok_utils.py +│ │ ├── tiktok_verification.py +│ │ ├── tiktok_workflow.py +│ │ └── __init__.py +│ └── twitter/ +│ ├── twitter_automation.py +│ ├── twitter_login.py +│ ├── twitter_registration.py +│ ├── twitter_selectors.py +│ ├── twitter_ui_helper.py +│ ├── twitter_utils.py +│ ├── twitter_verification.py +│ ├── twitter_workflow.py +│ └── __init__.py +testcases/ +│ └── imap_test.py +updates/ +│ ├── downloader.py +│ ├── update_checker.py +│ ├── update_v1.1.0.zip +│ ├── version.py +│ └── __init__.py +utils/ +│ ├── birthday_generator.py +│ ├── email_handler.py +│ ├── human_behavior.py +│ ├── logger.py +│ ├── password_generator.py +│ ├── proxy_rotator.py +│ ├── text_similarity.py +│ ├── theme_manager.py +│ ├── update_checker.py +│ └── username_generator.py +views/ + ├── about_dialog.py + ├── main_window.py + ├── platform_selector.py + ├── tabs/ + │ ├── accounts_tab.py + │ ├── generator_tab.py + │ └── settings_tab.py + └── widgets/ + ├── language_dropdown.py + └── platform_button.py +``` + +## Key Files + +- `README.md` +- `requirements.txt` + +## Claude Integration + +This project is managed with Claude Project Manager. To work with this project: + +1. Open Claude Project Manager +2. Click on this project's tile +3. Claude will open in the project directory + +## Notes + +*Add your project-specific notes here* + +--- + +## Development Log + +- README generated on 2025-07-01 20:43:39 +- README updated on 2025-07-01 21:09:06 +- README updated on 2025-07-01 21:59:23 +- README updated on 2025-07-01 22:08:40 +- README updated on 2025-07-01 22:08:50 +- README updated on 2025-07-01 22:09:15 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c459dfa --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# Social Media Account Generator + +Dieses Repository enthält eine Desktopanwendung zur automatisierten Erstellung und Verwaltung von Social‑Media‑Accounts. Die grafische Oberfläche basiert auf **PyQt5**, die Browser‑Automatisierung erfolgt mit **Playwright**. Der Code ist modular aufgebaut und kann leicht um weitere Plattformen erweitert werden. + +## Installation + +1. Python 3.8 oder neuer installieren. +2. Abhängigkeiten mit `pip install -r requirements.txt` einrichten. + +## Anwendung starten + +```bash +python main.py +``` + +Beim ersten Start werden benötigte Ordner wie `logs`, `config` und `resources` automatisch angelegt. Einstellungen können im Ordner `config` angepasst werden. + +## Projektstruktur (Auszug) + +```text +. +├── main.py +├── browser/ +│ ├── playwright_manager.py +│ └── stealth_config.py +├── controllers/ +│ ├── main_controller.py +│ ├── account_controller.py +│ ├── settings_controller.py +│ └── platform_controllers/ +│ ├── base_controller.py +│ ├── instagram_controller.py +│ └── tiktok_controller.py +├── views/ +│ ├── main_window.py +│ ├── platform_selector.py +│ ├── about_dialog.py +│ ├── widgets/ +│ │ └── platform_button.py +│ └── tabs/ +│ ├── generator_tab.py +│ ├── accounts_tab.py +│ └── settings_tab.py +├── social_networks/ +│ ├── base_automation.py +│ ├── instagram/ +│ │ └── ... +│ ├── tiktok/ +│ │ └── ... +│ ├── facebook/ +│ │ └── ... +│ └── twitter/ +│ └── ... +├── localization/ +│ ├── language_manager.py +│ └── languages/ +│ ├── de.json +│ ├── en.json +│ ├── es.json +│ ├── fr.json +│ └── ja.json +├── utils/ +│ ├── logger.py +│ ├── password_generator.py +│ ├── username_generator.py +│ ├── birthday_generator.py +│ ├── email_handler.py +│ ├── proxy_rotator.py +│ ├── human_behavior.py +│ ├── text_similarity.py +│ └── theme_manager.py +├── database/ +│ ├── db_manager.py +│ └── ... +├── licensing/ +│ ├── license_manager.py +│ ├── hardware_fingerprint.py +│ └── license_validator.py +├── updates/ +│ ├── update_checker.py +│ ├── downloader.py +│ ├── version.py +│ └── ... +├── config/ +│ ├── browser_config.json +│ ├── email_config.json +│ ├── proxy_config.json +│ ├── stealth_config.json +│ ├── license_config.json +│ ├── instagram_config.json +│ ├── facebook_config.json +│ ├── twitter_config.json +│ ├── tiktok_config.json +│ ├── theme.json +│ ├── app_version.json +│ └── update_config.json +├── resources/ +│ ├── icons/ +│ │ ├── instagram.svg +│ │ ├── facebook.svg +│ │ ├── twitter.svg +│ │ ├── tiktok.svg +│ │ └── vk.svg +│ └── themes/ +│ ├── light.qss +│ └── dark.qss +├── testcases/ +│ └── imap_test.py +├── requirements.txt +└── README.md +``` + +Weitere Ordner: + +- `logs/` – Protokolldateien und Screenshots +- `resources/` – Icons und Theme‑Dateien +- `updates/` – heruntergeladene Updates + +## Lokalisierung + +Im Ordner `localization/languages` befinden sich Übersetzungsdateien für Deutsch, Englisch, Spanisch, Französisch und Japanisch. Die aktuelle Sprache kann zur Laufzeit gewechselt werden. + +## Lizenz und Updates + +Die Ordner `licensing` und `updates` enthalten die Logik zur Lizenzprüfung und zum Update‑Management. Versionsinformationen werden in `updates/version.py` verwaltet. + +## Tests + +Im Ordner `testcases` liegt beispielhaft `imap_test.py`, mit dem die IMAP‑Konfiguration getestet werden kann. + diff --git a/browser/__init__.py b/browser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/browser/__pycache__/__init__.cpython-310.pyc b/browser/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b5a7f95390e0ef0ec23568daeac16dd9da5611a GIT binary patch literal 145 zcmd1j<>g`kf*(-<86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6wn*(xTqIJKxa zCL=X3r6|TFwK%&ZzaXY0wYWq#H!(9WCaEaD9H<~BK0Y%qvm`!Vub}c4hfQvNN@-52 N9mup|CLqDW0067$A#(r# literal 0 HcmV?d00001 diff --git a/browser/__pycache__/__init__.cpython-313.pyc b/browser/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..487ccabc7f8035648245561dd3601f9ae0e7bb1d GIT binary patch literal 144 zcmey&%ge<81X~XAXMpI(AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl>x0;?G3jLh7` z%%a2?=lqmZ-GH3L%JQPj^o)|2q@w)t;?$y;`1s7c%#!$cy@JYH95%W6DWy57c15f} T{UGy;L5z>gjEsy$%s>_Z&YvOy literal 0 HcmV?d00001 diff --git a/browser/__pycache__/fingerprint_protection.cpython-310.pyc b/browser/__pycache__/fingerprint_protection.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23383da55a90bd2df9ea7804b89e386d69884947 GIT binary patch literal 31787 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ⅅq0wY&cqpTt%13mEJtdmznx}tDxOUeZE!FH`??R-#to=)zEi}17kr)!71vZ z1jS?Ns88AT0eCe8d}|E&))erqnZCugM5|&g(JCx+EjuiPxAG;c7nB)(6sL4MlV!Wz zRZs6ZQzkfkJoWd?WA8=$bevF5ggy?bG==vrP%?jgwx8vpEKilw~49 z8>5=BagsS?NDdn(nPRtLMs9m#VSXnn9-jrW#>wAtp2lzR^iF-3q`|*KNfwAws&U|wJ+xLc@#o^9Lv<``6*hiTon6()~v6;J|vQ^OL5Jn^sPZe7|vh!gRlQfLRj>Bf@&{QU79-_K5jL(3~g*D^$^Eqj^V>2Sq z{EI`Xk&j;o__5doja^+B2ESXTmg8>~{#Hy|9o*}%>4P|Q+a{{83y{UYNQLpU$Zu2(KDA03!@t<3pfmQl6Z_C6!;xvbDW{RY=;X) zPCN;WoGCiTWnB`7CPxcIxK>Ps(B<=Es8MftS+r7n6yI~|WOrdmLwx%gAo>M$kS}No zqETo3f+Wlr^u$7k5ivlCv>Cdww=z9ZXIqZiIhah1sX4^efsYa1&bT}@LFdm##^{v! zGvabZw0AaN7M@x@#+i!o7bz9!75soXIsnWDR6iq*gq!mfI3)r%MJ)v2L&0Q6bYirh zPvby%KJTPsqWSPTPEQxUqfv&MZJmC?FA&s+CD9QyItSJk!qn;(vUwWF@B`Qepsb_W{%K^j^Wwvy_s8EG|5-HG82&tZ;**76Eqr=qH9E#$uk1aRYdj9+{p8tyJ^ru8 z|N8u!t0&$vKXvCCdp-xGo4>mGoBq|vr~#=f*LZBBT4>t3)_5@2c<@={Vf<>@L9>SS zmK|#?M{+Gkp0#xREoKmx-@ClB_u{|bu{Qd>-01gKzJGJ&;@c~4$5(d0^Q=)?-@fa= z{_U3CUkl}RduY0WS;b20@b9GG#a8x=K5M-6`S#uaZKDbw|EQDfeKdGt=oR7juhb8h zRs6nf_t0w*I0ku9!{Hb7MUI zgRGP?zHEu3#SofG>#5Z=KG($K$#fzf*A5di(7?lv8rP1~QzzXbbfef8jf@o<<15k! z_|g#4qP4f_M*SyWg$1TkMq8xkSK>@ZQxb7#oBY^3Js$s9;9AQe%0m+(b|VR@E4clc z@MU>LRqZ$RZB^}Goi8h|YWu3Ty6WgxE%(c+4u5sOVz3;4E>+IZuZtBp(&6w|x5{Vn z=bNh)TdTCPjidXjc0R6r9D8!;@%4|JzY*~B%h%3U?Rh-^B>Qpt4+0*sJy6vijs+nQ zZqn`$9#nk3h9<7iGV_(Ubo7;K?5jWuGOI9&35`{l$%-0EUk62VjLmg%eT}!q>)JoSPq4k9pT=#YqO7d!%j)v7%CBpMvgZFF;QxmIB=mhI^gXX@S*zRo c*L8bWTKiV(PW-U?50!%rWqZC7=*}SjFZA)DU;qFB literal 0 HcmV?d00001 diff --git a/browser/__pycache__/playwright_extensions.cpython-310.pyc b/browser/__pycache__/playwright_extensions.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6ee01ac301b90ce5dbdd1569a539699e7dc9a21 GIT binary patch literal 3882 zcmb_f-EQ2*73Pp!?uwFD8`XB5!bypyNW7`FaC%V`iW|g9tk`g?8kY0ZtqOwLncbzt zCATv}DI(;efK?>9XkQ>LAOpVfO`pJby6H0zz3WxqpvXng?~wavYoVk-3G8q<9G*Ep z-*?X0WO>;&aQ&x!umAJ!8OFcpVfM1{@GgGpHU@6A4bHe3u{Oiqip+u4wgz_F9#q;D zM(^y%8C2U)c=eIlcDcD_G;8Tc5a-^K2O`u$rg2ZiUib8w z^f)H>qTujAhP}T2RS*X~A-y$kv;p9+Xe5OULu&O#F zZ1!^S@GgF8h(Q=|gV8oQYg^pp*0IsHd4pF(ML41=YF+bX<7F5x7P>aC^4f6)<~R5P z-Y-tpUA~C*CG47f(>3`LUp}_m%lsu?=PQ`ixed3i<`<^Exi!%;{S%aNJzO&mReMpO zlsLz$t>TmSLZQX^p1tNJ{aAQB6yEJ19qN(fEcNz&e5$Al*1i{|uuA3t>o z!x1|&e1HTaK!g#VgH=}cWuXZfa3>CRh=8h4NL`$G)})p@*?v>@)cWL!1k8kSP=aUO zyA?wFJF(J1oB?3!EWjorHaCEr)w4qKOWi4K{NV&Q`DGFJgSaEOue1mv-S_mb$ z&D>TZ>dGoCe`?4@+`hi~{`&SuN=UWc7crOH9|*Olli{`&O0NxqFy7vg$pJQO&!{kk zV`~U3`+f)~`99ccD8i#tb(qVtSLQ*ZHQv3MJDnu%hP~X8LZ>o*E>t$eP#!2k^><9N zx3zI(?1JTwOu0<6U6Y%sAunp{#OBsx#*9x40;R4L`zoRF$a-iVG5Ok&dEy)~+M%oE zmq#Xd{!9U{;kkCQ@Yq1Ez&qcCv_)PUNvm!dwxO2}KW4^5iy7Gp-Y)ZnBkL>UZ)UmY zHwHH!T7R(Uq0MK<4d11dq5YmYlWf_p#n<4+&R;Qop>|8XJE?f7; z`9pYj3n!*BpsX<^*IBt!klru+{Q^#Rk|bL99>`R9J0b*eyGhi$=4}KKI7=%Vq8|x4 zF)6P?7Y!(EW}m)SV#FJyN+BfzPTK1ML4hPD9|(ygdJRJM6UwJ@j?rE)@ld(l+Y91& zBd?D^TpHdqWdnYdFJX9)T^&;q8T(wB*)K}v$&SG!81O!E@BC;8weB_Rd9@#?KxgdgIBRvUBzI!mra*BtZvzs z%S_8*CaZnpI*#R`lsK5*$~u;(M{+nBQ~0q0xkuVBxPB70Ykou zqz{<+Gm!^B3go;(n<&9g5hH(yN9vgrA+q7R%w|hw_Qu?BpOxHU_-cI7d=kwcrWt?Z2Y4q6nXuj}64Jl75A)lTdyb?x3K8grI&Zq6o7s*psS)l`}ydsnZj%yDYR&9FY(^x5HG&;o$<*~Sff<>V2LiEHfx6x6d)unE03|{od zY9Mq!DdI#KelVYg1~$jGa`d*dM{pr);z7Oyk;KREX<1KU0%b z)|&5QM%{aW)-HGUknQO%g-Rp+)NI!i{4og8^AilDp20wor|XjhUA(3)T@gj=tYJ6M zCD~?nXD%B0NwSB=3MFGW{Ub2yAX_)0q=UlzPV0H;&1zYniX8C+oRU{crLJIJBmhZp z9A^ot3uS_MgJy-kSI#?^W1CvV;MkNtYF6EHmw;%t#Im>kXDj4ukOW;;aNBIIP$L-! z1L6C*>-&R*rxDGUeE;`p5RKkceV->C-qzaU!=scS-2TZE9pB8Jt(TQ4;8> z-I5B=Oj<0MN>)qiT1ld!l3O!iTE$zu{N|-beW`fYT$C-m$*WZJsn$FCaENh(E8U9vU{VhULFeb8{*qeX>0agsb*f_?)Hn_1(4At7jiJhp6^jX??y{mq^ z3&HJy$#gpA)aij~jvR7;nI5=w${~kje92K;)LC^Won)rDNrP- z?Dmp(XcoV#V?~2rE$80Z&4abZ)_YL7gO(7G2oaya5WnCftl;lNBp~b+0!zWA&{B9Q za@rSwne;CNm$=ic5b8wlu(EwmX! z2L~$Qp6cOa>ue=_uzIv_o&8I=cMC?*tR2j$%Cc0jg9PgaQMNIR3Z=s^7A}7Wb)!X( zMe``*F^F#DCV{Cd_t9F9_$&!M6cQPwvBU8An2E+I>w`bnn2`3U6{p{$r&J6Z=SFmSu>xpvOIG`;^SUneq9l_J&dRL0scpNiR` zqNs_wP8`T_?qCOHwE$+$4&4xm0tjY@3RtIe?4VJ~i*P5ZaD?iDoe)bU`DRvEvo)fT z80}^yWm&Z&7vIfc$9&oWOo&QSETI}g_S1|$)n>3pwVI+Yp+avTcA*duu?CS(!O&M) zqS=48xEl`AK7$OfZhv>AbQHZsJw{#`DREVgJT(0P6N0MU^z1mGN~f zZ^Lqmf)1A7g~}a@$x!Z09zKFJw8lIKp*`fzgm&%##=8kY3xI^J>_jVMZy@g>1P1?o zE~Dxm#MCsmuL%~m;Y!o1o4`fm6>Na&WJE*D6&2|^77hMrf4aZ#WV%1~KLW}Rr+!48 zB|sGg{8GIE7L%!fqe+gI5o(tSMPhp&coCq1*(R+3=x~&(7shKogQH4>^2R>WQcs@T zKWV!a4a0f;j-`z=D?G-sTF z^)py%F$kg!j)PL9d&6St#MIHs39V;y^g8uKUrUY3*1sSdrJVM^2)fvh=9vr383sIN zpJx$PfYNNMw|O((bY^&xP(K7AZ^qQqX0(zTIL)JLW3N^c(C}PgO^fq0UX;+-r;Z~$ z!~uuUeP%WlY|`018t81@1)_4`x^vXiNCj+Ggc#EfuH0}-f;B_d?U)K5DkS&@K^2_n zu69!jgEMsNmg^41JU_^pV^6hI_M)n;K$r(!SgG+Tq87rYM`SezKJI9G`&K15NP8Qy z?{A^MwjaDZS8hLj?`LNF>GjEP!{Ecbf5_?_gx|vp*2q=(RRJv$$IZlXD=}y$1|PE4 z&}DPzvNd$o9J*RbEHsUen2C`}V)XO2j`i0ayhw#y_tLRrFTk00zNsO|oLP^38;nFcHigl zxxfYH9(zxCaPZ#3{n*!tw!ceu1qR&<&|_13$ayI25RG1F)DqNhmY{Ur>O9kVy4_v- zWRh-jp_)FI*uL>#J3QhtV3bV50JX{*#fVKm!!VD7K8D>2 tBPRYCI&7lDe?#X#k0h)}=bs~;<*rkYA}`+xe#a7q8T<-91v2h4{ta7LE*by; literal 0 HcmV?d00001 diff --git a/browser/__pycache__/playwright_manager.cpython-310.pyc b/browser/__pycache__/playwright_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05786d24347e3860fda3beb65dfaf45f1d8c1c16 GIT binary patch literal 15864 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�CNpoA!RtyvH=fbIi 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
s`{xV_r5$16v%O)?)l^MJq7lY%G#UzZf;AJ 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 { + // Originalmethoden speichern + const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData; + const originalToDataURL = HTMLCanvasElement.prototype.toDataURL; + const originalToBlob = HTMLCanvasElement.prototype.toBlob; + + // Funktion zum Hinzufügen von Rauschen zu Bilddaten + const addNoise = (data, noise) => { + const noiseFactor = noise || 0.03; // Standardwert für das Rauschlevel + + // Nur auf Canvas über Mindestgröße anwenden, um normale Canvas nicht zu stören + if (data.width > 16 && data.height > 16) { + const pixelCount = data.width * data.height * 4; // RGBA-Kanäle + + // Präzise, aber wenig wahrnehmbare Änderungen + const minPixels = Math.max(10, Math.floor(pixelCount * 0.005)); // Mindestens 10 Pixel + const maxPixels = Math.min(100, Math.floor(pixelCount * 0.01)); // Höchstens 100 Pixel + + // Zufällige Anzahl an Pixeln auswählen + const pixelsToModify = Math.floor(Math.random() * (maxPixels - minPixels)) + minPixels; + + // Pixel modifizieren + for (let i = 0; i < pixelsToModify; i++) { + // Zufälligen Pixel auswählen + const offset = Math.floor(Math.random() * pixelCount / 4) * 4; + + // Nur RGB modifizieren, Alpha-Kanal unverändert lassen + for (let j = 0; j < 3; j++) { + // Subtile Änderungen hinzufügen (±1-2) + const mod = Math.floor(Math.random() * 3) - 1; + data.data[offset + j] = Math.max(0, Math.min(255, data.data[offset + j] + mod)); + } + } + } + + return data; + }; + + // getImageData überschreiben + CanvasRenderingContext2D.prototype.getImageData = function() { + const imageData = originalGetImageData.apply(this, arguments); + return addNoise(imageData, NOISE_LEVEL); + }; + + // toDataURL überschreiben + HTMLCanvasElement.prototype.toDataURL = function() { + // Temporäre Modifikation für getImageData + const tempGetImageData = CanvasRenderingContext2D.prototype.getImageData; + CanvasRenderingContext2D.prototype.getImageData = originalGetImageData; + + // toDataURL aufrufen + let dataURL = originalToDataURL.apply(this, arguments); + + // Wenn das Canvas groß genug ist und nicht von kritischen Anwendungen verwendet wird + if (this.width > 16 && this.height > 16 && !this.hasAttribute('data-fingerprint-protect-ignore')) { + // Rauschen in binären Teil des DataURL einfügen + const parts = dataURL.split(','); + if (parts.length === 2) { + // Binäre Daten dekodieren + const binary = atob(parts[1]); + // Geringfügige Änderungen bei etwa 0,1% der Bytes + let modifiedBinary = ''; + for (let i = 0; i < binary.length; i++) { + if (Math.random() < 0.001 * NOISE_LEVEL) { + // Byte leicht ändern + const byte = binary.charCodeAt(i); + const mod = Math.floor(Math.random() * 3) - 1; + modifiedBinary += String.fromCharCode(Math.max(0, Math.min(255, byte + mod))); + } else { + modifiedBinary += binary[i]; + } + } + // Binäre Daten wieder kodieren + dataURL = parts[0] + ',' + btoa(modifiedBinary); + } + } + + // getImageData wiederherstellen + CanvasRenderingContext2D.prototype.getImageData = tempGetImageData; + + return dataURL; + }; + + // toBlob überschreiben + HTMLCanvasElement.prototype.toBlob = function(callback) { + // Original toBlob aufrufen + originalToBlob.apply(this, [function(blob) { + // Wenn das Canvas groß genug ist und nicht von kritischen Anwendungen verwendet wird + if (this.width > 16 && this.height > 16 && !this.hasAttribute('data-fingerprint-protect-ignore')) { + // Blob zu ArrayBuffer konvertieren + const reader = new FileReader(); + reader.onload = function() { + const arrayBuffer = reader.result; + const array = new Uint8Array(arrayBuffer); + + // Geringfügige Änderungen bei etwa 0,1% der Bytes + for (let i = 0; i < array.length; i++) { + if (Math.random() < 0.001 * NOISE_LEVEL) { + // Byte leicht ändern + const mod = Math.floor(Math.random() * 3) - 1; + array[i] = Math.max(0, Math.min(255, array[i] + mod)); + } + } + + // Neuen Blob erstellen + const modifiedBlob = new Blob([array], {type: blob.type}); + callback(modifiedBlob); + }; + reader.readAsArrayBuffer(blob); + } else { + // Unveränderten Blob zurückgeben + callback(blob); + } + }.bind(this)]); + }; + } + """.replace("NOISE_LEVEL", str(self.noise_level)) + + self.scripts.append(script) + + def _init_webgl_protection(self): + """ + Initialisiert den Schutz gegen WebGL-Fingerprinting. + Dies modifiziert WebGL-spezifische Werte, die für Fingerprinting verwendet werden. + """ + webgl_vendor = self.defaults["webgl_vendor"] + webgl_renderer = self.defaults["webgl_renderer"] + + script = f""" + () => {{ + // WebGL Vendor und Renderer spoofen + const getParameterProxies = [ + WebGLRenderingContext.prototype, + WebGL2RenderingContext.prototype + ]; + + getParameterProxies.forEach(contextPrototype => {{ + if (!contextPrototype) return; + + const originalGetParameter = contextPrototype.getParameter; + contextPrototype.getParameter = function(parameter) {{ + // WebGL Vendor (VENDOR) + if (parameter === 0x1F00) {{ + return "{webgl_vendor}"; + }} + + // WebGL Renderer (RENDERER) + if (parameter === 0x1F01) {{ + return "{webgl_renderer}"; + }} + + // Unshaded Language Version (SHADING_LANGUAGE_VERSION) + if (parameter === 0x8B8C) {{ + const originalValue = originalGetParameter.call(this, parameter); + + // Subtile Änderungen an der Version + const versionMatch = originalValue.match(/^WebGL GLSL ES ([0-9]\\.[0-9][0-9])/); + if (versionMatch) {{ + return originalValue.replace(versionMatch[1], + (parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(2)); + }} + }} + + // VERSION + if (parameter === 0x1F02) {{ + const originalValue = originalGetParameter.call(this, parameter); + + // Subtile Änderungen an der Version + const versionMatch = originalValue.match(/^WebGL ([0-9]\\.[0-9])/); + if (versionMatch) {{ + return originalValue.replace(versionMatch[1], + (parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(1)); + }} + }} + + return originalGetParameter.apply(this, arguments); + }}; + }}); + + // WebGL Vertex und Fragment Shader spoofen + const shaderSourceProxies = [ + WebGLRenderingContext.prototype, + WebGL2RenderingContext.prototype + ]; + + shaderSourceProxies.forEach(contextPrototype => {{ + if (!contextPrototype) return; + + const originalShaderSource = contextPrototype.shaderSource; + contextPrototype.shaderSource = function(shader, source) {{ + // Füge geringfügige Unterschiede in Kommentaren ein, ohne die Funktionalität zu beeinträchtigen + if (source.indexOf('//') !== -1) {{ + // Zufälligen Kommentar leicht modifizieren + source = source.replace(/\\/\\/(.*?)\\n/g, (match, comment) => {{ + if (Math.random() < 0.1) {{ + // Füge ein Leerzeichen oder einen Bindestrich hinzu oder entferne eines + const modifications = [ + ' ', '-', '', ' ' + ]; + const mod = modifications[Math.floor(Math.random() * modifications.length)]; + return `//` + comment + mod + `\\n`; + }} + return match; + }}); + }} + + // Ersetze bestimmte Whitespace-Muster + source = source.replace(/\\s{2,}/g, match => {{ + if (Math.random() < 0.05) {{ + return ' '.repeat(match.length + (Math.random() < 0.5 ? 1 : -1)); + }} + return match; + }}); + + return originalShaderSource.call(this, shader, source); + }}; + }}); + + // Canvas-Kontext spoofen + const getContextProxies = [ + HTMLCanvasElement.prototype + ]; + + getContextProxies.forEach(canvasPrototype => {{ + const originalGetContext = canvasPrototype.getContext; + canvasPrototype.getContext = function(contextType, contextAttributes) {{ + const context = originalGetContext.apply(this, arguments); + + if (context && (contextType === 'webgl' || contextType === 'experimental-webgl' || contextType === 'webgl2')) {{ + // Zufällige Werte für verschiedene Parameter einführen + if ({str(self.defaults["webgl_noise"]).lower()}) {{ + // Zufällige Modifikation für MAX_VERTEX_UNIFORM_VECTORS + const MAX_VERTEX_UNIFORM_VECTORS = context.getParameter(context.MAX_VERTEX_UNIFORM_VECTORS); + Object.defineProperty(context, 'MAX_VERTEX_UNIFORM_VECTORS', {{ + get: () => MAX_VERTEX_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1 + }}); + + // Zufällige Modifikation für MAX_FRAGMENT_UNIFORM_VECTORS + const MAX_FRAGMENT_UNIFORM_VECTORS = context.getParameter(context.MAX_FRAGMENT_UNIFORM_VECTORS); + Object.defineProperty(context, 'MAX_FRAGMENT_UNIFORM_VECTORS', {{ + get: () => MAX_FRAGMENT_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1 + }}); + }} + }} + + return context; + }}; + }}); + }} + """ + + self.scripts.append(script) + + def _init_audio_protection(self): + """ + Initialisiert den Schutz gegen Audio-Fingerprinting. + Dies modifiziert die Audio-API-Funktionen, die für Fingerprinting verwendet werden. + """ + script = f""" + () => {{ + // Audio-Kontext spoofen + if (window.AudioContext || window.webkitAudioContext) {{ + const AudioContextProxy = window.AudioContext || window.webkitAudioContext; + const originalAudioContext = AudioContextProxy; + + // AudioContext überschreiben + window.AudioContext = window.webkitAudioContext = function() {{ + const context = new originalAudioContext(); + + // createOscillator überschreiben + const originalCreateOscillator = context.createOscillator; + context.createOscillator = function() {{ + const oscillator = originalCreateOscillator.apply(this, arguments); + + // Frequenz leicht modifizieren + const originalFrequency = oscillator.frequency; + Object.defineProperty(oscillator, 'frequency', {{ + get: function() {{ + return originalFrequency; + }}, + set: function(value) {{ + if (typeof value === 'number') {{ + // Leichte Änderung hinzufügen + const noise = (Math.random() * 0.02 - 0.01) * value; + originalFrequency.value = value + noise; + }} else {{ + originalFrequency.value = value; + }} + }} + }}); + + return oscillator; + }}; + + // getChannelData überschreiben + const originalGetChannelData = context.createBuffer.prototype?.getChannelData || OfflineAudioContext.prototype?.getChannelData; + if (originalGetChannelData) {{ + context.createBuffer.prototype.getChannelData = function(channel) {{ + const array = originalGetChannelData.call(this, channel); + + if ({str(self.defaults["audio_noise"]).lower()} && this.length > 20) {{ + // Sehr subtiles Rauschen hinzufügen (bei etwa 0,1% der Samples) + const noise = {self.noise_level} * 0.0001; + + // Effiziente Implementierung + const samples = Math.min(200, Math.floor(array.length * 0.001)); + for (let i = 0; i < samples; i++) {{ + const idx = Math.floor(Math.random() * array.length); + array[idx] += (Math.random() * 2 - 1) * noise; + }} + }} + + return array; + }}; + }} + + // AnalyserNode.getFloatFrequencyData überschreiben + if (context.createAnalyser) {{ + const originalCreateAnalyser = context.createAnalyser; + context.createAnalyser = function() {{ + const analyser = originalCreateAnalyser.apply(this, arguments); + const originalGetFloatFrequencyData = analyser.getFloatFrequencyData; + + analyser.getFloatFrequencyData = function(array) {{ + originalGetFloatFrequencyData.call(this, array); + + if ({str(self.defaults["audio_noise"]).lower()} && array.length > 20) {{ + // Sehr subtiles Rauschen hinzufügen + const noise = {self.noise_level} * 0.001; + + // Effiziente Implementierung für große Arrays + const samples = Math.min(20, Math.floor(array.length * 0.01)); + for (let i = 0; i < samples; i++) {{ + const idx = Math.floor(Math.random() * array.length); + array[idx] += (Math.random() * 2 - 1) * noise; + }} + }} + + return array; + }}; + + return analyser; + }}; + }} + + return context; + }}; + }} + }} + """ + + self.scripts.append(script) + + def _init_navigator_protection(self): + """ + Initialisiert den Schutz gegen Navigator-Objekt-Fingerprinting. + Dies modifiziert verschiedene Navigator-Eigenschaften, die für Fingerprinting verwendet werden. + """ + hardware_concurrency = self.defaults["hardware_concurrency"] + device_memory = self.defaults["device_memory"] + + script = f""" + () => {{ + // Navigator-Eigenschaften überschreiben + + // hardwareConcurrency (CPU-Kerne) + if (navigator.hardwareConcurrency) {{ + Object.defineProperty(navigator, 'hardwareConcurrency', {{ + get: () => {hardware_concurrency} + }}); + }} + + // deviceMemory (RAM) + if (navigator.deviceMemory) {{ + Object.defineProperty(navigator, 'deviceMemory', {{ + get: () => {device_memory} + }}); + }} + + // language und languages + if (navigator.language) {{ + const originalLanguage = navigator.language; + const originalLanguages = navigator.languages; + + Object.defineProperty(navigator, 'language', {{ + get: () => originalLanguage + }}); + + Object.defineProperty(navigator, 'languages', {{ + get: () => originalLanguages + }}); + }} + + // userAgent-Konsistenz sicherstellen + if (navigator.userAgent) {{ + const userAgent = navigator.userAgent; + + // Wenn der userAgent bereits überschrieben wurde, stellen wir + // sicher, dass die appVersion und platform konsistent sind + const browserInfo = {{ + chrome: /Chrome\\/(\\d+)/.exec(userAgent), + firefox: /Firefox\\/(\\d+)/.exec(userAgent), + safari: /Safari\\/(\\d+)/.exec(userAgent), + edge: /Edg(e|)\\/(\\d+)/.exec(userAgent) + }}; + + // Platform basierend auf userAgent bestimmen + let platform = '{self.defaults.get("platform", "Win32")}'; + if (/Windows/.test(userAgent)) platform = 'Win32'; + else if (/Macintosh/.test(userAgent)) platform = 'MacIntel'; + else if (/Linux/.test(userAgent)) platform = 'Linux x86_64'; + else if (/Android/.test(userAgent)) platform = 'Linux armv8l'; + else if (/iPhone|iPad/.test(userAgent)) platform = 'iPhone'; + + Object.defineProperty(navigator, 'platform', {{ + get: () => platform + }}); + + // appVersion konsistent machen + if (navigator.appVersion) {{ + Object.defineProperty(navigator, 'appVersion', {{ + get: () => userAgent.substring(8) + }}); + }} + + // vendor basierend auf Browser setzen + let vendor = '{self.defaults.get("vendor", "Google Inc.")}'; + if (browserInfo.safari) vendor = 'Apple Computer, Inc.'; + else if (browserInfo.firefox) vendor = ''; + + Object.defineProperty(navigator, 'vendor', {{ + get: () => vendor + }}); + }} + + """ + self.scripts.append(script) + + def _init_misc_protections(self): + """ + Initialisiert verschiedene weitere Schutzmaßnahmen gegen Fingerprinting. + """ + timezone_id = self.defaults["timezone_id"] + + script = f""" + () => {{ + // Date.prototype.getTimezoneOffset überschreiben + const originalGetTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = function() {{ + // Zeitzonendaten für '{timezone_id}' + // Mitteleuropäische Zeit (CET/CEST): UTC+1 / UTC+2 (Sommerzeit) + const date = new Date(this); + + // Prüfen, ob Sommerzeit + const jan = new Date(date.getFullYear(), 0, 1); + const jul = new Date(date.getFullYear(), 6, 1); + const standardOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()); + + // Sommerzeit in Europa: Ende März bis Ende Oktober + const isDST = date.getMonth() > 2 && date.getMonth() < 10; + + // Offset in Minuten: CET = -60, CEST = -120 + return isDST ? -120 : -60; + }}; + + // Plugins und MimeTypes Schutz + if (navigator.plugins) {{ + // Leere oder gefälschte Plugins + Object.defineProperty(navigator, 'plugins', {{ + get: () => {{ + // Plugins-Array-Eigenschaften simulieren + const plugins = {{ + length: 0, + item: () => null, + namedItem: () => null, + refresh: () => {{}}, + [Symbol.iterator]: function* () {{}} + }}; + + return plugins; + }} + }}); + + // MimeTypes ebenfalls leeren + if (navigator.mimeTypes) {{ + Object.defineProperty(navigator, 'mimeTypes', {{ + get: () => {{ + // MimeTypes-Array-Eigenschaften simulieren + const mimeTypes = {{ + length: 0, + item: () => null, + namedItem: () => null, + [Symbol.iterator]: function* () {{}} + }}; + + return mimeTypes; + }} + }}); + }} + }} + + // Performance.now() und Date.now() - Schutz gegen Timing-Angriffe + if (window.performance && performance.now) {{ + const originalNow = performance.now; + + performance.now = function() {{ + const value = originalNow.call(performance); + // Subtile Abweichung hinzufügen + return value + (Math.random() * 0.01); + }}; + }} + + // Date.now() ebenfalls mit subtiler Abweichung + const originalDateNow = Date.now; + Date.now = function() {{ + const value = originalDateNow.call(Date); + // Subtile Abweichung hinzufügen (±1ms) + return value + (Math.random() < 0.5 ? 1 : 0); + }}; + + // screen-Eigenschaften konsistent machen + if (window.screen) {{ + const originalWidth = screen.width; + const originalHeight = screen.height; + const originalColorDepth = screen.colorDepth; + const originalPixelDepth = screen.pixelDepth; + + // Abweichungen verhindern - konsistente Werte liefern + Object.defineProperties(screen, {{ + 'width': {{ get: () => originalWidth }}, + 'height': {{ get: () => originalHeight }}, + 'availWidth': {{ get: () => originalWidth }}, + 'availHeight': {{ get: () => originalHeight - 40 }}, // Taskleiste simulieren + 'colorDepth': {{ get: () => originalColorDepth }}, + 'pixelDepth': {{ get: () => originalPixelDepth }} + }}); + }} + }} + """ + + self.scripts.append(script) + + def apply_to_context(self, context=None): + """ + Wendet alle Skripte auf den Browser-Kontext an. + + Args: + context: Der Browser-Kontext, falls er noch nicht gesetzt wurde + """ + if context: + self.context = context + + if not self.context: + logger.warning("Kein Browser-Kontext zum Anwenden der Fingerprint-Schutzmaßnahmen") + return + + for script in self.scripts: + self.context.add_init_script(script) + + logger.info(f"Fingerprint-Schutzmaßnahmen auf Browser-Kontext angewendet ({len(self.scripts)} Skripte)") + + def get_fingerprint_status(self) -> Dict[str, Any]: + """ + Gibt den aktuellen Status der Fingerprint-Schutzmaßnahmen zurück. + + Returns: + Dict[str, Any]: Status der Fingerprint-Schutzmaßnahmen + """ + status = { + "active": self.context is not None, + "script_count": len(self.scripts), + "protections": { + "canvas": self.defaults["canvas_noise"], + "webgl": self.defaults["webgl_noise"], + "audio": self.defaults["audio_noise"], + "navigator": True, + "battery": "getBattery" in self.scripts[-1], + "timing": "performance.now" in self.scripts[-1] + }, + "noise_level": self.noise_level, + "custom_values": { + "webgl_vendor": self.defaults["webgl_vendor"], + "webgl_renderer": self.defaults["webgl_renderer"], + "hardware_concurrency": self.defaults["hardware_concurrency"], + "device_memory": self.defaults["device_memory"], + "timezone_id": self.defaults["timezone_id"] + } + } + + return status + + def rotate_fingerprint(self, noise_level: Optional[float] = None): + """ + Rotiert den Fingerprint durch Neugenerierung der Schutzmaßnahmen. + + Args: + noise_level: Optionales neues Rauschniveau (0.0-1.0) + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if noise_level is not None: + self.noise_level = max(0.0, min(1.0, noise_level)) + + try: + # Skripte zurücksetzen + self.scripts = [] + + # Neues WebGL-Vendor/Renderer-Paar generieren + webgl_vendors = [ + "Google Inc.", + "Google Inc. (Intel)", + "Google Inc. (NVIDIA)", + "Google Inc. (AMD)", + "Intel Inc.", + "NVIDIA Corporation", + "AMD" + ] + + webgl_renderers = [ + "ANGLE (Intel, Intel(R) HD Graphics 620 Direct3D11 vs_5_0 ps_5_0)", + "ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0)", + "ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0)", + "Intel Iris OpenGL Engine", + "NVIDIA GeForce GTX 980 OpenGL Engine", + "AMD Radeon Pro 560 OpenGL Engine", + "Mesa DRI Intel(R) UHD Graphics 620 (KBL GT2)", + "Mesa DRI NVIDIA GeForce GTX 1650" + ] + + # Zufällige Werte wählen + self.defaults["webgl_vendor"] = random.choice(webgl_vendors) + self.defaults["webgl_renderer"] = random.choice(webgl_renderers) + + # Hardware-Concurrency und Device-Memory variieren + self.defaults["hardware_concurrency"] = random.choice([2, 4, 6, 8, 12, 16]) + self.defaults["device_memory"] = random.choice([2, 4, 8, 16]) + + # Schutzmaßnahmen neu initialisieren + self._init_protections() + + # Auf Kontext anwenden, falls vorhanden + if self.context: + self.apply_to_context() + + logger.info(f"Fingerprint erfolgreich rotiert (Noise-Level: {self.noise_level:.2f})") + return True + + except Exception as e: + logger.error(f"Fehler bei der Rotation des Fingerprints: {e}") + return False \ No newline at end of file diff --git a/browser/playwright_extensions.py b/browser/playwright_extensions.py new file mode 100644 index 0000000..8758d0d --- /dev/null +++ b/browser/playwright_extensions.py @@ -0,0 +1,127 @@ +# browser/playwright_extensions.py + +""" +Erweiterungen für den PlaywrightManager - Fügt zusätzliche Funktionalität hinzu +""" + +import logging +from typing import Dict, Any, Optional +from browser.fingerprint_protection import FingerprintProtection + +logger = logging.getLogger("playwright_extensions") + +class PlaywrightExtensions: + """ + Erweiterungsklasse für den PlaywrightManager. + Bietet zusätzliche Funktionalität, ohne die Hauptklasse zu verändern. + """ + + def __init__(self, playwright_manager): + """ + Initialisiert die Erweiterungsklasse. + + Args: + playwright_manager: Eine Instanz des PlaywrightManager + """ + self.playwright_manager = playwright_manager + self.fingerprint_protection = None + self.enhanced_stealth_enabled = False + + def enable_enhanced_fingerprint_protection(self, config: Optional[Dict[str, Any]] = None) -> bool: + """ + Aktiviert den erweiterten Fingerprint-Schutz. + + Args: + config: Optionale Konfiguration für den Fingerprint-Schutz + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Sicherstellen, dass der Browser gestartet wurde + if not hasattr(self.playwright_manager, 'context') or self.playwright_manager.context is None: + logger.warning("Browser muss zuerst gestartet werden, bevor der Fingerprint-Schutz aktiviert werden kann") + return False + + # Basis-Stealth-Konfiguration aus dem PlaywrightManager verwenden + stealth_config = getattr(self.playwright_manager, 'stealth_config', {}) + + # Mit der benutzerdefinierten Konfiguration erweitern, falls vorhanden + if config: + stealth_config.update(config) + + # Fingerprint-Schutz initialisieren + self.fingerprint_protection = FingerprintProtection( + context=self.playwright_manager.context, + stealth_config=stealth_config + ) + + # Schutzmaßnahmen auf den Kontext anwenden + self.fingerprint_protection.apply_to_context() + + # Status aktualisieren + self.enhanced_stealth_enabled = True + + logger.info("Erweiterter Fingerprint-Schutz aktiviert") + return True + + except Exception as e: + logger.error(f"Fehler beim Aktivieren des erweiterten Fingerprint-Schutzes: {e}") + return False + + def rotate_fingerprint(self, noise_level: Optional[float] = None) -> bool: + """ + Rotiert den Browser-Fingerprint. + + Args: + noise_level: Optionales neues Rauschniveau (0.0-1.0) + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self.enhanced_stealth_enabled or self.fingerprint_protection is None: + logger.warning("Erweiterter Fingerprint-Schutz ist nicht aktiviert") + return False + + return self.fingerprint_protection.rotate_fingerprint(noise_level) + + def get_fingerprint_status(self) -> Dict[str, Any]: + """ + Gibt den aktuellen Status des Fingerprint-Schutzes zurück. + + Returns: + Dict[str, Any]: Status des Fingerprint-Schutzes + """ + if not self.enhanced_stealth_enabled or self.fingerprint_protection is None: + return {"active": False, "message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert"} + + return self.fingerprint_protection.get_fingerprint_status() + + def hook_into_playwright_manager(self) -> None: + """ + Hängt die Erweiterungsmethoden an den PlaywrightManager. + """ + if not self.playwright_manager: + logger.error("Kein PlaywrightManager zum Anhängen der Erweiterungen") + return + + # Originalstart-Methode speichern + original_start = self.playwright_manager.start + + # Die start-Methode überschreiben, um den Fingerprint-Schutz automatisch zu aktivieren + def enhanced_start(*args, **kwargs): + result = original_start(*args, **kwargs) + + # Wenn start erfolgreich war und erweiterter Schutz aktiviert ist, + # wenden wir den Fingerprint-Schutz auf den neuen Kontext an + if result and self.enhanced_stealth_enabled and self.fingerprint_protection: + self.fingerprint_protection.set_context(self.playwright_manager.context) + self.fingerprint_protection.apply_to_context() + + return result + + # Methoden dynamisch zum PlaywrightManager hinzufügen + self.playwright_manager.enable_enhanced_fingerprint_protection = self.enable_enhanced_fingerprint_protection + self.playwright_manager.rotate_fingerprint = self.rotate_fingerprint + self.playwright_manager.get_fingerprint_status = self.get_fingerprint_status + self.playwright_manager.start = enhanced_start diff --git a/browser/playwright_manager.py b/browser/playwright_manager.py new file mode 100644 index 0000000..3dbe4d6 --- /dev/null +++ b/browser/playwright_manager.py @@ -0,0 +1,517 @@ +""" +Playwright Manager - Hauptklasse für die Browser-Steuerung mit Anti-Bot-Erkennung +""" + +import os +import json +import logging +import random +import time +from pathlib import Path +from typing import Dict, Optional, List, Any, Tuple +from playwright.sync_api import sync_playwright, Browser, Page, BrowserContext, ElementHandle + +# Konfiguriere Logger +logger = logging.getLogger("playwright_manager") + +class PlaywrightManager: + """ + Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen. + """ + + def __init__(self, + headless: bool = False, + proxy: Optional[Dict[str, str]] = None, + browser_type: str = "chromium", + user_agent: Optional[str] = None, + screenshots_dir: str = "screenshots", + slowmo: int = 0): + """ + Initialisiert den PlaywrightManager. + + Args: + headless: Ob der Browser im Headless-Modus ausgeführt werden soll + proxy: Proxy-Konfiguration (z.B. {'server': 'http://myproxy.com:3128', 'username': 'user', 'password': 'pass'}) + browser_type: Welcher Browser-Typ verwendet werden soll ("chromium", "firefox", oder "webkit") + user_agent: Benutzerdefinierter User-Agent + screenshots_dir: Verzeichnis für Screenshots + slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging) + """ + self.headless = headless + self.proxy = proxy + self.browser_type = browser_type + self.user_agent = user_agent + self.screenshots_dir = screenshots_dir + self.slowmo = slowmo + + # Stelle sicher, dass das Screenshots-Verzeichnis existiert + os.makedirs(self.screenshots_dir, exist_ok=True) + + # Playwright-Instanzen + self.playwright = None + self.browser = None + self.context = None + self.page = None + + # Zähler für Wiederhholungsversuche + self.retry_counter = {} + + # Lade Stealth-Konfigurationen + self.stealth_config = self._load_stealth_config() + + def _load_stealth_config(self) -> Dict[str, Any]: + """Lädt die Stealth-Konfigurationen aus der Datei oder verwendet Standardwerte.""" + try: + config_dir = Path(__file__).parent.parent / "config" + stealth_config_path = config_dir / "stealth_config.json" + + if stealth_config_path.exists(): + with open(stealth_config_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + logger.warning(f"Konnte Stealth-Konfiguration nicht laden: {e}") + + # Verwende Standardwerte, wenn das Laden fehlschlägt + return { + "vendor": "Google Inc.", + "platform": "Win32", + "webdriver": False, + "accept_language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", + "timezone_id": "Europe/Berlin", + "fingerprint_noise": True, + "device_scale_factor": 1.0, + } + + def start(self) -> Page: + """ + Startet die Playwright-Sitzung und gibt die Browser-Seite zurück. + + Returns: + Page: Die Browser-Seite + """ + if self.page is not None: + return self.page + + try: + self.playwright = sync_playwright().start() + + # Wähle den Browser-Typ + if self.browser_type == "firefox": + browser_instance = self.playwright.firefox + elif self.browser_type == "webkit": + browser_instance = self.playwright.webkit + else: + browser_instance = self.playwright.chromium + + # Browser-Startoptionen + browser_args = [] + + if self.browser_type == "chromium": + # Chrome-spezifische Argumente für Anti-Bot-Erkennung + browser_args.extend([ + '--disable-blink-features=AutomationControlled', + '--disable-features=IsolateOrigins,site-per-process', + '--disable-site-isolation-trials', + ]) + + # Browser starten + self.browser = browser_instance.launch( + headless=self.headless, + args=browser_args, + slow_mo=self.slowmo + ) + + # Kontext-Optionen für Stealth-Modus + context_options = { + "viewport": {"width": 1920, "height": 1080}, + "device_scale_factor": self.stealth_config.get("device_scale_factor", 1.0), + "locale": "de-DE", + "timezone_id": self.stealth_config.get("timezone_id", "Europe/Berlin"), + "accept_downloads": True, + } + + # User-Agent setzen + if self.user_agent: + context_options["user_agent"] = self.user_agent + + # Proxy-Einstellungen, falls vorhanden + if self.proxy: + context_options["proxy"] = self.proxy + + # Browserkontext erstellen + self.context = self.browser.new_context(**context_options) + + # JavaScript-Fingerprinting-Schutz + self._apply_stealth_scripts() + + # Neue Seite erstellen + self.page = self.context.new_page() + + # Event-Listener für Konsolen-Logs + self.page.on("console", lambda msg: logger.debug(f"BROWSER CONSOLE: {msg.text}")) + + return self.page + + except Exception as e: + logger.error(f"Fehler beim Starten des Browsers: {e}") + self.close() + raise + + def _apply_stealth_scripts(self): + """Wendet JavaScript-Skripte an, um Browser-Fingerprinting zu umgehen.""" + # Diese Skripte überschreiben Eigenschaften, die für Bot-Erkennung verwendet werden + scripts = [ + # WebDriver-Eigenschaft überschreiben + """ + () => { + Object.defineProperty(navigator, 'webdriver', { + get: () => false, + }); + } + """, + + # Navigator-Eigenschaften überschreiben + f""" + () => {{ + const newProto = navigator.__proto__; + delete newProto.webdriver; + navigator.__proto__ = newProto; + + Object.defineProperty(navigator, 'platform', {{ + get: () => '{self.stealth_config.get("platform", "Win32")}' + }}); + + Object.defineProperty(navigator, 'languages', {{ + get: () => ['de-DE', 'de', 'en-US', 'en'] + }}); + + Object.defineProperty(navigator, 'vendor', {{ + get: () => '{self.stealth_config.get("vendor", "Google Inc.")}' + }}); + }} + """, + + # Chrome-Objekte hinzufügen, die in normalen Browsern vorhanden sind + """ + () => { + // Fügt chrome.runtime hinzu, falls nicht vorhanden + if (!window.chrome) { + window.chrome = {}; + } + if (!window.chrome.runtime) { + window.chrome.runtime = {}; + window.chrome.runtime.sendMessage = function() {}; + } + } + """, + + # Plugin-Fingerprinting + """ + () => { + const originalQuery = window.navigator.permissions.query; + window.navigator.permissions.query = (parameters) => ( + parameters.name === 'notifications' ? + Promise.resolve({ state: Notification.permission }) : + originalQuery(parameters) + ); + } + """ + ] + + # Wenn Fingerprint-Noise aktiviert ist, füge zufällige Variationen hinzu + if self.stealth_config.get("fingerprint_noise", True): + scripts.append(""" + () => { + // Canvas-Fingerprinting leicht verändern + const originalToDataURL = HTMLCanvasElement.prototype.toDataURL; + HTMLCanvasElement.prototype.toDataURL = function(type) { + const result = originalToDataURL.apply(this, arguments); + + if (this.width > 16 && this.height > 16) { + // Kleines Rauschen in Pixels einfügen + const context = this.getContext('2d'); + const imageData = context.getImageData(0, 0, 2, 2); + const pixelArray = imageData.data; + + // Ändere einen zufälligen Pixel leicht + const randomPixel = Math.floor(Math.random() * pixelArray.length / 4) * 4; + pixelArray[randomPixel] = (pixelArray[randomPixel] + Math.floor(Math.random() * 10)) % 256; + + context.putImageData(imageData, 0, 0); + } + + return result; + }; + } + """) + + # Skripte auf den Browser-Kontext anwenden + for script in scripts: + self.context.add_init_script(script) + + def navigate_to(self, url: str, wait_until: str = "networkidle", timeout: int = 30000) -> bool: + """ + Navigiert zu einer bestimmten URL und wartet, bis die Seite geladen ist. + + Args: + url: Die Ziel-URL + wait_until: Wann die Navigation als abgeschlossen gilt ("load", "domcontentloaded", "networkidle") + timeout: Timeout in Millisekunden + + Returns: + bool: True bei erfolgreicher Navigation, False sonst + """ + if self.page is None: + self.start() + + try: + logger.info(f"Navigiere zu: {url}") + self.page.goto(url, wait_until=wait_until, timeout=timeout) + return True + except Exception as e: + logger.error(f"Fehler bei der Navigation zu {url}: {e}") + self.take_screenshot(f"navigation_error_{int(time.time())}") + return False + + def wait_for_selector(self, selector: str, timeout: int = 30000) -> Optional[ElementHandle]: + """ + Wartet auf ein Element mit dem angegebenen Selektor. + + Args: + selector: CSS- oder XPath-Selektor + timeout: Timeout in Millisekunden + + Returns: + Optional[ElementHandle]: Das Element oder None, wenn nicht gefunden + """ + if self.page is None: + raise ValueError("Browser nicht gestartet. Rufe zuerst start() auf.") + + try: + element = self.page.wait_for_selector(selector, timeout=timeout) + return element + except Exception as e: + logger.warning(f"Element nicht gefunden: {selector} - {e}") + return None + + def fill_form_field(self, selector: str, value: str, timeout: int = 5000) -> bool: + """ + Füllt ein Formularfeld aus. + + Args: + selector: Selektor für das Feld + value: Einzugebender Wert + timeout: Timeout in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Auf Element warten + element = self.wait_for_selector(selector, timeout) + if not element: + return False + + # Element fokussieren + element.focus() + time.sleep(random.uniform(0.1, 0.3)) + + # Vorhandenen Text löschen (optional) + current_value = element.evaluate("el => el.value") + if current_value: + element.fill("") + time.sleep(random.uniform(0.1, 0.2)) + + # Text menschenähnlich eingeben + for char in value: + element.type(char, delay=random.uniform(20, 100)) + time.sleep(random.uniform(0.01, 0.05)) + + logger.info(f"Feld {selector} gefüllt mit: {value}") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen von {selector}: {e}") + key = f"fill_{selector}" + return self._retry_action(key, lambda: self.fill_form_field(selector, value, timeout)) + + def click_element(self, selector: str, force: bool = False, timeout: int = 5000) -> bool: + """ + Klickt auf ein Element. + + Args: + selector: Selektor für das Element + force: Force-Click verwenden + timeout: Timeout in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Auf Element warten + element = self.wait_for_selector(selector, timeout) + if not element: + return False + + # Scroll zum Element + self.page.evaluate("element => element.scrollIntoView({ behavior: 'smooth', block: 'center' })", element) + time.sleep(random.uniform(0.3, 0.7)) + + # Menschenähnliches Verhalten - leichte Verzögerung vor dem Klick + time.sleep(random.uniform(0.2, 0.5)) + + # Element klicken + element.click(force=force, delay=random.uniform(20, 100)) + + logger.info(f"Element geklickt: {selector}") + return True + + except Exception as e: + logger.error(f"Fehler beim Klicken auf {selector}: {e}") + key = f"click_{selector}" + return self._retry_action(key, lambda: self.click_element(selector, force, timeout)) + + def select_option(self, selector: str, value: str, timeout: int = 5000) -> bool: + """ + Wählt eine Option aus einem Dropdown-Menü. + + Args: + selector: Selektor für das Dropdown + value: Wert oder sichtbarer Text der Option + timeout: Timeout in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Auf Element warten + element = self.wait_for_selector(selector, timeout) + if not element: + return False + + # Option auswählen + self.page.select_option(selector, value=value) + + logger.info(f"Option '{value}' ausgewählt in {selector}") + return True + + except Exception as e: + logger.error(f"Fehler bei der Auswahl von '{value}' in {selector}: {e}") + key = f"select_{selector}" + return self._retry_action(key, lambda: self.select_option(selector, value, timeout)) + + def is_element_visible(self, selector: str, timeout: int = 5000) -> bool: + """ + Prüft, ob ein Element sichtbar ist. + + Args: + selector: Selektor für das Element + timeout: Timeout in Millisekunden + + Returns: + bool: True wenn sichtbar, False sonst + """ + try: + element = self.page.wait_for_selector(selector, timeout=timeout, state="visible") + return element is not None + except: + return False + + def take_screenshot(self, name: str = None) -> str: + """ + Erstellt einen Screenshot der aktuellen Seite. + + Args: + name: Name für den Screenshot (ohne Dateierweiterung) + + Returns: + str: Pfad zum erstellten Screenshot + """ + if self.page is None: + raise ValueError("Browser nicht gestartet. Rufe zuerst start() auf.") + + timestamp = int(time.time()) + filename = f"{name}_{timestamp}.png" if name else f"screenshot_{timestamp}.png" + path = os.path.join(self.screenshots_dir, filename) + + self.page.screenshot(path=path, full_page=True) + logger.info(f"Screenshot erstellt: {path}") + return path + + def _retry_action(self, key: str, action_func, max_retries: int = 3) -> bool: + """ + Wiederholt eine Aktion bei Fehler. + + Args: + key: Eindeutiger Schlüssel für die Aktion + action_func: Funktion, die ausgeführt werden soll + max_retries: Maximale Anzahl der Wiederholungen + + Returns: + bool: Ergebnis der Aktion + """ + if key not in self.retry_counter: + self.retry_counter[key] = 0 + + self.retry_counter[key] += 1 + + if self.retry_counter[key] <= max_retries: + logger.info(f"Wiederhole Aktion {key} (Versuch {self.retry_counter[key]} von {max_retries})") + time.sleep(random.uniform(0.5, 1.0)) + return action_func() + else: + logger.warning(f"Maximale Anzahl von Wiederholungen für {key} erreicht") + self.retry_counter[key] = 0 + return False + + def close(self): + """Schließt den Browser und gibt Ressourcen frei.""" + try: + if self.page: + self.page.close() + self.page = None + + if self.context: + self.context.close() + self.context = None + + if self.browser: + self.browser.close() + self.browser = None + + if self.playwright: + self.playwright.stop() + self.playwright = None + + logger.info("Browser-Sitzung geschlossen") + + except Exception as e: + logger.error(f"Fehler beim Schließen des Browsers: {e}") + + def __enter__(self): + """Kontext-Manager-Eintritt.""" + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Kontext-Manager-Austritt.""" + self.close() + + +# Beispielnutzung, wenn direkt ausgeführt +if __name__ == "__main__": + # Konfiguriere Logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Beispiel für einen Proxy (ohne Anmeldedaten) + proxy_config = { + "server": "http://example-proxy.com:8080" + } + + # Browser starten und zu einer Seite navigieren + with PlaywrightManager(headless=False) as manager: + manager.navigate_to("https://www.instagram.com") + time.sleep(5) # Kurze Pause zum Anzeigen der Seite \ No newline at end of file diff --git a/browser/stealth_config.py b/browser/stealth_config.py new file mode 100644 index 0000000..2e2dee4 --- /dev/null +++ b/browser/stealth_config.py @@ -0,0 +1,216 @@ +""" +Stealth-Konfiguration für Playwright - Anti-Bot-Erkennung +""" + +import json +import logging +import os +import random +import platform +from pathlib import Path +from typing import Dict, Any, List + +# Konfiguriere Logger +logger = logging.getLogger("stealth_config") + +class StealthConfig: + """ + Konfiguriert Anti-Bot-Erkennungs-Einstellungen für Playwright. + Generiert und verwaltet verschiedene Fingerprint-Einstellungen. + """ + + # Standardwerte für User-Agents + CHROME_DESKTOP_AGENTS = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" + ] + + MOBILE_AGENTS = [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/135.0.0.0 Mobile/15E148 Safari/604.1" + ] + + # Plattformen + PLATFORMS = { + "windows": "Win32", + "macos": "MacIntel", + "linux": "Linux x86_64", + "android": "Linux armv8l", + "ios": "iPhone" + } + + # Browser-Sprachen + LANGUAGES = [ + "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", + "de-DE,de;q=0.9,en;q=0.8", + "de;q=0.9,en-US;q=0.8,en;q=0.7", + "en-US,en;q=0.9,de;q=0.8" + ] + + # Zeitzone für Deutschland + TIMEZONE_ID = "Europe/Berlin" + + def __init__(self, config_dir: str = None): + """ + Initialisiert die Stealth-Konfiguration. + + Args: + config_dir: Verzeichnis für Konfigurationsdateien + """ + self.config_dir = config_dir or os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config") + os.makedirs(self.config_dir, exist_ok=True) + + self.config_path = os.path.join(self.config_dir, "stealth_config.json") + + # Lade benutzerdefinierte User-Agents, falls vorhanden + self.user_agents = self._load_user_agents() + + # Lade gespeicherte Konfiguration oder erstelle eine neue + self.config = self._load_or_create_config() + + def _load_user_agents(self) -> Dict[str, List[str]]: + """Lädt benutzerdefinierte User-Agents aus der Konfigurationsdatei.""" + user_agents_path = os.path.join(self.config_dir, "user_agents.json") + + if os.path.exists(user_agents_path): + try: + with open(user_agents_path, 'r', encoding='utf-8') as f: + agents = json.load(f) + + if isinstance(agents, dict) and "desktop" in agents and "mobile" in agents: + return agents + except Exception as e: + logger.warning(f"Fehler beim Laden von user_agents.json: {e}") + + # Standardwerte zurückgeben + return { + "desktop": self.CHROME_DESKTOP_AGENTS, + "mobile": self.MOBILE_AGENTS + } + + def _load_or_create_config(self) -> Dict[str, Any]: + """Lädt die Konfiguration oder erstellt eine neue, falls keine existiert.""" + if os.path.exists(self.config_path): + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + logger.info("Stealth-Konfiguration geladen") + return config + except Exception as e: + logger.warning(f"Konnte Stealth-Konfiguration nicht laden: {e}") + + # Erstelle eine neue Konfiguration + config = self.generate_config() + self.save_config(config) + return config + + def generate_config(self, device_type: str = "desktop") -> Dict[str, Any]: + """ + Generiert eine neue Stealth-Konfiguration. + + Args: + device_type: "desktop" oder "mobile" + + Returns: + Dict[str, Any]: Die generierte Konfiguration + """ + # Wähle Plattform und entsprechenden User-Agent + if device_type == "mobile": + platform_name = random.choice(["android", "ios"]) + user_agent = random.choice(self.user_agents["mobile"]) + else: + # Wähle eine Plattform, die zum System passt + system = platform.system().lower() + if system == "darwin": + platform_name = "macos" + elif system == "windows": + platform_name = "windows" + else: + platform_name = "linux" + + user_agent = random.choice(self.user_agents["desktop"]) + + platform_value = self.PLATFORMS.get(platform_name, "Win32") + + # Wähle weitere Konfigurationen + config = { + "user_agent": user_agent, + "platform": platform_value, + "vendor": "Google Inc." if "Chrome" in user_agent else "Apple Computer, Inc.", + "accept_language": random.choice(self.LANGUAGES), + "timezone_id": self.TIMEZONE_ID, + "device_scale_factor": random.choice([1.0, 1.25, 1.5, 2.0]) if random.random() < 0.3 else 1.0, + "color_depth": random.choice([24, 30, 48]), + "hardware_concurrency": random.choice([2, 4, 8, 12, 16]), + "device_memory": random.choice([2, 4, 8, 16]), + "webdriver": False, + "fingerprint_noise": True, + "device_type": device_type + } + + return config + + def save_config(self, config: Dict[str, Any]) -> None: + """ + Speichert die Konfiguration in einer Datei. + + Args: + config: Die zu speichernde Konfiguration + """ + try: + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2) + logger.info(f"Stealth-Konfiguration gespeichert in: {self.config_path}") + except Exception as e: + logger.error(f"Fehler beim Speichern der Stealth-Konfiguration: {e}") + + def get_config(self) -> Dict[str, Any]: + """Gibt die aktuelle Konfiguration zurück.""" + return self.config + + def rotate_config(self, device_type: str = None) -> Dict[str, Any]: + """ + Generiert eine neue Konfiguration und speichert sie. + + Args: + device_type: "desktop" oder "mobile", oder None für bestehenden Typ + + Returns: + Dict[str, Any]: Die neue Konfiguration + """ + if device_type is None: + device_type = self.config.get("device_type", "desktop") + + self.config = self.generate_config(device_type) + self.save_config(self.config) + return self.config + + def get_user_agent(self) -> str: + """Gibt den aktuellen User-Agent aus der Konfiguration zurück.""" + return self.config.get("user_agent", self.CHROME_DESKTOP_AGENTS[0]) + + +# Beispielnutzung, wenn direkt ausgeführt +if __name__ == "__main__": + # Konfiguriere Logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Beispiel für Konfigurationserstellung + stealth = StealthConfig() + + print("Aktuelle Konfiguration:") + print(json.dumps(stealth.get_config(), indent=2)) + + print("\nNeue Desktop-Konfiguration:") + desktop_config = stealth.rotate_config("desktop") + print(json.dumps(desktop_config, indent=2)) + + print("\nNeue Mobile-Konfiguration:") + mobile_config = stealth.rotate_config("mobile") + print(json.dumps(mobile_config, indent=2)) \ No newline at end of file diff --git a/config/.machine_id b/config/.machine_id new file mode 100644 index 0000000..b9be037 --- /dev/null +++ b/config/.machine_id @@ -0,0 +1 @@ +ae30d891-0b45-408e-8f47-75fada7cb094 \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/app_version.json b/config/app_version.json new file mode 100644 index 0000000..375a42e --- /dev/null +++ b/config/app_version.json @@ -0,0 +1,7 @@ +{ + "current_version": "1.0.0", + "last_check": "2025-06-22T17:43:13.744626", + "channel": "stable", + "auto_check": true, + "auto_download": false +} \ No newline at end of file diff --git a/config/browser_config.json b/config/browser_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/email_config.json b/config/email_config.json new file mode 100644 index 0000000..3aa5d81 --- /dev/null +++ b/config/email_config.json @@ -0,0 +1,6 @@ +{ + "imap_server": "imap.ionos.de", + "imap_port": 993, + "imap_user": "info@z5m7q9dk3ah2v1plx6ju.com", + "imap_pass": "cz&ie.O9$!:!tYY@" + } \ No newline at end of file diff --git a/config/facebook_config.json b/config/facebook_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/instagram_config.json b/config/instagram_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/license.json b/config/license.json new file mode 100644 index 0000000..fe8c4b9 --- /dev/null +++ b/config/license.json @@ -0,0 +1,10 @@ +{ + "key": "", + "activation_date": "", + "expiry_date": "", + "status": "inactive", + "status_text": "Keine Lizenz aktiviert", + "features": [], + "last_online_check": "", + "signature": "" +} \ No newline at end of file diff --git a/config/license_config.json b/config/license_config.json new file mode 100644 index 0000000..87abb36 --- /dev/null +++ b/config/license_config.json @@ -0,0 +1,9 @@ +{ + "key": "", + "status": "inactive", + "hardware_id": "", + "activation_date": null, + "expiry_date": null, + "features": [], + "last_check": null + } \ No newline at end of file diff --git a/config/proxy_config.json b/config/proxy_config.json new file mode 100644 index 0000000..7b8b830 --- /dev/null +++ b/config/proxy_config.json @@ -0,0 +1,15 @@ +{ + "ipv4": [ + "85.254.81.222:44444:14a38ed2efe94:04ed25fb1b" + ], + "ipv6": [ + "92.119.89.251:30015:14a4622431481:a488401704" + ], + "mobile": [ + "de1.4g.iproyal.com:7296:1rtSh0G:XswBCIqi1joy5dX" + ], + "mobile_api": { + "marsproxies": "9zKXWpMEA1", + "iproyal": "" + } + } \ No newline at end of file diff --git a/config/stealth_config.json b/config/stealth_config.json new file mode 100644 index 0000000..51e99dd --- /dev/null +++ b/config/stealth_config.json @@ -0,0 +1,14 @@ +{ + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "platform": "Win32", + "vendor": "Google Inc.", + "accept_language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", + "timezone_id": "Europe/Berlin", + "device_scale_factor": 1.0, + "color_depth": 24, + "hardware_concurrency": 8, + "device_memory": 8, + "webdriver": false, + "fingerprint_noise": true, + "device_type": "desktop" + } \ No newline at end of file diff --git a/config/theme.json b/config/theme.json new file mode 100644 index 0000000..016f40f --- /dev/null +++ b/config/theme.json @@ -0,0 +1,46 @@ +# Path: config/theme.json + +{ + "dark": { + "name": "Dark", + "palette": { + "Window": "#1E1E1E", + "WindowText": "#FFFFFF", + "Base": "#2D2D30", + "AlternateBase": "#252526", + "ToolTipBase": "#2D2D30", + "ToolTipText": "#FFFFFF", + "Text": "#FFFFFF", + "Button": "#0E639C", + "ButtonText": "#FFFFFF", + "BrightText": "#FF0000", + "Link": "#3794FF", + "Highlight": "#264F78", + "HighlightedText": "#FFFFFF" + }, + "icons": { + "path_suffix": "dark" + } + }, + "light": { + "name": "Light", + "palette": { + "Window": "#FFFFFF", + "WindowText": "#1E1E1E", + "Base": "#F5F5F5", + "AlternateBase": "#E5E5E5", + "ToolTipBase": "#F5F5F5", + "ToolTipText": "#1E1E1E", + "Text": "#1E1E1E", + "Button": "#0078D7", + "ButtonText": "#FFFFFF", + "BrightText": "#FF0000", + "Link": "#0066CC", + "Highlight": "#CCE8FF", + "HighlightedText": "#1E1E1E" + }, + "icons": { + "path_suffix": "light" + } + } +} diff --git a/config/tiktok_config.json b/config/tiktok_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/twitter_config.json b/config/twitter_config.json new file mode 100644 index 0000000..e69de29 diff --git a/config/update_config.json b/config/update_config.json new file mode 100644 index 0000000..c7fb841 --- /dev/null +++ b/config/update_config.json @@ -0,0 +1,9 @@ +{ + "last_check": "2025-04-01 12:00:00", + "check_interval": 86400, + "auto_check": true, + "auto_download": false, + "update_channel": "stable", + "download_path": "updates", + "downloaded_updates": [] + } \ No newline at end of file diff --git a/config/user_agents.json b/config/user_agents.json new file mode 100644 index 0000000..26d2967 --- /dev/null +++ b/config/user_agents.json @@ -0,0 +1,31 @@ +{ + "desktop": [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0" + ], + "mobile": [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/135.0.0.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/134.0.0.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/123.0 Mobile/15E148 Safari/605.1.15", + "Mozilla/5.0 (Linux; Android 14; SM-S9180) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 14; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 14; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A536B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/133.0.0.0 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A546B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/133.0.0.0 Mobile Safari/537.36" + ] + } \ No newline at end of file diff --git a/controllers/__pycache__/account_controller.cpython-310.pyc b/controllers/__pycache__/account_controller.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bdb12a61d068dc45d13c3122d3a6df3f2db1b57 GIT binary patch literal 4459 zcmai1-;dkI5#C)=6h-NN+K%lssF~E~T4_#+o2CdZLD1NDZi3opyVv+&;R1rzuI?C9 zq_SLDF`-Kf*g*2oqUduIKq`s?erezGP~^@3#lH5b|3Kjuaow4vq?3JaLV=^%-Pz^t z@SFLD>D-)0;Ck4+y7$#lLjHk`*~^B;&*09QAUJ6fPC1KdlbV0lWbkjrR>x{u6vyn? z?v$FPj?;8ZyA+o@Zqubi&|!r;Bii(SPI#HS_Xu}|d)I1KIol%jO0NMA@Ghi@l4%?Z z>9-$zEqxvd|GJRkQ%OcCdU2#7B1nkP!uH5`cw3J>Bi$#Lg^m#2@Z1%#F*&G?vZARJHp{*QEuDZ<&}GO)8$LN%4_#%vjW;2p9jt33w#l*tDg~@G;46` zWxX^$&cVTZufX#Tn&0h*7Q=0hFVyk2;Le^0p~zh_q$5JffDTB&I00#KvPJIG&AP2i zS+6Ukc7q^FA{7MM<2Qp&n1Iuyu69Ey5*6HzL?3)>XCiLPDy({+>Pir_;xNmC;6C}! z`3q}1A7nyiJ9{GGa%Wv+w^Z8QQ6f{ToiIvv4m_Rh9QJ;rdq*M=7jfGT2#-3{qxtiP z+|!tB^qI%Lfb(%7b65Z*IGOc+1RYyK<;vjc!Phs1+!d+hiBJG9I9uW50yLrlkdif+ zQfG1w{Xgun)#7@&n}!tEh30S104V2AYMId=51GBE@cto(jYa?#Lk4`7sp9ia(03Gmg2 z7o|%AKosjzcP~wZuC#k`9H3FYv~pv0rL(%i{gwCDRxYotY}LyW5*d-tp*Vr!BnpfL z`8^QYYe@n5f(N0>UmCwDA9l13oypBGx#jPQtSh3{o{(zI*UpA)r}1vSIxCEJ+IOr+^*$YHBRzS0h4H4IEUN&q|vQi&+hRnzW-DoT^OZD3n?0b!}j7NKz*0Vqo@tp=L8`;qWb{yZAez8*B+657?GzBQt*JSle<$Z zo2b&AUsAFUFwA}B49naeSTmq@yo4xel&sz=A1>}w3~&xvYXl)aBH#m%x0qxD`Umzg(j@o?%;j+qpr1nh`$pHS@tU^aJ>RE= ziAh%`Q+aR4QKnjHr~A&EU~7`pS(3kg`0d4MIE7l)csov8VVu1K%4B}-Ule;t&NoHW z@h^p3B*p{dr810`!s0tEJq&>IUYLbSNrQBWo-oJJi^h!*<8UBD_PVn1NlSDQ-tq@v z1_?pRRL(?z+mexrV8hyi_?e9X?M_#rq6h&b1ZkWz_r#fBL;sus!Pp{Qq>kf#W7B2o zuz73&_7VLxYCl>mxxZl#^Hbm9!zZwhxL<(Vr1RT=2S6d|+8ofU0PzEcMFqD<_72;( z_5ruq5#+DDKY&;mP>6++f?mL_#i7{vo&EcZ`;Nji4rSGTc~BmfccG~I6Ien5FRhag zU)^`Hci>`z=N8ZeYTi_Ym&f#>?M)=V z>c)gT!;WSV|90Y&F97(a6;%e*g|t@CSP}x4<<*0Os(iU{(yUp2YU~fg)!>#xE7WFI zXyj*Rv&GcshjK=KZ2YJ;Bp2Y`)?N(NXOf@(w&}%A*1Y;skpz%YnhTIOw!+&2KLOA~ z@~6jICpb}Jjng;SGV5VtXVvtMBB=QXuPZy5`pr+|<18lM#5FWD`gB*iD_Zzzm`G$FH znR#^Havgih$P;FEAF{_U{A>9+n_*^RIR40aJYQZM=PJxr%a7(CL$>VmlqCvS{7`P4HwnaQ=8 z6-vsTPoS7nQ252;1)(kja%=Pu8M81f9%vw(O>@VZZ_0!JYiAytt$CXl>y@IYo99pm zDlq}T0gn}6GuShrGypW%E2=u}jma#CxGrbCRtr9GpM^2`KJ5J0aAz-rU<_D`O&H7r z26LRJIc>h+w8^Pvs=B8(>%xNdIs9bA5*$AngP@c09)v-lYeDdFFO0_{$&k~WeYwEVBSIC%%cf%wk(6d53>vhU1HN3b4$y% zcicO+xMoJ{HF*WbbQ#NK$h7i2w2BgY;p&}h>W#)VwH0#61Mgs+XEtUN*LETN7UA~TQT9J$hMV&M literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/account_controller.cpython-313.pyc b/controllers/__pycache__/account_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b09f4d06dcad30a9c88c14b889e3a68e59f7aa9c GIT binary patch literal 7269 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 literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/main_controller.cpython-310.pyc b/controllers/__pycache__/main_controller.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ef23721280989cf1b4878facca0336b50397a18 GIT binary patch literal 6398 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 literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/main_controller.cpython-313.pyc b/controllers/__pycache__/main_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d58bbbb6bd6fe6ae7523d6e8433bc94f8b77207c GIT binary patch literal 11618 zcmc&)U2Gdyb{_soLvlz_5~WC_OpYx(5^agL_2YH?BUzU1*rF}lqLnz2HY`PsD8>}2 zy)%pxq3xzyps;}e*%S!kb{AN05g-oG2lu5l(4a`;mp&DoNXDoG8x7E=-39ufwHzea z?L*JG^G8xtHbGHzB;L7m?mcJD%-s8(pL@4_J`V%O`TrbPda{pU{tYjT!>L!ceg&2H znIuD)q%CPr*oY0zj)Z;6K^#*~;^dL;Ot_|+NE5BQ63tUAVQIZ7;hypk53M&Ryi*+E zXq`>?ru@WD>+VF$RDc9%-IHjYY9np5?oG5$1xb+BxkSfQC+VbhU*fr`F49Ho{zUgw zh=gdpB@v$LAw4`ZZ<{%0Jcf1N!z2TvHtm{3x)``_4Kc~KQM*oy=IpWd@)@}DoKKfm zq)e$Oky4=`5PtFDCxp-D1wL8IU7Mmdaqs*L@}KcPM6+PxvLW^D}{U}E#*tam`(LuO6Q9&=Zo3WO}J{mP!y$f zj-;0-&3gkma4mo1TIog&tyRBy4Y4gLNK(F-6Kkj~XVOx7AuS4*(nU~%fJE*JDZTmD zOi4nm@%rp?I$t=SE@t6r@%q}5uw2!@4dT=>6&jIu_v2C%d@%D=x-1t?Kkdaos%|v+_{4W<^M?wJ8A*a$4#2nMAJ8vMP+9c&~NTnOG9@UXv zSs^}PR(%rURccuSID!LN4HR_DGRZhMR8MxnB;e#zeJhBCDWZWP!9l9}1jNJCk_Lzb zr=i*cu#JFca^uic{bdSzsf>nt#DzrX8+qX-!J(kGr8Ogw%9w~w+}O}A>R4UXq6SwA zX=$-UmaB*&K|CM`Bx(ytDGC`WCFXO*bU{>o^u8%wHMLc{1PLikFHv=hOJG~Z`sj3zoT#Ft=V$b8qfuEJI76zt{=p9Lk9+snr z?~f?M=jGw^YrzX3#0_n|Blpkz-Q~Zzytd~Ic8@;p9o^sp3Kx;NNNt6eIex3zxz~gJ z_)R~vuMcaxLCDZq8}xnm^6ksDET)j%GPirn?HU4Jh21T)yYC*5+4!>nb8!4SF&?xS z`(^gPW{7e76t+iZd)C>=W)r;%$!rLoT)&FQYy{QBws+mP-S7Hu`ya7gTkyh}jPCJZ zofQ)V)o=3+;>0gBQpr>6{bz<-7#mLy^ZRvO)xya?q#9CkrJ> zj8Df{!h=Y))tt&O@nFgdS+!Z8y=t&jOl@xtEu=FyQc@|UF~A%q!jH-@_kgt%XK_7G zi5S2qm2$ZPbPIrB4GMM2$2QUr+K4y`coKt>>_zP&b(6FXSLTps0gAU7b5tp8ugvx; zER5y@_s%E-XXSyj>+HFz_5qn4P}mWf9l3u^8NDKpURh_aR<%cEHmb0FGTV30sq~+e z`%gY%$H)Nc!y2!n#_>YTqvQ2gbiATg+A*V>b>kY(N!zT=&kWnEMxd?Hw>L}+I02@V z#$y?%4b1s*&?S8eOtyLR8Pka~Nlem?+9Vyr4ojaob9Bx$7=3$iJ~nW7lFp=y%4z~x zy2d<{Yi_LFjOi3wx-@<|L$9K}@qK4HED1*Kx*09e8ECqgMmxjIR=@v-&ok3)^)_n8 zH^JwMvE?H`e>qRGe7d{{0ZVyNC=T!tS;3sk7X|(b7)fd+pG>({#`P8NY%uWo5^2NoH>ME3O!bKocCIot!bDH$REmfC5NQq#a z0dpcl@)CrK1r1OfIYCkb)X1(FUP3t!CSP|6AjV5Al}a~6HBbu{<$TORQGlSMrM8>g zRVE#8$n!cv9KvqBbqLW2vKFDExCX`B%)hh#cfGg0mCnI?OZQ%_1dpz>FTm8_9s9`t zOaB^w2+ij%Ab`E~^0Jpl1elQfC)T_ZkGZyYFW$a*=lI<>@4j9+bfMBcwaz6TbI&Op z|2fBR1cD#9-*ayS_HMN8zT1B{R5>tJ2_&91JAM95594bGsnK2~@&h^Y10`}?jvRk* zMmcd+K5=y|GE-s0RRFX{X7}6$%_b;KVbO3_qC@NKa9yWI>|TmXHP+J!4~p+;>m4X| zvYv>xc%+A6E;P2UD5LH$#(&W^hj*dJ45DA%u^r=bcF0;dRu46vF4?rm(@>s=X#$oC zEQ>x-Ry(%QSIfuv>6nw?%urilw55zON=*uR0q6u1D=|AAPO3*UHO2I@p!<}Xg&9Es z(e2|$>#kd-KZQ=>Q7FJ2^tLG8UGTrry-VpnAa@^7x=+g8C)c{iZ%yIoNZd|TLI>}A z?)_n<>%=4O*}^cY_8Yw(HY(CpD;7Lry*d}@B8ari{GMyAC49QIjz zqxiScr@7Y9Sj>E@6H}v~eVcxNMD@b}r|vNIbIdv#nUY5O%`zj9wu5{MYXCpb0Cxj} zvm-ZTX6m@jnB7va5%&)7#*D}G9gNyc zk5RMEmVO{TV$qG-_mIvTJl45J`W}mJ)V_!Gy#|kUu95z{MK@~SL%K1A3|2VTNNHBdj|t>A9v{Q>=M))N;--i+3%(FnAt~pYEM6TUL5-(u}<08m#4vd~FJBrW!z` z)1!0|DVgefBUqzLwKWaJnC*QA#&UJLUT=gV(9;BSi^X`7z0Ak9X)w&Yp-J@>O6jbr z1UUgTf&-cQ?Qc_5RoN4T^zuSBJ*E!TLF+aTAbz4y0>42VgNnxU0Sd$M{LS0U9jDTD zQ0_XYbe)vDPCm#gV@Y`|x!!e6LsaNw9{-0=PcFe-jOT9=-drgW>oYWb1LQJrkSkF9 zCH%HO&A8k({;5+rH7lQ*UGI8H3oPi@HB&9{b*E#`sm)7i5nM<@ra+L|Vy0+_0Qu`I z!Caj7UE09wsc%gnPb(1?i6Aajg=hi&8LXf!kcG%k5~}+6b&4*NBvDpSOF)@Qis?5% z37r0rB!YUXwq514Ys|_($mhYc)ZlA(ouX|Y~ zeol^`Q{q?U_|m4?uVN`hC8v{6Gm9T-xj6_z|DeE zI~u_tI9|FN1`bCV>6IsSFlccvDiFFBH=^0G`SnAT2?&0(I=G zY?AhexPQ#!Ih~gzfu}4Gmt(dLoCeCK-E4p_FY61ui_*`h<<_vi~F!+ zPu&o#N%a3U^bk=V1W%nIrE|aBxnJo#B6l8H>pXhP`yVZ#jc`;6AC$ufmGCJ!d}=Lx z8sM#Cx6-js?$~#C`Ti?w9pkso0X+Et@}i$}Q8Uc$+h_~jIdZ4JvS*)gD{1vdNkl4#T&r^c{cmE_dIqT z@&*u6!DPl2r@F&hUKtj5N&1S82}jPS zofmD3%t=R-S+vFc<#v#Ch0apU#PQntz8F_M;6LdLOyySSqc!kv2n04t$d$H`$sEBUX=Hr zuf#3@Wy<#a#`A~o$K~fID*H|YVba#O(KmE|T<)8!JbwlVXMwcY?ChAdJ@YUf{0Eoc zyZqsekBYx6%E6Jx!6;sDwR^hTe(KunVuIa2PyFKYUtC^mKLBbd!G1Z|e=qzfIJ)Uz zf&-gjCJ_7PS%e96Z!tD_9zCB}2_Jtj@nE>pJ+5%4WbV}OHXYddTM^ele{y>G^ik%s zqn^pVj)(TH$*ALD)CKjL~LWK_Q>c!)?8z2eN{3WfYOdK*`ym}>fi`Kp#wKU4#15GfB-iThymRA4P|E# z2pcqnWq7A;i~SEh1o}P?^qJtcf1`8ce(KXX zdGu;!WCn05X?w*6r;b-_Up2d0+n)KE)*dC$`+1<(PFW_+p$lEAN=U=k38tF1SY@6<&EDk zQ2cJw1wwvH1C5_dbey&^|Jc}(57Pn+{=MEeb~$4Nu`!c*)j~|l**-2KPsmSdP_?x zwU~$HvH}?SVhNg?Q>knzlS&c5Gi{NX+{W4=EO14DOkjcG71@UcwUG2XccfD22j?>> zOv>aJ%3z>VsgD`zZfZ*k=!p<4=mwS(Q28_FUz{&Ib&k2ZpPsV2UE`abVb{KU$!83l zo*r|$#-6ryyI!+BJ>qa3cdW;QObd%ODnv~41QqYKR+nt4$MD5nXBlr?=BTzDA&`nsK znja*_b$^IPMX+WXKxQ|5u#0r9Rl^`UX#N~ptbqhsRBb?yk-vflur7FejccV_z}{pD+W){|ir;ktfWdFWdpe9s1l2@zR=m T&)ZF3JFh!z{-+F<^q&6%ggNmB literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/settings_controller.cpython-310.pyc b/controllers/__pycache__/settings_controller.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df33c6a3aa376302aa97c9a5d65da8c9e15211c3 GIT binary patch literal 7469 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 YRj$WLthy4?Tnzr7>XKFTvJ2s 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 literal 0 HcmV?d00001 diff --git a/controllers/account_controller.py b/controllers/account_controller.py new file mode 100644 index 0000000..2543b25 --- /dev/null +++ b/controllers/account_controller.py @@ -0,0 +1,149 @@ +""" +Controller für die Verwaltung von Accounts. +""" + +import logging +import csv +from datetime import datetime +from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtCore import QObject + +logger = logging.getLogger("account_controller") + +class AccountController(QObject): + """Controller für die Verwaltung von Accounts.""" + + def __init__(self, db_manager): + super().__init__() + self.db_manager = db_manager + self.parent_view = None + + def set_parent_view(self, view): + """Setzt die übergeordnete View für Dialoge.""" + self.parent_view = view + + def on_account_created(self, platform: str, account_data: dict): + """Wird aufgerufen, wenn ein Account erstellt wurde.""" + account = { + "platform": platform.lower(), + "username": account_data.get("username", ""), + "password": account_data.get("password", ""), + "email": account_data.get("email", ""), + "phone": account_data.get("phone", ""), + "full_name": account_data.get("full_name", ""), + "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + + self.db_manager.add_account(account) + logger.info(f"Account in Datenbank gespeichert: {account['username']}") + + # Erfolgsmeldung anzeigen + if self.parent_view: + QMessageBox.information( + self.parent_view, + "Erfolg", + f"Account erfolgreich erstellt!\n\nBenutzername: {account['username']}\nPasswort: {account['password']}\nE-Mail/Telefon: {account['email'] or account['phone']}" + ) + + def load_accounts(self, platform=None): + """Lädt Accounts aus der Datenbank.""" + try: + if platform and hasattr(self.db_manager, "get_accounts_by_platform"): + accounts = self.db_manager.get_accounts_by_platform(platform.lower()) + else: + accounts = self.db_manager.get_all_accounts() + if platform: + accounts = [acc for acc in accounts if acc.get("platform", "").lower() == platform.lower()] + + return accounts + except Exception as e: + logger.error(f"Fehler beim Laden der Accounts: {e}") + if self.parent_view: + QMessageBox.critical( + self.parent_view, + "Fehler", + f"Fehler beim Laden der Accounts:\n{str(e)}" + ) + return [] + + def export_accounts(self, platform=None): + """Exportiert Accounts in eine CSV-Datei.""" + parent = self.parent_view or None + + file_path, _ = QFileDialog.getSaveFileName( + parent, + "Konten exportieren", + "", + "CSV-Dateien (*.csv);;Alle Dateien (*)" + ) + + if not file_path: + return + + try: + # Accounts laden + accounts = self.load_accounts(platform) + + with open(file_path, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + + # Header + writer.writerow([ + "ID", "Plattform", "Benutzername", "Passwort", + "E-Mail", "Telefon", "Name", "Erstellt am" + ]) + + # Daten + for account in accounts: + writer.writerow([ + account.get("id", ""), + account.get("platform", ""), + account.get("username", ""), + account.get("password", ""), + account.get("email", ""), + account.get("phone", ""), + account.get("full_name", ""), + account.get("created_at", "") + ]) + + logger.info(f"Accounts erfolgreich nach {file_path} exportiert") + + if parent: + QMessageBox.information( + parent, + "Export erfolgreich", + f"Konten wurden erfolgreich nach {file_path} exportiert." + ) + + except Exception as e: + logger.error(f"Fehler beim Exportieren der Accounts: {e}") + if parent: + QMessageBox.critical( + parent, + "Export fehlgeschlagen", + f"Fehler beim Exportieren der Konten:\n{str(e)}" + ) + + def delete_account(self, account_id): + """Löscht einen Account aus der Datenbank.""" + try: + success = self.db_manager.delete_account(account_id) + + if not success: + if self.parent_view: + QMessageBox.critical( + self.parent_view, + "Fehler", + f"Konto mit ID {account_id} konnte nicht gelöscht werden." + ) + + return success + except Exception as e: + logger.error(f"Fehler beim Löschen des Accounts: {e}") + if self.parent_view: + QMessageBox.critical( + self.parent_view, + "Fehler", + f"Fehler beim Löschen des Kontos:\n{str(e)}" + ) + return False diff --git a/controllers/main_controller.py b/controllers/main_controller.py new file mode 100644 index 0000000..6f70165 --- /dev/null +++ b/controllers/main_controller.py @@ -0,0 +1,231 @@ +""" +Hauptcontroller für die Social Media Account Generator Anwendung. +""" + +import logging +from PyQt5.QtWidgets import QMessageBox, QApplication + +from views.main_window import MainWindow +from controllers.platform_controllers.instagram_controller import InstagramController +from controllers.platform_controllers.tiktok_controller import TikTokController +from controllers.account_controller import AccountController +from controllers.settings_controller import SettingsController + +from database.db_manager import DatabaseManager +from utils.proxy_rotator import ProxyRotator +from utils.email_handler import EmailHandler +from utils.theme_manager import ThemeManager +from localization.language_manager import LanguageManager +from licensing.license_manager import LicenseManager +from updates.update_checker import UpdateChecker + +logger = logging.getLogger("main") + +class MainController: + """Hauptcontroller, der die Anwendung koordiniert.""" + + def __init__(self, app): + # QApplication Referenz speichern + self.app = app + + # Theme Manager initialisieren + self.theme_manager = ThemeManager(app) + + # Language Manager initialisieren + self.language_manager = LanguageManager(app) + + # Modelle initialisieren + self.db_manager = DatabaseManager() + self.proxy_rotator = ProxyRotator() + self.email_handler = EmailHandler() + self.license_manager = LicenseManager() + self.update_checker = UpdateChecker() + + # Haupt-View erstellen + self.view = MainWindow(self.theme_manager, self.language_manager, self.db_manager) + + # Untercontroller erstellen + self.account_controller = AccountController(self.db_manager) + self.settings_controller = SettingsController( + self.proxy_rotator, + self.email_handler, + self.license_manager + ) + + # Plattform-Controller initialisieren + self.platform_controllers = {} + + # Instagram Controller hinzufügen + self.platform_controllers["instagram"] = InstagramController( + self.db_manager, + self.proxy_rotator, + self.email_handler, + self.language_manager + ) + + # TikTok Controller hinzufügen + self.platform_controllers["tiktok"] = TikTokController( + self.db_manager, + self.proxy_rotator, + self.email_handler, + self.language_manager + ) + + # Hier können in Zukunft weitere Controller hinzugefügt werden: + # self.platform_controllers["facebook"] = FacebookController(...) + # self.platform_controllers["twitter"] = TwitterController(...) + # self.platform_controllers["tiktok"] = TikTokController(...) + + # Signals verbinden + self.connect_signals() + + # Lizenz überprüfen + self.check_license() + + # Auf Updates prüfen + self.check_for_updates() + + # Hauptfenster anzeigen + self.view.show() + + def connect_signals(self): + """Verbindet alle Signale mit den entsprechenden Slots.""" + # Plattformauswahl-Signal verbinden + self.view.platform_selected.connect(self.on_platform_selected) + + # Zurück-Button verbinden + self.view.back_to_selector_requested.connect(self.show_platform_selector) + + # Theme-Toggle verbinden + self.view.theme_toggled.connect(self.on_theme_toggled) + + def on_platform_selected(self, platform: str): + """Wird aufgerufen, wenn eine Plattform ausgewählt wird.""" + logger.info(f"Plattform ausgewählt: {platform}") + + # Aktuelle Plattform setzen + self.current_platform = platform.lower() + + # Prüfen, ob die Plattform unterstützt wird + if self.current_platform not in self.platform_controllers: + logger.error(f"Plattform '{platform}' wird nicht unterstützt") + QMessageBox.critical( + self.view, + "Nicht unterstützt", + f"Die Plattform '{platform}' ist noch nicht implementiert." + ) + return + + # Plattformspezifischen Controller abrufen + platform_controller = self.platform_controllers.get(self.current_platform) + + # Plattform-View initialisieren + self.view.init_platform_ui(platform, platform_controller) + + # Tab-Hooks verbinden + self.connect_tab_hooks(platform_controller) + + # Plattformspezifische Ansicht anzeigen + self.view.show_platform_ui() + + def on_theme_toggled(self): + """Wird aufgerufen, wenn das Theme gewechselt wird.""" + if self.theme_manager: + theme_name = self.theme_manager.get_current_theme() + logger.info(f"Theme gewechselt zu: {theme_name}") + + # Hier kann zusätzliche Logik für Theme-Wechsel hinzugefügt werden + # z.B. UI-Elemente aktualisieren, die nicht automatisch aktualisiert werden + + def connect_tab_hooks(self, platform_controller): + """Verbindet die Tab-Hooks mit dem Plattform-Controller.""" + # Generator-Tab-Hooks + if hasattr(platform_controller, "get_generator_tab"): + generator_tab = platform_controller.get_generator_tab() + generator_tab.account_created.connect(self.account_controller.on_account_created) + + # Einstellungen-Tab-Hooks + if hasattr(platform_controller, "get_settings_tab"): + settings_tab = platform_controller.get_settings_tab() + settings_tab.proxy_settings_saved.connect(self.settings_controller.save_proxy_settings) + settings_tab.proxy_tested.connect(self.settings_controller.test_proxy) + settings_tab.email_settings_saved.connect(self.settings_controller.save_email_settings) + settings_tab.email_tested.connect(self.settings_controller.test_email) + settings_tab.license_activated.connect(self.settings_controller.activate_license) + + def show_platform_selector(self): + """Zeigt den Plattform-Selektor an.""" + logger.info("Zurück zur Plattformauswahl") + self.view.show_platform_selector() + if hasattr(self.view, "platform_selector"): + self.view.platform_selector.load_accounts() + + def check_license(self): + """Überprüft, ob eine gültige Lizenz vorhanden ist.""" + is_licensed = self.license_manager.is_licensed() + + if not is_licensed: + license_info = self.license_manager.get_license_info() + status = license_info.get("status_text", "Inaktiv") + + # Wenn keine Lizenz vorhanden ist, zeigen wir eine Warnung an + QMessageBox.warning( + self.view, + "Keine gültige Lizenz", + f"Status: {status}\n\nBitte aktivieren Sie eine Lizenz, um die Software zu nutzen." + ) + return False + + return True + + def check_for_updates(self): + """Prüft auf Updates.""" + try: + update_info = self.update_checker.check_for_updates() + + if update_info["has_update"]: + reply = QMessageBox.question( + self.view, + "Update verfügbar", + f"Eine neue Version ist verfügbar: {update_info['latest_version']}\n" + f"(Aktuelle Version: {update_info['current_version']})\n\n" + f"Release-Datum: {update_info['release_date']}\n" + f"Release-Notes:\n{update_info['release_notes']}\n\n" + "Möchten Sie das Update jetzt herunterladen?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + self.download_update(update_info) + except Exception as e: + logger.error(f"Fehler bei der Update-Prüfung: {e}") + + def download_update(self, update_info): + """Lädt ein Update herunter.""" + try: + download_result = self.update_checker.download_update( + update_info["download_url"], + update_info["latest_version"] + ) + + if download_result["success"]: + QMessageBox.information( + self.view, + "Download erfolgreich", + f"Update wurde heruntergeladen: {download_result['file_path']}\n\n" + "Bitte schließen Sie die Anwendung und führen Sie das Update aus." + ) + else: + QMessageBox.warning( + self.view, + "Download fehlgeschlagen", + f"Fehler beim Herunterladen des Updates:\n{download_result['error']}" + ) + except Exception as e: + logger.error(f"Fehler beim Herunterladen des Updates: {e}") + QMessageBox.critical( + self.view, + "Fehler", + f"Fehler beim Herunterladen des Updates:\n{str(e)}" + ) \ No newline at end of file diff --git a/controllers/platform_controllers/__pycache__/base_controller.cpython-310.pyc b/controllers/platform_controllers/__pycache__/base_controller.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39655b5e360ef97734e4a96e65db225dc1a29da5 GIT binary patch literal 4435 zcmb_fTW=f36`sBDDpHayM|RUTnWk=PrV-ns4bZ5DQQwiKRprQOP(r|B$r+MsEq9rj zSvw{epn#0#M-%}ppfCOp2Ku0Xv9EpdbDmlhXuq?(XeCij40soNW@dM0cFuP$^G#f@ zR~fEG2&oTBdYUD2$jo;zX+Yo}an6QK$S}hBu6WRl( zm~N~F8+s^5K{wEyzT|h3XwL+3q2YcKB=@^Q4qx^ ztW}t3)hddLsG?mGOQMFdEb3wzWksxrRg_h6PMk-%B-X?Qlr`~&SVvj^in*+{3@k3C z>%c*h)}-_i8p&_4kdqK?d>r~(%M(cDf%-$-3-+2@ogyhSy{Jz5Ie7Fggkk&asdd0C zHnzshvyJoA9a~Qe2kh^#>eu!Gy)BMy$kNz>ERS8t%D4bo6%2AIleJ9N$L#4c%_bN$ zvbS1GUT1FA#!6by$xy1a?0G>H7|%=V!>PJnh++0THnmJ*g~TceL=Jmqr4`TXkXbzM8T-%uo143zYN_;Y zUq(Xh-jaIH#KT=9wb{TzqunED^zQ68IlT048~%`c(HuTjW&DZ$tJfi_R?%9wicS$F z?yRgiqqi6PQFDfR$HLgzMRj^^;?d+A!Xncpe?zs(r`JcYR)C1pfmDXmS~DNbyW7X{ zU>HX-GBRq;$K47feDsgf^kX5pMsOR+AI1^?6#dj*=E4y_eBK63oxTcWTSgq#7WqlG z)c3HKXO<$Bv-(85NzFGP-oc}72)8i0e1`7FY5T z-^k<6sJ@R+C$vk?AqCC=%+7HDnH`YbebjoSJLT1UZ;F|I9sQDuMfVHPWch1 zbfR3H1t;YkL*9s-{J`F){nJVmUe3eq_RR54Ip}K+$%dn7S(98y$!`%F>WBD1F_XpS za$)qdGt9au98SSNsVRejLg)wvKmj_DLGd|0EQTWQ&955?SJBFHyTX}paP-SGplox# zFnaIhaJVI4;Uh`9Uiw;+;_h1ZQpLDA|ah#cP{7kUY0@89FmH3+-lTGRW# z-v-dJs3A;e7uDwg(Jlcx^x8t; z*3mb0g9s#(s0=nG@}aai`Sr;iXgwP_%hO&Ks$k!rVMrF%ZJvrD&L@kF(yY!0pEhq04HEBE6 zgYMMt@-1Ia-wzV4v)}NZAB`k$`TdY<>ivS8^+gAQhBQtL89mC5gc1McuXqv({^5r_ z3?!D) zwTZDmnal@QAWRc_Qw3UUuBbIJSC=7W*R-KPEwYyW%9?A@MGW zUqj$bH8}%yz<@Eqk&YLsOUBuWzEZN+l(x#uwU+JjTDep%;x1)(eRWAuuf~$1!>_7B zq_yqG_sj>)d*)`Wq@pyEUV0eFN4kl#oNgXF+GX@4y_ox)-%hhF``Fj~UNYIf==ZtR dXL``;)AH2`XxHe}q=}Tdtn9)nHd*F^{eQL1y%hie literal 0 HcmV?d00001 diff --git a/controllers/platform_controllers/__pycache__/base_controller.cpython-313.pyc b/controllers/platform_controllers/__pycache__/base_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d0e9f6593b7455a37116602919f6a29f15e182f GIT binary patch literal 6495 zcmd5=-ES1v6~D8)uf$qWpO3W=FIHHq#cyTFWVc^mef#^9NRgxi6K=DT zughgR;qsO8W17a{{hrUv_BF$mUkZn`nYX(wQc`j+Vjdjeolvjtk|LK5I3`rZH$h%>uv(U`QCmVZm|iz( z=Xkj^TQ&hj&CH0Sx(2HJmS)eC3z}qsVYD^rdfAk+&`+;1O*f^7_j7O=`5CHfIn9(n z&I&kq;#2Y|M|Z=z7H&wQDCu+;5O?nu(5{h>y*Lu@{RVj|!hvZL>TAx4W`Lb@8Y*`| z1t;OXz8vsA6hOFu!#Al7Y`(gI3fI4GJ{Ooo&N&WDXY`yc!6~J>k?#ZBEzLRf;r;wI z3DBYvU}9^4n555`QclyQ5v`!o=}bC6+hGcKN1ke`t*zB2%1*tCm%9ygcrA;2U<>p% zaQ4757M}kG`8wWOji=yQPVK9vj#W~}mQp8fjy{TZFSDLXJhdpKek>3lYlPVUZ+V{XFQtV8n`+7;oBgFx94xoFA7~7 z(G0;rW=2P#0=^eoA6CE!);%r%8#AIfVqF>VIGKsO44vX>-~Mf|lP~yVNe3xzpyoHA zqp%)Bx*tBeJzJ>pW@h&k46yK5b%T5(lEkis(dw>DWmo32Q1#I1%AwOs@v%i=Y$J3S ztT5;=Rci)5ULJkD?{0(_{Bxx;znc<5r!-}EcsH8K7sZ>Wc)A+iYhB(Al6(jl{~PFyM)N%ITjqHri9h%Y?#WhILU3`vRI0D^U{(sh6=68X)!6vjX zgsSZaEA0pGjaGYyE4{-@@sUMgqy|}|nXKCcJn2?m>1KYY%zJoT7x16U9&1xOV*%t4&NHM{mZUegWe^aE{RoyG^wvEd*Aog$`~T8|M3J z3cD}J!nl1~*rd(b3oxUFKL`jb5)fA0ZobuA71|+qTkP!rY_xi0ta4;*vH#42xy6?! zzYye&7hKngcKB1m1#^@2KPF7;6`AudtR8>>S23^!cTf)be^4`!4Xhc%cabZogRErh z`3f_5>9E{{C&>n7VQ@Pk1u!d6j|~2r5EWMww^*&&-#@16O~y;Z5K%*@i0naNN4~wIs^xtzIhMtik&3E0r%9L9 z8AGy=n_}weNgfCeNv|6+_Z~}Bn$qEZ$uSGk(W8=~Yp}hH+e)=!En@ZNKw%c>J<`#OY@qLx}zU9_&$fE4%_+;>-!G){$QcJCUH-*2(cP_UM zK5Ys`hPlVvU%E5%ao^2T%e%Vo4BUDPQV`Mjo$bGR{f@OT`SJV9t*2ppbo=eWTZ4;j zJxk5KpM{s2UwhE>C-HOf&nK%xmn%b;ms>A>9pCxgH#zA!VPlH(bx3vv6 z_PhA-aq{_baU>G_qrd@8ixk5Iis38E5>Sk^YRR#BUX0nyRqr9qO{7~f8;Hq2$Oejv zQYsf51M5*m`GunzUQb+6rgiYj4HVombcYqCP|hm~OL(!=3`ISsy+|;;rRYj9cbcO6 zN6{Uj_!p%kNYLAGUxM@;qz?7G0_h~G=Nw48VN%jd-sK?&q9|#Ob^)`6mU5_;3Bp7w zMS{6eYaGaTza(FVM!pSg=XXCl!1J+ZEg^o-vuKD<)k*iWD9@kap0)7&J3vKVeAdqM zr=TkE7r1ZRQhfVU0)$y@HhVfo-@%nIUFSy6eQ@3$$eg!Nm8nM2sJiXfb?rSX1DQ-K zvnH|0f(&`}ta@8OhxgMw*}#&l)`x}&SB z;TY;_Iwr1WP#Tn-@}S~W234o3Q{Pfx4QftJ)yqMBu;47HdL?KKnod*It3hk9=q#$b z6|@IS&XTIvg5|-Av!ZKUKU`(?Bi%Xkrp6Xn;}eZFc==<~Ig9%yYvI1dYq)=m8CzOs zF&=~1`sHC5iD3|MVedYCAna?QjJ&?^25WM}6Mxs2y*+Mk#Nk2Y4?{2TqlaHc-TK>p zWN-63(Uw2(15fzT-L-3B#PK3^%Z3ueomfU(aB07HZS88n2Rw|{-u9y2o*(wR^+%Xr z+w7QW^_}fK!9AANM|W>x$Uf#lDf0t$*%`qv^x}oDZzNsc$0@5a}LF} zw}bnIOTFGO4kMZ0Zt*C>T>0%vvA9cdG#n^W@ltDkA z&@+hs{Ghs-pYj{1MB2yNp?;+4+E^cJE|!MUj7miEXH+luHCFn9Y89q|u=3{if9$H` z-(SD4J4=l3dT|iB5o9a(h5_p|)5-?lgQ}(Ft3nKg)1uoxhl&MWpNCFm!}9~qoa%Mx z4)nz-Uz0NCaSQZp`n|o#9z6VMKR_*MYz`xPG?dau$K;7O{3zn~EiPmXeGTm`EFOBt zMabhQXewZ|Ve;7hv?T|)IYL|y`!0d1sIn`}{z=v_vsitKak3g&C zx_;-GXqO4q%w{rtnr>o?w$Ao~WWX5z*bE)SyN=tjh4v^D^xH>N5mZ;bLiFr%09#%bEx z9o-d6nC>Z@lBlff75o}z%S@g;ReidXRrYjrpBxq{^!jC-@^A4ZIT~yG&Cg;q z-$31CqvGx-+XFgh+Egc#6CUY(y>G#aBzF%ebq`$hZoY%(2*kr4pm-6?{_}Y>2rT!!R z9<|U)&Z^l;?6LEj_yO8z)^j-p)}cNwQT+prJ+-BcX|3nk1P&x{^@*r8u70e~v|^O3EvJX*eKH&M zLr`?D`bSlwE34hsZ)=^W<7XhZjfW5RL}ZgWIB5iIFP7b;Q>et+DP6EZBTqy;>UO)2 zl*(Isu+{;WawDz9lDi`@ymJ@Un=Wp2cf_~TGUGdOKW%U*ao{poTkN`X$t#0z-~0|c zc-hZkGs9$pi>c#gH-VZ6;GmY+S0qlUD*?yhp--z6r{_-Mv*)-LvCTn zZQ7(h9F+9Ah#sw?V|)R{yS(qqNT9J;zp{_(c3RtwgTM{F0Z&&%)=l{Ycfg~)Axq1O z__Q(F8;0Bso>w~(o$kEf7j|@k${9d@j4?MC@!p;n4@@#gscermeG@lgHx}BWQJNW}U z^9*a5Kft4NTfM#8T-^3K>?{qCbzQdwZTC%a9%W}Stp~$COqp`PB|h+@bP+-Yp)glj zv#6n`GG(dR=TX{>N6d>LDfN=m&iKfM>t&ueUEGCWH#}a2zcj{gt_bLC=3h0E#No(171KD?0pq|3rh4@1j z+|B3Hxms^lkdCA#EP59exrxFs8hS%FOe*WTfwG}je%`1x^mAql&nl*+TWB}*(k~n3 z$}bw0h1O+Q*SfiEEbA?!t)JD~`UTzkW8;^}`9fP~-E|i;if_@}pgM|0IOmNkctDI+ zc6SFA3Lwd{vNEf%Dg_6hTd;V>p?0KQ(LQ?WhH=kSR!qm;W3WiDfQQDBc1x2lz$Tgd zrEz(zQ!w$FDb~g%1Ad*(E$!ynn~P(e0*iP9x=^?CgTgr_B-&27x(s;05IMlV#v!x& z{!V0bKjhHL5MCBaEjS*Kf2Q5`J~W$x!6X(BAM_5o#n5czp?%pKA^i2$wqa9k53EE;i|rV76}RSM7db+tjh&jsF!qpktzvT?-GK2S zhCnl}2$=5N?Z%`kSEb$ec1SSPN@0|ZjjXH&7>|fPfkYuiw8)7akzG5BpomwydHh5k z<0_c%2WwY-gv43c$ir^@Hl{f~wxVDSGwk>wDamOgUPCCDk$QZ@4=|#`kz720jYT{N z(-;wr6MJ`ZT-9@yTs64T+K@-LrgX$tG4>TOug`Bk{Bkcq)He|%nSLAI0n^Zgk^;y+ zhc@Tjrr;3j8$A3Hab~|8ze*!{(!P%H?e-Ac0xJLTK@db)9GUspFcWFEPi{f~3q9y2 zACXGtJiWxotaAHcCK4qA@~r`D&A{tiF2@9)%Y8D`73E&L@tZ^+X4vKd6f1REqmpTRLxFDSR+I3>|&G|7xlf0h4fH|j9=ZQb~F!!Ukj{JK<0o-YJAXTXoU zvAMvRysCjy8E^{xRr-X0IZLTj25u(HTZr{Xv^PULbJg9A&5j{f@B$bH>irPT^}e35 zf}He>(<({4{ikS?l#5p2gOUrUlS&;s%aqBGoRG=XU3V~KaX|Hk>)wpLAb(PI5v=uG zSC}-}H>n`UD87awHNj@_n^Y??zZZ$$q9-p=LCJ>r9V&>wg0@`R%oCj+3>Qx61An3bW8bG@~;iUT3r5i`)SpuhsSdVt(XoOpqS0R*%_5r!&QD#bCkP8 z_FPtNaGS`rD+~z-+Nv2w;#e6r;?z{1Mb<{2I$PokY?&`04_{%c$itDXBmZu~O`c7s zFv1jzh{=^tZ!=F;2;=|6QZ{>KT1kFTy^53AFNx_{9uD=e;MRJYEEOPOZ z%D_vTX|0&Ek9TqW=j)=0OQ$T_xF|WLP1r8r1}Zc6n95?+_CqI>X%!iH2w*Bbn?1<`T^Ff}GD}X2w%g1L zXVm@iZugCh*-eB~8HD9V-p>c04;R!%Bf!g|aa3y~Z>`L=HuKi%Tx$!hI-oOau5~f* zTbpe?Y!fX+X{Vlawz4c%dFg^*rzE`H&25zxoiOOcCo;|dMM*GL2xUCN*!T6#j_%YY z@)>AE6ia|SCXx@B?GMp$#$sEq2Hs$Yd9VE)jY8U8*OFHYT4rQ-x@vV_4Te20kguVV z4fK^Xia(&KZ=e9eSw&*%B9{^$QKL)6bc_BWZvUQ|JQOF!BOReNOY+Z?HRGxfpj`rz zjm-@}Ty-SrxAXjJfmkuY$i8aIpVFYjg4L6gP8dD6h_ji|nIx9ke&ej8i&rp;xI)GJ zs{VnRPF>Zh6Euiym^+p3f@|elUJ%1fo*eieCj)nuGPC+V8q!7d_f<0Nhod->=@Js; z`JF3z^d>{$)Ox@qg_6oHHIge7-=jkL+xMtO_CNEl%F`;(TUM$81cFSpO2i!tQIncm z1xv~VBZyGCr#dN1OeB7RQ`#sRM$0VeFdcBXbvWC)UN_GoY0Z8uBl+6@Un%EQX3k>2 zg_&TFHSr9U6#0nfR1JwKfrn&H$ci79ayU_+m=Uy9GV{SofVA?nQ%+vo$|EL3umy>y z?cwCPlMso0J4Q@xdpn5n#gK5~jST+iR2Ao)nlInYq1~&XUHDY&P(dOSN>1OWn$okW zvQhT(0H+i$K2&7ZM@hELM0Y4S!nCxz(jR|c~XKxeiQJhdrpwA5H z;^cYeW0?3lTE#X!T2{c_EZ3W+F=BgxA@M=_0O ziYTT3r`DG3=G)sBFYRSpbsS=}LVAe0~9yJ}w$#$N%(cIF;^%gAuG zYL2VD!5XYNF8>%GT7QB$a(r!#EkYmK@t=U80%UrhT!li`C>IC30>}n>QDo=2m}WoM zP2Vjrg82`Z$KW%2G6#V%#mgCHP3h?)l@}Nr$vQzAmw<~1dk{+rv8z^HtCtihi=xhZ_}sfS84vfcGa0RAXW| z>h^m5zy2#a9=$GJ$Lkx(3PBh-NRlBN_6spN&KGP##cjA~LbVWV11OrZZ0Zi?p$%HV z1V)*_98(Bwf~4dSO2^pF7Ln&md$cOr11dohZ=g28pevb|cPLEoS>}45r=B(nXJsew zdI$I#fs`Tt{2`HhDxl_gl0=jID^B@1igpD~8Me=Yb8Z2HwhYT$)>i>Yl+5JWVv#eu zLd{PYd>(excYWZr>N|}14n~~YBeXYYpIpYNAdut8va)c%m631i0|sWU>9WAJ05+I0o;pEO}nsm13;z$!$+c%xNtH4 zNmO^bq;-26?-F)7JB=*f1q5stH(!nA9;cgO56li&3=ywj2$_$h*xsa1 zMvYk%F|{U`VA7h$_nhV&8t0VNPIK_jg`HSL5)L*Vz`>G*Kh=bjN;%e!FQPr>-9=od zEOH&#oF75)50RHG17P^=`#*9&q2LTxYwGbMCOA>$0$3QbVq>)Uwn z7dSJI0i>v9Sk*?;x?t6;R%7wPreidGOf8YAHpcuan8h%AAp|u z|0HDpRH#98Z)IQ9UPj+6B*QuJc=`T3IW9cb{*YYG>_|FmIXJV|K6d91wzy z2Q9`o+wK$a?dcSPP4kX}woUyLh}C0RP1PF`2zsbn}2S3miI8gO5XtJ19erW%Yd#}i=|S8t7+ zOR6#|QZa)ROD&`oRc5jx&7B*WOvuY}QXRP%RwGOCWGvwRoccKJx9QH8W|x>8j_U4} zTW^454DBHC(!w=4qWUfRwv%B+elZbN7gKDx(qZIy&d5m_ZKl|4cmdyy6Ol+Nom7?5 zXHZsEv{m{PCl*p^)yQfs4|F1}rj~iPL7;i#p!Bx5IUk`&d8Q)9c}L+dL|NStX{-g8aF*?4v6C(Uqf`l@}6)NPb17)=Y-d=qYjq7CE-d- zqFgs9%ftk~V|wluAVXO?ty=c?sODJ0=-RF};~SV=}lp5st}8WQXGkIjTEf1iOMob;mhH zNz3VGG&3EKEUD7+Cw~!3;4R~sPN~vLN>Sn{nCUqgS5;ZMAu}b7Es~Uim@=@s%#w0i z&9uCjkioT*%5F*FSU8@{c+RJi*dkJrsAgO*q|%9~luW6*b2h##gE9S1-Jzsc&{r`O ziYMc0D5N*6gs~SD-O19)BsiIQ&@bJqV(W#J^4`;%Wg1*)C5$eyq=G$VSLDPZ>!s4J zP$-fJD@rJ&&{Rv(ZC#Fx&(A67`us$68BHlF-$e7LQjs+6Qf2;3Ts@m!m{(;*ov(1d zGQU#VhE>}^nXk7=0xP#z4>jLgm+b=P5G9mR_9ObuZQFr5UhMv&!QSUywSOvhJw`PlQaSq-HkZY^u2)>Wrj81+T-z4Dx)-qnPlEoeVj+7G`)EywRv zt9%Ltoxep};_uX!E9f@I6>L1xki_1dA?B?Kb?4MmnN!qy+LdV9P^+dL`mEblbN!hJ zdgGOQv~XP8fL1j1_FW4$xhJ4C8(zfSy4{wuCyYDR#@uITToZ&5mIlh$g{d5DlepC zx<_7yLJ^8WrG(7)WezSMdgDm+`u_0J*o}QFi8r6UmJUQx%l-!D2VwRMMZ2kq7A1T` z4k-~P%Sq@is=`R)XQY^A2CBMS)>b7_5#rFumAK5*&uK-{orq@S7|C-amdDR^x^YQ& zEv6HRP%^wM>#fWv#ueYtvaBwpqPl~3UiYjlrIKdgU)_EAI< zTi+6nM$red3!r!?0GEtcLh#!695%E0J(bz8R5MXb$xTbSH+)!!(69ETLay z#Lv1rTwe65$!|bEq3r4|gWj?d(_6utEVPKVPev2BO6^%>g>(_A6xa|a4}DVaD6>_y zK@dRJUX)R;W2SBk>-POFf6>$O_N6$}@`zw^OduH$I2v-jTaySwl0y}S4R?g!~y=ka1!-@T)EkKTLk?sNB# z<+=_R`-h9$I^Vzk?)CRm@21vb_tUv;2a7%Z_paW(dM|W0bpQ42@t1QwR~|JvJDXRh z*0%~xTMC|D&C^@(3~HXi?9kb~=Ulxmtv8@(Ro@4XKaBp>^}oFS`-rnrknICV@4ZM`~^i}urM6PRm^#Z7ElM3ylKWQIb7aF!TmqxaB zUF#exw)x&a|L*xUht{^EICQAki>w7HNUH+eYw!VGg&V_8(fd?-=cp=+yIPW>K zksk(+e)Ph}mj7}3;o)rmsk~>BuS1XK=_z;yG|#{f_OHG1{bw<-+Vu$+Ji9c{uK&00 zLNouONMO1@JZn2QDEzo#q8pFD8FZgNX#1Oyi4*5XZGSuJzy~`^+zZxM(=55c1|{2( zYyQ8)3s4&%Hl$kz9l?g6GbjdK0TG&*b*9={KuHRwZQ3fFeG!K!*nZ^zBt0DzH321# zLjF3g?3*EVrY4wlP(+CKa}B3#tyYT8l&#!K+Lf1rZfMCiQ$LEL?Te%%*%hlK(BBYD z8%ekbV}_n{7FhJ2^#2D1H{mY)d?7JVI_8$;L=;v!tkQHcD#hXpswBsgGEUPZY*QS@ zOhyJ-wIn4j<3uLZCX;5Ld=$AJa3kR+otm{6&n8^QI8>`J9rpx_DdTQBYs$ycxO57l zIL(w5kWX@38i*3N%1iy^<=r!UQA@RP{EbT|H+Q$DVwzzJ%Ey?r6gCE$lwJt00A~u1 z%tDq+VN32mVpmoGX+c65mb;W9X~CzdWYc5fk|2R(JOLBDc`J}!K(mvI z6bmoV#8N7O2NGlmmsXfSKxY}O2vz|piYdR8fMnnXG#85{)POXVNlaoqF)|qk-ef>5 zax##b$sPrQ}3EHPHc`^CsCqGz90P8RpkW>x{S_zt=1{H!$Vlud~G}{D;fc+Am z`~c`mERa4%l){SiB4CS~DX=wI_LGki3DrPP4BakiEUCfgH^9GTJ_ux9BbF=8=|q$y zx-4CptxK>7^z4=v2f2{2X?n$j}Hy`ce)rqaq* z^f8*}P3`(c-5D{MRreqld=ueHneJHOTBcJq;2~Wk>CTlmamARIC>ujZP>}RBwiWL{ z!CV#e%b`2ss=TZ)97ZUXNMWo@qIY+kR2+`oLKPiAl5w)O8@slwv+$k(ekvhLdh^B{ z-EEY|_E}cwtTU7hu4AwXYG_A!Q8wrpwOY|4$|`pdLE-2Wd>5?iR-tFYT5OLNx33g^ zp`!0{v0W{;zy4^8tKXd!d)9rzuH%KBC$*g?|9HioFAc-hKJreebx7ZC&@i zb@yAjzR_IUCZ_DZ(xm137a-{ybp6#B*<*@Rt_ zmi1<#r6Vi)*bek&1GYr+fJ+#fbdoSYBk^Vg$P3D=38qAV9cT61GFyXcYGs9vh@@m_ zs)5tfek9_C|Q7lN741LY6pr+nge=;fWk5N`3>-Dzq+4gQi&5EMu z?dChppO%IK6`r_&E3>>o&ItZC0vm~`05-Zqq2*LGouGG5DD*};oG9gZL!reu)b<3O zg(+k^Lojh7p%AkZP1^YE1V!VBbQ_q2(H7S2(Rf5<&r{AxipaEN=P4rQV04P=?Z72f z@L41T^peFF(!dfK@{dw^1Q)bUZOw|S;tBU(Qp1p_^JKSqXwb9 z?*rvKmu_GD>6We!UiePm?Wvz`lh#_lle&G0UfOcocHphG=Yz<1p1XbFSN0}{`-|R# z4*z=6(&y;7Ke#U7@nG;vdi?sN#pdW(7Z7od;4MbbSUJ!PP3*nkv~z^O@;rl_$m919}94ys> z_X_w<1?R6Iqh(~@oHhYCzI|GEmAe%~8wmgRIY#X4cd%|e3@Et?vH*??j_-$~91AzN z*f_#;cXT1N98SVuWqK1oJwq%7-U9C5WZ30PkmKYHXh}dPNTZ-x-e9lt5q8_)F%DpN zm%CP($})WYHae$}0bd1Qf6%pd?c<)j7={Hyh;^p{VB596NrJ7rHy29Edh~1<#F9&BrOnxH`-Hu#0@J}S+Db%+a zIVD>>1SrK2q|J0dR#j-{-?ha)v>MiS6VztIrx;Sh3kprDR62#*I7vCQp@hDxJc=s* zs8{k&K}xxd=(g}n-_Y7@Vfc_XeCT0Y;mEXhWIE@&wA!@p5nS5};;<$T7sL@w9JwDY z1kP!Jb9r&9D7F{GAx#`Ah!*&`OnGt{KdA!t%QC zx_yIh&N0jP?p&4UV^*kV*;Gn!>u7M!TDDXqI1YswTCJL3%D%d6XR9zq%<|Rsm+iCl zvD+$S9wnM;n#ZTv14%$WNs5{ftm?(pEoNqAtg}L*%;@MN=~p#jrXCKuy`~U8=j=G_ zaqM3cE8~6w#WJ#6p)&I%id=krEkyzaYr+#LLJ&PgAL&G{33Vxwh$D~m*N>VUl{KL* zMVd$Wwx5C`DLXZx!VfWv-<9zP4V0H#6XiWz2M`WS&vMjogEmOVH3_K4q$iS0oVC=$ z7Ul$VeTJZ%PveS#TLBbE8c$mJ(5l;j+p#|Y3FDwKLd|A^=kV?#JSe64VNPCGuAz5Gi4_^X>m(C;uF_D3L%5jg<^hX7u( z)tYgz|AYkgCy4%na?GBL6vi)Uyn|nf{jn6D$Jx?>Un0e}bO{n;D4_G03 ztkb9cR!sg<>ShTTZKQ=T{pwHd`hNpmWf0NENz5I8H9H?K%%`>aboR#Y=jPwij(ubE zVE!G`y3J5lOl;YZXL1zw!Ws0&FcG+PDk&@F$$m&ZE=~5wLxq#CX(wOHz8=k=l>bBf z=b`z7mc;>c%&p@_IFXh&F>{3p{<6AXJ%x#@+QilDd^kU`!29LjVyJrGKr!75VsVbO z$CE2*RnfNqf8Qv08R=f^9V+dL;I;+hJosNJkq&#NP{gg0x9E*b6T=F5p5Bfj!hH)m z0!#~-4k2BHWz5^=1~DH^MK}CC*tgG@rki*3nWx{L-v@PN0Gi#uK>QmQ9d@kU$cq7> zwS9wy-Z8Cr?7_3S-f>OrS{47nyPd@0}8 zG;vo!9M#0p{JvN7;?=6GiY}55%;v?ps;r%wxU(Sc)x^E|(Qo9%Z&qbJqlwQH#4$}A z%kP)-;^Gss7V=``vr0E4O_V+r2kNsaZKcOFf$u%RcWmk;;cs&66~${qy>r5p3gz0b zKe2YTO;jRn)&?f~m0A#2%FEhBrC*g(3KT4LebiNY4ocL~bE@^2n4SLl9hqIh64n3+ zs?4PIgj;54vu{nhnFc5(g%<$Qu@qfzI&JvP{7$~DbXQ!NDcwj~z+l+>6#Zw4Xg@N( z32)=AT772ULV{j#y>1X_P|#}?^8ZQoX%hp|X!I5ud+>j4s4#Fq8#quHIH3)kC=5KW z4Lr~HNyQhG_Y4=ir9$_Z);*T%KClW;RY$?ouX*~j1BRCfpWJ07=RJFP<_^uXW9^Mk zJ^uAZVQ|8dZR{~Pu&yp$M2|QJf)i_l$qn4oK>S)0YF4`rKLxFo8yC0(V*U3Sa&tD* zDCGQEhj&n-G!9@?HorZy`!b9l1~?f0Z-QMnxoyi{WhHQ0_n3=@qdGhd?%<5TdEhw4 zu6xaV!ybw<@{O3lxDFirpCBr8FFZj~yp1bws z}g^nUw7S8b2u;V!kOypeLwSVW-ahwAh-SCss||*Ta3=L`v(fUk88V+KYS}M zk}J^FR1oQ$UGsb@j%?-_+U@+zdh09Btgkweu-of#h$Uz>SM>=caPRbMO4z@)gyU;V zG*n7lEyE4h6?hG=6j~#8`XTHIp0{(H&pQL(tNw%_n*o6 z4sZHy?=Q3uYwg2u@UvX|Sh4oIW4O@a*E;;TCBSkW<3Go}fsyZyWOp6Sbsl@<6K207ziZ1td%LMsA3!af;0fr2Wk2UEB=+>jZkjuLLwZwjw@uqQA>B$ z6urT?yk=NEB*bu5@K7>*s5O1CJdmmrgSSEVql`xX5e1e1jtG|XmmZtNP1i1WnT{`R#y*FI=pv*elwR&B+`=6AOK$>Cz_W#r)w%J`k} z?6%#x#yt<5xyD1quKs)DcgNR~4;FG=M^-QV?}3+bW9M;$&=q*6q1d{;(7ID=-C1ZI z(^|(K9LokTXIsY#tuJR=UoLh@*`Y%ZJrBOQj&-!et#}MxqqzH;^$S60zh-^p0GV}B zXfeEix_dE>3l;pLAG?V0Y`AzLA_3=u@OT!{=8Gp>+||8ya%Ia`34e311G;dfe+1I* zGv+mpq;vuME`hrZZ>k1o5%-_Qk7LsKEzTmkDlLTRRv_+h;J)J`#M(0MO(deYsaUz> zQFT?LGQ4qV7G>zdLNz(%Nf$I~`w_(+t*+=!x1#5l@%thLzZ*(MPhz zez@H+X}Pbg3-o-T{F*<%;4vQ&r|j?KJ=UFc36g&3LTUysdcj|pTGl-;-g-$r7b@Is`KA&eA5UO|L&tDEZLi8U z6(@2CG1~8XbuM5q{v$J(Dc8AgPL&gpM>Bqgnhpd_VWM!e-fsDA&h?JYALun sf8y%QcJ0f##%?!!X;&?ljt|a!DbVxFgzc==GHJtkcXHj>%Gr|7HGAoH$yre= zEtkBR-P38y3yR#OaSFI7kPkr{2ap1zfPsEaUtsjheJJ_|+} zDN>S)EU~k*GqW?#JoC)+@*588^{R&J^Uk&He_7GAf2Eh{tALkp;1+*_glQd(>C6c9 zjxL`@$B<96W8!HBg+Z}X9F#hxLAg`bscj*!29-`l=8HjfFxQ!r`BG3D)H`*VF9(gm zd}m(ft)Mwr=q$*5C0HDs>73CuPd`{<)kD2=_6?2AvD!V2)x6?evvUsbb=JUp!>i!^ zJTo@5)_lAVVyl;jVZ?_);BkBF{%73Y^mjLhyDMVkCH|H#dfT487Kgi$KMdW#kM4gG zwX1LYk-h2NjyC*(AGq9)_E+8vBd^b0YNq<(!)`1hk9)#?@6DB~fj98NXyt7;>TUaB zzg>NR!L{|4nU>$#+~%Ik(&}jcCc5ln929=5yW{numY$w_-4))oz>T(sd~if9YHDwI zp@%VteADgXeeShhZy1M>$X++RD8g9TYg5hawKy6MB%S!8F*}RSLgep8!(FF0VM>q4 zoW5wO=@L6gJPk{%(eidouK;H2ol^EJ<7wf&f@c-axvaeQeQgWE!OVMBr_Ktjcu(sz zSjj80vRB+HFpE|06*}{*%H~kh{JvJuIty5?TDqtv@cxkRdR*}={xXPAS;ckcd-)1x z%via~u?#^FgXukkD9wPxW+xd0TEAL{5bD6x+jqwd!eU&D(}-rNyh94|&?4*S-g_3f#UI zqT2NXkEP{nkQc~ET6|N8*ozyWXWj2@NA}?U&-wv!No{=?*`uKlJ{l&EyzWPlXW#O; zh#{k)y@8oS);u11ag;Q#1s=r4j`+Uq_FX?rYTq1&kWo9NUX#k@VH_|!97bt*(;s-l zIBJ#CqKHSJ)p8s^^drYfOCy)N1Cf?_9EK1jPHG^Xk0ASwmH@XpdB3c=M(i=y@CvJ=~ zxu23qapQ!LwnzK?JjQwyw;&R$dI`Up*)WrbPD)KXpI=W$+x)VSp>Hqa7JrMHE^Huq8K4y0(Og#Kz% zeb=)*joF~D+583V$J$Tim|1U4ws2_N$;Vtg)bE&MeW%RqyuEy$xuA`WvB{P`E@a=G z^|eFe6a6prJ5)k5IVVReu=5u*{ymh@sLL4z)`31QQ2u?5J-VTdX|5O8MT}e=YX_Ba z5#yG!Zyp+#rf96ET5CBX2V;GOru=)0zj_4IBpDip{p1 zJU@8x=1Xk%Id}W3x1JpZw_n(a+r8nSRpLY^f1HvhkfcrF-trvLBtrEGZv2cRk-`C_cGH zt9EiBYvd1XWeQf50YKIAMsIu1<9ogb-AWy(rmHrmjlRxFKeXo4YB20WeMv)G@CJUA z&SOPEC{&ax<`uM*>MJ$-UX<435pyFfllrKs_~>!RRTej$-@?L(ZI4ZwK!svvBko0x zoG>IrD#@&i%GKLFZ$xZNOCYC@%4dm-CRVX!@rNm2btmgS#s3oMgq2Avs=tBgr)MCW z+}XnNhb-97#?w7HUzOOhZ)ov0GGZNxVbt`RZkUu-bpvTlFa4rcsp-pR19c_S(k+x5 zdf`{KV(FJP%R=cQlxo#nG#2%S(bUiBP5q*7{i*S*-YT@l|S8*w{FS{c+x$ep))T!-)k8oVE8Kbsj)r_|yM)0SxPnFkV4{6Uz z9%udxxE`Vl6ypgO>Gpm*?qbZV!tT3WS`{iK$H~SvmO2Amhu@xHIkAK&5xEu-ZCm+F z#Hg*zV;w?cM)mJAj{;uOcaa8@2(% z(13zm#=Zwp&bUqPAY?bV{|P+Hemj1NdUAz*4G!Dh5PJghe*d!|h?M83Y-^~NRBe)5 z5Pl^D?d01ej2S~;W28|%dw0t5q|viY0Y%Ng=xkArt$McTc~@7YS#8H}5OEk`(+eO~ zA&k`klb#ts2642kM<$V&d}7@A^=zULqCV^eI0DlN&XF|!<};43qnl1_-||7y?3e;$ zO6NSE;~qH|O0r3@>e7NEwi^zefgA2S@L;m?uz*0<$J}L!&NPZp+u}YjAjI`>L zag^~coEWlCO1Ga?nrLF}B!!3=vS1)>%+{n;)gF8v9J5cHM9;%5oq04c{1nS`-1yEo`s523xTWP;L`DXBaPU>|C&2+TS=>NWG9sM< z>6QU{du_dC@C*0=90U1&2$$efUGaikiz!oE5cv8-lnIKLOR${D`BUhmR;@(}xM=kd za8(^=Fl2E+`I_V0jNKrsDLZg3dyd0R8tLnlyg zP2hn5#nbQrJ1YZ!82EdYjx_QJ?~Aew#vWimi3~o<$_l&bSPHZ0RAifZ)h@h~ZmehcKe2D8mU*&Z?T18HrmCKXXp=SYUNZi0> zGRBsB4vmx_$hx#~;ga*TF1NnJhY{gEX!@Zj^JPQI%fE0uP$k^6 zp%;wg(g|js_T7vpOk0LzOv+b^jFPXhgdIMFe@i)dzVLm=hFC_4d8mQ8Cb5>mr&T3C z&_2`>cswPivs2E#164_on8RxavH)YIrLU9w5&fxwpiqA1U z$lxJ5{#0LY>1kCU+JY=Zv4B}K5gI`~572N*Gg~hQ?x4%uSAIslkWg2(>Exg}muDt%PHCAu=2hFwV=bM( zieC72N@i#E4^(vOtWF)EMr1?3$w(YrE8cR07=gpdj{h<5xV514sE>kl9_@V@r~Bb3 zjzqeE*nM`x^4s*~KDDm&0FiRJDt|(SWb(L6$qSThQ$og7*->dxrG+g@(EvCieV}Eu zNGNk|g z8F`se(QjcGC}530Mk)C={0W&uyi9N=X%SM}2Zapa)N}2DvXasotN@TLyIaNN(njVJ zkqfTW_bFR9dE#WDy%)pVx7{urZ9XKxdQD+6t+M2KTJgoZ8OBUL3jYozdz8>(ak)a| zQXEsud>3W^gj*0|-ZU~^g0t2ybEt31u$`*^Pbg#Ba+j&kmyv)`03Rd>kt4xS$;==; zl@xMr^`MRo$<|V)m`!ZWLh{UYm=ul>53nKy7AF9)30^1KYiTXFO^{-D9lVijkQ{g( zw50QSy)r6{lY_$vJxLcOj+oa+Lx**98I{{pO5F8AosMom!m&$uOUZh?a}vq`}D zNeMiC|Ie_G_BCN=l6|yZx$F^6LkJJxa>~I^%5pef$#P`7RBefJ5!?v|_kmd-mnGm` zJSdG7{9Zl)Iy6RqNANhSg57h;*EU83HxiaZkWC1dE8G#;Oez7PN}%8hy!Wu{hP!S4 zCJ0S6{`+{yxQTz0axEmu!le&ZE)7;LG5gZ*tzLS2_0k5;NvzyEkzbznU0Tgn2*FnJ z*wo0AQ4y-+j8W0LYOl8{avAxzXk@w81&Z!c3y{nf<#-o0=^Po7Y>_#MXIk(PjJHgs zjc9#gY!oPYe4)RG%AezwmW0$!{qGt;ExY)0 zBuB2x#9?WWlT3!{NCW`k6_~lJ@r$HfWI4P>R@ycAW%-7PfX*6l-d6c3oivmH_no~% zCn^f!D-$#bi0^fR`o_f{<7D?I7$bxFR@gj*p&5S{1WoX+>tq`w$VcD+z#afd5Q+(0 zkgd`5apBRo6VPJzy!057XXj%OG&K2G;pY@|K9HWjv6j3@0E@yJ2C)#pt^jdCPD5co z`f5^MPCL#{2E?!B)pUvbn*y%rmXA=HGdLG zdZrt=y3lhm$U1@^Y`^pBMsvvIv=vXxCn+ zQ>)rEl$lyNFqt%FamKVhg9@faxxwg965W_b0v0tMz@m~?KUHm$Jf&9G=g~HE_u;=u zEnEcx=SPsb#LJcJ52;~IF)E-{5yv!bxhN{-xsN9fh2#6j{A zZe=0>`!o!zT&r6bt%}vC&0l<2wrDNOG1D>~T+zP*et=JK$O8~!I7n;P_TPz~Z@&{= z9&(S9OR69%@~YCWAY{+}ZX?>c6`elZZ$pf|sRCZ4&z1qN?ujeyTb% zRzk+`1ly$3wTLj}Ut(k}ZO_irV%WohsslisU>FP{{{oXeoT($Y3Uziij{E?PX8!@9 z{ufcfqP?Zg^2ju+h=N!;#p{XRm0B+=r0*3n4N@HyG^lau73uB-G^GHmv0TUpx?1TgdwJcrB!m{L?dj&z$ z-NW`)?V6gdO;V{;LVv+s-Md4+@nvV3om5S#@};Myrb>6DjMR6!XHv7%JCkoFq^rAT ztG4#{KlkckStNAN9`L#Mp7-N_{^$39=t*<4M?g6LFZ-9o1A_3s=%XAQ=F5{3UOo^) z0uw@(koBU4S@_e+to&(XHau+??b8nCn07Mfw2QeUDr>(ePP>_#zdJ5^rkhw3e|KK= zPB*h={_eWiGVNnN{w`i@o!-W_@pt#d?bB_njlX*?woiAk4oR4|%SsSwgPuzXRhpIGRzr!EL^8q>>g|yWDOHZM z2o*EhVd;g8qRLEGq`34rio}Yok^*R`5BT` z6@8hX;>1EaqZ(PQ#w;f?YI=o_8MRwB&q&{}zSZEHh)RzubXAXP!f3d>dO9 zOUUKzH&=3Gl3@>%$qaTZYBgs}51KuZ*pnNGPFp?@uHk8fS?iqeCXUb%^IHt&EA$WnJ)P?U_E zX+bxa6VWAATKVkH;z_(^y_eIfw3=3w1PW$*&LmV-mTt;S$>3O|q!88%Royk4Sdl@D0hjJjGOHM?7!D^= z2{jznovRTRSy6Ns%cN4EUgpKPbf1dj7FH_fO>dEDa^ckohQv||&XHY_lZ$LOm3D{2 z(PTtX!eND$T9WSQ3(@iUIR!(XpNOrXD@El8XZ~zDnxT`a%%4lB=Q9iQs;sE(WGjLF_E;=>AbtX=m4( z{YT!foq`nn)TZ^GU9%U&?ysEoUeB8SbFu3QstHk(+0nbugg;J$8=#pFgx$iNFvBx8 zM>7>_tEPu6<#Q)dt@2r}77!ZNvQ}$dbCnCDzF2;*K4)ljbs=O6*^ihf;rFQJ_=9Ry zI8Vs+d-Ns#L4CPHp7OjxO~;)noZ1;8+q%$jO}*7MMXhJuiRMkU>iVJ2hJCd(UWt&8 zm`;musAhi~mu$GJt)Xq@*{8t!o|zi9m*5a& zHrm-ad$m@)ch25$e!D`Q<@u!?7*X5Sw&xt>c2dsYW6WK1&Mnl_zP0s+Bd?rONr?CM z3D@Xm=D{UI)&wEty5=vZ%ygCCs?SjOoNG(p)$Gb!qrO8u*NA2FFy|`Id`oN3IFm+g zmD~Kixa{s~tzIG2n{rLK;=RUm(XuF5*4Ku=8Y#(*eszh$e zVI|6BIR(g~DvXdl8$p!qEswPVOeKN^2_SDJAv5($+KWslq8T|(cQ_I&Gv^>~T+-c( znPf7Yimb?b8#9V=j2m8&)unVyckq$x-qodaN)D$oD+>T1HejQGosH5^4pVfHBD$;f zZIM_EV<7wwyg}gP%NV7*0E41fs%($Bv_L|O>13SAz&m*7$WiS>e*{&s+vidX@^XX_ z5t9svmK`vj(t@0jVo2V)N8?g=*_9LR}_6?SU ze4+am2|+E%u}Tr3H%x1l$!eI-7UaS4nc0MU^6jX+N^7V)F`PIukI@p^u!RAU5hd%M zNM+M&7T<|+0^sUyqrDX+uD5|OS$Gk9pNb`KoBdg2g;0oO1~$l>2Zq#lGyr5shtMl1 z%UGs6!iN2jJ5chrzH{a7m7-VDyi%_3Lcu$YB;&K_9n!o*>k9>M0Qju)xzetl`(5|C z?)Tp7ebD)EF2CzUsdM-JefRd=KXC8BgMAM(`OZ_NuHO46?wz>*{JrNNoXmF}EAG#s>@dugwjw7X>zWdkiUArH?7k+R(cj~o#&(+7xuFjUV>5c6|^R}XQ zx8~hl^bTm=f!yF^!8_GxNE=QRtr`2ksYkKDT>kUrpYPL7zluh4eOC+K*T3lB^QZfM zvhPn1{NzAx?A*_H=KEjCd3$R|c{<-UzV;GY+aVP@M*p^BbVG3NXfbEDzN~eQmD>Fu zUVQK3xIpn{qsWFwXxsVWzW4Thc;LMQx&9*$S$_NY<0j;+O>MODdC;Po zs~9bKpZmhs@ou8%lS-}aANt<&6p_gEXW;(y zz3KH^xzV%v?#XxENZPxvxaWkn=ftB6`8`v))`2fJHMVy;cky~|I-DE&ZlN{8TkX?Y z`--i5wAMYjz2l#^p7`BA`}|J?yF2hp@Ah|Iy8BYD>+r)@9=@2{b*$h$zL_2dPJHs> zrqrg!39TTQ$AO2`{(K5iQ}lVa%m62T+8VwD8?8-!FN36~(p z&~wfL0lp>ke^J~+3Kq^_=pHcn+=`rxK{SUbnn}f^cw#}7E@YL)SiWVqw#!S_4wpC1 zNiHR;H>=~wOrq_xSiHD=>$9INC86CY?;ZgT377=kP=^XBO5!p|(Ohj(f`ImtpZyH_ zlz1?6oLY(~(#ud(+)9I{L9d^El1!?GzQn+EDPv0wBEJdZHHaXXeUpgIT+oRaUFNd1 zT%&HG-c8~LK!{b#v3pO`YX%E|@t-gF_FqauQUP0!UiHq&IcIwWYMJRVYS_ zyrtjF8FQi_qBw90Rs@g@D@WA$>@&{1ie}!(NH-H0(&j!D^vP9H&F@|z> zMPs%3Gz6g|bVZG`qOx%6eKXoks}nR z>2!D{lDZux<^U>gtgQWEA3n@`NJiDsMMjBI`rvHg(Ne(2$JZtiNX{ZO&} z^<4YwrLI7sJ@CXO^o~8Y3B!|?jTWJGS5EY^Js8s_Z5XZ+LmNi>L6;*mr+700uDJ$U zu<}I)$<)+Q8Of9TO?Q;(ief>sWZ+D@f|~ly z2J1-~cTrFwVf&8o#m;V0@|8sIJ1uuxJ~!t9xr3;HBXHguCxk#dX+??HpcVCm!z<}n zCQ0w!aQLlEBx&aO!r{dPWQ`=T@HDbrVF+c>aG2StP2yMVG)3cxbQ_3-ouoH=ED=@N z3zS0#lq)aUMS3IRU{OSR2Nb9(=qyT<%n}P3C`Zvlv#DN0j)S*4y4s-?>6B?fD&h z@YdGzQS|-i@4WOId$Ysy)$SvXz(&f_=h*dNU_-#;;lS7Q`0bR%=Ge6%AmZe}d!%t! z0<8drCk#;|8$}2jv9eX+hJ``X2>SsvA4qd>OM@%qM2blE1?J|~1rN`4^W3J8XWkoX zn)kv20mBn45Y3@xhBje)@BzB`bkopMHQRjoUoeym$(7YK18*4A^Noh5Xe}Be1uPNu zlNm&HCJ3l^$OI9xT-|>4@DZ~XENW0hRZS696>i1&P+l+rrUv_E-D!*|jt&t1^?62A z9B{C1JPd7jGj1+eRk-~j663mhgLnQUi@@ZA+j}OSXT~R8tsS&#-+~%!(th76u`o6RBbQ%7zf)JnU^s z^Kt!An8|NtfI-Qq1j|VZc1wMSkz;BM!qD~@H);!U#F`2n{JXx`$5ulp&@7l}w)7&YP6On@Z?ITPUMZv5(M>LU`hi@X!9i_1WUkQElkxqxRzQ%i8hFdH%w*WCX=CKmY>(T8neu-&`9>Gmf-T+z?`)rOG%Irgc;hZ zx=_C68WNnXN)mC)H`iaW&o)*p*O(1ZqPebne3nNb3CO2=p)Q1KMsaOhIkP&~Ss_^= zag34BR9%>9q@>+hR|ubTcCdCZ`5R(&-cM<IZzm0wEVGPSl%v&5=V(_MK(u)g z^QYd(%_oZU8Ern3yZJ}?`R{2bzrS@d|BhwdVW5+;;@h~!w2Q*yl|4Ab@ zxBb(>;+Z$KGjHau#|mfU|F7fo()s~bacw@<)^RhE%*b0PnN^73FKWltQ=GV_O( zM+y@Qd|ZKT2DbYFis?Qymf(hfL~1poD*84U2R7fg>|I(00$sX03il7jY48I|{-8A5Q9)9_YM?yk7>Kd9)2gkdt4K{*2I7C?c};#((dNHgGKKF z&3oXXyWkx!dAFm+uAZVlsQH6=|L9sX2LyeZ*tb4d5cl%0UQO&Rh<*IMUlaS+4;I8B z0MFf{51sj*qa24q3@nPBn%G$shc$7yD2{64XyM=+1@T%tL%Vqm zyg`Wu@TL~N#O?Gi;K&YzOl<;b(F|PO;7-d9ZcUVKWuUN=A`8%!u{3#FO&R9AfQ!+A z(%lJV#&nK~;+qkiWh7VflgRk-`w#f6#mnpt5_HoRTq8I&gx`Njl@&zLSTy;HO+EO( zK3MEOto0u*_Mg`JPZ#@N(E4BCC!}hkDtL!V-BPi8OzR%YcOPE6RPydBdiykQU#|ar z!Fz#!a!ZArHlAppe~nQPb|QPy^DfiLH)>2PcTDyV+S=hxZ~ z>Fp*HeGkj~G*V-xuZLWst zU8^V(W(d*Lg^+s=F2A&3n;mE3oP;Y(0^rli(+E7$>q$Qz;sn%BrltMX!OHR|iObc*?1(IhSzRvxjWs z&<{k=Q{4WP8WktEkaPSh%AN<{Xnf{jRWG0Dl;eO9a=&7MW1JKAari8y$g#9?0x?H3 zG}5zNeT{7Mb(ZfzXlP*%pL4Vbp_W6o!`7+=JnqBq6KljL|#=A#2~UyL(K@2Z*_gl@ohFz z($LGsLKytGp|olVFWW>-V`#uSYs|n@e1C{OIZt2`y#2te`L5$@Fa1;hYw#R;;uN}q z?>b9uJBw|5wYI&*wlS@3?BU5==+#`?Sh4N394vxeQf~0*Bk#i>Y+wg1i&i`a;uLqs ztzQX3N8I|@fo3*Dq177GT{cVpbukX^!jHaWnENZ^cwu7<4+F#L*Z==ySX+c#bK zS$8qE>fM|xHOA09rD6$~ zT%^n71nI5yP|(G)zd^QTv-^PFyvf+FJNUt;9~muVn5qC~W>x?*=^Ac$&k~*M;zEL{ zOR>mps7Uy=>49NAq2e@Tg}XvfZxLk44Xs?yi5bl zexqU*@O_2!3b-x(Lg#^!e^1HZU+Netbqto;I`N4+&F=xqY!_O)i@sjX*Sqd1?w`>1 zPvrN%nD?E{iD&r@h6zY$hf7?BmJNQ8l(0xqoT4R)5)}1N^dBjL=qs>06unQ;PbuOI zW0KxJrHIVZY=xo}qFTG@DkV}x!kj`_oZC%L{nqYqOj*8aX>#=Zrqk~j`0B9B@!VJ2 zy^i2l-OY}dEx*~_>F{qH>u^k39w-|EJs&E+<n?J`q@UM(j9%bv z1}j{8MfbjZ`xW&_@D=sNG?N+aieW`2c&CrTwqyQOP6?JFSM9H2f;b9tO#-9KQTM}R zumpG-~UR#%b-Nv&B;_AanItsVp zFx+>@M2Y_Sui4B;xW?DCiHdh*)I<`6Hvh`V`0bG!g!Yo*v`P@q&}{LoA{QQGX>pZ< z_)i)(`qS)h@Ciy3ln9~?o5f;zY_(bJPdtLf_xD26-wVzEC>+s*Bfl0xzY-4qN;ts( z2YxL~{z@4A!rfYQcmA!rGuIW&yARxPer>;LvF!S2=hp%~zh1SSw^}AGxt$}w5%?P) G!v6ze5_c{D literal 0 HcmV?d00001 diff --git a/controllers/platform_controllers/base_controller.py b/controllers/platform_controllers/base_controller.py new file mode 100644 index 0000000..ef5a8c7 --- /dev/null +++ b/controllers/platform_controllers/base_controller.py @@ -0,0 +1,132 @@ +""" +Basis-Controller für Plattform-spezifische Funktionalität. +""" + +import logging +from PyQt5.QtCore import QObject + +from views.tabs.generator_tab import GeneratorTab +from views.tabs.accounts_tab import AccountsTab +from views.tabs.settings_tab import SettingsTab + +class BasePlatformController(QObject): + """Basis-Controller-Klasse für Plattformspezifische Logik.""" + + def __init__(self, platform_name, db_manager, proxy_rotator, email_handler, language_manager=None): + super().__init__() + self.platform_name = platform_name + self.logger = logging.getLogger(f"{platform_name.lower()}_controller") + + # Modelle + self.db_manager = db_manager + self.proxy_rotator = proxy_rotator + self.email_handler = email_handler + self.language_manager = language_manager + + # Tabs + self._generator_tab = None + self._accounts_tab = None + self._settings_tab = None + + # Plattformspezifische Initialisierungen + self.init_platform() + + def init_platform(self): + """ + Initialisiert plattformspezifische Komponenten. + Diese Methode sollte von Unterklassen überschrieben werden. + """ + pass + + def get_generator_tab(self): + """Gibt den Generator-Tab zurück oder erstellt ihn bei Bedarf.""" + if not self._generator_tab: + self._generator_tab = self.create_generator_tab() + return self._generator_tab + + def get_accounts_tab(self): + """Gibt den Accounts-Tab zurück oder erstellt ihn bei Bedarf.""" + if not self._accounts_tab: + self._accounts_tab = self.create_accounts_tab() + return self._accounts_tab + + def get_settings_tab(self): + """Gibt den Settings-Tab zurück oder erstellt ihn bei Bedarf.""" + if not self._settings_tab: + self._settings_tab = self.create_settings_tab() + return self._settings_tab + + + def create_generator_tab(self): + """ + Erstellt den Generator-Tab. + Diese Methode sollte von Unterklassen überschrieben werden. + """ + return GeneratorTab(self.platform_name, self.language_manager) + + def create_accounts_tab(self): + """ + Erstellt den Accounts-Tab. + Diese Methode sollte von Unterklassen überschrieben werden. + """ + return AccountsTab(self.platform_name, self.db_manager, self.language_manager) + + def create_settings_tab(self): + """ + Erstellt den Settings-Tab. + Diese Methode sollte von Unterklassen überschrieben werden. + """ + return SettingsTab( + self.platform_name, + self.proxy_rotator, + self.email_handler, + self.language_manager + ) + + + def start_account_creation(self, params): + """ + Startet die Account-Erstellung. + Diese Methode sollte von Unterklassen überschrieben werden. + + Args: + params: Parameter für die Account-Erstellung + """ + self.logger.info(f"Account-Erstellung für {self.platform_name} gestartet") + # In Unterklassen implementieren + + def validate_inputs(self, inputs): + """ + Validiert die Eingaben für die Account-Erstellung. + + Args: + inputs: Eingaben für die Account-Erstellung + + Returns: + (bool, str): (Ist gültig, Fehlermeldung falls nicht gültig) + """ + # Basis-Validierungen + if not inputs.get("full_name"): + return False, "Bitte geben Sie einen vollständigen Namen ein." + + # Alter prüfen + age_text = inputs.get("age_text", "") + if not age_text: + return False, "Bitte geben Sie ein Alter ein." + + # Alter muss eine Zahl sein + try: + age = int(age_text) + inputs["age"] = age # Füge das konvertierte Alter zu den Parametern hinzu + except ValueError: + return False, "Das Alter muss eine ganze Zahl sein." + + # Alter-Bereich prüfen + if age < 13 or age > 99: + return False, "Das Alter muss zwischen 13 und 99 liegen." + + # Telefonnummer prüfen, falls erforderlich + if inputs.get("registration_method") == "phone" and not inputs.get("phone_number"): + return False, "Telefonnummer erforderlich für Registrierung via Telefon." + + return True, "" \ No newline at end of file diff --git a/controllers/platform_controllers/instagram_controller.py b/controllers/platform_controllers/instagram_controller.py new file mode 100644 index 0000000..eed13b2 --- /dev/null +++ b/controllers/platform_controllers/instagram_controller.py @@ -0,0 +1,275 @@ +""" +Controller für Instagram-spezifische Funktionalität. +Mit TextSimilarity-Integration für robusteres UI-Element-Matching. +""" + +import logging +import time +import random +from PyQt5.QtCore import QThread, pyqtSignal, QObject + +from controllers.platform_controllers.base_controller import BasePlatformController +from views.tabs.generator_tab import GeneratorTab +from views.tabs.accounts_tab import AccountsTab +from views.tabs.settings_tab import SettingsTab + +from social_networks.instagram.instagram_automation import InstagramAutomation +from utils.text_similarity import TextSimilarity + +logger = logging.getLogger("instagram_controller") + +class InstagramWorkerThread(QThread): + """Thread für die Instagram-Account-Erstellung.""" + + # Signale + update_signal = pyqtSignal(str) + log_signal = pyqtSignal(str) + progress_signal = pyqtSignal(int) + finished_signal = pyqtSignal(dict) + error_signal = pyqtSignal(str) + + def __init__(self, params): + super().__init__() + self.params = params + self.running = True + + # TextSimilarity für robustes Fehler-Matching + self.text_similarity = TextSimilarity(default_threshold=0.7) + + # Fehler-Patterns für robustes Fehler-Matching + self.error_patterns = [ + "Fehler", "Error", "Fehlgeschlagen", "Failed", "Problem", "Issue", + "Nicht möglich", "Not possible", "Bitte versuchen Sie es erneut", + "Please try again", "Konnte nicht", "Could not", "Timeout" + ] + + def run(self): + """Führt die Account-Erstellung aus.""" + try: + self.log_signal.emit("Instagram-Account-Erstellung gestartet...") + self.progress_signal.emit(10) + + # Instagram-Automation initialisieren + automation = InstagramAutomation( + headless=self.params.get("headless", False), + use_proxy=self.params.get("use_proxy", False), + proxy_type=self.params.get("proxy_type"), + save_screenshots=True, + debug=self.params.get("debug", False), + email_domain=self.params.get("email_domain", "z5m7q9dk3ah2v1plx6ju.com") + ) + + self.update_signal.emit("Instagram-Automation initialisiert") + self.progress_signal.emit(20) + + # Account registrieren + self.log_signal.emit(f"Registriere Account für: {self.params['full_name']}") + + registration_method = self.params.get("registration_method", "email") + phone_number = self.params.get("phone_number") + + # Account registrieren + result = automation.register_account( + full_name=self.params["full_name"], + age=self.params["age"], + registration_method=registration_method, + phone_number=phone_number, + **self.params.get("additional_params", {}) + ) + + self.progress_signal.emit(100) + + if result["success"]: + self.log_signal.emit("Account erfolgreich erstellt!") + self.finished_signal.emit(result) + else: + # Robuste Fehlerbehandlung mit TextSimilarity + error_msg = result.get("error", "Unbekannter Fehler") + + # Versuche, Fehler nutzerfreundlicher zu interpretieren + user_friendly_error = self._interpret_error(error_msg) + + self.log_signal.emit(f"Fehler bei der Account-Erstellung: {user_friendly_error}") + self.error_signal.emit(user_friendly_error) + + except Exception as e: + logger.error(f"Fehler im Worker-Thread: {e}") + self.log_signal.emit(f"Schwerwiegender Fehler: {str(e)}") + self.error_signal.emit(str(e)) + self.progress_signal.emit(0) + + def _interpret_error(self, error_msg: str) -> str: + """ + Interpretiert Fehlermeldungen und gibt eine benutzerfreundlichere Version zurück. + Verwendet TextSimilarity für robusteres Fehler-Matching. + + Args: + error_msg: Die ursprüngliche Fehlermeldung + + Returns: + str: Benutzerfreundliche Fehlermeldung + """ + # Bekannte Fehlermuster und deren Interpretationen + error_interpretations = { + "captcha": "Instagram hat einen Captcha-Test angefordert. Versuchen Sie es später erneut oder nutzen Sie einen anderen Proxy.", + "verification": "Es gab ein Problem mit der Verifizierung des Accounts. Bitte prüfen Sie die E-Mail-Einstellungen.", + "proxy": "Problem mit der Proxy-Verbindung. Bitte prüfen Sie Ihre Proxy-Einstellungen.", + "timeout": "Zeitüberschreitung bei der Verbindung. Bitte überprüfen Sie Ihre Internetverbindung.", + "username": "Der gewählte Benutzername ist bereits vergeben oder nicht zulässig.", + "password": "Das Passwort erfüllt nicht die Anforderungen von Instagram.", + "email": "Die E-Mail-Adresse konnte nicht verwendet werden. Bitte nutzen Sie eine andere E-Mail-Domain.", + "phone": "Die Telefonnummer konnte nicht für die Registrierung verwendet werden." + } + + # Versuche, den Fehler zu kategorisieren + for pattern, interpretation in error_interpretations.items(): + for error_term in self.error_patterns: + if (pattern in error_msg.lower() or + self.text_similarity.is_similar(error_term, error_msg, threshold=0.7)): + return interpretation + + # Fallback: Originale Fehlermeldung zurückgeben + return error_msg + + def stop(self): + """Stoppt den Thread.""" + self.running = False + self.terminate() + +class InstagramController(BasePlatformController): + """Controller für Instagram-spezifische Funktionalität.""" + + def __init__(self, db_manager, proxy_rotator, email_handler, language_manager=None): + super().__init__("Instagram", db_manager, proxy_rotator, email_handler, language_manager) + self.worker_thread = None + + # TextSimilarity für robustes UI-Element-Matching + self.text_similarity = TextSimilarity(default_threshold=0.75) + + def create_generator_tab(self): + """Erstellt den Instagram-Generator-Tab.""" + generator_tab = GeneratorTab(self.platform_name, self.language_manager) + + # Instagram-spezifische Anpassungen + # Diese Methode überschreiben, wenn spezifische Anpassungen benötigt werden + + # Signale verbinden + generator_tab.start_requested.connect(self.start_account_creation) + generator_tab.stop_requested.connect(self.stop_account_creation) + + return generator_tab + + def start_account_creation(self, params): + """Startet die Instagram-Account-Erstellung.""" + super().start_account_creation(params) + + # Validiere Eingaben + is_valid, error_msg = self.validate_inputs(params) + if not is_valid: + self.get_generator_tab().show_error(error_msg) + return + + # UI aktualisieren + generator_tab = self.get_generator_tab() + generator_tab.set_running(True) + generator_tab.clear_log() + generator_tab.set_progress(0) + + # Worker-Thread starten + self.worker_thread = InstagramWorkerThread(params) + self.worker_thread.update_signal.connect(lambda msg: generator_tab.set_status(msg)) + self.worker_thread.log_signal.connect(lambda msg: generator_tab.add_log(msg)) + self.worker_thread.error_signal.connect(lambda msg: (generator_tab.show_error(msg), generator_tab.set_running(False))) + self.worker_thread.finished_signal.connect(lambda result: self.handle_account_created(result)) + self.worker_thread.progress_signal.connect(lambda value: generator_tab.set_progress(value)) + + self.worker_thread.start() + + def stop_account_creation(self): + """Stoppt die Instagram-Account-Erstellung.""" + if self.worker_thread and self.worker_thread.isRunning(): + self.worker_thread.stop() + generator_tab = self.get_generator_tab() + generator_tab.add_log("Account-Erstellung wurde abgebrochen") + generator_tab.set_running(False) + generator_tab.set_progress(0) + + def handle_account_created(self, result): + """Verarbeitet erfolgreich erstellte Accounts.""" + generator_tab = self.get_generator_tab() + generator_tab.set_running(False) + + # Account-Daten aus dem Ergebnis holen + account_data = result.get("account_data", {}) + + # Account-Erfolgsereignis auslösen + generator_tab.account_created.emit(self.platform_name, account_data) + + # Account in der Datenbank speichern + self.save_account_to_db(account_data) + + def save_account_to_db(self, account_data): + """Speichert einen erstellten Account in der Datenbank.""" + account = { + "platform": self.platform_name.lower(), + "username": account_data.get("username", ""), + "password": account_data.get("password", ""), + "email": account_data.get("email", ""), + "phone": account_data.get("phone", ""), + "full_name": account_data.get("full_name", ""), + "created_at": time.strftime("%Y-%m-%d %H:%M:%S") + } + + self.db_manager.add_account(account) + logger.info(f"Account in Datenbank gespeichert: {account['username']}") + + def validate_inputs(self, inputs): + """ + Validiert die Eingaben für die Account-Erstellung. + Verwendet TextSimilarity für robustere Validierung. + """ + # Basis-Validierungen von BasePlatformController verwenden + valid, error_msg = super().validate_inputs(inputs) + if not valid: + return valid, error_msg + + # Instagram-spezifische Validierungen + age = inputs.get("age", 0) + if age < 13: # Änderung von 14 auf 13 + return False, "Das Alter muss mindestens 13 sein (Instagram-Anforderung)." + + # E-Mail-Domain-Validierung + if inputs.get("registration_method") == "email": + email_domain = inputs.get("email_domain", "") + # Blacklist von bekannten problematischen Domains + blacklisted_domains = ["temp-mail.org", "guerrillamail.com", "maildrop.cc"] + + # Prüfe mit TextSimilarity auf Ähnlichkeit mit Blacklist + for domain in blacklisted_domains: + if self.text_similarity.is_similar(email_domain, domain, threshold=0.8): + return False, f"Die E-Mail-Domain '{email_domain}' kann problematisch für die Instagram-Registrierung sein. Bitte verwenden Sie eine andere Domain." + + return True, "" + + def get_form_field_label(self, field_type: str) -> str: + """ + Gibt einen Label-Text für ein Formularfeld basierend auf dem Feldtyp zurück. + + Args: + field_type: Typ des Formularfelds + + Returns: + str: Label-Text für das Formularfeld + """ + # Mapping von Feldtypen zu Labels + field_labels = { + "full_name": "Vollständiger Name", + "username": "Benutzername", + "password": "Passwort", + "email": "E-Mail-Adresse", + "phone": "Telefonnummer", + "age": "Alter", + "birthday": "Geburtsdatum" + } + + return field_labels.get(field_type, field_type.capitalize()) \ No newline at end of file diff --git a/controllers/platform_controllers/tiktok_controller.py b/controllers/platform_controllers/tiktok_controller.py new file mode 100644 index 0000000..429e074 --- /dev/null +++ b/controllers/platform_controllers/tiktok_controller.py @@ -0,0 +1,277 @@ +""" +Controller für TikTok-spezifische Funktionalität. +Mit TextSimilarity-Integration für robusteres UI-Element-Matching. +""" + +import logging +import time +import random +from PyQt5.QtCore import QThread, pyqtSignal, QObject + +from controllers.platform_controllers.base_controller import BasePlatformController +from views.tabs.generator_tab import GeneratorTab +from views.tabs.accounts_tab import AccountsTab +from views.tabs.settings_tab import SettingsTab + +from social_networks.tiktok.tiktok_automation import TikTokAutomation +from utils.text_similarity import TextSimilarity + +logger = logging.getLogger("tiktok_controller") + +class TikTokWorkerThread(QThread): + """Thread für die TikTok-Account-Erstellung.""" + + # Signale + update_signal = pyqtSignal(str) + log_signal = pyqtSignal(str) + progress_signal = pyqtSignal(int) + finished_signal = pyqtSignal(dict) + error_signal = pyqtSignal(str) + + def __init__(self, params): + super().__init__() + self.params = params + self.running = True + + # TextSimilarity für robustes Fehler-Matching + self.text_similarity = TextSimilarity(default_threshold=0.7) + + # Fehler-Patterns für robustes Fehler-Matching + self.error_patterns = [ + "Fehler", "Error", "Fehlgeschlagen", "Failed", "Problem", "Issue", + "Nicht möglich", "Not possible", "Bitte versuchen Sie es erneut", + "Please try again", "Konnte nicht", "Could not", "Timeout" + ] + + def run(self): + """Führt die Account-Erstellung aus.""" + try: + self.log_signal.emit("TikTok-Account-Erstellung gestartet...") + self.progress_signal.emit(10) + + # TikTok-Automation initialisieren + automation = TikTokAutomation( + headless=self.params.get("headless", False), + use_proxy=self.params.get("use_proxy", False), + proxy_type=self.params.get("proxy_type"), + save_screenshots=True, + debug=self.params.get("debug", False), + email_domain=self.params.get("email_domain", "z5m7q9dk3ah2v1plx6ju.com") + ) + + self.update_signal.emit("TikTok-Automation initialisiert") + self.progress_signal.emit(20) + + # Account registrieren + self.log_signal.emit(f"Registriere Account für: {self.params['full_name']}") + + registration_method = self.params.get("registration_method", "email") + phone_number = self.params.get("phone_number") + + # Account registrieren + result = automation.register_account( + full_name=self.params["full_name"], + age=self.params["age"], + registration_method=registration_method, + phone_number=phone_number, + **self.params.get("additional_params", {}) + ) + + self.progress_signal.emit(100) + + if result["success"]: + self.log_signal.emit("Account erfolgreich erstellt!") + self.finished_signal.emit(result) + else: + # Robuste Fehlerbehandlung mit TextSimilarity + error_msg = result.get("error", "Unbekannter Fehler") + + # Versuche, Fehler nutzerfreundlicher zu interpretieren + user_friendly_error = self._interpret_error(error_msg) + + self.log_signal.emit(f"Fehler bei der Account-Erstellung: {user_friendly_error}") + self.error_signal.emit(user_friendly_error) + + except Exception as e: + logger.error(f"Fehler im Worker-Thread: {e}") + self.log_signal.emit(f"Schwerwiegender Fehler: {str(e)}") + self.error_signal.emit(str(e)) + self.progress_signal.emit(0) + + def _interpret_error(self, error_msg: str) -> str: + """ + Interpretiert Fehlermeldungen und gibt eine benutzerfreundlichere Version zurück. + Verwendet TextSimilarity für robusteres Fehler-Matching. + + Args: + error_msg: Die ursprüngliche Fehlermeldung + + Returns: + str: Benutzerfreundliche Fehlermeldung + """ + # Bekannte Fehlermuster und deren Interpretationen + error_interpretations = { + "captcha": "TikTok hat einen Captcha-Test angefordert. Versuchen Sie es später erneut oder nutzen Sie einen anderen Proxy.", + "verification": "Es gab ein Problem mit der Verifizierung des Accounts. Bitte prüfen Sie die E-Mail-Einstellungen.", + "proxy": "Problem mit der Proxy-Verbindung. Bitte prüfen Sie Ihre Proxy-Einstellungen.", + "timeout": "Zeitüberschreitung bei der Verbindung. Bitte überprüfen Sie Ihre Internetverbindung.", + "username": "Der gewählte Benutzername ist bereits vergeben oder nicht zulässig.", + "password": "Das Passwort erfüllt nicht die Anforderungen von TikTok.", + "email": "Die E-Mail-Adresse konnte nicht verwendet werden. Bitte nutzen Sie eine andere E-Mail-Domain.", + "phone": "Die Telefonnummer konnte nicht für die Registrierung verwendet werden.", + "age": "Das eingegebene Alter erfüllt nicht die Anforderungen von TikTok.", + "too_many_attempts": "Zu viele Registrierungsversuche. Bitte warten Sie und versuchen Sie es später erneut." + } + + # Versuche, den Fehler zu kategorisieren + for pattern, interpretation in error_interpretations.items(): + for error_term in self.error_patterns: + if (pattern in error_msg.lower() or + self.text_similarity.is_similar(error_term, error_msg, threshold=0.7)): + return interpretation + + # Fallback: Originale Fehlermeldung zurückgeben + return error_msg + + def stop(self): + """Stoppt den Thread.""" + self.running = False + self.terminate() + +class TikTokController(BasePlatformController): + """Controller für TikTok-spezifische Funktionalität.""" + + def __init__(self, db_manager, proxy_rotator, email_handler, language_manager=None): + super().__init__("TikTok", db_manager, proxy_rotator, email_handler, language_manager) + self.worker_thread = None + + # TextSimilarity für robustes UI-Element-Matching + self.text_similarity = TextSimilarity(default_threshold=0.75) + + def create_generator_tab(self): + """Erstellt den TikTok-Generator-Tab.""" + generator_tab = GeneratorTab(self.platform_name, self.language_manager) + + # TikTok-spezifische Anpassungen + # Diese Methode überschreiben, wenn spezifische Anpassungen benötigt werden + + # Signale verbinden + generator_tab.start_requested.connect(self.start_account_creation) + generator_tab.stop_requested.connect(self.stop_account_creation) + + return generator_tab + + def start_account_creation(self, params): + """Startet die TikTok-Account-Erstellung.""" + super().start_account_creation(params) + + # Validiere Eingaben + is_valid, error_msg = self.validate_inputs(params) + if not is_valid: + self.get_generator_tab().show_error(error_msg) + return + + # UI aktualisieren + generator_tab = self.get_generator_tab() + generator_tab.set_running(True) + generator_tab.clear_log() + generator_tab.set_progress(0) + + # Worker-Thread starten + self.worker_thread = TikTokWorkerThread(params) + self.worker_thread.update_signal.connect(lambda msg: generator_tab.set_status(msg)) + self.worker_thread.log_signal.connect(lambda msg: generator_tab.add_log(msg)) + self.worker_thread.error_signal.connect(lambda msg: (generator_tab.show_error(msg), generator_tab.set_running(False))) + self.worker_thread.finished_signal.connect(lambda result: self.handle_account_created(result)) + self.worker_thread.progress_signal.connect(lambda value: generator_tab.set_progress(value)) + + self.worker_thread.start() + + def stop_account_creation(self): + """Stoppt die TikTok-Account-Erstellung.""" + if self.worker_thread and self.worker_thread.isRunning(): + self.worker_thread.stop() + generator_tab = self.get_generator_tab() + generator_tab.add_log("Account-Erstellung wurde abgebrochen") + generator_tab.set_running(False) + generator_tab.set_progress(0) + + def handle_account_created(self, result): + """Verarbeitet erfolgreich erstellte Accounts.""" + generator_tab = self.get_generator_tab() + generator_tab.set_running(False) + + # Account-Daten aus dem Ergebnis holen + account_data = result.get("account_data", {}) + + # Account-Erfolgsereignis auslösen + generator_tab.account_created.emit(self.platform_name, account_data) + + # Account in der Datenbank speichern + self.save_account_to_db(account_data) + + def save_account_to_db(self, account_data): + """Speichert einen erstellten Account in der Datenbank.""" + account = { + "platform": self.platform_name.lower(), + "username": account_data.get("username", ""), + "password": account_data.get("password", ""), + "email": account_data.get("email", ""), + "phone": account_data.get("phone", ""), + "full_name": account_data.get("full_name", ""), + "created_at": time.strftime("%Y-%m-%d %H:%M:%S") + } + + self.db_manager.add_account(account) + logger.info(f"Account in Datenbank gespeichert: {account['username']}") + + def validate_inputs(self, inputs): + """ + Validiert die Eingaben für die Account-Erstellung. + Verwendet TextSimilarity für robustere Validierung. + """ + # Basis-Validierungen von BasePlatformController verwenden + valid, error_msg = super().validate_inputs(inputs) + if not valid: + return valid, error_msg + + # TikTok-spezifische Validierungen + age = inputs.get("age", 0) + if age < 13: + return False, "Das Alter muss mindestens 13 sein (TikTok-Anforderung)." + + # E-Mail-Domain-Validierung + if inputs.get("registration_method") == "email": + email_domain = inputs.get("email_domain", "") + # Blacklist von bekannten problematischen Domains + blacklisted_domains = ["temp-mail.org", "guerrillamail.com", "maildrop.cc"] + + # Prüfe mit TextSimilarity auf Ähnlichkeit mit Blacklist + for domain in blacklisted_domains: + if self.text_similarity.is_similar(email_domain, domain, threshold=0.8): + return False, f"Die E-Mail-Domain '{email_domain}' kann problematisch für die TikTok-Registrierung sein. Bitte verwenden Sie eine andere Domain." + + return True, "" + + def get_form_field_label(self, field_type: str) -> str: + """ + Gibt einen Label-Text für ein Formularfeld basierend auf dem Feldtyp zurück. + + Args: + field_type: Typ des Formularfelds + + Returns: + str: Label-Text für das Formularfeld + """ + # Mapping von Feldtypen zu Labels + field_labels = { + "full_name": "Vollständiger Name", + "username": "Benutzername", + "password": "Passwort", + "email": "E-Mail-Adresse", + "phone": "Telefonnummer", + "age": "Alter", + "birthday": "Geburtsdatum" + } + + return field_labels.get(field_type, field_type.capitalize()) \ No newline at end of file diff --git a/controllers/settings_controller.py b/controllers/settings_controller.py new file mode 100644 index 0000000..bc443f9 --- /dev/null +++ b/controllers/settings_controller.py @@ -0,0 +1,294 @@ +""" +Controller für die Verwaltung von Einstellungen. +""" + +import logging +import random +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtCore import QObject + +logger = logging.getLogger("settings_controller") + +class SettingsController(QObject): + """Controller für die Verwaltung von Einstellungen.""" + + def __init__(self, proxy_rotator, email_handler, license_manager): + super().__init__() + self.proxy_rotator = proxy_rotator + self.email_handler = email_handler + self.license_manager = license_manager + self.parent_view = None + + def set_parent_view(self, view): + """Setzt die übergeordnete View für Dialoge.""" + self.parent_view = view + + def load_proxy_settings(self): + """Lädt die Proxy-Einstellungen.""" + try: + proxy_config = self.proxy_rotator.get_config() or {} + + settings = { + "ipv4_proxies": proxy_config.get("ipv4", []), + "ipv6_proxies": proxy_config.get("ipv6", []), + "mobile_proxies": proxy_config.get("mobile", []), + "mobile_api": proxy_config.get("mobile_api", {}) + } + + return settings + except Exception as e: + logger.error(f"Fehler beim Laden der Proxy-Einstellungen: {e}") + return {} + + def save_proxy_settings(self, settings): + """Speichert die Proxy-Einstellungen.""" + try: + # IPv4 Proxies + ipv4_proxies = settings.get("ipv4_proxies", []) + if isinstance(ipv4_proxies, str): + ipv4_proxies = [line.strip() for line in ipv4_proxies.splitlines() if line.strip()] + + # IPv6 Proxies + ipv6_proxies = settings.get("ipv6_proxies", []) + if isinstance(ipv6_proxies, str): + ipv6_proxies = [line.strip() for line in ipv6_proxies.splitlines() if line.strip()] + + # Mobile Proxies + mobile_proxies = settings.get("mobile_proxies", []) + if isinstance(mobile_proxies, str): + mobile_proxies = [line.strip() for line in mobile_proxies.splitlines() if line.strip()] + + # API Keys + mobile_api = settings.get("mobile_api", {}) + + # Konfiguration aktualisieren + self.proxy_rotator.update_config({ + "ipv4": ipv4_proxies, + "ipv6": ipv6_proxies, + "mobile": mobile_proxies, + "mobile_api": mobile_api + }) + + logger.info("Proxy-Einstellungen gespeichert") + + if self.parent_view: + QMessageBox.information( + self.parent_view, + "Erfolg", + "Proxy-Einstellungen wurden gespeichert." + ) + + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Proxy-Einstellungen: {e}") + + if self.parent_view: + QMessageBox.critical( + self.parent_view, + "Fehler", + f"Proxy-Einstellungen konnten nicht gespeichert werden:\n{str(e)}" + ) + + return False + + def test_proxy(self, proxy_type): + """Testet einen zufälligen Proxy des ausgewählten Typs.""" + try: + # Überprüfe, ob Proxies konfiguriert sind + proxies = self.proxy_rotator.get_proxies_by_type(proxy_type) + if not proxies: + if self.parent_view: + QMessageBox.warning( + self.parent_view, + "Keine Proxies", + f"Keine {proxy_type.upper()}-Proxies konfiguriert.\nBitte fügen Sie Proxies in den Einstellungen hinzu." + ) + return False + + # Zufälligen Proxy auswählen + proxy = random.choice(proxies) + + # Proxy testen + result = self.proxy_rotator.test_proxy(proxy_type) + + if result["success"]: + if self.parent_view: + QMessageBox.information( + self.parent_view, + "Proxy-Test erfolgreich", + f"IP: {result['ip']}\nLand: {result['country'] or 'Unbekannt'}\nAntwortzeit: {result['response_time']:.2f}s" + ) + return True + else: + if self.parent_view: + QMessageBox.warning( + self.parent_view, + "Proxy-Test fehlgeschlagen", + f"Fehler: {result['error']}" + ) + return False + + except Exception as e: + logger.error(f"Fehler beim Testen des Proxy: {e}") + + if self.parent_view: + QMessageBox.critical( + self.parent_view, + "Fehler", + f"Fehler beim Testen des Proxy:\n{str(e)}" + ) + + return False + + def load_email_settings(self): + """Lädt die E-Mail-Einstellungen.""" + try: + email_config = self.email_handler.get_config() or {} + + settings = { + "imap_server": email_config.get("imap_server", ""), + "imap_port": email_config.get("imap_port", 993), + "imap_user": email_config.get("imap_user", ""), + "imap_pass": email_config.get("imap_pass", "") + } + + return settings + except Exception as e: + logger.error(f"Fehler beim Laden der E-Mail-Einstellungen: {e}") + return {} + + def save_email_settings(self, settings): + """Speichert die E-Mail-Einstellungen.""" + try: + # Einstellungen aktualisieren + self.email_handler.update_config(settings) + + logger.info("E-Mail-Einstellungen gespeichert") + + if self.parent_view: + QMessageBox.information( + self.parent_view, + "Erfolg", + "E-Mail-Einstellungen wurden gespeichert." + ) + + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der E-Mail-Einstellungen: {e}") + + if self.parent_view: + QMessageBox.critical( + self.parent_view, + "Fehler", + f"E-Mail-Einstellungen konnten nicht gespeichert werden:\n{str(e)}" + ) + + return False + + def test_email(self, settings=None): + """Testet die E-Mail-Verbindung.""" + try: + if settings: + # Temporär Einstellungen aktualisieren + self.email_handler.update_credentials( + settings.get("imap_user", ""), + settings.get("imap_pass", "") + ) + self.email_handler.update_server( + settings.get("imap_server", ""), + settings.get("imap_port", 993) + ) + + # Verbindung testen + result = self.email_handler.test_connection() + + if result["success"]: + if self.parent_view: + QMessageBox.information( + self.parent_view, + "E-Mail-Test erfolgreich", + f"Verbindung zu {result['server']}:{result['port']} hergestellt.\nGefundene Postfächer: {result['mailbox_count']}" + ) + return True + else: + if self.parent_view: + QMessageBox.warning( + self.parent_view, + "E-Mail-Test fehlgeschlagen", + f"Fehler: {result['error']}" + ) + return False + + except Exception as e: + logger.error(f"Fehler beim Testen der E-Mail-Verbindung: {e}") + + if self.parent_view: + QMessageBox.critical( + self.parent_view, + "Fehler", + f"Fehler beim Testen der E-Mail-Verbindung:\n{str(e)}" + ) + + return False + + def load_license_info(self): + """Lädt die Lizenzinformationen.""" + try: + license_info = self.license_manager.get_license_info() + return license_info + except Exception as e: + logger.error(f"Fehler beim Laden der Lizenzinformationen: {e}") + return {} + + def activate_license(self, license_key): + """Aktiviert eine Lizenz.""" + try: + success, message = self.license_manager.activate_license(license_key) + + if success: + if self.parent_view: + QMessageBox.information( + self.parent_view, + "Lizenz aktiviert", + message + ) + else: + if self.parent_view: + QMessageBox.warning( + self.parent_view, + "Lizenzaktivierung fehlgeschlagen", + message + ) + + return success, message + except Exception as e: + logger.error(f"Fehler bei der Lizenzaktivierung: {e}") + + if self.parent_view: + QMessageBox.critical( + self.parent_view, + "Fehler", + f"Fehler bei der Lizenzaktivierung:\n{str(e)}" + ) + + return False, str(e) + + def check_license(self): + """Überprüft, ob eine gültige Lizenz vorhanden ist.""" + try: + is_licensed = self.license_manager.is_licensed() + + if not is_licensed and self.parent_view: + license_info = self.license_manager.get_license_info() + status = license_info.get("status_text", "Inaktiv") + + QMessageBox.warning( + self.parent_view, + "Keine gültige Lizenz", + f"Status: {status}\n\nBitte aktivieren Sie eine Lizenz, um die Software zu nutzen." + ) + + return is_licensed + except Exception as e: + logger.error(f"Fehler bei der Lizenzprüfung: {e}") + return False diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/database/__pycache__/__init__.cpython-310.pyc b/database/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34bdd01194359690962c99c533426b15b1d8ae7b GIT binary patch literal 146 zcmd1j<>g`kf}c?V86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6w%*(xTqIJKxa zCL=X3r6|TFwK%&ZzaXY0wYWq#H!(9WCMB^XF)6V)H6}hjGcU6wK3=b&@)n0pZhlH> PPO2TqxMC(C!NLFl%HtuX literal 0 HcmV?d00001 diff --git a/database/__pycache__/__init__.cpython-313.pyc b/database/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f0ddea5ab9cb896f68fe1c0666a6b8db85fddeb GIT binary patch literal 145 zcmey&%ge<81pH6>P{wB#AY&>+I)f&o-%5reCLr%KNa~hBfmMuiMrLke zW>I2{bAC#yZa_|AWqDC%dPYf1N@7W3QetsxOniK1US>&ryk0@&Ee@O9{FKt1RJ$Tp UpaCEYib0Hz%#4hTMa)1J0H^~YumAu6 literal 0 HcmV?d00001 diff --git a/database/__pycache__/db_manager.cpython-310.pyc b/database/__pycache__/db_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6dc85ed17df8c8f996fa13b26f57169588f14f10 GIT binary patch literal 13791 zcmb_jOKcoRdhXZE^vv)jO4iG=+?Gv~V@s61w%1w4Yl#*qn`?^Fij=jQ$UA6G^^il( zgR1T}myMyYSo^RDl0!}bvM@jeiv+#|K@co*@F9T?x#SS!5}*U*k^q6viy(*)c=P?$ zkLeyZDap_fQ`3*C>guZh`~Fw4J3L&H@cG~BjrIS#BuW24C&`bBlgs$pEfh?uN=#-- zORmb|SE(xESFNhzSF39HRa?1su9}mnuGY%83)Mnfuj=h$wb(9IOYL&C+#ae9$&#h* z4YS+>xjOQp#PY20g~SSrJy|T(elyG-m7duG?{p53P>HO{dGJOW&Zmv{aFOouLn>*|sWqUz=;z9ba2)+K#Wz zc6NOI`j*q|cFdNaTXDBqmY=)T!MR^ztF`cvb?PYjp((qJue~Zuk|kj>B$^0J!s|@6 zw1&bomiq!zz|J$B6~EA`c~-ItmS*V<_PM{s9^ja%BjyBa&nj^Xa8==HgNOVk`|Mlq*>t_aUPJqntN>A%_D3NG=BjC1*8(YUA*<5IEz`Cw z!*jXuiN&|gmg9ETAR63=g(i@eP&h6ubQ)dWHfg=B&h(>WkPFf2MKh2ddz!8K1uXoQ z>8#fkJRqi}>X#*K!_#QVoDe1VRdaqX3_ApX|R zAnmp8=29ikhcI?u>Ds>DHaCGBZu=U&End6HJ7(MR3r&o%##Sqeuh~|sA)0(8@#oO; z(Yse>?%uL3Zr@$EI*i|)v+PZ$yLH#GZ0ADTY| z-Rhh-wyjRbfM^(2@l0q`|Bz3od1W@pVlKw37fonR6YvXcFZpP4OpV7d2=V_={sV(v1CA!2s)Dp#%b z7RY?nYNkm&Sq+RoV=R4D_6zpCR@1TG@(cBDr(@Nffa>yfm)l+L7p$LIb=R@{e7)Ol zHyuA$Z*^^)V$on#_*xUX!OzWej>5( zP?RBAmrH7CTrcSIm@+1xmGz%1zw_uFlj>_)a3Pe

9nS(Pm2Hl+j;Ibf-aq-qson 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 L_m>o=r1}2?ZnD;9 literal 0 HcmV?d00001 diff --git a/database/account_repository.py b/database/account_repository.py new file mode 100644 index 0000000..e69de29 diff --git a/database/accounts.db b/database/accounts.db new file mode 100644 index 0000000000000000000000000000000000000000..0af961be9f19713c268a1587f173d1cbd1f864ba GIT binary patch literal 20480 zcmeI&&uZH+0LO7V$+}T$=q#8m5*>&G>uC31Ajph7te(&8llXYRPBY*$`2q1s}0tg_000PYd-=DT0 zcY8hiN88AcvC_(zD7nye**PA&1K$(M$|I-+kkb?>dRW#6KN+c}FL%=Qpb@Vrhyk<|x|}^E ztL8F_%V6_!7oXCkdT8_g^DK^o{{w5zgi9rjng#h*x?E265@}AyQkx)7FQR0_u#(g$ zz2SbCrkByigf`M-RTuY{s#7ex{o;oM0R#|0009ILKmY**5I_I{1h!Rx{r|SUS>}ZR z0tg_000IagfB*srAb`NF0Q>)23u*`;fB*srAb None: + """Initialisiert die Datenbank und erstellt die benötigten Tabellen, wenn sie nicht existieren.""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Accounts-Tabelle erstellen + cursor.execute(''' + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + platform TEXT NOT NULL, + username TEXT NOT NULL, + password TEXT NOT NULL, + email TEXT, + phone TEXT, + full_name TEXT, + created_at TEXT NOT NULL, + last_login TEXT, + notes TEXT, + cookies TEXT, + status TEXT + ) + ''') + + # Settings-Tabelle erstellen + cursor.execute(''' + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + ''') + + conn.commit() + conn.close() + + logger.info("Datenbank initialisiert") + except sqlite3.Error as e: + logger.error(f"Fehler bei der Datenbankinitialisierung: {e}") + + def add_account(self, account_data: Dict[str, Any]) -> int: + """ + Fügt einen Account zur Datenbank hinzu. + + Args: + account_data: Dictionary mit Account-Daten + + Returns: + ID des hinzugefügten Accounts oder -1 im Fehlerfall + """ + try: + # Prüfe, ob erforderliche Felder vorhanden sind + required_fields = ["platform", "username", "password"] + for field in required_fields: + if field not in account_data: + logger.error(f"Fehlendes Pflichtfeld: {field}") + return -1 + + # Sicherstellen, dass created_at vorhanden ist + if "created_at" not in account_data: + account_data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # SQL-Anweisung vorbereiten + fields = ", ".join(account_data.keys()) + placeholders = ", ".join(["?" for _ in account_data]) + + query = f"INSERT INTO accounts ({fields}) VALUES ({placeholders})" + + # Anweisung ausführen + cursor.execute(query, list(account_data.values())) + + # ID des hinzugefügten Datensatzes abrufen + account_id = cursor.lastrowid + + conn.commit() + conn.close() + + logger.info(f"Account hinzugefügt: {account_data['username']} (ID: {account_id})") + + return account_id + except sqlite3.Error as e: + logger.error(f"Fehler beim Hinzufügen des Accounts: {e}") + return -1 + + def get_account(self, account_id: int) -> Optional[Dict[str, Any]]: + """ + Gibt einen Account anhand seiner ID zurück. + + Args: + account_id: ID des Accounts + + Returns: + Dictionary mit Account-Daten oder None, wenn der Account nicht gefunden wurde + """ + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row # Für dict-like Zugriff auf Zeilen + cursor = conn.cursor() + + cursor.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)) + row = cursor.fetchone() + + conn.close() + + if row: + # Konvertiere Row in Dictionary + account = dict(row) + logger.debug(f"Account gefunden: {account['username']} (ID: {account_id})") + return account + else: + logger.warning(f"Account nicht gefunden: ID {account_id}") + return None + + except sqlite3.Error as e: + logger.error(f"Fehler beim Abrufen des Accounts: {e}") + return None + + def get_all_accounts(self) -> List[Dict[str, Any]]: + """ + Gibt alle Accounts zurück. + + Returns: + Liste von Dictionaries mit Account-Daten + """ + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute("SELECT * FROM accounts ORDER BY id DESC") + rows = cursor.fetchall() + + conn.close() + + # Konvertiere Rows in Dictionaries + accounts = [dict(row) for row in rows] + + logger.info(f"{len(accounts)} Accounts abgerufen") + + return accounts + except sqlite3.Error as e: + logger.error(f"Fehler beim Abrufen aller Accounts: {e}") + return [] + + def get_accounts_by_platform(self, platform: str) -> List[Dict[str, Any]]: + """ + Gibt alle Accounts einer bestimmten Plattform zurück. + + Args: + platform: Plattformname (z.B. "instagram") + + Returns: + Liste von Dictionaries mit Account-Daten + """ + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute("SELECT * FROM accounts WHERE platform = ? ORDER BY id DESC", (platform.lower(),)) + rows = cursor.fetchall() + + conn.close() + + # Konvertiere Rows in Dictionaries + accounts = [dict(row) for row in rows] + + logger.info(f"{len(accounts)} Accounts für Plattform '{platform}' abgerufen") + + return accounts + except sqlite3.Error as e: + logger.error(f"Fehler beim Abrufen der Accounts für Plattform '{platform}': {e}") + return [] + + def update_account(self, account_id: int, update_data: Dict[str, Any]) -> bool: + """ + Aktualisiert einen Account in der Datenbank. + + Args: + account_id: ID des zu aktualisierenden Accounts + update_data: Dictionary mit zu aktualisierenden Feldern + + Returns: + True bei Erfolg, False im Fehlerfall + """ + if not update_data: + logger.warning("Keine Aktualisierungsdaten bereitgestellt") + return False + + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # SQL-Anweisung vorbereiten + set_clause = ", ".join([f"{field} = ?" for field in update_data.keys()]) + values = list(update_data.values()) + values.append(account_id) + + query = f"UPDATE accounts SET {set_clause} WHERE id = ?" + + # Anweisung ausführen + cursor.execute(query, values) + + conn.commit() + conn.close() + + logger.info(f"Account aktualisiert: ID {account_id}") + + return True + except sqlite3.Error as e: + logger.error(f"Fehler beim Aktualisieren des Accounts: {e}") + return False + + def delete_account(self, account_id: int) -> bool: + """ + Löscht einen Account aus der Datenbank. + + Args: + account_id: ID des zu löschenden Accounts + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute("DELETE FROM accounts WHERE id = ?", (account_id,)) + + conn.commit() + conn.close() + + logger.info(f"Account gelöscht: ID {account_id}") + + return True + except sqlite3.Error as e: + logger.error(f"Fehler beim Löschen des Accounts: {e}") + return False + + def search_accounts(self, query: str, platform: Optional[str] = None) -> List[Dict[str, Any]]: + """ + Sucht nach Accounts in der Datenbank. + + Args: + query: Suchbegriff + platform: Optional, Plattform für die Einschränkung der Suche + + Returns: + Liste von Dictionaries mit gefundenen Account-Daten + """ + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # Suchbegriff für LIKE-Operator vorbereiten + search_term = f"%{query}%" + + if platform: + query_sql = """ + SELECT * FROM accounts + WHERE (username LIKE ? OR email LIKE ? OR phone LIKE ? OR full_name LIKE ?) + AND platform = ? + ORDER BY id DESC + """ + cursor.execute(query_sql, (search_term, search_term, search_term, search_term, platform.lower())) + else: + query_sql = """ + SELECT * FROM accounts + WHERE username LIKE ? OR email LIKE ? OR phone LIKE ? OR full_name LIKE ? + ORDER BY id DESC + """ + cursor.execute(query_sql, (search_term, search_term, search_term, search_term)) + + rows = cursor.fetchall() + + conn.close() + + # Konvertiere Rows in Dictionaries + accounts = [dict(row) for row in rows] + + logger.info(f"{len(accounts)} Accounts gefunden für Suchbegriff '{query}'") + + return accounts + except sqlite3.Error as e: + logger.error(f"Fehler bei der Suche nach Accounts: {e}") + return [] + + def get_account_count(self, platform: Optional[str] = None) -> int: + """ + Gibt die Anzahl der Accounts zurück. + + Args: + platform: Optional, Plattform für die Einschränkung der Zählung + + Returns: + Anzahl der Accounts + """ + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + if platform: + cursor.execute("SELECT COUNT(*) FROM accounts WHERE platform = ?", (platform.lower(),)) + else: + cursor.execute("SELECT COUNT(*) FROM accounts") + + count = cursor.fetchone()[0] + + conn.close() + + return count + except sqlite3.Error as e: + logger.error(f"Fehler beim Zählen der Accounts: {e}") + return 0 + + def get_setting(self, key: str, default: Any = None) -> Any: + """ + Gibt einen Einstellungswert zurück. + + Args: + key: Schlüssel der Einstellung + default: Standardwert, falls die Einstellung nicht gefunden wurde + + Returns: + Wert der Einstellung oder der Standardwert + """ + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute("SELECT value FROM settings WHERE key = ?", (key,)) + row = cursor.fetchone() + + conn.close() + + if row: + # Versuche, den Wert als JSON zu parsen + try: + return json.loads(row[0]) + except json.JSONDecodeError: + # Wenn kein gültiges JSON, gib den Rohwert zurück + return row[0] + else: + return default + + except sqlite3.Error as e: + logger.error(f"Fehler beim Abrufen der Einstellung '{key}': {e}") + return default + + def set_setting(self, key: str, value: Any) -> bool: + """ + Setzt einen Einstellungswert. + + Args: + key: Schlüssel der Einstellung + value: Wert der Einstellung (wird als JSON gespeichert, wenn es kein String ist) + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Wert als JSON speichern, wenn es kein String ist + if not isinstance(value, str): + value = json.dumps(value) + + # Prüfen, ob die Einstellung bereits existiert + cursor.execute("SELECT COUNT(*) FROM settings WHERE key = ?", (key,)) + exists = cursor.fetchone()[0] > 0 + + if exists: + cursor.execute("UPDATE settings SET value = ? WHERE key = ?", (value, key)) + else: + cursor.execute("INSERT INTO settings (key, value) VALUES (?, ?)", (key, value)) + + conn.commit() + conn.close() + + logger.info(f"Einstellung gespeichert: {key}") + + return True + except sqlite3.Error as e: + logger.error(f"Fehler beim Speichern der Einstellung '{key}': {e}") + return False + + def delete_setting(self, key: str) -> bool: + """ + Löscht eine Einstellung. + + Args: + key: Schlüssel der zu löschenden Einstellung + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute("DELETE FROM settings WHERE key = ?", (key,)) + + conn.commit() + conn.close() + + logger.info(f"Einstellung gelöscht: {key}") + + return True + except sqlite3.Error as e: + logger.error(f"Fehler beim Löschen der Einstellung '{key}': {e}") + return False + + def backup_database(self, backup_path: Optional[str] = None) -> bool: + """ + Erstellt ein Backup der Datenbank. + + Args: + backup_path: Optional, Pfad für das Backup + + Returns: + True bei Erfolg, False im Fehlerfall + """ + if not backup_path: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = f"database/backup/accounts_{timestamp}.db" + + # Stelle sicher, dass das Backup-Verzeichnis existiert + os.makedirs(os.path.dirname(backup_path), exist_ok=True) + + try: + # SQLite-Backup-API verwenden + conn = sqlite3.connect(self.db_path) + backup_conn = sqlite3.connect(backup_path) + + conn.backup(backup_conn) + + conn.close() + backup_conn.close() + + logger.info(f"Datenbank-Backup erstellt: {backup_path}") + + return True + except sqlite3.Error as e: + logger.error(f"Fehler beim Erstellen des Datenbank-Backups: {e}") + return False \ No newline at end of file diff --git a/database/instagram_accounts.db b/database/instagram_accounts.db new file mode 100644 index 0000000000000000000000000000000000000000..fe2cd7fe7ea19c515ce64fe20c156944b1ace3dc GIT binary patch literal 28672 zcmeI)O;6h}7zglVly-Dt;?@(A5t?WPT8H)p)-e}Vl)yre#wqer!y?k8#14bqHYE0W z_U(4t@-mZl^{O5?Ow~V9h?6{VoZn-+$%T{meP42N9>qf@$!lZHFiqnPA%GSf~BIJ}?;#nB)UDeU-~=R?$*aD5}BpH?Z^a5PD-4`!Yh zC82c7XV%A!;^^kqO#~n0wd#YqM>g&4TUoy$&9deev1ymu)Y_vb#IUh1Vm@$Y(pQ3Qq z@IFt1z;00bZa0SG_< z0uX=z1Rzif;QPO{KnVg6fB*y_009U<00Izz00bbgr~>uu!SMh87B#(iE(kyX0uX=z R1Rwwb2tWV=5P(1~@Ef3Dw^9HA literal 0 HcmV?d00001 diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..27002d0 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,112 @@ +-- SQLite-Datenbankschema für Instagram Account Generator + +-- Accounts-Tabelle +CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + password TEXT NOT NULL, + email TEXT, + phone TEXT, + full_name TEXT, + created_at TEXT, + notes TEXT, + status TEXT DEFAULT 'active', + proxy_used TEXT, + metadata TEXT +); + +-- Proxy-Nutzungen-Tabelle +CREATE TABLE IF NOT EXISTS proxy_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + proxy_type TEXT NOT NULL, + proxy_string TEXT NOT NULL, + used_at TEXT NOT NULL, + success INTEGER DEFAULT 0, + account_id INTEGER, + FOREIGN KEY (account_id) REFERENCES accounts (id) +); + +-- Fehler-Protokoll-Tabelle +CREATE TABLE IF NOT EXISTS error_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + error_type TEXT NOT NULL, + error_message TEXT NOT NULL, + stack_trace TEXT, + timestamp TEXT NOT NULL, + account_id INTEGER, + proxy_used TEXT, + FOREIGN KEY (account_id) REFERENCES accounts (id) +); + +-- Einstellungen-Tabelle +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- SMS-Verifizierungen-Tabelle +CREATE TABLE IF NOT EXISTS sms_verifications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone_number TEXT NOT NULL, + verification_code TEXT, + service_name TEXT NOT NULL DEFAULT 'instagram', + timestamp TEXT NOT NULL, + status TEXT DEFAULT 'pending', + account_id INTEGER, + FOREIGN KEY (account_id) REFERENCES accounts (id) +); + +-- E-Mail-Verifizierungen-Tabelle +CREATE TABLE IF NOT EXISTS email_verifications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email_address TEXT NOT NULL, + verification_code TEXT, + service_name TEXT NOT NULL DEFAULT 'instagram', + timestamp TEXT NOT NULL, + status TEXT DEFAULT 'pending', + account_id INTEGER, + FOREIGN KEY (account_id) REFERENCES accounts (id) +); + +-- Lizenzen-Tabelle +CREATE TABLE IF NOT EXISTS licenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + license_key TEXT NOT NULL UNIQUE, + activated_at TEXT, + expires_at TEXT, + status TEXT DEFAULT 'inactive', + hardware_id TEXT, + metadata TEXT +); + +-- Nutzungsdaten-Tabelle (für Statistiken) +CREATE TABLE IF NOT EXISTS usage_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + action_type TEXT NOT NULL, + timestamp TEXT NOT NULL, + success INTEGER DEFAULT 0, + details TEXT +); + +-- Indizes für bessere Performance +CREATE INDEX IF NOT EXISTS idx_accounts_username ON accounts(username); +CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email); +CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status); +CREATE INDEX IF NOT EXISTS idx_accounts_created_at ON accounts(created_at); +CREATE INDEX IF NOT EXISTS idx_proxy_usage_proxy_type ON proxy_usage(proxy_type); +CREATE INDEX IF NOT EXISTS idx_proxy_usage_used_at ON proxy_usage(used_at); +CREATE INDEX IF NOT EXISTS idx_error_logs_error_type ON error_logs(error_type); +CREATE INDEX IF NOT EXISTS idx_error_logs_timestamp ON error_logs(timestamp); +CREATE INDEX IF NOT EXISTS idx_sms_verifications_phone ON sms_verifications(phone_number); +CREATE INDEX IF NOT EXISTS idx_email_verifications_email ON email_verifications(email_address); +CREATE INDEX IF NOT EXISTS idx_licenses_key ON licenses(license_key); +CREATE INDEX IF NOT EXISTS idx_usage_stats_action_type ON usage_stats(action_type); +CREATE INDEX IF NOT EXISTS idx_usage_stats_timestamp ON usage_stats(timestamp); + +-- Beispieldaten für Testzwecke +INSERT OR IGNORE INTO settings (key, value, updated_at) +VALUES ('app_version', '1.0.0', datetime('now')); + +INSERT OR IGNORE INTO settings (key, value, updated_at) +VALUES ('last_update_check', datetime('now'), datetime('now')); diff --git a/licensing/__init__.py b/licensing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/licensing/__pycache__/__init__.cpython-310.pyc b/licensing/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b39c2b7ed4cce3d54771a0f4385e8ce53af15efd GIT binary patch literal 147 zcmd1j<>g`kf}c?V86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6wv*(xTqIJKxa zCL=X3r6|TFwK%&ZzaXY0wYWq#H!(9WCMPpFHLo}`FFhtcJ~J<~BtBlRpz;=nO>TZl QX-=vg$h=}EAi=@_0QN;9E&u=k literal 0 HcmV?d00001 diff --git a/licensing/__pycache__/__init__.cpython-313.pyc b/licensing/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e579a4b5fb6997e05d1d5b966b12a59f473884e5 GIT binary patch literal 146 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 literal 0 HcmV?d00001 diff --git a/licensing/__pycache__/license_manager.cpython-313.pyc b/licensing/__pycache__/license_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b7140b029b19023f500f3b1d7ce625c6b51f0ac8 GIT binary patch literal 18107 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 Dict[str, Any]: + """Lädt die Lizenzdaten aus der Konfigurationsdatei.""" + if not os.path.exists(self.CONFIG_FILE): + return { + "key": "", + "activation_date": "", + "expiry_date": "", + "status": "inactive", + "status_text": "Keine Lizenz aktiviert", + "features": [], + "last_online_check": "", + "signature": "" + } + + try: + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + license_data = json.load(f) + + logger.info(f"Lizenzdaten geladen: Status '{license_data.get('status', 'unbekannt')}'") + + return license_data + except Exception as e: + logger.error(f"Fehler beim Laden der Lizenzdaten: {e}") + return { + "key": "", + "activation_date": "", + "expiry_date": "", + "status": "inactive", + "status_text": "Fehler beim Laden der Lizenz", + "features": [], + "last_online_check": "", + "signature": "" + } + + def save_license_data(self) -> bool: + """Speichert die Lizenzdaten in die Konfigurationsdatei.""" + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.license_data, f, indent=2) + + logger.info("Lizenzdaten gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Lizenzdaten: {e}") + return False + + def get_license_info(self) -> Dict[str, Any]: + """Gibt die aktuellen Lizenzdaten zurück.""" + return self.license_data + + def get_machine_id(self) -> str: + """ + Generiert eine eindeutige Maschinen-ID. + + Returns: + Eindeutige Maschinen-ID + """ + try: + # Versuche, eine eindeutige Hardware-ID zu generieren + # In der Produktion sollte dies mit einer robusteren Methode ersetzt werden + + machine_id_file = os.path.join("config", ".machine_id") + + if os.path.exists(machine_id_file): + # Bestehende ID laden + with open(machine_id_file, "r") as f: + return f.read().strip() + else: + # Neue ID generieren + machine_id = str(uuid.uuid4()) + + with open(machine_id_file, "w") as f: + f.write(machine_id) + + return machine_id + except Exception as e: + logger.error(f"Fehler bei der Generierung der Maschinen-ID: {e}") + + # Fallback: UUID auf Basis der aktuellen Zeit + return str(uuid.uuid5(uuid.NAMESPACE_DNS, f"fallback-{time.time()}")) + + def is_licensed(self) -> bool: + """ + Überprüft, ob eine gültige Lizenz vorhanden ist. + + Returns: + True, wenn eine gültige Lizenz vorhanden ist, sonst False + """ + # Prüfe den Status der Lizenz + if self.license_data["status"] not in ["active", "trial"]: + return False + + # Prüfe, ob die Lizenz abgelaufen ist + if self.license_data["expiry_date"]: + try: + expiry_date = datetime.fromisoformat(self.license_data["expiry_date"]) + + if datetime.now() > expiry_date: + logger.warning("Lizenz ist abgelaufen") + self.license_data["status"] = "expired" + self.license_data["status_text"] = "Lizenz abgelaufen" + self.save_license_data() + return False + except Exception as e: + logger.error(f"Fehler beim Parsen des Ablaufdatums: {e}") + return False + + # Prüfe, ob regelmäßige Online-Verifizierung erforderlich ist + if self.license_data["last_online_check"]: + try: + last_check = datetime.fromisoformat(self.license_data["last_online_check"]) + max_offline_days = 7 # Maximale Tage ohne Online-Check + + if datetime.now() > last_check + timedelta(days=max_offline_days): + logger.warning(f"Letzte Online-Überprüfung ist mehr als {max_offline_days} Tage her") + + # Versuche, eine Online-Überprüfung durchzuführen + if not self.online_verification(): + self.license_data["status"] = "verification_required" + self.license_data["status_text"] = "Online-Überprüfung erforderlich" + self.save_license_data() + return False + except Exception as e: + logger.error(f"Fehler bei der Überprüfung der Online-Verifizierung: {e}") + + # Prüfe die Signatur (in der Produktion sollte dies erweitert werden) + if not self.verify_signature(): + logger.warning("Ungültige Lizenzsignatur") + self.license_data["status"] = "invalid" + self.license_data["status_text"] = "Ungültige Lizenz (manipuliert)" + self.save_license_data() + return False + + return True + + def verify_license(self) -> bool: + """ + Überprüft die aktuelle Lizenz. + + Returns: + True, wenn die Lizenz gültig ist, sonst False + """ + # Lizenzschlüssel vorhanden? + if not self.license_data["key"]: + logger.info("Kein Lizenzschlüssel vorhanden") + self.license_data["status"] = "inactive" + self.license_data["status_text"] = "Keine Lizenz aktiviert" + self.save_license_data() + return False + + return self.is_licensed() + + def create_signature(self, data: str) -> str: + """ + Erstellt eine Signatur für die angegebenen Daten. + + Args: + data: Zu signierende Daten + + Returns: + Signatur als Hexadezimalstring + """ + # In der Produktion sollte ein sicherer Schlüssel verwendet werden + secret_key = "development_secret_key" + + # HMAC-SHA256-Signatur erstellen + signature = hmac.new( + secret_key.encode(), + data.encode(), + hashlib.sha256 + ).hexdigest() + + return signature + + def verify_signature(self) -> bool: + """ + Überprüft die Signatur der Lizenzdaten. + + Returns: + True, wenn die Signatur gültig ist, sonst False + """ + if not self.license_data["signature"]: + return False + + # Daten für die Signaturprüfung vorbereiten + data_to_verify = f"{self.license_data['key']}|{self.machine_id}|{self.license_data['activation_date']}|{self.license_data['expiry_date']}" + + # Signatur erstellen + computed_signature = self.create_signature(data_to_verify) + + # Signatur vergleichen + return computed_signature == self.license_data["signature"] + + def online_verification(self) -> bool: + """ + Führt eine Online-Überprüfung der Lizenz durch. + + Returns: + True, wenn die Überprüfung erfolgreich war, sonst False + """ + if not self.license_data["key"]: + return False + + try: + # Daten für die Lizenzüberprüfung + verification_data = { + "license_key": self.license_data["key"], + "machine_id": self.machine_id, + "product_version": "1.0.0", # In der Produktion aus einer Konfiguration laden + "timestamp": time.time() + } + + # Anfrage an den Lizenzserver senden + response = requests.post( + self.LICENSE_SERVER_URL + "/verify", + json=verification_data, + timeout=10 + ) + + if response.status_code == 200: + result = response.json() + + if result.get("status") == "active": + # Lizenz ist gültig + logger.info("Online-Lizenzüberprüfung erfolgreich") + + # Aktualisiere das Datum der letzten Überprüfung + self.license_data["last_online_check"] = datetime.now().isoformat() + self.save_license_data() + + return True + else: + # Lizenz ist ungültig + logger.warning(f"Lizenz ungültig: {result.get('message', 'Unbekannter Fehler')}") + + self.license_data["status"] = result.get("status", "invalid") + self.license_data["status_text"] = result.get("message", "Lizenz ungültig") + self.save_license_data() + + return False + else: + logger.warning(f"Fehler bei der Online-Überprüfung: HTTP {response.status_code}") + return False + + except requests.RequestException as e: + logger.error(f"Netzwerkfehler bei der Online-Überprüfung: {e}") + + # Bei Verbindungsproblemen sollte die lokale Lizenz weiterhin gültig bleiben + # In der Produktion kann hier eine Begrenzung der Offline-Zeit implementiert werden + return True + except Exception as e: + logger.error(f"Unerwarteter Fehler bei der Online-Überprüfung: {e}") + return False + + def activate_license(self, license_key: str) -> Tuple[bool, str]: + """ + Aktiviert eine Lizenz mit dem angegebenen Schlüssel. + + Args: + license_key: Zu aktivierender Lizenzschlüssel + + Returns: + (Erfolg, Nachricht) + """ + if not license_key: + return False, "Bitte geben Sie einen Lizenzschlüssel ein." + + try: + # In der Produktionsumgebung sollte hier eine Online-Aktivierung erfolgen + # Für Entwicklungszwecke implementieren wir eine einfache lokale Aktivierung + + # Simulierte Online-Aktivierung + activation_data = { + "license_key": license_key, + "machine_id": self.machine_id, + "product_version": "1.0.0", + "timestamp": time.time() + } + + # Nur für Entwicklung: Prüfe, ob der Lizenzschlüssel bekannt ist + if license_key.startswith("DEV-"): + # Entwicklungslizenzen haben unbegrenzte Laufzeit + expiry_date = (datetime.now() + timedelta(days=365)).isoformat() + activation_response = { + "status": "active", + "message": "Entwicklungslizenz aktiviert", + "activation_date": datetime.now().isoformat(), + "expiry_date": expiry_date, + "features": ["all"] + } + elif license_key.startswith("TRIAL-"): + # Trial-Lizenzen haben begrenzte Laufzeit + expiry_date = (datetime.now() + timedelta(days=30)).isoformat() + activation_response = { + "status": "trial", + "message": "Trial-Lizenz aktiviert (30 Tage)", + "activation_date": datetime.now().isoformat(), + "expiry_date": expiry_date, + "features": ["basic"] + } + else: + # Alle anderen Schlüssel simulieren eine Online-Aktivierung + try: + # Anfrage an den Lizenzserver senden + response = requests.post( + self.LICENSE_SERVER_URL + "/activate", + json=activation_data, + timeout=10 + ) + + if response.status_code == 200: + activation_response = response.json() + else: + logger.warning(f"Fehler bei der Lizenzaktivierung: HTTP {response.status_code}") + return False, f"Fehler bei der Lizenzaktivierung: HTTP {response.status_code}" + except requests.RequestException as e: + logger.error(f"Netzwerkfehler bei der Lizenzaktivierung: {e}") + return False, f"Netzwerkfehler bei der Lizenzaktivierung: {e}" + except Exception as e: + logger.error(f"Unerwarteter Fehler bei der Lizenzaktivierung: {e}") + return False, f"Unerwarteter Fehler bei der Lizenzaktivierung: {e}" + + # Lizenzdaten aktualisieren + self.license_data["key"] = license_key + self.license_data["status"] = activation_response.get("status", "inactive") + self.license_data["status_text"] = activation_response.get("message", "Unbekannter Status") + self.license_data["activation_date"] = activation_response.get("activation_date", datetime.now().isoformat()) + self.license_data["expiry_date"] = activation_response.get("expiry_date", "") + self.license_data["features"] = activation_response.get("features", []) + self.license_data["last_online_check"] = datetime.now().isoformat() + + # Signatur erstellen + data_to_sign = f"{self.license_data['key']}|{self.machine_id}|{self.license_data['activation_date']}|{self.license_data['expiry_date']}" + self.license_data["signature"] = self.create_signature(data_to_sign) + + # Lizenzdaten speichern + self.save_license_data() + + logger.info(f"Lizenz '{license_key}' erfolgreich aktiviert: {self.license_data['status_text']}") + + return True, self.license_data["status_text"] + + except Exception as e: + error_msg = f"Fehler bei der Lizenzaktivierung: {e}" + logger.error(error_msg) + return False, error_msg + + def deactivate_license(self) -> Tuple[bool, str]: + """ + Deaktiviert die aktuelle Lizenz. + + Returns: + (Erfolg, Nachricht) + """ + if not self.license_data["key"]: + return False, "Keine Lizenz aktiviert" + + old_key = self.license_data["key"] + + try: + # Online-Deaktivierung simulieren + deactivation_data = { + "license_key": self.license_data["key"], + "machine_id": self.machine_id, + "timestamp": time.time() + } + + # Anfrage für die Produktionsumgebung + # response = requests.post( + # self.LICENSE_SERVER_URL + "/deactivate", + # json=deactivation_data, + # timeout=10 + # ) + + # Lizenzdaten zurücksetzen + self.license_data = { + "key": "", + "activation_date": "", + "expiry_date": "", + "status": "inactive", + "status_text": "Keine Lizenz aktiviert", + "features": [], + "last_online_check": "", + "signature": "" + } + + # Lizenzdaten speichern + self.save_license_data() + + logger.info(f"Lizenz '{old_key}' erfolgreich deaktiviert") + + return True, "Lizenz erfolgreich deaktiviert" + + except Exception as e: + error_msg = f"Fehler bei der Lizenzdeaktivierung: {e}" + logger.error(error_msg) + return False, error_msg + + def has_feature(self, feature_name: str) -> bool: + """ + Überprüft, ob die aktuelle Lizenz eine bestimmte Funktion unterstützt. + + Args: + feature_name: Name der zu überprüfenden Funktion + + Returns: + True, wenn die Funktion unterstützt wird, sonst False + """ + if not self.is_licensed(): + return False + + # "all" bedeutet, dass alle Funktionen unterstützt werden + if "all" in self.license_data["features"]: + return True + + return feature_name in self.license_data["features"] \ No newline at end of file diff --git a/licensing/license_validator.py b/licensing/license_validator.py new file mode 100644 index 0000000..51db972 --- /dev/null +++ b/licensing/license_validator.py @@ -0,0 +1,304 @@ +""" +Lizenzvalidator - Validiert Lizenzschlüssel und enthält Sicherheitsalgorithmen +""" + +import os +import logging +import hashlib +import hmac +import base64 +import json +import time +import random +import string +from datetime import datetime, timedelta +from typing import Dict, Optional, Tuple, Any, List + +# Konfiguriere Logger +logger = logging.getLogger("license_validator") + +class LicenseValidator: + """ + Validiert Lizenzschlüssel und führt kryptografische Operationen durch. + Enthält Platzhaltercode für die Lizenzvalidierung. + """ + + # Sicherheitsschlüssel (würde in einer echten Implementierung nicht im Code stehen) + SECRET_KEY = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + + def __init__(self): + """Initialisiert den LicenseValidator.""" + logger.info("Lizenzvalidator initialisiert") + + def validate_key_format(self, license_key: str) -> bool: + """ + Prüft, ob der Lizenzschlüssel das richtige Format hat. + + Args: + license_key: Der zu prüfende Lizenzschlüssel + + Returns: + bool: True, wenn das Format gültig ist, False sonst + """ + # Einfacher Formatcheck für XXXXX-XXXXX-XXXXX-XXXXX + import re + return bool(re.match(r'^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$', license_key)) + + def validate_key_checksum(self, license_key: str) -> bool: + """ + Prüft, ob die Prüfsumme des Lizenzschlüssels gültig ist. + + Args: + license_key: Der zu prüfende Lizenzschlüssel + + Returns: + bool: True, wenn die Prüfsumme gültig ist, False sonst + """ + # Platzhalterimplementierung - in einer echten Implementierung würde hier + # eine Prüfsummenberechnung stehen + + # Entferne Bindestriche für die Verarbeitung + key_parts = license_key.split('-') + if len(key_parts) != 4: + return False + + # Einfacher Check: Letzter Buchstabe des ersten Teils ist abhängig von den ersten Buchstaben + # der anderen Teile (XOR der ASCII-Werte) + try: + check_char = key_parts[0][-1] + calculated_char = chr(ord(key_parts[1][0]) ^ ord(key_parts[2][0]) ^ ord(key_parts[3][0])) + + # In einer echten Implementierung wäre diese Prüfung viel stärker + return check_char == calculated_char + except IndexError: + return False + except Exception as e: + logger.error(f"Fehler bei der Prüfsummenberechnung: {e}") + return False + + def decrypt_license_data(self, license_key: str) -> Optional[Dict[str, Any]]: + """ + Entschlüsselt Lizenzinformationen aus dem Schlüssel. + + Args: + license_key: Der zu entschlüsselnde Lizenzschlüssel + + Returns: + Optional[Dict[str, Any]]: Entschlüsselte Lizenzdaten oder None bei Fehler + """ + # Platzhalterimplementierung - in einer echten Implementierung würde hier + # eine Entschlüsselung stehen + + if not self.validate_key_format(license_key): + return None + + # Mock-Daten generieren + key_parts = license_key.split('-') + + # Aus dem Schlüssel Informationen "ableiten" + try: + # Verwende den ersten Teil für die Lizenzart + license_type_index = sum(ord(c) for c in key_parts[0]) % 3 + license_types = ["basic", "premium", "enterprise"] + license_type = license_types[license_type_index] + + # Verwende den zweiten Teil für die Gültigkeitsdauer + validity_months = (sum(ord(c) for c in key_parts[1]) % 12) + 1 + + # Verwende den dritten Teil für die Funktionen + features_count = (sum(ord(c) for c in key_parts[2]) % 5) + 1 + all_features = ["multi_account", "proxy_rotation", "advanced_analytics", + "sms_verification", "captcha_solving", "phone_verification", + "export", "scheduling"] + features = all_features[:features_count] + + # Generiere ein "verschlüsseltes" Token + token = hashlib.sha256(license_key.encode()).hexdigest() + + # Aktuelle Zeit für Aktivierung + now = datetime.now() + activation_date = now.strftime("%Y-%m-%d %H:%M:%S") + expiry_date = (now + timedelta(days=30*validity_months)).strftime("%Y-%m-%d %H:%M:%S") + + # Lizenzdaten zusammenstellen + license_data = { + "license_type": license_type, + "features": features, + "activation_date": activation_date, + "expiry_date": expiry_date, + "token": token + } + + return license_data + + except Exception as e: + logger.error(f"Fehler bei der Entschlüsselung des Lizenzschlüssels: {e}") + return None + + def generate_license_key(self, license_type: str = "basic", validity_months: int = 12, + features: List[str] = None) -> str: + """ + Generiert einen Lizenzschlüssel. + + Args: + license_type: Art der Lizenz ("basic", "premium", "enterprise") + validity_months: Gültigkeitsdauer in Monaten + features: Liste der Funktionen + + Returns: + str: Generierter Lizenzschlüssel + """ + # Platzhalterimplementierung - in einer echten Implementierung würde hier + # eine sichere Schlüsselgenerierung stehen + + # Verwende die Eingabeparameter als Seed für die Generierung + seed = f"{license_type}{validity_months}{','.join(features or [])}{time.time()}" + random.seed(hashlib.md5(seed.encode()).hexdigest()) + + # Generiere 4 Teile mit jeweils 5 Zeichen (Großbuchstaben und Zahlen) + chars = string.ascii_uppercase + string.digits + parts = [] + + for _ in range(4): + part = ''.join(random.choice(chars) for _ in range(5)) + parts.append(part) + + # Stelle sicher, dass der letzte Buchstabe des ersten Teils + # ein XOR der ersten Buchstaben der anderen Teile ist + # (für die einfache Prüfsumme) + calc_char = chr(ord(parts[1][0]) ^ ord(parts[2][0]) ^ ord(parts[3][0])) + parts[0] = parts[0][:-1] + calc_char + + # Verbinde die Teile mit Bindestrichen + license_key = '-'.join(parts) + + return license_key + + def sign_data(self, data: str) -> str: + """ + Signiert Daten mit dem geheimen Schlüssel. + + Args: + data: Zu signierende Daten + + Returns: + str: Signatur + """ + return hmac.new( + self.SECRET_KEY.encode(), + data.encode(), + hashlib.sha256 + ).hexdigest() + + def verify_signature(self, data: str, signature: str) -> bool: + """ + Überprüft die Signatur von Daten. + + Args: + data: Signierte Daten + signature: Zu überprüfende Signatur + + Returns: + bool: True, wenn die Signatur gültig ist, False sonst + """ + expected_signature = self.sign_data(data) + return hmac.compare_digest(expected_signature, signature) + + def encode_license_data(self, data: Dict[str, Any]) -> str: + """ + Kodiert Lizenzdaten zur sicheren Übertragung. + + Args: + data: Zu kodierende Lizenzdaten + + Returns: + str: Kodierte Lizenzdaten + """ + # Daten in JSON konvertieren + json_data = json.dumps(data, sort_keys=True) + + # Signatur hinzufügen + signature = self.sign_data(json_data) + + # Zusammen mit der Signatur kodieren + combined = f"{json_data}|{signature}" + encoded = base64.b64encode(combined.encode()).decode() + + return encoded + + def decode_license_data(self, encoded: str) -> Optional[Dict[str, Any]]: + """ + Dekodiert und überprüft kodierte Lizenzdaten. + + Args: + encoded: Kodierte Lizenzdaten + + Returns: + Optional[Dict[str, Any]]: Dekodierte Lizenzdaten oder None bei Fehler + """ + try: + # Dekodieren + decoded = base64.b64decode(encoded).decode() + + # In Daten und Signatur aufteilen + json_data, signature = decoded.split('|', 1) + + # Signatur überprüfen + if not self.verify_signature(json_data, signature): + logger.warning("Ungültige Signatur in lizenzierten Daten") + return None + + # JSON parsen + data = json.loads(json_data) + + return data + + except Exception as e: + logger.error(f"Fehler beim Dekodieren der Lizenzdaten: {e}") + return None + + +# Beispielnutzung, wenn direkt ausgeführt +if __name__ == "__main__": + # Konfiguriere Logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Beispiel für LicenseValidator + validator = LicenseValidator() + + # Generiere einen Lizenzschlüssel + features = ["multi_account", "proxy_rotation", "advanced_analytics"] + key = validator.generate_license_key("premium", 12, features) + print(f"Generierter Lizenzschlüssel: {key}") + + # Validiere den Schlüssel + is_valid_format = validator.validate_key_format(key) + is_valid_checksum = validator.validate_key_checksum(key) + print(f"Format gültig: {is_valid_format}") + print(f"Prüfsumme gültig: {is_valid_checksum}") + + # Entschlüssele Lizenzdaten + license_data = validator.decrypt_license_data(key) + if license_data: + print("\nEntschlüsselte Lizenzdaten:") + for k, v in license_data.items(): + print(f" {k}: {v}") + + # Beispiel für Kodierung und Dekodierung + test_data = { + "name": "Test License", + "type": "premium", + "expires": "2026-01-01" + } + + encoded = validator.encode_license_data(test_data) + print(f"\nKodierte Daten: {encoded}") + + decoded = validator.decode_license_data(encoded) + if decoded: + print("\nDekodierte Daten:") + for k, v in decoded.items(): + print(f" {k}: {v}") \ No newline at end of file diff --git a/localization/__init__.py b/localization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/localization/__pycache__/__init__.cpython-310.pyc b/localization/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6735da7cb6dacc34d46a89c23e2ad9998362e2a1 GIT binary patch literal 150 zcmd1j<>g`kf}c?V86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6w#*(xTqIJKxa zCL=X3r6|TFwK%&ZzaXY0wYWq#H!(9WCMQ2RF(PO2Tq$YLfS!NLFlaAPBW literal 0 HcmV?d00001 diff --git a/localization/__pycache__/__init__.cpython-313.pyc b/localization/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d3d0da3cace9427a641cb41958e5b01c9f89903 GIT binary patch literal 149 zcmey&%ge<81X~XAXMpI(AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl=c0;?G3jLh7` z%%a2?=lqmZ-GH3L%JQPj^o)|2oc!d(oXo1klFa<~G91eGOcC=crrASd2xrsgPYU;F3>ugjfvVJvyvbMa6)i5%q+~J*B zuDG1l<)t<|m^BPYZ2$vJ3iPoFFk2)KehKmvpnx6a?{04UvES#y?HxW7ZV+*_Q5GVol%Mfc<<5K?=hjoE?5v=JL=9jFe{2sKDk$x5K(*$NEEYG6Xz zvbN=n=aj6=`2%)9Z5rx*VWe#}U{utmInu^wsAsuI=JkZi&f@#o@ls$1>_FewM=0B> zJl4c*dg@?|@pC9K4j5W3kDm`}^!At94|FOC9H?h*m^kj__q8~bp&x}w$OZI*wI8w8 zc3H7}xF-6^YWYUp_&YnR?hRP<-D^8LQP}m#IInyyPNW|X%bJ-EtupAVj|;zFe&v=| zD~vPo?CYE1wjT=LyRbg!VoG1|y%PL8_!_mCcp^BYOW?vd zbT?sPyd%SHo-IsXx`RO!40hveDNOPTfMD4&(+5dr?)Y*us|KNneYC9l8;L5x#Cjoy z>3H^rpYRSUWaic&jI;S-Ziy;93z>8WkGUv2P6RJtd?2P zpbrMh%rNc^h*1(A^+W@0y+-0c=F_Slu&mbUbR$1WI-L)+53gNceejJ0Z1rFhm=F(c z@Z_N!>^zV>kyo&5@q=j4g%u5z-X0V)E-0tHvnS>;%z0eNWeBHgvDv?6W0BcL-DogJ zw^&_Y)`#cr97S-u*tss*Q1P3?Lmk(3T*)$EssN`A|GBU2Gs(sT4nX0C_RgmXDq}GF z0TV4e0h9;evh&)$A=#ESu2RjB4oI0JHnvB`$m}V|JgWD#!2Bz=Ul~=<&;5OCME$+> zK;Jh|*BDt3b@8Xe6U5{J~rF;)og$MiD&!%%xvGs z+M47m)Ltm!J75KIk)GC}CprJ;Y!8}H+E`H9SWtRUdo|Sq)t>gkT z3-pH71j%j)?y4l`fbD zBYkm&#$;T44)L1=0b%L7Zt6|uum)@5(%-WUP!qG6u1f2;>#XtXi~nW5 z*Qj1*_IvtA`oHTR)fXJ5qlLpxt??h1>rMTVsy%$+1ht()=~pqA%o1FtB0ds-Q0zg* zrzr35@IXc}wlfu{dZ0?tj(2CsIyOc5jN_AnLEUxBMu{OLCl-oeb?s z4CGb3GBK6hP{`pA?3hOG3A zC%RY}nfEo6B1%9IPw+COJu;ur6MBI`TV(npooc~H)%M02=Uv#Jedf|}KlRhT~2 zw8&?(ed}Ed#;@+m`0ex!1URLQ+zvPLu>A1ZBwLiQ{CoV{GWP{!{_tMi&FJu1f_Tmy zrsD8f_u(gk0#rHyz_h*cFyw;N7!x>9h}V{(jQBA?O!i~a3*!C27lFj=R@|N+MdV81 z$fmvLCf?^gBwZL|HwAT@g>4U3ujIc5TfgurHwz!MR7(bWm}L+2^V{rngY zjScy19EoE#UM#Z*B zXlvwJfL}xWA-*842I`Rf0&(GTHLf0p}?+jtOL%2r?)^UPF8u!lP{VZI#BF$zI&e>TmAd zm0xV%mDdM?i#Mr?18DUlzP>q-*-{74fay;%@x0GjcjeWKTwkT^)gJm4{D@3jMRVj>03?%K7|w6Vgw1pX)o{W$%v}R04+>PgbYvTo(iz-JU!WH19CEV9 zS0=3{{-RIX8_G4POl{*j>gQYH($igt4_MAh#~l6J`1B9B5(~o8>u~79-#D&-({|^C z7DyAMg@2`BSO6PBN`3g* z-Go9OX zRP4fbPKnA-HH?`|690ryG;plf+^-kOC5~JYSrv){?orxLi{;TTQ{K6`hEwlo`ijI7CO> zA>Kk!W+7&$7c~z8m>^#uzCofwLM@R{m(^gachmvAaOn$8WM+nHt~A5UzMAXpH8Q}2 Uq9;?gO&cdrHe14VEkW`Zj1cFNt61bpu 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 literal 0 HcmV?d00001 diff --git a/localization/language_manager.py b/localization/language_manager.py new file mode 100644 index 0000000..a5073d7 --- /dev/null +++ b/localization/language_manager.py @@ -0,0 +1,272 @@ +# Path: localization/language_manager.py + +""" +Sprachmanager für die Übersetzung der Benutzeroberfläche. +""" + +import os +import json +import logging +import time +from typing import Dict, Any, Optional +from PyQt5.QtCore import QObject, pyqtSignal, QSettings + +logger = logging.getLogger("language_manager") + +class LanguageManager(QObject): + """Verwaltet die Sprachen und Übersetzungen der Anwendung.""" + + # Signal, das ausgelöst wird, wenn sich die Sprache ändert + language_changed = pyqtSignal(str) + + # Signal, das den Status des Sprachwechsels anzeigt (True = läuft, False = abgeschlossen) + language_change_status = pyqtSignal(bool) + + def __init__(self, app=None): + """ + Initialisiert den Sprachmanager. + + Args: + app: Die QApplication-Instanz + """ + super().__init__() + self.app = app + self.current_language = "de" # Standard ist Deutsch + self.translations = {} + self.available_languages = {} + self.last_change_time = 0 # Zeitpunkt des letzten Sprachwechsels + self.change_cooldown = 0.5 # Cooldown in Sekunden zwischen Sprachwechseln + self.is_changing_language = False # Status-Variable für laufenden Sprachwechsel + + # Basisverzeichnis für Sprachdateien ermitteln + self.base_dir = os.path.dirname(os.path.abspath(__file__)) + self.languages_dir = os.path.join(self.base_dir, "languages") + + # Verfügbare Sprachen ermitteln und laden + self._discover_languages() + + # Lade die gespeicherte Sprache, falls vorhanden + self.settings = QSettings("Chimaira", "SocialMediaAccountGenerator") + saved_language = self.settings.value("language", "de") + + if saved_language in self.available_languages: + self.current_language = saved_language + + self._load_language(self.current_language) + + logger.info(f"Sprachmanager initialisiert mit Sprache: {self.current_language}") + + def _discover_languages(self): + """Ermittelt die verfügbaren Sprachen aus den Sprachdateien.""" + self.available_languages = {} + + try: + # Alle JSON-Dateien im Sprachverzeichnis suchen + language_files = [] + for filename in os.listdir(self.languages_dir): + if filename.endswith(".json"): + language_code = filename.split(".")[0] + language_path = os.path.join(self.languages_dir, filename) + language_files.append((language_code, language_path)) + + # Definierte Reihenfolge der Sprachen + ordered_codes = ["de", "en", "fr", "es", "ja"] + + # Sortierte Liste erstellen + for code in ordered_codes: + for language_code, language_path in language_files: + if language_code == code: + # Sprachinformationen aus der Datei lesen + try: + with open(language_path, 'r', encoding='utf-8') as file: + language_data = json.load(file) + language_name = language_data.get("language_name", language_code) + self.available_languages[language_code] = { + "name": language_name, + "path": language_path + } + except Exception as e: + logger.error(f"Fehler beim Laden der Sprachinformationen für {language_code}: {e}") + + # Eventuelle restliche Sprachen hinzufügen + for language_code, language_path in language_files: + if language_code not in self.available_languages: + try: + with open(language_path, 'r', encoding='utf-8') as file: + language_data = json.load(file) + language_name = language_data.get("language_name", language_code) + self.available_languages[language_code] = { + "name": language_name, + "path": language_path + } + except Exception as e: + logger.error(f"Fehler beim Laden der Sprachinformationen für {language_code}: {e}") + + logger.info(f"Verfügbare Sprachen: {', '.join(self.available_languages.keys())}") + + except Exception as e: + logger.error(f"Fehler beim Ermitteln der verfügbaren Sprachen: {e}") + + def _load_language(self, language_code: str) -> bool: + """ + Lädt die Übersetzungen für eine Sprache. + + Args: + language_code: Der Sprachcode (z.B. "de", "en") + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if language_code not in self.available_languages: + logger.error(f"Sprache {language_code} nicht verfügbar") + return False + + try: + language_path = self.available_languages[language_code]["path"] + + with open(language_path, 'r', encoding='utf-8') as file: + self.translations = json.load(file) + + self.current_language = language_code + logger.info(f"Sprache {language_code} geladen") + + # Signal auslösen, dass sich die Sprache geändert hat + self.language_changed.emit(language_code) + + return True + + except Exception as e: + logger.error(f"Fehler beim Laden der Sprache {language_code}: {e}") + return False + + def get_text(self, key: str, default: str = None) -> str: + """ + Gibt den übersetzten Text für einen Schlüssel zurück. + + Args: + key: Der Schlüssel für den Text (z.B. "main.title") + default: Der Standardtext, falls der Schlüssel nicht gefunden wurde + + Returns: + str: Der übersetzte Text oder der Standardtext + """ + # Wenn kein Standardtext angegeben wurde, verwende den Schlüssel + if default is None: + default = key + + # Versuche, den Text aus den Übersetzungen zu holen + try: + # Unterstütze verschachtelte Schlüssel mit Punktnotation + parts = key.split('.') + result = self.translations + + for part in parts: + if part in result: + result = result[part] + else: + return default + + # Wenn das Ergebnis ein unterstützter Datentyp ist, gib es zurück + if isinstance(result, (str, list, dict)): + return result + else: + logger.warning( + f"Schlüssel {key} hat unerwarteten Typ: {type(result)} - {result}" + ) + return default + + except Exception as e: + logger.warning(f"Fehler beim Abrufen des Textes für Schlüssel {key}: {e}") + return default + + def change_language(self, language_code: str) -> bool: + """ + Ändert die aktive Sprache. + + Args: + language_code: Der Sprachcode (z.B. "de", "en") + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + # Wenn bereits ein Sprachwechsel läuft, blockiere weitere Wechsel + if self.is_changing_language: + logger.debug(f"Ein Sprachwechsel läuft bereits, ignoriere Wechsel zu {language_code}") + return False + + # Cooldown prüfen + current_time = time.time() + if current_time - self.last_change_time < self.change_cooldown: + logger.debug("Sprachwechsel zu schnell hintereinander") + return False + + # Wenn es die gleiche Sprache ist, nichts tun + if language_code == self.current_language: + return True + + # Status auf "Sprachwechsel läuft" setzen + self.is_changing_language = True + self.language_change_status.emit(True) + + # Versuche die Sprache zu wechseln + success = self._load_language(language_code) + + if success: + # Sprache in Einstellungen speichern + self.settings.setValue("language", language_code) + self.settings.sync() + # Aktualisiere den Zeitpunkt des letzten Sprachwechsels + self.last_change_time = time.time() + + # Nach einer kurzen Verzögerung den Status zurücksetzen + # um sicherzustellen, dass die UI-Aktualisierung abgeschlossen ist + from PyQt5.QtCore import QTimer + QTimer.singleShot(500, self._reset_change_status) + + return success + + def _reset_change_status(self): + """Setzt den Status des Sprachwechsels zurück.""" + self.is_changing_language = False + self.language_change_status.emit(False) + logger.debug("Sprachwechsel-Status zurückgesetzt") + + def get_current_language(self) -> str: + """Gibt den Code der aktuellen Sprache zurück.""" + return self.current_language + + def get_language_name(self, language_code: str = None) -> str: + """ + Gibt den Namen einer Sprache zurück. + + Args: + language_code: Der Sprachcode oder None für die aktuelle Sprache + + Returns: + str: Der Name der Sprache + """ + if language_code is None: + language_code = self.current_language + + if language_code in self.available_languages: + return self.available_languages[language_code].get("name", language_code) + else: + return language_code + + def get_available_languages(self) -> Dict[str, str]: + """ + Gibt eine Liste der verfügbaren Sprachen zurück. + + Returns: + Dict[str, str]: Ein Dictionary mit Sprachcodes als Schlüssel und Sprachnamen als Werten + """ + return {code: info.get("name", code) for code, info in self.available_languages.items()} + + def is_language_change_in_progress(self) -> bool: + """ + Gibt zurück, ob gerade ein Sprachwechsel im Gange ist. + + Returns: + bool: True wenn ein Sprachwechsel läuft, False sonst + """ + return self.is_changing_language \ No newline at end of file diff --git a/localization/languages/de.json b/localization/languages/de.json new file mode 100644 index 0000000..151b547 --- /dev/null +++ b/localization/languages/de.json @@ -0,0 +1,102 @@ +{ + "language_name": "Deutsch", + "main": { + "title": "Social Media Account Generator", + "subtitle": "Wählen Sie eine Plattform", + "version": "Version 1.0.0", + "overview": "Übersicht" + }, + "buttons": { + "back": "↩ Zurück", + "create": "Account erstellen", + "cancel": "Abbrechen", + "refresh": "Aktualisieren", + "export": "Exportieren", + "delete": "Löschen", + "test_proxy": "Proxy testen", + "save_proxy": "Proxy-Einstellungen speichern", + "test_email": "E-Mail testen", + "save_email": "E-Mail-Einstellungen speichern", + "activate_license": "Lizenz aktivieren", + "check_updates": "Auf Updates prüfen", + "ok": "OK" + }, + "tabs": { + "generator": "Account Generator", + "accounts": "Konten", + "settings": "Einstellungen", + "about": "Über" + }, + "menu": { + "about": "Über", + "about_app": "Über die Anwendung", + "language": "Sprache" + }, + "status": { + "ready": "Bereit" + }, + "generator_tab": { + "form_title": "Account-Informationen", + "first_name_label": "Vorname:", + "first_name_placeholder": "z.B. Max", + "last_name_label": "Nachname:", + "last_name_placeholder": "z.B. Mustermann", + "age_label": "Alter:", + "age_placeholder": "Alter zwischen 13 und 99", + "registration_method_label": "Registrierungsmethode:", + "email_radio": "E-Mail", + "phone_radio": "Telefon", + "phone_label": "Telefonnummer:", + "phone_placeholder": "z.B. +49123456789", + "email_domain_label": "E-Mail-Domain:", + "proxy_use": "Proxy verwenden", + "proxy_label": "Proxy:", + "proxy_type_label": "Typ:", + "proxy_type_ipv4": "IPv4", + "proxy_type_ipv6": "IPv6", + "proxy_type_mobile": "Mobile", + "headless": "Browser im Hintergrund ausführen", + "debug": "Debug-Modus (detaillierte Protokollierung)", + "log_title": "Log", + "error_title": "Fehler", + "first_name_error": "Bitte geben Sie einen Vornamen ein.", + "last_name_error": "Bitte geben Sie einen Nachnamen ein.", + "age_empty_error": "Bitte geben Sie ein Alter ein.", + "age_int_error": "Das Alter muss eine ganze Zahl sein.", + "age_range_error": "Das Alter muss zwischen 13 und 99 liegen.", + "phone_error": "Bitte geben Sie eine Telefonnummer ein.", + "tiktok_category_label": "Kategorie/Nische:", + "tiktok_category_general": "Allgemein", + "tiktok_category_gaming": "Gaming", + "tiktok_category_fashion": "Mode", + "tiktok_category_fitness": "Fitness", + "tiktok_category_travel": "Reisen", + "tiktok_category_cooking": "Kochen", + "tiktok_category_technology": "Technologie", + "tiktok_category_education": "Bildung" + }, + "accounts_tab": { + "headers": [ + "ID", + "Benutzername", + "Passwort", + "E-Mail", + "Handynummer", + "Name", + "Plattform", + "Erstellt am" + ], + "no_selection_title": "Kein Konto ausgewählt", + "no_selection_text": "Bitte wählen Sie ein Konto zum Löschen aus.", + "delete_title": "Konto löschen", + "delete_text": "Möchten Sie das Konto '{username}' wirklich löschen?", + "delete_success_title": "Erfolg", + "delete_success_text": "Konto '{username}' wurde gelöscht.", + "delete_error_title": "Fehler", + "delete_error_text": "Konto '{username}' konnte nicht gelöscht werden." + }, + "about_dialog": { + "support": "Für Support kontaktieren Sie uns unter: support@example.com", + "license": "Diese Software ist lizenzpflichtig und darf nur mit gültiger Lizenz verwendet werden." + } +} \ No newline at end of file diff --git a/localization/languages/en.json b/localization/languages/en.json new file mode 100644 index 0000000..282d5bf --- /dev/null +++ b/localization/languages/en.json @@ -0,0 +1,102 @@ +{ + "language_name": "English", + "main": { + "title": "Social Media Account Generator", + "subtitle": "Select a platform", + "version": "Version 1.0.0", + "overview": "Overview" + }, + "buttons": { + "back": "↩ Back", + "create": "Create Account", + "cancel": "Cancel", + "refresh": "Refresh", + "export": "Export", + "delete": "Delete", + "test_proxy": "Test Proxy", + "save_proxy": "Save Proxy Settings", + "test_email": "Test Email", + "save_email": "Save Email Settings", + "activate_license": "Activate License", + "check_updates": "Check for Updates", + "ok": "OK" + }, + "tabs": { + "generator": "Account Generator", + "accounts": "Accounts", + "settings": "Settings", + "about": "About" + }, + "menu": { + "about": "About", + "about_app": "About the Application", + "language": "Language" + }, + "status": { + "ready": "Ready" + }, + "generator_tab": { + "form_title": "Account Information", + "first_name_label": "First Name:", + "first_name_placeholder": "e.g. Max", + "last_name_label": "Last Name:", + "last_name_placeholder": "e.g. Mustermann", + "age_label": "Age:", + "age_placeholder": "Age between 13 and 99", + "registration_method_label": "Registration Method:", + "email_radio": "Email", + "phone_radio": "Phone", + "phone_label": "Phone Number:", + "phone_placeholder": "e.g. +49123456789", + "email_domain_label": "Email Domain:", + "proxy_use": "Use Proxy", + "proxy_label": "Proxy:", + "proxy_type_label": "Type:", + "proxy_type_ipv4": "IPv4", + "proxy_type_ipv6": "IPv6", + "proxy_type_mobile": "Mobile", + "headless": "Run browser headless", + "debug": "Debug mode (detailed logging)", + "log_title": "Log", + "error_title": "Error", + "first_name_error": "Please enter a first name.", + "last_name_error": "Please enter a last name.", + "age_empty_error": "Please enter an age.", + "age_int_error": "Age must be a whole number.", + "age_range_error": "Age must be between 13 and 99.", + "phone_error": "Please enter a phone number.", + "tiktok_category_label": "Category/Niche:", + "tiktok_category_general": "General", + "tiktok_category_gaming": "Gaming", + "tiktok_category_fashion": "Fashion", + "tiktok_category_fitness": "Fitness", + "tiktok_category_travel": "Travel", + "tiktok_category_cooking": "Cooking", + "tiktok_category_technology": "Technology", + "tiktok_category_education": "Education" + }, + "accounts_tab": { + "headers": [ + "ID", + "Username", + "Password", + "Email", + "Phone", + "Name", + "Platform", + "Created At" + ], + "no_selection_title": "No Account Selected", + "no_selection_text": "Please select an account to delete.", + "delete_title": "Delete Account", + "delete_text": "Do you really want to delete the account '{username}'?", + "delete_success_title": "Success", + "delete_success_text": "Account '{username}' was deleted.", + "delete_error_title": "Error", + "delete_error_text": "Account '{username}' could not be deleted." + }, + "about_dialog": { + "support": "For support contact us at: support@example.com", + "license": "This software is licensed and may only be used with a valid license." + } +} \ No newline at end of file diff --git a/localization/languages/es.json b/localization/languages/es.json new file mode 100644 index 0000000..4c403f5 --- /dev/null +++ b/localization/languages/es.json @@ -0,0 +1,102 @@ +{ + "language_name": "Español", + "main": { + "title": "Generador de Cuentas de Redes Sociales", + "subtitle": "Seleccione una plataforma", + "version": "Versión 1.0.0", + "overview": "Resumen" + }, + "buttons": { + "back": "↩ Volver", + "create": "Crear cuenta", + "cancel": "Cancelar", + "refresh": "Actualizar", + "export": "Exportar", + "delete": "Eliminar", + "test_proxy": "Probar proxy", + "save_proxy": "Guardar ajustes de proxy", + "test_email": "Probar correo", + "save_email": "Guardar ajustes de correo", + "activate_license": "Activar licencia", + "check_updates": "Buscar actualizaciones", + "ok": "Aceptar" + }, + "tabs": { + "generator": "Generador de Cuentas", + "accounts": "Cuentas", + "settings": "Configuración", + "about": "Acerca de" + }, + "menu": { + "about": "Acerca de", + "about_app": "Acerca de la aplicación", + "language": "Idioma" + }, + "status": { + "ready": "Listo" + }, + "generator_tab": { + "form_title": "Información de la cuenta", + "first_name_label": "Nombre:", + "first_name_placeholder": "p.ej. Max", + "last_name_label": "Apellido:", + "last_name_placeholder": "p.ej. Mustermann", + "age_label": "Edad:", + "age_placeholder": "Edad entre 13 y 99", + "registration_method_label": "Método de registro:", + "email_radio": "Correo electrónico", + "phone_radio": "Teléfono", + "phone_label": "Número de teléfono:", + "phone_placeholder": "p.ej. +49123456789", + "email_domain_label": "Dominio de correo:", + "proxy_use": "Usar proxy", + "proxy_label": "Proxy:", + "proxy_type_label": "Tipo:", + "proxy_type_ipv4": "IPv4", + "proxy_type_ipv6": "IPv6", + "proxy_type_mobile": "Móvil", + "headless": "Ejecutar navegador en segundo plano", + "debug": "Modo depuración (registro detallado)", + "log_title": "Registro", + "error_title": "Error", + "first_name_error": "Por favor, introduzca un nombre.", + "last_name_error": "Por favor, introduzca un apellido.", + "age_empty_error": "Por favor, introduzca una edad.", + "age_int_error": "La edad debe ser un número entero.", + "age_range_error": "La edad debe estar entre 13 y 99.", + "phone_error": "Por favor, introduzca un número de teléfono.", + "tiktok_category_label": "Categoría/Nicho:", + "tiktok_category_general": "General", + "tiktok_category_gaming": "Juegos", + "tiktok_category_fashion": "Moda", + "tiktok_category_fitness": "Fitness", + "tiktok_category_travel": "Viajes", + "tiktok_category_cooking": "Cocina", + "tiktok_category_technology": "Tecnología", + "tiktok_category_education": "Educación" + }, + "accounts_tab": { + "headers": [ + "ID", + "Nombre de usuario", + "Contraseña", + "Correo", + "Teléfono", + "Nombre", + "Plataforma", + "Creado el" + ], + "no_selection_title": "Ninguna cuenta seleccionada", + "no_selection_text": "Seleccione una cuenta para eliminar.", + "delete_title": "Eliminar cuenta", + "delete_text": "¿Realmente desea eliminar la cuenta '{username}'?", + "delete_success_title": "Éxito", + "delete_success_text": "La cuenta '{username}' fue eliminada.", + "delete_error_title": "Error", + "delete_error_text": "No se pudo eliminar la cuenta '{username}'." + }, + "about_dialog": { + "support": "Para soporte contáctenos en: support@example.com", + "license": "Este software está sujeto a licencia y solo puede utilizarse con una licencia válida." + } +} \ No newline at end of file diff --git a/localization/languages/fr.json b/localization/languages/fr.json new file mode 100644 index 0000000..6081651 --- /dev/null +++ b/localization/languages/fr.json @@ -0,0 +1,102 @@ +{ + "language_name": "Français", + "main": { + "title": "Générateur de Comptes de Médias Sociaux", + "subtitle": "Sélectionnez une plateforme", + "version": "Version 1.0.0", + "overview": "Aperçu" + }, + "buttons": { + "back": "↩ Retour", + "create": "Créer un compte", + "cancel": "Annuler", + "refresh": "Rafraîchir", + "export": "Exporter", + "delete": "Supprimer", + "test_proxy": "Tester le proxy", + "save_proxy": "Enregistrer le proxy", + "test_email": "Tester l'e-mail", + "save_email": "Enregistrer l'e-mail", + "activate_license": "Activer la licence", + "check_updates": "Vérifier les mises à jour", + "ok": "OK" + }, + "tabs": { + "generator": "Générateur de Compte", + "accounts": "Comptes", + "settings": "Paramètres", + "about": "À Propos" + }, + "menu": { + "about": "À propos", + "about_app": "À propos de l'application", + "language": "Langue" + }, + "status": { + "ready": "Prêt" + }, + "generator_tab": { + "form_title": "Informations du compte", + "first_name_label": "Prénom:", + "first_name_placeholder": "ex. Max", + "last_name_label": "Nom:", + "last_name_placeholder": "ex. Mustermann", + "age_label": "Âge:", + "age_placeholder": "Âge entre 13 et 99", + "registration_method_label": "Méthode d'enregistrement:", + "email_radio": "E-mail", + "phone_radio": "Téléphone", + "phone_label": "Numéro de téléphone:", + "phone_placeholder": "ex. +49123456789", + "email_domain_label": "Domaine e-mail:", + "proxy_use": "Utiliser un proxy", + "proxy_label": "Proxy:", + "proxy_type_label": "Type:", + "proxy_type_ipv4": "IPv4", + "proxy_type_ipv6": "IPv6", + "proxy_type_mobile": "Mobile", + "headless": "Exécuter le navigateur en arrière-plan", + "debug": "Mode débogage (journal détaillé)", + "log_title": "Journal", + "error_title": "Erreur", + "first_name_error": "Veuillez saisir un prénom.", + "last_name_error": "Veuillez saisir un nom.", + "age_empty_error": "Veuillez saisir un âge.", + "age_int_error": "L'âge doit être un nombre entier.", + "age_range_error": "L'âge doit être compris entre 13 et 99.", + "phone_error": "Veuillez saisir un numéro de téléphone.", + "tiktok_category_label": "Catégorie/Niche:", + "tiktok_category_general": "Général", + "tiktok_category_gaming": "Jeux", + "tiktok_category_fashion": "Mode", + "tiktok_category_fitness": "Fitness", + "tiktok_category_travel": "Voyage", + "tiktok_category_cooking": "Cuisine", + "tiktok_category_technology": "Technologie", + "tiktok_category_education": "Éducation" + }, + "accounts_tab": { + "headers": [ + "ID", + "Nom d'utilisateur", + "Mot de passe", + "E-mail", + "Téléphone", + "Nom", + "Plateforme", + "Créé le" + ], + "no_selection_title": "Aucun compte sélectionné", + "no_selection_text": "Veuillez sélectionner un compte à supprimer.", + "delete_title": "Supprimer le compte", + "delete_text": "Voulez-vous vraiment supprimer le compte '{username}' ?", + "delete_success_title": "Succès", + "delete_success_text": "Le compte '{username}' a été supprimé.", + "delete_error_title": "Erreur", + "delete_error_text": "Le compte '{username}' n'a pas pu être supprimé." + }, + "about_dialog": { + "support": "Pour toute assistance, contactez-nous à : support@example.com", + "license": "Ce logiciel est soumis à licence et ne peut être utilisé qu'avec une licence valide." + } +} \ No newline at end of file diff --git a/localization/languages/ja.json b/localization/languages/ja.json new file mode 100644 index 0000000..5ecbc9f --- /dev/null +++ b/localization/languages/ja.json @@ -0,0 +1,102 @@ +{ + "language_name": "日本語", + "main": { + "title": "ソーシャルメディアアカウントジェネレーター", + "subtitle": "プラットフォームを選択", + "version": "バージョン 1.0.0", + "overview": "概要" + }, + "buttons": { + "back": "↩ 戻る", + "create": "アカウント作成", + "cancel": "キャンセル", + "refresh": "更新", + "export": "エクスポート", + "delete": "削除", + "test_proxy": "プロキシテスト", + "save_proxy": "プロキシ設定を保存", + "test_email": "メールをテスト", + "save_email": "メール設定を保存", + "activate_license": "ライセンスを有効化", + "check_updates": "アップデートを確認", + "ok": "OK" + }, + "tabs": { + "generator": "アカウント生成", + "accounts": "アカウント", + "settings": "設定", + "about": "情報" + }, + "menu": { + "about": "情報", + "about_app": "アプリについて", + "language": "言語" + }, + "status": { + "ready": "完了" + }, + "generator_tab": { + "form_title": "アカウント情報", + "first_name_label": "名:", + "first_name_placeholder": "例: Max", + "last_name_label": "姓:", + "last_name_placeholder": "例: Mustermann", + "age_label": "年齢:", + "age_placeholder": "13~99歳", + "registration_method_label": "登録方法:", + "email_radio": "メール", + "phone_radio": "電話", + "phone_label": "電話番号:", + "phone_placeholder": "例: +49123456789", + "email_domain_label": "メールドメイン:", + "proxy_use": "プロキシを使用", + "proxy_label": "プロキシ:", + "proxy_type_label": "タイプ:", + "proxy_type_ipv4": "IPv4", + "proxy_type_ipv6": "IPv6", + "proxy_type_mobile": "モバイル", + "headless": "ブラウザをバックグラウンドで実行", + "debug": "デバッグモード (詳細ログ)", + "log_title": "ログ", + "error_title": "エラー", + "first_name_error": "名を入力してください。", + "last_name_error": "姓を入力してください。", + "age_empty_error": "年齢を入力してください。", + "age_int_error": "年齢は整数である必要があります。", + "age_range_error": "年齢は13から99の間である必要があります。", + "phone_error": "電話番号を入力してください。", + "tiktok_category_label": "カテゴリ/ニッチ:", + "tiktok_category_general": "一般", + "tiktok_category_gaming": "ゲーム", + "tiktok_category_fashion": "ファッション", + "tiktok_category_fitness": "フィットネス", + "tiktok_category_travel": "旅行", + "tiktok_category_cooking": "料理", + "tiktok_category_technology": "テクノロジー", + "tiktok_category_education": "教育" + }, + "accounts_tab": { + "headers": [ + "ID", + "ユーザー名", + "パスワード", + "メール", + "電話", + "名前", + "プラットフォーム", + "作成日" + ], + "no_selection_title": "アカウントが選択されていません", + "no_selection_text": "削除するアカウントを選択してください。", + "delete_title": "アカウントの削除", + "delete_text": "アカウント '{username}' を本当に削除しますか?", + "delete_success_title": "成功", + "delete_success_text": "アカウント '{username}' は削除されました。", + "delete_error_title": "エラー", + "delete_error_text": "アカウント '{username}' を削除できませんでした。" + }, + "about_dialog": { + "support": "サポートが必要な場合は次へご連絡ください: support@example.com", + "license": "このソフトウェアはライセンス制で、有効なライセンスでのみ使用できます。" + } +} \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/logs/main.log b/logs/main.log new file mode 100644 index 0000000..ebe0e40 --- /dev/null +++ b/logs/main.log @@ -0,0 +1,34 @@ +2025-05-05 21:38:41,856 - main - INFO - Anwendung wird gestartet... +2025-05-05 21:57:27,120 - main - INFO - Anwendung wird gestartet... +2025-05-11 14:34:47,588 - main - INFO - Anwendung wird gestartet... +2025-05-11 14:35:09,257 - main - INFO - Plattform ausgewhlt: instagram +2025-05-11 22:14:11,965 - main - INFO - Anwendung wird gestartet... +2025-05-11 22:14:15,499 - main - INFO - Plattform ausgewhlt: tiktok +2025-05-11 22:51:23,397 - main - INFO - Zurck zur Plattformauswahl +2025-05-11 22:55:24,731 - main - INFO - Anwendung wird gestartet... +2025-05-11 22:55:27,782 - main - INFO - Plattform ausgewhlt: tiktok +2025-05-21 00:35:56,633 - main - INFO - Anwendung wird gestartet... +2025-05-21 00:36:03,187 - main - INFO - Plattform ausgewhlt: instagram +2025-05-21 00:36:04,994 - main - INFO - Zurck zur Plattformauswahl +2025-05-21 00:36:07,722 - main - INFO - Plattform ausgewhlt: vk +2025-05-21 00:36:07,722 - main - ERROR - Plattform 'vk' wird nicht untersttzt +2025-05-21 00:36:12,032 - main - INFO - Plattform ausgewhlt: tiktok +2025-05-21 02:02:11,243 - main - INFO - Anwendung wird gestartet... +2025-06-16 19:57:45,252 - main - INFO - Anwendung wird gestartet... +2025-06-16 19:57:54,936 - main - INFO - Plattform ausgewhlt: instagram +2025-06-16 19:57:56,286 - main - INFO - Zurck zur Plattformauswahl +2025-06-16 19:58:06,367 - main - INFO - Plattform ausgewhlt: instagram +2025-06-16 19:58:07,276 - main - INFO - Zurck zur Plattformauswahl +2025-06-16 19:58:07,706 - main - INFO - Plattform ausgewhlt: facebook +2025-06-16 19:58:07,706 - main - ERROR - Plattform 'facebook' wird nicht untersttzt +2025-06-16 19:58:09,011 - main - INFO - Plattform ausgewhlt: tiktok +2025-06-16 19:58:10,828 - main - INFO - Zurck zur Plattformauswahl +2025-06-16 19:58:11,603 - main - INFO - Plattform ausgewhlt: twitter +2025-06-16 19:58:11,603 - main - ERROR - Plattform 'twitter' wird nicht untersttzt +2025-06-16 19:58:13,674 - main - INFO - Plattform ausgewhlt: tiktok +2025-06-16 19:58:14,871 - main - INFO - Zurck zur Plattformauswahl +2025-06-22 17:43:11,967 - main - INFO - Anwendung wird gestartet... +2025-06-22 17:52:27,938 - main - INFO - Plattform ausgewhlt: instagram +2025-06-22 17:52:35,074 - main - INFO - Zurck zur Plattformauswahl +2025-06-22 17:52:48,316 - main - INFO - Plattform ausgewhlt: instagram +2025-06-22 17:52:56,088 - main - INFO - Zurck zur Plattformauswahl diff --git a/main.py b/main.py new file mode 100644 index 0000000..ccc3312 --- /dev/null +++ b/main.py @@ -0,0 +1,46 @@ +""" +Social Media Account Generator - Hauptanwendung (Einstiegspunkt) +""" + +import os +import sys +from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import Qt + +# Stelle sicher, dass das Hauptverzeichnis im Pythonpfad ist +if os.path.dirname(os.path.abspath(__file__)) not in sys.path: + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Import der Hauptcontroller-Klasse +from controllers.main_controller import MainController +from utils.logger import setup_logger + +# Stelle sicher, dass benötigte Verzeichnisse existieren +os.makedirs("logs", exist_ok=True) +os.makedirs("config", exist_ok=True) +os.makedirs(os.path.join("logs", "screenshots"), exist_ok=True) +os.makedirs("resources", exist_ok=True) +os.makedirs(os.path.join("resources", "themes"), exist_ok=True) +os.makedirs(os.path.join("resources", "icons"), exist_ok=True) + +def main(): + """Hauptfunktion für die Anwendung.""" + # Logger initialisieren + logger = setup_logger() + logger.info("Anwendung wird gestartet...") + + # QApplication erstellen + app = QApplication(sys.argv) + + # High DPI Skalierung aktivieren + app.setAttribute(Qt.AA_EnableHighDpiScaling, True) + app.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + # Hauptcontroller initialisieren (mit QApplication-Instanz) + controller = MainController(app) + + # Anwendung starten + sys.exit(app.exec_()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e8aa591 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +# requirements.txt + +# Core dependencies +PyQt5>=5.15.0 +playwright>=1.20.0 +requests>=2.25.0 + +# Database +SQLite3>=3.30.0 + +# Email handling +IMAPClient>=2.1.0 +email>=6.0.0 + +# Utilities +python-dateutil>=2.8.1 +difflib>=3.7.0 + +# Logging +logging>=0.5.1 + +# Type hints +typing>=3.7.4 + +# Web automation and anti-detection +undetected-playwright>=0.1.0 +random-user-agent>=1.0.1 diff --git a/resources/icons/de.svg b/resources/icons/de.svg new file mode 100644 index 0000000..20a017e --- /dev/null +++ b/resources/icons/de.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/en.svg b/resources/icons/en.svg new file mode 100644 index 0000000..016c075 --- /dev/null +++ b/resources/icons/en.svg @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/resources/icons/es.svg b/resources/icons/es.svg new file mode 100644 index 0000000..7570f40 --- /dev/null +++ b/resources/icons/es.svg @@ -0,0 +1,114 @@ + + + \ No newline at end of file diff --git a/resources/icons/facebook.svg b/resources/icons/facebook.svg new file mode 100644 index 0000000..88c6485 --- /dev/null +++ b/resources/icons/facebook.svg @@ -0,0 +1,17 @@ + + + + + Facebook-color + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/fr.svg b/resources/icons/fr.svg new file mode 100644 index 0000000..a035bda --- /dev/null +++ b/resources/icons/fr.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/instagram.svg b/resources/icons/instagram.svg new file mode 100644 index 0000000..b7b9792 --- /dev/null +++ b/resources/icons/instagram.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/ja.svg b/resources/icons/ja.svg new file mode 100644 index 0000000..73fe223 --- /dev/null +++ b/resources/icons/ja.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/moon.svg b/resources/icons/moon.svg new file mode 100644 index 0000000..8dbdf3a --- /dev/null +++ b/resources/icons/moon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/icons/sun.svg b/resources/icons/sun.svg new file mode 100644 index 0000000..1c0898f --- /dev/null +++ b/resources/icons/sun.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/icons/tiktok.svg b/resources/icons/tiktok.svg new file mode 100644 index 0000000..c1b2ac6 --- /dev/null +++ b/resources/icons/tiktok.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/twitter.svg b/resources/icons/twitter.svg new file mode 100644 index 0000000..781cbc6 --- /dev/null +++ b/resources/icons/twitter.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/icons/vk.svg b/resources/icons/vk.svg new file mode 100644 index 0000000..0b9bd6c --- /dev/null +++ b/resources/icons/vk.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/resources/themes/dark.qss b/resources/themes/dark.qss new file mode 100644 index 0000000..cff011d --- /dev/null +++ b/resources/themes/dark.qss @@ -0,0 +1 @@ +/* Auto-generated empty stylesheet */ diff --git a/resources/themes/light.qss b/resources/themes/light.qss new file mode 100644 index 0000000..cff011d --- /dev/null +++ b/resources/themes/light.qss @@ -0,0 +1 @@ +/* Auto-generated empty stylesheet */ diff --git a/social_networks/__init__.py b/social_networks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/__pycache__/__init__.cpython-310.pyc b/social_networks/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b8cc975169749b85988febf7d6eb4be71422448 GIT binary patch literal 153 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 C8R&%o literal 0 HcmV?d00001 diff --git a/social_networks/base_automation.py b/social_networks/base_automation.py new file mode 100644 index 0000000..dac597e --- /dev/null +++ b/social_networks/base_automation.py @@ -0,0 +1,487 @@ +""" +Basis-Automatisierungsklasse für soziale Netzwerke +""" + +import os +import logging +import time +import random +from typing import Dict, List, Optional, Any, Tuple +from abc import ABC, abstractmethod + +from browser.playwright_manager import PlaywrightManager +from utils.proxy_rotator import ProxyRotator +from utils.email_handler import EmailHandler +from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button + +# Konfiguriere Logger +logger = logging.getLogger("base_automation") + +class BaseAutomation(ABC): + """ + Abstrakte Basisklasse für die Automatisierung von sozialen Netzwerken. + Definiert die gemeinsame Schnittstelle für alle Implementierungen. + """ + + def __init__(self, + headless: bool = False, + use_proxy: bool = False, + proxy_type: str = None, + save_screenshots: bool = True, + screenshots_dir: str = None, + slowmo: int = 0, + debug: bool = False, + email_domain: str = "z5m7q9dk3ah2v1plx6ju.com"): + """ + Initialisiert die Basis-Automatisierung. + + Args: + headless: Ob der Browser im Headless-Modus ausgeführt werden soll + use_proxy: Ob ein Proxy verwendet werden soll + proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ + save_screenshots: Ob Screenshots gespeichert werden sollen + screenshots_dir: Verzeichnis für Screenshots + slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging) + debug: Ob Debug-Informationen angezeigt werden sollen + email_domain: Domain für generierte E-Mail-Adressen + """ + self.headless = headless + self.use_proxy = use_proxy + self.proxy_type = proxy_type + self.save_screenshots = save_screenshots + self.slowmo = slowmo + self.debug = debug + self.email_domain = email_domain + + # Verzeichnis für Screenshots + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.screenshots_dir = screenshots_dir or os.path.join(base_dir, "logs", "screenshots") + os.makedirs(self.screenshots_dir, exist_ok=True) + + # Initialisiere Hilfsklassen + self.proxy_rotator = ProxyRotator() + self.email_handler = EmailHandler() + + # Initialisiere TextSimilarity für robustes UI-Element-Matching + self.text_similarity = TextSimilarity(default_threshold=0.75) + + # Playwright-Manager wird bei Bedarf initialisiert + self.browser = None + + # Status und Ergebnis der Automatisierung + self.status = { + "success": False, + "stage": "initialized", + "error": None, + "account_data": {} + } + + # Debug-Logging + if self.debug: + logging.getLogger().setLevel(logging.DEBUG) + + logger.info(f"Basis-Automatisierung initialisiert (Proxy: {use_proxy}, Typ: {proxy_type})") + + def _initialize_browser(self) -> bool: + """ + Initialisiert den Browser mit den entsprechenden Einstellungen. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Proxy-Konfiguration, falls aktiviert + proxy_config = None + if self.use_proxy: + proxy_config = self.proxy_rotator.get_proxy(self.proxy_type) + if not proxy_config: + logger.warning(f"Kein Proxy vom Typ '{self.proxy_type}' verfügbar, verwende direkten Zugriff") + + # Browser initialisieren + self.browser = PlaywrightManager( + headless=self.headless, + proxy=proxy_config, + browser_type="chromium", + screenshots_dir=self.screenshots_dir, + slowmo=self.slowmo + ) + + # Browser starten + self.browser.start() + + logger.info("Browser erfolgreich initialisiert") + return True + + except Exception as e: + logger.error(f"Fehler bei der Browser-Initialisierung: {e}") + self.status["error"] = f"Browser-Initialisierungsfehler: {str(e)}" + return False + + def _close_browser(self) -> None: + """Schließt den Browser und gibt Ressourcen frei.""" + if self.browser: + self.browser.close() + self.browser = None + logger.info("Browser geschlossen") + + def _take_screenshot(self, name: str) -> Optional[str]: + """ + Erstellt einen Screenshot der aktuellen Seite. + + Args: + name: Name für den Screenshot (ohne Dateierweiterung) + + Returns: + Optional[str]: Pfad zum erstellten Screenshot oder None bei Fehler + """ + if not self.save_screenshots: + return None + + try: + if self.browser and hasattr(self.browser, 'take_screenshot'): + return self.browser.take_screenshot(name) + + except Exception as e: + logger.warning(f"Fehler beim Erstellen eines Screenshots: {e}") + + return None + + def _random_delay(self, min_seconds: float = 1.0, max_seconds: float = 3.0) -> None: + """ + Führt eine zufällige Wartezeit aus, um menschliches Verhalten zu simulieren. + + Args: + min_seconds: Minimale Wartezeit in Sekunden + max_seconds: Maximale Wartezeit in Sekunden + """ + delay = random.uniform(min_seconds, max_seconds) + logger.debug(f"Zufällige Wartezeit: {delay:.2f} Sekunden") + time.sleep(delay) + + def _fill_field_fuzzy(self, field_labels: List[str], value: str, fallback_selector: str = None) -> bool: + """ + Füllt ein Formularfeld mit Fuzzy-Text-Matching aus. + + Args: + field_labels: Liste mit möglichen Bezeichnungen des Feldes + value: Einzugebender Wert + fallback_selector: CSS-Selektor für Fallback + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + # Versuche, das Feld mit Fuzzy-Matching zu finden + field = fuzzy_find_element(self.browser.page, field_labels, selector_type="input", threshold=0.6, wait_time=3000) + + if field: + try: + field.fill(value) + return True + except Exception as e: + logger.warning(f"Fehler beim Ausfüllen des Feldes mit Fuzzy-Match: {e}") + # Fallback auf normales Ausfüllen + + # Fallback: Versuche mit dem angegebenen Selektor + if fallback_selector: + field_success = self.browser.fill_form_field(fallback_selector, value) + if field_success: + return True + + return False + + def _click_button_fuzzy(self, button_texts: List[str], fallback_selector: str = None) -> bool: + """ + Klickt einen Button mit Fuzzy-Text-Matching. + + Args: + button_texts: Liste mit möglichen Button-Texten + fallback_selector: CSS-Selektor für Fallback + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + # Versuche, den Button mit Fuzzy-Matching zu finden + success = click_fuzzy_button(self.browser.page, button_texts, threshold=0.6, timeout=3000) + + if success: + return True + + # Fallback: Versuche mit dem angegebenen Selektor + if fallback_selector: + return self.browser.click_element(fallback_selector) + + return False + + def _find_element_by_text(self, texts: List[str], selector_type: str = "any", threshold: float = 0.7) -> Optional[Any]: + """ + Findet ein Element basierend auf Textähnlichkeit. + + Args: + texts: Liste mit möglichen Texten + selector_type: Art des Elements ("button", "link", "input", "any") + threshold: Ähnlichkeitsschwellenwert + + Returns: + Das gefundene Element oder None + """ + return fuzzy_find_element(self.browser.page, texts, selector_type, threshold, wait_time=3000) + + def _check_for_text_on_page(self, texts: List[str], threshold: float = 0.7) -> bool: + """ + Prüft, ob ein Text auf der Seite vorhanden ist. + + Args: + texts: Liste mit zu suchenden Texten + threshold: Ähnlichkeitsschwellenwert + + Returns: + True wenn einer der Texte gefunden wurde, False sonst + """ + # Hole den gesamten Seiteninhalt + try: + page_content = self.browser.page.content() + if not page_content: + return False + + # Versuche, Text im HTML zu finden (einfache Suche) + for text in texts: + if text.lower() in page_content.lower(): + return True + + # Wenn nicht gefunden, versuche über alle sichtbaren Textelemente + elements = self.browser.page.query_selector_all("p, h1, h2, h3, h4, h5, h6, span, div, button, a, label") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip() + + # Prüfe die Textähnlichkeit mit jedem der gesuchten Texte + for text in texts: + if self.text_similarity.is_similar(text, element_text, threshold=threshold): + return True + + return False + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Text auf der Seite: {e}") + return False + + def _check_for_error(self, error_selectors: List[str], error_texts: List[str]) -> Optional[str]: + """ + Prüft, ob Fehlermeldungen angezeigt werden. + + Args: + error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen + error_texts: Liste mit typischen Fehlertexten + + Returns: + Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden + """ + try: + # Prüfe selektoren + for selector in error_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + error_text = element.text_content() + if error_text: + return error_text.strip() + + # Fuzzy-Suche nach Fehlermeldungen + elements = self.browser.page.query_selector_all("p, div[role='alert'], span.error, .error-message") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip() + + # Prüfe, ob der Text einem Fehlermuster ähnelt + for error_text in error_texts: + if self.text_similarity.is_similar(error_text, element_text, threshold=0.6): + return element_text + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}") + return None + + def _attempt_ocr_fallback(self, action_name: str, target_text: str = None, value: str = None) -> bool: + """ + Versucht, eine Aktion mit OCR-Fallback durchzuführen, wenn Playwright fehlschlägt. + + Args: + action_name: Name der Aktion ("click", "type", "select") + target_text: Text, nach dem gesucht werden soll + value: Wert, der eingegeben werden soll (bei "type" oder "select") + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + # Diese Methode wird in abgeleiteten Klassen implementiert + logger.warning(f"OCR-Fallback für '{action_name}' wurde aufgerufen, aber nicht implementiert") + return False + + def _rotate_proxy(self) -> bool: + """ + Rotiert den Proxy und aktualisiert die Browser-Sitzung. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self.use_proxy: + return False + + try: + # Browser schließen + self._close_browser() + + # Proxy rotieren + new_proxy = self.proxy_rotator.rotate_proxy(self.proxy_type) + if not new_proxy: + logger.warning("Konnte Proxy nicht rotieren") + return False + + # Browser neu initialisieren + success = self._initialize_browser() + + logger.info(f"Proxy rotiert zu: {new_proxy['server']}") + return success + + except Exception as e: + logger.error(f"Fehler bei der Proxy-Rotation: {e}") + return False + + def _generate_random_email(self, length: int = 10) -> str: + """ + Generiert eine zufällige E-Mail-Adresse. + + Args: + length: Länge des lokalen Teils der E-Mail + + Returns: + str: Die generierte E-Mail-Adresse + """ + import string + local_chars = string.ascii_lowercase + string.digits + local_part = ''.join(random.choice(local_chars) for _ in range(length)) + return f"{local_part}@{self.email_domain}" + + def _get_confirmation_code(self, email_address: str, search_criteria: str, + max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]: + """ + Ruft einen Bestätigungscode aus einer E-Mail ab. + + Args: + email_address: E-Mail-Adresse, an die der Code gesendet wurde + search_criteria: Suchkriterium für die E-Mail + max_attempts: Maximale Anzahl an Versuchen + delay_seconds: Verzögerung zwischen Versuchen in Sekunden + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + logger.info(f"Suche nach Bestätigungscode für {email_address}") + + code = self.email_handler.get_confirmation_code( + expected_email=email_address, + search_criteria=search_criteria, + max_attempts=max_attempts, + delay_seconds=delay_seconds + ) + + if code: + logger.info(f"Bestätigungscode gefunden: {code}") + else: + logger.warning(f"Kein Bestätigungscode für {email_address} gefunden") + + return code + + def _is_text_similar(self, text1: str, text2: str, threshold: float = None) -> bool: + """ + Prüft, ob zwei Texte ähnlich sind. + + Args: + text1: Erster Text + text2: Zweiter Text + threshold: Ähnlichkeitsschwellenwert (None für Standardwert) + + Returns: + True wenn die Texte ähnlich sind, False sonst + """ + return self.text_similarity.is_similar(text1, text2, threshold) + + @abstractmethod + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Registriert einen neuen Account im sozialen Netzwerk. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + pass + + @abstractmethod + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Meldet sich bei einem bestehenden Account an. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Anmeldung mit Status + """ + pass + + @abstractmethod + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Verifiziert einen Account mit einem Bestätigungscode. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + pass + + def get_status(self) -> Dict[str, Any]: + """ + Gibt den aktuellen Status der Automatisierung zurück. + + Returns: + Dict[str, Any]: Aktueller Status + """ + return self.status + + def __enter__(self): + """Kontext-Manager-Eintritt.""" + self._initialize_browser() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Kontext-Manager-Austritt.""" + self._close_browser() + + +# Wenn direkt ausgeführt, zeige Informationen zur Klasse +if __name__ == "__main__": + print("Dies ist eine abstrakte Basisklasse und kann nicht direkt instanziiert werden.") + print("Bitte verwende eine konkrete Implementierung wie InstagramAutomation.") \ No newline at end of file diff --git a/social_networks/facebook/__init__.py b/social_networks/facebook/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_automation.py b/social_networks/facebook/facebook_automation.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_login.py b/social_networks/facebook/facebook_login.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_registration.py b/social_networks/facebook/facebook_registration.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_selectors.py b/social_networks/facebook/facebook_selectors.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_ui_helper.py b/social_networks/facebook/facebook_ui_helper.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_utils.py b/social_networks/facebook/facebook_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_verification.py b/social_networks/facebook/facebook_verification.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/facebook/facebook_workflow.py b/social_networks/facebook/facebook_workflow.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/instagram/__init__.py b/social_networks/instagram/__init__.py new file mode 100644 index 0000000..893b1a7 --- /dev/null +++ b/social_networks/instagram/__init__.py @@ -0,0 +1,4 @@ +# social_networks/instagram/__init__.py +from .instagram_automation import InstagramAutomation + +__all__ = ['InstagramAutomation'] \ No newline at end of file diff --git a/social_networks/instagram/__pycache__/__init__.cpython-310.pyc b/social_networks/instagram/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c16a8b6c4a0f90f81117a02bad585127e9654887 GIT binary patch literal 249 zcmY*TO$$Lm6rFh#y;za|@D}_4Nkqyl+hist^P#yT+4(#E(%V}339QUh#Ho93 zr*k{!x@I#a5s!SiyTpAf#V?X5MsctcNf1FbF*K)?h-6FiL{wHJtKMleGg`Ff76adU zZpO#p3=hg#UnVTFO5Ro<6ZpSd)Bw2F09WFx@%l~A*`&|bC_S>Bw8FEg#C>oF79@tQ q;mR^}8^twP8BWgck$npofU+t8^bY5uJ}^!kwH%gDVwvBdY4QSD14FF< literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/__init__.cpython-313.pyc b/social_networks/instagram/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59e7d123a0e1f11ac8cf4a32627d7f747f388fc4 GIT binary patch literal 257 zcmey&%ge<81lzOZGn#?)V-N=hn4pZ$VnD`JhG2$ZMsEf$#v(=qhF~Ur#v-P4W=)ot zj6g|E##_RkdBr7(=|zdTj-@5}xrrs2`FTamKsi56=363}Fv<8tRM}hX@$rc{Iq~r; z89oCQGu(14u!?cc$jnX5EJ}=V&QD3z4aiBXEHBDT&nSs0&QH!v%!$uSEh*10$}Wz9 z8ygcJpP83g5+AQuP4BO~Ke2Ga-J3KzH(8`+E4 Gfr0?4-AA?n literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/instagram_automation.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_automation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60ae8807f8e48ddb41bf58557603a88076d5aa37 GIT binary patch literal 9483 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 literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/instagram_login.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_login.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cabc5a4f6e24bf864ed0bddb5f3b4d4f72e1cc1 GIT binary patch literal 13361 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 Bl9m7f literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/instagram_login.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_login.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53ba629273afdd935730a5f04e913ad58958bd38 GIT binary patch literal 21283 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 literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/instagram_registration.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_registration.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27b5ec9752282f80c03a581ce0400f83c1272b89 GIT binary patch literal 27936 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 literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/instagram_selectors.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_selectors.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fae7e36b4eb6e676f2bda782d6b535d9812e5e5 GIT binary patch literal 8602 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 literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/instagram_selectors.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_selectors.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7658ee4dca8d0fad2a6c98f559add8ce16b24dc9 GIT binary patch literal 9228 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 literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/instagram_ui_helper.cpython-310.pyc b/social_networks/instagram/__pycache__/instagram_ui_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..526ff876502eb6ff8d8c82040e0b1cc745fb27c6 GIT binary patch literal 16751 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 literal 0 HcmV?d00001 diff --git a/social_networks/instagram/__pycache__/instagram_utils.cpython-313.pyc b/social_networks/instagram/__pycache__/instagram_utils.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d81d6bb47d4bebdf2b61b414fe5f827212660b54 GIT binary patch literal 20308 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 Xp3&>x`isi`Z_CJ4ut#v4w@d#Ax8$XU literal 0 HcmV?d00001 diff --git a/social_networks/instagram/instagram_automation.py b/social_networks/instagram/instagram_automation.py new file mode 100644 index 0000000..5476585 --- /dev/null +++ b/social_networks/instagram/instagram_automation.py @@ -0,0 +1,330 @@ +# social_networks/instagram/instagram_automation.py + +""" +Instagram-Automatisierung - Hauptklasse für Instagram-Automatisierungsfunktionalität +""" + +import logging +import time +import random +from typing import Dict, List, Any, Optional, Tuple + +from browser.playwright_manager import PlaywrightManager +from browser.playwright_extensions import PlaywrightExtensions +from social_networks.base_automation import BaseAutomation +from utils.password_generator import PasswordGenerator +from utils.username_generator import UsernameGenerator +from utils.birthday_generator import BirthdayGenerator +from utils.human_behavior import HumanBehavior + +# Importiere Helferklassen +from .instagram_registration import InstagramRegistration +from .instagram_login import InstagramLogin +from .instagram_verification import InstagramVerification +from .instagram_ui_helper import InstagramUIHelper +from .instagram_utils import InstagramUtils + +# Konfiguriere Logger +logger = logging.getLogger("instagram_automation") + +class InstagramAutomation(BaseAutomation): + """ + Hauptklasse für die Instagram-Automatisierung. + Implementiert die Registrierung und Anmeldung bei Instagram. + """ + + def __init__(self, + headless: bool = False, + use_proxy: bool = False, + proxy_type: str = None, + save_screenshots: bool = True, + screenshots_dir: str = None, + slowmo: int = 0, + debug: bool = False, + email_domain: str = "z5m7q9dk3ah2v1plx6ju.com", + enhanced_stealth: bool = True, + fingerprint_noise: float = 0.5): + """ + Initialisiert die Instagram-Automatisierung. + + Args: + headless: Ob der Browser im Headless-Modus ausgeführt werden soll + use_proxy: Ob ein Proxy verwendet werden soll + proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ + save_screenshots: Ob Screenshots gespeichert werden sollen + screenshots_dir: Verzeichnis für Screenshots + slowmo: Verzögerung zwischen Aktionen in Millisekunden (nützlich für Debugging) + debug: Ob Debug-Informationen angezeigt werden sollen + email_domain: Domain für generierte E-Mail-Adressen + enhanced_stealth: Ob erweiterter Stealth-Modus aktiviert werden soll + fingerprint_noise: Menge an Rauschen für Fingerprint-Verschleierung (0.0-1.0) + """ + # Initialisiere die Basisklasse + super().__init__( + headless=headless, + use_proxy=use_proxy, + proxy_type=proxy_type, + save_screenshots=save_screenshots, + screenshots_dir=screenshots_dir, + slowmo=slowmo, + debug=debug, + email_domain=email_domain + ) + + # Stealth-Modus-Einstellungen + self.enhanced_stealth = enhanced_stealth + self.fingerprint_noise = max(0.0, min(1.0, fingerprint_noise)) + + # Initialisiere Helferklassen + self.registration = InstagramRegistration(self) + self.login = InstagramLogin(self) + self.verification = InstagramVerification(self) + self.ui_helper = InstagramUIHelper(self) + self.utils = InstagramUtils(self) + + # Zusätzliche Hilfsklassen + self.password_generator = PasswordGenerator() + self.username_generator = UsernameGenerator() + self.birthday_generator = BirthdayGenerator() + self.human_behavior = HumanBehavior(speed_factor=0.8, randomness=0.6) + + logger.info("Instagram-Automatisierung initialisiert") + + def _initialize_browser(self) -> bool: + """ + Initialisiert den Browser mit den entsprechenden Einstellungen. + Diese Methode überschreibt die Methode der Basisklasse, um den erweiterten + Fingerprint-Schutz zu aktivieren. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Proxy-Konfiguration, falls aktiviert + proxy_config = None + if self.use_proxy: + proxy_config = self.proxy_rotator.get_proxy(self.proxy_type) + if not proxy_config: + logger.warning(f"Kein Proxy vom Typ '{self.proxy_type}' verfügbar, verwende direkten Zugriff") + + # Browser initialisieren + self.browser = PlaywrightManager( + headless=self.headless, + proxy=proxy_config, + browser_type="chromium", + screenshots_dir=self.screenshots_dir, + slowmo=self.slowmo + ) + + # Browser starten + self.browser.start() + + # Erweiterten Fingerprint-Schutz aktivieren, wenn gewünscht + if self.enhanced_stealth: + # Erstelle Extensions-Objekt + extensions = PlaywrightExtensions(self.browser) + + # Methoden anhängen + extensions.hook_into_playwright_manager() + + # Fingerprint-Schutz aktivieren mit angepasster Konfiguration + fingerprint_config = { + "noise_level": self.fingerprint_noise, + "canvas_noise": True, + "audio_noise": True, + "webgl_noise": True, + "hardware_concurrency": random.choice([4, 6, 8]), + "device_memory": random.choice([4, 8]), + "timezone_id": "Europe/Berlin" + } + + success = self.browser.enable_enhanced_fingerprint_protection(fingerprint_config) + if success: + logger.info("Erweiterter Fingerprint-Schutz erfolgreich aktiviert") + else: + logger.warning("Erweiterter Fingerprint-Schutz konnte nicht aktiviert werden") + + logger.info("Browser erfolgreich initialisiert") + return True + + except Exception as e: + logger.error(f"Fehler bei der Browser-Initialisierung: {e}") + self.status["error"] = f"Browser-Initialisierungsfehler: {str(e)}" + return False + + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Registriert einen neuen Instagram-Account. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + logger.info(f"Starte Instagram-Account-Registrierung für '{full_name}' via {registration_method}") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Rotiere Fingerprint vor der Hauptaktivität, um Erkennung weiter zu erschweren + if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'): + self.browser.rotate_fingerprint() + logger.info("Browser-Fingerprint vor der Registrierung rotiert") + + # Delegiere die Hauptregistrierungslogik an die Registration-Klasse + result = self.registration.register_account(full_name, age, registration_method, phone_number, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"registration_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Account-Registrierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"registration_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Meldet sich bei einem bestehenden Instagram-Account an. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Anmeldung mit Status + """ + logger.info(f"Starte Instagram-Login für '{username_or_email}'") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Rotiere Fingerprint vor dem Login + if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'): + self.browser.rotate_fingerprint() + logger.info("Browser-Fingerprint vor dem Login rotiert") + + # Delegiere die Hauptlogin-Logik an die Login-Klasse + result = self.login.login_account(username_or_email, password, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"login_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Login: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"login_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Verifiziert einen Instagram-Account mit einem Bestätigungscode. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + logger.info(f"Starte Instagram-Account-Verifizierung mit Code: {verification_code}") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Delegiere die Hauptverifizierungslogik an die Verification-Klasse + result = self.verification.verify_account(verification_code, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"verification_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Account-Verifizierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"verification_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def get_fingerprint_status(self) -> Dict[str, Any]: + """ + Gibt den aktuellen Status des Fingerprint-Schutzes zurück. + + Returns: + Dict[str, Any]: Status des Fingerprint-Schutzes + """ + if not self.enhanced_stealth or not hasattr(self.browser, 'get_fingerprint_status'): + return { + "active": False, + "message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert" + } + + return self.browser.get_fingerprint_status() \ No newline at end of file diff --git a/social_networks/instagram/instagram_login.py b/social_networks/instagram/instagram_login.py new file mode 100644 index 0000000..f28c926 --- /dev/null +++ b/social_networks/instagram/instagram_login.py @@ -0,0 +1,576 @@ +# social_networks/instagram/instagram_login.py + +""" +Instagram-Login - Klasse für die Anmeldefunktionalität bei Instagram +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple + +from .instagram_selectors import InstagramSelectors +from .instagram_workflow import InstagramWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("instagram_login") + +class InstagramLogin: + """ + Klasse für die Anmeldung bei Instagram-Konten. + Enthält alle Methoden für den Login-Prozess. + """ + + def __init__(self, automation): + """ + Initialisiert die Instagram-Login-Funktionalität. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + self.workflow = InstagramWorkflow.get_login_workflow() + + logger.debug("Instagram-Login initialisiert") + + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Führt den Login-Prozess für ein Instagram-Konto durch. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis des Logins mit Status + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere die Eingaben + if not self._validate_login_inputs(username_or_email, password): + return { + "success": False, + "error": "Ungültige Login-Eingaben", + "stage": "input_validation" + } + + # Account-Daten für die Anmeldung + account_data = { + "username": username_or_email, + "password": password, + "handle_2fa": kwargs.get("handle_2fa", False), + "two_factor_code": kwargs.get("two_factor_code"), + "skip_save_login": kwargs.get("skip_save_login", True) + } + + logger.info(f"Starte Instagram-Login für {username_or_email}") + + try: + # 1. Zur Login-Seite navigieren + if not self._navigate_to_login_page(): + return { + "success": False, + "error": "Konnte nicht zur Login-Seite navigieren", + "stage": "navigation" + } + + # 2. Cookie-Banner behandeln + self._handle_cookie_banner() + + # 3. Login-Formular ausfüllen + if not self._fill_login_form(account_data): + return { + "success": False, + "error": "Fehler beim Ausfüllen des Login-Formulars", + "stage": "login_form" + } + + # 4. Auf 2FA prüfen und behandeln, falls nötig + needs_2fa, two_fa_error = self._check_needs_two_factor_auth() + + if needs_2fa: + if not account_data["handle_2fa"]: + return { + "success": False, + "error": "Zwei-Faktor-Authentifizierung erforderlich, aber nicht aktiviert", + "stage": "two_factor_required" + } + + # 2FA behandeln + if not self._handle_two_factor_auth(account_data["two_factor_code"]): + return { + "success": False, + "error": "Fehler bei der Zwei-Faktor-Authentifizierung", + "stage": "two_factor_auth" + } + + # 5. "Anmeldedaten speichern"-Dialog behandeln + if account_data["skip_save_login"]: + self._handle_save_login_prompt() + + # 6. Benachrichtigungsdialog behandeln + self._handle_notifications_prompt() + + # 7. Erfolgreichen Login überprüfen + if not self._check_login_success(): + error_message = self._get_login_error() + return { + "success": False, + "error": f"Login fehlgeschlagen: {error_message or 'Unbekannter Fehler'}", + "stage": "login_check" + } + + # Login erfolgreich + logger.info(f"Instagram-Login für {username_or_email} erfolgreich") + + return { + "success": True, + "stage": "completed", + "username": username_or_email + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Instagram-Login: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception" + } + + def _validate_login_inputs(self, username_or_email: str, password: str) -> bool: + """ + Validiert die Eingaben für den Login. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + + Returns: + bool: True wenn alle Eingaben gültig sind, False sonst + """ + if not username_or_email or len(username_or_email) < 3: + logger.error("Ungültiger Benutzername oder E-Mail") + return False + + if not password or len(password) < 6: + logger.error("Ungültiges Passwort") + return False + + return True + + def _navigate_to_login_page(self) -> bool: + """ + Navigiert zur Instagram-Login-Seite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Zur Login-Seite navigieren + self.browser.navigate_to(InstagramSelectors.LOGIN_URL) + + # Warten, bis die Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Screenshot erstellen + self.automation._take_screenshot("login_page") + + # Prüfen, ob Login-Formular sichtbar ist + if not self.browser.is_element_visible(InstagramSelectors.LOGIN_USERNAME_FIELD, timeout=5000): + logger.warning("Login-Formular nicht sichtbar") + return False + + logger.info("Erfolgreich zur Login-Seite navigiert") + return True + + except Exception as e: + logger.error(f"Fehler beim Navigieren zur Login-Seite: {e}") + return False + + def _handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("reject_cookies"), + InstagramSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(InstagramSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + def _fill_login_form(self, account_data: Dict[str, Any]) -> bool: + """ + Füllt das Login-Formular aus und sendet es ab. + + Args: + account_data: Account-Daten für den Login + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Benutzername/E-Mail eingeben + username_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("username"), + account_data["username"], + InstagramSelectors.LOGIN_USERNAME_FIELD + ) + + if not username_success: + logger.error("Konnte Benutzername-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Passwort eingeben + password_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("password"), + account_data["password"], + InstagramSelectors.LOGIN_PASSWORD_FIELD + ) + + if not password_success: + logger.error("Konnte Passwort-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot vorm Absenden + self.automation._take_screenshot("login_form_filled") + + # Formular absenden + submit_success = self.automation.ui_helper.click_button_fuzzy( + ["Anmelden", "Log in", "Login"], + InstagramSelectors.SUBMIT_BUTTON + ) + + if not submit_success: + logger.error("Konnte Login-Formular nicht absenden") + return False + + # Nach dem Absenden warten + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Überprüfen, ob es eine Fehlermeldung gab + error_message = self._get_login_error() + if error_message: + logger.error(f"Login-Fehler erkannt: {error_message}") + return False + + logger.info("Login-Formular erfolgreich ausgefüllt und abgesendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen des Login-Formulars: {e}") + return False + + def _get_login_error(self) -> Optional[str]: + """ + Überprüft, ob eine Login-Fehlermeldung angezeigt wird. + + Returns: + Optional[str]: Fehlermeldung oder None, wenn keine gefunden wurde + """ + try: + # Auf Fehlermeldungen prüfen + error_selectors = [ + InstagramSelectors.ERROR_MESSAGE, + "p[class*='error']", + "div[role='alert']", + "p[data-testid='login-error-message']" + ] + + for selector in error_selectors: + error_element = self.browser.wait_for_selector(selector, timeout=2000) + if error_element: + error_text = error_element.text_content() + if error_text and len(error_text.strip()) > 0: + return error_text.strip() + + # Wenn keine spezifische Fehlermeldung gefunden wurde, nach bekannten Fehlermustern suchen + error_texts = [ + "Falsches Passwort", + "Benutzername nicht gefunden", + "incorrect password", + "username you entered doesn't belong", + "please wait a few minutes", + "try again later", + "Bitte warte einige Minuten", + "versuche es später noch einmal" + ] + + page_content = self.browser.page.content() + for error_text in error_texts: + if error_text.lower() in page_content.lower(): + return f"Erkannter Fehler: {error_text}" + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Login-Fehler: {e}") + return None + + def _check_needs_two_factor_auth(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob eine Zwei-Faktor-Authentifizierung erforderlich ist. + + Returns: + Tuple[bool, Optional[str]]: (2FA erforderlich, Fehlermeldung falls vorhanden) + """ + try: + # Nach 2FA-Indikatoren suchen + two_fa_selectors = [ + "input[name='verificationCode']", + "input[aria-label='Sicherheitscode']", + "input[aria-label='Security code']", + "input[placeholder*='code']" + ] + + for selector in two_fa_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Zwei-Faktor-Authentifizierung erforderlich") + return True, None + + # Texte, die auf 2FA hinweisen + two_fa_indicators = InstagramSelectors.get_two_fa_indicators() + + # Seiteninhalt durchsuchen + page_content = self.browser.page.content() + + for indicator in two_fa_indicators: + if indicator.lower() in page_content.lower(): + logger.info(f"Zwei-Faktor-Authentifizierung erkannt durch Text: {indicator}") + return True, None + + return False, None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf 2FA: {e}") + return False, f"Fehler bei der 2FA-Erkennung: {str(e)}" + + def _handle_two_factor_auth(self, two_factor_code: Optional[str] = None) -> bool: + """ + Behandelt die Zwei-Faktor-Authentifizierung. + + Args: + two_factor_code: Optional vorhandener 2FA-Code + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Screenshot erstellen + self.automation._take_screenshot("two_factor_auth") + + # 2FA-Eingabefeld finden + two_fa_selectors = [ + "input[name='verificationCode']", + "input[aria-label='Sicherheitscode']", + "input[aria-label='Security code']", + "input[placeholder*='code']" + ] + + two_fa_field = None + for selector in two_fa_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + two_fa_field = selector + break + + if not two_fa_field: + logger.error("Konnte 2FA-Eingabefeld nicht finden") + return False + + # Wenn kein Code bereitgestellt wurde, Benutzer auffordern + if not two_factor_code: + logger.warning("Kein 2FA-Code bereitgestellt, kann nicht fortfahren") + return False + + # 2FA-Code eingeben + code_success = self.browser.fill_form_field(two_fa_field, two_factor_code) + + if not code_success: + logger.error("Konnte 2FA-Code nicht eingeben") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Bestätigen-Button finden und klicken + confirm_button_selectors = [ + "button[type='submit']", + "//button[contains(text(), 'Bestätigen')]", + "//button[contains(text(), 'Confirm')]", + "//button[contains(text(), 'Verify')]" + ] + + confirm_clicked = False + for selector in confirm_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + confirm_clicked = True + break + + if not confirm_clicked: + # Alternative: Mit Tastendruck bestätigen + self.browser.page.keyboard.press("Enter") + logger.info("Enter-Taste gedrückt, um 2FA zu bestätigen") + + # Warten nach der Bestätigung + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Überprüfen, ob 2FA erfolgreich war + still_on_2fa = self._check_needs_two_factor_auth()[0] + + if still_on_2fa: + # Prüfen, ob Fehlermeldung angezeigt wird + error_message = self._get_login_error() + if error_message: + logger.error(f"2FA-Fehler: {error_message}") + else: + logger.error("2FA fehlgeschlagen, immer noch auf 2FA-Seite") + return False + + logger.info("Zwei-Faktor-Authentifizierung erfolgreich") + return True + + except Exception as e: + logger.error(f"Fehler bei der Zwei-Faktor-Authentifizierung: {e}") + return False + + def _handle_save_login_prompt(self) -> bool: + """ + Behandelt den "Anmeldedaten speichern"-Dialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Nach "Nicht jetzt"-Button suchen + not_now_selectors = [ + "//button[contains(text(), 'Nicht jetzt')]", + "//button[contains(text(), 'Not now')]", + "//button[contains(text(), 'Skip')]", + "//button[contains(text(), 'Überspringen')]" + ] + + for selector in not_now_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + if self.browser.click_element(selector): + logger.info("'Anmeldedaten speichern'-Dialog übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden + logger.debug("Kein 'Anmeldedaten speichern'-Dialog erkannt") + return True + + except Exception as e: + logger.warning(f"Fehler beim Behandeln des 'Anmeldedaten speichern'-Dialogs: {e}") + # Dies ist nicht kritisch, daher geben wir trotzdem True zurück + return True + + def _handle_notifications_prompt(self) -> bool: + """ + Behandelt den Benachrichtigungen-Dialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Nach "Nicht jetzt"-Button suchen + not_now_selectors = [ + "//button[contains(text(), 'Nicht jetzt')]", + "//button[contains(text(), 'Not now')]", + "//button[contains(text(), 'Skip')]", + "//button[contains(text(), 'Später')]", + "//button[contains(text(), 'Later')]" + ] + + for selector in not_now_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + if self.browser.click_element(selector): + logger.info("Benachrichtigungen-Dialog übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden + logger.debug("Kein Benachrichtigungen-Dialog erkannt") + return True + + except Exception as e: + logger.warning(f"Fehler beim Behandeln des Benachrichtigungen-Dialogs: {e}") + # Dies ist nicht kritisch, daher geben wir trotzdem True zurück + return True + + def _check_login_success(self) -> bool: + """ + Überprüft, ob der Login erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Warten nach dem Login + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Screenshot erstellen + self.automation._take_screenshot("login_final") + + # Erfolg anhand verschiedener Indikatoren prüfen + success_indicators = InstagramSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=2000): + logger.info(f"Login-Erfolgsindikator gefunden: {indicator}") + return True + + # Alternativ prüfen, ob wir auf der Instagram-Startseite sind + current_url = self.browser.page.url + if "instagram.com" in current_url and "/accounts/login" not in current_url: + logger.info(f"Login-Erfolg basierend auf URL: {current_url}") + return True + + # Prüfen, ob immer noch auf der Login-Seite + if "/accounts/login" in current_url or self.browser.is_element_visible(InstagramSelectors.LOGIN_USERNAME_FIELD, timeout=1000): + logger.warning("Immer noch auf der Login-Seite, Login fehlgeschlagen") + return False + + logger.warning("Keine Login-Erfolgsindikatoren gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Login-Erfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/instagram/instagram_registration.py b/social_networks/instagram/instagram_registration.py new file mode 100644 index 0000000..39ca88d --- /dev/null +++ b/social_networks/instagram/instagram_registration.py @@ -0,0 +1,735 @@ +# social_networks/instagram/instagram_registration.py + +""" +Instagram-Registrierung - Klasse für die Kontoerstellung bei Instagram +""" + +import logging +import time +import random +import re +from typing import Dict, List, Any, Optional, Tuple + +from .instagram_selectors import InstagramSelectors +from .instagram_workflow import InstagramWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("instagram_registration") + +class InstagramRegistration: + """ + Klasse für die Registrierung von Instagram-Konten. + Enthält alle Methoden zur Kontoerstellung. + """ + + def __init__(self, automation): + """ + Initialisiert die Instagram-Registrierung. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + self.workflow = InstagramWorkflow.get_registration_workflow() + + logger.debug("Instagram-Registrierung initialisiert") + + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Führt den vollständigen Registrierungsprozess für einen Instagram-Account durch. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere die Eingaben + if not self._validate_registration_inputs(full_name, age, registration_method, phone_number): + return { + "success": False, + "error": "Ungültige Eingabeparameter", + "stage": "input_validation" + } + + # Account-Daten generieren + account_data = self._generate_account_data(full_name, age, registration_method, phone_number, **kwargs) + + # Starte den Registrierungsprozess + logger.info(f"Starte Instagram-Registrierung für {account_data['username']} via {registration_method}") + + try: + # 1. Zur Registrierungsseite navigieren + if not self._navigate_to_signup_page(): + return { + "success": False, + "error": "Konnte nicht zur Registrierungsseite navigieren", + "stage": "navigation", + "account_data": account_data + } + + # 2. Cookie-Banner behandeln + self._handle_cookie_banner() + + # 3. Registrierungsmethode wählen + if not self._select_registration_method(registration_method): + return { + "success": False, + "error": f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen", + "stage": "registration_method", + "account_data": account_data + } + + # 4. Registrierungsformular ausfüllen + if not self._fill_registration_form(account_data): + return { + "success": False, + "error": "Fehler beim Ausfüllen des Registrierungsformulars", + "stage": "registration_form", + "account_data": account_data + } + + # 5. Geburtsdatum eingeben + if not self._select_birthday(account_data["birthday"]): + return { + "success": False, + "error": "Fehler beim Eingeben des Geburtsdatums", + "stage": "birthday", + "account_data": account_data + } + + # 6. Bestätigungscode abrufen und eingeben + if not self._handle_verification(account_data, registration_method): + return { + "success": False, + "error": "Fehler bei der Verifizierung", + "stage": "verification", + "account_data": account_data + } + + # 7. Erfolgreiche Registrierung überprüfen + if not self._check_registration_success(): + return { + "success": False, + "error": "Registrierung fehlgeschlagen oder konnte nicht verifiziert werden", + "stage": "final_check", + "account_data": account_data + } + + # Registrierung erfolgreich abgeschlossen + logger.info(f"Instagram-Account {account_data['username']} erfolgreich erstellt") + + return { + "success": True, + "stage": "completed", + "account_data": account_data + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Instagram-Registrierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception", + "account_data": account_data + } + + def _validate_registration_inputs(self, full_name: str, age: int, + registration_method: str, phone_number: str) -> bool: + """ + Validiert die Eingaben für die Registrierung. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + + Returns: + bool: True wenn alle Eingaben gültig sind, False sonst + """ + # Vollständiger Name prüfen + if not full_name or len(full_name) < 3: + logger.error("Ungültiger vollständiger Name") + return False + + # Alter prüfen + if age < 13: + logger.error("Benutzer muss mindestens 13 Jahre alt sein") + return False + + # Registrierungsmethode prüfen + if registration_method not in ["email", "phone"]: + logger.error(f"Ungültige Registrierungsmethode: {registration_method}") + return False + + # Telefonnummer prüfen, falls erforderlich + if registration_method == "phone" and not phone_number: + logger.error("Telefonnummer erforderlich für Registrierung via Telefon") + return False + + return True + + def _generate_account_data(self, full_name: str, age: int, registration_method: str, + phone_number: str, **kwargs) -> Dict[str, Any]: + """ + Generiert Account-Daten für die Registrierung. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Generierte Account-Daten + """ + # Benutzername generieren + username = kwargs.get("username") + if not username: + username = self.automation.username_generator.generate_username("instagram", full_name) + + # Passwort generieren + password = kwargs.get("password") + if not password: + password = self.automation.password_generator.generate_password("instagram") + + # E-Mail generieren (falls nötig) + email = None + if registration_method == "email": + email_prefix = username.lower().replace(".", "").replace("_", "") + email = f"{email_prefix}@{self.automation.email_domain}" + + # Geburtsdatum generieren + birthday = self.automation.birthday_generator.generate_birthday_components("instagram", age) + + # Account-Daten zusammenstellen + account_data = { + "username": username, + "password": password, + "full_name": full_name, + "email": email, + "phone": phone_number, + "birthday": birthday, + "age": age, + "registration_method": registration_method + } + + logger.debug(f"Account-Daten generiert: {account_data['username']}") + + return account_data + + def _navigate_to_signup_page(self) -> bool: + """ + Navigiert zur Instagram-Registrierungsseite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Zur Registrierungsseite navigieren + self.browser.navigate_to(InstagramSelectors.SIGNUP_URL) + + # Warten, bis die Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Screenshot erstellen + self.automation._take_screenshot("signup_page") + + # Prüfen, ob Registrierungsformular sichtbar ist + if not self.browser.is_element_visible(InstagramSelectors.EMAIL_PHONE_FIELD, timeout=5000): + logger.warning("Registrierungsformular nicht sichtbar") + return False + + logger.info("Erfolgreich zur Registrierungsseite navigiert") + return True + + except Exception as e: + logger.error(f"Fehler beim Navigieren zur Registrierungsseite: {e}") + return False + + def _handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("reject_cookies"), + InstagramSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(InstagramSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + def _select_registration_method(self, method: str) -> bool: + """ + Wählt die Registrierungsmethode (E-Mail oder Telefon). + + Args: + method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + if method == "email": + # Prüfen, ob der E-Mail-Tab ausgewählt werden muss + if self.browser.is_element_visible("//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]"): + success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_tab_texts("email"), + InstagramSelectors.EMAIL_TAB + ) + + if success: + logger.info("E-Mail-Registrierungsmethode ausgewählt") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + else: + logger.warning("Konnte E-Mail-Tab nicht auswählen") + return False + + # E-Mail ist vermutlich bereits ausgewählt + logger.debug("E-Mail-Registrierung erscheint bereits ausgewählt") + return True + + elif method == "phone": + # Prüfen, ob der Telefon-Tab sichtbar ist und klicken + if self.browser.is_element_visible("//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]"): + success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_tab_texts("phone"), + InstagramSelectors.PHONE_TAB + ) + + if success: + logger.info("Telefon-Registrierungsmethode ausgewählt") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + else: + logger.warning("Konnte Telefon-Tab nicht auswählen") + return False + + # Telefon ist möglicherweise bereits ausgewählt + logger.debug("Telefon-Registrierung erscheint bereits ausgewählt") + return True + + logger.error(f"Ungültige Registrierungsmethode: {method}") + return False + + except Exception as e: + logger.error(f"Fehler beim Auswählen der Registrierungsmethode: {e}") + return False + + def _fill_registration_form(self, account_data: Dict[str, Any]) -> bool: + """ + Füllt das Registrierungsformular aus und sendet es ab. + + Args: + account_data: Account-Daten für die Registrierung + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # E-Mail/Telefon eingeben + if account_data["registration_method"] == "email": + email_phone_value = account_data["email"] + else: + email_phone_value = account_data["phone"] + + # E-Mail-/Telefon-Feld ausfüllen + email_phone_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("email_phone"), + email_phone_value, + InstagramSelectors.EMAIL_PHONE_FIELD + ) + + if not email_phone_success: + logger.error("Konnte E-Mail/Telefon-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Vollständigen Namen eingeben + name_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("full_name"), + account_data["full_name"], + InstagramSelectors.FULLNAME_FIELD + ) + + if not name_success: + logger.error("Konnte Vollständiger-Name-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Benutzernamen eingeben + username_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("username"), + account_data["username"], + InstagramSelectors.USERNAME_FIELD + ) + + if not username_success: + logger.error("Konnte Benutzername-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Passwort eingeben + password_success = self.automation.ui_helper.fill_field_fuzzy( + InstagramSelectors.get_field_labels("password"), + account_data["password"], + InstagramSelectors.PASSWORD_FIELD + ) + + if not password_success: + logger.error("Konnte Passwort-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot vorm Absenden + self.automation._take_screenshot("registration_form_filled") + + # Formular absenden + submit_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("submit"), + InstagramSelectors.SUBMIT_BUTTON + ) + + if not submit_success: + logger.error("Konnte Registrierungsformular nicht absenden") + return False + + # Prüfen, ob es Fehler gab + self.automation.human_behavior.wait_for_page_load() + + # Nach dem Absenden prüfen, ob das Formular für das Geburtsdatum erscheint + birthday_visible = self.browser.is_element_visible(InstagramSelectors.BIRTHDAY_MONTH_SELECT, timeout=10000) + + if not birthday_visible: + # Auf mögliche Fehlermeldung prüfen + error_message = self.automation.ui_helper.check_for_error( + error_selectors=[InstagramSelectors.ERROR_MESSAGE], + error_texts=InstagramSelectors.get_error_indicators() + ) + + if error_message: + logger.error(f"Fehler beim Absenden des Formulars: {error_message}") + return False + + logger.error("Geburtstagsformular nicht sichtbar nach Absenden") + return False + + logger.info("Registrierungsformular erfolgreich ausgefüllt und abgesendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen des Registrierungsformulars: {e}") + return False + + def _select_birthday(self, birthday: Dict[str, int]) -> bool: + """ + Wählt das Geburtsdatum aus den Dropdown-Menüs aus. + + Args: + birthday: Geburtsdatum als Dictionary mit 'year', 'month', 'day' Keys + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Dropdowns sichtbar sind + if not self.browser.is_element_visible(InstagramSelectors.BIRTHDAY_MONTH_SELECT, timeout=5000): + logger.error("Geburtstags-Dropdowns nicht sichtbar") + return False + + # Screenshot zu Beginn + self.automation._take_screenshot("birthday_form") + + # Monat auswählen + month_success = self.browser.select_option( + InstagramSelectors.BIRTHDAY_MONTH_SELECT, + str(birthday["month"]) + ) + + if not month_success: + logger.error(f"Konnte Monat nicht auswählen: {birthday['month']}") + return False + + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Tag auswählen + day_success = self.browser.select_option( + InstagramSelectors.BIRTHDAY_DAY_SELECT, + str(birthday["day"]) + ) + + if not day_success: + logger.error(f"Konnte Tag nicht auswählen: {birthday['day']}") + return False + + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Jahr auswählen + year_success = self.browser.select_option( + InstagramSelectors.BIRTHDAY_YEAR_SELECT, + str(birthday["year"]) + ) + + if not year_success: + logger.error(f"Konnte Jahr nicht auswählen: {birthday['year']}") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot vor dem Absenden + self.automation._take_screenshot("birthday_selected") + + # Weiter-Button klicken + next_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("next"), + InstagramSelectors.NEXT_BUTTON + ) + + if not next_success: + logger.error("Konnte Weiter-Button nicht klicken") + return False + + # Prüfen, ob es zum Bestätigungscode-Formular weitergeht + self.automation.human_behavior.wait_for_page_load() + + # Nach dem Absenden auf den Bestätigungscode-Bildschirm warten + confirmation_visible = self.browser.is_element_visible( + InstagramSelectors.CONFIRMATION_CODE_FIELD, timeout=10000 + ) or self.browser.is_element_visible( + InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, timeout=2000 + ) + + if not confirmation_visible: + # Auf mögliche Fehlermeldung prüfen + error_message = self.automation.ui_helper.check_for_error( + error_selectors=[InstagramSelectors.ERROR_MESSAGE], + error_texts=InstagramSelectors.get_error_indicators() + ) + + if error_message: + logger.error(f"Fehler nach dem Absenden des Geburtsdatums: {error_message}") + return False + + logger.error("Bestätigungscode-Formular nicht sichtbar nach Absenden des Geburtsdatums") + return False + + logger.info("Geburtsdatum erfolgreich ausgewählt und abgesendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Auswählen des Geburtsdatums: {e}") + return False + + def _handle_verification(self, account_data: Dict[str, Any], registration_method: str) -> bool: + """ + Behandelt den Verifizierungsprozess (E-Mail/SMS). + + Args: + account_data: Account-Daten mit E-Mail/Telefon + registration_method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Bestätigungscode-Eingabe sichtbar ist + if not self.browser.is_element_visible(InstagramSelectors.CONFIRMATION_CODE_FIELD, timeout=5000) and not \ + self.browser.is_element_visible(InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, timeout=1000): + logger.error("Bestätigungscode-Eingabe nicht sichtbar") + return False + + # Screenshot erstellen + self.automation._take_screenshot("verification_page") + + # Verifizierungscode je nach Methode abrufen + if registration_method == "email": + # Verifizierungscode von E-Mail abrufen + verification_code = self._get_email_confirmation_code(account_data["email"]) + else: + # Verifizierungscode von SMS abrufen + verification_code = self._get_sms_confirmation_code(account_data["phone"]) + + if not verification_code: + logger.error("Konnte keinen Verifizierungscode abrufen") + return False + + logger.info(f"Verifizierungscode erhalten: {verification_code}") + + # Verifizierungscode eingeben und absenden + return self.automation.verification.enter_and_submit_verification_code(verification_code) + + except Exception as e: + logger.error(f"Fehler bei der Verifizierung: {e}") + return False + + def _get_email_confirmation_code(self, email: str) -> Optional[str]: + """ + Ruft den Bestätigungscode von einer E-Mail ab. + + Args: + email: E-Mail-Adresse, an die der Code gesendet wurde + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + try: + # Warte auf die E-Mail + verification_code = self.automation.email_handler.get_verification_code( + email_domain=self.automation.email_domain, + platform="instagram", + timeout=120 # Warte bis zu 2 Minuten auf den Code + ) + + if verification_code: + return verification_code + + # Wenn kein Code gefunden wurde, prüfen, ob der Code vielleicht direkt angezeigt wird + verification_code = self._extract_code_from_page() + + if verification_code: + logger.info(f"Verifizierungscode direkt von der Seite extrahiert: {verification_code}") + return verification_code + + logger.warning(f"Konnte keinen Verifizierungscode für {email} finden") + return None + + except Exception as e: + logger.error(f"Fehler beim Abrufen des E-Mail-Bestätigungscodes: {e}") + return None + + def _get_sms_confirmation_code(self, phone: str) -> Optional[str]: + """ + Ruft den Bestätigungscode aus einer SMS ab. + Hier müsste ein SMS-Empfangs-Service eingebunden werden. + + Args: + phone: Telefonnummer, an die der Code gesendet wurde + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + # Diese Implementierung ist ein Platzhalter + # In einer echten Implementierung würde hier ein SMS-Empfangs-Service verwendet + logger.warning("SMS-Verifizierung ist noch nicht implementiert") + + # Versuche, den Code trotzdem zu extrahieren, falls er auf der Seite angezeigt wird + return self._extract_code_from_page() + + def _extract_code_from_page(self) -> Optional[str]: + """ + Versucht, einen Bestätigungscode direkt von der Seite zu extrahieren. + + Returns: + Optional[str]: Der extrahierte Code oder None, wenn nicht gefunden + """ + try: + # Gesamten Seiteninhalt abrufen + page_content = self.browser.page.content() + + # Mögliche Regex-Muster für Bestätigungscodes + patterns = [ + r"Dein Code ist (\d{6})", + r"Your code is (\d{6})", + r"Bestätigungscode: (\d{6})", + r"Confirmation code: (\d{6})", + r"(\d{6}) ist dein Instagram-Code", + r"(\d{6}) is your Instagram code" + ] + + for pattern in patterns: + match = re.search(pattern, page_content) + if match: + return match.group(1) + + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Codes von der Seite: {e}") + return None + + def _check_registration_success(self) -> bool: + """ + Überprüft, ob die Registrierung erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Warten nach der Verifizierung + self.automation.human_behavior.wait_for_page_load(multiplier=2.0) + + # Screenshot erstellen + self.automation._take_screenshot("registration_final") + + # Erfolg anhand verschiedener Indikatoren prüfen + success_indicators = InstagramSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=2000): + logger.info(f"Erfolgsindikator gefunden: {indicator}") + return True + + # Alternativ prüfen, ob wir auf der Instagram-Startseite sind + current_url = self.browser.page.url + if "instagram.com" in current_url and "/accounts/" not in current_url: + logger.info(f"Erfolg basierend auf URL: {current_url}") + return True + + # Prüfen, ob wir auf weitere Einrichtungsschritte weitergeleitet wurden + # (z.B. "Folge anderen Benutzern" oder "Füge ein Profilbild hinzu") + # Diese sind optional und gelten als erfolgreiche Registrierung + if self.automation.ui_helper.check_for_next_steps(): + logger.info("Auf weitere Einrichtungsschritte weitergeleitet, Registrierung erfolgreich") + return True + + logger.warning("Keine Erfolgsindikatoren gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Registrierungserfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/instagram/instagram_selectors.py b/social_networks/instagram/instagram_selectors.py new file mode 100644 index 0000000..0c64e61 --- /dev/null +++ b/social_networks/instagram/instagram_selectors.py @@ -0,0 +1,242 @@ +""" +Instagram-Selektoren - CSS-Selektoren und XPath-Ausdrücke für die Instagram-Automatisierung +Mit zusätzlichen Text-Matching-Funktionen für robustere Element-Erkennung +""" + +from typing import List, Dict, Optional, Any + +class InstagramSelectors: + """ + Zentrale Sammlung aller Selektoren für die Instagram-Automatisierung. + Bei Änderungen der Instagram-Webseite müssen nur hier Anpassungen vorgenommen werden. + Enthält auch Fuzzy-Text-Matching-Daten für robustere Element-Erkennung. + """ + + # URL-Konstanten + BASE_URL = "https://www.instagram.com" + SIGNUP_URL = "https://www.instagram.com/accounts/emailsignup/" + LOGIN_URL = "https://www.instagram.com/accounts/login/" + + # Cookie-Banner + COOKIE_DIALOG = "div[role='dialog']" + COOKIE_REJECT_BUTTON = "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]" + COOKIE_ACCEPT_BUTTON = "//button[contains(text(), 'Akzeptieren') or contains(text(), 'Accept') or contains(text(), 'Zulassen')]" + + # Registrierungsformular - Hauptfelder + EMAIL_PHONE_FIELD = "input[name='emailOrPhone']" + FULLNAME_FIELD = "input[name='fullName']" + USERNAME_FIELD = "input[name='username']" + PASSWORD_FIELD = "input[name='password']" + + # Registrierungsformular - Alternative Selektoren + ALT_EMAIL_FIELD = "//input[@aria-label='Handynummer oder E-Mail-Adresse']" + ALT_FULLNAME_FIELD = "//input[@aria-label='Vollständiger Name']" + ALT_USERNAME_FIELD = "//input[@aria-label='Benutzername']" + ALT_PASSWORD_FIELD = "//input[@aria-label='Passwort']" + + # Geburtsdatum-Selektoren + BIRTHDAY_MONTH_SELECT = "//select[@title='Monat:']" + BIRTHDAY_DAY_SELECT = "//select[@title='Tag:']" + BIRTHDAY_YEAR_SELECT = "//select[@title='Jahr:']" + + # Buttons + SUBMIT_BUTTON = "button[type='submit']" + NEXT_BUTTON = "//button[contains(text(), 'Weiter') or contains(text(), 'Next')]" + CONFIRMATION_BUTTON = "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]" + + # Bestätigungscode + CONFIRMATION_CODE_FIELD = "input[name='confirmationCode']" + ALT_CONFIRMATION_CODE_FIELD = "//input[@aria-label='Bestätigungscode']" + RESEND_CODE_BUTTON = "//button[contains(text(), 'Code erneut senden') or contains(text(), 'Resend code')]" + + # Fehlermeldungen + ERROR_MESSAGE = "p[class*='error'], div[role='alert']" + CAPTCHA_CONTAINER = "div[class*='captcha']" + + # Registrierungsmethode umschalten + EMAIL_TAB = "//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]" + PHONE_TAB = "//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]" + + # Login-Felder + LOGIN_USERNAME_FIELD = "input[name='username']" + LOGIN_PASSWORD_FIELD = "input[name='password']" + + # Erfolgs-Indikatoren für Login/Registrierung + SUCCESS_INDICATORS = [ + "svg[aria-label='Home']", + "img[alt='Instagram']", + "a[href='/direct/inbox/']", + "a[href='/explore/']" + ] + + # OCR-Fallback-Texte + OCR_TEXTS = { + "email_field": ["Handynummer oder E-Mail", "Mobile number or email"], + "fullname_field": ["Vollständiger Name", "Full name"], + "username_field": ["Benutzername", "Username"], + "password_field": ["Passwort", "Password"], + "birthday_day": ["Tag", "Day"], + "birthday_month": ["Monat", "Month"], + "birthday_year": ["Jahr", "Year"], + "confirmation_code": ["Bestätigungscode", "Confirmation code"], + "next_button": ["Weiter", "Next"], + "submit_button": ["Registrieren", "Sign up"], + "confirm_button": ["Bestätigen", "Confirm"], + "cookie_reject": ["Nur erforderliche Cookies erlauben", "Optionale Cookies ablehnen", "Reject"] + } + + # Text-Matching-Parameter für Fuzzy-Matching + TEXT_MATCH = { + # Formularfelder + "form_fields": { + "email_phone": ["Handynummer oder E-Mail-Adresse", "Mobile Number or Email", + "Phone number or email", "E-Mail-Adresse oder Telefonnummer"], + "full_name": ["Vollständiger Name", "Full Name", "Name"], + "username": ["Benutzername", "Username"], + "password": ["Passwort", "Password"], + "confirmation_code": ["Bestätigungscode", "Verification code", "Confirmation code", "Code"], + }, + + # Buttons + "buttons": { + "submit": ["Weiter", "Registrieren", "Next", "Sign up", "Continue", "Anmelden"], + "confirm": ["Bestätigen", "Confirm", "Verify", "Weiter", "Next"], + "next": ["Weiter", "Next", "Continue", "Fortfahren"], + "reject_cookies": ["Ablehnen", "Nur erforderliche", "Reject", "Not now", "Decline", + "Optionale Cookies ablehnen", "Nur notwendige Cookies"], + "accept_cookies": ["Akzeptieren", "Accept", "Allow", "Zulassen", "Alle Cookies akzeptieren"], + "skip": ["Überspringen", "Skip", "Später", "Later", "Nicht jetzt", "Not now"], + }, + + # Tabs + "tabs": { + "email": ["E-Mail", "Email", "E-mail", "Mail"], + "phone": ["Telefon", "Telefonnummer", "Phone", "Mobile"], + }, + + # Erfolgs-Indikatoren + "success_indicators": [ + "Home", "Startseite", "Feed", "Timeline", "Nachrichten", + "Profil", "Suche", "Entdecken", "Explore" + ], + + # Fehler-Indikatoren + "error_indicators": [ + "Fehler", "Error", "Leider", "Ungültig", "Invalid", "Nicht verfügbar", + "Fehlgeschlagen", "Problem", "Failed", "Nicht möglich", "Bereits verwendet" + ], + + # 2FA-Indikatoren + "two_fa_indicators": [ + "Bestätigungscode", "Verifizierungscode", "Sicherheitscode", + "2-Faktor", "Verification code", "Two-factor", "2FA" + ] + } + + @classmethod + def birthday_month_option(cls, month_num: int) -> str: + """ + Erstellt einen Selektor für eine bestimmte Monatsoption. + + Args: + month_num: Monatsnummer (1-12) + + Returns: + str: XPath-Selektor für die Monatsoption + """ + return f"//select[@title='Monat:']/option[@value='{month_num}']" + + @classmethod + def birthday_day_option(cls, day_num: int) -> str: + """ + Erstellt einen Selektor für eine bestimmte Tagesoption. + + Args: + day_num: Tagesnummer (1-31) + + Returns: + str: XPath-Selektor für die Tagesoption + """ + return f"//select[@title='Tag:']/option[@value='{day_num}']" + + @classmethod + def birthday_year_option(cls, year: int) -> str: + """ + Erstellt einen Selektor für eine bestimmte Jahresoption. + + Args: + year: Jahr (z.B. 1990) + + Returns: + str: XPath-Selektor für die Jahresoption + """ + return f"//select[@title='Jahr:']/option[@value='{year}']" + + @classmethod + def get_field_labels(cls, field_type: str) -> List[str]: + """ + Gibt die möglichen Bezeichnungen für ein Formularfeld zurück. + + Args: + field_type: Typ des Formularfelds (z.B. "email_phone", "full_name") + + Returns: + List[str]: Liste mit möglichen Bezeichnungen + """ + return cls.TEXT_MATCH["form_fields"].get(field_type, []) + + @classmethod + def get_button_texts(cls, button_type: str) -> List[str]: + """ + Gibt die möglichen Texte für einen Button zurück. + + Args: + button_type: Typ des Buttons (z.B. "submit", "confirm") + + Returns: + List[str]: Liste mit möglichen Button-Texten + """ + return cls.TEXT_MATCH["buttons"].get(button_type, []) + + @classmethod + def get_tab_texts(cls, tab_type: str) -> List[str]: + """ + Gibt die möglichen Texte für einen Tab zurück. + + Args: + tab_type: Typ des Tabs (z.B. "email", "phone") + + Returns: + List[str]: Liste mit möglichen Tab-Texten + """ + return cls.TEXT_MATCH["tabs"].get(tab_type, []) + + @classmethod + def get_success_indicators(cls) -> List[str]: + """ + Gibt die möglichen Texte für Erfolgsindikatoren zurück. + + Returns: + List[str]: Liste mit möglichen Erfolgsindikator-Texten + """ + return cls.TEXT_MATCH["success_indicators"] + + @classmethod + def get_error_indicators(cls) -> List[str]: + """ + Gibt die möglichen Texte für Fehlerindikatoren zurück. + + Returns: + List[str]: Liste mit möglichen Fehlerindikator-Texten + """ + return cls.TEXT_MATCH["error_indicators"] + + @classmethod + def get_two_fa_indicators(cls) -> List[str]: + """ + Gibt die möglichen Texte für 2FA-Indikatoren zurück. + + Returns: + List[str]: Liste mit möglichen 2FA-Indikator-Texten + """ + return cls.TEXT_MATCH["two_fa_indicators"] \ No newline at end of file diff --git a/social_networks/instagram/instagram_ui_helper.py b/social_networks/instagram/instagram_ui_helper.py new file mode 100644 index 0000000..d115490 --- /dev/null +++ b/social_networks/instagram/instagram_ui_helper.py @@ -0,0 +1,668 @@ +# social_networks/instagram/instagram_ui_helper.py + +""" +Instagram-UI-Helper - Hilfsmethoden für die Interaktion mit der Instagram-UI +""" + +import logging +import re +import time +from typing import Dict, List, Any, Optional, Tuple, Union, Callable + +from .instagram_selectors import InstagramSelectors +from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button + +# Konfiguriere Logger +logger = logging.getLogger("instagram_ui_helper") + +class InstagramUIHelper: + """ + Hilfsmethoden für die Interaktion mit der Instagram-Benutzeroberfläche. + Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen. + """ + + def __init__(self, automation): + """ + Initialisiert den Instagram-UI-Helper. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + + # Initialisiere TextSimilarity für Fuzzy-Matching + self.text_similarity = TextSimilarity(default_threshold=0.7) + + logger.debug("Instagram-UI-Helper initialisiert") + + def _ensure_browser(self) -> bool: + """ + Stellt sicher, dass die Browser-Referenz verfügbar ist. + + Returns: + bool: True wenn Browser verfügbar, False sonst + """ + if self.browser is None: + self.browser = self.automation.browser + + if self.browser is None: + logger.error("Browser-Referenz nicht verfügbar") + return False + + return True + + def fill_field_fuzzy(self, field_labels: Union[str, List[str]], + value: str, fallback_selector: str = None, + threshold: float = 0.7, timeout: int = 5000) -> bool: + """ + Füllt ein Formularfeld mit Fuzzy-Text-Matching aus. + + Args: + field_labels: Bezeichner oder Liste von Bezeichnern des Feldes + value: Einzugebender Wert + fallback_selector: CSS-Selektor für Fallback + threshold: Schwellenwert für die Textähnlichkeit (0-1) + timeout: Zeitlimit für die Suche in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere field_labels zu einer Liste + if isinstance(field_labels, str): + field_labels = [field_labels] + + # Versuche, das Feld mit Fuzzy-Matching zu finden + element = fuzzy_find_element( + self.browser.page, + field_labels, + selector_type="input", + threshold=threshold, + wait_time=timeout + ) + + if element: + # Versuche, das Feld zu fokussieren und den Wert einzugeben + element.focus() + time.sleep(0.1) + element.fill("") # Leere das Feld zuerst + time.sleep(0.2) + + # Text menschenähnlich eingeben + for char in value: + element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000) + time.sleep(0.01) + + logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}") + return True + + # Fuzzy-Matching fehlgeschlagen, versuche über Attribute + if fallback_selector: + field_success = self.browser.fill_form_field(fallback_selector, value) + if field_success: + logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}") + return True + + # Versuche noch alternative Selektoren basierend auf field_labels + for label in field_labels: + # Versuche aria-label Attribut + aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']" + if self.browser.is_element_visible(aria_selector, timeout=1000): + if self.browser.fill_form_field(aria_selector, value): + logger.info(f"Feld über aria-label gefüllt: {label}") + return True + + # Versuche placeholder Attribut + placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']" + if self.browser.is_element_visible(placeholder_selector, timeout=1000): + if self.browser.fill_form_field(placeholder_selector, value): + logger.info(f"Feld über placeholder gefüllt: {label}") + return True + + # Versuche name Attribut + name_selector = f"input[name='{label.lower().replace(' ', '')}']" + if self.browser.is_element_visible(name_selector, timeout=1000): + if self.browser.fill_form_field(name_selector, value): + logger.info(f"Feld über name-Attribut gefüllt: {label}") + return True + + logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen") + return False + + except Exception as e: + logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {e}") + return False + + def click_button_fuzzy(self, button_texts: Union[str, List[str]], + fallback_selector: str = None, threshold: float = 0.7, + timeout: int = 5000) -> bool: + """ + Klickt einen Button mit Fuzzy-Text-Matching. + + Args: + button_texts: Text oder Liste von Texten des Buttons + fallback_selector: CSS-Selektor für Fallback + threshold: Schwellenwert für die Textähnlichkeit (0-1) + timeout: Zeitlimit für die Suche in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere button_texts zu einer Liste + if isinstance(button_texts, str): + button_texts = [button_texts] + + # Logging der Suche + logger.info(f"Suche nach Button mit Texten: {button_texts}") + + if not button_texts or button_texts == [[]]: + logger.warning("Leere Button-Text-Liste angegeben!") + return False + + # Versuche, den Button mit Fuzzy-Matching zu finden und zu klicken + try: + # Direktes Matching für alle angegebenen Texte + for text in button_texts: + if not text or text == "[]": + continue + + # Direkte Selektoren versuchen + button_selector = f"button:has-text('{text}')" + button = self.browser.page.query_selector(button_selector) + if button: + logger.info(f"Button mit exaktem Text gefunden: {text}") + button.click() + return True + + # Alternativer Selektor für Links und andere klickbare Elemente + link_selector = f"a:has-text('{text}')" + link = self.browser.page.query_selector(link_selector) + if link: + logger.info(f"Link mit exaktem Text gefunden: {text}") + link.click() + return True + + # Versuche alle Buttons auf der Seite zu finden und zu prüfen + all_buttons = self.browser.page.query_selector_all("button, [role='button'], a.button, input[type='submit']") + logger.info(f"Gefundene Button-ähnliche Elemente: {len(all_buttons)}") + + for button in all_buttons: + button_text = button.inner_text() + if not button_text: + continue + + button_text = button_text.strip() + logger.debug(f"Button-Text: '{button_text}'") + + # Prüfe auf Textähnlichkeit + for search_text in button_texts: + if not search_text: + continue + + if self.text_similarity.is_similar(search_text, button_text, threshold=threshold): + logger.info(f"Button mit ähnlichem Text gefunden: '{button_text}' (ähnlich zu '{search_text}')") + button.click() + return True + + logger.warning(f"Kein Button mit Text ähnlich zu '{button_texts}' gefunden") + except Exception as inner_e: + logger.error(f"Innerer Fehler beim Fuzzy-Klicken: {inner_e}") + + # Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector + if fallback_selector: + logger.info(f"Versuche Fallback-Selektor: {fallback_selector}") + if self.browser.click_element(fallback_selector): + logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}") + return True + + # Versuche alternative Methoden + + # 1. Versuche über aria-label + for text in button_texts: + if not text: + continue + + aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']" + if self.browser.is_element_visible(aria_selector, timeout=1000): + if self.browser.click_element(aria_selector): + logger.info(f"Button über aria-label geklickt: {text}") + return True + + # 2. Versuche über role='button' mit Text + for text in button_texts: + if not text: + continue + + xpath_selector = f"//div[@role='button' and contains(., '{text}')]" + if self.browser.is_element_visible(xpath_selector, timeout=1000): + if self.browser.click_element(xpath_selector): + logger.info(f"Button über role+text geklickt: {text}") + return True + + # 3. Versuche über Link-Text + for text in button_texts: + if not text: + continue + + link_selector = f"//a[contains(text(), '{text}')]" + if self.browser.is_element_visible(link_selector, timeout=1000): + if self.browser.click_element(link_selector): + logger.info(f"Link mit passendem Text geklickt: {text}") + return True + + # 4. Als letzten Versuch, klicke auf einen beliebigen Button + logger.warning("Kein spezifischer Button gefunden, versuche beliebigen Button zu klicken") + buttons = self.browser.page.query_selector_all("button") + if buttons and len(buttons) > 0: + for button in buttons: + visible = button.is_visible() + if visible: + logger.info("Klicke auf beliebigen sichtbaren Button") + button.click() + return True + + logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken") + return False + + except Exception as e: + logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}") + return False + + def check_for_error(self, error_selectors: List[str] = None, + error_texts: List[str] = None) -> Optional[str]: + """ + Überprüft, ob Fehlermeldungen angezeigt werden. + + Args: + error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen + error_texts: Liste mit typischen Fehlertexten + + Returns: + Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden + """ + if not self._ensure_browser(): + return None + + try: + # Standardselektoren verwenden, wenn keine angegeben sind + if error_selectors is None: + error_selectors = [ + InstagramSelectors.ERROR_MESSAGE, + "p[class*='error']", + "div[role='alert']", + "span[class*='error']", + ".error-message" + ] + + # Standardfehlertexte verwenden, wenn keine angegeben sind + if error_texts is None: + error_texts = InstagramSelectors.get_error_indicators() + + # 1. Nach Fehlerselektoren suchen + for selector in error_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + error_text = element.text_content() + if error_text and len(error_text.strip()) > 0: + logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}") + return error_text.strip() + + # 2. Alle Texte auf der Seite durchsuchen + page_content = self.browser.page.content() + + for error_text in error_texts: + if error_text.lower() in page_content.lower(): + # Versuche, den genauen Fehlertext zu extrahieren + matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower()) + if matches: + full_error = matches[0].strip() + logger.info(f"Fehlermeldung gefunden (Text): {full_error}") + return full_error + else: + logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}") + return error_text + + # 3. Nach weiteren Fehlerelementen suchen + elements = self.browser.page.query_selector_all("p, div, span") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip() + + # Prüfe Textähnlichkeit mit Fehlertexten + for error_text in error_texts: + if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \ + self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7): + logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}") + return element_text + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}") + return None + + def check_for_captcha(self) -> bool: + """ + Überprüft, ob ein Captcha angezeigt wird. + + Returns: + bool: True wenn Captcha erkannt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Selektoren für Captcha-Erkennung + captcha_selectors = [ + InstagramSelectors.CAPTCHA_CONTAINER, + "div[class*='captcha']", + "iframe[src*='captcha']", + "iframe[title*='captcha']", + "iframe[title*='reCAPTCHA']" + ] + + # Captcha-Texte für textbasierte Erkennung + captcha_texts = [ + "captcha", "recaptcha", "sicherheitsüberprüfung", "security check", + "i'm not a robot", "ich bin kein roboter", "verify you're human", + "bestätige, dass du ein mensch bist" + ] + + # Nach Selektoren suchen + for selector in captcha_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.warning(f"Captcha erkannt (Selektor): {selector}") + return True + + # Nach Texten suchen + page_content = self.browser.page.content().lower() + + for text in captcha_texts: + if text in page_content: + logger.warning(f"Captcha erkannt (Text): {text}") + return True + + return False + + except Exception as e: + logger.error(f"Fehler bei der Captcha-Erkennung: {e}") + return False + + def check_for_next_steps(self) -> bool: + """ + Überprüft, ob Elemente für weitere Einrichtungsschritte angezeigt werden. + + Returns: + bool: True wenn weitere Schritte erkannt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Texte, die auf weitere Einrichtungsschritte hinweisen + next_step_texts = [ + "profilbild hinzufügen", "add profile photo", + "freunde finden", "find friends", + "konten folgen", "follow accounts", + "profilbild hochladen", "upload profile picture", + "profil einrichten", "set up your profile", + "willkommen bei instagram", "welcome to instagram", + "personalisieren", "personalize", + "für dich empfohlen", "recommended for you" + ] + + # Seiteninhalt durchsuchen + page_content = self.browser.page.content().lower() + + for text in next_step_texts: + if text in page_content: + logger.info(f"Weitere Einrichtungsschritte erkannt (Text): {text}") + return True + + # Nach typischen Buttons für weitere Schritte suchen + next_step_button_texts = [ + "überspringen", "skip", + "später", "later", + "folgen", "follow", + "hinzufügen", "add", + "hochladen", "upload", + "weiter", "next", + "fertig", "done" + ] + + # Textähnlichkeit mit Button-Texten prüfen + elements = self.browser.page.query_selector_all("button, [role='button'], a") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip().lower() + + for button_text in next_step_button_texts: + if button_text in element_text or \ + self.text_similarity.is_similar(button_text, element_text, threshold=0.8): + logger.info(f"Button für weitere Einrichtungsschritte erkannt: {element_text}") + return True + + return False + + except Exception as e: + logger.error(f"Fehler bei der Erkennung weiterer Einrichtungsschritte: {e}") + return False + + def find_element_by_text(self, text: Union[str, List[str]], + element_type: str = "any", threshold: float = 0.7) -> Optional[Any]: + """ + Findet ein Element basierend auf Textähnlichkeit. + + Args: + text: Zu suchender Text oder Liste von Texten + element_type: Art des Elements ("button", "link", "input", "any") + threshold: Schwellenwert für die Textähnlichkeit (0-1) + + Returns: + Optional[Any]: Das gefundene Element oder None, wenn keines gefunden wurde + """ + if not self._ensure_browser(): + return None + + try: + # Normalisiere text zu einer Liste + if isinstance(text, str): + text = [text] + + # Verwende die Fuzzy-Find-Funktion + element = fuzzy_find_element( + self.browser.page, + text, + selector_type=element_type, + threshold=threshold, + wait_time=5000 + ) + + if element: + logger.info(f"Element mit Text '{text}' gefunden") + return element + + logger.warning(f"Kein Element mit Text '{text}' gefunden") + return None + + except Exception as e: + logger.error(f"Fehler beim Suchen nach Element mit Text '{text}': {e}") + return None + + def is_text_on_page(self, text: Union[str, List[str]], threshold: float = 0.7) -> bool: + """ + Überprüft, ob ein Text auf der Seite vorhanden ist. + + Args: + text: Zu suchender Text oder Liste von Texten + threshold: Schwellenwert für die Textähnlichkeit (0-1) + + Returns: + bool: True wenn der Text gefunden wurde, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere text zu einer Liste + if isinstance(text, str): + text = [text] + + # Hole den gesamten Seiteninhalt + page_content = self.browser.page.content().lower() + + # Direkte Textsuche + for t in text: + if t.lower() in page_content: + logger.info(f"Text '{t}' auf der Seite gefunden (exakte Übereinstimmung)") + return True + + # Suche in allen sichtbaren Textelementen mit Ähnlichkeitsvergleich + elements = self.browser.page.query_selector_all("p, h1, h2, h3, h4, h5, h6, span, div, button, a, label") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip().lower() + + for t in text: + if self.text_similarity.is_similar(t.lower(), element_text, threshold=threshold) or \ + self.text_similarity.contains_similar_text(element_text, [t.lower()], threshold=threshold): + logger.info(f"Text '{t}' auf der Seite gefunden (Ähnlichkeit)") + return True + + logger.info(f"Text '{text}' nicht auf der Seite gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Suchen nach Text auf der Seite: {e}") + return False + + def wait_for_element(self, selectors: Union[str, List[str]], + timeout: int = 10000, check_interval: int = 500) -> Optional[Any]: + """ + Wartet auf das Erscheinen eines Elements. + + Args: + selectors: CSS-Selektor oder Liste von Selektoren + timeout: Zeitlimit in Millisekunden + check_interval: Intervall zwischen den Prüfungen in Millisekunden + + Returns: + Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist + """ + if not self._ensure_browser(): + return None + + try: + # Normalisiere selectors zu einer Liste + if isinstance(selectors, str): + selectors = [selectors] + + start_time = time.time() + end_time = start_time + (timeout / 1000) + + while time.time() < end_time: + for selector in selectors: + element = self.browser.wait_for_selector(selector, timeout=check_interval) + if element: + logger.info(f"Element mit Selektor '{selector}' gefunden") + return element + + # Kurze Pause vor der nächsten Prüfung + time.sleep(check_interval / 1000) + + logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'") + return None + + except Exception as e: + logger.error(f"Fehler beim Warten auf Element: {e}") + return None + + def is_page_loading(self) -> bool: + """ + Überprüft, ob die Seite noch lädt. + + Returns: + bool: True wenn die Seite lädt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Lade-Indikatoren auf Instagram + loading_selectors = [ + "div[class*='spinner']", + "div[class*='loading']", + "div[role='progressbar']", + "svg[aria-label='Loading...']", + "svg[aria-label='Wird geladen...']" + ] + + for selector in loading_selectors: + if self.browser.is_element_visible(selector, timeout=500): + return True + + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Ladestatus: {e}") + return False + + def wait_for_page_load(self, timeout: int = 30000, check_interval: int = 500) -> bool: + """ + Wartet, bis die Seite vollständig geladen ist. + + Args: + timeout: Zeitlimit in Millisekunden + check_interval: Intervall zwischen den Prüfungen in Millisekunden + + Returns: + bool: True wenn die Seite geladen wurde, False bei Zeitüberschreitung + """ + if not self._ensure_browser(): + return False + + try: + # Warten auf Netzwerk-Idle + self.browser.page.wait_for_load_state("networkidle", timeout=timeout) + + # Zusätzlich auf das Verschwinden der Ladeindikatoren warten + start_time = time.time() + end_time = start_time + (timeout / 1000) + + while time.time() < end_time: + if not self.is_page_loading(): + # Noch eine kurze Pause für Animationen + time.sleep(0.5) + logger.info("Seite vollständig geladen") + return True + + # Kurze Pause vor der nächsten Prüfung + time.sleep(check_interval / 1000) + + logger.warning("Zeitüberschreitung beim Warten auf das Laden der Seite") + return False + + except Exception as e: + logger.error(f"Fehler beim Warten auf das Laden der Seite: {e}") + return False \ No newline at end of file diff --git a/social_networks/instagram/instagram_utils.py b/social_networks/instagram/instagram_utils.py new file mode 100644 index 0000000..50a2289 --- /dev/null +++ b/social_networks/instagram/instagram_utils.py @@ -0,0 +1,549 @@ +# social_networks/instagram/instagram_utils.py + +""" +Instagram-Utils - Hilfsfunktionen für die Instagram-Automatisierung +""" + +import logging +import time +import re +import random +from typing import Dict, List, Any, Optional, Tuple, Union + +from .instagram_selectors import InstagramSelectors + +# Konfiguriere Logger +logger = logging.getLogger("instagram_utils") + +class InstagramUtils: + """ + Hilfsfunktionen für die Instagram-Automatisierung. + Enthält allgemeine Hilfsmethoden und kleinere Funktionen. + """ + + def __init__(self, automation): + """ + Initialisiert die Instagram-Utils. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + + logger.debug("Instagram-Utils initialisiert") + + def _ensure_browser(self) -> bool: + """ + Stellt sicher, dass die Browser-Referenz verfügbar ist. + + Returns: + bool: True wenn Browser verfügbar, False sonst + """ + if self.browser is None: + self.browser = self.automation.browser + + if self.browser is None: + logger.error("Browser-Referenz nicht verfügbar") + return False + + return True + + def handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + InstagramSelectors.get_button_texts("reject_cookies"), + InstagramSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(InstagramSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + except Exception as e: + logger.error(f"Fehler beim Behandeln des Cookie-Banners: {e}") + return False + + def is_logged_in(self) -> bool: + """ + Überprüft, ob der Benutzer bei Instagram angemeldet ist. + + Returns: + bool: True wenn angemeldet, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Login-Status über verschiedene Indikatoren prüfen + + # 1. URL prüfen + current_url = self.browser.page.url + if "/accounts/login" in current_url: + logger.debug("Nicht angemeldet (Auf Login-Seite)") + return False + + # 2. Erfolgs-Indikatoren prüfen + success_indicators = InstagramSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=1000): + logger.debug(f"Angemeldet (Indikator: {indicator})") + return True + + # 3. Profil-Icon prüfen + profile_selectors = [ + "img[data-testid='user-avatar']", + "span[role='link'][aria-label*='profil']", + "span[role='link'][aria-label*='profile']" + ] + + for selector in profile_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + logger.debug(f"Angemeldet (Profil-Icon sichtbar: {selector})") + return True + + # 4. Login-Formular prüfen (umgekehrte Logik) + login_selectors = [ + InstagramSelectors.LOGIN_USERNAME_FIELD, + InstagramSelectors.LOGIN_PASSWORD_FIELD + ] + + for selector in login_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + logger.debug(f"Nicht angemeldet (Login-Formular sichtbar: {selector})") + return False + + # Wenn wir hier ankommen, können wir den Status nicht eindeutig bestimmen + # Wir gehen davon aus, dass wir nicht angemeldet sind + logger.debug("Login-Status konnte nicht eindeutig bestimmt werden, nehme 'nicht angemeldet' an") + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Login-Status: {e}") + return False + + def check_for_captcha(self) -> bool: + """ + Überprüft, ob ein Captcha angezeigt wird. + + Returns: + bool: True wenn Captcha erkannt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Delegiere an UI-Helper + return self.automation.ui_helper.check_for_captcha() + + except Exception as e: + logger.error(f"Fehler bei der Captcha-Erkennung: {e}") + return False + + def handle_rate_limiting(self, rotate_proxy: bool = True) -> bool: + """ + Behandelt eine Rate-Limiting-Situation. + + Args: + rotate_proxy: Ob der Proxy rotiert werden soll + + Returns: + bool: True wenn erfolgreich behandelt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + logger.warning("Rate-Limiting erkannt, warte und versuche es erneut") + + # Screenshot erstellen + self.automation._take_screenshot("rate_limit_detected") + + # Proxy rotieren, falls gewünscht + if rotate_proxy and self.automation.use_proxy: + success = self.automation._rotate_proxy() + if not success: + logger.warning("Konnte Proxy nicht rotieren") + + # Längere Wartezeit + wait_time = random.uniform(120, 300) # 2-5 Minuten + logger.info(f"Warte {wait_time:.1f} Sekunden vor dem nächsten Versuch") + time.sleep(wait_time) + + # Seite neuladen + self.browser.page.reload() + self.automation.human_behavior.wait_for_page_load() + + # Prüfen, ob Rate-Limiting noch aktiv ist + rate_limit_texts = [ + "bitte warte einige minuten", + "please wait a few minutes", + "try again later", + "versuche es später erneut", + "zu viele anfragen", + "too many requests" + ] + + page_content = self.browser.page.content().lower() + + still_rate_limited = False + for text in rate_limit_texts: + if text in page_content: + still_rate_limited = True + break + + if still_rate_limited: + logger.warning("Immer noch Rate-Limited nach dem Warten") + return False + else: + logger.info("Rate-Limiting scheint aufgehoben zu sein") + return True + + except Exception as e: + logger.error(f"Fehler bei der Behandlung des Rate-Limitings: {e}") + return False + + def scroll_page(self, direction: str = "down", amount: int = 3) -> bool: + """ + Scrollt die Seite. + + Args: + direction: "up" oder "down" + amount: Scroll-Menge (1=wenig, 5=viel) + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Scroll-Faktor je nach Richtung + scroll_factor = 1 if direction == "down" else -1 + + # Scroll-Menge in Pixeln + pixel_amount = amount * 300 * scroll_factor + + # Scroll ausführen + self.browser.page.evaluate(f"window.scrollBy(0, {pixel_amount})") + + # Menschliche Verzögerung + self.automation.human_behavior.random_delay(0.5, 1.0) + + logger.debug(f"Seite ge{direction}scrollt um {abs(pixel_amount)} Pixel") + return True + + except Exception as e: + logger.error(f"Fehler beim Scrollen der Seite: {e}") + return False + + def extract_username_from_url(self, url: str) -> Optional[str]: + """ + Extrahiert den Benutzernamen aus einer Instagram-URL. + + Args: + url: Die Instagram-URL + + Returns: + Optional[str]: Der extrahierte Benutzername oder None + """ + try: + # Muster für Profil-URLs + patterns = [ + r'instagram\.com/([a-zA-Z0-9._]+)/?(?:$|\?|#)', + r'instagram\.com/([a-zA-Z0-9._]+)/(?:saved|tagged|reels)/?', + r'instagram\.com/p/[^/]+/(?:by|from)/([a-zA-Z0-9._]+)/?' + ] + + for pattern in patterns: + match = re.search(pattern, url) + if match: + username = match.group(1) + # Einige Ausnahmen filtern + if username not in ["explore", "accounts", "p", "reel", "stories", "direct"]: + return username + + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Benutzernamens aus der URL: {e}") + return None + + def get_current_username(self) -> Optional[str]: + """ + Versucht, den Benutzernamen des aktuell angemeldeten Kontos zu ermitteln. + + Returns: + Optional[str]: Der Benutzername oder None, wenn nicht gefunden + """ + if not self._ensure_browser(): + return None + + try: + # Verschiedene Methoden zur Erkennung des Benutzernamens + + # 1. Benutzername aus URL des Profils + profile_link_selectors = [ + "a[href*='/profile/']", + "a[href*='instagram.com/'][role='link']", + "a[href*='instagram.com/']:not([href*='/explore/']):not([href*='/direct/'])" + ] + + for selector in profile_link_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + href = element.get_attribute("href") + if href: + username = self.extract_username_from_url(href) + if username: + logger.info(f"Benutzername aus Profil-Link ermittelt: {username}") + return username + + # 2. Benutzername aus aria-label Attributen + profile_aria_selectors = [ + "img[data-testid='user-avatar']", + "span[role='link'][aria-label*='profil']", + "span[role='link'][aria-label*='profile']" + ] + + for selector in profile_aria_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + aria_label = element.get_attribute("aria-label") + if aria_label: + # Verschiedene Formate: "profil von username", "username's profile" + for pattern in [r'profil von ([a-zA-Z0-9._]+)', r"([a-zA-Z0-9._]+)'s profile"]: + match = re.search(pattern, aria_label, re.IGNORECASE) + if match: + username = match.group(1) + logger.info(f"Benutzername aus aria-label ermittelt: {username}") + return username + + # 3. Fallback: Zur Profilseite navigieren und aus URL extrahieren + # Dies sollte nur gemacht werden, wenn ein wichtiger Grund dafür besteht + # und kein anderer Weg verfügbar ist, den Benutzernamen zu ermitteln + + logger.warning("Konnte Benutzernamen nicht ermitteln") + return None + + except Exception as e: + logger.error(f"Fehler bei der Ermittlung des Benutzernamens: {e}") + return None + + def is_account_private(self) -> Optional[bool]: + """ + Überprüft, ob das aktuelle Konto privat ist. + + Returns: + Optional[bool]: True wenn privat, False wenn öffentlich, None wenn nicht erkennbar + """ + if not self._ensure_browser(): + return None + + try: + # Texte, die auf ein privates Konto hinweisen + private_indicators = [ + "this account is private", + "dieses konto ist privat", + "must be following", + "musst diesem konto folgen" + ] + + # Privat-Icon suchen + private_icon_selectors = [ + "svg[aria-label='Private Account']", + "svg[aria-label='Privates Konto']" + ] + + for selector in private_icon_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + logger.info("Konto ist privat (Icon gefunden)") + return True + + # Texte prüfen + page_content = self.browser.page.content().lower() + + for indicator in private_indicators: + if indicator in page_content: + logger.info(f"Konto ist privat (Text gefunden: {indicator})") + return True + + # Wenn keine Privat-Indikatoren gefunden wurden, gehen wir davon aus, dass das Konto öffentlich ist + # Dies ist jedoch nicht 100% sicher + logger.debug("Konto scheint öffentlich zu sein") + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Privat-Status: {e}") + return None + + def count_followers(self) -> Optional[int]: + """ + Versucht, die Anzahl der Follower des aktuellen Kontos zu ermitteln. + + Returns: + Optional[int]: Die Anzahl der Follower oder None, wenn nicht ermittelbar + """ + if not self._ensure_browser(): + return None + + try: + # Selektoren für Follower-Zähler + follower_selectors = [ + "a[href*='/followers/'] span", + "a[href*='/followers/']", + "ul li:nth-child(2) span" # Typisches Layout auf Profilseiten + ] + + for selector in follower_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + follower_text = element.inner_text() + if follower_text: + # Zahlen extrahieren (möglicherweise mit K, M, B für Tausend, Million, Milliarde) + follower_text = follower_text.strip().replace(',', '').replace('.', '') + + # Direkter Zahlenwert + if follower_text.isdigit(): + return int(follower_text) + + # Wert mit K (Tausend) + if 'k' in follower_text.lower(): + value = float(follower_text.lower().replace('k', '')) * 1000 + return int(value) + + # Wert mit M (Million) + if 'm' in follower_text.lower(): + value = float(follower_text.lower().replace('m', '')) * 1000000 + return int(value) + + # Wert mit B (Milliarde) + if 'b' in follower_text.lower(): + value = float(follower_text.lower().replace('b', '')) * 1000000000 + return int(value) + + logger.warning("Konnte Follower-Anzahl nicht ermitteln") + return None + + except Exception as e: + logger.error(f"Fehler bei der Ermittlung der Follower-Anzahl: {e}") + return None + + def get_account_stats(self) -> Dict[str, Any]: + """ + Sammelt verfügbare Statistiken zum aktuellen Konto. + + Returns: + Dict[str, Any]: Konto-Statistiken + """ + if not self._ensure_browser(): + return {} + + try: + stats = {} + + # Benutzername ermitteln + username = self.get_current_username() + if username: + stats["username"] = username + + # Privat-Status prüfen + is_private = self.is_account_private() + if is_private is not None: + stats["is_private"] = is_private + + # Follower-Anzahl + followers = self.count_followers() + if followers is not None: + stats["followers"] = followers + + # Optional: Weitere Statistiken sammeln + # ... + + return stats + + except Exception as e: + logger.error(f"Fehler beim Sammeln der Konto-Statistiken: {e}") + return {} + + def check_login_banned(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob der Login gesperrt wurde. + + Returns: + Tuple[bool, Optional[str]]: (Gesperrt, Fehlermeldung falls vorhanden) + """ + if not self._ensure_browser(): + return False, None + + try: + # Texte, die auf eine Sperrung hinweisen + ban_indicators = [ + "your account has been disabled", + "dein konto wurde deaktiviert", + "your account has been locked", + "dein konto wurde gesperrt", + "suspicious activity", + "verdächtige aktivität", + "we've detected suspicious activity", + "wir haben verdächtige aktivitäten festgestellt", + "your account has been temporarily locked", + "dein konto wurde vorübergehend gesperrt" + ] + + # Seiteninhalt durchsuchen + page_content = self.browser.page.content().lower() + + for indicator in ban_indicators: + if indicator in page_content: + # Vollständigen Text der Fehlermeldung extrahieren + error_element = self.browser.wait_for_selector("p[data-testid='login-error-message'], div[role='alert'], p[class*='error']", timeout=2000) + error_message = None + + if error_element: + error_message = error_element.inner_text().strip() + + if not error_message: + error_message = f"Konto gesperrt oder eingeschränkt (Indikator: {indicator})" + + logger.warning(f"Konto-Sperrung erkannt: {error_message}") + return True, error_message + + return False, None + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung auf Konto-Sperrung: {e}") + return False, None \ No newline at end of file diff --git a/social_networks/instagram/instagram_verification.py b/social_networks/instagram/instagram_verification.py new file mode 100644 index 0000000..c403afb --- /dev/null +++ b/social_networks/instagram/instagram_verification.py @@ -0,0 +1,491 @@ +# social_networks/instagram/instagram_verification.py + +""" +Instagram-Verifizierung - Klasse für die Verifizierungsfunktionalität bei Instagram +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple + +from .instagram_selectors import InstagramSelectors +from .instagram_workflow import InstagramWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("instagram_verification") + +class InstagramVerification: + """ + Klasse für die Verifizierung von Instagram-Konten. + Enthält alle Methoden für den Verifizierungsprozess. + """ + + def __init__(self, automation): + """ + Initialisiert die Instagram-Verifizierung. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = InstagramSelectors() + self.workflow = InstagramWorkflow.get_verification_workflow() + + logger.debug("Instagram-Verifizierung initialisiert") + + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Führt den Verifizierungsprozess für ein Instagram-Konto durch. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere den Verifizierungscode + if not self._validate_verification_code(verification_code): + return { + "success": False, + "error": "Ungültiger Verifizierungscode", + "stage": "code_validation" + } + + try: + # 1. Überprüfen, ob wir auf der Verifizierungsseite sind + if not self._is_on_verification_page(): + # Versuche, zur Verifizierungsseite zu navigieren, falls möglich + # Direktnavigation ist jedoch normalerweise nicht möglich + return { + "success": False, + "error": "Nicht auf der Verifizierungsseite", + "stage": "page_check" + } + + # 2. Verifizierungscode eingeben und absenden + if not self.enter_and_submit_verification_code(verification_code): + return { + "success": False, + "error": "Fehler beim Eingeben oder Absenden des Verifizierungscodes", + "stage": "code_entry" + } + + # 3. Überprüfen, ob die Verifizierung erfolgreich war + success, error_message = self._check_verification_success() + + if not success: + return { + "success": False, + "error": f"Verifizierung fehlgeschlagen: {error_message or 'Unbekannter Fehler'}", + "stage": "verification_check" + } + + # 4. Zusätzliche Dialoge behandeln + self._handle_post_verification_dialogs() + + # Verifizierung erfolgreich + logger.info("Instagram-Verifizierung erfolgreich abgeschlossen") + + return { + "success": True, + "stage": "completed" + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Instagram-Verifizierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception" + } + + def _validate_verification_code(self, verification_code: str) -> bool: + """ + Validiert den Verifizierungscode. + + Args: + verification_code: Der zu validierende Code + + Returns: + bool: True wenn der Code gültig ist, False sonst + """ + # Leerer Code + if not verification_code: + logger.error("Verifizierungscode ist leer") + return False + + # Code-Format prüfen (normalerweise 6-stellige Zahl) + if not re.match(r"^\d{6}$", verification_code): + logger.warning(f"Verifizierungscode hat unerwartetes Format: {verification_code}") + # Wir geben trotzdem True zurück, da einige Codes andere Formate haben könnten + return True + + return True + + def _is_on_verification_page(self) -> bool: + """ + Überprüft, ob wir auf der Verifizierungsseite sind. + + Returns: + bool: True wenn auf der Verifizierungsseite, False sonst + """ + try: + # Screenshot erstellen + self.automation._take_screenshot("verification_page_check") + + # Nach Verifizierungsfeld suchen + verification_selectors = [ + InstagramSelectors.CONFIRMATION_CODE_FIELD, + InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, + "input[name='confirmationCode']", + "input[aria-label='Bestätigungscode']", + "input[aria-label='Confirmation code']", + "input[aria-label='Verification code']" + ] + + for selector in verification_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + logger.info("Auf Verifizierungsseite") + return True + + # Textbasierte Erkennung + verification_texts = [ + "Bestätigungscode", + "Confirmation code", + "Verification code", + "Sicherheitscode", + "Security code", + "Enter the code we sent to" + ] + + page_content = self.browser.page.content().lower() + + for text in verification_texts: + if text.lower() in page_content: + logger.info(f"Auf Verifizierungsseite (erkannt durch Text: {text})") + return True + + logger.warning("Nicht auf der Verifizierungsseite") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen der Verifizierungsseite: {e}") + return False + + def enter_and_submit_verification_code(self, verification_code: str) -> bool: + """ + Gibt den Verifizierungscode ein und sendet ihn ab. + + Args: + verification_code: Der einzugebende Verifizierungscode + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + logger.info(f"Versuche Verifizierungscode einzugeben: {verification_code}") + + # Mögliche Selektoren für das Verifizierungscode-Feld + code_field_selectors = [ + InstagramSelectors.CONFIRMATION_CODE_FIELD, + InstagramSelectors.ALT_CONFIRMATION_CODE_FIELD, + "input[name='email_confirmation_code']", + "input[name='confirmationCode']", + "input[aria-label='Bestätigungscode']", + "input[aria-label='Confirmation code']", + "input[placeholder*='code']", + "input[placeholder='Bestätigungscode']" + ] + + # Versuche, das Feld zu finden und auszufüllen + code_field_found = False + + for selector in code_field_selectors: + logger.debug(f"Versuche Selektor: {selector}") + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Codefeld gefunden mit Selektor: {selector}") + if self.browser.fill_form_field(selector, verification_code): + code_field_found = True + logger.info(f"Verifizierungscode eingegeben: {verification_code}") + break + + # Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen + if not code_field_found: + logger.info("Versuche Fuzzy-Matching für Codefeld") + code_field_found = self.automation.ui_helper.fill_field_fuzzy( + ["Bestätigungscode", "Confirmation code", "Verification code", "Code"], + verification_code + ) + + if not code_field_found: + logger.error("Konnte Verifizierungscode-Feld nicht finden oder ausfüllen") + + # Erstelle einen Screenshot zum Debuggen + self.automation._take_screenshot("code_field_not_found") + return False + + # Menschliche Verzögerung vor dem Absenden + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot erstellen + self.automation._take_screenshot("verification_code_entered") + + # Absenden-Button finden und klicken + submit_button_selectors = [ + "button[type='submit']", + InstagramSelectors.CONFIRMATION_BUTTON, + "//button[contains(text(), 'Bestätigen')]", + "//button[contains(text(), 'Confirm')]", + "//button[contains(text(), 'Verify')]", + "//button[contains(text(), 'Next')]", + "//button[contains(text(), 'Weiter')]", + "button" # Falls alle anderen fehlschlagen, probiere jeden Button + ] + + submit_button_found = False + + logger.info("Suche nach Submit-Button") + for selector in submit_button_selectors: + logger.debug(f"Versuche Submit-Button-Selektor: {selector}") + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Submit-Button gefunden mit Selektor: {selector}") + if self.browser.click_element(selector): + submit_button_found = True + logger.info("Verifizierungscode-Formular abgesendet") + break + + # Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen + if not submit_button_found: + logger.info("Versuche Fuzzy-Matching für Submit-Button") + weiter_buttons = ["Weiter", "Next", "Continue", "Bestätigen", "Confirm", "Submit", "Verify", "Senden"] + submit_button_found = self.automation.ui_helper.click_button_fuzzy( + weiter_buttons + ) + + if not submit_button_found: + # Erstelle einen Screenshot zum Debuggen + self.automation._take_screenshot("submit_button_not_found") + + # Versuche es mit Enter-Taste als letzten Ausweg + logger.info("Konnte Submit-Button nicht finden, versuche Enter-Taste") + self.browser.page.keyboard.press("Enter") + logger.info("Enter-Taste zur Bestätigung des Verifizierungscodes gedrückt") + submit_button_found = True + + # Warten nach dem Absenden + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + return submit_button_found + + except Exception as e: + logger.error(f"Fehler beim Eingeben und Absenden des Verifizierungscodes: {e}") + return False + + def _check_verification_success(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob die Verifizierung erfolgreich war. + + Returns: + Tuple[bool, Optional[str]]: (Erfolg, Fehlermeldung falls vorhanden) + """ + try: + # Warten nach der Verifizierung + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Screenshot erstellen + self.automation._take_screenshot("verification_result") + + # Immer noch auf der Verifizierungsseite? + still_on_verification = self._is_on_verification_page() + + if still_on_verification: + # Fehlermeldung suchen + error_message = self.automation.ui_helper.check_for_error( + error_selectors=[InstagramSelectors.ERROR_MESSAGE], + error_texts=InstagramSelectors.get_error_indicators() + ) + + if error_message: + logger.error(f"Verifizierungsfehler: {error_message}") + return False, error_message + else: + logger.error("Verifizierung fehlgeschlagen, immer noch auf der Verifizierungsseite") + return False, "Immer noch auf der Verifizierungsseite" + + # Erfolg anhand verschiedener Indikatoren prüfen + current_url = self.browser.page.url + + # Wenn wir auf der Startseite sind + if "instagram.com" in current_url and "/accounts/" not in current_url: + logger.info("Verifizierung erfolgreich, jetzt auf der Startseite") + return True, None + + # Prüfe auf weitere Einrichtungsschritte (auch ein Erfolgszeichen) + setup_indicators = [ + "add profile photo", + "profilbild hinzufügen", + "find friends", + "freunde finden", + "follow accounts", + "konten folgen", + "set up your profile", + "einrichten deines profils" + ] + + page_content = self.browser.page.content().lower() + + for indicator in setup_indicators: + if indicator in page_content: + logger.info(f"Verifizierung erfolgreich, jetzt bei weiteren Einrichtungsschritten: {indicator}") + return True, None + + # Erfolg anhand von UI-Elementen prüfen + success_indicators = InstagramSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=2000): + logger.info(f"Verifizierung erfolgreich, Erfolgsindikator gefunden: {indicator}") + return True, None + + # Wenn keine eindeutigen Indikatoren gefunden wurden, aber auch keine Fehler + logger.warning("Keine eindeutigen Erfolgsindikatoren für die Verifizierung gefunden") + return True, None # Wir gehen davon aus, dass es erfolgreich war + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Verifizierungserfolgs: {e}") + return False, f"Fehler bei der Erfolgsprüfung: {str(e)}" + + def _handle_post_verification_dialogs(self) -> None: + """ + Behandelt Dialoge, die nach erfolgreicher Verifizierung erscheinen können. + """ + try: + # Liste der möglichen Dialoge und wie man sie überspringt + dialogs_to_handle = [ + { + "name": "profile_photo", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "find_friends", + "skip_texts": ["Nicht jetzt", "Später", "Not now", "Later"], + "skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"] + }, + { + "name": "follow_accounts", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "save_login_info", + "skip_texts": ["Nicht jetzt", "Not now"], + "skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"] + }, + { + "name": "notifications", + "skip_texts": ["Nicht jetzt", "Not now"], + "skip_selectors": ["//button[contains(text(), 'Nicht jetzt')]", "//button[contains(text(), 'Not now')]"] + } + ] + + # Versuche, jeden möglichen Dialog zu behandeln + for dialog in dialogs_to_handle: + self._try_skip_dialog(dialog) + + logger.info("Nachverifikations-Dialoge behandelt") + + except Exception as e: + logger.warning(f"Fehler beim Behandeln der Nachverifikations-Dialoge: {e}") + # Nicht kritisch, daher keine Fehlerbehandlung + + def _try_skip_dialog(self, dialog: Dict[str, Any]) -> bool: + """ + Versucht, einen bestimmten Dialog zu überspringen. + + Args: + dialog: Informationen zum Dialog + + Returns: + bool: True wenn Dialog gefunden und übersprungen, False sonst + """ + try: + # Zuerst mit Fuzzy-Matching versuchen + skip_clicked = self.automation.ui_helper.click_button_fuzzy( + dialog["skip_texts"], + threshold=0.7, + timeout=3000 + ) + + if skip_clicked: + logger.info(f"Dialog '{dialog['name']}' mit Fuzzy-Matching übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn Fuzzy-Matching fehlschlägt, direkte Selektoren versuchen + for selector in dialog["skip_selectors"]: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info(f"Dialog '{dialog['name']}' mit direktem Selektor übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + return False + + except Exception as e: + logger.warning(f"Fehler beim Versuch, Dialog '{dialog['name']}' zu überspringen: {e}") + return False + + def resend_verification_code(self) -> bool: + """ + Versucht, den Verifizierungscode erneut senden zu lassen. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Resend-Button suchen und klicken + resend_selectors = [ + InstagramSelectors.RESEND_CODE_BUTTON, + "//button[contains(text(), 'Code erneut senden')]", + "//button[contains(text(), 'Resend code')]", + "//button[contains(text(), 'Send again')]", + "//button[contains(text(), 'Noch einmal senden')]", + "a[href*='resend']" + ] + + for selector in resend_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + if self.browser.click_element(selector): + logger.info("Code erneut angefordert") + self.automation.human_behavior.wait_between_actions("decision", 1.5) + return True + + # Fuzzy-Matching versuchen + resend_texts = ["Code erneut senden", "Resend code", "Send again", "Noch einmal senden"] + + resend_clicked = self.automation.ui_helper.click_button_fuzzy( + resend_texts, + threshold=0.7, + timeout=3000 + ) + + if resend_clicked: + logger.info("Code erneut angefordert (über Fuzzy-Matching)") + self.automation.human_behavior.wait_between_actions("decision", 1.5) + return True + + logger.warning("Konnte keinen 'Code erneut senden'-Button finden") + return False + + except Exception as e: + logger.error(f"Fehler beim erneuten Anfordern des Codes: {e}") + return False \ No newline at end of file diff --git a/social_networks/instagram/instagram_workflow.py b/social_networks/instagram/instagram_workflow.py new file mode 100644 index 0000000..eb7e7bf --- /dev/null +++ b/social_networks/instagram/instagram_workflow.py @@ -0,0 +1,454 @@ +""" +Instagram-Workflow - Definiert die Schritte für die Instagram-Anmeldung und -Registrierung +Mit TextSimilarity-Integration für robusteres Element-Matching +""" + +import logging +from typing import Dict, List, Any, Optional, Tuple +import re + +from utils.text_similarity import TextSimilarity + +# Konfiguriere Logger +logger = logging.getLogger("instagram_workflow") + +class InstagramWorkflow: + """ + Definiert die Workflow-Schritte für verschiedene Instagram-Aktionen + wie Registrierung, Anmeldung und Verifizierung. + Enthält TextSimilarity-Integration für robusteres Element-Matching. + """ + + # Text-Ähnlichkeits-Threshold für Fuzzy-Matching + SIMILARITY_THRESHOLD = 0.7 + + # Initialisiere TextSimilarity für Matching + text_similarity = TextSimilarity(default_threshold=SIMILARITY_THRESHOLD) + + # Mögliche alternative Texte für verschiedene UI-Elemente + TEXT_ALTERNATIVES = { + "email": ["E-Mail", "Email", "E-mail", "Mail", "email"], + "phone": ["Telefon", "Telefonnummer", "Phone", "Mobile", "mobile"], + "fullname": ["Vollständiger Name", "Full Name", "Name", "full name"], + "username": ["Benutzername", "Username", "user name"], + "password": ["Passwort", "Password", "pass"], + "submit": ["Registrieren", "Sign up", "Anmelden", "Login", "Log in", "Submit"], + "next": ["Weiter", "Next", "Continue", "Fortfahren"], + "confirm": ["Bestätigen", "Confirm", "Verify", "Verifizieren"], + "reject_cookies": ["Ablehnen", "Nur erforderliche", "Decline", "Reject", "Only necessary"], + "skip": ["Überspringen", "Skip", "Later", "Später", "Not now", "Nicht jetzt"] + } + + @staticmethod + def get_registration_workflow(registration_method: str = "email") -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die Instagram-Registrierung zurück. + + Args: + registration_method: "email" oder "phone" + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + # Basisschritte für beide Methoden + common_steps = [ + { + "name": "navigate_to_signup", + "description": "Zur Registrierungsseite navigieren", + "url": "https://www.instagram.com/accounts/emailsignup/", + "wait_for": ["input[name='emailOrPhone']", "div[role='dialog']"], + "fuzzy_match": None # Kein Fuzzy-Matching für die Navigation + }, + { + "name": "handle_cookie_banner", + "description": "Cookie-Banner behandeln", + "action": "click", + "target": "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]", + "optional": True, + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["reject_cookies"] # Fuzzy-Matching für Cookie-Ablehnung + } + ] + + # Registrierungsmethode wechseln, falls nötig + method_steps = [] + if registration_method == "phone": + method_steps.append({ + "name": "switch_to_phone", + "description": "Zur Telefon-Registrierungsmethode wechseln", + "action": "click", + "target": "//button[contains(text(), 'Telefon') or contains(text(), 'Phone')]", + "wait_for": ["input[name='emailOrPhone']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["phone"] # Fuzzy-Matching für Telefon-Tab + }) + elif registration_method == "email": + method_steps.append({ + "name": "switch_to_email", + "description": "Zur E-Mail-Registrierungsmethode wechseln", + "action": "click", + "target": "//button[contains(text(), 'E-Mail') or contains(text(), 'Email')]", + "wait_for": ["input[name='emailOrPhone']"], + "optional": True, # Meist Standard + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["email"] # Fuzzy-Matching für E-Mail-Tab + }) + + # Formularausfüllung für E-Mail/Telefon + form_steps = [ + { + "name": "fill_email_or_phone", + "description": f"E-Mail/Telefon eingeben ({registration_method})", + "action": "fill", + "target": "input[name='emailOrPhone']", + "value": "{EMAIL_OR_PHONE}", + "fuzzy_match": ["Handynummer oder E-Mail-Adresse", "Mobile Number or Email", + "Phone number or email", "E-Mail-Adresse oder Telefonnummer"] + }, + { + "name": "fill_fullname", + "description": "Vollständigen Namen eingeben", + "action": "fill", + "target": "input[name='fullName']", + "value": "{FULL_NAME}", + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["fullname"] + }, + { + "name": "fill_username", + "description": "Benutzernamen eingeben", + "action": "fill", + "target": "input[name='username']", + "value": "{USERNAME}", + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["username"] + }, + { + "name": "fill_password", + "description": "Passwort eingeben", + "action": "fill", + "target": "input[name='password']", + "value": "{PASSWORD}", + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["password"] + }, + { + "name": "submit_form", + "description": "Formular absenden", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["select[title='Monat:']", "select[title='Tag:']", "select[title='Jahr:']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["submit"] + } + ] + + # Geburtsdatumschritte + birthday_steps = [ + { + "name": "select_month", + "description": "Monat auswählen", + "action": "select", + "target": "select[title='Monat:']", + "value": "{MONTH}", + "fuzzy_match": ["Monat", "Month"] + }, + { + "name": "select_day", + "description": "Tag auswählen", + "action": "select", + "target": "select[title='Tag:']", + "value": "{DAY}", + "fuzzy_match": ["Tag", "Day"] + }, + { + "name": "select_year", + "description": "Jahr auswählen", + "action": "select", + "target": "select[title='Jahr:']", + "value": "{YEAR}", + "fuzzy_match": ["Jahr", "Year"] + }, + { + "name": "submit_birthday", + "description": "Geburtsdatum bestätigen", + "action": "click", + "target": "//button[contains(text(), 'Weiter') or contains(text(), 'Next')]", + "wait_for": ["input[name='confirmationCode']", "input[aria-label='Bestätigungscode']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["next"] + } + ] + + # Bestätigungscodeschritte + verification_steps = [ + { + "name": "enter_confirmation_code", + "description": "Bestätigungscode eingeben", + "action": "fill", + "target": "input[name='confirmationCode']", + "alternative_target": "input[aria-label='Bestätigungscode']", + "value": "{CONFIRMATION_CODE}", + "fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"] + }, + { + "name": "submit_verification", + "description": "Bestätigungscode absenden", + "action": "click", + "target": "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]", + "wait_for": ["img[alt='Instagram']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["confirm"] + } + ] + + # Vollständigen Workflow zusammenstellen + workflow = common_steps + method_steps + form_steps + birthday_steps + verification_steps + + return workflow + + @staticmethod + def get_login_workflow() -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die Instagram-Anmeldung zurück. + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + login_steps = [ + { + "name": "navigate_to_login", + "description": "Zur Anmeldeseite navigieren", + "url": "https://www.instagram.com/accounts/login/", + "wait_for": ["input[name='username']", "div[role='dialog']"], + "fuzzy_match": None # Kein Fuzzy-Matching für die Navigation + }, + { + "name": "handle_cookie_banner", + "description": "Cookie-Banner behandeln", + "action": "click", + "target": "//button[contains(text(), 'Ablehnen') or contains(text(), 'Nur erforderliche') or contains(text(), 'Reject')]", + "optional": True, + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["reject_cookies"] + }, + { + "name": "fill_username_or_email", + "description": "Benutzername oder E-Mail eingeben", + "action": "fill", + "target": "input[name='username']", + "value": "{USERNAME_OR_EMAIL}", + "fuzzy_match": ["Benutzername", "Username", "E-Mail", "Email", "Telefonnummer", "Phone number"] + }, + { + "name": "fill_password", + "description": "Passwort eingeben", + "action": "fill", + "target": "input[name='password']", + "value": "{PASSWORD}", + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["password"] + }, + { + "name": "submit_login", + "description": "Anmeldung absenden", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["svg[aria-label='Home']", "img[alt='Instagram']"], + "fuzzy_match": ["Anmelden", "Log in", "Einloggen", "Login"] + }, + { + "name": "handle_save_info_prompt", + "description": "Optional: 'Anmeldedaten speichern'-Prompt ablehnen", + "action": "click", + "target": "//button[contains(text(), 'Nicht jetzt') or contains(text(), 'Not now')]", + "optional": True, + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["skip"] + }, + { + "name": "handle_notifications_prompt", + "description": "Optional: Benachrichtigungen-Prompt ablehnen", + "action": "click", + "target": "//button[contains(text(), 'Nicht jetzt') or contains(text(), 'Not now')]", + "optional": True, + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["skip"] + } + ] + + return login_steps + + @staticmethod + def get_verification_workflow(verification_method: str = "email") -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die Instagram-Verifizierung zurück. + + Args: + verification_method: "email" oder "phone" + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + verification_steps = [ + { + "name": "wait_for_verification_page", + "description": "Auf Verifizierungsseite warten", + "wait_for": ["input[name='confirmationCode']", "input[aria-label='Bestätigungscode']"], + "fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"] + }, + { + "name": "enter_verification_code", + "description": f"Verifizierungscode eingeben ({verification_method})", + "action": "fill", + "target": "input[name='confirmationCode']", + "alternative_target": "input[aria-label='Bestätigungscode']", + "value": "{VERIFICATION_CODE}", + "fuzzy_match": ["Bestätigungscode", "Confirmation code", "Verification code", "Code"] + }, + { + "name": "submit_verification", + "description": "Verifizierungscode absenden", + "action": "click", + "target": "//button[contains(text(), 'Confirm') or contains(text(), 'Verify') or contains(text(), 'Weiter')]", + "wait_for": ["img[alt='Instagram']"], + "fuzzy_match": InstagramWorkflow.TEXT_ALTERNATIVES["confirm"] + } + ] + + return verification_steps + + @staticmethod + def identify_current_step(page_title: str, page_url: str, visible_elements: List[str]) -> str: + """ + Identifiziert den aktuellen Schritt basierend auf dem Seitentitel, der URL und sichtbaren Elementen. + + Args: + page_title: Titel der Seite + page_url: URL der Seite + visible_elements: Liste sichtbarer Elemente (Selektoren) + + Returns: + str: Name des identifizierten Schritts + """ + # Registrierungsseite + if "signup" in page_url: + if "input[name='emailOrPhone']" in visible_elements: + return "fill_registration_form" + elif "select[title='Monat:']" in visible_elements: + return "select_birthday" + elif "input[name='confirmationCode']" in visible_elements: + return "enter_confirmation_code" + + # Anmeldeseite + elif "login" in page_url: + return "fill_login_form" + + # Verifizierungsseite - robuste Erkennung mit Text-Matching + elif any(element for element in visible_elements if + InstagramWorkflow.text_similarity.contains_similar_text(element, + ["Bestätigungscode", "Verification", "Code"], + threshold=InstagramWorkflow.SIMILARITY_THRESHOLD)): + return "enter_verification_code" + + # Verifizierungsseite - Fallback mit Selektoren + elif "input[name='confirmationCode']" in visible_elements: + return "enter_verification_code" + + # Startseite / Dashboard - robuste Erkennung mit Text-Matching + elif "instagram.com/" in page_url and any(element for element in visible_elements if + InstagramWorkflow.text_similarity.contains_similar_text(element, + ["Home", "Feed", "Startseite"], + threshold=InstagramWorkflow.SIMILARITY_THRESHOLD)): + return "logged_in" + + # Startseite / Dashboard - Fallback mit Selektoren + elif "instagram.com/" in page_url and ("svg[aria-label='Home']" in visible_elements or "img[alt='Instagram']" in visible_elements): + return "logged_in" + + # Nicht identifizierbar + return "unknown" + + @staticmethod + def find_similar_element(elements: List[Dict[str, str]], target_text: str, threshold: float = None) -> Optional[Dict[str, str]]: + """ + Findet ein Element, das dem Zieltext ähnlich ist. + + Args: + elements: Liste von Elementen mit Text-Eigenschaft + target_text: Zu suchender Text + threshold: Ähnlichkeitsschwellenwert (None für Standardwert) + + Returns: + Element oder None, wenn keines gefunden wurde + """ + if threshold is None: + threshold = InstagramWorkflow.SIMILARITY_THRESHOLD + + for element in elements: + element_text = element.get("text", "") + if not element_text: + continue + + if InstagramWorkflow.text_similarity.is_similar(target_text, element_text, threshold=threshold): + return element + + return None + + @staticmethod + def get_alternative_texts(element_type: str) -> List[str]: + """ + Gibt alternative Texte für einen Elementtyp zurück. + + Args: + element_type: Typ des Elements (z.B. "email", "submit") + + Returns: + Liste mit alternativen Texten + """ + return InstagramWorkflow.TEXT_ALTERNATIVES.get(element_type, []) + + @staticmethod + def fuzzy_find_step_by_name(workflow: List[Dict[str, Any]], step_name_pattern: str) -> Optional[Dict[str, Any]]: + """ + Findet einen Workflow-Schritt anhand eines Namensmusters. + + Args: + workflow: Workflow-Schritte + step_name_pattern: Name oder Muster für den gesuchten Schritt + + Returns: + Der gefundene Schritt oder None + """ + # Exakte Übereinstimmung prüfen + for step in workflow: + if step["name"] == step_name_pattern: + return step + + # Mustersuche mit regulären Ausdrücken + pattern = re.compile(step_name_pattern, re.IGNORECASE) + for step in workflow: + if pattern.search(step["name"]) or pattern.search(step.get("description", "")): + return step + + # Fuzzy-Matching als letzter Ausweg + for step in workflow: + name_similarity = InstagramWorkflow.text_similarity.similarity_ratio(step_name_pattern, step["name"]) + desc_similarity = InstagramWorkflow.text_similarity.similarity_ratio(step_name_pattern, step.get("description", "")) + + if name_similarity > 0.8 or desc_similarity > 0.8: + return step + + return None + + +# Beispielnutzung, wenn direkt ausgeführt +if __name__ == "__main__": + # Konfiguriere Logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Beispiel für Workflow-Generierung + email_workflow = InstagramWorkflow.get_registration_workflow("email") + phone_workflow = InstagramWorkflow.get_registration_workflow("phone") + login_workflow = InstagramWorkflow.get_login_workflow() + + print(f"E-Mail-Registrierung: {len(email_workflow)} Schritte") + print(f"Telefon-Registrierung: {len(phone_workflow)} Schritte") + print(f"Anmeldung: {len(login_workflow)} Schritte") + + # Beispiel für Workflow-Details + print("\nDetails zum E-Mail-Registrierungs-Workflow:") + for i, step in enumerate(email_workflow): + print(f"{i+1}. {step['name']}: {step['description']}") + if 'fuzzy_match' in step and step['fuzzy_match']: + print(f" Fuzzy-Match-Texte: {step['fuzzy_match']}") \ No newline at end of file diff --git a/social_networks/tiktok/__init__.py b/social_networks/tiktok/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/tiktok/__pycache__/__init__.cpython-310.pyc b/social_networks/tiktok/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e046d681170b5c4b21754247fb6cbd64296a0a8 GIT binary patch literal 160 zcmd1j<>g`kf}c?V86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6w+*(xTqIJKxa zCL=X3r6|TFwK%&ZzaXY0wYWq#H!(9WrZ_)2GchMVFSVpRzbLynrX&-nAUh^LJ~J<~ cBtBlRpz;=nO>TZlX-=vg$nat&Ai=@_03dWH$N&HU literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/__init__.cpython-313.pyc b/social_networks/tiktok/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40869558d13170ca0a8d910e98492e31691f46ab GIT binary patch literal 176 zcmey&%ge<81QJ?;86f&Gh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABz4O_*(xTqIJKxa z#xW%~GcU8aq$sf@zbM8fKe;qFHLs*N#yzvdqcka|B(=CCrZ_)2GchMVFSVpRzbLyn yrX(}FBtJVQJ|3vEBtBlRpz;=nO>TZlX-=wL5i8Jqkp0CV#z$sGM#ds$APWFaMJ{px literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_automation.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_automation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..414deb5909bab507ba35c2ee6d30fabacdc96507 GIT binary patch literal 9387 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 literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_automation.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_automation.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4476f25ac5813c02bc342599568e711626f91ad GIT binary patch literal 14135 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 A3jhEB literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_login.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_login.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..075a4dc5159c4367f8d73798ffafc3d1cece5687 GIT binary patch literal 21589 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 literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_registration.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_registration.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfafe7a29763159030857fab4c59348a03fc5a53 GIT binary patch literal 22761 zcmeHvTWlOxnqF6T^}X3_wn*x3*GRG?%Az#3XJ%)#p0z2Fk~oqnfuyXsN1mP*tJrL^ z*-f9SlFZtsb`v|;0K2m>u-Qv8_J$hxAu|XD!5|oH0`HT6yyPW75+Eob$p-t7=q1QY zf{igV&iDW4RCN{Eq%_FxVjq@*s!!FqpF02dUr%+1h6)M%{omT{m9PA9BJt;R6a7o! z=1u&!9bC*rEn#YAvaQuL{!P}B{F|z!_*bv#_)WFb>zP_+JzLAJ=W4n2d@a9Ts1>w? zshjC`alKS4aXQl;S})hiTEfz{ht2F`tyZ~~Fmq=9!-SdtAXytRlM9K7f_nvB7ZzJ< zi`})!d8^s79nrFc+i4n;#*KEvwk>1%$!EeaTb6O7+i|*fr&~*ouir#Nerl?->E~{3IIV7{(e~4e?nc{ss-fhB<`2u73s&1&a=OCCqq2N- zw=33`+ueu$0jIU*bl2)aj;lfKEKx_%zaiYbiU0P;XxK_%@e;IfShS>>`Y=(`^LjpQ ze%Z`eX)|kO%p896_+PNHR@%xfCl4o<3ue(QeVDH02b3xxcgQS%sMm@Ea-(ewn-!EQ zq2 z85%K{)y)^o7m;(oH2QN1>t)O@nJ17tx<~Fw^AvK&Or6(a+)85|bn_+iG@c$b&%~ad zH7D@&5PFs6lIA({Wjr|?Yg>-?yg7-SBWgAm%!{}?YNoN-Uh%8)1*?6=@6&DK-4zUD z*Jg`v#s}Sw+G>-u3$4xtsx#AZR-Sy?c8o^5Z5eM_&Pvy`I)>+poqLswFD21e@JDGP zKq3J9JT5)0m#BjTan+pU2MPS99DO_em{J)hyPY$Wk2MgNhPkA?k7xk&H`{4Btwy_L zgKiwA6FJK`Iq?9fcu;LhH0?{l4P`W3r@PK;a>l3E6bvU27Z_jqxkuJ?#iWmchHM{Z^i1E;@-4ngRVF3IhO5A zt~Xkpdvx-L>cZ{Ic6dca3$+Pc@pBe;^7m^>yAzeraQ++p+2$cl|!*ml2lZiq29R-35QN zO4R)g{I~xL@TZqpO|0?`>Ls?5*ip$|s;58J?E?hiX3Eq*PAyQWRFJoidaLblP*?_t9SFal-xv+5w28OyOhwZi3p{&OT0P^qWKe zEb@V6vHU!q-@$XBSnPQL&l`9S+=@Lf_EL{ETOenIMqpMyNtp*?C8+(;!wHo7yC^kg zj>d9_dKt8kwErRU#?6DVJdA+zK0)3g^KdM0xR*hTKSItCGZV|HV5C3Ab0A^=Ij`1n z+{@K^;S-D+)cK-m;MtcxPSNV<31?*Ue`<-XEY|R-dBUmovL9$z)#O&r(N_=jawvcD zdr6{dq)!E{XA%`cKX}Oj!Cigw*$PoS5a&U+-L{=4pLWbv z6Spz!vNuH6vus=PQLEKK0Y#`&OG{n1gC<oJIZtY=%aM8GHwJ=u8=qlmCGHy2nCW(j93PF0FX)}(e zB#3+);>>viV#vFfj2Y3i?sr-?7C^+`kg?u!j0LCRxVGVT%wYW{ryF22K^IruqoDFT zo#vCz+78~bG1KZa8~3e^U|fEs)!A^J`UCI+lW}|^eTP^Q}5iadi{x@?#)(bEvz{tw}B<{jm+inUA8xU#M1e`@bztmla zXzy6&L`v>614|`N;^Mt}r(+3FwL`RP^yk9!R@*l=?n?$#ft>aJ5}5pQ*UuwmF$&C0 zjIvlENaXjA2ZUzDh~KRG~^$sFA)XQqubvfRS)GGRy*52te)=Pu>R4yl(FJtRszRx z-2!!sdayD5gZ1SWy5lz)Zy|uB{7kU2ehN6r*f5b7 z2k6xuq6>*vOe=z*&(BD3=GW?L7Tyd&T7BJavY00?V^p#du7_ItGk9wM2$w>ol1e6% zT2jmW{807?(mCAHeNxN)ESJlvTzMDG>8A^*QcyL@R9&S@d!(s+P$m~ikLQvp?)T^A zOj#>y@-C@Wf0p~rBpK@X&l*JsiJm5M4z3?Sp40(6q1`+E`T0>f@~%=5<8MjJL0 zyW6pyKy-LYdZtUPw=X`3xXdU;xT{YI+PrfCt{dwvTh3rz_KZ@luF@vX)R2%?zw+N&M#McPrn^LsiOiy&M#b6lzcp`RxLFNTNgms!tYbU#_S33Zxi9 zh0ARfo#J-MEcS|~MoQl&OzgcP$hqYHG(yhTh?_AvD{d^M)4!!>n0)^Fb&2q8Sc8=X zK?1&?aoB$8=QdzRg<->-lfBF81gRTP&zm#CTPQ=Fz$$b!AY@%PTs z4ebYrOGADNmKv6g{A$4a1NjF^$4Dp&1(|*|7}CIEL8hOE85XinR#+SD#**cir1WjV zP}S=A)j&{*G;%ELE6gO}5QdbFV@tFkp?spWH(Gp?CQb_|mxb|7b-W?0<<|RhX-K-< zi7XfJ>_6i_8}telU8`t?ln&mmr*hgMt*Sx#(4{O8Tr+<$S_IZi48odlE>RJ32~-*W zkrk0FK+N+$Lc%)_vQKQog1E}|2;AwbX@U&0(X8qK3_uX46RSCt%9CB_drA8xr?6c# zb;`S&AbTn9OC~HLd=EPWY#BqF*I>)Q6K@!~Wi!`fTZSwHi$#7*d!$ianPQto!Ta@q z56vmna1YgSmk{s_R!Ext4C?~{*P|y=!VkKv4NDpb#p|d40P^JzH9zaL)~zm#5NG7V zPexT(R&Uq3LRf2#(X`qPs3upvmt|*C`5+tbfSSP!7rhI8Rbw6Ykw7UPtTkUU<|c-v zUQvuF1=1{oShMOj zbK$#l>55QZvayQ|iF5ATDAriC?7) z$qiuhESCQoJs_KEU)UC3#_cfnqUbS=gs$#Vk^;S>kp^%ptx* zs}p7K$H88_j5h{J3X&6hpbw+dg}fP7B2qtc1F~)W6$R6us4|_3?_fHTK8LQ{x^-iA zranD81%i@1hRKH{KtJ!c>MK@z18l4+HF#;dsxP~qx9L~N$Dy7$*88v=jH>45XTCLa zWig=6Z&O<(feqAMuVWK6HpO@8(U5FYk#<;l>dKXw+hJw4x-r{JF&&4mFi^$IU@%zO zy1L&<;;*2m^F-ojk*MYh5SMchl=WZe=>k}s4ni-dbST9#P4~JsnA`{1Hg=JF$kE_e zPI#?d1b>oARF^!CiTk7j9!KIc6L)qsN91p&AH%W^ZkA!6hXjp+0*#rl4G=cBF~e=Z zRlQFeS%Qrw0OJP)Ekg=g?sLrmJO=cReE43eWsC;GHF)Xp=7->=7$0~k<(7KMK>WT- z*8iccW$$|dnA{*^5FC%>p97lgw3f=}f~?B0ACh7AmqnE4;jW^A{UEX8 z)Xot^39@Oi_0|zfRNlm8(R+1gg|l6 zwaFkaFd0Aj430D7D$yA%y3W!H7~8;xy;BM%)h^1F{ zK!vE&{MYEhdTWRhg2-E};!+!rGUfW>t=ska*=yGq#WIy>Qi-uxiK|mLZ(g3dazmQd ziDUmz5ji}Dw%c~ZAf`>G;lHeaX;)MIbS{;N(D^QgetHl?-)*Lh z(KonDUt;|Z(f993`i5-^?3^eT_o-FgNw0#UO&tvW2z$TTMio-CERk0wUZ*bpmPz?W28oE_Wb;q~-#sIUQ2-J6w|)dN{IJO|t~CfW}xxXbV{VR;rhpPP}_& zONUymZ^K*oAR&I+8QHGFOB)>M$L-Cj1k6+ zvCSU?Gct@Blw0j(VwiDYEAuF0qO8d&#*9(#clN-HzHue+l(g0jrl!ehc*DERzY54u z{QrLifRZ0 ziL=qpCQfK^x+v}5yAm<5ecN;={R6Ti^{uAAC@gSucJ79RyFPl2$8zfdeEB*%KdSG!me^EI3aB)Y ziv2>193XX)j{Kys{ETfi;IH=6j5K}Ro5)HfMd0n#s)Fsy$|WKz=aiIW``JKV$|B+g z!lgYBBws^AJp##ZA(6~hl2WVL4hX(!p3_O z4qf`f(yIZ^1!CHcVfN{gu#d&JMep2>Ldre;4ERey498~g2Ssooh4OGunGg}+E#P2` zj#s^dG#m1bt}xit=9O<#5K?&WzJZwx9?0{<<3tp^_IO1Be!aFQe(m*k3ByMA$1uM* z^VZbt&HB~ZnVZvo@%Hswb2IWL0%KMASeCqXdvO*v5*FS1Sax)Gkr2@OxFp`Aw?7(Q z$VDl>g3G_`3t)sT6)pJ%A;o_9vI9xZ?=67scK*h~56Mv&!pToa4Exhi*a^fiMEkRh zj=eFg4j$Y}JxWnolT!?PhulA-w`j<}yA)g^13=^W5rm_hl~gb7L5hYac46MGxKPPR zRrH8#>)Byh!Jz&UOt=vQB~yIse}EH8ofCdaDJ}*us$eZXeF>k8zAjPSz(Tz8D6 z9O)Pnx|CSS4=qS+9Vmv86#~AHPWLj{BWbOy#eb*7vLJD`zR8LW-93pgR zJ_P64F^W}~^|*Ja_x5ja3jw#GTIpxZVHjhmMCO3V0P!Pu_<80$hY?v~;2g?42h`^| zP~5=3;Sp5BI?p!;9R1)qwo_BkHKcvveo4_=pp$iv%5i-(ZH8mJW7LF(`)tge8& z9+BMjBW?3t@YkZ^uQ~H*uNX6(9NQ{B!T}rbS57g1J?{QT+Ruu=UTfV4cWuOjG7aeV z&t{>4EBcZ_LBVtyNo;b^@0m@gvS-d4XVzhhSfK>sLC+Wqh?#uy8KAq}33{hN1nIwA zrusJKDEaB}w=gnia$0mZ%7NuK zKW4bL;SvVGC>rukZ%B#zDxXkxS4b?5O*EQ&_3Wo8N>)b+!z-%B_~sY|BkfsBr^WTs zRL1a1s-C4XO%uK*5U>%%d)^Wrg!s`hD8z*(zq?&DRNKZnF45~CSAqhk3N@B z#~iM@6pt1y!F+~u5s1cm(dXLBzGet=GoiD;j~}ljZCL6cU7GLQbNw!3stSC31A2B1 z6dzD7-v1H`v3t|c-IgJDPM9(|n#?6NM$I%RvPX|J)p(IeXHd(ES76FR5H9g%FTa3a zV+l{ZaZW}9^X2ov>UJhl?@Zt-l~EV$TtI6R1It8pNI$SQTzMmP0bhf;`JR;5lx z<}|;;2Mv~6SbG^5!7MVOma)!6{!+hSvrn5;YaD0VXtx|Ojpu^&uf8AEFW;V8Sh#y@ zep(s!*mYW2n3 z0_7$nxnLzpio%H3De%+2&vuu`_S_&1Z}1_H80U$3ZNE&Mrx&1eL^IUw?ar(wD?gc^2KyJib^ioc7wFLYph3#8h zmraVm|Gtujz-GiaNrn1*vTzG5YIm~qXF$X$d zC{gpG!mz(ljKKi~5P9OS@+OBUOAA+UC{RUZ#8-(33?W>?U8aDo;{!4+v4Ud?RtFKv zFxo+V<474B0VX!PeEFy>gCWFy@8ArbeWGwPoP=9qE-7(yL^d6HVdic(qfPh`7}4EVqRGOaNn?e}r@GbsM)k@}xm6L~a?v z@(0we)PLTdnV-Endu57TD-x91AtR$)hOa;=nw`57M{4Eb4UyU(c@TMFazv#l_SPyG z7BBR^XiWCWKPNQ4ibS$lf}oyDRa8_P3GNgL6UX0ZYz*OS2@af0G7|1O(9JvG++56p z7W2#eBMip7Ac1z3LQY^ap&%IqY^T`cQ0*nvA$$bd#wTXG=-=!P!v?bB!We zkRD_xk=Ex%n~rAVRQ5Dl+dXVx3rqaMGgP za7d^Ze}`V(S7H<jSb+W*Rrhx#Wu_BLCxTH-hblDsEAoIeiku{{Bbk3z% zg+6(&8&eV)*-oJra@RC*&V8c5B=0MMNA6DHj}gW1qAWSbgLNJ)2LMCHSr!OLrPz(W z`CNP#gNX!>D=Rn2|KbO98KkUlBjZ#G6iCxg;>;w`8y(ZsGf>zOUCOa(cp9Owi-V+@ zeZY#@N{rk<^8K%{RB_|r>_l)vVmtX5-*A{ny5D@h%?|loZFaEi=uW?mH*2huMGpt$ zK@JA;Cui0-mJ#o5PcB&EL2HRKU|V8an>r5tY*JxibjNoO{svNtcY$UcqXq+!=rTS^ z5?6~(aMSSwq5{wpJ7d0!HzNKSF2MnWEA(JAys#_%F>bHX&SC5v%B8%?z4paGHWdrA z$)K^&#L~l!ccL%oZfOuDwIA@Q00jJ1w^B|D0xmwL0>Kg@sdy1)5mt0}Hfa+3^?Hz&G6bc|*xu17TwZS{fq(0>~n53?~qy1cClX{k!+y+|<=Db&; z06G&uvNHBkBQ_4rMqnH8nU~RVn;5m9B}oGtSkSf+Tl6U16kT^iLLqNx5{4!+G6=uK z=XF#iI;7`70PP?+O{HL6r{J`KXh}5j4(<74NF=i*(gpJ+=!Xj%@oY!1f2Xow8@%LT_amSq1{5V>eWvoOdV}kd+!QV#v(lP#-L9S3txS4%`@# zWNSa*)<%>T8nhM*gFFuNV$xVjyNfgGAW}SlT%E}W8R+x6gQ(jToID7 z{1^^@MdR6!zmK99bnqeCaXb0%J5EVgTbUGEMxf^KSJ-Mrofpr!+7A-!Qpy1B_gHMjx zbkGN9lJMK^!oSw>OVI}W93tTOi#L4!GioD@7VAVO4W=JjCyiYrXJcZ-ZU-Y}7!U~W zzl#duAzdEO<)7jbe$pl$|2x=FcW!W@L6$o2TnycUsAm=sW0Qka7>}2@M?<3z14Pa| zmG3Of%+F1|6$_jDF3QwK7jDenu7}y`a3KfI@o|S`YKWCURt#6^mThyxVPLST!=^cj zmFxP5 z3s4g%hnjd=+7X#MwAljc=`6tHfWA1}(656NOT%0GBi)pdxkxeURb2AD1-SI&rjzFnuw8d{>VbGQ2O{AURH*VeU5$I1HMJE$ zqBDH-$RwK?KBMaA!?a&qa&d$j2ac(QK|0?h4CY@GDV{}w0_;IlDRR!j`l2O&QAi)+ z6G=OQUI#`g?fso0^@)-b9|f)4%jDQNKurCy~AspA~uD=2zPe2p$-@fT!h z5=8rAn=Yigu!189mIN(GM7?1}hjknTSR@YMKOaTNC2+VP4O5d=`2srf7X_{GMdCLK z2PiH77rrQ;3UbP?mXDWDm(P{IUVZtn503t|r2qf` literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_registration.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_registration.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75c2374021fbcdd7356240494f761fad10b9ab2b GIT binary patch literal 38900 zcmd^od2Ab3nrBfYb&!%Mk(NkNS6Py+!#Zrshb+qnb<36|TO~Ta5;iSS6l02X7s)tg zoTG!mj)IxpBsibER?v+_Rm2QA)#-GA>7CvDr%XIO zjh@9~zwgz-Dyl@;j;D99TafBtz5Bg--|su#_r5MGvruq+KhwEb{+AT>claP5Ch6wu zKZTp`QVhjXjDaaQXak?WSD7?v&>$bD5hw?L40N_TogPPyD6q*e}VK{Ww4;# zk{E&~%WUY%Z1hUkOduGF#o17RO+8y1K}`+ zxEu)4VpPkQnCMiySuhQU=Hh~B5)u-Op2&5fc=~EQ6pi@9f_XM^H5~X7%hYZVY+_6% z5Dv`6qihU*Di?oxD#~722uH68j(F%wJbJ~)iskZSI&+wje3VHyU;h{q{VsKhf`+BA zQK3N#pfQbZiZwS-4HQE&g_jDMqDw_gFuzGq8y-fGh+BjL&*ee1pgrkp&LOHVfPWRspM$Ww*%R7svHr6)UivXjzcN+!lZ zesM6{NV$u|a+A;|ri#4V##F28q{J;`oaBAgrte+keKlhu?c%&tNZQ22)R5m@j63J| zTJn1hv zgHSt2pNmGMA=rhZGZ5*vj6~v#_r4d7)BbQcK%WZ47o+om2%Su@xdXV{GADfuWE>gc zjQ?N%8JxaLovR3wpYg;Pyi{2Fm?-KZHV--WOILV{G0AbUSiYMfQ-k~@UYjUYoI?5C z#d7&>rc%DkUYP>|`C{d+FdPjA1FT@44_rN3AG!?XSe*a21|1gPOnhw<&?YS4_?kxyI+a_ z!Hv&ct!u7!xO?d{=Z>`V*eA|oYfcY5zxeB-vOm1>og3V?zE6t|eGO45!5mofhr&}r z$wDFw1@tck1fxF~5FF~6&$mQ|vQT<;F&YW@B8jET&^m=|Af8|&bIOo}^l(^N;s0wB zP@wNpUTOv&a*rAH(3f7S2RX!2rzwRvXFs=ZzeLyCq~GQtKcrubkuh}}TQEOmU}tk} zqA>10({9RZ9yS~>L<&_O3YIqKV9ee^hD=1|K*_wua;n#q{R|o~7kTBsVZHZv~rG3;*~Mws(4j7k>GKYcs8a& z70n47e7EmJ4Q+hUZ~LpXVqHuNg3 zPg6`IL&Ll6_(g1|E;h+|7g7FRii#95O&52_zj+m;2BW+nQmo3eRSuKAdW#|L=0gRL zhXL+e#FEAe8YyN+q{vg$B3%~@3)B%~Beh^?-*iX*_ zy;+DxV2)gZaBUHw{Rmcdk~|_M(oRz8=(uvtj}()BDiDHV1?Z@RUIX-LKMNIsA4y2d z44HQ&+KQm}B2b@B8X)h}{qzVM3|t0!1l3}-ZA&kO;xrTY#}hF+5t)~pqifg?;KeeU zz%R-h3Eq1z9EXmfM?#UH|8n4}oVQRBid;>^ea`{ypC`n;-6#~r5_5CVUqDA#HVPCo z76&j1$--^*u=|hV4Q} z{Lh7gfD!@`!9woEqL|ng$dko!&F+MgQ0f=Qix0}`Pqt}NM(E**ECs?a9J>Y+@i;)a zE8)=G6-cxcXn!c;yG(vfc5H~3tq8=O3`MReG0Vh04zRv3{G9CG5LFr)JtJMGfaZ3I z+=&w1E{-EI%6wwq|E@twBbzCLBas>h=+=Ld#B4CvIDIX^LT3xsg%I$AzPZJ~+?Axn2X@Ib zOR|a%unW;}kPW~9gnPnn#%G0+x#$w`kMY2KyHOlxIKezQ36{2YM1g{V*hcLDOHPql9&5R*j^Ak@nrT0nyjArgFqF`?QAbq+wA z2Yl**<knGXkibJ6IPP{4QDAAy7fhi?wQ z0H1pJ3)`e0#U7SMwooJex^ch?wLTGVYs)ObN1G3TF(e`nOu^|}2m$KcFn%Cksf_B8 zB2@V1*Z@{Xo;M&n(v#X0x1w)G)74#k zbr&M{^+L+#NSE*C%XhETaGu$8`5a$9_l3JLNm!UdP${K}07g;a6*AHMLN7r3eeD>v?*;15o72TpURz1-Ol=Y5uQTuBv$ z*G*Rq#bsaEtJC&Q-rkwEck}k{FB+ONbtf}*ZJFktuS(3$%5}N-o@LyNU-rt-7dXgyLtQWw0$pc-%El` zX6kn91;YSb?`YaS#@oj*WWxY=_UOgh$=i3{y0LPCZ#|Z_ALs4I zNgl(Qx}AEVu^n#TNZZfw_A?~l=}cXZUO=qc?a8!#inmXZfc=}aK`U=>yCY5P9jzK;a+W$M~B89#uIu)luzwZm^W-mY7DmT&iPZG+t4Q=Ic$%6c9; zrWS@tbEa`uc9{J3K`CW(eNB}Vmt|^B{(Oi#edcGQt9_hvcKOg}*2>ohUK@D3c5(kXhz(Pa*Cf|$e(SQAsLK$vv|AISPzcLegkyxnp`26#ZIP_x zbVe;%{eKTOYr*j4XfzBEoJ|CP;71~&98PHy3EI-JP-MQ79`%Po!W@f6VsXotINTBl zo3a3y)iNp~Gco!la{kE<8JW;a31C)000d|XP;`vm(@UT5F9Le^gBTDHYoxt^t%E3n znNZ6FGGp01v;bWOcKtLv1;G-B;G|_I5S{>zVS!RFie0SfUV@^f7_)%RjR*!25s0+U zyeaLAA=^!;8zFA&0eBR4>(H~jpyVeIEA|qcUZH-|029w&^uB%l&yU{rtkrf&vkm7u zl(P0`>`s_%tmSVM{85|oVmM_TkzO=#I}WZ{4$80G@-*+7Nm&^=%q{1drR~Rcav&Fz zvd+T0FUsvKGBTP81<5y&O7+9^027LNMwvw5jdU=|bP@s1UHLEp>498EnO5Y`6g7j> zRL(I2@`l`lDS(MeiVrF4=dEz>ks@!Ak~eTOo8(=bn+udjPm-lfJ;3#wBE>NIn91aG z5>hVAC&il4kyJ6D#*k~G`@T5)K2qW>0jW>PCgn#_YR&=j2P2eFzpatc$vI|lVCNjr z)&+72BNlI=x5x`}3a`m)@fJYcE#1Wt>qVGq#bZWC2E|ytR>pvz1Ik0PQ?A$ALbaDB zF5{FthRB;txf16fve5tE%(+iWRH3wEAV(I#B1{$v<3tB16ki2hAjr+&{VoG-SFbGfMsCld7&5pM(tl8UMdH(bAYEVjuOimwP-IuOD!B?LE1nPZet=hNT z2V~%(!mm}&lE4+)UP)vu^|-M+=tWdA>KW7cWBkV z=AK+0$W%AI@yx9~w`RZZd)JrV;o*09xRycAF|@oNGk3n#^Jb5Tx<$~>&0OFv`PS^; zdgb}`V#-BloPesgXSUU3wvl^#Ri75Jw?s2YXg4=c? zRdn);ayM7x7DHKRu!bToqb z2mp;G(@RH>Fes#x#L6i*KI$npRQ1uB-chM@s6Ziwh>y3)3D4-L9~hO2CTLcL)tlrM zp*`UFA`p?g<&;504hSt`P{r3#7cj+MgO)si0K6n(@EC$-@v0JlTaxE2Q^u!&QoBHa0M85BW3FbGBQ&{7}@675#;EACPYP>FE$ zN|a>-SK@Rq5cY#$Y&5w`%taJ^p~hM#rd*5JWOr83wFFXjStP43v+bv++HIm#%c>Gd zi3-}{K@T(H^Uh3)6L9%rV#yEU*T5nq3X`w>njbWwfO>r>yaMTze;(vOasQQoF9y;e zfZU7GxL^;(d?0s6#jEc*&`e(r!(=--JvK4r8}!c3PEUz)u_BPuML?|0_CbHM!*~LS zMFB)7LZLCZm-M^hM2G-fXw#brRn<@$-v(cF?5`mTDrC16yS}j6ki$+}n|W*V_omY= z1ANOs%6bImozjZekG*y*ZEfeR?P)8p-|+iU)$hA_>n^VIi1;H+Qk`#tjAjpS-}9rM z^xi3c?-Y0L5@+A@H}=-gqA9Pj0`JT}-)MLC?<{IiS-s>>K z{haPJ_FEBAKcPtsb-CG(e$3zpQl!#I=^8Sy`>5L-A~N-^K_XM{q!+*tC`S7u!9X$)3dZxy^F&-BMgx7JoaD6xaQmWWL)3`{ zo(D@4#H~8i;7bm;Pc{gpPF88k0h){}P@uR_&IX$E* zVDaQW-8`-ZP^~HFK}Vt>2T>v+8FQZmnTxg?Xh|gf5)V8NI+<-!ax)_*Muui( z%5w$NDrI4iM3;OpQ2f`~tN2ryn5IOwl*pc;p^?){WF${4LHtHSqm(X`l^o@O#Gz$< z(sK-PBVc)ve)VHMy7Ws=1=NZ3s~Ao|zbYsoUY)OXrY-HfrTwpZ(j8-b#~63U%URmf zma|Aq2$kthS$lwP)o*`i`sQ@HZa-hQKV3J@*NuN>Ye?IAKC$(1yN|EgJmj&LxAlH) zHrAIdAN`GmvehH)df~MfxZ2|>n*sC#cY5y{Q?_B?Abu@V zxl1YA(~#fx9p5i~w=_-n^K}1v$I^#={9)f3{jKGf{@zZ11{}rVHxF}-BPqvd28ide zH;<(qdw9p5wBso6IC>{`cPQl;&$t_Y4Iu~K9N_4)DaTV8cO%kHAlRI1U&^{aW3SGK z-l3hJBzo5pK0lcMZNe9zC@besO~!>eui#Ohup+tSNZ|+cxBx~=R`p1zjtFN4&}Z&} zro%Ex1aefkNxky(J33_c3ALC`%8%J%iXjalUr)Bh^iq3BV%P)5*Jli>F{1D~*`AX7 z=Ml$<7CPva@iE89#q8MB0UwMCCo7|flp)HD59&7+iX=3df!v;AzXb6hsSjoc4Xb|jb1v6Yj5u$sL-^nI2_ud1u zPkI!oA6T%&=N5sL(oHzoA`$N^K#X--42&=c&)KEsB1tPl;7yttnaGM90?tOU{x9N* zXgL(x%_7ldZ@@{YRZipK;Z_Q+l&B?b^$`_WtNtx_6rIonCXDUViR#yX)a89gs{QubOOT`R-Zn znad=bxeeKz=X=kuxh`n4VN#CSyaW}psj7Kv`pxO|wl02KS9;rVe%o=u)=jAYKHjzu zsDD-2@&Tm&RSNY#o3cHXpZd#m`Dn^|40JpjdD^j6!`&F?I+?Ogs(9K?=$2OpU1Px0 z){@JI`#gQ{^ifw8obMYgV?DoB&A4{zNSEAmxt8rX@*e9mvbPw zlBZchAaqFej2>~%PK7!=A=*7>`DLO2SEgY-9v4V4j66IfQvB$p&=b{ZOCf?6y_9t{ zHma#wCuO;`8wgYWT53tLSYgdsSD&ZXYhb;d?X^w8%fpjm3y|V$k3I@1&R=5{54#D# zdf0KAQ)8wSxOiQw`We(Lmb}Kz-CMW;200>yp2A=yL#`?q zv@IFRg+W=%1KAKZv_uo|&@~K;U82FqMM09FVO^FuP{&ocsBsWv2%&h4hRm`hK&#k! zlId)6XZB~gVEM6AyGbZj$N60m6ADJ?#MPt?U`#v;5Qi9og868%7!KAb;Rsbn5>0_0 z9wF@*PSA_~SX>lgAWjmOh#gba3NkcwB)7G7cN}YPQ)kxRlB7L=MqAZCw$ig8g#dxR zhz*lFwHQntR4lBF!g}(duno}=hgFMqstU-;fn`F~%*faTGdnUPNmhKb6SI>eS&Sx> zDFG%YrcR0oE(^%2HE(?~A`?u+e%1c%M1Z|6*_nb~07PtpC4@GtJ`}?U1#Cblj0OC# zN zfLxOjBsGiFSj4WmThDa%22kJF-@pk~1U;gPK&1#cz*`Tj#O@5GtjBc_8^d=q+|z!7 z*e)yRX&_}g0umCP@F)51lbrWF34dV&!aL3Po?dgE(IUJt6pZ8(nQVgaCJ4gY&)fFv zBD`}c+xff*50c$JZs4r-s4Wh^8BSNX^3|>B>VCc&q(E!c$Cmqk;ci;Kma(^_+xq#o z{N*|x$kI$?fpIxi>zH#tZ)%9-#zi>9Z_2Qc^{^`q~ zIqTng;msGg=8=?hGy_(*FTeS6+PR;1?oT@hdFSBW-c@7DIr+B@OIZ(B$cZ%TJFSf%8X4JK%i3 z(^CqU)k97=e_UyqIAZ*`)-};*{J75y_Zkq2kc5EH&-2Th9+tTO9zf`P7m!tbz>pu_ zDgX!SEFC)VR*@*^%9_48yw#Bh<&$qKgLs5{IpkwS+Z~|rzlxifY6Hwy=IdgIf0}F zPFtWaW3o+(C#{(y3+$vdQl5Dbx`3H3S)CV2+P!d33~A{oOA~9VWcg`al&m}_a1Pcl z$it(vb~Sd|q#I=T)VnEs+N=i=LRD@7p{ygPJQI_?(TS1CVZnNOe0pj`d{Ch%yZEyh za{Bb_1lTzcVOSPQ)ou);;HfMuV*e1@Oe@kl0>22In~StIuKoEDVw-sh+cZ5Qwwd6& zCphLQf>q9Caz$dAw+ix^PT5ZDBcIcJ_i66JGbArQ8_QgBYCL9`zT`D9<gAc2#mE(i3{J)cjyt zQHf1ZRy~q*G0Np{Q0xfZ3}vV)Ph;RpM0z zXI+WgPy)+SxFlV46QsqA@*?&&5$TCWP7-cw2P~WQ9>l7Aa7mtM@_1u-JWE;?2*wXM z^gn^$78S9u)JqgN%1Bs3rrO$w2$%rq#6CzsH;`OqH6Vsn`@sNNx3ALXf}f?vQ{b#2 z2LwwEAzD#6!~%dq_F5Q3>PoYc`A0$J3r?Rlwc zi0K{qF+F<=vp{4(?2m=ge89a*{W1&L--XyhsR-&t#4l8-pRyr$U_QfeYBz~8Pvmb| z6nu(;OcUH?rI<(IN$A#t`}n z#XWVAm@;2d)Y$}!yBk0#nAh|lBgS z`IPg*<|4mLLlb0EQ~!?brY-H-!@KsRUBkR<7~COz=4eVg`glhl0Q;KC<>x?Y?Wj@8 zcrN8QpO;_wstAIwTd2yGTh~$*-49vy$&}+1Sf)Lhs$WPsFFsDypUz+P^C`!Lyj7QZ z8h0OeXp-wY#hvnTu5YEReqH`%a`nvW5a&9bvYt`#Hy#*}uX=h;4p8s!9dyCvL#w9` zE+3gZR=C`+AU}TGPJaAZQw{v~vjNLwoAKv`uE}QO&zsF~PgXi1u32mWSs@Q(Dnyfu492=Vv^=G5P>kmNEh&X+1hV)e(j^!9stg1X)uV zB{3GXZa}SZ8!0y|nn&TXF@N1u7)D1R3+p?jTmTW3su9ujg-y6bodrZHv93=4T~vgl za$a(cYI~o$;$$O-{ydVg9D{yxjdoqtJ8m)@vEtkGJe&o z)VChLc2$}GwT)j|np#vBY2GRJ7R&R_L(IPp`5PyeRmJ)HcoPow_xIH{sj4g8lJTqY zzbLf>tmK#5fsEgh{B_%6{8qtOAh|rD@k?*4iDvw2YNU)`i`OF8X@2YRTdgW{&iLJ; zwoZsiXd2YkrTPl+UT%e&UX?;G=+@!Ot*Tt~=tHg7y1|mCE@JgqgM|#as`&Ey#D9Pm zvy6o<16S_Xx`3m>3`N6w%Zt$aX>>D)zO>ov#EoFGR(Z!w# zLeNR8lo)xaLq9N^#xh|8Ce0XzPL@jX#HTW~S3X)W^dy}cD{JVaU;vyv1|qXL&hzjE z8hvkY?YAj+DpV@#Yehy6mIWh57Mz0{}e7gRxG<|ZUmE8)+Nc)+GeDvIF3 z2-uFfF+{#s@{xd!AcS(Ui1X6u5lYoX7RtmYc}-O*7D5SX$reel;=LTKjAX5Zv#LrW zl#&N2q{wiSp@$r|bcp6dsI3$vC)bi-K{6lG`pVl0v>~<%tP3*R&lvJ?+rT`+dMv^j z_w#jRebib|{v^mAAx1uLp6UkH+`N%)Jj6F1B1~vM z@96)`QT@jAw~BaY+nS>tm~pyt`9(U77eAFQw}b^7RK5CS7bF^P4ZPIa;B48Y-7xz~*U^n`hVU z#g)VlqFmQe?&vf}pH4Z>Y}Gm-SsA!Fkgh$%*B*MWC*A)ozW-a?vk|WL5LX*rKJvM} zQE8q2lB&~Ds6WhT%#doXuzVS8NgxEap zK4>0y+45m*9(Ts(PTOeSMyG5|a`Sj^_1zxjn|ryYeJhh(t>;tQAhb_o^E=_2;dDbE z-_S?e=Me9JfrR6ykaxDOIohCs8Y`D?U;{PF4b*x2`8$Pt&oQq1ICuOEN6(}jjI6(-E^=5-s+b{%dgTS4e97pP`Sk2~?-pnB+qK8)H(;Cr-M9NC*S@&+0XNbTqf9L9@2#q;`mBaDxvyCP8TUrc9- zLmC`X|3|n8<`Gl?CJRLcV4@j*lb8xgB{&M+&a<42ell6Yz?GhZUy{}2_BoZ$ za`AWBNU&8EuT@?vD>}mvvdS6{q5QOmVV;_v8J6r2 zuaj(;k*Q(d(Dd+#?Cyf794(^Cu^m&u2}md-`e~VUVrl%%3|k9;*NigJlu+iXDy~Z2 z{^Y+T zx2Q)y+++N~N$$WYu6F8E+w>O|&9|*&xAXS)+s%?Q?%LLL zZ5LnLb$f1Q&sy#NH}-#4UGw&iKY8}noE#Q$m z;XBW8wUa5^sk}^+t%_ZQ4>p`ZljI-9E zinc}JOmaP94W_I^y7+u*_1mj+9Puuz!sop(lwS4JO;l0uHx3?v%ZJs2Zn%83XRr(| ztH-L~{Bf1#gu(c6qibTo`0;=l?)AaA%k(7FjgO@ODS~?B965`T!P6dr%K&Hrr05uy z0h}@-oRSH4#5lbTLV{o|_kfTlZOE+hjsQ|cU8d_mRsqr|M8B8~t0(l_;9GkWfTd%` zIio=LZ218cfu9VoQRN<12bw96QLVs3$e1mw9;>1<(|jbAonGHrz)`WpJOFUk2ZDNa zR+N&OI$HqP#in1q?WGdIgoMN;Va``|iZu%nvydDW|{r&*zAHz@! zio>nqIC0)9T9wJW@ks7Lw6#S-4<5eQ!~**Bs@HiLU>Cu=dLRP6d%;i+G#SJ#OAI`n z#K=NjvaJ*WWgsd~I!AER5%fj|ThPoAL@wgDN-=5GPYIM2OBi-zzWT6|C@WwIaiO?v zlTbVa8WC`y5)e#NxE!^RNnD0LP+X$4is3c^ioF}NMWu`+?AL{ghaEDoqQdNZ1C091M~-Ee|$I3d_XF>N<*>qg-z;H+B2S=rJKrffs1(mK(z zC=^oH$LZ!R2L^H*{krslz!iLzqSsUvGDB0%S7Gf-J*<84?1BrfvW3e}k9Zp4a=+d( zUINQrT;nF=s>uxZ8WS)Sdk_IBF2?@mi(fXOz>07Ma+3~3UEtH$s5b2K!+7%+kgXwg zaF(w$QFVTQ|_um%s~}L521iC6;#xP8sj4LjI6A0R7}LP0w5g|9q^2d+huA^ z#EXcDco9RcD!NyaScRss%p?{-Bm-*8LD)m%-uGbhIb2gn$N=uK3)3G=-y#VEx}cHH zkhu%?yc0K6lNJkhg2PR+vn;xbBxC?~2}7~E{KyJbNG3K+$6_6wK2loZR1_HW1s0rKqML@Soip)vNi?!VZ7iE3qwtFT z0#9RbiUEH_rfpFx`WZY4J$h_>9*WfhUv=!q5EE6T&SIy+91rk~1L?*YzHtUkhKZxf zKEAFmgWlcU>qQjJ7+_Z|k%NQ{ljux!D`&&4uIQF@<6*w>@LJ;$;4c5ZoXn-@v9kUQ zH#5hbndfQ)Dcgb=9<5%FzIK$W8@#*!ZtJR(bK{ad6~pONTrg{v^i>lV%+BA%TA=8! zdJKa$>V4C0Jnyf8^9MG|;9=tjHLk$}#t#md;htb9&6I}I8JW^1;pV&4ML3Ixo*Vh> zJ|~2IKyEGPkf$Bk@uDE{zu{%RPZVTm(kBYAlzgA#FmmZ7Gy{R{8sY1u;YHWT($xj< zgB$B&0_=04Ir0LG6^QYMv^k&V%O*mhoZU44FMZg_ZmfXjVX@#A9(3IFLBkQ zsiH9$uefWGmrbyolk`H$>Stq0Or|n#(-oB{5!$q6EUi^3n)3xIbcWT zJqlx~W~G4~o`TwWO?tuw@Sq0e#v@ihQ!TKi4~PkjK%`2=Unxo=Qsyn}rA%J)1|nsQ zm_6p8ks((VkuoRH&cy=V55mVq@VkukNRL+OPUYbGSeeanhj{JkBptG&goba2(5@}0 zPJ3apY8XE<5*UYN+okyz4!qc&+;%<+Ui=9eh23DKUlZtZY{S&wFTYN@fqPN-01`R- zu1-pW4E}k{QyjTnn02x?`v(xv*%EeVowY8ez_gb+Y9jAVi~M3zTfQ{v>HFKn&DnU zK#&L!0`iLKNnm1mVju)Y2MO!R7*+UG`JACt(xLVCI}e}a7N5A*fAOQ2K}UiH%XndP z0u2^F22+*(q7NL6Iftr-I;qQ38?COHo&mU4Mw(p|q8C;%ZXia<`m#bDDW{%y7c_n# ziggPP7FAtzdP|X#s+Ml#m{5b!ZSIldh(CYLa7hFyiFP92gc}30l6&a7DlBJgGFc*H zH#ZVI&*-(+_Kg97Bvxpa5hKygIpn-F%Rx}KnytHDJ5^@K3T#-A4XZZ-RK^OA&^CEM zA&w*Ltp+j=@%Iwe~x!VZIEoI16MRKc>uUJN;&6gE&EAG!uc7oOb#XPc49Y*h= zO^_F$a>VdBaRKh+?H)u+3OJ1p8wHDi`<3j)G-2>~OA5 zKqv;6I%E@evS*lDi?0qAQCAuuzhXZ?>$i?dvkqniiiaU!{{AgQU|+}6zr)in;H2!b zuJvcAuS)OG52U9{Pj+YkhwR0XaKW0&0qd(>y&J}&q0&jUSwy*)8JU^#oYMH=xe8%~ zZOqAu(>~?3wByi=7`+cq&*SOmctUtWWUrDaRB?1D#Tkb85_}Q2p)QSG30+mTG^&s{ zpp!+Os$Hlxs_|SjGYIMG?oXY&qxN-#G5-ZnHuBK>H zB%2Z?Q??f%uxyrjgl{&-rX5|pgLuK7;k(fc z6E((UGl$`nV+0n)!tT};)#9cR#I+7_e5e&#j`I!UnF>@#+jfEF@!KbOTPqQa;>Hpn z7!@U$AQ{a{FvnA1c&d`O9=g+cH_7+UaEBP!D>-HL=G9GWvlnHi$4QQx%S>}}bfWhk zXeeEosjg&n`0kmzuxfTZWt~t-M)yKLzdGa?ucYo(SK+zSQw^7&8VB*W`_4mfUaho@ zA2F`hy2ksAt9@p;*SH+Sp+lhHFc8geE*>So&9Esk)l+b)pqpwp9!2a3<&zF{n+N>n z0PvgWqgV%s=qZ_T+h?^GAL0ub$XQm@scZXLm@8BTLRf?x=q5SqtZX%kW)sH z>0$W650eAzmue(iOM}r2W0CH`ZfT3MS#yQ1!6H=*h@X2fYV|BSwnMg%91=MnElUCEOBvB?FiXo z4>yJmUGc}kx13DaK?Iew#K}ga-LMm9vZyBp&Vj)i_4>vu-sK^=NlnG@be6=ybhI56q;#SduU7{*JY#30KM%kE9qTCDCxdiMY z2z%~f3Pg;Hz>%=gA{RY`h0%2@0YGW)icixiQeOWRNzng-h{#F1bx9EHUx%CKO{!*0 zamPG#*9s(J`|dP7$kT)1Q1dk*?|HYr2;YXUg4gUs)?}za6^s!dmx<)#o|ivuo4O zN(&RX`h$0@D=%`jBPrV`?p!2Eh26NBpRWG{ee0uL&;YM`b`SMZ?;ogv^9L24Qn=i! zuflV$Ww_CJ?}%&IZT!$}hI;^3sHqV<8M#Xq=O1bB@u~I(q1fj`@appk7N2h^I-dyR zeW}m)?Swxpy|MXxAp42M!zkg3!s{ZRZ$3Kb^RYOEh-%@3y&Ii&PHU!&k1!6wm4=5tr6vtQ@2F4Srqhh}R?DB7@uL_K&qJOZIn)=u4s9D2B zLuOacSH{x?rn1|PbqcR5z2bG15wDpYZR;dJ<|uIoe7El>k$)sVtk0X8OpcYgbqX$b zjpUL!G_Y=j$IRrkc%41>0H4>#4ZBUXw`1!RT<$5i(%n{Gs=DV@G zZ9jSD0Y0zK8){9iTk{VnxUBcLn4EYIm-R_Q8@w2PK*42w)^GqGW*<;+S-)y%H@R-N zJOJk|uJyBq5rfHf2MUPSyAT<#>r(}baJdz~J+$JuJ^t=X>l8lUfk=2=pEaI=%gVkx z#yiZN;vXJer|_9OcTu|e7Nmo}tS=NCgK(x5=1%j-ZLoGNg}xSwU>b&bI4GEK&r_jL+-Qx+2w?~5 z>sR5GI9^RSPRO7oLk^LLP$ACj%9_QPIDN|7ssf94WdZhZW)XY{C7^o(|MsBU66O13 z;8j2wvV&159uU8+qpS;Fz#kREalCFc7z|$(78p!l7gGk?FDUCTsIp&BCBLG2Kc#yA pJJt0|Y8OmqmdbRA`;!tkSG#+yWX~&w|704s8C?HB;hALee*jDiML7Tf literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_selectors.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_selectors.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aaf6e72704a872a7bd421be394ee36fc05eaae15 GIT binary patch literal 5312 zcmb_gNpl;=6~;1H2vVfDNS0*D8j-RoI0or8wi(MY2n;DiB)|ed$r{NuHAFXoF$Xh9 z&kSi!`DUGS%T0&a@Q+6`blE)8NS{U1`^X_Bu)}`yqnMy-K3uED!S55>8W^ZJw}p5 zc^V_iHwisWQjJ(X9sC}fa!qTm>FyO8)TVpB%c!Fk)Ka4{N(daHe!gY;okB722s?V- z+N0|3(Q~E}i>iGA#lUyFrf+!`Wr5SqZCbwCq+j`kP1A36ET>&q3!FXQavdBYH#2u9 z@O-LjHtkZ!FKBF!I*!=;Hz*d%D_q&IJfACNtL5`-ttU2{Hcu9v!C30)ycJw+5@x1J$sJxenB7|H56GNzA{oIXLu^$TP|pClLbDKe>F zBvbk{xv0;OX?>Q==yPONzeMKr%jA;&3c0LbA+P9H$rb$?xvIZPuIbmwtNM+0j9mXN zuD?cZ=r_r0x{CSD@8THM@8aYZnQtfXz91>LL3#aI3iCJQd{K^fK|UbAl+?vz z)DOu=l6r@H3{0%>sVJNR4lTgJ3;qtL=2U!i#E6+TRU2lvYa>jmY1@>kBgpv)Oe{$` ztJG4Do;pOr1X#e!07DPyjz=v(qI>k*^RVUwOzi+rYSHPLo`*w0+;=^|gI|F`^wD5Ar_j}&T^76sK!IEzwIrf%X zZa2JfHhtN%+D_05ub;DQyKTz}uk8fB?>Y+|YQpYDwBUC>({({ZR>fQP$S zT|6mexz|B<7F<@KUDL9c<@hp;avvDq#(7IE&nvvMYrWeE_X}lfKSI)mxkK%hmh1SY z<#=}%H<8KyVm@4Y=^ah84gTeq=Kqb^Vc0O@7FQM!sJUP{y}*Cmv&|OmxHbZDcTq!~ zvg|^UFbd>>>BbpMTfpFf%X}>7Pc0)F9&L3zlr-D61W-#Z5t445V!gY#N}a$DDRa!O zV3~ti{ony&JMNDA6{z!PY-}PicU_c8z@LFLp6yWP^%yF@6n;4} zCX(Ii(AJ(bGOvWw5e@<7U6&oY0gmhg93=Wx&&I9vLX8nid(7RnY+A5@D==L5_S@&L z-ZDqbufq8#Cf*Ju&`*{X*s%aRM!96U@LC-isXhDXdvUQX=ArWBakzx(XwFlbNF1&R zEaCpTa4cFp)1UIxmPj<7-gI}MKhF+uHGCs^C76abtJL$4zV|Jmq9i#Sa2)rpnyt3KFKFWVaBuDUIaLCoauqPE%|TjGpidb3C4t`wdZac$!f^U`{Imy#^oguHak3_s1CwU&p>g4}CTE zhxpg%-M&qrCrShvw3M=S5pB@AwFAeY?;S)4g?^QW6tNP$>!807h6B^xz&6<~Y*52M zHqKFVAp{L27FpX{8P3Gg3$0hw=3x(U@{S4f)H~tQ>XJGyZJ&QjotGIrpFbvLIH^m+ zPn;oI>&GbKU#zI&751QEKhLJ2C{}=zen{|KQ~SJWY!;iPdpy~uem=pIE!$)7h~*gj z2#FReTgCC7Wx0dCooGWF0z^`7`BY6a9u=ZWXG(5e^?^-*^AyeCnY)qfb?pa)&* z9rBAfBtnB>i(bO}5=Zh_j=AKR#{SE}MwpN?Kvx0}sc)!5M1n)=K!QoXW%uc?pscW+dx_x0Jjwq9v8 z>&0dTd&-p}Hs~{3_i9zm&^C*e4I>gaVASfy!%FjBw04pxxA9A>m3s4Dx%kM~tX09< z&^EMElg|#xVyRD@8xkLB#d@ErUl~%%_1ads_ONQywuFLwJkn{cqHUD<xU+G-` z?ceEaI+Olu;`WtzVnTf9;)$^7&-6<(4{ilSsuqJAvNe{0E!#Kffhn;+LQOp%mC{g4WWisD$oc14cJjh4IY z?5-5=A;**XO1sc&YNGWFvoO;G|G z&dhuB=KXx{&7%&^oQVkV`ByAa`{#fl{GBbDAJLl}+=9s$0uqRTe8@lVoAVR@Tz~}T zL?X@wNl+37g%JS-h6N@Q#yDXAMJVea-w zt#4SWp~DsYFfrC_)520#!*#4%i7eT`y3Wr2z|@S3R9sL^i;5YwY|(I`!44}L4W#r( zW!DgQ40(%#U;CIZRt4e%fB2D)1dyMIC_sWpBq0a-9@I@@sE72T80ka3q#yN>0n|^Xjgvd`m>vhlP*D+n)L4b>O&hjN|4JjxpeP zdESq19ifgNquxG3jia~78`Zbt@6f0lat4_)K*szb3>w(~fFbfB1d=EkmXK1fYYp&elE@h%ogS1T;B!TOSs!j0m=P>efz<3SrOO{PDaeZCAQFAsE z8FkZz&w{dswL4`)w-i-3$Hx{S#jUZpll<8yN=g;@Cw?~noKhojpv5f)uPmVEsH!(? z>tRDv%D84|5QxdKEEF(ROQb3U1LA<`!YNEkfW0k)SU`@SAVV}vT&|f=>oiSe08JYe zCh77C*2%FctlO4@iLTTcOCOl!1rH#$W~>>X!0yN?2TPD)>ju#=P~>;TrmSN)zW>rn&HV%lqUm6>|!#OWfgLd7PKknEf- zK}k5lv-77lOw0y>CxHteaE)Z(J@Fxuoo~xH? zv{>38#=5HEgbKI#E~E!Y(C>RB$V)LE+>Z@c`{?g&7Smn=as}mEP&bVM9ZR!^df7-xA|>dR*$p&YF_8D;35zN7z8)}0F7ogx1yA8;w0low=9eMDSC4G!7$Aa71nI)c;tU__M z!h~ZSR25AS6OYo+G_JuL2QenZAT+dqRj3?xI|yFZt2<9M2or6~>6?nCR_t!rcCP_n z!fR87B2K%Qh$;q1X$oWoZCiro2-+f$4S>NFyX){&edjM#4b*gcrXb58q!P&4Vy?EF zo|0imbwz(9DHfFShGjZEAKKEU3bjE3-<$CEVJA`q!zE3HBz8KcRN$1h6k=iM2#4Vs zNMLw!Rb4uPF{kg4<~4mR-cGM-N|2q+N7@bbP;(Qs13L8r3;H+khvdwya zb9F|wX6?0A3+mabX_TQsk#%fAvDq+J-QLRklFeqjnQT0w-Eg6c0a;@mgh{;?@vtdP z9_RR&=_SFiE$j~n-MxSK^mm{BvHmQ2_50|xXVGgO!l6akmw0gb50P+K%@`V*_3W1cf-A!NXRJraO|XEx z3q#yb?y`9b1`h&c66S8WnmU46uLxHw#}<(b^?sqQ&Jn6*2w&&j`TXM6S5CI%6=DxdCxlNWU zMj0mIsT9h}%f$s6LAjay@)Do7FBE2Sc|PrNr&G(NQXwxdWT#6sc4VuVo0%=q4)63r zE|}4Ae$;ScLxu5GsVJErm&Ki3rkEv+TrRnoy#s{ zXxCB-p_M{0BfIOf?`Mm->0H{iC0)p5Jwn$q$`sk!LQXIIcdwp0RodN!M$pDKJn zV`w>@&LSk|@)@Rjp@`VqT3kdj4^Zb)G53Bd{eZ7{eV`p>4Lb5VwrWO&2H;U8tlW`% zY)G?VmJRbT(0*AqEoj%vGPFKKg$j#JStg5&;XWH!eJ1Q5IDB9e^Y@S{zZIVQ#b9{9 zOXwcop55!YwmrAko!rjthlHWqyJG*|PoZ0DJ*>x?z_78g7? zxhwYV4c%vg2NK(by@9u3h~08;1wm(Dgo48%AVY17-3BE=>cWdoVIb|J{h7U?(Jw#T z9lS;_SLxv0=N+NJ$csK<_#+=3%+SFi?OS@@9_)*}=oT+TUUapMN1opviG{Z3_Gf)U zsAu0F38s9HNB0Hx{p89M^PBhqpC1Gc$@_!e^xor}jPc%+Ti*u0d6)6sJ7l}(akT|+ zd|7%t`sW7+0!%$J%=TkkNLz67iTDJ4)6U3~oP6MinHwnDuvV0Ypml{`sBYjz z7CzRC685i9H=0KyOv88Ga&wPZz`|;wc@c*FfY0Z9?hp9HA0vV<@ttt>M=|gBO}-G= H$nX6(i904K literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66fd7b3d715711f85f6d9c04aa51623f9a7e38ba GIT binary patch literal 13341 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 WrOYE;!D$%0$R}VGW>gb-PyY#6mZbv# literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_ui_helper.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c719158253381cd8b57b7d9caa657ded718181c GIT binary patch literal 21463 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 literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_utils.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28ba7ce2fdb2dbd492717d3401b7215e5cc5906b GIT binary patch literal 12561 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 literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_utils.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_utils.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ed5bb01ced3012c233669735a585eae6332127b GIT binary patch literal 19318 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 T@1L3f$>_J}TwgK7r4{^NZZW|J literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_verification.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_verification.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8104d597b6ab3fca4dbf33f40ca2fdee87d86a7 GIT binary patch literal 11527 zcmbta+i%=fdM7y?4reqPjU?Zlc+mlzE>BT6%* zd`L+jGTA1_Jx{ZIYf)vG^|q<^EA;a?6fSMl$6aIvJiWXYD&k?S(Q zD|Ln6bM+kVxsKY+*YjPiu5}CbLRYWr-D15cOO|ToJEiVeeT+YAopN`)J}yhPyj!sf zdvbl^$C9O6#ZM%w__0#2TFR1CD}{fIE=$YK`m(n^`@YSbmJ>NP3*ELcYrNTM_`Yql z9)8XY%dw42z26Gm^}zAmM#l*r{wgq5Y{w9cN?(Afv{=i>>P@E^#Ohm)AH=z9?slxd zvmx5V`Q>nsWdJRLaefwyjM@=-P$#HUFe z4gWN}T*bftcevORmQbSQ!_p~M?h~o5TB@D5^0sE@?LtdADYaBfvkIT6bv>ymqJ)-7 zt(QPoY$;aB8pGSzP<`1NM@bp=<95-m*m>|FUW!$*CQvhBovdt-BR{s2&0Ud<)XjfqoHZsa-G0|)c6pzfD4wGbXNYv5Qsi)c#XCF zx%7>y8e!mdxg^XPx9yhAY&Sw-i*tRW5pD#13hE3}?XPp0PMIu5PmSu_aE8ZNE5@Zh zAJ2^oE6m&SZN_x0LR|FwGRuneezI)`85uRV`Vy?CPg=)ow`~^ZEqf(w*OXWVmli97 z!LKED1h+?vH|FlXhhF@u>D}R+i={wzSqPunywuXU;nPqkK~pa z!sQKUo@vsgO!EY|`(%)`f+p*7Nv4Rc-xlbQY4r`p*g!sf7vxAVi838GPC9!>z z`2Q;|J!wr^rk5en5b+(gHRL`(N)?EEg@_t|^=8T&*Gp<7>V1k`CNjPm#IwDe% zix=0o8l*bL9oq?@BZik4VcU4O!5Uo~g_#!V%Wb~R`;?As`Uif%E*Y@s_vVaw*0xt% z$H%-`)|?n!CoqKp{s1qZ<{OtbKxj5s?dEzkw_vY!KnG{lHRc@`BgbZ; zGiF>{@u6sVvur;*4L&A6F7ZLxZosyqGa0jKw7|7(`_0u3x^m}?XzD$8#a?f?u*1v{ zea2JS-Q(lAJkt8utmSpu%*ND=#)@F{b3VUq2tB@$qE!E?Tu|{?=B0W8)H&3DUCCGE zicHUnJoT0Sk@AP=>*9tP7OgoRxzKRs55v zLCn+R29DR00mFYy3;&tsK?Z_;?~|?xhkU?#(RTfR%u^H^T$RC|G|afM-$JFM7-?@- zJg+lnEVIxywrrR5lW`VCVnPf=mzRu%MhCH~@40@EvWrf?KOE9>Jtk_SW7}*YD*W`W z^`}>M&PA6-kgYZXBOEA|Z!CBWp#}z0%X8V|QWh)Bj`JvQuEquEp9_-$#Y`n{pFFX1 zSaTQuegzkGtR$;)Iaifc$WN5m0x=d5`&~Xn{hAay_kVro%cbI2E_`HO=F>9R4z`4R5ad~?kj#z^u zoqrMt zcWzWW-)^q@eqeVx@Tcy~y+<`c1>f12wO_Pfn;9O)EU}v96f^g}`1>)0oN`|uu^YG=!sAB8 zCD;YgboMd*8y9TG{i%qSjAi>n=;9-}#-7Be7Qz?lI^^32a=|_NzQ6>&6p_K4u~@5c zXBSt^ps{Y7ev{d@>#ur&2=0%+KY#ny!mS(EmT$eYXx@0|=DfLZYyPd9!Z}XC{UP}8 z!r7UdNM}|MS8ZNQQJz=5AbFm=KBZq~Xd5P*T(q+g=#m9y>$V@YeSnogX3myW@ z-jB;dppp($DrA?5H6;)OC&td>_0RC{leC4;j-DOir5@-0GyaN_B1#LmWs=~%qJJ0OYT%Ow)~T)TC{`Y}MuBaa z-BG#s(t2v&D!>Ni*YHkh0qoT^&Iw7taQ$wfN9hAh$wc;)3oYjO|CLP9Y`6qwJ1 zHBEWGbqv-Pcmq-kvc<}M}MGGydj>r1wS8snVRn(J1e8| zB{C5P7We!(wA@Z3ZMs?;MBb{D|rc?e)ut)$3 z*qrgjKO!n%uMj_Ofm?RRGTJt9JIZo+YL?P2N8dZRAT+SH%@=IW2+4}1y)1-Lv_1Pa z#XX=Ac`aFV>Z7KzIqGzr3Iq=%S?q@>W3S>8(i0#mqzEH+VRnIr>GG_u1!s++f(-36TUQ)!v;BN@CLtX7QWs`+R+N%Me(t%1f($)i8UF&=;S04(#*9d} zXKMGN6H9#W-3AgmQ965_4 zK7*Z#evoW_rlZW3UotlP`Z7?Hc(a0oHqhX=?AeH%JJvBdF#_vhgQyNZFbDY{TH&931RPqds) z2O`V5LV`v7Q4Ae()$VKn6RZjXF5N9sGaf0h*Qs%3HS9KAbH!e5Y&rlz$_!bu*EKD> z)7Y+$-b!C0>)=5S*_QQ+?guUhx{tit`(c5SFQ} z1~PUGQADgAuVL|!rB)myHW?-CM=5?>9a2rMmTUoC>=%WUzCN9m5#A)OWD5O}F%4zn zzs0}*GhCDsQm7=RQU9RoI^q`kt028XjjVj6<|xL|DB}5A&6kjLX>v7J&h=YSB&6yk zMd$6Rav7~CIqSDXd?c&isA^d$Azh=IYEDyz;-i;G#7ErN914OwK8G`q-9;VvT|lU7 z3ZPDf`(4WEklPE7y{jS*L%hOXz@yhfw;$ifOs z6es9ByQ6Gw8d;sRR$MC~!YW!NL|AWve#{ypdfuXpjPMmsQ(hZKWHa8&Bk!}V3hpxQ z6XMQWRg+%O2JaQbNupQ01B9K}{_`aFJI=>6HOTz{22Tgoo+e;f8aIbuk5D#+&yT7;AROY7(LcZ<1oR%z&kL#Z=h;UW zPKi#>S~za;aVEnE$Fkj8I!N|DhRp8Z5>La(;lM4)4xxH5MA0#BMf<04ev5ORGZvzo znTmao<0ZrCcHt#mk08UxgP}ELA$sQ4cO%V3XAfkXugzSyU8IRNbAcNVl2v+pZN{~? zW<#7=Bz5qOg2C0HKxRJ&MsS*lQm$ubIJ8-p5*t70+C@CD)mZHS5+Y2yjYtOfpr$M@ zMx}tC6kY~Y8fnh~EJS=&c+Pj)ZnzP>bVw&_b`TA)WB}JJ;E7^*6JvZ6(GU-3EITCN z+D*bfLKK|!20|_rjXo<)eRlYLFdFtT1Cd4JgTS*`0w2+rk!i>=Fd=w82R|TkUaXKPN*|C?HuwP09gYM#j{t&Jwq;JouQ2NS+Gc8)%d6%kRRKJ z{<2w+)TjC~Fvw)N8;Hl6A;T&KCh;AWsm`GJ6R>8fiN;SCzYfiTKjL20-0%>CAo5LEHfX#~;D@@pGny>y1UZPjX`dYcR!qGEe zWuN7cf9@~AY3DwgqCn1YFjYodv_f_(khcOMZ@W^DBVG4vfxN4v$k+t8j=Ak6SjQaC zt7IU!m0qrI%XwZkv6Fj{vp{R{CvFX^;XN9=k^HVBNPy|V$?r{$nA?~5sVI*@hg1tj zW4}h=9D`)tHP#>g*2R5bChL`7ZvXFw_$mbm1jz!VTjg+Hh9L|k+zT*Pshk3h@o0CVX>_kKK%Tp=~LJ^=6n2D!9> zgEO1e5!}K`jEU1EZU^H0TMg92%Ayw)7QFzrb&DrKhYeM5qZyq)ge>7kWak!<69}nH zKD_W}ll{W^O|pa0m0_FF-&lc!KfXN~gghZzY}@3^Bu-9-f>cTcxPBvLBgi_QOb%Df zz)KEbVl8PkN?k9Y&23UwLf@dHfQ&R*#evA^yZWVmqAXVtHgQG$Ci?z}py}HWem2or z24c#iAXVAl<0V?^2Tna4i=33ine(SP9vqzya!{x|YP6FQfa-z(s{0`nef&Xy4#J}n zU>?~TJ1HSFn&AL=VNLJlEPVcf(1-x`!fvsLV2r)Hk7s2x&py}tHEBa#9=;XcFY!>aHpTxK}# zu`lb*bl3(Je*f}1dkeQ^=Hlx7J_ki1pTrG-U}8B8K#Scq`l))lvV7nR(UZeUn{3Uc z{zlA%1k%nN*bq|MIn@V-pB|;d`%F&8!D{Fu)mCv- z1xG0(2@%fmEI*c_FGm#8TWHz^(oK9p0(Z$Sa82V{fl`3MIIzOEBc!qYl;(aRO=^e& z&@OZq>5Mf73!wH?c8l}qL%$gwA}KVqbhtGk{ngx ziEHO%^p~kN3{;a4ExZlBv=hgYh?)^sCMO~!7W_k{A>W&_`ZmS-PKabJA3dD_R|9B^ z5F&9rB&rbK*A8)G9N*)pl1r}a`EMi5Op#hVdq|7<;q@TmGmQ@hBZZ!)W9>NZI)Koh zkaTmj^SA&W1%n2x2+S5B#DHrwdmD6ghWi@s(bO=hm`XIB={g#_&HEC^Q>7(zYc$#b z`q&nANT!Ej~%cC(zGgZJIucqma@xf20$A{VuWy7$uRjiXhd;U zhKm% N$YmTUmU2_d{{i@k4^998 literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_verification.cpython-313.pyc b/social_networks/tiktok/__pycache__/tiktok_verification.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fe482f11f39d994ca929c088a5e29444e76c10b GIT binary patch literal 17330 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 literal 0 HcmV?d00001 diff --git a/social_networks/tiktok/__pycache__/tiktok_workflow.cpython-310.pyc b/social_networks/tiktok/__pycache__/tiktok_workflow.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e71fac1f2f4a6c3a780223e79e88a06742b74ff GIT binary patch literal 8736 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 bool: + """ + Initialisiert den Browser mit den entsprechenden Einstellungen. + Diese Methode überschreibt die Methode der Basisklasse, um den erweiterten + Fingerprint-Schutz zu aktivieren. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Proxy-Konfiguration, falls aktiviert + proxy_config = None + if self.use_proxy: + proxy_config = self.proxy_rotator.get_proxy(self.proxy_type) + if not proxy_config: + logger.warning(f"Kein Proxy vom Typ '{self.proxy_type}' verfügbar, verwende direkten Zugriff") + + # Browser initialisieren + self.browser = PlaywrightManager( + headless=self.headless, + proxy=proxy_config, + browser_type="chromium", + screenshots_dir=self.screenshots_dir, + slowmo=self.slowmo + ) + + # Browser starten + self.browser.start() + + # Erweiterten Fingerprint-Schutz aktivieren, wenn gewünscht + if self.enhanced_stealth: + # Erstelle Extensions-Objekt + extensions = PlaywrightExtensions(self.browser) + + # Methoden anhängen + extensions.hook_into_playwright_manager() + + # Fingerprint-Schutz aktivieren mit angepasster Konfiguration + fingerprint_config = { + "noise_level": self.fingerprint_noise, + "canvas_noise": True, + "audio_noise": True, + "webgl_noise": True, + "hardware_concurrency": random.choice([4, 6, 8]), + "device_memory": random.choice([4, 8]), + "timezone_id": "Europe/Berlin" + } + + success = self.browser.enable_enhanced_fingerprint_protection(fingerprint_config) + if success: + logger.info("Erweiterter Fingerprint-Schutz erfolgreich aktiviert") + else: + logger.warning("Erweiterter Fingerprint-Schutz konnte nicht aktiviert werden") + + logger.info("Browser erfolgreich initialisiert") + return True + + except Exception as e: + logger.error(f"Fehler bei der Browser-Initialisierung: {e}") + self.status["error"] = f"Browser-Initialisierungsfehler: {str(e)}" + return False + + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Registriert einen neuen TikTok-Account. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + logger.info(f"Starte TikTok-Account-Registrierung für '{full_name}' via {registration_method}") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Rotiere Fingerprint vor der Hauptaktivität, um Erkennung weiter zu erschweren + if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'): + self.browser.rotate_fingerprint() + logger.info("Browser-Fingerprint vor der Registrierung rotiert") + + # Delegiere die Hauptregistrierungslogik an die Registration-Klasse + result = self.registration.register_account(full_name, age, registration_method, phone_number, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"registration_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Account-Registrierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"registration_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Meldet sich bei einem bestehenden TikTok-Account an. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Anmeldung mit Status + """ + logger.info(f"Starte TikTok-Login für '{username_or_email}'") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Rotiere Fingerprint vor dem Login + if self.enhanced_stealth and hasattr(self.browser, 'rotate_fingerprint'): + self.browser.rotate_fingerprint() + logger.info("Browser-Fingerprint vor dem Login rotiert") + + # Delegiere die Hauptlogin-Logik an die Login-Klasse + result = self.login.login_account(username_or_email, password, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"login_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Login: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"login_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Verifiziert einen TikTok-Account mit einem Bestätigungscode. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + logger.info(f"Starte TikTok-Account-Verifizierung mit Code: {verification_code}") + + try: + # Initialisiere Browser, falls noch nicht geschehen + if not self.browser or not hasattr(self.browser, 'page'): + if not self._initialize_browser(): + return {"success": False, "error": "Browser konnte nicht initialisiert werden"} + + # Delegiere die Hauptverifizierungslogik an die Verification-Klasse + result = self.verification.verify_account(verification_code, **kwargs) + + # Nehme Abschlussfoto auf + self._take_screenshot(f"verification_finished_{int(time.time())}") + + # Aktualisiere Status + self.status.update(result) + + return result + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Account-Verifizierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Fehler-Screenshot + self._take_screenshot(f"verification_error_{int(time.time())}") + + # Aktualisiere Status + self.status.update({ + "success": False, + "error": error_msg, + "stage": "error" + }) + + return self.status + finally: + # Browser schließen + self._close_browser() + + def get_fingerprint_status(self) -> Dict[str, Any]: + """ + Gibt den aktuellen Status des Fingerprint-Schutzes zurück. + + Returns: + Dict[str, Any]: Status des Fingerprint-Schutzes + """ + if not self.enhanced_stealth or not hasattr(self.browser, 'get_fingerprint_status'): + return { + "active": False, + "message": "Erweiterter Fingerprint-Schutz ist nicht aktiviert" + } + + return self.browser.get_fingerprint_status() \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_login.py b/social_networks/tiktok/tiktok_login.py new file mode 100644 index 0000000..0d80e5e --- /dev/null +++ b/social_networks/tiktok/tiktok_login.py @@ -0,0 +1,582 @@ +""" +TikTok-Login - Klasse für die Anmeldefunktionalität bei TikTok +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple + +from .tiktok_selectors import TikTokSelectors +from .tiktok_workflow import TikTokWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_login") + +class TikTokLogin: + """ + Klasse für die Anmeldung bei TikTok-Konten. + Enthält alle Methoden für den Login-Prozess. + """ + + def __init__(self, automation): + """ + Initialisiert die TikTok-Login-Funktionalität. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + self.workflow = TikTokWorkflow.get_login_workflow() + + logger.debug("TikTok-Login initialisiert") + + def login_account(self, username_or_email: str, password: str, **kwargs) -> Dict[str, Any]: + """ + Führt den Login-Prozess für ein TikTok-Konto durch. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis des Logins mit Status + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere die Eingaben + if not self._validate_login_inputs(username_or_email, password): + return { + "success": False, + "error": "Ungültige Login-Eingaben", + "stage": "input_validation" + } + + # Account-Daten für die Anmeldung + account_data = { + "username": username_or_email, + "password": password, + "handle_2fa": kwargs.get("handle_2fa", False), + "two_factor_code": kwargs.get("two_factor_code"), + "skip_save_login": kwargs.get("skip_save_login", True) + } + + logger.info(f"Starte TikTok-Login für {username_or_email}") + + try: + # 1. Zur Login-Seite navigieren + if not self._navigate_to_login_page(): + return { + "success": False, + "error": "Konnte nicht zur Login-Seite navigieren", + "stage": "navigation" + } + + # 2. Cookie-Banner behandeln + self._handle_cookie_banner() + + # 3. Login-Formular ausfüllen + if not self._fill_login_form(account_data): + return { + "success": False, + "error": "Fehler beim Ausfüllen des Login-Formulars", + "stage": "login_form" + } + + # 4. Auf 2FA prüfen und behandeln, falls nötig + needs_2fa, two_fa_error = self._check_needs_two_factor_auth() + + if needs_2fa: + if not account_data["handle_2fa"]: + return { + "success": False, + "error": "Zwei-Faktor-Authentifizierung erforderlich, aber nicht aktiviert", + "stage": "two_factor_required" + } + + # 2FA behandeln + if not self._handle_two_factor_auth(account_data["two_factor_code"]): + return { + "success": False, + "error": "Fehler bei der Zwei-Faktor-Authentifizierung", + "stage": "two_factor_auth" + } + + # 5. Benachrichtigungserlaubnis-Dialog behandeln + self._handle_notifications_prompt() + + # 6. Erfolgreichen Login überprüfen + if not self._check_login_success(): + error_message = self._get_login_error() + return { + "success": False, + "error": f"Login fehlgeschlagen: {error_message or 'Unbekannter Fehler'}", + "stage": "login_check" + } + + # Login erfolgreich + logger.info(f"TikTok-Login für {username_or_email} erfolgreich") + + return { + "success": True, + "stage": "completed", + "username": username_or_email + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler beim TikTok-Login: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception" + } + + def _validate_login_inputs(self, username_or_email: str, password: str) -> bool: + """ + Validiert die Eingaben für den Login. + + Args: + username_or_email: Benutzername oder E-Mail-Adresse + password: Passwort + + Returns: + bool: True wenn alle Eingaben gültig sind, False sonst + """ + if not username_or_email or len(username_or_email) < 3: + logger.error("Ungültiger Benutzername oder E-Mail") + return False + + if not password or len(password) < 6: + logger.error("Ungültiges Passwort") + return False + + return True + + def _navigate_to_login_page(self) -> bool: + """ + Navigiert zur TikTok-Login-Seite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Zur Login-Seite navigieren + self.browser.navigate_to(TikTokSelectors.LOGIN_URL) + + # Warten, bis die Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Screenshot erstellen + self.automation._take_screenshot("login_page") + + # Prüfen, ob Login-Dialog sichtbar ist + if not self.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=5000): + logger.warning("Login-Dialog nicht sichtbar, versuche Login-Button zu klicken") + + # Versuche, den Login-Button zu klicken, um das Login-Modal zu öffnen + login_buttons = [ + TikTokSelectors.LOGIN_BUTTON_TOP, + TikTokSelectors.LOGIN_BUTTON_SIDEBAR + ] + + button_clicked = False + for button in login_buttons: + if self.browser.is_element_visible(button, timeout=2000): + self.browser.click_element(button) + button_clicked = True + break + + if not button_clicked: + logger.warning("Keine Login-Buttons gefunden") + return False + + # Warten, bis der Login-Dialog erscheint + self.automation.human_behavior.wait_between_actions("decision", 1.5) + + # Erneut prüfen, ob der Login-Dialog sichtbar ist + if not self.browser.is_element_visible(TikTokSelectors.LOGIN_DIALOG, timeout=5000): + logger.warning("Login-Dialog nach dem Klicken auf den Login-Button nicht sichtbar") + return False + + logger.info("Erfolgreich zur Login-Seite navigiert") + return True + + except Exception as e: + logger.error(f"Fehler beim Navigieren zur Login-Seite: {e}") + return False + + def _handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(TikTokSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + TikTokSelectors.get_button_texts("reject_cookies"), + TikTokSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(TikTokSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + def _fill_login_form(self, account_data: Dict[str, Any]) -> bool: + """ + Füllt das Login-Formular aus und sendet es ab. + + Args: + account_data: Account-Daten für den Login + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # E-Mail/Telefon-Login-Option auswählen + email_phone_option = self.automation.ui_helper.click_button_fuzzy( + ["Telefon-Nr./E-Mail/Anmeldename nutzen", "Use phone / email / username"], + TikTokSelectors.LOGIN_EMAIL_PHONE_OPTION + ) + + if not email_phone_option: + logger.warning("Konnte die E-Mail/Telefon-Option nicht auswählen, versuche direkt das Formular auszufüllen") + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # E-Mail/Benutzername eingeben + username_success = self.automation.ui_helper.fill_field_fuzzy( + TikTokSelectors.get_field_labels("email_username"), + account_data["username"], + TikTokSelectors.LOGIN_EMAIL_FIELD + ) + + if not username_success: + logger.error("Konnte Benutzername-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Passwort eingeben + password_success = self.automation.ui_helper.fill_field_fuzzy( + TikTokSelectors.get_field_labels("password"), + account_data["password"], + TikTokSelectors.LOGIN_PASSWORD_FIELD + ) + + if not password_success: + logger.error("Konnte Passwort-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot vorm Absenden + self.automation._take_screenshot("login_form_filled") + + # Formular absenden + submit_success = self.automation.ui_helper.click_button_fuzzy( + ["Anmelden", "Log in", "Login"], + TikTokSelectors.LOGIN_SUBMIT_BUTTON + ) + + if not submit_success: + logger.error("Konnte Login-Formular nicht absenden") + return False + + # Nach dem Absenden warten + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Überprüfen, ob es eine Fehlermeldung gab + error_message = self._get_login_error() + if error_message: + logger.error(f"Login-Fehler erkannt: {error_message}") + return False + + logger.info("Login-Formular erfolgreich ausgefüllt und abgesendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen des Login-Formulars: {e}") + return False + + def _get_login_error(self) -> Optional[str]: + """ + Überprüft, ob eine Login-Fehlermeldung angezeigt wird. + + Returns: + Optional[str]: Fehlermeldung oder None, wenn keine gefunden wurde + """ + try: + # Auf Fehlermeldungen prüfen + error_selectors = [ + TikTokSelectors.ERROR_MESSAGE, + "p[class*='error']", + "div[role='alert']", + "div[class*='error']", + "div[class*='Error']" + ] + + for selector in error_selectors: + error_element = self.browser.wait_for_selector(selector, timeout=2000) + if error_element: + error_text = error_element.text_content() + if error_text and len(error_text.strip()) > 0: + return error_text.strip() + + # Wenn keine spezifische Fehlermeldung gefunden wurde, nach bekannten Fehlermustern suchen + error_texts = [ + "Falsches Passwort", + "Benutzername nicht gefunden", + "incorrect password", + "username you entered doesn't belong", + "please wait a few minutes", + "try again later", + "Bitte warte einige Minuten", + "versuche es später noch einmal" + ] + + page_content = self.browser.page.content() + for error_text in error_texts: + if error_text.lower() in page_content.lower(): + return f"Erkannter Fehler: {error_text}" + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Login-Fehler: {e}") + return None + + def _check_needs_two_factor_auth(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob eine Zwei-Faktor-Authentifizierung erforderlich ist. + + Returns: + Tuple[bool, Optional[str]]: (2FA erforderlich, Fehlermeldung falls vorhanden) + """ + try: + # Nach 2FA-Indikatoren suchen + two_fa_selectors = [ + "input[name='verificationCode']", + "input[placeholder*='code']", + "input[placeholder*='Code']", + "div[class*='verification-code']", + "div[class*='two-factor']" + ] + + for selector in two_fa_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Zwei-Faktor-Authentifizierung erforderlich") + return True, None + + # Texte, die auf 2FA hinweisen + two_fa_indicators = [ + "Verifizierungscode", + "Verification code", + "Sicherheitscode", + "Security code", + "zwei-faktor", + "two-factor", + "2FA" + ] + + # Seiteninhalt durchsuchen + page_content = self.browser.page.content().lower() + + for indicator in two_fa_indicators: + if indicator.lower() in page_content: + logger.info(f"Zwei-Faktor-Authentifizierung erkannt durch Text: {indicator}") + return True, None + + return False, None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf 2FA: {e}") + return False, f"Fehler bei der 2FA-Erkennung: {str(e)}" + + def _handle_two_factor_auth(self, two_factor_code: Optional[str] = None) -> bool: + """ + Behandelt die Zwei-Faktor-Authentifizierung. + + Args: + two_factor_code: Optional vorhandener 2FA-Code + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Screenshot erstellen + self.automation._take_screenshot("two_factor_auth") + + # 2FA-Eingabefeld finden + two_fa_selectors = [ + "input[name='verificationCode']", + "input[placeholder*='code']", + "input[placeholder*='Code']" + ] + + two_fa_field = None + for selector in two_fa_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + two_fa_field = selector + break + + if not two_fa_field: + logger.error("Konnte 2FA-Eingabefeld nicht finden") + return False + + # Wenn kein Code bereitgestellt wurde, Benutzer auffordern + if not two_factor_code: + logger.warning("Kein 2FA-Code bereitgestellt, kann nicht fortfahren") + return False + + # 2FA-Code eingeben + code_success = self.browser.fill_form_field(two_fa_field, two_factor_code) + + if not code_success: + logger.error("Konnte 2FA-Code nicht eingeben") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Bestätigen-Button finden und klicken + confirm_button_selectors = [ + "button[type='submit']", + "//button[contains(text(), 'Bestätigen')]", + "//button[contains(text(), 'Confirm')]", + "//button[contains(text(), 'Verify')]" + ] + + confirm_clicked = False + for selector in confirm_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + confirm_clicked = True + break + + if not confirm_clicked: + # Alternative: Mit Tastendruck bestätigen + self.browser.page.keyboard.press("Enter") + logger.info("Enter-Taste gedrückt, um 2FA zu bestätigen") + + # Warten nach der Bestätigung + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Überprüfen, ob 2FA erfolgreich war + still_on_2fa = self._check_needs_two_factor_auth()[0] + + if still_on_2fa: + # Prüfen, ob Fehlermeldung angezeigt wird + error_message = self._get_login_error() + if error_message: + logger.error(f"2FA-Fehler: {error_message}") + else: + logger.error("2FA fehlgeschlagen, immer noch auf 2FA-Seite") + return False + + logger.info("Zwei-Faktor-Authentifizierung erfolgreich") + return True + + except Exception as e: + logger.error(f"Fehler bei der Zwei-Faktor-Authentifizierung: {e}") + return False + + def _handle_notifications_prompt(self) -> bool: + """ + Behandelt den Benachrichtigungen-Dialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Nach "Nicht jetzt"-Button suchen + not_now_selectors = [ + "//button[contains(text(), 'Nicht jetzt')]", + "//button[contains(text(), 'Not now')]", + "//button[contains(text(), 'Skip')]", + "//button[contains(text(), 'Später')]", + "//button[contains(text(), 'Later')]" + ] + + for selector in not_now_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + if self.browser.click_element(selector): + logger.info("Benachrichtigungen-Dialog übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn kein Button gefunden wurde, ist der Dialog wahrscheinlich nicht vorhanden + logger.debug("Kein Benachrichtigungen-Dialog erkannt") + return True + + except Exception as e: + logger.warning(f"Fehler beim Behandeln des Benachrichtigungen-Dialogs: {e}") + # Dies ist nicht kritisch, daher geben wir trotzdem True zurück + return True + + def _check_login_success(self) -> bool: + """ + Überprüft, ob der Login erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Warten nach dem Login + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Screenshot erstellen + self.automation._take_screenshot("login_final") + + # Erfolg anhand verschiedener Indikatoren prüfen + success_indicators = TikTokSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=2000): + logger.info(f"Login-Erfolgsindikator gefunden: {indicator}") + return True + + # Alternativ prüfen, ob wir auf der TikTok-Startseite sind + current_url = self.browser.page.url + if "tiktok.com" in current_url and "/login" not in current_url: + logger.info(f"Login-Erfolg basierend auf URL: {current_url}") + return True + + # Prüfen, ob immer noch auf der Login-Seite + if "/login" in current_url or self.browser.is_element_visible(TikTokSelectors.LOGIN_EMAIL_FIELD, timeout=1000): + logger.warning("Immer noch auf der Login-Seite, Login fehlgeschlagen") + return False + + logger.warning("Keine Login-Erfolgsindikatoren gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Login-Erfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_registration.py b/social_networks/tiktok/tiktok_registration.py new file mode 100644 index 0000000..4468424 --- /dev/null +++ b/social_networks/tiktok/tiktok_registration.py @@ -0,0 +1,987 @@ +# social_networks/tiktok/tiktok_registration.py + +""" +TikTok-Registrierung - Klasse für die Kontoerstellung bei TikTok +""" + +import logging +import time +import random +import re +from typing import Dict, List, Any, Optional, Tuple + +from .tiktok_selectors import TikTokSelectors +from .tiktok_workflow import TikTokWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_registration") + +class TikTokRegistration: + """ + Klasse für die Registrierung von TikTok-Konten. + Enthält alle Methoden zur Kontoerstellung. + """ + + def __init__(self, automation): + """ + Initialisiert die TikTok-Registrierung. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + self.workflow = TikTokWorkflow.get_registration_workflow() + + logger.debug("TikTok-Registrierung initialisiert") + + def register_account(self, full_name: str, age: int, registration_method: str = "email", + phone_number: str = None, **kwargs) -> Dict[str, Any]: + """ + Führt den vollständigen Registrierungsprozess für einen TikTok-Account durch. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Registrierung mit Status und Account-Daten + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere die Eingaben + if not self._validate_registration_inputs(full_name, age, registration_method, phone_number): + return { + "success": False, + "error": "Ungültige Eingabeparameter", + "stage": "input_validation" + } + + # Account-Daten generieren + account_data = self._generate_account_data(full_name, age, registration_method, phone_number, **kwargs) + + # Starte den Registrierungsprozess + logger.info(f"Starte TikTok-Registrierung für {account_data['username']} via {registration_method}") + + try: + # 1. Zur Startseite navigieren + if not self._navigate_to_homepage(): + return { + "success": False, + "error": "Konnte nicht zur TikTok-Startseite navigieren", + "stage": "navigation", + "account_data": account_data + } + + # 2. Cookie-Banner behandeln + self._handle_cookie_banner() + + # 3. Anmelden-Button klicken + if not self._click_login_button(): + return { + "success": False, + "error": "Konnte nicht auf Anmelden-Button klicken", + "stage": "login_button", + "account_data": account_data + } + + # 4. Registrieren-Link klicken + if not self._click_register_link(): + return { + "success": False, + "error": "Konnte nicht auf Registrieren-Link klicken", + "stage": "register_link", + "account_data": account_data + } + + # 5. Telefon/E-Mail-Option auswählen + if not self._click_phone_email_option(): + return { + "success": False, + "error": "Konnte nicht auf Telefon/E-Mail-Option klicken", + "stage": "phone_email_option", + "account_data": account_data + } + + # 6. E-Mail oder Telefon als Registrierungsmethode wählen + if not self._select_registration_method(registration_method): + return { + "success": False, + "error": f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen", + "stage": "registration_method", + "account_data": account_data + } + + # 7. Geburtsdatum eingeben + if not self._enter_birthday(account_data["birthday"]): + return { + "success": False, + "error": "Fehler beim Eingeben des Geburtsdatums", + "stage": "birthday", + "account_data": account_data + } + + # 8. Registrierungsformular ausfüllen + if not self._fill_registration_form(account_data, registration_method): + return { + "success": False, + "error": "Fehler beim Ausfüllen des Registrierungsformulars", + "stage": "registration_form", + "account_data": account_data + } + + # 9. Bestätigungscode abrufen und eingeben + if not self._handle_verification(account_data, registration_method): + return { + "success": False, + "error": "Fehler bei der Verifizierung", + "stage": "verification", + "account_data": account_data + } + + # 10. Benutzernamen erstellen + if not self._create_username(account_data): + return { + "success": False, + "error": "Fehler beim Erstellen des Benutzernamens", + "stage": "username", + "account_data": account_data + } + + # 11. Erfolgreiche Registrierung überprüfen + if not self._check_registration_success(): + return { + "success": False, + "error": "Registrierung fehlgeschlagen oder konnte nicht verifiziert werden", + "stage": "final_check", + "account_data": account_data + } + + # Registrierung erfolgreich abgeschlossen + logger.info(f"TikTok-Account {account_data['username']} erfolgreich erstellt") + + return { + "success": True, + "stage": "completed", + "account_data": account_data + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der TikTok-Registrierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception", + "account_data": account_data + } + + def _validate_registration_inputs(self, full_name: str, age: int, + registration_method: str, phone_number: str) -> bool: + """ + Validiert die Eingaben für die Registrierung. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + + Returns: + bool: True wenn alle Eingaben gültig sind, False sonst + """ + # Vollständiger Name prüfen + if not full_name or len(full_name) < 3: + logger.error("Ungültiger vollständiger Name") + return False + + # Alter prüfen + if age < 13: + logger.error("Benutzer muss mindestens 13 Jahre alt sein") + return False + + # Registrierungsmethode prüfen + if registration_method not in ["email", "phone"]: + logger.error(f"Ungültige Registrierungsmethode: {registration_method}") + return False + + # Telefonnummer prüfen, falls erforderlich + if registration_method == "phone" and not phone_number: + logger.error("Telefonnummer erforderlich für Registrierung via Telefon") + return False + + return True + + def _generate_account_data(self, full_name: str, age: int, registration_method: str, + phone_number: str, **kwargs) -> Dict[str, Any]: + """ + Generiert Account-Daten für die Registrierung. + + Args: + full_name: Vollständiger Name für den Account + age: Alter des Benutzers + registration_method: "email" oder "phone" + phone_number: Telefonnummer (nur bei registration_method="phone") + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Generierte Account-Daten + """ + # Benutzername generieren + username = kwargs.get("username") + if not username: + username = self.automation.username_generator.generate_username("tiktok", full_name) + + # Passwort generieren + password = kwargs.get("password") + if not password: + password = self.automation.password_generator.generate_password("tiktok") + + # E-Mail generieren (falls nötig) + email = None + if registration_method == "email": + email_prefix = username.lower().replace(".", "").replace("_", "") + email = f"{email_prefix}@{self.automation.email_domain}" + + # Geburtsdatum generieren + birthday = self.automation.birthday_generator.generate_birthday_components("tiktok", age) + + # Account-Daten zusammenstellen + account_data = { + "username": username, + "password": password, + "full_name": full_name, + "email": email, + "phone": phone_number, + "birthday": birthday, + "age": age, + "registration_method": registration_method + } + + logger.debug(f"Account-Daten generiert: {account_data['username']}") + + return account_data + + def _navigate_to_homepage(self) -> bool: + """ + Navigiert zur TikTok-Startseite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Zur Startseite navigieren + self.browser.navigate_to(TikTokSelectors.BASE_URL) + + # Warten, bis die Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Screenshot erstellen + self.automation._take_screenshot("tiktok_homepage") + + # Prüfen, ob die Seite korrekt geladen wurde + if not self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON, timeout=5000): + logger.warning("TikTok-Startseite nicht korrekt geladen") + return False + + logger.info("Erfolgreich zur TikTok-Startseite navigiert") + return True + + except Exception as e: + logger.error(f"Fehler beim Navigieren zur TikTok-Startseite: {e}") + return False + + def _handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + # Cookie-Dialog-Erkennung + if self.browser.is_element_visible(TikTokSelectors.COOKIE_DIALOG, timeout=2000): + logger.info("Cookie-Banner erkannt") + + # Ablehnen-Button suchen und klicken + reject_success = self.automation.ui_helper.click_button_fuzzy( + TikTokSelectors.get_button_texts("reject_cookies"), + TikTokSelectors.COOKIE_REJECT_BUTTON + ) + + if reject_success: + logger.info("Cookie-Banner erfolgreich abgelehnt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.warning("Konnte Cookie-Banner nicht ablehnen, versuche zu akzeptieren") + + # Akzeptieren-Button als Fallback + accept_success = self.browser.click_element(TikTokSelectors.COOKIE_ACCEPT_BUTTON) + + if accept_success: + logger.info("Cookie-Banner erfolgreich akzeptiert") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + else: + logger.error("Konnte Cookie-Banner weder ablehnen noch akzeptieren") + return False + else: + logger.debug("Kein Cookie-Banner erkannt") + return True + + def _click_login_button(self) -> bool: + """ + Klickt auf den Anmelden-Button auf der Startseite. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Versuche zuerst den Hauptbutton + if self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON, timeout=2000): + result = self.browser.click_element(TikTokSelectors.LOGIN_BUTTON) + if result: + logger.info("Anmelden-Button erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche alternativ den Button in der oberen rechten Ecke + if self.browser.is_element_visible(TikTokSelectors.LOGIN_BUTTON_TOP_RIGHT, timeout=2000): + result = self.browser.click_element(TikTokSelectors.LOGIN_BUTTON_TOP_RIGHT) + if result: + logger.info("Anmelden-Button (oben rechts) erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Anmelden", "Log in", "Login"], + TikTokSelectors.LOGIN_BUTTON_FALLBACK + ) + + if result: + logger.info("Anmelden-Button über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + logger.error("Konnte keinen Anmelden-Button finden") + return False + + except Exception as e: + logger.error(f"Fehler beim Klicken auf den Anmelden-Button: {e}") + return False + + def _click_register_link(self) -> bool: + """ + Klickt auf den Registrieren-Link im Login-Dialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis der Login-Dialog angezeigt wird + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Prüfen, ob wir bereits im Registrierungsdialog sind + if self.browser.is_element_visible(TikTokSelectors.REGISTER_DIALOG_TITLE, timeout=2000): + logger.info("Bereits im Registrierungsdialog") + return True + + # Versuche, den Registrieren-Link zu finden und zu klicken + if self.browser.is_element_visible(TikTokSelectors.REGISTER_LINK, timeout=2000): + result = self.browser.click_element(TikTokSelectors.REGISTER_LINK) + if result: + logger.info("Registrieren-Link erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Registrieren", "Sign up", "Konto erstellen", "Register"], + TikTokSelectors.REGISTER_LINK_FALLBACK + ) + + if result: + logger.info("Registrieren-Link über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Prüfe, ob der Text "Du hast noch kein Konto? Registrieren" vorhanden ist + register_link_text = "Du hast noch kein Konto? Registrieren" + elements = self.browser.page.query_selector_all("*") + for element in elements: + if register_link_text in element.inner_text(): + # Finde das "Registrieren"-Wort und klicke darauf + matches = re.search(r"(.*?)(Registrieren)$", element.inner_text()) + if matches: + # Versuche, nur auf das Wort "Registrieren" zu klicken + element.click() + logger.info("Auf 'Registrieren' Text geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + logger.error("Konnte keinen Registrieren-Link finden") + return False + + except Exception as e: + logger.error(f"Fehler beim Klicken auf den Registrieren-Link: {e}") + return False + + def _click_phone_email_option(self) -> bool: + """ + Klickt auf die Telefon/E-Mail-Option im Registrierungsdialog. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis der Registrierungsdialog angezeigt wird + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Prüfen, ob wir bereits die Optionen für Telefon/E-Mail sehen + if self.browser.is_element_visible(TikTokSelectors.EMAIL_FIELD, timeout=2000) or \ + self.browser.is_element_visible(TikTokSelectors.PHONE_FIELD, timeout=2000): + logger.info("Bereits auf der Telefon/E-Mail-Registrierungsseite") + return True + + # Versuche, die Telefon/E-Mail-Option zu finden und zu klicken + if self.browser.is_element_visible(TikTokSelectors.PHONE_EMAIL_OPTION, timeout=2000): + result = self.browser.click_element(TikTokSelectors.PHONE_EMAIL_OPTION) + if result: + logger.info("Telefon/E-Mail-Option erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Telefonnummer oder E-Mail-Adresse nutzen", "Use phone or email", "Phone or email"], + TikTokSelectors.PHONE_EMAIL_OPTION_FALLBACK + ) + + if result: + logger.info("Telefon/E-Mail-Option über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + logger.error("Konnte keine Telefon/E-Mail-Option finden") + return False + + except Exception as e: + logger.error(f"Fehler beim Klicken auf die Telefon/E-Mail-Option: {e}") + return False + + def _select_registration_method(self, registration_method: str) -> bool: + """ + Wählt die Registrierungsmethode (E-Mail oder Telefon). + + Args: + registration_method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Registrierungsmethoden-Seite geladen ist + self.automation.human_behavior.random_delay(1.0, 2.0) + + if registration_method == "email": + # Wenn bereits das E-Mail-Feld sichtbar ist, sind wir schon auf der richtigen Seite + if self.browser.is_element_visible(TikTokSelectors.EMAIL_FIELD, timeout=1000): + logger.info("Bereits auf der E-Mail-Registrierungsseite") + return True + + # Suche nach dem "Mit E-Mail-Adresse registrieren" Link + if self.browser.is_element_visible(TikTokSelectors.EMAIL_OPTION, timeout=2000): + result = self.browser.click_element(TikTokSelectors.EMAIL_OPTION) + if result: + logger.info("E-Mail-Registrierungsmethode erfolgreich ausgewählt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Mit E-Mail-Adresse registrieren", "Register with email", "E-Mail-Adresse"], + TikTokSelectors.EMAIL_OPTION_FALLBACK + ) + + if result: + logger.info("E-Mail-Option über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + elif registration_method == "phone": + # Wenn bereits das Telefon-Feld sichtbar ist, sind wir schon auf der richtigen Seite + if self.browser.is_element_visible(TikTokSelectors.PHONE_FIELD, timeout=1000): + logger.info("Bereits auf der Telefon-Registrierungsseite") + return True + + # Suche nach dem "Mit Telefonnummer registrieren" Link + if self.browser.is_element_visible(TikTokSelectors.PHONE_OPTION, timeout=2000): + result = self.browser.click_element(TikTokSelectors.PHONE_OPTION) + if result: + logger.info("Telefon-Registrierungsmethode erfolgreich ausgewählt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + # Versuche es mit Fuzzy-Button-Matching + result = self.automation.ui_helper.click_button_fuzzy( + ["Mit Telefonnummer registrieren", "Register with phone", "Telefonnummer"], + TikTokSelectors.PHONE_OPTION_FALLBACK + ) + + if result: + logger.info("Telefon-Option über Fuzzy-Matching erfolgreich geklickt") + self.automation.human_behavior.random_delay(0.5, 1.5) + return True + + logger.error(f"Konnte Registrierungsmethode '{registration_method}' nicht auswählen") + return False + + except Exception as e: + logger.error(f"Fehler beim Auswählen der Registrierungsmethode: {e}") + return False + + def _enter_birthday(self, birthday: Dict[str, int]) -> bool: + """ + Gibt das Geburtsdatum ein. + + Args: + birthday: Dictionary mit 'year', 'month', 'day' Schlüsseln + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Geburtstagsauswahl angezeigt wird + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Monat auswählen + month_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_MONTH_DROPDOWN, timeout=5000) + if not month_dropdown: + logger.error("Monat-Dropdown nicht gefunden") + return False + + month_dropdown.click() + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Monat-Option auswählen + month_option = self.browser.wait_for_selector( + TikTokSelectors.get_month_option_selector(birthday["month"]), + timeout=3000 + ) + if month_option: + month_option.click() + logger.info(f"Monat {birthday['month']} ausgewählt") + else: + # Fallback: Monat über Select-Funktion auswählen + month_success = self.browser.select_option( + TikTokSelectors.BIRTHDAY_MONTH_DROPDOWN, + str(birthday["month"]) + ) + if not month_success: + logger.error(f"Konnte Monat {birthday['month']} nicht auswählen") + return False + + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Tag auswählen + day_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_DAY_DROPDOWN, timeout=3000) + if not day_dropdown: + logger.error("Tag-Dropdown nicht gefunden") + return False + + day_dropdown.click() + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Tag-Option auswählen + day_option = self.browser.wait_for_selector( + TikTokSelectors.get_day_option_selector(birthday["day"]), + timeout=3000 + ) + if day_option: + day_option.click() + logger.info(f"Tag {birthday['day']} ausgewählt") + else: + # Fallback: Tag über Select-Funktion auswählen + day_success = self.browser.select_option( + TikTokSelectors.BIRTHDAY_DAY_DROPDOWN, + str(birthday["day"]) + ) + if not day_success: + logger.error(f"Konnte Tag {birthday['day']} nicht auswählen") + return False + + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Jahr auswählen + year_dropdown = self.browser.wait_for_selector(TikTokSelectors.BIRTHDAY_YEAR_DROPDOWN, timeout=3000) + if not year_dropdown: + logger.error("Jahr-Dropdown nicht gefunden") + return False + + year_dropdown.click() + self.automation.human_behavior.random_delay(0.3, 0.8) + + # Jahr-Option auswählen + year_option = self.browser.wait_for_selector( + TikTokSelectors.get_year_option_selector(birthday["year"]), + timeout=3000 + ) + if year_option: + year_option.click() + logger.info(f"Jahr {birthday['year']} ausgewählt") + else: + # Fallback: Jahr über Select-Funktion auswählen + year_success = self.browser.select_option( + TikTokSelectors.BIRTHDAY_YEAR_DROPDOWN, + str(birthday["year"]) + ) + if not year_success: + logger.error(f"Konnte Jahr {birthday['year']} nicht auswählen") + return False + + logger.info(f"Geburtsdatum {birthday['month']}/{birthday['day']}/{birthday['year']} erfolgreich eingegeben") + return True + + except Exception as e: + logger.error(f"Fehler beim Eingeben des Geburtsdatums: {e}") + return False + + def _fill_registration_form(self, account_data: Dict[str, Any], registration_method: str) -> bool: + """ + Füllt das Registrierungsformular aus. + + Args: + account_data: Account-Daten für die Registrierung + registration_method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Je nach Registrierungsmethode das entsprechende Feld ausfüllen + if registration_method == "email": + # E-Mail-Feld ausfüllen + email_success = self.automation.ui_helper.fill_field_fuzzy( + ["E-Mail-Adresse", "Email", "E-Mail"], + account_data["email"], + TikTokSelectors.EMAIL_FIELD + ) + + if not email_success: + logger.error("Konnte E-Mail-Feld nicht ausfüllen") + return False + + logger.info(f"E-Mail-Feld ausgefüllt: {account_data['email']}") + + elif registration_method == "phone": + # Telefonnummer-Feld ausfüllen (ohne Ländervorwahl) + phone_number = account_data["phone"] + if phone_number.startswith("+"): + # Entferne Ländervorwahl, wenn vorhanden + parts = phone_number.split(" ", 1) + if len(parts) > 1: + phone_number = parts[1] + + phone_success = self.automation.ui_helper.fill_field_fuzzy( + ["Telefonnummer", "Phone number", "Phone"], + phone_number, + TikTokSelectors.PHONE_FIELD + ) + + if not phone_success: + logger.error("Konnte Telefonnummer-Feld nicht ausfüllen") + return False + + logger.info(f"Telefonnummer-Feld ausgefüllt: {phone_number}") + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Bei E-Mail-Registrierung auch das Passwort-Feld ausfüllen + if registration_method == "email": + password_success = self.automation.ui_helper.fill_field_fuzzy( + ["Passwort", "Password"], + account_data["password"], + TikTokSelectors.PASSWORD_FIELD + ) + + if not password_success: + logger.error("Konnte Passwort-Feld nicht ausfüllen") + return False + + logger.info("Passwort-Feld ausgefüllt") + + self.automation.human_behavior.random_delay(0.5, 1.5) + + # Code senden Button klicken + send_code_success = self.automation.ui_helper.click_button_fuzzy( + ["Code senden", "Send code", "Send verification code"], + TikTokSelectors.SEND_CODE_BUTTON + ) + + if not send_code_success: + logger.error("Konnte 'Code senden'-Button nicht klicken") + return False + + logger.info("'Code senden'-Button erfolgreich geklickt") + return True + + except Exception as e: + logger.error(f"Fehler beim Ausfüllen des Registrierungsformulars: {e}") + return False + + def _handle_verification(self, account_data: Dict[str, Any], registration_method: str) -> bool: + """ + Behandelt den Verifizierungsprozess (E-Mail/SMS). + + Args: + account_data: Account-Daten mit E-Mail/Telefon + registration_method: "email" oder "phone" + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis der Bestätigungscode gesendet wurde + self.automation.human_behavior.wait_for_page_load() + self.automation.human_behavior.random_delay(2.0, 4.0) + + # Verifizierungscode je nach Methode abrufen + if registration_method == "email": + # Verifizierungscode von E-Mail abrufen + verification_code = self._get_email_confirmation_code(account_data["email"]) + else: + # Verifizierungscode von SMS abrufen + verification_code = self._get_sms_confirmation_code(account_data["phone"]) + + if not verification_code: + logger.error("Konnte keinen Verifizierungscode abrufen") + return False + + logger.info(f"Verifizierungscode erhalten: {verification_code}") + + # Verifizierungscode-Feld ausfüllen + code_success = self.automation.ui_helper.fill_field_fuzzy( + ["Gib den sechsstelligen Code ein", "Enter verification code", "Verification code"], + verification_code, + TikTokSelectors.VERIFICATION_CODE_FIELD + ) + + if not code_success: + logger.error("Konnte Verifizierungscode-Feld nicht ausfüllen") + return False + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Weiter-Button klicken + continue_success = self.automation.ui_helper.click_button_fuzzy( + ["Weiter", "Continue", "Next", "Submit"], + TikTokSelectors.CONTINUE_BUTTON + ) + + if not continue_success: + logger.error("Konnte 'Weiter'-Button nicht klicken") + return False + + logger.info("Verifizierungscode eingegeben und 'Weiter' geklickt") + + # Warten nach der Verifizierung + self.automation.human_behavior.wait_for_page_load() + self.automation.human_behavior.random_delay(1.0, 2.0) + + return True + + except Exception as e: + logger.error(f"Fehler bei der Verifizierung: {e}") + return False + + def _get_email_confirmation_code(self, email: str) -> Optional[str]: + """ + Ruft den Bestätigungscode von einer E-Mail ab. + + Args: + email: E-Mail-Adresse, an die der Code gesendet wurde + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + try: + # Warte auf die E-Mail + verification_code = self.automation.email_handler.get_verification_code( + email_domain=self.automation.email_domain, + platform="tiktok", + timeout=120 # Warte bis zu 2 Minuten auf den Code + ) + + if verification_code: + return verification_code + + # Wenn kein Code gefunden wurde, prüfen, ob der Code vielleicht direkt angezeigt wird + verification_code = self._extract_code_from_page() + + if verification_code: + logger.info(f"Verifizierungscode direkt von der Seite extrahiert: {verification_code}") + return verification_code + + logger.warning(f"Konnte keinen Verifizierungscode für {email} finden") + return None + + except Exception as e: + logger.error(f"Fehler beim Abrufen des E-Mail-Bestätigungscodes: {e}") + return None + + def _get_sms_confirmation_code(self, phone: str) -> Optional[str]: + """ + Ruft den Bestätigungscode aus einer SMS ab. + Hier müsste ein SMS-Empfangs-Service eingebunden werden. + + Args: + phone: Telefonnummer, an die der Code gesendet wurde + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + # Diese Implementierung ist ein Platzhalter + # In einer echten Implementierung würde hier ein SMS-Empfangs-Service verwendet + logger.warning("SMS-Verifizierung ist noch nicht implementiert") + + # Versuche, den Code trotzdem zu extrahieren, falls er auf der Seite angezeigt wird + return self._extract_code_from_page() + + def _extract_code_from_page(self) -> Optional[str]: + """ + Versucht, einen Bestätigungscode direkt von der Seite zu extrahieren. + + Returns: + Optional[str]: Der extrahierte Code oder None, wenn nicht gefunden + """ + try: + # Gesamten Seiteninhalt abrufen + page_content = self.browser.page.content() + + # Mögliche Regex-Muster für Bestätigungscodes + patterns = [ + r"Dein Code ist (\d{6})", + r"Your code is (\d{6})", + r"Bestätigungscode: (\d{6})", + r"Confirmation code: (\d{6})", + r"(\d{6}) ist dein TikTok-Code", + r"(\d{6}) is your TikTok code" + ] + + for pattern in patterns: + match = re.search(pattern, page_content) + if match: + return match.group(1) + + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Codes von der Seite: {e}") + return None + + def _create_username(self, account_data: Dict[str, Any]) -> bool: + """ + Erstellt einen Benutzernamen. + + Args: + account_data: Account-Daten + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Warten, bis die Benutzernamen-Seite geladen ist + self.automation.human_behavior.wait_for_page_load() + + # Prüfen, ob wir auf der Benutzernamen-Seite sind + if not self.browser.is_element_visible(TikTokSelectors.USERNAME_FIELD, timeout=5000): + logger.warning("Benutzernamen-Feld nicht gefunden, möglicherweise ist dieser Schritt übersprungen worden") + + # Versuche, den "Überspringen"-Button zu klicken, falls vorhanden + skip_visible = self.browser.is_element_visible(TikTokSelectors.SKIP_USERNAME_BUTTON, timeout=2000) + if skip_visible: + self.browser.click_element(TikTokSelectors.SKIP_USERNAME_BUTTON) + logger.info("Benutzernamen-Schritt übersprungen") + return True + + # Möglicherweise wurde der Benutzername automatisch erstellt + logger.info("Benutzernamen-Schritt möglicherweise automatisch abgeschlossen") + return True + + # Benutzernamen eingeben + username_success = self.automation.ui_helper.fill_field_fuzzy( + ["Benutzername", "Username"], + account_data["username"], + TikTokSelectors.USERNAME_FIELD + ) + + if not username_success: + logger.error("Konnte Benutzernamen-Feld nicht ausfüllen") + return False + + logger.info(f"Benutzernamen-Feld ausgefüllt: {account_data['username']}") + + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Registrieren-Button klicken + register_success = self.automation.ui_helper.click_button_fuzzy( + ["Registrieren", "Register", "Sign up", "Submit"], + TikTokSelectors.REGISTER_BUTTON + ) + + if not register_success: + logger.error("Konnte 'Registrieren'-Button nicht klicken") + return False + + logger.info("'Registrieren'-Button erfolgreich geklickt") + + # Warten nach der Registrierung + self.automation.human_behavior.wait_for_page_load() + + return True + + except Exception as e: + logger.error(f"Fehler beim Erstellen des Benutzernamens: {e}") + return False + + def _check_registration_success(self) -> bool: + """ + Überprüft, ob die Registrierung erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Warten nach der Registrierung + self.automation.human_behavior.wait_for_page_load(multiplier=2.0) + + # Screenshot erstellen + self.automation._take_screenshot("registration_final") + + # Erfolg anhand verschiedener Indikatoren prüfen + success_indicators = TikTokSelectors.SUCCESS_INDICATORS + + for indicator in success_indicators: + if self.browser.is_element_visible(indicator, timeout=3000): + logger.info(f"Erfolgsindikator gefunden: {indicator}") + return True + + # Alternativ prüfen, ob wir auf der TikTok-Startseite sind + current_url = self.browser.page.url + if "tiktok.com" in current_url and "/signup" not in current_url and "/login" not in current_url: + logger.info(f"Erfolg basierend auf URL: {current_url}") + return True + + logger.warning("Keine Erfolgsindikatoren gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Registrierungserfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_selectors.py b/social_networks/tiktok/tiktok_selectors.py new file mode 100644 index 0000000..2606e8f --- /dev/null +++ b/social_networks/tiktok/tiktok_selectors.py @@ -0,0 +1,150 @@ +""" +TikTok-Selektoren - CSS-Selektoren und XPath-Ausdrücke für die TikTok-Automatisierung +Mit Text-Matching-Funktionen für robuste Element-Erkennung +""" + +from typing import List, Dict, Optional, Any + +class TikTokSelectors: + """ + Zentrale Sammlung aller Selektoren für die TikTok-Automatisierung. + Bei Änderungen der TikTok-Webseite müssen nur hier Anpassungen vorgenommen werden. + Enthält auch Fuzzy-Text-Matching-Daten für robustere Element-Erkennung. + """ + + # URL-Konstanten + BASE_URL = "https://www.tiktok.com" + SIGNUP_URL = "https://www.tiktok.com/signup" + LOGIN_URL = "https://www.tiktok.com/login" + + # Anmelden/Registrieren-Buttons Hauptseite + LOGIN_BUTTON_LEFT = "button#header-login-button" + LOGIN_BUTTON_RIGHT = "button#top-right-action-bar-login-button" + SIGNUP_LINK = "a[href*='/signup']" + + # Registrierungsdialog - Methoden + REGISTRATION_DIALOG = "div[role='dialog']" + PHONE_EMAIL_BUTTON = "div[data-e2e='channel-item']" + REGISTER_WITH_EMAIL = "a[href*='/signup/phone-or-email/email']" + REGISTER_WITH_PHONE = "a[href*='/signup/phone-or-email/phone']" + + # Geburtsdatum-Selektoren + BIRTHDAY_MONTH_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Monat')" + BIRTHDAY_DAY_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Tag')" + BIRTHDAY_YEAR_SELECT = "div.css-1fi2hzv-DivSelectLabel:contains('Jahr')" + BIRTHDAY_DROPDOWN_OPTION = "div[role='option']" + + # Formularfelder - E-Mail-Registrierung + EMAIL_FIELD = "input[placeholder='E-Mail-Adresse']" + PASSWORD_FIELD = "input[placeholder='Passwort']" + VERIFICATION_CODE_FIELD = "input[placeholder*='sechsstelligen Code']" + USERNAME_FIELD = "input[placeholder='Benutzername']" + + # Formularfelder - Telefon-Registrierung + COUNTRY_CODE_SELECT = "div[role='combobox']" + PHONE_FIELD = "input[placeholder='Telefonnummer']" + + # Buttons + SEND_CODE_BUTTON = "button[data-e2e='send-code-button']" + RESEND_CODE_BUTTON = "button:contains('Code erneut senden')" + CONTINUE_BUTTON = "button[type='submit']" + REGISTER_BUTTON = "button:contains('Registrieren')" + SKIP_BUTTON = "button:contains('Überspringen')" + + # Checkbox + NEWSLETTER_CHECKBOX = "input[type='checkbox']" + + # Erfolgs-Indikatoren für Registrierung + SUCCESS_INDICATORS = [ + "a[href='/foryou']", + "a[href='/explore']", + "button[data-e2e='profile-icon']", + "svg[data-e2e='profile-icon']" + ] + + # Links für Nutzungsbedingungen und Datenschutz + TERMS_LINK = "a:contains('Nutzungsbedingungen')" + PRIVACY_LINK = "a:contains('Datenschutzerklärung')" + + # Text-Matching-Parameter für Fuzzy-Matching + TEXT_MATCH = { + # Formularfelder + "form_fields": { + "email": ["E-Mail-Adresse", "E-Mail", "Email", "Mail"], + "phone": ["Telefonnummer", "Telefon", "Phone", "Mobile"], + "password": ["Passwort", "Password"], + "verification_code": ["Bestätigungscode", "Code", "Verifizierungscode", "Sicherheitscode"], + "username": ["Benutzername", "Username", "Name"] + }, + + # Buttons + "buttons": { + "send_code": ["Code senden", "Senden", "Send code", "Verification code", "Send"], + "continue": ["Weiter", "Continue", "Next", "Fortfahren"], + "register": ["Registrieren", "Register", "Sign up", "Konto erstellen"], + "skip": ["Überspringen", "Skip", "Later", "Später", "Nicht jetzt"], + }, + + # Fehler-Indikatoren + "error_indicators": [ + "Fehler", "Error", "Leider", "Ungültig", "Invalid", "Nicht verfügbar", + "Fehlgeschlagen", "Problem", "Failed", "Nicht möglich", "Bereits verwendet", + "Too many attempts", "Zu viele Versuche", "Rate limit", "Bitte warte" + ], + + # Bestätigungscode-Texte in E-Mails + "email_verification_patterns": [ + "ist dein Bestätigungscode", + "ist dein TikTok-Code", + "is your TikTok code", + "is your verification code", + "Dein Bestätigungscode lautet", + "Your verification code is" + ] + } + + @classmethod + def get_field_labels(cls, field_type: str) -> List[str]: + """ + Gibt die möglichen Bezeichnungen für ein Formularfeld zurück. + + Args: + field_type: Typ des Formularfelds (z.B. "email", "phone") + + Returns: + List[str]: Liste mit möglichen Bezeichnungen + """ + return cls.TEXT_MATCH["form_fields"].get(field_type, []) + + @classmethod + def get_button_texts(cls, button_type: str) -> List[str]: + """ + Gibt die möglichen Texte für einen Button zurück. + + Args: + button_type: Typ des Buttons (z.B. "send_code", "continue") + + Returns: + List[str]: Liste mit möglichen Button-Texten + """ + return cls.TEXT_MATCH["buttons"].get(button_type, []) + + @classmethod + def get_error_indicators(cls) -> List[str]: + """ + Gibt die möglichen Texte für Fehlerindikatoren zurück. + + Returns: + List[str]: Liste mit möglichen Fehlerindikator-Texten + """ + return cls.TEXT_MATCH["error_indicators"] + + @classmethod + def get_email_verification_patterns(cls) -> List[str]: + """ + Gibt die möglichen Texte für Bestätigungscodes in E-Mails zurück. + + Returns: + List[str]: Liste mit möglichen E-Mail-Bestätigungscode-Texten + """ + return cls.TEXT_MATCH["email_verification_patterns"] \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_ui_helper.py b/social_networks/tiktok/tiktok_ui_helper.py new file mode 100644 index 0000000..767f16e --- /dev/null +++ b/social_networks/tiktok/tiktok_ui_helper.py @@ -0,0 +1,523 @@ +""" +TikTok-UI-Helper - Hilfsmethoden für die Interaktion mit der TikTok-UI +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple, Union, Callable + +from .tiktok_selectors import TikTokSelectors +from utils.text_similarity import TextSimilarity, fuzzy_find_element, click_fuzzy_button + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_ui_helper") + +class TikTokUIHelper: + """ + Hilfsmethoden für die Interaktion mit der TikTok-Benutzeroberfläche. + Bietet robuste Funktionen zum Finden und Interagieren mit UI-Elementen. + """ + + def __init__(self, automation): + """ + Initialisiert den TikTok-UI-Helper. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + + # Initialisiere TextSimilarity für Fuzzy-Matching + self.text_similarity = TextSimilarity(default_threshold=0.7) + + logger.debug("TikTok-UI-Helper initialisiert") + + def _ensure_browser(self) -> bool: + """ + Stellt sicher, dass die Browser-Referenz verfügbar ist. + + Returns: + bool: True wenn Browser verfügbar, False sonst + """ + if self.browser is None: + self.browser = self.automation.browser + + if self.browser is None: + logger.error("Browser-Referenz nicht verfügbar") + return False + + return True + + def fill_field_fuzzy(self, field_labels: Union[str, List[str]], + value: str, fallback_selector: str = None, + threshold: float = 0.7, timeout: int = 5000) -> bool: + """ + Füllt ein Formularfeld mit Fuzzy-Text-Matching aus. + + Args: + field_labels: Bezeichner oder Liste von Bezeichnern des Feldes + value: Einzugebender Wert + fallback_selector: CSS-Selektor für Fallback + threshold: Schwellenwert für die Textähnlichkeit (0-1) + timeout: Zeitlimit für die Suche in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere field_labels zu einer Liste + if isinstance(field_labels, str): + field_labels = [field_labels] + + # Versuche, das Feld mit Fuzzy-Matching zu finden + element = fuzzy_find_element( + self.browser.page, + field_labels, + selector_type="input", + threshold=threshold, + wait_time=timeout + ) + + if element: + # Versuche, das Feld zu fokussieren und den Wert einzugeben + element.focus() + time.sleep(0.1) + element.fill("") # Leere das Feld zuerst + time.sleep(0.2) + + # Text menschenähnlich eingeben + for char in value: + element.type(char, delay=self.automation.human_behavior.delays["typing_per_char"] * 1000) + time.sleep(0.01) + + logger.info(f"Feld mit Fuzzy-Matching gefüllt: {value}") + return True + + # Fuzzy-Matching fehlgeschlagen, versuche über Attribute + if fallback_selector: + field_success = self.browser.fill_form_field(fallback_selector, value) + if field_success: + logger.info(f"Feld mit Fallback-Selektor gefüllt: {fallback_selector}") + return True + + # Versuche noch alternative Selektoren basierend auf field_labels + for label in field_labels: + # Versuche aria-label Attribut + aria_selector = f"input[aria-label='{label}'], textarea[aria-label='{label}']" + if self.browser.is_element_visible(aria_selector, timeout=1000): + if self.browser.fill_form_field(aria_selector, value): + logger.info(f"Feld über aria-label gefüllt: {label}") + return True + + # Versuche placeholder Attribut + placeholder_selector = f"input[placeholder*='{label}'], textarea[placeholder*='{label}']" + if self.browser.is_element_visible(placeholder_selector, timeout=1000): + if self.browser.fill_form_field(placeholder_selector, value): + logger.info(f"Feld über placeholder gefüllt: {label}") + return True + + # Versuche name Attribut + name_selector = f"input[name='{label.lower().replace(' ', '')}']" + if self.browser.is_element_visible(name_selector, timeout=1000): + if self.browser.fill_form_field(name_selector, value): + logger.info(f"Feld über name-Attribut gefüllt: {label}") + return True + + logger.warning(f"Konnte kein Feld für '{field_labels}' finden oder ausfüllen") + return False + + except Exception as e: + logger.error(f"Fehler beim Fuzzy-Ausfüllen des Feldes: {e}") + return False + + def click_button_fuzzy(self, button_texts: Union[str, List[str]], + fallback_selector: str = None, threshold: float = 0.7, + timeout: int = 5000) -> bool: + """ + Klickt einen Button mit Fuzzy-Text-Matching. + + Args: + button_texts: Text oder Liste von Texten des Buttons + fallback_selector: CSS-Selektor für Fallback + threshold: Schwellenwert für die Textähnlichkeit (0-1) + timeout: Zeitlimit für die Suche in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Normalisiere button_texts zu einer Liste + if isinstance(button_texts, str): + button_texts = [button_texts] + + # Logging der Suche + logger.info(f"Suche nach Button mit Texten: {button_texts}") + + if not button_texts or button_texts == [[]]: + logger.warning("Leere Button-Text-Liste angegeben!") + return False + + # TikTok-spezifische Selektoren zuerst prüfen + # Diese Selektoren sind häufig in TikTok's UI zu finden + tiktok_button_selectors = [ + "button[type='submit']", + "button[data-e2e='send-code-button']", + "button.e1w6iovg0", + "button.css-10nhlj9-Button-StyledButton" + ] + + for selector in tiktok_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + button_element = self.browser.wait_for_selector(selector, timeout=1000) + if button_element: + button_text = button_element.inner_text().strip() + + # Überprüfe, ob der Button-Text mit einem der gesuchten Texte übereinstimmt + for text in button_texts: + if self.text_similarity.is_similar(text, button_text, threshold=threshold): + logger.info(f"Button mit passendem Text gefunden: '{button_text}'") + button_element.click() + return True + + # Die allgemeine fuzzy_click_button-Funktion verwenden + result = click_fuzzy_button( + self.browser.page, + button_texts, + threshold=threshold, + timeout=timeout + ) + + if result: + logger.info(f"Button mit Fuzzy-Matching geklickt") + return True + + # Wenn Fuzzy-Matching fehlschlägt, versuche mit fallback_selector + if fallback_selector: + logger.info(f"Versuche Fallback-Selektor: {fallback_selector}") + if self.browser.click_element(fallback_selector): + logger.info(f"Button mit Fallback-Selektor geklickt: {fallback_selector}") + return True + + # Versuche alternative Methoden + + # 1. Versuche über aria-label + for text in button_texts: + if not text: + continue + + aria_selector = f"button[aria-label*='{text}'], [role='button'][aria-label*='{text}']" + if self.browser.is_element_visible(aria_selector, timeout=1000): + if self.browser.click_element(aria_selector): + logger.info(f"Button über aria-label geklickt: {text}") + return True + + # 2. Versuche über role='button' mit Text + for text in button_texts: + if not text: + continue + + xpath_selector = f"//div[@role='button' and contains(., '{text}')]" + if self.browser.is_element_visible(xpath_selector, timeout=1000): + if self.browser.click_element(xpath_selector): + logger.info(f"Button über role+text geklickt: {text}") + return True + + # 3. Versuche über Link-Text + for text in button_texts: + if not text: + continue + + link_selector = f"//a[contains(text(), '{text}')]" + if self.browser.is_element_visible(link_selector, timeout=1000): + if self.browser.click_element(link_selector): + logger.info(f"Link mit passendem Text geklickt: {text}") + return True + + # 4. Als letzten Versuch, klicke auf einen beliebigen Button + logger.warning("Kein spezifischer Button gefunden, versuche beliebigen Button zu klicken") + buttons = self.browser.page.query_selector_all("button") + if buttons and len(buttons) > 0: + for button in buttons: + visible = button.is_visible() + if visible: + logger.info("Klicke auf beliebigen sichtbaren Button") + button.click() + return True + + logger.warning(f"Konnte keinen Button für '{button_texts}' finden oder klicken") + return False + + except Exception as e: + logger.error(f"Fehler beim Fuzzy-Klicken des Buttons: {e}") + return False + + def select_dropdown_option(self, dropdown_selector: str, option_value: str, + option_type: str = "text", timeout: int = 5000) -> bool: + """ + Wählt eine Option aus einer Dropdown-Liste aus. + + Args: + dropdown_selector: Selektor für das Dropdown-Element + option_value: Wert oder Text der auszuwählenden Option + option_type: "text" für Text-Matching, "value" für Wert-Matching + timeout: Zeitlimit in Millisekunden + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Auf Dropdown-Element klicken, um die Optionen anzuzeigen + dropdown_element = self.browser.wait_for_selector(dropdown_selector, timeout=timeout) + if not dropdown_element: + logger.warning(f"Dropdown-Element nicht gefunden: {dropdown_selector}") + return False + + # Dropdown öffnen + dropdown_element.click() + time.sleep(0.5) # Kurz warten, damit die Optionen angezeigt werden + + # Optionen suchen + option_selector = "div[role='option']" + options = self.browser.page.query_selector_all(option_selector) + + if not options or len(options) == 0: + logger.warning(f"Keine Optionen gefunden für Dropdown: {dropdown_selector}") + return False + + # Option nach Text oder Wert suchen + selected = False + for option in options: + option_text = option.inner_text().strip() + + if option_type == "text": + if option_text == option_value or self.text_similarity.is_similar(option_text, option_value, threshold=0.9): + option.click() + selected = True + break + elif option_type == "value": + option_val = option.get_attribute("value") or "" + if option_val == option_value: + option.click() + selected = True + break + + if not selected: + logger.warning(f"Keine passende Option für '{option_value}' gefunden") + return False + + logger.info(f"Option '{option_value}' im Dropdown ausgewählt") + return True + + except Exception as e: + logger.error(f"Fehler bei der Auswahl der Dropdown-Option: {e}") + return False + + def check_for_error(self, error_selectors: List[str] = None, + error_texts: List[str] = None) -> Optional[str]: + """ + Überprüft, ob Fehlermeldungen angezeigt werden. + + Args: + error_selectors: Liste mit CSS-Selektoren für Fehlermeldungen + error_texts: Liste mit typischen Fehlertexten + + Returns: + Optional[str]: Die Fehlermeldung oder None, wenn keine Fehler gefunden wurden + """ + if not self._ensure_browser(): + return None + + try: + # Standardselektoren verwenden, wenn keine angegeben sind + if error_selectors is None: + error_selectors = [ + "div[role='alert']", + "p[class*='error']", + "span[class*='error']", + ".error-message" + ] + + # Standardfehlertexte verwenden, wenn keine angegeben sind + if error_texts is None: + error_texts = TikTokSelectors.get_error_indicators() + + # 1. Nach Fehlerselektoren suchen + for selector in error_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + error_text = element.text_content() + if error_text and len(error_text.strip()) > 0: + logger.info(f"Fehlermeldung gefunden (Selektor): {error_text.strip()}") + return error_text.strip() + + # 2. Alle Texte auf der Seite durchsuchen + page_content = self.browser.page.content() + + for error_text in error_texts: + if error_text.lower() in page_content.lower(): + # Versuche, den genauen Fehlertext zu extrahieren + matches = re.findall(r'<[^>]*>([^<]*' + re.escape(error_text.lower()) + '[^<]*)<', page_content.lower()) + if matches: + full_error = matches[0].strip() + logger.info(f"Fehlermeldung gefunden (Text): {full_error}") + return full_error + else: + logger.info(f"Fehlermeldung gefunden (Allgemein): {error_text}") + return error_text + + # 3. Nach weiteren Fehlerelementen suchen + elements = self.browser.page.query_selector_all("p, div, span") + + for element in elements: + element_text = element.inner_text() + if not element_text: + continue + + element_text = element_text.strip() + + # Prüfe Textähnlichkeit mit Fehlertexten + for error_text in error_texts: + if self.text_similarity.is_similar(error_text, element_text, threshold=0.7) or \ + self.text_similarity.contains_similar_text(element_text, error_texts, threshold=0.7): + logger.info(f"Fehlermeldung gefunden (Ähnlichkeit): {element_text}") + return element_text + + return None + + except Exception as e: + logger.error(f"Fehler beim Prüfen auf Fehlermeldungen: {e}") + return None + + def check_for_captcha(self) -> bool: + """ + Überprüft, ob ein Captcha angezeigt wird. + + Returns: + bool: True wenn Captcha erkannt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Selektoren für Captcha-Erkennung + captcha_selectors = [ + "div[data-testid='captcha']", + "iframe[src*='captcha']", + "iframe[title*='captcha']", + "iframe[title*='reCAPTCHA']" + ] + + # Captcha-Texte für textbasierte Erkennung + captcha_texts = [ + "captcha", "recaptcha", "sicherheitsüberprüfung", "security check", + "i'm not a robot", "ich bin kein roboter", "verify you're human", + "bestätige, dass du ein mensch bist" + ] + + # Nach Selektoren suchen + for selector in captcha_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.warning(f"Captcha erkannt (Selektor): {selector}") + return True + + # Nach Texten suchen + page_content = self.browser.page.content().lower() + + for text in captcha_texts: + if text in page_content: + logger.warning(f"Captcha erkannt (Text): {text}") + return True + + return False + + except Exception as e: + logger.error(f"Fehler bei der Captcha-Erkennung: {e}") + return False + + def wait_for_element(self, selectors: Union[str, List[str]], + timeout: int = 10000, check_interval: int = 500) -> Optional[Any]: + """ + Wartet auf das Erscheinen eines Elements. + + Args: + selectors: CSS-Selektor oder Liste von Selektoren + timeout: Zeitlimit in Millisekunden + check_interval: Intervall zwischen den Prüfungen in Millisekunden + + Returns: + Optional[Any]: Das gefundene Element oder None, wenn die Zeit abgelaufen ist + """ + if not self._ensure_browser(): + return None + + try: + # Normalisiere selectors zu einer Liste + if isinstance(selectors, str): + selectors = [selectors] + + start_time = time.time() + end_time = start_time + (timeout / 1000) + + while time.time() < end_time: + for selector in selectors: + element = self.browser.wait_for_selector(selector, timeout=check_interval) + if element: + logger.info(f"Element mit Selektor '{selector}' gefunden") + return element + + # Kurze Pause vor der nächsten Prüfung + time.sleep(check_interval / 1000) + + logger.warning(f"Zeitüberschreitung beim Warten auf Element mit Selektoren '{selectors}'") + return None + + except Exception as e: + logger.error(f"Fehler beim Warten auf Element: {e}") + return None + + def is_registration_successful(self) -> bool: + """ + Überprüft, ob die Registrierung erfolgreich war. + + Returns: + bool: True wenn erfolgreich, False sonst + """ + try: + # Erfolgsindikatoren überprüfen + success_indicators = TikTokSelectors.SUCCESS_INDICATORS + + for selector in success_indicators: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Registrierung erfolgreich (Indikator gefunden: {selector})") + return True + + # URL überprüfen + current_url = self.browser.page.url + if "/foryou" in current_url or "tiktok.com/explore" in current_url: + logger.info("Registrierung erfolgreich (Erfolgreiche Navigation erkannt)") + return True + + # Überprüfen, ob Fehler angezeigt werden + error_message = self.check_for_error() + if error_message: + logger.warning(f"Registrierung nicht erfolgreich: {error_message}") + return False + + logger.warning("Konnte Registrierungserfolg nicht bestätigen") + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Registrierungserfolgs: {e}") + return False \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_utils.py b/social_networks/tiktok/tiktok_utils.py new file mode 100644 index 0000000..2e2b3b6 --- /dev/null +++ b/social_networks/tiktok/tiktok_utils.py @@ -0,0 +1,495 @@ +""" +TikTok-Utils - Hilfsfunktionen für die TikTok-Automatisierung. +""" + +import logging +import re +import time +import random +from typing import Dict, List, Any, Optional, Tuple, Union + +from .tiktok_selectors import TikTokSelectors + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_utils") + +class TikTokUtils: + """ + Hilfsfunktionen für die TikTok-Automatisierung. + Enthält allgemeine Hilfsmethoden und kleinere Funktionen. + """ + + def __init__(self, automation): + """ + Initialisiert die TikTok-Utils. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + + logger.debug("TikTok-Utils initialisiert") + + def _ensure_browser(self) -> bool: + """ + Stellt sicher, dass die Browser-Referenz verfügbar ist. + + Returns: + bool: True wenn Browser verfügbar, False sonst + """ + if self.browser is None: + self.browser = self.automation.browser + + if self.browser is None: + logger.error("Browser-Referenz nicht verfügbar") + return False + + return True + + def handle_cookie_banner(self) -> bool: + """ + Behandelt den Cookie-Banner, falls angezeigt. + + Returns: + bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler + """ + if not self._ensure_browser(): + return False + + try: + # Cookie-Dialoge in TikTok prüfen + cookie_selectors = [ + "button[data-e2e='cookie-banner-reject']", + "button:contains('Ablehnen')", + "button:contains('Nur erforderliche')", + "button:contains('Reject')", + "button[data-e2e='cookie-banner-accept']" + ] + + for selector in cookie_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Cookie-Banner erkannt: {selector}") + + # Versuche, den Ablehnen-Button zu klicken + if "reject" in selector.lower() or "ablehnen" in selector.lower() or "erforderliche" in selector.lower(): + if self.browser.click_element(selector): + logger.info("Cookie-Banner erfolgreich abgelehnt") + time.sleep(random.uniform(0.5, 1.5)) + return True + + # Fallback: Akzeptieren-Button klicken, wenn Ablehnen nicht funktioniert + else: + if self.browser.click_element(selector): + logger.info("Cookie-Banner erfolgreich akzeptiert") + time.sleep(random.uniform(0.5, 1.5)) + return True + + # Wenn kein Cookie-Banner gefunden wurde + logger.debug("Kein Cookie-Banner erkannt") + return True + + except Exception as e: + logger.error(f"Fehler beim Behandeln des Cookie-Banners: {e}") + return False + + def extract_username_from_url(self, url: str) -> Optional[str]: + """ + Extrahiert den Benutzernamen aus einer TikTok-URL. + + Args: + url: Die TikTok-URL + + Returns: + Optional[str]: Der extrahierte Benutzername oder None + """ + try: + # Muster für Profil-URLs + patterns = [ + r'tiktok\.com/@([a-zA-Z0-9._]+)/?(?:$|\?|#)', + r'tiktok\.com/user/([a-zA-Z0-9._]+)/?', + r'tiktok\.com/video/[^/]+/by/([a-zA-Z0-9._]+)/?' + ] + + for pattern in patterns: + match = re.search(pattern, url) + if match: + username = match.group(1) + # Einige Ausnahmen filtern + if username not in ["explore", "accounts", "video", "foryou", "trending"]: + return username + + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Benutzernamens aus der URL: {e}") + return None + + def get_current_username(self) -> Optional[str]: + """ + Versucht, den Benutzernamen des aktuell angemeldeten Kontos zu ermitteln. + + Returns: + Optional[str]: Der Benutzername oder None, wenn nicht gefunden + """ + if not self._ensure_browser(): + return None + + try: + # Verschiedene Methoden zur Erkennung des Benutzernamens + + # 1. Benutzername aus URL des Profils + profile_link_selectors = [ + "a[href*='/@']", + "a[href*='/user/']" + ] + + for selector in profile_link_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + href = element.get_attribute("href") + if href: + username = self.extract_username_from_url(href) + if username: + logger.info(f"Benutzername aus Profil-Link ermittelt: {username}") + return username + + # 2. Profilicon prüfen auf data-e2e-Attribut + profile_icon_selectors = [ + "button[data-e2e='profile-icon']", + "svg[data-e2e='profile-icon']" + ] + + for selector in profile_icon_selectors: + element = self.browser.wait_for_selector(selector, timeout=2000) + if element: + # Prüfen, ob ein Elternelement möglicherweise ein data-e2e-Attribut mit dem Benutzernamen hat + parent = element.evaluate("node => node.parentElement") + if parent: + data_e2e = parent.get_attribute("data-e2e") + if data_e2e and "profile" in data_e2e: + username_match = re.search(r'profile-([a-zA-Z0-9._]+)', data_e2e) + if username_match: + username = username_match.group(1) + logger.info(f"Benutzername aus data-e2e-Attribut ermittelt: {username}") + return username + + # 3. TikTok-spezifisches Element mit Benutzername suchen + username_element = self.browser.wait_for_selector("h1[data-e2e='user-title']", timeout=2000) + if username_element: + username = username_element.inner_text().strip() + if username: + logger.info(f"Benutzername aus user-title-Element ermittelt: {username}") + return username + + logger.warning("Konnte Benutzernamen nicht ermitteln") + return None + + except Exception as e: + logger.error(f"Fehler bei der Ermittlung des Benutzernamens: {e}") + return None + + def wait_for_navigation(self, expected_url_pattern: str = None, + timeout: int = 30000, check_interval: int = 500) -> bool: + """ + Wartet, bis die Seite zu einer URL mit einem bestimmten Muster navigiert. + + Args: + expected_url_pattern: Erwartetes Muster der URL (Regex) + timeout: Zeitlimit in Millisekunden + check_interval: Intervall zwischen den Prüfungen in Millisekunden + + Returns: + bool: True wenn die Navigation erfolgreich war, False sonst + """ + if not self._ensure_browser(): + return False + + try: + start_time = time.time() + end_time = start_time + (timeout / 1000) + + while time.time() < end_time: + current_url = self.browser.page.url + + if expected_url_pattern and re.search(expected_url_pattern, current_url): + logger.info(f"Navigation zu URL mit Muster '{expected_url_pattern}' erfolgreich") + return True + + # Kurze Pause vor der nächsten Prüfung + time.sleep(check_interval / 1000) + + logger.warning(f"Zeitüberschreitung bei Navigation zu URL mit Muster '{expected_url_pattern}'") + return False + + except Exception as e: + logger.error(f"Fehler beim Warten auf Navigation: {e}") + return False + + def handle_dialog_or_popup(self, expected_text: Union[str, List[str]] = None, + action: str = "close", timeout: int = 5000) -> bool: + """ + Behandelt einen Dialog oder Popup. + + Args: + expected_text: Erwarteter Text im Dialog oder Liste von Texten + action: Aktion ("close", "confirm", "cancel") + timeout: Zeitlimit in Millisekunden + + Returns: + bool: True wenn der Dialog erfolgreich behandelt wurde, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Dialog-Element suchen + dialog_selector = "div[role='dialog']" + dialog_element = self.browser.wait_for_selector(dialog_selector, timeout=timeout) + + if not dialog_element: + logger.debug("Kein Dialog gefunden") + return False + + logger.info("Dialog gefunden") + + # Text im Dialog prüfen, falls angegeben + if expected_text: + if isinstance(expected_text, str): + expected_text = [expected_text] + + dialog_text = dialog_element.inner_text() + text_found = False + + for text in expected_text: + if text in dialog_text: + logger.info(f"Erwarteter Text im Dialog gefunden: '{text}'") + text_found = True + break + + if not text_found: + logger.warning(f"Erwarteter Text nicht im Dialog gefunden: {expected_text}") + return False + + # Aktion ausführen + if action == "close": + # Schließen-Button suchen und klicken + close_button_selectors = [ + "button[data-e2e='modal-close']", + "svg[data-e2e='modal-close']", + "button.css-1afoydx-StyledCloseButton", + "div[role='dialog'] button:first-child" + ] + + for selector in close_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info("Dialog geschlossen") + return True + + # Wenn kein Schließen-Button gefunden wurde, Escape-Taste drücken + self.browser.page.keyboard.press("Escape") + logger.info("Dialog mit Escape-Taste geschlossen") + + elif action == "confirm": + # Bestätigen-Button suchen und klicken + confirm_button_selectors = [ + "button[type='submit']", + "button:contains('OK')", + "button:contains('Ja')", + "button:contains('Yes')", + "button:contains('Bestätigen')", + "button:contains('Confirm')" + ] + + for selector in confirm_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info("Dialog bestätigt") + return True + + elif action == "cancel": + # Abbrechen-Button suchen und klicken + cancel_button_selectors = [ + "button:contains('Abbrechen')", + "button:contains('Cancel')", + "button:contains('Nein')", + "button:contains('No')" + ] + + for selector in cancel_button_selectors: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info("Dialog abgebrochen") + return True + + logger.warning(f"Konnte keine {action}-Aktion für den Dialog ausführen") + return False + + except Exception as e: + logger.error(f"Fehler bei der Dialog-Behandlung: {e}") + return False + + def handle_rate_limiting(self, rotate_proxy: bool = True) -> bool: + """ + Behandelt eine Rate-Limiting-Situation. + + Args: + rotate_proxy: Ob der Proxy rotiert werden soll + + Returns: + bool: True wenn erfolgreich behandelt, False sonst + """ + if not self._ensure_browser(): + return False + + try: + logger.warning("Rate-Limiting erkannt, warte und versuche es erneut") + + # Screenshot erstellen + self.automation._take_screenshot("rate_limit_detected") + + # Proxy rotieren, falls gewünscht + if rotate_proxy and self.automation.use_proxy: + success = self.automation._rotate_proxy() + if not success: + logger.warning("Konnte Proxy nicht rotieren") + + # Längere Wartezeit + wait_time = random.uniform(120, 300) # 2-5 Minuten + logger.info(f"Warte {wait_time:.1f} Sekunden vor dem nächsten Versuch") + time.sleep(wait_time) + + # Seite neuladen + self.browser.page.reload() + self.automation.human_behavior.wait_for_page_load() + + # Prüfen, ob Rate-Limiting noch aktiv ist + rate_limit_texts = [ + "bitte warte einige minuten", + "please wait a few minutes", + "try again later", + "versuche es später erneut", + "zu viele anfragen", + "too many requests" + ] + + page_content = self.browser.page.content().lower() + + still_rate_limited = False + for text in rate_limit_texts: + if text in page_content: + still_rate_limited = True + break + + if still_rate_limited: + logger.warning("Immer noch Rate-Limited nach dem Warten") + return False + else: + logger.info("Rate-Limiting scheint aufgehoben zu sein") + return True + + except Exception as e: + logger.error(f"Fehler bei der Behandlung des Rate-Limitings: {e}") + return False + + def is_logged_in(self) -> bool: + """ + Überprüft, ob der Benutzer bei TikTok angemeldet ist. + + Returns: + bool: True wenn angemeldet, False sonst + """ + if not self._ensure_browser(): + return False + + try: + # Erfolgsindikatoren überprüfen + success_indicators = TikTokSelectors.SUCCESS_INDICATORS + + for selector in success_indicators: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Benutzer ist angemeldet (Indikator: {selector})") + return True + + # URL überprüfen + current_url = self.browser.page.url + if "/foryou" in current_url or "tiktok.com/explore" in current_url: + logger.info("Benutzer ist angemeldet (URL-Check)") + return True + + # Anmelden-Button prüfen - wenn sichtbar, dann nicht angemeldet + login_button_selectors = [ + TikTokSelectors.LOGIN_BUTTON_LEFT, + TikTokSelectors.LOGIN_BUTTON_RIGHT + ] + + for selector in login_button_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Benutzer ist nicht angemeldet (Anmelde-Button sichtbar)") + return False + + # Profilicon checken - wenn sichtbar, dann angemeldet + profile_selectors = [ + "button[data-e2e='profile-icon']", + "svg[data-e2e='profile-icon']" + ] + + for selector in profile_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Benutzer ist angemeldet (Profilicon sichtbar)") + return True + + logger.warning("Konnte Login-Status nicht eindeutig bestimmen") + return False + + except Exception as e: + logger.error(f"Fehler bei der Überprüfung des Login-Status: {e}") + return False + + def extract_verification_code_from_email(self, email_body: str) -> Optional[str]: + """ + Extrahiert den Verifizierungscode aus einer E-Mail. + + Args: + email_body: Der E-Mail-Text + + Returns: + Optional[str]: Der Verifizierungscode oder None, wenn nicht gefunden + """ + try: + # Muster für TikTok-Verifizierungscodes + patterns = [ + r'(\d{6}) ist dein Bestätigungscode', + r'(\d{6}) ist dein TikTok-Code', + r'(\d{6}) is your TikTok code', + r'(\d{6}) is your verification code', + r'Dein Bestätigungscode lautet (\d{6})', + r'Your verification code is (\d{6})', + r'Verification code: (\d{6})', + r'Bestätigungscode: (\d{6})', + r'TikTok code: (\d{6})', + r'TikTok-Code: (\d{6})' + ] + + for pattern in patterns: + match = re.search(pattern, email_body) + if match: + code = match.group(1) + logger.info(f"Verifizierungscode aus E-Mail extrahiert: {code}") + return code + + # Allgemeine Suche nach 6-stelligen Zahlen, wenn keine spezifischen Muster passen + general_match = re.search(r'[^\d](\d{6})[^\d]', email_body) + if general_match: + code = general_match.group(1) + logger.info(f"6-stelliger Code aus E-Mail extrahiert: {code}") + return code + + logger.warning("Kein Verifizierungscode in der E-Mail gefunden") + return None + + except Exception as e: + logger.error(f"Fehler beim Extrahieren des Verifizierungscodes aus der E-Mail: {e}") + return None \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_verification.py b/social_networks/tiktok/tiktok_verification.py new file mode 100644 index 0000000..c3519b5 --- /dev/null +++ b/social_networks/tiktok/tiktok_verification.py @@ -0,0 +1,457 @@ +# social_networks/tiktok/tiktok_verification.py + +""" +TikTok-Verifizierung - Klasse für die Verifizierungsfunktionalität bei TikTok +""" + +import logging +import time +import re +from typing import Dict, List, Any, Optional, Tuple + +from .tiktok_selectors import TikTokSelectors +from .tiktok_workflow import TikTokWorkflow + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_verification") + +class TikTokVerification: + """ + Klasse für die Verifizierung von TikTok-Konten. + Enthält alle Methoden für den Verifizierungsprozess. + """ + + def __init__(self, automation): + """ + Initialisiert die TikTok-Verifizierung. + + Args: + automation: Referenz auf die Hauptautomatisierungsklasse + """ + self.automation = automation + self.browser = None # Wird zur Laufzeit auf automation.browser gesetzt + self.selectors = TikTokSelectors() + self.workflow = TikTokWorkflow.get_verification_workflow() + + logger.debug("TikTok-Verifizierung initialisiert") + + def verify_account(self, verification_code: str, **kwargs) -> Dict[str, Any]: + """ + Führt den Verifizierungsprozess für ein TikTok-Konto durch. + + Args: + verification_code: Der Bestätigungscode + **kwargs: Weitere optionale Parameter + + Returns: + Dict[str, Any]: Ergebnis der Verifizierung mit Status + """ + # Hole Browser-Referenz von der Hauptklasse + self.browser = self.automation.browser + + # Validiere den Verifizierungscode + if not self._validate_verification_code(verification_code): + return { + "success": False, + "error": "Ungültiger Verifizierungscode", + "stage": "code_validation" + } + + try: + # 1. Überprüfen, ob wir auf der Verifizierungsseite sind + if not self._is_on_verification_page(): + # Versuche, zur Verifizierungsseite zu navigieren, falls möglich + # Direktnavigation ist jedoch normalerweise nicht möglich + return { + "success": False, + "error": "Nicht auf der Verifizierungsseite", + "stage": "page_check" + } + + # 2. Verifizierungscode eingeben und absenden + if not self.enter_and_submit_verification_code(verification_code): + return { + "success": False, + "error": "Fehler beim Eingeben oder Absenden des Verifizierungscodes", + "stage": "code_entry" + } + + # 3. Überprüfen, ob die Verifizierung erfolgreich war + success, error_message = self._check_verification_success() + + if not success: + return { + "success": False, + "error": f"Verifizierung fehlgeschlagen: {error_message or 'Unbekannter Fehler'}", + "stage": "verification_check" + } + + # 4. Zusätzliche Dialoge behandeln + self._handle_post_verification_dialogs() + + # Verifizierung erfolgreich + logger.info("TikTok-Verifizierung erfolgreich abgeschlossen") + + return { + "success": True, + "stage": "completed" + } + + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der TikTok-Verifizierung: {str(e)}" + logger.error(error_msg, exc_info=True) + + return { + "success": False, + "error": error_msg, + "stage": "exception" + } + + def _validate_verification_code(self, verification_code: str) -> bool: + """ + Validiert den Verifizierungscode. + + Args: + verification_code: Der zu validierende Code + + Returns: + bool: True wenn der Code gültig ist, False sonst + """ + # Leerer Code + if not verification_code: + logger.error("Verifizierungscode ist leer") + return False + + # Code-Format prüfen (normalerweise 6-stellige Zahl) + if not re.match(r"^\d{6}$", verification_code): + logger.warning(f"Verifizierungscode hat unerwartetes Format: {verification_code}") + # Wir geben trotzdem True zurück, da einige Codes andere Formate haben könnten + return True + + return True + + def _is_on_verification_page(self) -> bool: + """ + Überprüft, ob wir auf der Verifizierungsseite sind. + + Returns: + bool: True wenn auf der Verifizierungsseite, False sonst + """ + try: + # Screenshot erstellen + self.automation._take_screenshot("verification_page_check") + + # Nach Verifizierungsfeld suchen + verification_selectors = [ + TikTokSelectors.VERIFICATION_CODE_FIELD, + "input[placeholder*='Code']", + "input[placeholder*='code']", + "input[placeholder*='sechsstelligen']", + "input[data-e2e='verification-code-input']" + ] + + for selector in verification_selectors: + if self.browser.is_element_visible(selector, timeout=3000): + logger.info("Auf Verifizierungsseite") + return True + + # Textbasierte Erkennung + verification_texts = [ + "Bestätigungscode", + "Verification code", + "sechsstelligen Code", + "6-digit code", + "Code senden", + "Send code" + ] + + page_content = self.browser.page.content().lower() + + for text in verification_texts: + if text.lower() in page_content: + logger.info(f"Auf Verifizierungsseite (erkannt durch Text: {text})") + return True + + logger.warning("Nicht auf der Verifizierungsseite") + return False + + except Exception as e: + logger.error(f"Fehler beim Überprüfen der Verifizierungsseite: {e}") + return False + + def enter_and_submit_verification_code(self, verification_code: str) -> bool: + """ + Gibt den Verifizierungscode ein und sendet ihn ab. + + Args: + verification_code: Der einzugebende Verifizierungscode + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + logger.info(f"Versuche Verifizierungscode einzugeben: {verification_code}") + + # Mögliche Selektoren für das Verifizierungscode-Feld + code_field_selectors = [ + TikTokSelectors.VERIFICATION_CODE_FIELD, + "input[placeholder*='Code']", + "input[placeholder*='sechsstelligen Code']", + "input[data-e2e='verification-code-input']" + ] + + # Versuche, das Feld zu finden und auszufüllen + code_field_found = False + + for selector in code_field_selectors: + logger.debug(f"Versuche Selektor: {selector}") + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Codefeld gefunden mit Selektor: {selector}") + if self.browser.fill_form_field(selector, verification_code): + code_field_found = True + logger.info(f"Verifizierungscode eingegeben: {verification_code}") + break + + # Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen + if not code_field_found: + logger.info("Versuche Fuzzy-Matching für Codefeld") + code_field_found = self.automation.ui_helper.fill_field_fuzzy( + ["Bestätigungscode", "Code eingeben", "Verification code", "6-digit code"], + verification_code + ) + + if not code_field_found: + logger.error("Konnte Verifizierungscode-Feld nicht finden oder ausfüllen") + + # Erstelle einen Screenshot zum Debuggen + self.automation._take_screenshot("code_field_not_found") + return False + + # Menschliche Verzögerung vor dem Absenden + self.automation.human_behavior.random_delay(1.0, 2.0) + + # Screenshot erstellen + self.automation._take_screenshot("verification_code_entered") + + # "Weiter"-Button finden und klicken + weiter_button_selectors = [ + TikTokSelectors.WEITER_BUTTON, + "button[type='submit']", + "button.e1w6iovg0", + "button[data-e2e='next-button']", + "//button[contains(text(), 'Weiter')]" + ] + + weiter_button_found = False + + logger.info("Suche nach Weiter-Button") + for selector in weiter_button_selectors: + logger.debug(f"Versuche Weiter-Button-Selektor: {selector}") + if self.browser.is_element_visible(selector, timeout=2000): + logger.info(f"Weiter-Button gefunden mit Selektor: {selector}") + if self.browser.click_element(selector): + weiter_button_found = True + logger.info("Verifizierungscode-Formular abgesendet") + break + + # Versuche es mit der Fuzzy-Matching-Methode, wenn direkte Selektoren fehlschlagen + if not weiter_button_found: + logger.info("Versuche Fuzzy-Matching für Weiter-Button") + weiter_buttons = ["Weiter", "Next", "Continue", "Fertig", "Submit", "Verify", "Senden"] + weiter_button_found = self.automation.ui_helper.click_button_fuzzy( + weiter_buttons + ) + + if not weiter_button_found: + # Erstelle einen Screenshot zum Debuggen + self.automation._take_screenshot("weiter_button_not_found") + + # Versuche es mit Enter-Taste als letzten Ausweg + logger.info("Konnte Weiter-Button nicht finden, versuche Enter-Taste") + self.browser.page.keyboard.press("Enter") + logger.info("Enter-Taste zur Bestätigung des Verifizierungscodes gedrückt") + weiter_button_found = True + + # Warten nach dem Absenden + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + return weiter_button_found + + except Exception as e: + logger.error(f"Fehler beim Eingeben und Absenden des Verifizierungscodes: {e}") + return False + + def _check_verification_success(self) -> Tuple[bool, Optional[str]]: + """ + Überprüft, ob die Verifizierung erfolgreich war. + + Returns: + Tuple[bool, Optional[str]]: (Erfolg, Fehlermeldung falls vorhanden) + """ + try: + # Warten nach der Verifizierung + self.automation.human_behavior.wait_for_page_load(multiplier=1.5) + + # Screenshot erstellen + self.automation._take_screenshot("verification_result") + + # Immer noch auf der Verifizierungsseite? + still_on_verification = self._is_on_verification_page() + + if still_on_verification: + # Fehlermeldung suchen + error_message = self.automation.ui_helper.check_for_error() + + if error_message: + logger.error(f"Verifizierungsfehler: {error_message}") + return False, error_message + else: + logger.error("Verifizierung fehlgeschlagen, immer noch auf der Verifizierungsseite") + return False, "Immer noch auf der Verifizierungsseite" + + # Prüfe, ob wir zur Benutzernamen-Erstellung weitergeleitet wurden + username_selectors = [ + "input[placeholder='Benutzername']", + "input[name='new-username']", + "//input[@placeholder='Benutzername']" + ] + + for selector in username_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + logger.info("Verifizierung erfolgreich, zur Benutzernamenauswahl weitergeleitet") + return True, None + + # Prüfe auf TikTok-Startseite + current_url = self.browser.page.url + if "tiktok.com" in current_url and "/login" not in current_url and "/signup" not in current_url: + logger.info("Verifizierung erfolgreich, jetzt auf der Startseite") + return True, None + + # Wenn keine eindeutigen Indikatoren gefunden wurden, aber auch keine Fehler + logger.warning("Keine eindeutigen Erfolgsindikatoren für die Verifizierung gefunden") + return True, None # Wir gehen davon aus, dass es erfolgreich war + + except Exception as e: + logger.error(f"Fehler beim Überprüfen des Verifizierungserfolgs: {e}") + return False, f"Fehler bei der Erfolgsprüfung: {str(e)}" + + def _handle_post_verification_dialogs(self) -> None: + """ + Behandelt Dialoge, die nach erfolgreicher Verifizierung erscheinen können. + """ + try: + # Liste der möglichen Dialoge und wie man sie überspringt + dialogs_to_handle = [ + { + "name": "username_setup", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "interests", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "follow_accounts", + "skip_texts": ["Überspringen", "Skip"], + "skip_selectors": ["//button[contains(text(), 'Überspringen')]", "//button[contains(text(), 'Skip')]"] + }, + { + "name": "notifications", + "skip_texts": ["Später", "Nein", "Nicht jetzt", "Later", "No", "Not now"], + "skip_selectors": ["//button[contains(text(), 'Später')]", "//button[contains(text(), 'Not now')]"] + } + ] + + # Versuche, jeden möglichen Dialog zu behandeln + for dialog in dialogs_to_handle: + self._try_skip_dialog(dialog) + + logger.info("Nachverifizierungs-Dialoge behandelt") + + except Exception as e: + logger.warning(f"Fehler beim Behandeln der Nachverifizierungs-Dialoge: {e}") + # Nicht kritisch, daher keine Fehlerbehandlung + + def _try_skip_dialog(self, dialog: Dict[str, Any]) -> bool: + """ + Versucht, einen bestimmten Dialog zu überspringen. + + Args: + dialog: Informationen zum Dialog + + Returns: + bool: True wenn Dialog gefunden und übersprungen, False sonst + """ + try: + # Zuerst mit Fuzzy-Matching versuchen + skip_clicked = self.automation.ui_helper.click_button_fuzzy( + dialog["skip_texts"], + threshold=0.7, + timeout=3000 + ) + + if skip_clicked: + logger.info(f"Dialog '{dialog['name']}' mit Fuzzy-Matching übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + # Wenn Fuzzy-Matching fehlschlägt, direkte Selektoren versuchen + for selector in dialog["skip_selectors"]: + if self.browser.is_element_visible(selector, timeout=1000): + if self.browser.click_element(selector): + logger.info(f"Dialog '{dialog['name']}' mit direktem Selektor übersprungen") + self.automation.human_behavior.random_delay(0.5, 1.0) + return True + + return False + + except Exception as e: + logger.warning(f"Fehler beim Versuch, Dialog '{dialog['name']}' zu überspringen: {e}") + return False + + def resend_verification_code(self) -> bool: + """ + Versucht, den Verifizierungscode erneut senden zu lassen. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Resend-Button suchen und klicken + resend_selectors = [ + "button[data-e2e='send-code-button']", + "//button[contains(text(), 'Code senden')]", + "//button[contains(text(), 'Code erneut senden')]", + "//button[contains(text(), 'Erneut senden')]", + "a[data-e2e='resend-code-link']" + ] + + for selector in resend_selectors: + if self.browser.is_element_visible(selector, timeout=2000): + if self.browser.click_element(selector): + logger.info("Code erneut angefordert") + self.automation.human_behavior.random_delay(1.0, 2.0) + return True + + # Fuzzy-Matching versuchen + resend_texts = ["Code senden", "Code erneut senden", "Erneut senden", "Resend code", "Send again"] + + resend_clicked = self.automation.ui_helper.click_button_fuzzy( + resend_texts, + threshold=0.7, + timeout=3000 + ) + + if resend_clicked: + logger.info("Code erneut angefordert (über Fuzzy-Matching)") + self.automation.human_behavior.random_delay(1.0, 2.0) + return True + + logger.warning("Konnte keinen 'Code erneut senden'-Button finden") + return False + + except Exception as e: + logger.error(f"Fehler beim erneuten Anfordern des Codes: {e}") + return False \ No newline at end of file diff --git a/social_networks/tiktok/tiktok_workflow.py b/social_networks/tiktok/tiktok_workflow.py new file mode 100644 index 0000000..37a62a0 --- /dev/null +++ b/social_networks/tiktok/tiktok_workflow.py @@ -0,0 +1,427 @@ +""" +TikTok-Workflow - Definiert die Schritte für die TikTok-Anmeldung und -Registrierung +""" + +import logging +from typing import Dict, List, Any, Optional, Tuple +import re + +from utils.text_similarity import TextSimilarity + +# Konfiguriere Logger +logger = logging.getLogger("tiktok_workflow") + +class TikTokWorkflow: + """ + Definiert die Workflow-Schritte für verschiedene TikTok-Aktionen + wie Registrierung, Anmeldung und Verifizierung. + """ + + # Text-Ähnlichkeits-Threshold für Fuzzy-Matching + SIMILARITY_THRESHOLD = 0.7 + + # Initialisiere TextSimilarity für Matching + text_similarity = TextSimilarity(default_threshold=SIMILARITY_THRESHOLD) + + # Mögliche alternative Texte für verschiedene UI-Elemente + TEXT_ALTERNATIVES = { + "email": ["E-Mail", "Email", "E-mail", "Mail", "email"], + "phone": ["Telefon", "Telefonnummer", "Phone", "Mobile", "mobile"], + "password": ["Passwort", "Password", "pass"], + "code": ["Code", "Bestätigungscode", "Verification code", "Sicherheitscode"], + "username": ["Benutzername", "Username", "user name"], + "submit": ["Registrieren", "Sign up", "Anmelden", "Login", "Log in", "Submit"], + "next": ["Weiter", "Next", "Continue", "Fortfahren"], + "skip": ["Überspringen", "Skip", "Later", "Später", "Not now", "Nicht jetzt"] + } + + @staticmethod + def get_registration_workflow(registration_method: str = "email") -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die TikTok-Registrierung zurück. + + Args: + registration_method: "email" oder "phone" + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + # Basisschritte für beide Methoden + common_steps = [ + { + "name": "navigate_to_signup", + "description": "Zur TikTok-Startseite navigieren", + "url": "https://www.tiktok.com", + "wait_for": ["button#header-login-button", "button#top-right-action-bar-login-button"], + "fuzzy_match": None + }, + { + "name": "click_login_button", + "description": "Anmelden-Button klicken", + "action": "click", + "target": "button#top-right-action-bar-login-button", + "wait_for": ["a[href*='/signup']", "div[role='dialog']"], + "fuzzy_match": ["Anmelden", "Sign in", "Log in"] + }, + { + "name": "click_register_link", + "description": "Registrieren-Link klicken", + "action": "click", + "target": "a[href*='/signup']", + "wait_for": ["div[data-e2e='channel-item']"], + "fuzzy_match": ["Registrieren", "Sign up", "Register"] + }, + { + "name": "click_phone_email_button", + "description": "Telefon/E-Mail-Option auswählen", + "action": "click", + "target": "div[data-e2e='channel-item']", + "wait_for": ["a[href*='/signup/phone-or-email/email']", "a[href*='/signup/phone-or-email/phone']"], + "fuzzy_match": ["Telefonnummer oder E-Mail-Adresse", "Phone or Email"] + } + ] + + # Spezifische Schritte je nach Registrierungsmethode + method_steps = [] + if registration_method == "email": + method_steps.append({ + "name": "click_email_registration", + "description": "Mit E-Mail registrieren auswählen", + "action": "click", + "target": "a[href*='/signup/phone-or-email/email']", + "wait_for": ["input[placeholder='E-Mail-Adresse']"], + "fuzzy_match": ["Mit E-Mail-Adresse registrieren", "Email", "E-Mail"] + }) + else: # phone + method_steps.append({ + "name": "click_phone_registration", + "description": "Mit Telefonnummer registrieren auswählen", + "action": "click", + "target": "a[href*='/signup/phone-or-email/phone']", + "wait_for": ["input[placeholder='Telefonnummer']"], + "fuzzy_match": ["Mit Telefonnummer registrieren", "Phone", "Telefon"] + }) + + # Geburtsdatum-Schritte + birthday_steps = [ + { + "name": "select_birth_month", + "description": "Geburtsmonat auswählen", + "action": "click", + "target": "div.css-1fi2hzv-DivSelectLabel:contains('Monat')", + "wait_for": ["div[role='option']"], + "fuzzy_match": ["Monat", "Month"] + }, + { + "name": "select_month_option", + "description": "Monats-Option auswählen", + "action": "select_option", + "target": "div[role='option']", + "value": "{MONTH_NAME}", + "wait_for": [], + "fuzzy_match": None + }, + { + "name": "select_birth_day", + "description": "Geburtstag auswählen", + "action": "click", + "target": "div.css-1fi2hzv-DivSelectLabel:contains('Tag')", + "wait_for": ["div[role='option']"], + "fuzzy_match": ["Tag", "Day"] + }, + { + "name": "select_day_option", + "description": "Tags-Option auswählen", + "action": "select_option", + "target": "div[role='option']", + "value": "{DAY}", + "wait_for": [], + "fuzzy_match": None + }, + { + "name": "select_birth_year", + "description": "Geburtsjahr auswählen", + "action": "click", + "target": "div.css-1fi2hzv-DivSelectLabel:contains('Jahr')", + "wait_for": ["div[role='option']"], + "fuzzy_match": ["Jahr", "Year"] + }, + { + "name": "select_year_option", + "description": "Jahres-Option auswählen", + "action": "select_option", + "target": "div[role='option']", + "value": "{YEAR}", + "wait_for": [], + "fuzzy_match": None + } + ] + + # Formularschritte für E-Mail + email_form_steps = [ + { + "name": "fill_email", + "description": "E-Mail-Adresse eingeben", + "action": "fill", + "target": "input[placeholder='E-Mail-Adresse']", + "value": "{EMAIL}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["email"] + }, + { + "name": "fill_password", + "description": "Passwort eingeben", + "action": "fill", + "target": "input[placeholder='Passwort']", + "value": "{PASSWORD}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["password"] + }, + { + "name": "click_send_code", + "description": "Code senden klicken", + "action": "click", + "target": "button[data-e2e='send-code-button']", + "wait_for": ["input[placeholder*='sechsstelligen Code']"], + "fuzzy_match": ["Code senden", "Send code", "Senden"] + }, + { + "name": "fill_verification_code", + "description": "Bestätigungscode eingeben", + "action": "fill", + "target": "input[placeholder*='sechsstelligen Code']", + "value": "{VERIFICATION_CODE}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"] + }, + { + "name": "click_continue", + "description": "Weiter klicken", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["input[placeholder='Benutzername']"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"] + } + ] + + # Formularschritte für Telefon + phone_form_steps = [ + { + "name": "select_country_code", + "description": "Ländervorwahl auswählen", + "action": "click", + "target": "div[role='combobox']", + "wait_for": ["div[role='option']"], + "fuzzy_match": None + }, + { + "name": "select_country_option", + "description": "Land auswählen", + "action": "select_option", + "target": "div[role='option']", + "value": "{COUNTRY_NAME}", + "wait_for": [], + "fuzzy_match": None + }, + { + "name": "fill_phone", + "description": "Telefonnummer eingeben", + "action": "fill", + "target": "input[placeholder='Telefonnummer']", + "value": "{PHONE}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["phone"] + }, + { + "name": "click_send_code", + "description": "Code senden klicken", + "action": "click", + "target": "button[data-e2e='send-code-button']", + "wait_for": ["input[placeholder*='sechsstelligen Code']"], + "fuzzy_match": ["Code senden", "Send code", "Senden"] + }, + { + "name": "fill_verification_code", + "description": "Bestätigungscode eingeben", + "action": "fill", + "target": "input[placeholder*='sechsstelligen Code']", + "value": "{VERIFICATION_CODE}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"] + }, + { + "name": "click_continue", + "description": "Weiter klicken", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["input[placeholder='Benutzername']"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"] + } + ] + + # Benutzername-Schritte + username_steps = [ + { + "name": "fill_username", + "description": "Benutzernamen eingeben", + "action": "fill", + "target": "input[placeholder='Benutzername']", + "value": "{USERNAME}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["username"] + }, + { + "name": "click_register", + "description": "Registrieren klicken", + "action": "click", + "target": "button:contains('Registrieren')", + "wait_for": ["a[href='/foryou']", "button:contains('Überspringen')"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["submit"] + }, + { + "name": "handle_skip_option", + "description": "Optional: Überspringen klicken", + "action": "click", + "target": "button:contains('Überspringen')", + "optional": True, + "wait_for": ["a[href='/foryou']"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["skip"] + } + ] + + # Vollständigen Workflow zusammenstellen + if registration_method == "email": + workflow = common_steps + method_steps + birthday_steps + email_form_steps + username_steps + else: # phone + workflow = common_steps + method_steps + birthday_steps + phone_form_steps + username_steps + + return workflow + + @staticmethod + def get_login_workflow() -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die TikTok-Anmeldung zurück. + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + login_steps = [ + { + "name": "navigate_to_login", + "description": "Zur TikTok-Startseite navigieren", + "url": "https://www.tiktok.com", + "wait_for": ["button#header-login-button", "button#top-right-action-bar-login-button"], + "fuzzy_match": None + }, + { + "name": "click_login_button", + "description": "Anmelden-Button klicken", + "action": "click", + "target": "button#top-right-action-bar-login-button", + "wait_for": ["div[role='dialog']"], + "fuzzy_match": ["Anmelden", "Sign in", "Log in"] + }, + { + "name": "click_phone_email_button", + "description": "Telefon/E-Mail-Option auswählen", + "action": "click", + "target": "div[data-e2e='channel-item']", + "wait_for": ["input[type='text']"], + "fuzzy_match": ["Telefon-Nr./E-Mail/Anmeldename", "Phone or Email"] + }, + { + "name": "fill_login_field", + "description": "Benutzername/E-Mail/Telefon eingeben", + "action": "fill", + "target": "input[type='text']", + "value": "{USERNAME_OR_EMAIL}", + "fuzzy_match": ["Email", "Benutzername", "Telefon"] + }, + { + "name": "fill_password", + "description": "Passwort eingeben", + "action": "fill", + "target": "input[type='password']", + "value": "{PASSWORD}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["password"] + }, + { + "name": "click_login", + "description": "Anmelden klicken", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["a[href='/foryou']"], + "fuzzy_match": ["Anmelden", "Log in", "Login"] + } + ] + + return login_steps + + @staticmethod + def get_verification_workflow() -> List[Dict[str, Any]]: + """ + Gibt den Workflow für die TikTok-Verifizierung zurück. + + Returns: + List[Dict[str, Any]]: Liste von Workflow-Schritten + """ + verification_steps = [ + { + "name": "fill_verification_code", + "description": "Bestätigungscode eingeben", + "action": "fill", + "target": "input[placeholder*='sechsstelligen Code']", + "value": "{VERIFICATION_CODE}", + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["code"] + }, + { + "name": "click_continue", + "description": "Weiter klicken", + "action": "click", + "target": "button[type='submit']", + "wait_for": ["input[placeholder='Benutzername']", "a[href='/foryou']"], + "fuzzy_match": TikTokWorkflow.TEXT_ALTERNATIVES["next"] + } + ] + + return verification_steps + + @staticmethod + def identify_current_step(page_title: str, page_url: str, visible_elements: List[str]) -> str: + """ + Identifiziert den aktuellen Schritt basierend auf dem Seitentitel, der URL und sichtbaren Elementen. + + Args: + page_title: Titel der Seite + page_url: URL der Seite + visible_elements: Liste sichtbarer Elemente (Selektoren) + + Returns: + str: Name des identifizierten Schritts + """ + # Auf der Startseite + if "tiktok.com" in page_url and not "/signup" in page_url and not "/login" in page_url: + return "navigate_to_signup" + + # Anmelde-/Registrierungsauswahl + if "signup" in page_url or "login" in page_url: + if any("channel-item" in element for element in visible_elements): + return "click_phone_email_button" + + # Geburtsdatum + if "Monat" in page_title or "Month" in page_title or any("Geburtsdatum" in element for element in visible_elements): + return "select_birth_month" + + # E-Mail-/Telefon-Eingabe + if any("E-Mail-Adresse" in element for element in visible_elements): + return "fill_email" + if any("Telefonnummer" in element for element in visible_elements): + return "fill_phone" + + # Bestätigungscode + if any("sechsstelligen Code" in element for element in visible_elements): + return "fill_verification_code" + + # Benutzernamen-Erstellung + if any("Benutzername" in element for element in visible_elements): + return "fill_username" + + # Erfolgreiche Anmeldung + if "foryou" in page_url or any("Für dich" in element for element in visible_elements): + return "logged_in" + + return "unknown" \ No newline at end of file diff --git a/social_networks/twitter/__init__.py b/social_networks/twitter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_automation.py b/social_networks/twitter/twitter_automation.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_login.py b/social_networks/twitter/twitter_login.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_registration.py b/social_networks/twitter/twitter_registration.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_selectors.py b/social_networks/twitter/twitter_selectors.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_ui_helper.py b/social_networks/twitter/twitter_ui_helper.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_utils.py b/social_networks/twitter/twitter_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_verification.py b/social_networks/twitter/twitter_verification.py new file mode 100644 index 0000000..e69de29 diff --git a/social_networks/twitter/twitter_workflow.py b/social_networks/twitter/twitter_workflow.py new file mode 100644 index 0000000..e69de29 diff --git a/testcases/imap_test.py b/testcases/imap_test.py new file mode 100644 index 0000000..2137f70 --- /dev/null +++ b/testcases/imap_test.py @@ -0,0 +1,679 @@ +#!/usr/bin/env python3 +# Path: testcase/imap_test.py + +""" +IMAP-Verbindungstest für Social Media Account Generator. +Dieses eigenständige Skript testet die IMAP-Verbindungsdaten aus der Konfigurationsdatei +unter Verwendung der Logik aus email_handler.py. +""" + +import imaplib +import email +import json +import os +import sys +import time +import re +from email.header import decode_header +from datetime import datetime, timedelta +from typing import Dict, List, Any, Optional, Tuple, Union + +# Füge das Hauptverzeichnis zum Pythonpfad hinzu, um Zugriff auf die Konfigurationsdateien zu erhalten +# Der Pfad wird relativ zum Skriptverzeichnis aufgelöst +script_dir = os.path.dirname(os.path.abspath(__file__)) +base_dir = os.path.dirname(script_dir) # Gehe eine Ebene hoch zum Hauptverzeichnis +sys.path.insert(0, base_dir) + + +class EmailTester: + """ + Tester für IMAP-Email-Funktionalität unter Verwendung der Logik aus email_handler.py. + """ + + CONFIG_FILE = os.path.join(base_dir, "config", "email_config.json") + + def __init__(self): + """Initialisiert den EmailTester und lädt die Konfiguration.""" + self.config = self.load_config() + + # Typische Betreffzeilen für Verifizierungs-E-Mails nach Plattform + self.verification_subjects = { + "instagram": [ + "Bestätige deine E-Mail-Adresse", + "Bestätigungscode für Instagram", + "Dein Instagram-Code", + "Bestätige deinen Instagram-Account", + "Verify your email address", + "Instagram Verification Code", + "Your Instagram Code", + "Verify your Instagram account", + "Instagram-Bestätigungscode", + "Instagram security code" + ], + "facebook": [ + "Bestätigungscode für Facebook", + "Facebook-Bestätigungscode", + "Dein Facebook-Code", + "Facebook Verification Code", + "Your Facebook Code" + ], + "twitter": [ + "Bestätige dein Twitter-Konto", + "Twitter-Bestätigungscode", + "Verify your Twitter account", + "Twitter Verification Code" + ], + "tiktok": [ + "TikTok-Bestätigungscode", + "Bestätige dein TikTok-Konto", + "TikTok Verification Code", + "Verify your TikTok account" + ], + "default": [ + "Bestätigungscode", + "Verification Code", + "Account Verification", + "Konto-Bestätigung", + "Security Code", + "Sicherheitscode" + ] + } + + print(f"EmailTester initialisiert, suche nach Konfiguration in: {self.CONFIG_FILE}") + + def load_config(self) -> Dict[str, Any]: + """ + Lädt die E-Mail-Konfiguration aus der Konfigurationsdatei. + + Returns: + Dict[str, Any]: Die geladene Konfiguration oder Standardwerte + """ + default_config = { + "imap_server": "imap.ionos.de", + "imap_port": 993, + "imap_user": "info@z5m7q9dk3ah2v1plx6ju.com", + "imap_pass": "cz&ie.O9$!:!tYY@" + } + + try: + if os.path.exists(self.CONFIG_FILE): + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + config = json.load(f) + + print(f"E-Mail-Konfiguration geladen aus: {self.CONFIG_FILE}") + return config + else: + print(f"Konfigurationsdatei nicht gefunden: {self.CONFIG_FILE}") + print("Verwende Standardkonfiguration:") + print(json.dumps(default_config, indent=2)) + return default_config + + except Exception as e: + print(f"Fehler beim Laden der E-Mail-Konfiguration: {e}") + print("Verwende Standardkonfiguration.") + return default_config + + def test_connection(self) -> Dict[str, Any]: + """ + Testet die Verbindung zum IMAP-Server. + + Returns: + Dict[str, Any]: Ergebnis des Tests + """ + try: + print(f"Teste Verbindung zu {self.config['imap_server']}:{self.config['imap_port']}") + print(f"Benutzer: {self.config['imap_user']}") + + # SSL-Verbindung zum IMAP-Server herstellen + mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) + + # Anmelden + mail.login(self.config["imap_user"], self.config["imap_pass"]) + + # Verfügbare Postfächer auflisten + status, mailboxes = mail.list() + + if status == 'OK': + print("\nVerfügbare Postfächer:") + mailbox_count = 0 + + for mailbox in mailboxes: + if isinstance(mailbox, bytes): + try: + # Decode von bytes zu string + decoded_mailbox = mailbox.decode('utf-8') + print(f" - {decoded_mailbox}") + mailbox_count += 1 + except: + print(f" - [Nicht decodierbar: {mailbox}]") + else: + print(f" - {mailbox}") + mailbox_count += 1 + + # INBOX auswählen + mail.select("INBOX") + + # Abmelden + mail.logout() + + print(f"\nVerbindungstest erfolgreich! {mailbox_count} Postfächer gefunden.") + return { + "success": True, + "server": self.config["imap_server"], + "port": self.config["imap_port"], + "mailbox_count": mailbox_count + } + else: + print(f"Fehler beim Abrufen der Postfächer: {status}") + mail.logout() + return { + "success": False, + "error": f"Fehler beim Abrufen der Postfächer: {status}" + } + except imaplib.IMAP4.error as e: + print(f"IMAP-Fehler: {e}") + return { + "success": False, + "error": f"IMAP-Fehler: {e}" + } + except Exception as e: + print(f"Allgemeiner Fehler: {e}") + return { + "success": False, + "error": f"Allgemeiner Fehler: {e}" + } + + def _extract_email_from_addr(self, addr_str: str) -> str: + """ + Extrahiert die E-Mail-Adresse aus einem Adressstring im Format 'Name '. + + Args: + addr_str: Adressstring + + Returns: + str: Die extrahierte E-Mail-Adresse oder der ursprüngliche String + """ + # Regulärer Ausdruck für die Extraktion der E-Mail-Adresse + email_pattern = r'?' + match = re.search(email_pattern, addr_str) + + if match: + return match.group(1).lower() + + return addr_str.lower() + + def search_emails(self, search_criteria: str = "ALL", max_emails: int = 5) -> List[Dict[str, Any]]: + """ + Sucht nach E-Mails mit den angegebenen Kriterien. + + Args: + search_criteria: IMAP-Suchkriterien + max_emails: Maximale Anzahl der abzurufenden E-Mails + + Returns: + List[Dict[str, Any]]: Liste der gefundenen E-Mails + """ + try: + print(f"Suche E-Mails mit Kriterien: {search_criteria}") + print(f"Maximale Anzahl: {max_emails}") + + # Verbindung zum IMAP-Server herstellen + mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) + + # Anmelden + mail.login(self.config["imap_user"], self.config["imap_pass"]) + + # INBOX auswählen + mail.select("INBOX") + + # Nach E-Mails suchen + status, data = mail.search(None, search_criteria) + + emails = [] + + if status == 'OK': + # E-Mail-IDs abrufen + email_ids = data[0].split() + + if not email_ids: + print("Keine E-Mails gefunden.") + mail.logout() + return [] + + # Newest emails first + email_ids = list(reversed(email_ids)) + + # Begrenze die Anzahl der abzurufenden E-Mails + if max_emails > 0: + email_ids = email_ids[:max_emails] + + print(f"Gefunden: {len(email_ids)} E-Mails. Abrufen der Details...") + + for i, email_id in enumerate(email_ids): + # E-Mail abrufen + status, data = mail.fetch(email_id, '(RFC822)') + + if status == 'OK': + print(f"Verarbeite E-Mail {i+1}/{len(email_ids)}...") + + # E-Mail-Inhalt parsen + raw_email = data[0][1] + msg = email.message_from_bytes(raw_email) + + # Betreff decodieren + subject = decode_header(msg.get("Subject", ""))[0] + if isinstance(subject[0], bytes): + subject = subject[0].decode(subject[1] or 'utf-8', errors='replace') + else: + subject = subject[0] + + # Absender decodieren + from_addr = decode_header(msg.get("From", ""))[0] + if isinstance(from_addr[0], bytes): + from_addr = from_addr[0].decode(from_addr[1] or 'utf-8', errors='replace') + else: + from_addr = from_addr[0] + + # Empfänger decodieren + to_addr = decode_header(msg.get("To", ""))[0] + if isinstance(to_addr[0], bytes): + to_addr = to_addr[0].decode(to_addr[1] or 'utf-8', errors='replace') + else: + to_addr = to_addr[0] + + # Extrahiere E-Mail-Adresse aus dem To-Feld + to_email = self._extract_email_from_addr(to_addr) + + # Datum decodieren + date = msg.get("Date", "") + + # E-Mail-Text extrahieren + body = "" + + if msg.is_multipart(): + for part in msg.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + + if "attachment" not in content_disposition: + if content_type == "text/plain": + try: + # Textinhalt decodieren + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='replace') + break + except: + body = "[Fehler beim Decodieren des Inhalts]" + elif content_type == "text/html" and not body: + try: + # HTML-Inhalt decodieren + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='replace') + except: + body = "[Fehler beim Decodieren des HTML-Inhalts]" + else: + try: + # Einzel-Teil-E-Mail decodieren + charset = msg.get_content_charset() or 'utf-8' + body = msg.get_payload(decode=True).decode(charset, errors='replace') + except: + body = "[Fehler beim Decodieren des Inhalts]" + + # E-Mail-Informationen speichern + email_info = { + "id": email_id.decode(), + "subject": subject, + "from": from_addr, + "to": to_addr, + "to_email": to_email, + "date": date, + "body": body + } + + # Nach Bestätigungscode im Inhalt suchen + verification_code = self._extract_verification_code(body) + if verification_code: + email_info["verification_code"] = verification_code + + emails.append(email_info) + + # Abmelden + mail.logout() + + print(f"Insgesamt {len(emails)} E-Mails verarbeitet") + return emails + + except Exception as e: + print(f"Fehler beim Suchen nach E-Mails: {e}") + return [] + + def _is_subject_relevant(self, subject: str, platform: str) -> bool: + """ + Prüft, ob der Betreff relevant für eine Verifizierungs-E-Mail der angegebenen Plattform ist. + + Args: + subject: Betreff der E-Mail + platform: Plattform (instagram, facebook, twitter, etc.) + + Returns: + bool: True, wenn der Betreff relevant ist, False sonst + """ + # Betreffzeilen für die angegebene Plattform und Standard + subject_patterns = self.verification_subjects.get(platform.lower(), []) + subject_patterns += self.verification_subjects["default"] + + # Prüfe auf exakte Übereinstimmung (schneller) + for pattern in subject_patterns: + if pattern.lower() in subject.lower(): + print(f"Relevanter Betreff gefunden (exakte Übereinstimmung): {subject}") + return True + + return False + + def _extract_verification_code(self, text: str, platform: str = "instagram") -> Optional[str]: + """ + Extrahiert einen Bestätigungscode aus einem Text. + + Args: + text: Zu durchsuchender Text + platform: Plattform (instagram, facebook, twitter, etc.) + + Returns: + Optional[str]: Der gefundene Bestätigungscode oder None + """ + # Plattformspezifische Muster für Bestätigungscodes + patterns = { + "instagram": [ + r"Dein Code ist (\d{6})", + r"Your code is (\d{6})", + r"Bestätigungscode: (\d{6})", + r"Confirmation code: (\d{6})", + r"(\d{6}) ist dein Instagram-Code", + r"(\d{6}) is your Instagram code", + r"Instagram-Code: (\d{6})", + r"Instagram code: (\d{6})", + r"Instagram: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "facebook": [ + r"Dein Code ist (\d{5})", + r"Your code is (\d{5})", + r"Bestätigungscode: (\d{5})", + r"Confirmation code: (\d{5})", + r"Facebook-Code: (\d{5})", + r"Facebook code: (\d{5})", + r"Facebook: (\d{5})", + r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern + ], + "twitter": [ + r"Code: (\d{6})", + r"Verification code: (\d{6})", + r"Twitter-Code: (\d{6})", + r"Twitter code: (\d{6})", + r"Twitter: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "tiktok": [ + r"TikTok-Code: (\d{6})", + r"TikTok code: (\d{6})", + r"TikTok: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "default": [ + r"Code[:\s]*(\d{4,8})", + r"[Vv]erification [Cc]ode[:\s]*(\d{4,8})", + r"[Bb]estätigungscode[:\s]*(\d{4,8})", + r"(\d{4,8}) is your code", + r"(\d{4,8}) ist dein Code", + r"[^\d](\d{6})[^\d]", # 6-stellige Zahl umgeben von Nicht-Ziffern + r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern + ] + } + + # Plattformspezifische Muster verwenden + platform_patterns = patterns.get(platform.lower(), []) + + # Alle Muster dieser Plattform durchsuchen + for pattern in platform_patterns: + match = re.search(pattern, text) + if match: + code = match.group(1) + print(f"Code gefunden mit Muster '{pattern}': {code}") + return code + + # Wenn keine plattformspezifischen Muster gefunden wurden, Default-Muster verwenden + for pattern in patterns["default"]: + match = re.search(pattern, text) + if match: + code = match.group(1) + print(f"Code gefunden mit Default-Muster '{pattern}': {code}") + return code + + # Generische Suche nach Zahlen (für die jeweilige Plattform typische Länge) + code_length = 6 # Standard + if platform.lower() == "facebook": + code_length = 5 + + # Suche nach alleinstehenden Zahlen der richtigen Länge + generic_pattern = r"\b(\d{" + str(code_length) + r"})\b" + matches = re.findall(generic_pattern, text) + + if matches: + # Nehme die erste gefundene Zahl + code = matches[0] + print(f"Code gefunden mit generischem Muster: {code}") + return code + + return None + + def find_verification_codes(self, target_email: Optional[str] = None, platform: str = "instagram", + max_emails: int = 10) -> List[Dict[str, Any]]: + """ + Sucht nach Bestätigungscodes in E-Mails. + + Args: + target_email: Ziel-E-Mail-Adresse oder None für alle + platform: Plattform (instagram, facebook, twitter, etc.) + max_emails: Maximale Anzahl der zu durchsuchenden E-Mails + + Returns: + List[Dict[str, Any]]: Liste der gefundenen E-Mails mit Bestätigungscodes + """ + # Letzter Tag als Suchkriterium + today = datetime.now() + yesterday = today - timedelta(days=1) + date_str = yesterday.strftime("%d-%b-%Y") + + search_criteria = f'(SINCE "{date_str}")' + + if target_email: + search_criteria = f'(SINCE "{date_str}" TO "{target_email}")' + print(f"Suche nach E-Mails für {target_email} seit {date_str}") + else: + print(f"Suche nach allen E-Mails seit {date_str}") + + # Alle E-Mails abrufen + emails = self.search_emails(search_criteria, max_emails=max_emails) + + # Relevante E-Mails filtern + verification_emails = [] + + for email_info in emails: + # Extrahierte E-Mail-Adresse des Empfängers + to_email = email_info.get("to_email", "").lower() + + # Wenn eine bestimmte Ziel-E-Mail angegeben ist, prüfe auf Übereinstimmung + if target_email and target_email.lower() != to_email: + # Wenn Domain angegeben wurde, prüfe auf Domain-Übereinstimmung + if '@' in target_email and '@' in to_email: + target_domain = target_email.split('@')[1] + email_domain = to_email.split('@')[1] + if target_domain != email_domain: + continue + else: + continue + + # Betreff auf Relevanz prüfen + subject = email_info.get("subject", "") + if self._is_subject_relevant(subject, platform): + verification_emails.append(email_info) + + # Alternativ: In der E-Mail nach Bestätigungscodes suchen + elif "verification_code" in email_info or self._extract_verification_code(email_info.get("body", ""), platform): + verification_emails.append(email_info) + + print(f"Gefunden: {len(verification_emails)} E-Mails mit Bestätigungscodes oder relevanten Betreffs") + return verification_emails + + +def display_email(email_info, truncate_body=True): + """ + Zeigt eine E-Mail formatiert an. + + Args: + email_info: E-Mail-Informationen + truncate_body: Wenn True, wird der Body gekürzt + """ + print("\n" + "="*80) + print(f"ID: {email_info.get('id', 'N/A')}") + print(f"Von: {email_info.get('from', 'N/A')}") + print(f"An: {email_info.get('to', 'N/A')}") + print(f"Datum: {email_info.get('date', 'N/A')}") + print(f"Betreff: {email_info.get('subject', 'N/A')}") + + # Bestätigungscode anzeigen, falls vorhanden + verification_code = email_info.get("verification_code") + if verification_code: + print(f"\n>>> BESTÄTIGUNGSCODE GEFUNDEN: {verification_code} <<<\n") + + # Body anzeigen + print("\nInhalt:") + print("-"*80) + body = email_info.get("body", "") + if truncate_body and len(body) > 500: + print(body[:500] + "...\n[Gekürzt - Vollständigen Inhalt mit Option 2 anzeigen]") + else: + print(body) + print("="*80) + + +def main(): + """Hauptfunktion des Skripts.""" + print("="*80) + print("IMAP-Verbindungs- und E-Mail-Test") + print("="*80) + + # EmailTester initialisieren + tester = EmailTester() + + # Verbindung testen + connection_result = tester.test_connection() + + if not connection_result["success"]: + print("\nVerbindungstest fehlgeschlagen!") + print(f"Fehler: {connection_result.get('error', 'Unbekannter Fehler')}") + sys.exit(1) + + # Interaktives Menü + while True: + print("\n" + "="*80) + print("MENU") + print("="*80) + print("1. Letzte E-Mails anzeigen") + print("2. Vollständigen Inhalt einer E-Mail anzeigen") + print("3. Nach Bestätigungscodes suchen") + print("4. Bestätigungscodes für eine bestimmte E-Mail-Adresse suchen") + print("5. Verbindungstest erneut durchführen") + print("0. Beenden") + print("="*80) + + choice = input("Wählen Sie eine Option: ") + + if choice == "1": + # Letzte E-Mails anzeigen + max_emails = int(input("Anzahl der anzuzeigenden E-Mails (Standard: 5): ") or "5") + emails = tester.search_emails(max_emails=max_emails) + + if emails: + print(f"\n{len(emails)} E-Mails gefunden:") + for email_info in emails: + display_email(email_info) + else: + print("Keine E-Mails gefunden.") + + elif choice == "2": + # Vollständigen Inhalt einer E-Mail anzeigen + email_id = input("Geben Sie die E-Mail-ID ein: ") + if not email_id: + print("Keine ID angegeben.") + continue + + # Suche nach der E-Mail mit der angegebenen ID + emails = tester.search_emails(search_criteria=f"(UID {email_id})", max_emails=1) + + if emails: + display_email(emails[0], truncate_body=False) + else: + print(f"Keine E-Mail mit ID {email_id} gefunden.") + + elif choice == "3": + # Nach Bestätigungscodes in allen E-Mails suchen + platform = input("Plattform (instagram, facebook, twitter, tiktok, default): ") or "instagram" + max_emails = int(input("Maximale Anzahl zu durchsuchender E-Mails (Standard: 10): ") or "10") + + verification_emails = tester.find_verification_codes(platform=platform, max_emails=max_emails) + + if verification_emails: + print(f"\n{len(verification_emails)} E-Mails mit Bestätigungscodes oder relevanten Betreffs gefunden:") + for email_info in verification_emails: + display_email(email_info) + else: + print("Keine relevanten E-Mails mit Bestätigungscodes gefunden.") + + elif choice == "4": + # Nach Bestätigungscodes für eine bestimmte E-Mail-Adresse suchen + email_address = input("E-Mail-Adresse: ") + if not email_address: + print("Keine E-Mail-Adresse angegeben.") + continue + + platform = input("Plattform (instagram, facebook, twitter, tiktok, default): ") or "instagram" + max_emails = int(input("Maximale Anzahl zu durchsuchender E-Mails (Standard: 10): ") or "10") + + verification_emails = tester.find_verification_codes( + target_email=email_address, + platform=platform, + max_emails=max_emails + ) + + if verification_emails: + print(f"\n{len(verification_emails)} E-Mails mit Bestätigungscodes für {email_address} gefunden:") + for email_info in verification_emails: + display_email(email_info) + else: + print(f"Keine relevanten E-Mails mit Bestätigungscodes für {email_address} gefunden.") + + elif choice == "5": + # Verbindungstest erneut durchführen + connection_result = tester.test_connection() + + if not connection_result["success"]: + print("\nVerbindungstest fehlgeschlagen!") + print(f"Fehler: {connection_result.get('error', 'Unbekannter Fehler')}") + + elif choice == "0": + # Beenden + print("Programm wird beendet.") + break + + else: + print("Ungültige Eingabe. Bitte versuchen Sie es erneut.") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nProgramm durch Benutzer abgebrochen. Auf Wiedersehen!") + except Exception as e: + print(f"\nUnerwarteter Fehler: {e}") \ No newline at end of file diff --git a/updates/__init__.py b/updates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/updates/__pycache__/__init__.cpython-310.pyc b/updates/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1e1cea4b7e71b535e6a2bd349c19513448aa6a1 GIT binary patch literal 145 zcmd1j<>g`kf}c?V86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6wn*(xTqIJKxa zCL=X3r6|TFwK%&ZzaXY0wYWq#H!(9WrnDd>5y*>)kI&4@EQycTE2zB1VUwGmQks)$ N2Qsag2}rOo0063zA!Yyo literal 0 HcmV?d00001 diff --git a/updates/__pycache__/__init__.cpython-313.pyc b/updates/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8feca492812f148a3708b799cdf88171fc2d70dd GIT binary patch literal 144 zcmey&%ge<81pH6>P{wB#AY&>+I)f&o-%5reCLr%KNa~h;fmMuiMrLke zW>I2{bAC#yZa_|AWqDC%dPYf1X+cV2NosLSe0*kJW=VX!UP0w84x8Nkl+v73yCPPg TevtXaAjU^#Mn=XWW*`dyhEE}I literal 0 HcmV?d00001 diff --git a/updates/__pycache__/update_checker.cpython-310.pyc b/updates/__pycache__/update_checker.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f72afb7c44de2a52abe5b773bfa7e910848118f5 GIT binary patch literal 11499 zcmc&)%WoV3gn)P8H$BK25%cg@I-WlQur*2=OdS=m`Rli^fxIOI$Z zUENKZjiKR%dXQ=xcxgJ{v)@>-8$Xulm86;czW) z76_%OuBxu*SKs6Jebv2*iGqT^U)64`9$8S7|DeL)pN_(7_<24OrqmRsGOeT5RQc3u zntbXtT|SMPA)jW=#8dBBU8`oP)W_)Lx^~U(=4<(Gp;qV?YsGGEh-0c9rY%ygNr5&Huz_w@EU&JqN*rd z0i6`0QBCK@nz?H+jp=(@Eys+S%}i!}qSW#%#|ym3ODo!OWkq8)%YS0jCQw^OslbXT zO|lX%vI+chd}_sFWj2Z0X?B54^YT!CgH_m(Pt00{9YyP7vd8}D7Mo#@q34lg^jUTs zrK2bnG1m$9I7-KoUgZ^oon%j-W`>=T<510M=Ah=Wq{ox&DU@c}({dH`{u%ZxYK|x4 zKgXU&=>$8Iy|1$Gq2}@A{R`|YN+;Re$T|%+&(7h^6YM-V;9@ippT0bNE`lF`z;AaN zp2wXq=sFKRTjpXzJh;Dt;}2Zs{1|OqpNmd|7-F;MI&pt*KC+rUcctA5ry3g@^-V53 z9P0d<*K@VCEu1^o*l5r54;x*a`h2t3Jr|$2*TjKlf0Q@ydjmi3MI?P?SJ^{a zSAA_w$IsX``^uWNCk5qGO;xC#Nim@kGq5o;xP-26xosaD+xFUA_>zT(b}M((dGJey zcWv&xE!Q7_nQ7&>h>!V)VB30cJ<26Zy&^P&Ti33Bp+<(sJ1bu(;xR1u*9&hf+`a9Abaz*| z%f#I)+*|j18+U!~`E%Vy+r67CBYCE>GQR<_s$=rDU#}k{!dXZP+N@sD!V}^r{;N0&Us+RkHIQ51Q@tCcfk0@3b7pX#b|M`0xcCwfoj`|6T%LGk9cR8^sWKgI8kLbZnC=eFNbl^wJTCw+T2 z&nEi%FO<)JiC?*D@A+FBS`rm_~FZ zqd|-sfd}Rk!wl$=4sFke=7st5^XDTavOK@B+~Kztt7bIW420mWpK@VTfXw)9 zuhrrrGPw{v5gC#khLmEx#r>)&W-vyr#P~`h==iZjt1FSp#UxEff*L2Ylq(}Sji0xO zq^#jjjsIC+Yx#;=QA-aEv+}iJmcKcn8k+sF_KjwuRz3M}R=2dWI;HOSseFz1zYR|h zOY8x4GbFMEBQlZw3lzet4{f<7wIkCZl+!y>bE3}Z11>N#5lbr-ODP&p42cFA)Lblh z%ua;i3WNdr6~bxmLfreAz)0b=cC?Qm^k9FPBjKzT!#_^BCRVXykJ4#_rAGB{!v5kZ z>_|LK$upEZN6GU@u7^*KAFRc_gi-xl;e|ntN|zEd@Yvo9&SEt$&e9~rNn(zYc}nEj zpQqgSDLF^U1te8n@{4$hD#&_{Fi14*H8y!Fr;?klpp{2VrfC)RC>WujTPiU^Ssi!$ z2l_|$W~@doWVwq?VuSRLlz(PI{|*JxR-Zu2?kd1Y>J{alxa>A6!&m$IJ(Zd3 zig>oK?`jN27Ungp^-dsfBrSdb&AHzqsr6p)b!Ohf9PiKWn0>u(9#{GXr5N)~80XxQ zGE{4bo2UhgeXzhR818)IC77Lpll3mz7uZocBTtFUqE4b&KZw)L5=Es)+5VY z7A{_X253e2qdZ)=1PtW^v2DEiK8;6%A63GO!+MI|H$=wk-u$Ibui5B$uOgFnkJR}? zmnq;SCF1i(lzSyBN}Vg!?Q5v}GpfIgNtuXj7=@S?%{%3ra+tK=} z_b$EJh2-w3;@$9+ukGrxE#wY32%ve_*i&{4NTNQg?3#N}(vZiOd}}ustNFf0nCFh- zo&XNBw{Jkzn^5&-w8%5NZ>HEts`~tn`H|_GOg<$Fdz}?_ECBUFy28G-r?KL?vXkrQ z#5OAd4^CWC`WBw$OA1hz#-{L`#0D4;QZi!$&c5YIO-HAP5 zMY-Z*G|F)WJ!U?KK2xE^62w+so811#WbN?B;&t!X=rz^1WlI}7Am|_8S%>=`cA!F?7MHG^?`#Ev@oasx&a5z2qmc_w0Rge? zblZNifw{K^L2Nd_qJ(wgaZmuhQem~>)dT6+$8fLQ0jvd7O>|>a61>A39&D8x7%MAjUl>9?KmbjG#);^%k7#1l9r9rAoRb zeu8ntbtGZo;`xg&&7H^p3*sGA1pkWUz1TW9vdcnY&bdvFwdX&$?}xMv8FpE0H2HF` zw?3DITRdmA?S?^%6X*Eogi)3`+;xF1-VnVHx94v5eCb%=27Z;n(#r}PTk`Obudp1 z!@($rDP2gVc$@l9-@b9>@~x}&rK>mJyLz*J`{uPMpA9E|NKI_PKL{WUUNm(x&Sh>& zj4kuAxw=M;;jVk|N2CAs||7UiWwQ}24MQS&t|Luz&GBoRfghh84pQLSjJ zcH*&Us`aa>q6X__t5dLPmTLUAte+%M`cTu2i5c==@DA-Y>)}+cqTA|>W@%HfdPv*cn1hGhzOO(M7h#fM6C!`rpMmqreIdz z*wc1QAFz|cobY_CzO`$>fKkhQY%CBIC=7W%^MjrbEB19(f`I^dC6ncoCsdaGC#5kk zq#4oE5W$+gy$*AcgRz`pQ~ewnN*Ek-H=h|ColRpr^pFOpu#@`;AQ%P{&n*=Or?CB( z*$9>IJ^~ERk>Gzo$4mvs@dlWNL{mHP+GW)4kxWb~#iVl=9Pq7-1P%xONEgO}(rq|l&WSr+za=EIM< z#uG7ZlW_F>$oL0b2h!AU(4c>SCm!6PW1V#ng#rp&1j^WlTvB6KRdE;Vt7`HjqGH0IGB!9u=p*eEMb#Ud zjdq6uf&WGOCKu1n*=qRYFyY3uA0i&PN*VFU+mJvzCEGeGGP1qdUXFt|jdec&NXYzV z=mp#iG}j-MT$e}sXER7-2x`a}3x%&o`RoV+bM=vs=R>sfXbXkh=;p_?9`dlrZOl9@ zGOl3(5o_`^ROxA`d+>y0&`aBX7OFW8dg(AEFhE0|kS>E>2GqIC$)J}R{zJC2CBmLE zxHU?=F+@8wU;$*wgN*Fi;NOnhi9sdsxhhtA9k>RP3mpV=V??;p=ybB-=Zvfg3xJHVgwDO)z|ATGS2CJ^7RTc^&caS^K3WVbiS5HMON|?kmQD5tW(geP zNzg4aXpxbQMFuBOzU{Ui-0%3(c^X^>qNnl{!so^^Q3@I2HIu)Dc#LG2+Zc3|lLncH zj&^KtwL**##CAQQ%yHr&GHrHYN;LpgM>r`VTxwws4sbwEl8z-K^A*MVNlk!zz(0F* zg{}ubM`cF;SXbh#_r+z9K;Knk4)q&;}ftak{iZ+Hsg<^B|dw>6LTg z2zd@R77jLisGNOu18>7p9ApHoKq)CtS|(5W_VbxM;U@RdRBCVxOU!l)L(<-qSGqky zY>b22Nd?^_&`lTJt--nEshI5Bet4{pq z*}}M+E&dNIqVhNbikFU+IVkP{YGgWSh#aKJzrcqYh%OAa4JHM@Gy2ZN!M~kxoB5EZ_P4u-@-6%ONPx z!DeoW4pK2rRF470?USa?U4nh=$Dpim(I>#yh8{=_H9vY5Pg#?LKBwF1ftKGYF!wP^b_@pu2I_P0Je`!_gU9)Q7nnn5N%f`_#Y2RI$hLU6 zkj$oRpVBUslVzfzl#}Pi%gJbm%mbt7!H~$%r)QbN@NX!{&p#*<1=yc_BBSC1nUjb~ zNmPkA6)r7&xD1q|OBlMO0LnazI($4stynION6G%Ekry>N+^pMue8@(ZHR5$#vF2GG zZ}7zvE?029gK>aBSpk?1FP+rNIaarN;RRed&_% z=(iilk?V+|dx$vtFI}$7E+YI4_I_*>YB9c|(Br^P7A6Xl2*jLRl;g^)XN243-9Q?x zDaH&1(Sc%m+ClID-j$$hYS5#_Xta`=K830u7K^X8A!vWG~|qMSftW365`tctbQ-BeNxX!G>s2 zhjU1(b{zHDq6!N5h-Dku}NLhl*^ z$Ot5VQ?kl1`h>qLc1bTGbQ}kUZ3K+V0|8?MiBAlR=ispTm~^pvioR{5t<~#Mp#xE{`W zHSpV=_}0Smw>RhwZBvlr5^s~f2oX6-NR>r-!1`Pl9+5Bn6Nnxuj@`5J^ZPsW_zdFrTvCGFOmkQw|0smmn-~LGJ>R zg=A8XGfkwl9aF9y$!(@#I%$Va+X>t0OxdZYl}snm^cP^#C9+ngo@pjC{s%`)o!HY! z&$+vc7gv&-N&a*x-Mzc_?zyjX&*M86_p7Vz6kNwY@y`9-6BPAd_@G^knz{cvWZs}S zN~AagXFO>T4diYVjpR;?G`TY(L+&Qg1b6zR`Gi?Cv-k~j(sIHoT2I(S+X=g9KT#!C zop6Yb6V+lhOI@VLcWO-*?`;%kI%rUyMVF6)XLAF^Sq>W2ckZY$U`?HXCwpjNCX(d4 zhv)d|OEDqZJ(dzKC1VL85|1Ucjn)VN7pO7b%!S3g-{z&L}b$P2-Nz>Pmjxwf!AOb4+0l ztei=-;k<};&T_HpqT^z9pV0&#^%*&91N8zUI^n$wo^0gF&Dk$HIH(72J$+`*LEhGI zZCv$5R|N%zb7JWW2GPs8;0rhTsSMTC@@5BD zM=0T&dh*7nezAi*`8gly3zqxIn+BDB19@uX8Vlt+$(x;Oc@ueR;+nVhl;K)PSu@9q zEz#D%F1cFa8NxEW_z_NY>I;yZj7KDiXVa;9cJ13!ytp8)y*&$@lM-gwr+5+gg%{%y z$iEbs2Zjkw zC+7Dl%py&zJWf^`myU-2_cM^ZL4~OC(o#}|q7JI}5EUEPwj*($_Z!(#y1MNGx2iRxOu&>n1jLUEu_SE5m=xp1B-xc^OF?Ej zzV^KtD2wszNiwe~*q=C~=W%QDFTqX>Coahrbqdb}EV3t_h|Fj+AC3vL3E82pgYsld zNV0W4atXG+D9Ox1BsnKrATLDbdAVx%?3uCS$HHUBPmR81kQs@O&q_GCtbFj?z{TOY z*nA`=MlKE~X87)N@yOLHVk|nByr_0Sy=xmkxBxOD49&)p;jmm^HoTzrz7D8MW01T| zeYlg|+}V@e*|Wa0?;5jV^>3LeTh*&o->iCl=;pB-$8Mgzadv5Bv%N3d-nU+R@HX?F zb+k}r@%S>cZtcDglqlIO^2wAal<|}W3J6dAElAEcs7b6yu5ne8YR`BdM-HsGfD?4q zR^x;Ywf4*rif9LvPn?M}2Tg*ZnVPg|#Pq3f7S77q`e`F27$=D)!uoog*G5g$aCWY$ zp9YQMz&}k2vQ)^>PJNEHoVsn*YCSC8+o+IH8=t^bwl?Y3YL4&Fa>YC6dWISI7oW8p z=jJ?GkDXL+fj;z}X{TDLE~+q-z<9H;w^}F`{%~xE=@x3%&`M1@HEI?rvWubwx}+~& zt*m(GszY>nTP-km0zF70RV6oTm@UwMP#?~ub$hV)bM&C5vIi&+iaa72wH|HnPrys$ z#i@cUxS+^Y33s? zDDpy56Bn`_gnm-ly|QI`E+PngT((D2$%Oi{n&f8^R|FI@vf;|7rc`pa`->neth_Lt zm;tNfBiv5#!WpH8fIk!FCu?RC|+2+#74-8HCVZ&TtIFU8W;WNjE7#nXYu){Em3gQ0+NpdEQ~f2wy*mG`_RWU9*@nH# z!>fDVs~fr zj?>#`qt^LL%3SUIEN_BOK9w%Pl&;ndH&b`2>xUimon2PU2kS;!sCVccBTo9Ah69ja zbJ&L)Eo=2Ol&&?BwAnsVMXz=EM$GiPW)tM!b=pUETHf_pp#Hlp_E9hWZlGn z&w;A+QB8~}#*yycW@8{@DlI2f9*U@A!~iBByeY8@lWs`Pq+9igMR`e;T*>q41EnHH z>xv@LbiW>8#{xF72OB}rBKBg^hY1P;5$$cU9}_fb#m6BD&_sZUXef$@A(7B@(d32f zks_CQO>8T|r9ygat1yB@u0rxMm1nFrzwX9%*WR?fWHG4;q1{wg*q$efsWj#(!?Z@$@~f4|W?Aek$Pvzfw22hq~E1SWmAu zLGDhyeX!kfhqYjSkA28X-#OqLf_>*SL0-0AR#e@ALat{TCqlcB_wo6Dj_je3LC;U% z3>x=(sSC7Z+($`9FZC4I5MVbHsD)^));Z&8uxzR*mJ%3&4jF&PG)>TI!MY4FTC1FK z0JJ~^_DYBuM_aYx;tKtP-TL6~L2jG|8YMLf=s`&frvF0=mEUVE6f9b3 zX#t=cnT`pN#K9afgI~=HGqOpH2vI&@QKZBWemo2b`g39uHp_u9X28?C+@t0UF$uW} zrv)xzcF1nyUsDX`niLe-L~txa!^5dwAV@BWi%*;Hp{d_@nt-k#%QF&Qbg7+3RO-4Q@D^x9CF2`_7i1 zIX$1F)|Qp3*|GTIw|v(IbFLlNzkF-r&B=9F$4_?na~}VV##=AF`Q`ULdw;US_c6$> zhCJ8~o?ri>m8w1ZYuS55LKXOW-EbT8uXV$F7}*5I^)x??YocAKQgNZ~TNzZ@oB`CV zR#tN72=w%@GBEs=KPG4qj3Gn8<@p9|e_fj6*lxvD1O~P5BT)(s3Fw`2={c;#IM6DT z+&L3&S4|v&kSbEaaaUYz1^7i^CWgCV?)1PD4Wi-9L=1qNI9UKwbV0omLnCBFXSSK* z?1CBQ1aX3jOMj-+*)JG3T-5-qp4Fmvu5=sBlc|IrS|=43XAPOQ8EQ7cG-x{W9~`+C4K<)%0JO)OD+s|OvPI83+6TFacHb$qtc^^AU*;Bd#D(8lF;E+<5sf*82mY$PtRJ$)>f1Spyp z0x8@HrS|x_h$sP}8D3%)T0%K7bOBQH(g1q~Q4zpJh}u0bUI04cGmxb1J^OnOcJGIO z2gC_@k@|N?E-1R6C7%q~yV(#z;8Jq!?PMB_7V&oar#p{0K`y9g4I)6g$G^`pT$AC_i3xO@3Jxt&*4yU!E~1? zG6gQs^jtgvy%z@96hh@CyqzN66Rh18Uc5A00DMTd_2>a`gMLm5ouUxLovwhn!o!5B zgD(tO|J#5^@tg+WYY{c0TsCc}VOm45LxEqf`kGZL5#~Dv}FS z9oS~|R3n>f*|piSKijfDb6_ajG7NywX7j#m^S;df!EEzTZfA3@sRh%z9Ur@_HTG-f z|E{Opb+>9a{d=?iy_^1E)*oE{%BKHN)_*8(v{hd;+-rJlv#IyJrrwp2&A!oW-{|dl zwvStHI=^VYSKoXy{#yJ`g+)4Vrs{mz+V)LvZ`Rwp?mf6_TK68lSKDy&6PjN4&t#%=S^wO% z6F+zPb9*`$&5K{jy4!P|{h1wmvhI#t;2^yEYSz6w&oFf-40)RJ>{$GRtb0%1Np&CC z>>A2;4P`oqGs9daa9*tnw&%Xbe-QtE{BMNK;8->|wjO-y+Q_D(DeGvu70)_4a}M{W zqb=)b+ihE;^WuP=DhwtIsMr^mrt#%xY)jP52>8D0ohNb6TldEst4Og+o*T? zhPuZ*)DPS2V}ML``NnMYk8CE$|HxDOl!5-yV=c$*^p6<>;2~h4J_|aghs80@^lit;#tTMRvB#=$WZ~Q)3mRNt9ND$faUH{~%sBcQ=OM-s z_|p(qunU$!3m8Y_K5Lk))#w!3sQF#DQ813YsTF9=t^_Z@yiu(X7D50dhFoS2X&MRb zhXEF3Fo{2xhnrEaD@s8s_2UOL=bkA3(h3i?iAGe5Wt&w*3?fFhNU7;*P|LDyHWuf> zl}^ryQ5X;r__8&n9CWyxW`gP1f{4H-ffmy~2#!wkbBQ>H9zcp~&OV#Au*yeg&Wkau z{sJUvtI}cia=NB4Gld98df%CCdUd`c2wyTQs%B01JwUBK^wU69NjQXgArTV@Y$=-{ z&}q-l6mXAZ;5WUNI};r@&TD&xk;HG z^^iiaqWCtFsWK47=c9a0Ie%kr2hnRb6Y90Rv(?t{VME)}<4b|%zKm}_h)iF@cPwvO zzTvr9$WV0+@7138+fae}CZPdxD5`<}->+p4V; z`p56K-t}gDrynB6i$ed*-52kI${Bx1WmV{7ME!ctkem9K`oW|09Zxr8?;N!cRa@S% zSs?$8yLN=4-)V0dw$W>}0rG3MYE0`}hM%C<4w5%dnBYxW&;kW8K|l~Of7HPY!2kgV z1NxrrRKS>uLuQ+oR3Z{UrD%>4*sUNK%0RgDVv1K$wg-OG#X_(Se55h(5qLYXvjPT@ zhjs%-!kcoHx??d?C*fc;D9jtv9G`IHawHbVAgSD1&UZRhehDO`CP={Oa#A}QH|x8y z^4U$2WgiL|EcGJaX zU2LYcC+q6XSbLSRtBM!7xr1 zLSnk602n3P5Dc1xni!7UiwH)*lup?>^hil4sdC=Sl$UA|fnUj|3M-@Il_Mh)_hwX_ zO~k>$C)~!@}2^Z2nyDfMPsnz|U~(hY-=GH<0xP*1eq>N9V7I z6e|r9joyyj<}!X1k|jYRG)`V=VyPwjpo9LtZ_q;Dv6vt$J_ZG4Xl#Lx?m-FcA&@ms zK$jxG*Cpm9a$3a&W~rf^1EOG_0v8rIsrUiMs9b0%zSDALTv)(J3tXsG(?v!(lMWXu z0Z)2cz|)fcLyZf80~DdueKG>)Iw9^s&U17DMd#pFiIx#}L#z5C;vdNT zm7TFDM1=u-FI7)a6E99vKPYA@uCk3^P&S2)Ul0X)ArbV6%Y_ekXb)6BsrR92JLvJ) zWpYdu&RyaOZdgC{1)gxgLs@tX4B$Cl+vjEYX_=lX9_Jch6{8By`09|5fE|!gNJG4! z=_x!BJW}WYFZ{N$ge+F-2^gNPZ~!G&GOB{mD;pMSy|>_8Iv}p`N^!AG_aSfS$P6b+ zgyNLQI~8Uxl^4a*$B`Ei-cvQ+y`8lp9WRbWDlHr@zm zfDkjT19)>bZA5|zOgJ>CE3Q!C=o*|C80YNBI{@+<6_6ii0Zu3&azQQ?3^&7%1oX7KAJ z-vsGRt5|F|X_OVni9CpJlVH3CJy$9F4TJ?q`&}&HdH{5YczO@U73Hxx7=MQpn@`2j ztPP~?mpdl+clQUM?+l1fVO!Xsf{>@%N`<-7S%QOT{v+`m9Yx$)Jb(lW1|T_ED_f`F z)B^d9lTqa1R}g$eoSu_SQ8AHP5YzYp8ubCI0%4>0Bfnb|1<1+ODz*~_D;vJ568J0H zi3p-46eJtXeiFikL_yRXa9S^ZRdspfhH@!XT6PH~&;P*ppe5=N!iE!T5SO(uh+pFw z_nN|S|4W;$y;;}Z9D8{6((3b>=97R#LwHb&7}hUY-fY=$1Rz{^?R1`Lv^8wG)wB7x zI+rdjJ)h|bWqcPl9M3@b)8B|F_@=KT>+4uw0tFVpR^DFn0$;U6&g-+y`^*`xdWe-K`(8?J6d&1VevTh*Tb0 zeR_2`<9qVapf&!&;f{02Fzlw*-1ZTMUTbI>K1#3k8!&&=1gUr#>cB5k#{w+=whUJk zep_Y_hvySBDTvU7Wk)#tVk#0>OI+dbEd0$7q)=Qbd6YDjHUOt)g^=Fv6UocivFkidNvxZAQSg{QVX&M0 zOMP1u-15FAlV{nRr{K0qliO{23!gqda@JsKSW4z8yst8=-0fYf6aNgqo51J%l(E;; zusocn;I_Jp+-|qumhKMUe(6qd3*Y8nFj!52rSn@9-14;+Q{NJ|MZqm!<1+b|d-D|B zR-hYj%eQ&q6_A13O8XYx@_Q|&fu&*mVnF$#R`~*o$rlz=|56f*`;}toDZF203dM() zp{m7nIA60Lx(b7X+X}P7t?pWx_)9oEmWz*RmaCqkvd)hV)v? zB|E#cqOIH(P#*e{_6MW}q#!SO?_+>I_@Vs^-M3zVB0r%9lJq;XT#}OFCVl7}m9}VoI1T{fvguXaIKRj27uAaA zvEq_=0cBOZh;>c1?w`aw|9-MAU#6Pt8|tlG>R*RWi+6<#+^)ZbdfV~BzTdW%tTjKp z_xMvUv<9<*>$keHUK+wXT8LdLn~J;(QjXU}$Azuxnlkl3yd_K#0CyPX~m zfvwW9AU354`_gX`|Kks^u3g0&7$`z*L#wd9i)PWn^QPMfE$#}-*$Vr@^F%5siwkLO zbX(!JBZU?9<>SwrTUGUqDsHLGf|}~kN2wrEPL@#}69LUB5s(K6wS9Jwh#^8(v6zTKgpTWZu8`qS3`YR-9ppf$c@M(4 zXaPCz*HU>NUMP`^9+w94s9#^d_I7MEyS<%}V3O7}c#O+qr>zr)U&g39ay}cxFS})+$EySH=8IM+TuKTz@30NVrXCu+{b2!g76@w7^x{qwxk{6j-n$;f25M$M{ZBnfm>RB~}PRPG_uae8qE+r`B+6j zmM{8n1}^kX@)}6Qy7H}hbZXYxxVVSvN_lV8EPGqHBZUkpRO6UJ8{Mu~vu?}2xM*z) z-yibwR30x{*TA!|g03HgYEcND1XwX7xea*^g*}}z^;m?4TiUCvXF ztgmA9|Ar6MDQ=GRq`bG^7H!cGBWtueE*v}5mOA#4u(k%lLwKaqZ_^@F)VF%H9h~#f za{gCE5tAu!4)1?lL$~Dwj(H6lnnW#UP{bKHKqXYDkHmOXA*Y~_<3aJ~pbW^u@)HIt zXAD-vYh>hk3>^v*WGAFOrJYqYAbIT}^21%^7-y6hTDQqAn6)=MD9C_2P z8e_|;ouk5Fs%9z4MY$`Kb)Z)ObCGT>N{S9oSS8B6<@gQ^Oi~&hvtmV0VvBLV>-rTv zHYoBS@5L6TCr{T(sFN>z6AogVDLE>RmSgYQLp~i)l zi^K*A&b>n>lmR($rj+zsG@?avX&eFU_vMEyWcPI#4N`iZ`6$D*cc3ls7)0D&7G@Edx+#Wng&gd=XUiG4Z@)DQ2&+A^Ds%(+GYrf zh3Jn;)*C$)0%ej?r7Ag`b0&cidR|3J<382;h9an0H(L%T93)?SXUnr1!iE2(aGcs0 zja(D=JR#L8G28xvkdFk-bYa|nxE@{jX08zmhiAhHZbvK0Id1Mo1{u36VA z`Pk_8gdZD9%Leavddj-xvtUBLgd#4ieb5vNT5(oT;QTBjD>T7cF%w7OHWN4xhQU$+ z=$D`k=1}Ox5<9^bzNVNt#mac|ztO*vuVd=zpxVc5v=VCf;f;5-112w!7ghk90>A(n zvg~&dR=J)+i!vvG>+CqV04Cf{LKE=8_bGhB{C?p8uv9QB?z}yK*$L21aAO3sl+L8A zODDL|O^0d)wru~;U{fjZ#^X=<)9yA-KTzq}{|jhG^V~Ou=Kw-OH-9+cwYus)5jsA1 zBuC&3#Q=hGJ$mIEFc(n9?^-@m$IxmET+pz1&5D-Bad(4u{BM}6lw!Rb#0L42*c2Zi z-UV_A;*(VrvEg+co}iENz66XXs70BVT%m$u^)cMhcWLNXxceQb0!p$7cO{l*i(i*B zMTNZC1bIKgWSpMfJj>?+lxscp$v7en$j>(*_}Ya8^ym zW|EIaGe>V+wP-%Q3Ln-$4X)mbbck?nNH88OYL%?a(OD@DM(T!(`|3&+SKTOkN!@p6 zhabXcCuzN?MW<@kx=Ii!IUHmg*k$UX_0}-apk3)YcSvT+*+*{-7d`AJk9eGK{u6go`2&)3jFXaHmHd@^zA;h@{*&E+AOlP+55b0m?S);*~qs zZ>-i=FW+9XOG$hE&d;u_-5TT}xL&C{rizS`G@j(+2IZzILs8jkUQIx{=7 ja56t{=%8WdS0yvgO!(3{mIt=;8CnH3TR Dict[str, Any]: + """Lädt die Versionsinformationen aus der Konfigurationsdatei.""" + if not os.path.exists(self.CONFIG_FILE): + default_info = { + "current_version": "1.0.0", + "last_check": "", + "channel": "stable", + "auto_check": True, + "auto_download": False + } + + # Standardwerte speichern + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(default_info, f, indent=2) + except Exception as e: + logger.error(f"Fehler beim Speichern der Standardversionsinformationen: {e}") + + return default_info + + try: + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + version_info = json.load(f) + + logger.info(f"Versionsinformationen geladen: {version_info.get('current_version', 'unbekannt')}") + + return version_info + except Exception as e: + logger.error(f"Fehler beim Laden der Versionsinformationen: {e}") + return { + "current_version": "1.0.0", + "last_check": "", + "channel": "stable", + "auto_check": True, + "auto_download": False + } + + def save_version_info(self) -> bool: + """Speichert die Versionsinformationen in die Konfigurationsdatei.""" + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.version_info, f, indent=2) + + logger.info("Versionsinformationen gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Versionsinformationen: {e}") + return False + + def compare_versions(self, version1: str, version2: str) -> int: + """ + Vergleicht zwei Versionsstrings (semver). + + Args: + version1: Erste Version + version2: Zweite Version + + Returns: + -1, wenn version1 < version2 + 0, wenn version1 == version2 + 1, wenn version1 > version2 + """ + v1_parts = [int(part) for part in version1.split(".")] + v2_parts = [int(part) for part in version2.split(".")] + + # Fülle fehlende Teile mit Nullen auf + while len(v1_parts) < 3: + v1_parts.append(0) + while len(v2_parts) < 3: + v2_parts.append(0) + + # Vergleiche die Teile + for i in range(3): + if v1_parts[i] < v2_parts[i]: + return -1 + elif v1_parts[i] > v2_parts[i]: + return 1 + + return 0 + + def check_for_updates(self, force: bool = False) -> Dict[str, Any]: + """ + Überprüft, ob Updates verfügbar sind. + + Args: + force: Erzwingt eine Überprüfung, auch wenn erst kürzlich geprüft wurde + + Returns: + Dictionary mit Update-Informationen + """ + result = { + "has_update": False, + "current_version": self.version_info["current_version"], + "latest_version": self.version_info["current_version"], + "release_date": "", + "release_notes": "", + "download_url": "", + "error": "" + } + + # Prüfe, ob seit der letzten Überprüfung genügend Zeit vergangen ist (24 Stunden) + if not force and self.version_info.get("last_check"): + try: + last_check = datetime.fromisoformat(self.version_info["last_check"]) + now = datetime.now() + + # Wenn weniger als 24 Stunden seit der letzten Überprüfung vergangen sind + if (now - last_check).total_seconds() < 86400: + logger.info("Update-Überprüfung übersprungen (letzte Überprüfung vor weniger als 24 Stunden)") + return result + except Exception as e: + logger.warning(f"Fehler beim Parsen des letzten Überprüfungsdatums: {e}") + + try: + # Simuliere eine Online-Überprüfung für Entwicklungszwecke + # In der Produktion sollte eine echte API-Anfrage implementiert werden + # response = requests.get( + # f"{self.UPDATE_SERVER_URL}/check", + # params={ + # "version": self.version_info["current_version"], + # "channel": self.version_info["channel"] + # }, + # timeout=10 + # ) + + # For demonstration purposes only + latest_version = "1.1.0" + has_update = self.compare_versions(self.version_info["current_version"], latest_version) < 0 + + if has_update: + result["has_update"] = True + result["latest_version"] = latest_version + result["release_date"] = "2025-05-01" + result["release_notes"] = ( + "Version 1.1.0:\n" + "- Unterstützung für Facebook-Accounts hinzugefügt\n" + "- Verbesserte Proxy-Rotation\n" + "- Bessere Fehlerbehandlung bei der Account-Erstellung\n" + "- Verschiedene Bugfixes und Leistungsverbesserungen" + ) + result["download_url"] = f"{self.UPDATE_SERVER_URL}/download/v1.1.0" + + # Update der letzten Überprüfung + self.version_info["last_check"] = datetime.now().isoformat() + self.save_version_info() + + logger.info(f"Update-Überprüfung abgeschlossen: {result['latest_version']} verfügbar") + + return result + + except requests.RequestException as e: + error_msg = f"Netzwerkfehler bei der Update-Überprüfung: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Update-Überprüfung: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + + def download_update(self, download_url: str, version: str) -> Dict[str, Any]: + """ + Lädt ein Update herunter. + + Args: + download_url: URL zum Herunterladen des Updates + version: Version des Updates + + Returns: + Dictionary mit Download-Informationen + """ + result = { + "success": False, + "file_path": "", + "version": version, + "error": "" + } + + try: + # Zieldateiname erstellen + file_name = f"update_v{version}.zip" + file_path = os.path.join("updates", file_name) + + # Simuliere einen Download für Entwicklungszwecke + # In der Produktion sollte ein echter Download implementiert werden + + # response = requests.get(download_url, stream=True, timeout=60) + # if response.status_code == 200: + # with open(file_path, "wb") as f: + # shutil.copyfileobj(response.raw, f) + + # Simulierter Download (erstelle eine leere Datei) + with open(file_path, "w") as f: + f.write(f"Placeholder for version {version} update") + + result["success"] = True + result["file_path"] = file_path + + logger.info(f"Update v{version} heruntergeladen: {file_path}") + + return result + + except requests.RequestException as e: + error_msg = f"Netzwerkfehler beim Herunterladen des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Herunterladen des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + + def is_update_available(self) -> bool: + """ + Überprüft, ob ein Update verfügbar ist. + + Returns: + True, wenn ein Update verfügbar ist, sonst False + """ + update_info = self.check_for_updates() + return update_info["has_update"] + + def get_current_version(self) -> str: + """ + Gibt die aktuelle Version zurück. + + Returns: + Aktuelle Version + """ + return self.version_info["current_version"] + + def set_current_version(self, version: str) -> bool: + """ + Setzt die aktuelle Version. + + Args: + version: Neue Version + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["current_version"] = version + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen der aktuellen Version: {e}") + return False + + def set_update_channel(self, channel: str) -> bool: + """ + Setzt den Update-Kanal (stable, beta, dev). + + Args: + channel: Update-Kanal + + Returns: + True bei Erfolg, False im Fehlerfall + """ + if channel not in ["stable", "beta", "dev"]: + logger.warning(f"Ungültiger Update-Kanal: {channel}") + return False + + try: + self.version_info["channel"] = channel + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen des Update-Kanals: {e}") + return False + + def get_update_channel(self) -> str: + """ + Gibt den aktuellen Update-Kanal zurück. + + Returns: + Update-Kanal (stable, beta, dev) + """ + return self.version_info.get("channel", "stable") + + def set_auto_check(self, auto_check: bool) -> bool: + """ + Aktiviert oder deaktiviert die automatische Update-Überprüfung. + + Args: + auto_check: True, um automatische Updates zu aktivieren, False zum Deaktivieren + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["auto_check"] = bool(auto_check) + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen der automatischen Update-Überprüfung: {e}") + return False + + def is_auto_check_enabled(self) -> bool: + """ + Überprüft, ob die automatische Update-Überprüfung aktiviert ist. + + Returns: + True, wenn die automatische Update-Überprüfung aktiviert ist, sonst False + """ + return self.version_info.get("auto_check", True) + + def set_auto_download(self, auto_download: bool) -> bool: + """ + Aktiviert oder deaktiviert den automatischen Download von Updates. + + Args: + auto_download: True, um automatische Downloads zu aktivieren, False zum Deaktivieren + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["auto_download"] = bool(auto_download) + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen des automatischen Downloads: {e}") + return False + + def is_auto_download_enabled(self) -> bool: + """ + Überprüft, ob der automatische Download von Updates aktiviert ist. + + Returns: + True, wenn der automatische Download aktiviert ist, sonst False + """ + return self.version_info.get("auto_download", False) + + def apply_update(self, update_file: str) -> Dict[str, Any]: + """ + Wendet ein heruntergeladenes Update an. + + Args: + update_file: Pfad zur Update-Datei + + Returns: + Dictionary mit Informationen über die Anwendung des Updates + """ + result = { + "success": False, + "version": "", + "error": "" + } + + if not os.path.exists(update_file): + result["error"] = f"Update-Datei nicht gefunden: {update_file}" + logger.error(result["error"]) + return result + + try: + # In der Produktion sollte hier die tatsächliche Update-Logik implementiert werden + # 1. Extrahieren des Updates + # 2. Sichern der aktuellen Version + # 3. Anwenden der Änderungen + # 4. Aktualisieren der Versionsinformationen + + # Simuliere ein erfolgreiches Update + logger.info(f"Update aus {update_file} erfolgreich angewendet (simuliert)") + + # Extrahiere Version aus dem Dateinamen + file_name = os.path.basename(update_file) + version_match = re.search(r"v([0-9.]+)", file_name) + + if version_match: + new_version = version_match.group(1) + self.set_current_version(new_version) + result["version"] = new_version + + result["success"] = True + + return result + + except Exception as e: + error_msg = f"Fehler beim Anwenden des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result \ No newline at end of file diff --git a/updates/update_v1.1.0.zip b/updates/update_v1.1.0.zip new file mode 100644 index 0000000..446d6b6 --- /dev/null +++ b/updates/update_v1.1.0.zip @@ -0,0 +1 @@ +Placeholder for version 1.1.0 update \ No newline at end of file diff --git a/updates/version.py b/updates/version.py new file mode 100644 index 0000000..fe3516c --- /dev/null +++ b/updates/version.py @@ -0,0 +1,193 @@ +""" +Version-Verwaltung - Enthält Versionsinformationen und Hilfsfunktionen +""" + +import os +import logging +import json +from typing import Dict, Any, Tuple + +# Konfiguriere Logger +logger = logging.getLogger("version") + +# Aktuelle Version der Software +CURRENT_VERSION = "1.0.0" + +# Build-Informationen +BUILD_DATE = "2025-04-30" +BUILD_NUMBER = "1001" + +# Versionsinformationen +VERSION_INFO = { + "version": CURRENT_VERSION, + "build_date": BUILD_DATE, + "build_number": BUILD_NUMBER, + "channel": "stable", + "min_platform_version": "10.0.0", + "compatible_versions": ["0.9.0", "0.9.1", "0.9.2"] +} + +def get_version() -> str: + """ + Gibt die aktuelle Version der Software zurück. + + Returns: + str: Aktuelle Version + """ + return CURRENT_VERSION + +def get_version_info() -> Dict[str, Any]: + """ + Gibt detaillierte Versionsinformationen zurück. + + Returns: + Dict[str, Any]: Versionsinformationen + """ + return VERSION_INFO.copy() + +def parse_version(version_str: str) -> Tuple[int, ...]: + """ + Parst eine Versionszeichenfolge in ein vergleichbares Tupel. + + Args: + version_str: Versionszeichenfolge im Format x.y.z + + Returns: + Tuple[int, ...]: Geparste Version als Tupel + """ + try: + return tuple(map(int, version_str.split('.'))) + except ValueError: + # Fallback bei ungültigem Format + logger.warning(f"Ungültiges Versionsformat: {version_str}") + return (0, 0, 0) + +def is_newer_version(version_a: str, version_b: str) -> bool: + """ + Prüft, ob Version A neuer ist als Version B. + + Args: + version_a: Erste Version + version_b: Zweite Version + + Returns: + bool: True, wenn Version A neuer ist als Version B, False sonst + """ + version_a_tuple = parse_version(version_a) + version_b_tuple = parse_version(version_b) + + return version_a_tuple > version_b_tuple + +def is_compatible_version(version: str) -> bool: + """ + Prüft, ob die angegebene Version mit der aktuellen Version kompatibel ist. + + Args: + version: Zu prüfende Version + + Returns: + bool: True, wenn die Version kompatibel ist, False sonst + """ + # Wenn es die gleiche Version ist, ist sie kompatibel + if version == CURRENT_VERSION: + return True + + # Prüfe, ob die Version in der Liste der kompatiblen Versionen ist + if version in VERSION_INFO.get("compatible_versions", []): + return True + + # Wenn es eine neuere Version ist, nehmen wir an, sie ist kompatibel + if is_newer_version(version, CURRENT_VERSION): + return True + + # Ansonsten ist die Version nicht kompatibel + return False + +def get_version_description() -> str: + """ + Gibt eine menschenlesbare Beschreibung der Versionsinfos zurück. + + Returns: + str: Versionsbeschreibung + """ + info = get_version_info() + + description = [ + f"Version: {info['version']}", + f"Build: {info['build_number']} ({info['build_date']})", + f"Kanal: {info['channel']}" + ] + + return "\n".join(description) + +def save_version_info(file_path: str = "version_info.json") -> bool: + """ + Speichert die Versionsinformationen in einer Datei. + + Args: + file_path: Pfad zur Datei + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + with open(file_path, 'w') as f: + json.dump(VERSION_INFO, f, indent=2) + logger.info(f"Versionsinformationen gespeichert in {file_path}") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Versionsinformationen: {e}") + return False + +def load_version_info(file_path: str = "version_info.json") -> Dict[str, Any]: + """ + Lädt Versionsinformationen aus einer Datei. + + Args: + file_path: Pfad zur Datei + + Returns: + Dict[str, Any]: Geladene Versionsinformationen oder Standardwerte + """ + try: + if os.path.exists(file_path): + with open(file_path, 'r') as f: + return json.load(f) + else: + logger.warning(f"Versionsinformationsdatei nicht gefunden: {file_path}") + return VERSION_INFO.copy() + except Exception as e: + logger.error(f"Fehler beim Laden der Versionsinformationen: {e}") + return VERSION_INFO.copy() + + +# Beispielnutzung, wenn direkt ausgeführt +if __name__ == "__main__": + # Konfiguriere Logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Versionsinformationen anzeigen + print("Aktuelle Version:", get_version()) + print("\nVersionsinformationen:") + info = get_version_info() + for key, value in info.items(): + print(f" {key}: {value}") + + # Versionsbeschreibung + print("\nVersionsbeschreibung:") + print(get_version_description()) + + # Versionsvergleich + test_versions = ["0.9.0", "1.0.0", "1.0.1", "1.1.0", "2.0.0"] + print("\nVersionsvergleiche:") + for version in test_versions: + is_newer = is_newer_version(version, CURRENT_VERSION) + is_compat = is_compatible_version(version) + print(f" {version}: Neuer als aktuell: {is_newer}, Kompatibel: {is_compat}") + + # Versionsinformationen speichern + save_version_info("test_version_info.json") + print("\nVersionsinformationen gespeichert in test_version_info.json") \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a16522620f620a45376364404179513d09bae2f GIT binary patch literal 143 zcmd1j<>g`kf}c?V86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6wr*(xTqIJKxa zCL=X3r6|TFwK%&ZzaXY0wYWq#H!(9WrnDq8r#L1)J~J<~BtBlRpz;=nO>TZlX-=vg M$gE-}Ai=@_0AO|?p8x;= literal 0 HcmV?d00001 diff --git a/utils/__pycache__/__init__.cpython-313.pyc b/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8ff459f9eb58d0664a5959226f27aa1eb65a588 GIT binary patch literal 142 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 b<}27<>URvqHjuJWUDbj1D<3N#pDX+axXR7Z literal 0 HcmV?d00001 diff --git a/utils/__pycache__/birthday_generator.cpython-313.pyc b/utils/__pycache__/birthday_generator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98db6d302daafcd2f7b1f01d2a88bfe4b410739e GIT binary patch literal 10814 zcmcgST~HfGnxlVd00|IagpEB0<1h}$u_1qo?HtB`!2u@>vWZQi64D^Xh!O4?#epj4 zaxZ&0d9a<`l-b-?ncYk6;-@IJReNkz9-QNaeUnz36Q@npx$06|%RS+#zU5|JK8r8kT`{zuq@_;sC?^JAUZNQK&rp7%HDI0z(+VB$&^d zh>4bFVy2~qSfI3=wGP;bZNN_K0}kRCa1!T$i?{|@!g9=*Ww=R4yIHm|g7tZm_D!mr z8ThufFoOMgbD^z9s1h7wZoxU`5nN-QUc11yFqf^DO{6wv=Btx^0LPvd$CE@=qG2^T zt;9rGBw;l{xQUf72^SS*PDn(gaGV;aaaDH|jCmZ7JdFu%&qUNoy zONyGe^vSb%=eZeGO32}O-Zqk)iHmt#P=@b({kTNb$!K^sWK0*)R;=Y!g^!1E*xV<~ z7y}2!;9UfTw26J-oXxZ&5L3FW|NdRr=8h)nnB{4gA~2ssX2);;gHH6W3O4jQ14A z*9$E|ZRObtd-4L-x&m~=_rl`rR-vBG-Uv8xMYu9A{A*`p?`4IC0-h#-XoQ}oqF$%4 zqtLrUXeP}uC%-dacf4>n#tpAm!+9j%gzRiQtSBNkmn7Q#OCm|iG49=j%n>mhhkKWl z$Rv==CP~2uB_xfAx+xKL7l=slM+(GC58Mg41^yqNf(+ONSVgc1RzEU_8SYQ7ZMJDO zyWl9mIsLXE6Q{^9i@A7qZr^LNqyqg)3anP81ifX4{`DFFUynjI=j?c^yJNb$Bg$Ly zj%i5_g<~RA!tc>a6>y0dnn;l8unM5l2Rot%JKkEa$Kh{4#@q5PNmkTwjD)B2&WUhD z98V;s@(%T?q^csx+f`{w#fGRj5l+U{p}VGh-Ap{JBF@lEA}&QFQQ=K_t0KlH6g(d; z|Kb~gvHnSEIxLa!SbriacE1r1&t4@`Y*HOds#07T+saY?8DPLrNP_K$Liv`ho7Ve0 zFo!Y**$2!9%k0{{wsZH|9{z#D+Tz}*F8=nFeK(hV@6f+nweijF2hX;&vGdliox2|b zekSkKcbc~oQB4v#Qjk>G%OEFG0w7TNgbA94i^&5aAU&@YjBE-r!^NWu?Zb$rEREJy zf)q>$s>{lK3~x^U1RB`W(m2qGByxRmxG{xDkx4ZU$0%x)EK0J-jf+6*)6=TR!A+?2 z@{lc7zKl%yS+?()SzShQiY#P#@VGz_P>i3<{tqJW*JZS#DZFLM2-kz!D!U z5D3#2Ph1rV*$H2HOH5RGGuZ`ovKKN14~64MD|ULJGo&voInx`=dd{bv=Lv^%+bK2A z4n^=z_4Jf?JjzwNxuEG8z<0^2BOTT?t=<5oU<#Vtj9~7u=o*q^Viw+-{0Hb{1yP+- zIp8G1g{Rcyrg(V#wHuec{Y$1)fNVV*;I?zc$sMr1%9Quaoco!Q?NQ zVS@?)M553}qYCsNo>ngXZxw3^Uph07)L+1GI(8h^ArP7||P`6f8kA z(3Ul5gWT3*)7Ql@L7Qwo7Z}|i_z~a%b7EU~>_I!=ag^Y(1D>W!sCj7`cI&mxvW*!% zuS+l49JCJCZuaT5pt&3DQ`u3333XLj0=v;ExDJ~2eaOzB6KMuh6pkwB5ZG>@OWCp6 z9-f8WJz;$U3#)3-@D%69s$HW$s?;ACh0|->CjJU`v4NjG_r*5W$PA-cElZ=fbu4-* zv*$j161K|w6{-vzkJ~XY)i&@~*e~1;tRA>#yZhMxq<;d-p`#E$yE_rOZxoD4nVEph>?|Ft=uU>a%SaM zkx2tdlG-Olc#6CIBd6BUpd%8XAn0V;O1OhCsl$0E^p#_57`gkd8d9n zc`MfPHZ=iGBmuvh$vduwi44a9mStFuCZ_WaEF~En(pfPKhIeE#Aw^)!bV61q^A;GG zcZVQXz(fvdjZQ@+6L|dOSwP9x6sb_OhNvDTsQNZfJkL_gJ4EMbnG7qT$#67eSmWxX zq-cFr8k{zfOZ&hz6vos2xHZQ@6{D~)xN`JL(0(wIro8J4UbAS-lW#v>?zQ>10G$SG z%74HOfZ}dq>YH+Pe5MY}w`^U{y#0}#sq@{gxmA;^ZO_!UXKOp=Z5wvR>s#<;>f7fX zYxT{A8njk>uD`nGssHHOH?>W--M8H7y}cvot#GN!fVoY_X zO!u2wZ&}vr8h+Ti{Q);Ee}Of5eYgD|`xkd)>krTO!^}-PZ(qE1aj`wq*fGyS|NgFD zy#2GcQzw=?Qb)5LeQ9UgA3Xe8b<@J$>*wYN{sc?i|7{C*$N#B+scz}y=V$Mp&9=NW zfA+qo<<8WPr?MXY+g)vUhCUryI z-+ue!w-*D;uVoug&9iI1_MERPFpBl_`AJ6*w=iT4x=9Pcx{j~qsvb1Wsmt5^j zHw^vGdG5Z)m-g^!Cr`~S5~5Z(T?0aUC|t ztivvtbu{+YF$}bh$3u{12(6)$EZu~Ct)n(tM{jE#>_p}+xMP}_FiyF|(k4d$D9>$W z&r;EI6QGo1kWt>OuDdyyt=hZhZCtCVzZw4M;(d?zX88Jh3x__swzzM}`_ls{(?1?e zjV$+naq-@vUxwCp`0m4>w~^j=$t~ml=q=Ac<@+q_(R$rJNy27ldV`jVUYDhktqrhv zd}T>6`7P*C&;7AT1*1=WHZeVukVRRAzz|-1sajLGPN0e|?RvWC-SYnrdU%UpCYP)e zv}B3$g;gT$b#at3yFh8r@+^=6vY5tf$VlLuoxp~&R$IAJISxR1Q%FIV1nIcoq3zlt zze5rI?3BEJD?vRD{^x*Oc^NWFUcS~`vp>`9UwSXweDr$FJPU%ganJ2@x6Z9K?#VTF zfN)*&wxYieL7-Z8gR#0;O_JbROUMgJi;|ZNtnq#172I?z{&$X>b=YvrVBSK4Fj0k8 z>h{>c90&KOJi&+xt?2qiLAgKG7?7RlF2WeY722E)=Bdz1T}*VUY=Vh8RfZcHv?=DJ z<^Y&8M@_o>6^6ZW44B6jqz~-d2N50)atLLtLKD04lW~#EK(E@?~6=VrUnZ@Rx3vs9?<&+h$J=W+CO4ir^8F1QQnQNf|gqw?>O5?&6dLgi5(n zV33L|c@8$SX;<+MYVwi8*xG{`wY$og$*wUIk|m9O67F-peGkJB&4BdBcK z%{(z2Q6IDz;Rp!5RSFiOY{q36;Rr|2v5(nX6wY%n;a4Fb=DcKRl411L%8~$vG=wR% za3K&J)U83Mo9S`tm_fEP!)RQVrJ%7V!G{Lq?rZ@PN#U%v799x@gfQ(b@K_3N*vclwvqrHNEuN=#i??puy52k#wNeRH)x z?R(>Q)o4_#pL-lr8}Bx<^~o%)`!BNXNwD~sY(U$e z3=Qmw>n%&8qK-o^A;u1-nwBbQC5+Yw9fsh62zw=g5riK%6bl||=KTh+7P(Pq;LRl` zRz8czrI^kO;7CJkZ!#RG1|YPeUo0wM8jA3Ksg&^a<^=^f0Wa&a?nCR>72r~sPi+m^NbLnZs)xKdn7Mnh7NO)GDP0h*)xJ&QOx#Y)`?je z%c7}Ri9s6-2GzoYnr7Vp7``lmV}MSTD}Wi;vALWbzx6cUc>lxq7cOT#J@Zx?;QH9T zaAmQL#?ZcTH{6gvlo#GywC1>;4A+x7ndSZ_>+YMkeCye9CgWJI4u!73 zduE}>6ADd8M8WrKQBFX&BNU1zBB2n$AV=N;0Z)SZl_1e+VzUctXxim15IrSG{6*Js z0zc^$jUb2>e%%l!7qIanW?{@Gu%|H;0yTGOE*kGVoeJMS1;WtNVJsq7)OB9cEB@+ca(Enin z4%^Yi_kMBhr`I+ZsNS>CB0Vzt9e!>cZm@Y%&HvQ;MJvwWz1M>?cpr2%*Pn4<~w~)C=rBUCI_^~ z8H__xi79iPiNcT8v?$|K3O{2_f*-nXGI(wiuP`UET=-eN&#~Z!);sSTetQbEG3p zz#ll!CmwZn21-qGA?0e~3x$>@uTT@?`qkIzPa?FfQWlUAW*ReGb(uKIJCA5`^cZ;; zdSOe9f?J2T43p`B*=({tbTTIQ?-|eU8TY?2fgBUK@2btYT7K(lN$)(Ebv^fi{m<4j fR#Wc-hUO#AMpMf|&jtfU%1euT!k_SqZuP$bg6-2r literal 0 HcmV?d00001 diff --git a/utils/__pycache__/email_handler.cpython-310.pyc b/utils/__pycache__/email_handler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d023b56856e4b789334a9fb5d3ba3282c401720 GIT binary patch literal 15599 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 literal 0 HcmV?d00001 diff --git a/utils/__pycache__/email_handler.cpython-313.pyc b/utils/__pycache__/email_handler.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ba3ee12a1ab648f497613faee97980f3a14bb1b GIT binary patch literal 23277 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 x>2iL9o literal 0 HcmV?d00001 diff --git a/utils/__pycache__/logger.cpython-310.pyc b/utils/__pycache__/logger.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6f2d401a883133a1cff82b8e1331a7478023a4b GIT binary patch literal 2197 zcmZ`)-EQ1O6rQnXyO!*u1UCU8qSAzlvi(UbBrB5T?94h&{jIT zlaSxBv-~jVdz*Z#K11uQJ2znLG3oR@jr_x^yx$e>57*4_K!2YrJl z8ai#$xn4?D$RVy+e$Zlk)cYU=0ZR#52^Kb=k`Y+R*>NjiwN*jmDPbgV0J&Qs>aU}m zr$SCXnm4ZbTu2`#lpp23NOJ!a$oh8)kA?OXfEBtw7+OYeEwF9RKZ8V*CuBxX2qjZG zC80GX2bR2uat6LXq2P70W4@j>xZh({LzQD8D>n?2JkeoTwKaGkJTyv!wi5cDi1_V1 zJ?dE%Qz9KnAOLv=z6!%(8YvZq&&XdNUfh zIkTn~T5H<2W(SRB+n>=v bIA+tyudnzXyj{$k$WyK>=$6TVOz`l|5o-JKySB@0J zLh{PaR4m&tQi~4GF_9(u60}YY>d*@o`+KswWDu^AU0Bp>qRaCr6X0?$46}llDYiRd z_gmqiJ``yR6K5z|Fe?#?L~i1^LeMT@bj4b8%xJ|h7U$fF zacID2-1#FkG!SS?j5{qLk2i&cn*x2%#gw2cXAXxIKM`*IY@Jw-ZP?MA+OUI7+02Dr zi$l-2-l8K)3*Zs>^5y8A^v@P{Bp;Q#ZY;UD%!sZw- zR4qvL+OW#quZx7LT*yKLuCfYMv2mpLD|S#MdDY%ZQZcU~RR@-B8A@x&=+;@&i#T5_ z>h5SMqHNK%)sdNCeQYRo9lBQ`1?sdDPPH-8hXtTA+bnjrcj^T z84I{^K+B7g0;5R@&5&CO*`5E literal 0 HcmV?d00001 diff --git a/utils/__pycache__/logger.cpython-313.pyc b/utils/__pycache__/logger.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..183fb6e7a4be3b30e7b9ad038d1d385534c6d391 GIT binary patch literal 3389 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 UJz$tKhW3H>!LjEK96aXyFRjY`+5i9m literal 0 HcmV?d00001 diff --git a/utils/__pycache__/password_generator.cpython-313.pyc b/utils/__pycache__/password_generator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52f3fbdc1c997032bc85bdde463d8426d26c645a GIT binary patch literal 13532 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 Ag8%>k literal 0 HcmV?d00001 diff --git a/utils/__pycache__/proxy_rotator.cpython-310.pyc b/utils/__pycache__/proxy_rotator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13577916dc084099b6c5db0455195afaf1174379 GIT binary patch literal 10229 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 RG&I|=K|S)%g$cZ%{TCi(JF5Ty literal 0 HcmV?d00001 diff --git a/utils/__pycache__/proxy_rotator.cpython-313.pyc b/utils/__pycache__/proxy_rotator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5888bf0bf33aa32893ad3d13a0074f39cb493f9a GIT binary patch literal 15674 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 literal 0 HcmV?d00001 diff --git a/utils/__pycache__/theme_manager.cpython-313.pyc b/utils/__pycache__/theme_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8824bce324dae74d53bf7f9591c586649d8bbc6 GIT binary patch literal 7731 zcmcIJZEPFIl}j!^mdm9mS&}Wumb8*n*>og1j-td?B&U>Q%a$nF;)=E#t6*2;irR=; zs4RHSBe`@W6 z59yEg%`BH(QA$%M>Bycp^XAQ)H*aU&y!ZIj=VK`-|M|zxxqq&ssQ<+svp9;4rz_C7 zP6-sE1e;(VvmqOSc4Q}zMl?Wr%rWXj&QTX~jk=MW$Fz~9${~*Q zU1Pq{DpWP3B5p^*9$=SGu~T!>g2a!D zikOfP-^HJi(2|(cB#n=YDt`>Au{lXr(n>;|k&|&g4n#p^NmAl~;`b|Q#E0a>oW_r* z;!=Qpg1JuwXx%X^$26S|DwlNT)S@P*6fqgF>D**+aWN^!M2rd8b!PIkn3Ob4(p{57 zsbmTP&OWK>o=E|)GOVimG%R6s!H_U!h;P&-Ei2H!PEAvwT?%U!v}#8VfksZjG3|QI zE;yU0cO1wqxTYDwJ(sgGBthfqJjRoPPmuXN zBUG|9KSG6FCQTUvg{d#a1 zVgub(nH0dI)ohpH_MA-)uxs9=4evEuWhi)HK2z?SttzU=UgnWt1K#CP_M{?fvY3=r zSwdiGB!#zLrf!xEqJ-MV@=)Mv^zp;8#9K{A*GWayL}i)PnV~s(L6niG?-o)q;5jbE zWic3wrP7KvA}JCQwG`4lNK#X26q8ilMGT5M1?Gs6F3PGFP0j00IhIn?We>TJ-9J{< zX?hJ|Y{96y7ZjJgR9w-OOeMg+>ke6&O@WPds8Vv4$gN_t!Sni1tZzD^ zN=Tg!#us3>szwwyJ)DZA7eI7%dPLSvq-UlzN!6y)nw(UpjYW%=xqA^zeKd*{1;W=` zwbyN;cfnffQ>fmfHmaz)T|Z+UHMFfB$aIh88bX&HKl43oZYwl*Wt+P$JMv7^GZ)pk z`oK{NVWf14_ZN}k_rPR(O^S#e8%n`>gtk)(9ihWMbEFJ= zi%r!oxbUC(c=6Or)cKNe3UBi?2=0h|vmx2EGP7yr+VYBK3ml)KGXxG)#o7tBjq0S# zUB!WJ7EF9A#lv61H9)sgv$nrN!I~83OOJ<}!n#o$mObH#9@KX8!MspaNZKmmjyp4JigP~_xpMeCJ0G?1w-PrKw~{xL zw_p2k;LgC>%j*|2lRwC{ynXr9BY(}+r7KHUe|qJoE2)CNJL~U$RNuIA@OIDIuFvX& z>yr<6@45B*&Da0!@N=5lIcUq)2f<#|?7C5Ptt!(txc*jVP{_1S<*Or)H&3gp$HpwP z=UBe(xH0vMU5(HEn8)Y!!7rb)xRtLD0`^aj{0&bjJ4njyxEi_=%IqFkn_GK(9X)7! zP@8ES&vO%xYU}^B;RKQ|)wf}1?+14e?V^6uTs_!J|E9}>+i&cJ`je&|LoWK0P6oGo z*&*8f$y+qE@6)7qu|sw4`_-gfN9tYdu#3LmwtLt{|Ch}PZJjlq2W;#%89D4D(D481 zW$;q3Q()wZSCgVl1EJU>_AsWYC>FC-@@2~;=dssJ3Ya_iGd-uWnfY3-Shk8{@iVva zm0y5&$-tEtu<*t{L`8W4-X+hu%)+7#y8wURKpQ24X5m`$zr){7;%)q9i#EQUrW?xg znyv4QcazDn3@w`ersobD!iw^pN!(Jw7`hhT{MOMIICt1~R}_mjXlv~B0$g9JU$I4LfeI@Uz2))I%@+)+ZPE-p7T3oA|1PZrI0fzsl5do7J&1 z6jAi;$Yhn@w@mMYX!v-b4dG{)M8YJar@PL|N<6iM@Y|}hMrTU8s39C>=+2p>7@OA} zgAnY3dpC#OJ6{kIkPsJZ62W?>Kr2)7;-(JfpgTzI8cb_iO3_&Z5*#~-aAu9PX(YvU zM@UxYb^n=;E!rI<7HP=0q=Y}70wdP=<`IT(* zEBD$z-v815Jolz$`3X_(F3}wz=otnYE$y+VxnjcQnt9Z5lq5 zZ9a7G)sGK<)5sL_9q2qTC-4)krbDaZu?xiCuM z0!QY)aeKJX*`Mv~&vOHh!EGPAGPbgB^`%1ltJ(HfGi^PYp0P|rD9?>Qtljnhj}8C6 zvE<*AJi~Z)U_8@5@gSB73z^BOOk*U^oq1H-Kpg$XFI9X>-w!!M4D~6)j@#%@cYH6@ zPk-9)gm$dtd;<|~6W1Ic?(6sg16WyJq5`&bCtye986$T$0WQ7~=!x@MT1rBO!!TM9 z?WNKEU&ZFT12%GN0N^K4#S0m6m|g=nD!Ms8*0SjqY#B$G1NNrhqrULh7W}PQe`|(m zMOY@?gA;ktEkQ>qq5j`M1Umyq?M-Om2gVXq6dzF-D3uq;u|NidMDz|wc;dSa1@an3 z1L|*?!P%C3z?uFBV9l03#pziw4oY9(Pl6gs+PCIYOZow84d0E#=a^XK_owC*$w(dG zCkiYvf**MT+dNZ{9PHyaEyV_$pn9B?O(1ar(p+Lpg2!xLcWX;B{Jw)A?JCb}fUsYf zH}VoVC9>@3Rxc#LM(&1AMLHm0H!SB23>CAKBy!h%_(YIxkm$Z?%V{Z%UBFY%K?PQG zC&ksnMap?Y5CY$L+fd5XEMjm=4#JnxN{JjSMMMJ z{ZFp^WM%l)_|5U#Z?3u5Pp-fDAf5>dIsa6KnIdNnk`LH>Bsq-ovq0X9iGUv`Me%Wj zq$nGWE~MgVlHKK^(I2P9WO2lgv%W~xl90VoQb6jCM&qeiG-|vx_~#siUvQlU)f=e` z2lf>Zel^gKa3$m6`Y7ap;m5UDRMa#i&!iz+8;#zfh@3{=1Lx%suJErsssz~mJ0rGL70-#-ZmZ7;G$g?525M0X0QuyQ&<4*%cjNRl&hNOiON#&zHbMyB zA%wFwy8ra0Nv*egQadZh;p3|aD@6C2*pakMoDRdq;7s&T3Q5GtFmm%Am>D8zaO0gO zics+y*l-v}i|IE$&{6a*;INfa2cg=aZ8jSy$>w;u8hF)_yQimTc6N5SU!jHO{2h+@l;Z6Ok)S&BAH`^7N)4LVM;wgCfv@k0>1jN~N&k_GY< zfU?qx#ynqzvrA=o35-hYyA9g?{mZd`Gs2TKg+|> zAB%@i@|*o6MNso6!YkY+>a~eK%+Z%d{@ZPjFH9SqYn=bDF^t zIcT0*YQe)}i}uEe`PahJ%p$951m}Ybj9Cws*t7SPr$2unyTuL^*AdeX#eSBB?P;N^ zt%*WaB!l*LnzWB?aJ8-5p~`y0C}exX_UB?h&-*s6_ToqCEBt<%-|RV_MD6n0 zmcg%it`=D5YKKpUwZNv$8K7!!7(qQ~e6tZO13wR^ zn>mla4`kfhVivaKNc%f+C!C#GjM)etIWp$#Vum?q()-+^_xW!+vs%aE-Gw&{YBsft zx+eV|eZ!!9OqcJ`<>R`1FXP*Kq#L}CO?+bdfgenw9HWXd_9@p^c4Awv+!guXeVk%y zUg?y^mFG6Caw$r(+#e`^TsnR*j*_yG_WO}o=!=_;vdNoH943Q&NZr3Vudn!VobH4{ zr#JLfR-TzZLQCZbFHg^3hRM9&qw`n2G|9qVkw@EMC!2&aUAY!S*}Q!xRY6v+`9+>~ zvSKg@Gco+^t|LqMXtjjPu;1wWy|A06qtYIS%)Kh@wiKr*N~1%cdcG6od8l-wlJ|E_ zDb7>S?B>xZPe(^u1vc+)P)w~W9cIx@rLEF5FWqc6%fs=eqePR{MJe7Z#S;`|JqY`L z5$Bs9@tmPQne2K_RwT);=l0?dBk`QB^0!0aH5v?aUV~S89D4Rn6bGK&^W(ASc9k@i z&Z|Wf<`CG4`nhL!Q|Yl2WufQv{iL^Bx-rWF1g&l~pxYS9Ln2LM&lI z4kuBsbjJQ5>M<*{!o+j4C>ExeF-@k>&4v*uYX`m>l};>Ncy`~cEMg4N}8X3c#SW<$>zsE}FgZd`<(rNY2-CxrsGJ$E-0_3_+P`N@FgPoiYx zIXlBBhl_6P_eLVYJQ|3|LVqA4ictUrkj8yThQ9KwetOfh(w$gX*%^6m-;dENuO6iF zgq6s8Sc%Z}WzxJ*B5P;jCxP!dqtuUmb|Oti;cjX5{G78OMUul*A!K`!vTyYwiF&C* zR)O*dLVPkz!z8-txjWHR1mKPmEvWjAuT+%6ZxSV2zE{uuaiRRkV;{8`MiHWOCn`#M zq1UMXW*NMPDrGN2R3RvBF(7m~RBzX}>6VD0!DBIl>)$v7vx}jo;{kULiEBDcrEM&3cPzGX1Ia&dd_Yv~Y zFJLe5dqdC83)<`b5U`Mr15O7zr12CX=OL(JXGj$_QvrLu6QU%Oz8@fkT**OF+G5IR zfzKMdSUKY+QOKV>Vm_f#4B5zTn8U#+VhHPv{qcmA#8D4>fLl0EU`LE1O%h={Wm4fl zc4!!l$3hVm88J$UMD=PCgoEV>e_g*Ai-r|rMuU2pRyUcmuL@1spY$POV-mdF5#HI- zaTLR(JM#N|kye+kV#-n2m)XTWopF&xBhgay+|K=ETH5^xbGEx;+L)~B!qlihf+eCA zX8N)T_9&eoY`?$`taONQ6JOMCTu7IxKSb$=KI$%tGUZJeVQyPKJsp;h*0SIpZQl<7 zvD}<-=mNN1U`$pa6rsYBz*MFdw@g+W(yz2pN@15%!*jONfrzg^-w;1wqF8epq^b{W zfG}dv2#7(b8n!i3zF+|(p(eOeB4aUT;AjMI6BR}Xc_c%5Dw0vYNuQlGg~Tw5WA$;( z_cl5a?#g`Z9XbhQbyF5+gGrMTo5aNv%})E^Q;L~ke@Os_IHHT0dQPaY7b))M_vtzi z!J%dC_#oQE&rxEPqJ0>;DuIMBjMYD;BU~eq66op*IB=dtKucGn73?tsJdG#niuAPt zsA1qBqxwg@P#=>*&Lj0b6bz5kf`wuC$SF(G9rd&{eyLtvIqdhOfpFJqI|ck z>tH}p*+%xPLMt6-6`}$ywBpFwG!dZF+=RR8S*B3`gaR-}>Ez4=T>)9nQ^ll{G)lD6 zbyHMPFhk5e4huPmwca5L&K%DgV+L*)@%x46CjNG$=rIOB<>bjz*{TUf6lx?40bPs+ zzams)Jv)HL16o+A5ph2&&(1Pf>f<97c#UF2F=Lgi+`rP_gEu5Ggv77-xc~kRuhA zU-{VB(Q!E1b_nK3nU+^ACc);EszS7i#nq%h1f!bO7#3bBjacr0$M5cvh&?I zC29l+Ir88floQE`Zi1qso9gp=6|6}nRX@s8X(M37i}28eJVd&7D$A*Jwh1eqm4s8a zB?WY8Lm5~y)$Opr-crpx9@zwx@i`ReGM0Tt;zF3iRj*`7zfkL`SLc9_!8$ZG9g7I8 zF<5W$;934SLV^hnR;~gW=t4^tUPp^Ey&BO>EtB0$4P*iC7HY;41UtqPug;0L?ME>x zn<})>^_6&er755VHi~RSQT1w2)SCZNj6pvCM*hDVc!yNRfLNRkh?>7BKq$?kmpoeA5umFI zhQg4n5$Ot(jw(VNCh0c8|wD(fiFJ~xgF;R1wZdxde9!0ORd8H#^{2njP)mq~{yUX~!JtaZ$R z%UVH3A6@bX1x+#H2-QSye;je3ms-%YU=1+ril+7$+ygtsxkRR7IgLq($5DuLsF5^L zF9{7>Hi?D%34RPBL<#+FIAx*f7CfS@!cXF>h*P?oLf8$2f6*0LB9^TDDbt9+#}kNX zN(d4QMpqfRqvt_Ag*iFEp^KQ(Ezk&$ML}eO(@G4XlwKC)&SMHlg9dKmk~{GzB`AKu+Q8pA^1SL}raQ6P*21*`B~bffqo-Q#fmbZ3EqM2u}^R5fy+UUVI+S` zQ)us|(gDwTH!V2u8Os#Iw3=6hZjXww?2BODuC&4}nJnRtgHCLT9vO@bQQ;Bdlhvvtd4Y%d4R{vUmKD~G{Lo{co zY|KSex+)}YPI{6tng2*#5-~cD_ZXF#v0uAg+arUuW$c?X{WmZ!)NV^!*ew1#U7GEK zpX*VV1XUCsPa|G_UShl$d4yF)k(e@UcabAOU4A5|C>hB+Z}&!576ccSXVf6Ox_FZ5 z=Bv2czJX1X7E_1G$wkLS<$1l*`$$L%nzO-IyX|o#nKku}j4ueOODveolB45s_47QH z)*#F`P4zYE>g!U70#(H?OZyqxJ9B<3#oYZ>8Zw)r(cCbm-*s}o9x$eKwNmfi=t&l9 z{v-9pj(w1a#g3V>XEWnkEjRbM2ACP&s0HTh)@{QeAIt99#oy9>v9uS$e44jY5raR< zi^GiL!)@s2=BeR-xb4&XEOxwmwf&uJRw3EDV=3(ux_D{2dg<75Z*1D?Uox!PS{G-M zL{(`=Nk1i9c(})}OP~8UIy<@1zfHf)rD!yo#@PpM^#ev}HLC)wm0m&UdjM#=J<*x@ zk8HalQA=AwiqpSsTnAMQGAZP@j?Nv%LNM_ zSl$*3z5g;`5RUJ>dFNX?0exuE$v^CFO9ta=dwvy!CT^#3oaJ}kOaOQuwXIjTO=#tu zVQw!Lix8bTCb1*r0G;aoid3$^5l&zGa48ECi^>C0V?HS-$T7HBj6zMm$zGJ}ga&dy z7IylI{gb7Y=NIqDj&kC5=Eny~=R@6+z*^mw;z3$=dO>48_+QgCYf;#%uF*7Yt7SB; z>H8l9DhC|RM;=3xH7JOtYIXV|no%Liz5)L0YsA-3uxD$x44$}JJA9g1*`r#mOi`*&v3DoFX>i0Xx=t&8Ru$y&6y3BEzjz@Y<{Emy0f>OH)qSYQ1hU5p*FJB&s8qwPen{`h(_RMa#2gD4+U@`LemU zvbQ>0$$mF)?XLuDvt_tBJzIqq=4I4m{7fjDd&?)rosw~fGK0cuur3r{M}~}D#yrmy zKlt$GNkUhf?fAIKB-&(>b~J83hg;<5?S-DcMD81%>^!eE@l)YIks5xc$etyZF0w(m zbLY)rjCc0JMB)5}o$FzjEPE#D$uE52q0GLr=9f;I@PY@i*6L8Zdd!%TBS5|Ayr_I< zc6sc+Qv94czc28cU8EQQjIS*1ahJEx^|73mx2><5uWB!6y7c2e;a{FUb@}^VDRFvA zQ*kqs72UU}UU{k{3?mPB-&xSS#*JOxOUIL^|C`Jq6JD%MKk+U`xqyi)*T}Z{arQKo zIgu0G%ek+)-s|3A z?*9F~d-rbKU81VwZc&|dmg>{S+lP{ zJ9(|4YjQSJHCxvV-kRLk^A?2X+!#1}W>DX=a?Xld#C%o(83{OZgNmZ~R>&WG(y+DSgrzPt~WEl!%PIz4$`(E#&XUxD)XLiNk$R%7PU zx}x#Bj(b#BalW+Q3|43E0)1VxW?)>ew&7~|fL5!Ty?P4x(%_bP+uUmi-^{ooCtI)< zoPMoFkM#>RIVXRvR&n*SHRL0odNf#ZBY5OIZPH%xq-Q^Sbat@+T=6GJcY$~d3p;Sl zrbYkGn=#k8_q%Bih0Fin5SJw!`w?gL48PfV3PIX0^L5B=jk(l_OwJ+ow+!*Ye}^9G zccu6RDZVO&*8hL8sOjLUagC&8H}Ic+5=GQlcD^>f_AWYoV5xKCJBqaxp&k<)`w?B& zTz`nlA=iPij|OkqxwGZ+wk{tV-8KVTbWPT=>D*8f<|+A*OGzwEj=>mhHjb=KmL&;) zZdpm32$W^&pGo(BF2(Oj@qk6Ee@nwh=Ii}X?VUOPI2{i;KB#Fq!@Ujn+FE$ucz5pK zFP!{4ip`}X+#l}DpGy0mN%4@}e{|0M$4_#<(2^%1sChO-O9Hedz%$#fsg=M;_?%6? zz(I#}tCqNZqXJ4f!L;o0#Sax}p+f}g+OgIix0bIy5k;!sGw18clYEIDyufev1ciLx zv|j^n%-li;jC}{6m;ftq7W?nsC~Z-*N()4Ucg4M~k8odWOA}`EW)visS{?3ed?kFkQ{1dAYo%$HR z+SmJO@%0*&ulHqQke5LFSIvOss((WtSY0U10oz)wHVsX?;$MT$(lrs|&53&YMOlDM zVtxty`Tp-{33FN+FsAQ&j*FTupUYf6n!z}_Z=0)JIm$;ihwQ39fQ3r4?o+Bilm>Bt zwCAIth1(`BAOC`PShcnV?E^KqQJ3^+R<^`1k%#dl&$I}<)3o>)Z5uuZZcRRGKSIt7)|?*RubIy-vJJQgjW6WzG6-?WC+34a5BV zC!ChCZhipg1xMvW4=+^B*s<7ll{v+JKh1Ae!UeHcP5T?x%);Zxns1xg$^ac05WCJK zVg=8&ck-xsBRP?5UIh1P|=CR{X#C719vBo1{Rz=Jw6_Q!~k z=W~B8Xw97c=568QYgKK3IcNpT+KV~9j;|fkSj_&!UNczPZ{_yZO1`>v>OdZ=ARe*s zyYTZzhqPY(IFBX?I4zJ&Q{c4%~}fy{t41;kp-}b=KmA(ohEAM zgxzc9>-%S_HXU(GUUU5*=5Fb@S=4OdJH5~w#&_NvDWgl7TeQCVKoa!8w@S=u(jW^MMwXkJ5OfX*^?6s(u#$|2GcY8djwtn+eX{3io znm)~{^X9`ezl)F;Cr)(>aOv-iOPd=yRi?fn)c;b7-R|c||Vqrnjymw$4#P8W`_8?8=RUFJ6!J{4qhrm*Awi+#*1; zLA#bC2)HH}KVNE)&pUrmZ&9DWKQDMmkCyfL;+<_ZjP>eL=}Xf0qG_Ac?>>o~4`Sn3 zZn*(F;sR*E^aL=itbPcZIj(>PKCXfYzb=UTh$-`gG1{+NeG(^&K77xP34fSAE`4~S zPrd3h6JT2haP0%2GSm;yZ%HTLn$qVW6uUe#g5?X6TqM1>ZK&tzwNkx<+UlHIa7y=C z(|v-2KD?vP>4QF&glE|==4LhHEhDd7IBN?yl@y^QD=XfEGgYYe%9%Zvt1m!)<^Z2x zm3naITy6f$g~|ru;uoR#h=hKxY;Z)>_v^PMg-Xeo-+-61I$?S4$f|xhI1gZ5C`1Dm zrg`U^pw5J=uzvnzJMBuycPu$}e+s@$t|61pqK5ys(Bu{EJ8#b?!^GYxrTx$Vr8^$n z*etU6M{?}#5AvN&mbZ8KI!xab%9M+nBSBeEIN&;MA&f=kfl0p)UaxXX+;JnT=fWY@ z&WEA_Hv)$wv~&F2^t+DvQD7zajcqDjgyB1)h>RNN%PpnRjc zsiATD#Di!^UI-fgP5JVzJw1JdK3K#Eh2y(xdQ1-pT>yR{#4z{f!Lw zrMNFZ64c)pFclZmUX?7^2F~M#ArQJ@nD^Ffo_W`>9^?5jxrqlR*L0ixz$=pzSwxOU zxdXm&gK$79LHmj(V%pd6nDLgO+-y6QsRPNBs(8tK-g; zn#2y3toEfQ(5v2*;teUjEd>X3P5nnHB#hMGjmG+*J0ga|U{SnX`4179Tnsj9w$U(a zKXq~A@f8w*#@ao;L%3%e&3lhuym zH=^SetsS(Ye?5GYEN#Wa8h&?3-{+y+RI zu^ms%?i4-Qt;lu~Dc($p$#}|4C0j*jycK%NwUHxdqpke|2(rm^R5PyH)J&;rt5&j| z$z*>Qh?6;wl(3Q`F6KH?%1V#8nfpi? zEAt5F>;ugv_{HBfg3tbp#kgk`%>wQnErQSaj5YVHGGg_Xr1s&?y-!k7$w`SRVq8+X z4oQl{#H7kR;p^`+Pe@WcJ~b$dG0!n6B#WNC!JwK_lAe6KjqW=Hv)}8~?FZywQnw$K zwWMy_t4!#w7`oJu8PlJ4*;xYuh_nlau;#2hYYAZDD)ayOrC01e*}&I#a; zK)eB8tl){QMPLq}{hZV1I9GDcdCt{ib)ZX+-REo(F4$Qqe^*lY&gbyCdYrye{@&dt zvZf`yg)+!%8iT(ofdDmdJVBJb?JO8&0W zSMRGrZv$&wag3e5>KyE9j#ERvO-WH~YR=VSJcC-7uO`>G&R5IoBQ9^FUgOVkHZPw$ zRiG_(2&d;`qNYin$rLlBCrd1)L_A}v;#r~thUCX2rUj$21PzcpCt_kU8CDrIWMeR= zSAZx875Hz%|IfnEZD=&Kd6=H_V<4w(<~{uuyftS-!Rszu`)qVBX!j>U`Lg|ec8-gq z8?)x5#aH5UZFKoceeN#}x9qW~`;%z*l>qBAf>T$W zmr0GU_DjS4|3l+Q*Xr_g)%zO0G+m94CAB|I*9hyTJl4${9eyE37J(mlx8kG)Px2ap zCoTL5nLo2l>q!fqujJ=d{M^RRs{z-;Av(?0k#_Hz$&Tj~ISHR6Yp`KS?xgOsdmHbN zox0P8)1-Cdh;G@~=e6mjvZ5u$2ovMFLkz`bMR%#;uuKl_l-H%Z@nj?>DUoCp*WyL< zx#$U2JYC)2{Ltz45JCpKgQXMULOVYYikJF@W;|NCKuXH<8)uis!CbXm!_c~Y& z8ZhD}TSKRHIO!!JDJ-U9NpH9Ah>D5CgznH%3LL*P7?a>YbVq=RW0LNS%aLdjkKyi< z2m|cnaxA3VgJLYMI|Gbf>W;6aWC=goxEw|h5m2ecxU3;)2#ZP(ons&iooxX*f@X1W z9w1aTrrV;x6eA}vBKQ&!b-R?1gSsOwM)2c|0G6bH7CA;pVMJA#1Y9jD16O-UWTUzx z#<8%6MFcN)4bn#YBpOC=rxHexe2CDKfF&NKKwT1zL35Z%T2yyLn1oU6fmjMiGAX1x z6DdYfk8?sI_0gRw6O{J9FNLLU=fIk!BOI1QjQSIBr!rPicz3sA6H{xfRse0 z+rsKa-KLJm2$k_s-5C~R&?^W*jU_+{(ZeYjkBT&CGR25pj)bU$MBOo}iZKzAP?b?> zLbnA)#AqQEoQ$drOtvRf@S-H8$U&8XS0N@w2zVl@N{W0@caF=G!~o~0!WmUEE;1&o zz*mu#3!+}4iSZN@Ws)Oj!>A0_ITK7)f|Bksel-oaOH73zS!qB!PW}Qt#S11W}#6%(uN0bu=lcQn^=!L{!6a%F2z9cNcSWv@AOeDS+hN-7FL2Bah~ z7?sfl^v1+^0+ht$Am#(G1?CFSA>CFLh0seYQ8fW(iz&ZG5}l<(b56(p|v15h<9(30m2kTKW<>KhylbDK?D?# z1e=PUw5lO$B@Q1-N$A3B=#DWpLhK8h0ue)K9^;sXR2GKWfQ6w64F;hTR1I@>l!;^u z&=Zfyt`LuDGQ%8=0=EQ{WH5P@y5cUSj3&7qtn0VElBAg^_V0=cc65TrURaW#ttwE2s*|^ zKF6SQuxUYfW3F=K<6tGok?FPs%?{GIAVwjRjA;(D!am?b$T0IniW&o-Y+wWXB|W!? zX?jDAhzrsL*i2T`u5fDsn*tU&l4t@>aV9v>k~AhM(`1>T#-wrd9yWiJIS@~vDGWcm z#m5ElK_IE5;;=GY-$HDG-ay@CvToomO$Mi^07aT)G7KoD37myJD8qA<@U=9<#Ta-` zRFKkfE)ST)$`R*CH(^CTATc!)Sduag%3A2o4hDn0U>8jwR!0X5?~0`5oP zMA4bFh6PnJ=g?xvLqkN9l!8%&5()U~BmxkUvZx@$8IwZ<1cQc<%Hn#8XoeI(qSvIC zxV4SRFuE*-u0V)r1a}EZ2TMr^m?#3ps|zU!I3Qr45fMP*8HSHjC-nljP(s#HBzUpZ zBn&2iIus7a20i6UD(Wr?aRFjL>IF3=I#SR+XDpT?EMQ?FUP2rsfO=>ubwzvw79s`1 z0vI%^BFZ8QO0>q|aEVqMf;%|VND8lFV28wHF8-JdhfZ`6XB?OfK-kF*C^`pWJ1MvX zPif}35)vYg6a|Oek}7Nk!LXB%NLv+%0NM&A6a&&E##Ao=G_;lQQA8%gO9#{-x`zmV z(iI{RTF=Bu3_}c$Cjf*)0wXblp{wZmQ$7yVlaQ0)zyTL*QZI0$N^xRI0zoU*5bV!5 zSx{FJ1|m+?P*)qN1Ka~9Oi)gRDB8m0#RyP}0ND4Jyc^_&@kvnwB5-$xp)nu?_+q{Q zF_m})RfU3;z$OO85UCZzGEHUwjYEJVPpVK>>!=LZK;pNLisLv#A3e!P-F+@}~#wDiPM^kZ< z3-@>Q7T6*YY~TlL##=z|tx>4(DA)^MLLx$Bud-i&3idl9#Tl7MDHFhGRK>7ZjFHT- z3X99Ep1+6329{tDDWi(u3M0WXi@Jqq0rOoENxqN*hPsvXM$YdsZGXS-=f~bV zmRa}Qjs2N*#}~SL^p3uTj`jCyN?R)LHa;5E`++TVZ_zvUE_8SSXT^iy*fQEIWp(i?#GwsHwRf)**PUsh{A1`;Z(!K2esI z?uc{^(2_g`C6j_6TjRTz@|}y3y_4%OYB@+k?ZTyVuhXQ;TdR zTij1^y9YQga!qLi4X#;a6P~zCiB0x}h`8AXAdG~bgQr0lQK{P^QqpT>r^(=*!$~6+ zdpvB2Ue@8|h>4703?}q-_s_^8%_thzC=ZFrSufPDyuACh-PyXeQ?7-oy7{VgnW}a3 zRo+aMcV_*3&yh^eksI4HJ^pOfVA?gvhG}dkw>=a6!F*~Mz#dcPL!M%i^jkUsYM7Fs z{1_-PuNHrO7QdxZ@L9Lm%uP{`5V3jfsUM?_+b1O_lOEVJ<`GAesl~MNqp1Omps_dh z=tAdOLB@EZ*Yh|cPpt;NOZMvS)6CAv%FZQZ^n2}W1R^jL#EF#vWf|?V5>}Dw&1(vy z*{G3pfLu;rv`5pIBF%mZI9d-*(3k2Z`ts!Fc~@J;)pq4##17pP7x$ zo}GI+UElwa>p0gKVItNgFoPTkWxYB74C!!$ zSR-cCfL(?~eWXkZlSy&8W)jbsAuiKQV z+ceu~JmQMF#G<(Xnk%In>iIA)R!rIQ9?s^s)d_=k&DsqkUMC!G6$HgzVCC`~Vi+-Z zpvVK;?@-2^5}&XhTO|l(f=5s=9hSkX5Crvrsr(_Z`51~W{C4y~y})9hd<$cGl$v8L z{`#%vbn3vg^VvvHieuO;;h;%HdsEAf0jf$^c9^uA@D%58wb^8V!oy|98o}?(w=H>S z+oE{*O9|RG+-AN-#iqm%~xug z8WRR>;3{Q)2fUZt@8ajON@0sbDIZ>uAFbbEIj5?^U*W6h1?NTRbq)s_-KLZuukyQmE`NnxP+Y~ZgVdq1%a5U7_$s$sQ_!}= z|Mpkrb?bRxveIljKt68S;j8kOgZ65F6-Lh+TMx&{PZ(L1@`sJ6R4Ua4V?L5T7hu)C z3?dHn}Ij2|tm4F^ z$32KGI#|+?0d7C{NfCw}ti37ifW&Z)NaNSvipH>r^d%V9sM5$GDS*vDjdoFnhL$)P z!-ovs?>M-RFoL$;C8k{|2ss|r8%&lCpBK9(H+5})K}Q`ISRvm*n0ALI^&L`!JOAg8 zfBa+r9WpPBIL#X9w3bd!;PfL4cLI~Oh5m-Sx6TypOLuy(Z5d5^BG=!KVHdU6Gr1Og zFdQIv6x@w1by2+@Zw(KOkk(63>e))EQ7si6b6w7JDSy3)lrEGf% z-DA;=K1pmlplCp^LE2Idk+U=X4U-6fBLx0OSQytBle;+X-N>;aJBLIY^7Qnhmxg7g zCApWZ<5^Wot&d<+3K>2W1?W{69I5sE6TK|wLTSCOmj{t#Mhb3Zf;KPQ*ltKhjq5Uc zg31DvI$YFUCU$xil{ipWv(AAMuX0E)MTS&mBXKRlrJ)gzx$`YN@gR|n5)B;>aJ_lI zPb6ziXpWe$MW|`G?0(JtM)`d8noRYYY<0(!wmD09&Y-_s`^jJTNk=!cvXn(8k?fxJ3&ptiNvK_sdhTbVRb!)k@ z;SF^%1~NW0`q!W>va%>uau0+Pvuvy)%2#Z96WNEiAsA73a!k zUrM(gyyOBcD?RhAn=`GOXV<4&H>X>VWL(Wtj=!&}|805gw~oGgG~J3|Xy$Bs>){(K zZj_~0oVr!+N3Yh7E3MPR)5p?HPfs2Gq^WhjsVmcjmg%OhbW`7}ho=r++Whx*&HPPI zrm1IkFx#{dUHG&zqAYjQaJ{Loi zQ>Y%yJ7sdDPM^zs2KV_zeM)&Ba=r(6ANN3!;Hv;rxbx-58eupedJR-Ae=7*Uf{#)R zYYG5-W!v+Vb|GL?oTr2VvIWZyzY~zkbB;afFUdQ0@bTD>;oS4V+W^`1WrwfA?_B0a zo#aG2co9w=P9x0r!PDld16aw_+%;TD}fm5{mp= z1sg%2kUvPWR08+eU*NRc@|}m&6+@qdVsqZ)(t|59dG8_%GqX3zv;)BmrDVPiJ!CJ^ z$w#MCbQ;8o=h|N}5-_fv*jx;R#NGPN2RPVV1YkP#jXP16AjY)axG;J@1yiSWVC;W+ zMfi5#a&>!z-KQoY8VJ98%IA&5dWz!^WcPy z56h_6RUjmi4^<)1j;|2_YV0`mIZ3CN=(J3F>6i!E?=?kOTp;%^HJiAG`S+6`mG>sVI%ZsLf zjX-<(@hI92;TH2!c~Ap+G3Fn^hCkR67GRZ62Nd>x9O2Spf_)dnc}oj~yEp@{kj%bE zr$-g;(kt*9~|O=Y5~TNB^-y&z0l z4qHDpe(nmwbJinP{2a9&vtkQn($a6eCkSUP$LVgsa)MemoUq=n7I~xhL*TXB`UpnP z@*tzVA{Vy<|0%8KvmD1~No39ngoEeH4|2~Bf@~N>uf5+`^pS(-GZERtMf*ro z4|^IE2=cWP-_v--Fp-OsjBKp78vqLBU`(<(+uYbSHM3_hyKV5Dy7am8nfj4if#`fd zy%|u`iR64@GLx9h2EKl?{_FQMGjIOB3*g2VF2%CS4~-`Beqo@9fh*`;bcXnB_?|1r zV4veY1Lo}4?PUKAGe$~&hlVpW-O0FCbA}dC?H>u8W@9y%w^`eWof`q;72#6fm4U0L zzjykr!MD%<@cg@dKRfo$u@47-b^aIU)2ICD(?i*VXEN)~W>*d0tRLo}?hpxl1>eso zeP&=qvyP>3N|7r%n-2+=uF5_6<$AA&t)$MR6iYriBJJLS|DT<}X}4vq;Gs+_Tolid zS{|yA`H*%@_>RLPm^spju+e2QD`rcUM)_ zyQf@tJA~$zsq))3jh9~hxTa%j@9oz1tGnLZHPf7JeR}HXubbAs@$%GR0I9CKwC$TO zT^XB>Wma#=R&7~mU7POMK0P>d6jcZ5*4|6BiodT>b@P>dR}X#fkdZQY_bWf!_0Fzq zeK&Sy4h-EIKA#?mWQU`f1JO)}oLzY#Q+wIYb{AdRM4vuE_#IDw4I(B55<5Lw2uIfwsE+2aB(5==}AFjLM zOrIS1@9taseA(7h+4@r%*QqI2e)Yd~&NVxkuHSddwLiD|FBMiO7L69EzbspS$Zr4T zRwvF}O_uDdkVWAO|D$k2G~0`r;DTj2c2A1!uF(F7X+{edsojVIyRq>1Td;FMj*cIQ z8KFvaz_uKvYgT=}3GT&nFgnts++nZbT z!P)xl#r8=S`@nkj8%si832;0O*rh{n_*|y1=8H#+HFSo?eJt#Y??gNr=6#QZ`56QA z0)FRrPICNq7jU^~=m$N>rxr|(E-rGFvZ4R-|BT;dkXN41A~^3W_dANEH_YS^kDc~| zGY@Pvkl#mIq{mn8D>HTJ;kqLtLGy6E`2wzM&>*+3Vj!PPS;V417Ddt_2;#|G^JAdR zyjuJ%=v_R(!DrGYjlQbI0Ov7f7VCj;AiYNiQ09nlI_Z{8x<-z`4yCKcN2*2)N43zjaa7B9heiqk@nB9P-kPDC@oOZo>T}<+F6p8_aeqKp ztlb$nUyW9q*e_}nY8&TkyqOwQm}P6WOgXU`QQi2h$yX=SEl=IWLI=&8#Yb#E!3{g)UKb}f4i#o z(w1*t%x%7WRMovuy>h;KZKitd^!|^l*WW7@YFj?56q-Ay4lPtQPOr|iZ2qWf^Frg= zOk>x(HmryGf7*X-^R+;>=eg{rBbmk{mu$D|8ZT*YY|DjLdd~J88wtLn#>&w<|Uru>;3FWt{*CV1e8Mx5Ubouma zr@udV_57RXf6_Pq)V|D9`#uc+O8JG7_7A3q&Sd+}W>yVn8_qp8?rom5tK~O!tEcua zEU4dY$zR)()Hn6uE-}o&gemh#bsrBOair->Xi~wFa zj~dZOG{&ef-=SBfIfk*-f!5VUTJvoXw64juw)twQRo-hV@O1QmAArH*Co}$Cd^7yu zRLSDoB9R2ednQC)!{S)}o^gB=$qTM&hQiz}!Vi7dNpKqdjd`F!ll_SO|hM#OYuuQHVjz#BWjag9xMZsSa0KdG}Qz=4V z3C_gYT+I!3OUPIA+!B4zoHR1%C+Hd9BjJ$`#WZNiOJJ78Cw6>J$n=U_;9>kt4fY;& ze-bLL(R!P7iVh<=#G{`gMW6Q-;Y?Hizs89deO5OvEUWi~pIcsku~65XUb*+$6E{|7 z_6?@@o=%^Rr(2YCosuqBep^<#v{-{T-i;h{EVIX#-hC>4N=mnc({$#mVxboogHKxn-0)ty(y(oby9tnSU0_2wHt{oYe^ zZP&)rEyvS!$J6D9*tP zrsFrOj}!UbcU?mDChm%{J<7La{3Lz8GfIX32-!D43HtlJ7UR8%{JA$(BO|=H3SV(Zii+1IBO@U-I5NV{&@enEC%4VWgd4gX zp%*-n9;YiRb>hWgj6wzWbGoH^5BmI3jTzB+!^jB!q(xE=QW})W_-;(nFvL}AOG$o1 zb>UzIMkn$B+Ots2zYut};=TV|v0v}M*mdzYFm96kO!`n}t8(Bf#G*6s@QyNH2te@*+GKfl{=-R5YX zNzU${tD8OiqnGarxV^UabNacLw4m#n86WSmW)8&Aefq?yw0|gb;>_>(Eg%TbTAs5k z?0oi~ZL?z&p^2ZlI{vfJ?w#qF(PsD0T>R0-O#Ak`)MVk{ksCeflc(^7$wB|`=?QOZ zFqc^Btz!R!7|qvLd>3yVxA;yMA3pFuNs^qvpI?bEE^97GE^sbpk|rZdqnF|%n4|nt z31eZ!CAL(Gx@)JQYP(qmBS46PMsEAA&0;~8*=n(W<`OKGzY!{aBb5F|sQd?^cV6iI hq_ldzwB=@LOB#h Dict[str, Any]: + """ + Gibt die Altersrichtlinie für eine bestimmte Plattform zurück. + + Args: + platform: Name der Plattform + + Returns: + Dictionary mit der Altersrichtlinie + """ + platform = platform.lower() + return self.platform_policies.get(platform, self.platform_policies["default"]) + + def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None: + """ + Setzt oder aktualisiert die Altersrichtlinie für eine Plattform. + + Args: + platform: Name der Plattform + policy: Dictionary mit der Altersrichtlinie + """ + platform = platform.lower() + self.platform_policies[platform] = policy + logger.info(f"Altersrichtlinie für '{platform}' aktualisiert") + + def generate_birthday(self, platform: str = "default", age: Optional[int] = None) -> Tuple[datetime.date, str]: + """ + Generiert ein Geburtsdatum gemäß den Plattformrichtlinien. + + Args: + platform: Name der Plattform + age: Optionales spezifisches Alter + + Returns: + (Geburtsdatum als datetime.date, Formatiertes Geburtsdatum als String) + """ + policy = self.get_platform_policy(platform) + + # Aktuelles Datum + today = datetime.date.today() + + # Altersbereich bestimmen + min_age = policy["min_age"] + max_age = policy["max_age"] + + # Wenn ein spezifisches Alter angegeben ist, dieses verwenden + if age is not None: + if age < min_age: + logger.warning(f"Angegebenes Alter ({age}) ist kleiner als das Mindestalter " + f"({min_age}). Verwende Mindestalter.") + age = min_age + elif age > max_age: + logger.warning(f"Angegebenes Alter ({age}) ist größer als das Höchstalter " + f"({max_age}). Verwende Höchstalter.") + age = max_age + else: + # Zufälliges Alter im erlaubten Bereich + age = random.randint(min_age, max_age) + + # Berechne das Geburtsjahr + birth_year = today.year - age + + # Berücksichtige, ob der Geburtstag in diesem Jahr bereits stattgefunden hat + has_had_birthday_this_year = random.choice([True, False]) + + if not has_had_birthday_this_year: + birth_year -= 1 + + # Generiere Monat und Tag + if has_had_birthday_this_year: + # Geburtstag war bereits in diesem Jahr + birth_month = random.randint(1, today.month) + + if birth_month == today.month: + # Wenn gleicher Monat, Tag muss vor oder gleich dem heutigen sein + birth_day = random.randint(1, today.day) + else: + # Wenn anderer Monat, beliebiger Tag + birth_day = random.randint(1, self._days_in_month(birth_month, birth_year)) + else: + # Geburtstag ist noch in diesem Jahr + birth_month = random.randint(today.month, 12) + + if birth_month == today.month: + # Wenn gleicher Monat, Tag muss nach dem heutigen sein + birth_day = random.randint(today.day + 1, self._days_in_month(birth_month, birth_year)) + else: + # Wenn anderer Monat, beliebiger Tag + birth_day = random.randint(1, self._days_in_month(birth_month, birth_year)) + + # Erstelle und formatiere das Geburtsdatum + birth_date = datetime.date(birth_year, birth_month, birth_day) + formatted_date = birth_date.strftime(policy["date_format"]) + + logger.info(f"Geburtsdatum generiert: {formatted_date} (Alter: {age})") + + return birth_date, formatted_date + + def _days_in_month(self, month: int, year: int) -> int: + """ + Gibt die Anzahl der Tage in einem Monat zurück. + + Args: + month: Monat (1-12) + year: Jahr + + Returns: + Anzahl der Tage im angegebenen Monat + """ + if month in [4, 6, 9, 11]: + return 30 + elif month == 2: + # Schaltjahr prüfen + if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0): + return 29 + else: + return 28 + else: + return 31 + + def generate_birthday_components(self, platform: str = "default", age: Optional[int] = None) -> Dict[str, int]: + """ + Generiert die Komponenten eines Geburtsdatums (Tag, Monat, Jahr). + + Args: + platform: Name der Plattform + age: Optionales spezifisches Alter + + Returns: + Dictionary mit den Komponenten des Geburtsdatums (year, month, day) + """ + birth_date, _ = self.generate_birthday(platform, age) + + return { + "year": birth_date.year, + "month": birth_date.month, + "day": birth_date.day + } + + def is_valid_age(self, birth_date: datetime.date, platform: str = "default") -> bool: + """ + Überprüft, ob ein Geburtsdatum für eine Plattform gültig ist. + + Args: + birth_date: Geburtsdatum + platform: Name der Plattform + + Returns: + True, wenn das Alter gültig ist, sonst False + """ + policy = self.get_platform_policy(platform) + + # Aktuelles Datum + today = datetime.date.today() + + # Alter berechnen + age = today.year - birth_date.year + + # Berücksichtigen, ob der Geburtstag in diesem Jahr bereits stattgefunden hat + if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day): + age -= 1 + + return policy["min_age"] <= age <= policy["max_age"] + + def generate_age_from_date(self, birth_date: datetime.date) -> int: + """ + Berechnet das Alter basierend auf einem Geburtsdatum. + + Args: + birth_date: Geburtsdatum + + Returns: + Berechnetes Alter + """ + # Aktuelles Datum + today = datetime.date.today() + + # Alter berechnen + age = today.year - birth_date.year + + # Berücksichtigen, ob der Geburtstag in diesem Jahr bereits stattgefunden hat + if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day): + age -= 1 + + return age + + def generate_date_from_components(self, year: int, month: int, day: int, platform: str = "default") -> str: + """ + Formatiert ein Datum aus seinen Komponenten gemäß dem Plattformformat. + + Args: + year: Jahr + month: Monat + day: Tag + platform: Name der Plattform + + Returns: + Formatiertes Geburtsdatum als String + """ + policy = self.get_platform_policy(platform) + + try: + birth_date = datetime.date(year, month, day) + formatted_date = birth_date.strftime(policy["date_format"]) + return formatted_date + except ValueError as e: + logger.error(f"Ungültiges Datum: {year}-{month}-{day}, Fehler: {e}") + # Fallback: Gültiges Datum zurückgeben + return self.generate_birthday(platform)[1] + + def generate_random_date(self, start_year: int, end_year: int, platform: str = "default") -> str: + """ + Generiert ein zufälliges Datum innerhalb eines Jahresbereichs. + + Args: + start_year: Startjahr + end_year: Endjahr + platform: Name der Plattform + + Returns: + Formatiertes Datum als String + """ + policy = self.get_platform_policy(platform) + + year = random.randint(start_year, end_year) + month = random.randint(1, 12) + day = random.randint(1, self._days_in_month(month, year)) + + date = datetime.date(year, month, day) + + return date.strftime(policy["date_format"]) + +def generate_birthday(age: int = None, platform: str = "default") -> str: + """ + Kompatibilitätsfunktion für ältere Codeversionen. + Generiert ein Geburtsdatum basierend auf einem Alter. + + Args: + age: Alter in Jahren (optional) + platform: Name der Plattform + + Returns: + Generiertes Geburtsdatum im Format "TT.MM.JJJJ" + """ + # Logger-Warnung für Legacy-Funktion + logger.warning("Die Funktion generate_birthday() ist für Kompatibilität, bitte verwende stattdessen die BirthdayGenerator-Klasse.") + + # Eine Instanz der Generator-Klasse erstellen und die Methode aufrufen + generator = BirthdayGenerator() + + # Geburtsdatum generieren + _, formatted_date = generator.generate_birthday(platform, age) + + return formatted_date \ No newline at end of file diff --git a/utils/email_handler.py b/utils/email_handler.py new file mode 100644 index 0000000..31512fc --- /dev/null +++ b/utils/email_handler.py @@ -0,0 +1,646 @@ +""" +E-Mail-Handler für den Social Media Account Generator. +Verwaltet den Abruf von Bestätigungscodes und E-Mail-Verifizierungen. +""" + +import os +import json +import logging +import time +import imaplib +import email +import re +from typing import Dict, List, Any, Optional, Tuple, Union +from email.header import decode_header +from datetime import datetime, timedelta + +from utils.text_similarity import TextSimilarity + +logger = logging.getLogger("email_handler") + +class EmailHandler: + """ + Handler für den Zugriff auf E-Mail-Dienste und den Abruf von Bestätigungscodes. + """ + + CONFIG_FILE = os.path.join("config", "email_config.json") + + def __init__(self): + """Initialisiert den EmailHandler und lädt die Konfiguration.""" + self.config = self.load_config() + + # Stelle sicher, dass das Konfigurationsverzeichnis existiert + os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True) + + # TextSimilarity-Instanz für Fuzzy-Matching + self.text_similarity = TextSimilarity(default_threshold=0.75) + + # Cache für die letzten erfolgreichen Verbindungsdaten + self.last_connection = None + + # Typische Betreffzeilen für Verifizierungs-E-Mails nach Plattform + self.verification_subjects = { + "instagram": [ + "Bestätige deine E-Mail-Adresse", + "Bestätigungscode für Instagram", + "Dein Instagram-Code", + "Bestätige deinen Instagram-Account", + "Verify your email address", + "Instagram Verification Code", + "Your Instagram Code", + "Verify your Instagram account", + "Instagram-Bestätigungscode", + "Instagram security code" + ], + "facebook": [ + "Bestätigungscode für Facebook", + "Facebook-Bestätigungscode", + "Dein Facebook-Code", + "Facebook Verification Code", + "Your Facebook Code" + ], + "twitter": [ + "Bestätige dein Twitter-Konto", + "Twitter-Bestätigungscode", + "Verify your Twitter account", + "Twitter Verification Code" + ], + "tiktok": [ + "TikTok-Bestätigungscode", + "Bestätige dein TikTok-Konto", + "TikTok Verification Code", + "Verify your TikTok account" + ], + "default": [ + "Bestätigungscode", + "Verification Code", + "Account Verification", + "Konto-Bestätigung", + "Security Code", + "Sicherheitscode" + ] + } + + logger.info("E-Mail-Handler initialisiert") + + def load_config(self) -> Dict[str, Any]: + """ + Lädt die E-Mail-Konfiguration aus der Konfigurationsdatei. + + Returns: + Dict[str, Any]: Die geladene Konfiguration oder Standardwerte + """ + default_config = { + "imap_server": "imap.ionos.de", + "imap_port": 993, + "imap_user": "info@z5m7q9dk3ah2v1plx6ju.com", + "imap_pass": "cz&ie.O9$!:!tYY@" + } + + try: + if os.path.exists(self.CONFIG_FILE): + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + config = json.load(f) + + logger.info("E-Mail-Konfiguration geladen") + return config + else: + # Standardwerte speichern + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(default_config, f, indent=2) + + logger.info("Standard-E-Mail-Konfiguration erstellt") + return default_config + + except Exception as e: + logger.error(f"Fehler beim Laden der E-Mail-Konfiguration: {e}") + return default_config + + def save_config(self) -> bool: + """ + Speichert die aktuelle Konfiguration in die Konfigurationsdatei. + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.config, f, indent=2) + + logger.info("E-Mail-Konfiguration gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der E-Mail-Konfiguration: {e}") + return False + + def get_config(self) -> Dict[str, Any]: + """ + Gibt die aktuelle Konfiguration zurück. + + Returns: + Dict[str, Any]: Die aktuelle Konfiguration + """ + return self.config + + def update_config(self, new_config: Dict[str, Any]) -> bool: + """ + Aktualisiert die Konfiguration mit den neuen Werten. + + Args: + new_config: Neue Konfiguration + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + try: + # Aktuelle Konfiguration speichern + old_config = self.config.copy() + + # Neue Werte übernehmen + self.config.update(new_config) + + # Konfiguration speichern + success = self.save_config() + + if not success: + # Bei Speicherfehler zur alten Konfiguration zurückkehren + self.config = old_config + return False + + return True + except Exception as e: + logger.error(f"Fehler beim Aktualisieren der E-Mail-Konfiguration: {e}") + return False + + def update_credentials(self, username: str, password: str) -> bool: + """ + Aktualisiert nur die Anmeldeinformationen. + + Args: + username: Benutzername + password: Passwort + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + return self.update_config({ + "imap_user": username, + "imap_pass": password + }) + + def update_server(self, server: str, port: int) -> bool: + """ + Aktualisiert nur die Serverinformationen. + + Args: + server: IMAP-Server + port: IMAP-Port + + Returns: + bool: True bei Erfolg, False bei Fehler + """ + return self.update_config({ + "imap_server": server, + "imap_port": port + }) + + def test_connection(self) -> Dict[str, Any]: + """ + Testet die Verbindung zum IMAP-Server. + + Returns: + Dict[str, Any]: Ergebnis des Tests + """ + try: + logger.info(f"Teste Verbindung zu {self.config['imap_server']}:{self.config['imap_port']}") + + # SSL-Verbindung zum IMAP-Server herstellen + mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) + + # Anmelden + mail.login(self.config["imap_user"], self.config["imap_pass"]) + + # Verfügbare Postfächer auflisten + status, mailboxes = mail.list() + + if status == 'OK': + mailbox_count = len(mailboxes) + + # INBOX auswählen + mail.select("INBOX") + + # Abmelden + mail.logout() + + # Verbindungsdaten im Cache speichern + self.last_connection = { + "server": self.config["imap_server"], + "port": self.config["imap_port"], + "username": self.config["imap_user"], + "password": self.config["imap_pass"] + } + + logger.info(f"Verbindungstest erfolgreich: {mailbox_count} Postfächer gefunden") + + return { + "success": True, + "server": self.config["imap_server"], + "port": self.config["imap_port"], + "mailbox_count": mailbox_count + } + else: + logger.error(f"Fehler beim Abrufen der Postfächer: {status}") + mail.logout() + return { + "success": False, + "error": f"Fehler beim Abrufen der Postfächer: {status}" + } + except imaplib.IMAP4.error as e: + logger.error(f"IMAP-Fehler: {e}") + return { + "success": False, + "error": f"IMAP-Fehler: {e}" + } + except Exception as e: + logger.error(f"Allgemeiner Fehler: {e}") + return { + "success": False, + "error": f"Allgemeiner Fehler: {e}" + } + + def search_emails(self, search_criteria: str = "ALL", max_emails: int = 5) -> List[Dict[str, Any]]: + """ + Sucht nach E-Mails mit den angegebenen Kriterien. + + Args: + search_criteria: IMAP-Suchkriterien + max_emails: Maximale Anzahl der abzurufenden E-Mails + + Returns: + List[Dict[str, Any]]: Liste der gefundenen E-Mails + """ + try: + # Verbindung zum IMAP-Server herstellen + mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) + + # Anmelden + mail.login(self.config["imap_user"], self.config["imap_pass"]) + + # INBOX auswählen + mail.select("INBOX") + + # Nach E-Mails suchen + status, data = mail.search(None, search_criteria) + + emails = [] + + if status == 'OK': + # E-Mail-IDs abrufen + email_ids = data[0].split() + + # Newest emails first + email_ids = list(reversed(email_ids)) + + # Begrenze die Anzahl der abzurufenden E-Mails + if max_emails > 0: + email_ids = email_ids[:max_emails] + + for email_id in email_ids: + # E-Mail abrufen + status, data = mail.fetch(email_id, '(RFC822)') + + if status == 'OK': + # E-Mail-Inhalt parsen + raw_email = data[0][1] + msg = email.message_from_bytes(raw_email) + + # Betreff decodieren + subject = decode_header(msg.get("Subject", ""))[0] + if isinstance(subject[0], bytes): + subject = subject[0].decode(subject[1] or 'utf-8', errors='replace') + else: + subject = subject[0] + + # Absender decodieren + from_addr = decode_header(msg.get("From", ""))[0] + if isinstance(from_addr[0], bytes): + from_addr = from_addr[0].decode(from_addr[1] or 'utf-8', errors='replace') + else: + from_addr = from_addr[0] + + # Empfänger decodieren + to_addr = decode_header(msg.get("To", ""))[0] + if isinstance(to_addr[0], bytes): + to_addr = to_addr[0].decode(to_addr[1] or 'utf-8', errors='replace') + else: + to_addr = to_addr[0] + + # Extrahiere E-Mail-Adresse aus dem To-Feld + to_email = self._extract_email_from_addr(to_addr) + + # Datum decodieren + date = msg.get("Date", "") + + # E-Mail-Text extrahieren + body = "" + + if msg.is_multipart(): + for part in msg.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + + if "attachment" not in content_disposition: + if content_type == "text/plain": + try: + # Textinhalt decodieren + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='replace') + break + except: + body = "[Fehler beim Decodieren des Inhalts]" + elif content_type == "text/html" and not body: + try: + # HTML-Inhalt decodieren + charset = part.get_content_charset() or 'utf-8' + body = part.get_payload(decode=True).decode(charset, errors='replace') + except: + body = "[Fehler beim Decodieren des HTML-Inhalts]" + else: + try: + # Einzel-Teil-E-Mail decodieren + charset = msg.get_content_charset() or 'utf-8' + body = msg.get_payload(decode=True).decode(charset, errors='replace') + except: + body = "[Fehler beim Decodieren des Inhalts]" + + # E-Mail-Informationen speichern + email_info = { + "id": email_id.decode(), + "subject": subject, + "from": from_addr, + "to": to_addr, + "to_email": to_email, + "date": date, + "body": body + } + + emails.append(email_info) + + # Abmelden + mail.logout() + + logger.info(f"{len(emails)} E-Mails gefunden") + return emails + + except Exception as e: + logger.error(f"Fehler beim Suchen nach E-Mails: {e}") + return [] + + def _extract_email_from_addr(self, addr_str: str) -> str: + """ + Extrahiert die E-Mail-Adresse aus einem Adressstring im Format 'Name '. + + Args: + addr_str: Adressstring + + Returns: + str: Die extrahierte E-Mail-Adresse oder der ursprüngliche String + """ + # Regulärer Ausdruck für die Extraktion der E-Mail-Adresse + email_pattern = r'?' + match = re.search(email_pattern, addr_str) + + if match: + return match.group(1).lower() + + return addr_str.lower() + + def _is_subject_relevant(self, subject: str, platform: str) -> bool: + """ + Prüft, ob der Betreff relevant für eine Verifizierungs-E-Mail der angegebenen Plattform ist. + Verwendet Fuzzy-Matching für die Erkennung. + + Args: + subject: Betreff der E-Mail + platform: Plattform (instagram, facebook, twitter, etc.) + + Returns: + bool: True, wenn der Betreff relevant ist, False sonst + """ + # Standardschwellenwert für Fuzzy-Matching + threshold = 0.75 + + # Betreffzeilen für die angegebene Plattform und Standard + subject_patterns = self.verification_subjects.get(platform.lower(), []) + subject_patterns += self.verification_subjects["default"] + + # Prüfe auf exakte Übereinstimmung (schneller) + for pattern in subject_patterns: + if pattern.lower() in subject.lower(): + logger.debug(f"Relevanter Betreff gefunden (exakte Übereinstimmung): {subject}") + return True + + # Wenn keine exakte Übereinstimmung, Fuzzy-Matching verwenden + for pattern in subject_patterns: + similarity = self.text_similarity.similarity_ratio(pattern.lower(), subject.lower()) + if similarity >= threshold: + logger.debug(f"Relevanter Betreff gefunden (Fuzzy-Matching, {similarity:.2f}): {subject}") + return True + + # Alternativ: Prüfe, ob der Betreff den Pattern enthält (mit Fuzzy-Matching) + if self.text_similarity.contains_similar_text(subject.lower(), [pattern.lower()], threshold=threshold): + logger.debug(f"Relevanter Betreff gefunden (Fuzzy-Contains): {subject}") + return True + + return False + + def get_verification_code(self, target_email: Optional[str] = None, platform: str = "instagram", + max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]: + """ + Ruft einen Bestätigungscode von einer E-Mail ab. + + Args: + target_email: Ziel-E-Mail-Adresse oder None für alle + platform: Plattform (instagram, facebook, twitter, etc.) + max_attempts: Maximale Anzahl an Versuchen + delay_seconds: Verzögerung zwischen Versuchen in Sekunden + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + logger.info(f"Suche nach Bestätigungscode für {platform} mit E-Mail {target_email or 'alle'}") + + # Domain aus der Ziel-E-Mail-Adresse extrahieren, falls vorhanden + target_domain = None + if target_email and "@" in target_email: + target_domain = target_email.split("@")[-1].lower() + logger.debug(f"Ziel-Domain: {target_domain}") + + # Letzter Tag als Suchkriterium + today = datetime.now() + yesterday = today - timedelta(days=1) + date_str = yesterday.strftime("%d-%b-%Y") + + search_criteria = f'(SINCE "{date_str}")' + + # E-Mail-Abruf mit Wiederholungsversuch + for attempt in range(max_attempts): + logger.debug(f"Versuch {attempt + 1}/{max_attempts}, Abruf neuer E-Mails...") + + # Alle neuen E-Mails abrufen + emails = self.search_emails(search_criteria, max_emails=10) + + # E-Mails filtern und nach Bestätigungscode suchen + for email_info in emails: + # Extrahierte E-Mail-Adresse des Empfängers + to_email = email_info.get("to_email", "").lower() + + # Wenn eine bestimmte Ziel-E-Mail angegeben ist, prüfe auf exakte Übereinstimmung + if target_email and target_email.lower() != to_email: + # Wenn keine exakte Übereinstimmung, prüfe auf Domain-Übereinstimmung (für Catch-All) + if not target_domain or target_domain not in to_email: + logger.debug(f"E-Mail übersprungen: Empfänger {to_email} stimmt nicht mit Ziel {target_email} überein") + continue + + # Betreff auf Relevanz prüfen (mit Fuzzy-Matching) + subject = email_info.get("subject", "") + if not subject or not self._is_subject_relevant(subject, platform): + logger.debug(f"E-Mail übersprungen: Betreff '{subject}' ist nicht relevant") + continue + + # Nach Bestätigungscode im Text suchen + body = email_info.get("body", "") + code = self._extract_verification_code(body, platform) + + if code: + logger.info(f"Bestätigungscode gefunden: {code} (E-Mail an {to_email})") + return code + else: + logger.debug(f"Kein Code in relevanter E-Mail gefunden (Betreff: {subject})") + + # Wenn kein Code gefunden wurde und noch Versuche übrig sind, warten und erneut versuchen + if attempt < max_attempts - 1: + logger.debug(f"Kein Code gefunden, warte {delay_seconds} Sekunden...") + time.sleep(delay_seconds) + + logger.warning("Kein Bestätigungscode gefunden nach allen Versuchen") + return None + + def _extract_verification_code(self, text: str, platform: str = "instagram") -> Optional[str]: + """ + Extrahiert einen Bestätigungscode aus einem Text. + + Args: + text: Zu durchsuchender Text + platform: Plattform (instagram, facebook, twitter, etc.) + + Returns: + Optional[str]: Der gefundene Bestätigungscode oder None + """ + # Plattformspezifische Muster für Bestätigungscodes + patterns = { + "instagram": [ + r"Dein Code ist (\d{6})", + r"Your code is (\d{6})", + r"Bestätigungscode: (\d{6})", + r"Confirmation code: (\d{6})", + r"(\d{6}) ist dein Instagram-Code", + r"(\d{6}) is your Instagram code", + r"Instagram-Code: (\d{6})", + r"Instagram code: (\d{6})", + r"Instagram: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "facebook": [ + r"Dein Code ist (\d{5})", + r"Your code is (\d{5})", + r"Bestätigungscode: (\d{5})", + r"Confirmation code: (\d{5})", + r"Facebook-Code: (\d{5})", + r"Facebook code: (\d{5})", + r"Facebook: (\d{5})", + r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern + ], + "twitter": [ + r"Code: (\d{6})", + r"Verification code: (\d{6})", + r"Twitter-Code: (\d{6})", + r"Twitter code: (\d{6})", + r"Twitter: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "tiktok": [ + r"TikTok-Code: (\d{6})", + r"TikTok code: (\d{6})", + r"TikTok: (\d{6})", + r"[^\d](\d{6})[^\d]" # 6-stellige Zahl umgeben von Nicht-Ziffern + ], + "default": [ + r"Code[:\s]*(\d{4,8})", + r"[Vv]erification [Cc]ode[:\s]*(\d{4,8})", + r"[Bb]estätigungscode[:\s]*(\d{4,8})", + r"(\d{4,8}) is your code", + r"(\d{4,8}) ist dein Code", + r"[^\d](\d{6})[^\d]", # 6-stellige Zahl umgeben von Nicht-Ziffern + r"[^\d](\d{5})[^\d]" # 5-stellige Zahl umgeben von Nicht-Ziffern + ] + } + + # Plattformspezifische Muster verwenden + platform_patterns = patterns.get(platform.lower(), []) + + # Alle Muster dieser Plattform durchsuchen + for pattern in platform_patterns: + match = re.search(pattern, text) + if match: + code = match.group(1) + logger.debug(f"Code gefunden mit Muster '{pattern}': {code}") + return code + + # Wenn keine plattformspezifischen Muster gefunden wurden, Default-Muster verwenden + for pattern in patterns["default"]: + match = re.search(pattern, text) + if match: + code = match.group(1) + logger.debug(f"Code gefunden mit Default-Muster '{pattern}': {code}") + return code + + # Generische Suche nach Zahlen (für die jeweilige Plattform typische Länge) + code_length = 6 # Standard + if platform.lower() == "facebook": + code_length = 5 + + # Suche nach alleinstehenden Zahlen der richtigen Länge + generic_pattern = r"\b(\d{" + str(code_length) + r"})\b" + matches = re.findall(generic_pattern, text) + + if matches: + # Nehme die erste gefundene Zahl + code = matches[0] + logger.debug(f"Code gefunden mit generischem Muster: {code}") + return code + + logger.debug("Kein Code gefunden") + return None + + def get_confirmation_code(self, expected_email: str, search_criteria: str = "ALL", + max_attempts: int = 30, delay_seconds: int = 2) -> Optional[str]: + """ + Ruft einen Bestätigungscode von einer E-Mail ab (Kompatibilitätsmethode). + + Args: + expected_email: E-Mail-Adresse, von der der Code erwartet wird + search_criteria: IMAP-Suchkriterien + max_attempts: Maximale Anzahl an Versuchen + delay_seconds: Verzögerung zwischen Versuchen in Sekunden + + Returns: + Optional[str]: Der Bestätigungscode oder None, wenn nicht gefunden + """ + logger.info(f"Suche nach Bestätigungscode für {expected_email}") + + # Vermutete Plattform basierend auf der E-Mail-Adresse oder dem Inhalt + platform = "instagram" # Standard + + # Bestätigungscode abrufen + return self.get_verification_code(expected_email, platform, max_attempts, delay_seconds) \ No newline at end of file diff --git a/utils/human_behavior.py b/utils/human_behavior.py new file mode 100644 index 0000000..af4eb7c --- /dev/null +++ b/utils/human_behavior.py @@ -0,0 +1,488 @@ +""" +Menschliches Verhalten für den Social Media Account Generator. +""" + +import random +import time +import logging +from typing import Optional, Tuple, List, Dict, Any, Callable + +logger = logging.getLogger("human_behavior") + +class HumanBehavior: + """Klasse zur Simulation von menschlichem Verhalten für die Automatisierung.""" + + def __init__(self, speed_factor: float = 1.0, randomness: float = 0.5): + """ + Initialisiert die HumanBehavior-Klasse. + + Args: + speed_factor: Faktormultiplikator für die Geschwindigkeit (höher = schneller) + randomness: Faktor für die Zufälligkeit (0-1, höher = zufälliger) + """ + self.speed_factor = max(0.1, min(10.0, speed_factor)) # Begrenzung auf 0.1-10.0 + self.randomness = max(0.0, min(1.0, randomness)) # Begrenzung auf 0.0-1.0 + + # Typische Verzögerungen (in Sekunden) + self.delays = { + "typing_per_char": 0.05, # Verzögerung pro Zeichen beim Tippen + "mouse_movement": 0.5, # Verzögerung für Mausbewegung + "click": 0.1, # Verzögerung für Mausklick + "page_load": 2.0, # Verzögerung für das Laden einer Seite + "form_fill": 1.0, # Verzögerung zwischen Formularfeldern + "decision": 1.5, # Verzögerung für Entscheidungen + "scroll": 0.3, # Verzögerung für Scrollbewegungen + "verification": 5.0, # Verzögerung für Verifizierungsprozesse + "image_upload": 3.0, # Verzögerung für Bildupload + "navigation": 1.0 # Verzögerung für Navigation + } + + def sleep(self, delay_type: str, multiplier: float = 1.0) -> None: + """ + Pausiert die Ausführung für eine bestimmte Zeit, basierend auf dem Verzögerungstyp. + + Args: + delay_type: Typ der Verzögerung (aus self.delays) + multiplier: Zusätzlicher Multiplikator für die Verzögerung + """ + base_delay = self.delays.get(delay_type, 0.5) # Standardverzögerung, wenn der Typ nicht bekannt ist + + # Berechne die effektive Verzögerung + delay = base_delay * multiplier / self.speed_factor + + # Füge Zufälligkeit hinzu + if self.randomness > 0: + # Variiere die Verzögerung um ±randomness% + variation = 1.0 + (random.random() * 2 - 1) * self.randomness + delay *= variation + + # Stelle sicher, dass die Verzögerung nicht negativ ist + delay = max(0, delay) + + logger.debug(f"Schlafe für {delay:.2f}s ({delay_type})") + time.sleep(delay) + + def random_delay(self, min_seconds: float = 1.0, max_seconds: float = 3.0) -> None: + """ + Führt eine zufällige Wartezeit aus, um menschliches Verhalten zu simulieren. + + Args: + min_seconds: Minimale Wartezeit in Sekunden + max_seconds: Maximale Wartezeit in Sekunden + """ + return self._random_delay(min_seconds, max_seconds) + + def _random_delay(self, min_seconds: float = 1.0, max_seconds: float = 3.0) -> None: + """ + Führt eine zufällige Wartezeit aus, um menschliches Verhalten zu simulieren. + + Args: + min_seconds: Minimale Wartezeit in Sekunden + max_seconds: Maximale Wartezeit in Sekunden + """ + delay = random.uniform(min_seconds, max_seconds) + logger.debug(f"Zufällige Wartezeit: {delay:.2f} Sekunden") + time.sleep(delay) + + def type_text(self, text: str, on_char_typed: Optional[Callable[[str], None]] = None, + error_probability: float = 0.05, correction_probability: float = 0.9) -> str: + """ + Simuliert menschliches Tippen mit möglichen Tippfehlern und Korrekturen. + + Args: + text: Zu tippender Text + on_char_typed: Optionale Funktion, die für jedes getippte Zeichen aufgerufen wird + error_probability: Wahrscheinlichkeit für Tippfehler (0-1) + correction_probability: Wahrscheinlichkeit, Tippfehler zu korrigieren (0-1) + + Returns: + Der tatsächlich getippte Text (mit oder ohne Fehler) + """ + # Anpassen der Fehlerwahrscheinlichkeit basierend auf Zufälligkeit + adjusted_error_prob = error_probability * self.randomness + + result = "" + i = 0 + + while i < len(text): + char = text[i] + + # Potentieller Tippfehler + if random.random() < adjusted_error_prob: + # Auswahl eines Fehlertyps: + # - Falsches Zeichen (Tastatur-Nachbarn) + # - Ausgelassenes Zeichen + # - Doppeltes Zeichen + error_type = random.choices( + ["wrong", "skip", "double"], + weights=[0.6, 0.2, 0.2], + k=1 + )[0] + + if error_type == "wrong": + # Falsches Zeichen tippen (Tastatur-Nachbarn) + keyboard_neighbors = self.get_keyboard_neighbors(char) + if keyboard_neighbors: + wrong_char = random.choice(keyboard_neighbors) + result += wrong_char + if on_char_typed: + on_char_typed(wrong_char) + self.sleep("typing_per_char") + + # Entscheiden, ob der Fehler korrigiert wird + if random.random() < correction_probability: + # Löschen des falschen Zeichens + result = result[:-1] + if on_char_typed: + on_char_typed("\b") # Backspace + self.sleep("typing_per_char", 1.5) # Längere Pause für Korrektur + + # Korrektes Zeichen tippen + result += char + if on_char_typed: + on_char_typed(char) + self.sleep("typing_per_char") + else: + # Wenn keine Nachbarn gefunden werden, normales Zeichen tippen + result += char + if on_char_typed: + on_char_typed(char) + self.sleep("typing_per_char") + + elif error_type == "skip": + # Zeichen auslassen (nichts tun) + pass + + elif error_type == "double": + # Zeichen doppelt tippen + result += char + char + if on_char_typed: + on_char_typed(char) + on_char_typed(char) + self.sleep("typing_per_char") + + # Entscheiden, ob der Fehler korrigiert wird + if random.random() < correction_probability: + # Löschen des doppelten Zeichens + result = result[:-1] + if on_char_typed: + on_char_typed("\b") # Backspace + self.sleep("typing_per_char", 1.2) + else: + # Normales Tippen ohne Fehler + result += char + if on_char_typed: + on_char_typed(char) + self.sleep("typing_per_char") + + i += 1 + + return result + + def get_keyboard_neighbors(self, char: str) -> List[str]: + """ + Gibt die Tastatur-Nachbarn eines Zeichens zurück. + + Args: + char: Das Zeichen, für das Nachbarn gefunden werden sollen + + Returns: + Liste von benachbarten Zeichen + """ + # QWERTY-Tastaturlayout + keyboard_layout = { + "1": ["2", "q"], + "2": ["1", "3", "q", "w"], + "3": ["2", "4", "w", "e"], + "4": ["3", "5", "e", "r"], + "5": ["4", "6", "r", "t"], + "6": ["5", "7", "t", "y"], + "7": ["6", "8", "y", "u"], + "8": ["7", "9", "u", "i"], + "9": ["8", "0", "i", "o"], + "0": ["9", "-", "o", "p"], + "-": ["0", "=", "p", "["], + "=": ["-", "[", "]"], + "q": ["1", "2", "w", "a"], + "w": ["2", "3", "q", "e", "a", "s"], + "e": ["3", "4", "w", "r", "s", "d"], + "r": ["4", "5", "e", "t", "d", "f"], + "t": ["5", "6", "r", "y", "f", "g"], + "y": ["6", "7", "t", "u", "g", "h"], + "u": ["7", "8", "y", "i", "h", "j"], + "i": ["8", "9", "u", "o", "j", "k"], + "o": ["9", "0", "i", "p", "k", "l"], + "p": ["0", "-", "o", "[", "l", ";"], + "[": ["-", "=", "p", "]", ";", "'"], + "]": ["=", "[", "'", "\\"], + "a": ["q", "w", "s", "z"], + "s": ["w", "e", "a", "d", "z", "x"], + "d": ["e", "r", "s", "f", "x", "c"], + "f": ["r", "t", "d", "g", "c", "v"], + "g": ["t", "y", "f", "h", "v", "b"], + "h": ["y", "u", "g", "j", "b", "n"], + "j": ["u", "i", "h", "k", "n", "m"], + "k": ["i", "o", "j", "l", "m", ","], + "l": ["o", "p", "k", ";", ",", "."], + ";": ["p", "[", "l", "'", ".", "/"], + "'": ["[", "]", ";", "/"], + "z": ["a", "s", "x"], + "x": ["s", "d", "z", "c"], + "c": ["d", "f", "x", "v"], + "v": ["f", "g", "c", "b"], + "b": ["g", "h", "v", "n"], + "n": ["h", "j", "b", "m"], + "m": ["j", "k", "n", ","], + ",": ["k", "l", "m", "."], + ".": ["l", ";", ",", "/"], + "/": [";", "'", "."], + " ": ["c", "v", "b", "n", "m"] # Leertaste hat viele benachbarte Tasten + } + + # Für Großbuchstaben die Nachbarn der Kleinbuchstaben verwenden + if char.lower() != char and char.lower() in keyboard_layout: + return [neighbor.upper() if random.choice([True, False]) else neighbor + for neighbor in keyboard_layout[char.lower()]] + + return keyboard_layout.get(char, []) + + def mouse_move(self, from_point: Optional[Tuple[int, int]] = None, + to_point: Tuple[int, int] = (0, 0), + on_move: Optional[Callable[[Tuple[int, int]], None]] = None) -> None: + """ + Simuliert eine menschliche Mausbewegung mit natürlicher Beschleunigung und Verzögerung. + + Args: + from_point: Startpunkt der Bewegung (oder None für aktuelle Position) + to_point: Zielpunkt der Bewegung + on_move: Optionale Funktion, die für jede Zwischenposition aufgerufen wird + """ + # Wenn kein Startpunkt angegeben ist, einen zufälligen verwenden + if from_point is None: + from_point = (random.randint(0, 1000), random.randint(0, 800)) + + # Berechne die Entfernung + dx = to_point[0] - from_point[0] + dy = to_point[1] - from_point[1] + distance = (dx**2 + dy**2)**0.5 + + # Anzahl der Zwischenschritte basierend auf der Entfernung + steps = max(10, int(distance / 10)) + + # Berechne die Bewegungskurve (Bézierkurve) + # Zufällige Kontrollpunkte für eine natürliche Bewegung + control_point_1 = ( + from_point[0] + dx * 0.3 + random.randint(-int(distance/10), int(distance/10)), + from_point[1] + dy * 0.1 + random.randint(-int(distance/10), int(distance/10)) + ) + control_point_2 = ( + from_point[0] + dx * 0.7 + random.randint(-int(distance/10), int(distance/10)), + from_point[1] + dy * 0.9 + random.randint(-int(distance/10), int(distance/10)) + ) + + # Bewegung durchführen + for i in range(steps + 1): + t = i / steps + + # Kubische Bézierkurve + x = (1-t)**3 * from_point[0] + 3*(1-t)**2*t * control_point_1[0] + 3*(1-t)*t**2 * control_point_2[0] + t**3 * to_point[0] + y = (1-t)**3 * from_point[1] + 3*(1-t)**2*t * control_point_1[1] + 3*(1-t)*t**2 * control_point_2[1] + t**3 * to_point[1] + + # Runde auf ganze Zahlen + curr_point = (int(x), int(y)) + + # Callback aufrufen, wenn vorhanden + if on_move: + on_move(curr_point) + + # Verzögerung basierend auf der Position in der Bewegung + # Am Anfang und Ende langsamer, in der Mitte schneller + if i < 0.2 * steps or i > 0.8 * steps: + self.sleep("mouse_movement", 1.5 / steps) + else: + self.sleep("mouse_movement", 1.0 / steps) + + def click(self, double: bool = False, right: bool = False) -> None: + """ + Simuliert einen Mausklick mit menschlicher Verzögerung. + + Args: + double: True für Doppelklick, False für Einzelklick + right: True für Rechtsklick, False für Linksklick + """ + click_type = "right" if right else "left" + click_count = 2 if double else 1 + + for _ in range(click_count): + logger.debug(f"{click_type.capitalize()}-Klick") + self.sleep("click") + + if double and _ == 0: + # Kürzere Pause zwischen Doppelklicks + self.sleep("click", 0.3) + + def scroll(self, direction: str = "down", amount: int = 5, + on_scroll: Optional[Callable[[int], None]] = None) -> None: + """ + Simuliert Scrollen mit menschlicher Verzögerung. + + Args: + direction: "up" oder "down" + amount: Anzahl der Scroll-Ereignisse + on_scroll: Optionale Funktion, die für jedes Scroll-Ereignis aufgerufen wird + """ + if direction not in ["up", "down"]: + logger.warning(f"Ungültige Scrollrichtung: {direction}") + return + + # Vorzeichenwechsel für die Richtung + scroll_factor = -1 if direction == "up" else 1 + + for i in range(amount): + # Zufällige Variation der Scrollmenge + scroll_amount = scroll_factor * (random.randint(1, 3) if self.randomness > 0.5 else 1) + + logger.debug(f"Scrolle {direction} ({scroll_amount})") + + if on_scroll: + on_scroll(scroll_amount) + + # Verzögerung zwischen Scroll-Ereignissen + if i < amount - 1: # Keine Verzögerung nach dem letzten Scroll + self.sleep("scroll") + + def wait_for_page_load(self, multiplier: float = 1.0) -> None: + """ + Wartet eine angemessene Zeit auf das Laden einer Seite. + + Args: + multiplier: Multiplikator für die Standardwartezeit + """ + self.sleep("page_load", multiplier) + + def wait_between_actions(self, action_type: str = "decision", multiplier: float = 1.0) -> None: + """ + Wartet zwischen Aktionen, um menschliches Verhalten zu simulieren. + + Args: + action_type: Art der Aktion + multiplier: Multiplikator für die Standardwartezeit + """ + self.sleep(action_type, multiplier) + + def navigate_sequence(self, steps: int, min_delay: float = 0.5, max_delay: float = 2.0) -> None: + """ + Simuliert eine Sequenz von Navigationsschritten mit variierenden Verzögerungen. + + Args: + steps: Anzahl der Navigationsschritte + min_delay: Minimale Verzögerung zwischen Schritten + max_delay: Maximale Verzögerung zwischen Schritten + """ + for i in range(steps): + # Zufällige Verzögerung zwischen Navigationsschritten + delay = random.uniform(min_delay, max_delay) + + logger.debug(f"Navigationsschritt {i+1}/{steps}, Verzögerung: {delay:.2f}s") + time.sleep(delay / self.speed_factor) + + def human_delay_pattern(self, action_type: str = "default", intensity: str = "medium") -> None: + """ + Erzeugt ein komplexes, menschliches Verzögerungsmuster. + + Args: + action_type: Art der Aktion (entscheidet über Basismuster) + intensity: Intensität des Musters ("low", "medium", "high") + """ + # Verzögerungsmuster basierend auf Aktionstyp und Intensität + patterns = { + "default": { + "low": (0.2, 0.5), + "medium": (0.5, 1.0), + "high": (1.0, 2.0) + }, + "reading": { + "low": (1.0, 2.0), + "medium": (2.0, 4.0), + "high": (3.0, 6.0) + }, + "thinking": { + "low": (1.5, 3.0), + "medium": (3.0, 5.0), + "high": (5.0, 8.0) + }, + "verification": { + "low": (3.0, 5.0), + "medium": (5.0, 8.0), + "high": (8.0, 12.0) + } + } + + # Standardmuster verwenden, wenn nicht bekannt + pattern = patterns.get(action_type, patterns["default"]) + delay_range = pattern.get(intensity, pattern["medium"]) + + # Zufällige Verzögerung im angegebenen Bereich + delay = random.uniform(delay_range[0], delay_range[1]) + + # Anpassung basierend auf Geschwindigkeit und Zufälligkeit + delay = delay / self.speed_factor + + if self.randomness > 0: + # Füge ein zufälliges "Zittern" hinzu + jitter = random.uniform(-0.2, 0.2) * self.randomness * delay + delay += jitter + + logger.debug(f"Menschliche Verzögerung ({action_type}, {intensity}): {delay:.2f}s") + time.sleep(max(0, delay)) + + def simulate_form_filling(self, fields: int, field_callback: Optional[Callable[[int], None]] = None) -> None: + """ + Simuliert das Ausfüllen eines Formulars mit menschlichem Verhalten. + + Args: + fields: Anzahl der auszufüllenden Felder + field_callback: Optionale Funktion, die für jedes Feld aufgerufen wird + """ + for i in range(fields): + logger.debug(f"Fülle Formularfeld {i+1}/{fields} aus") + + if field_callback: + field_callback(i) + + # Verzögerung zwischen Feldern + if i < fields - 1: # Keine Verzögerung nach dem letzten Feld + # Gelegentlich längere Pausen einbauen + if random.random() < 0.2 * self.randomness: + self.human_delay_pattern("thinking", "low") + else: + self.sleep("form_fill") + + def simulate_captcha_solving(self, on_progress: Optional[Callable[[float], None]] = None) -> None: + """ + Simuliert das Lösen eines CAPTCHAs mit menschlichem Verhalten. + + Args: + on_progress: Optionale Funktion, die mit dem Fortschritt (0-1) aufgerufen wird + """ + # Simuliere einen komplexen Prozess mit mehreren Schritten + steps = random.randint(4, 8) + + for i in range(steps): + progress = (i + 1) / steps + + logger.debug(f"CAPTCHA-Lösung Fortschritt: {progress:.0%}") + + if on_progress: + on_progress(progress) + + # Verschiedene Verzögerungsmuster für die einzelnen Schritte + if i == 0: + # Anfängliches Lesen/Verstehen + self.human_delay_pattern("reading", "medium") + elif i == steps - 1: + # Abschließende Überprüfung/Bestätigung + self.human_delay_pattern("verification", "low") + else: + # Auswahl/Interaktion + self.human_delay_pattern("thinking", "medium") \ No newline at end of file diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..d02aefd --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,69 @@ +""" +Logger-Konfiguration für die Social Media Account Generator Anwendung. +""" + +import os +import logging +import sys +from PyQt5.QtWidgets import QTextEdit +from PyQt5.QtGui import QTextCursor + +class LogHandler(logging.Handler): + """Handler, der Logs an ein QTextEdit-Widget sendet.""" + + def __init__(self, text_widget=None): + super().__init__() + self.text_widget = text_widget + if self.text_widget: + self.text_widget.setReadOnly(True) + + def emit(self, record): + msg = self.format(record) + if self.text_widget: + self.text_widget.append(msg) + # Scrolle nach unten + self.text_widget.moveCursor(QTextCursor.End) + +def setup_logger(name="main", level=logging.INFO): + """ + Konfiguriert und gibt einen Logger zurück. + + Args: + name: Name des Loggers + level: Logging-Level + + Returns: + Konfigurierter Logger + """ + logger = logging.getLogger(name) + + # Verhindere doppelte Handler + if logger.handlers: + return logger + + logger.setLevel(level) + + # Datehandler + log_file = os.path.join("logs", f"{name}.log") + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + logger.addHandler(file_handler) + + # Konsolen-Handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + logger.addHandler(console_handler) + + return logger + +def add_gui_handler(logger, text_widget): + """ + Fügt einem Logger einen GUI-Handler hinzu. + + Args: + logger: Logger, dem der Handler hinzugefügt werden soll + text_widget: QTextEdit-Widget für die Ausgabe + """ + gui_handler = LogHandler(text_widget) + gui_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + logger.addHandler(gui_handler) diff --git a/utils/password_generator.py b/utils/password_generator.py new file mode 100644 index 0000000..32cf02f --- /dev/null +++ b/utils/password_generator.py @@ -0,0 +1,338 @@ +""" +Passwortgenerator für den Social Media Account Generator. +""" + +import random +import string +import logging +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("password_generator") + +class PasswordGenerator: + """Klasse zur Generierung sicherer und plattformkonformer Passwörter.""" + + def __init__(self): + """Initialisiert den PasswordGenerator.""" + # Passwort-Richtlinien für verschiedene Plattformen + self.platform_policies = { + "instagram": { + "min_length": 6, + "max_length": 20, + "require_uppercase": False, + "require_lowercase": True, + "require_digits": False, + "require_special": False, + "allowed_special": "", + "disallowed_chars": "" + }, + "facebook": { + "min_length": 8, + "max_length": 20, + "require_uppercase": False, + "require_lowercase": True, + "require_digits": False, + "require_special": False, + "allowed_special": "!@#$%^&*()", + "disallowed_chars": "" + }, + "twitter": { + "min_length": 8, + "max_length": 20, + "require_uppercase": False, + "require_lowercase": True, + "require_digits": False, + "require_special": False, + "allowed_special": "!@#$%^&*()", + "disallowed_chars": "" + }, + "tiktok": { + "min_length": 8, + "max_length": 20, + "require_uppercase": False, + "require_lowercase": True, + "require_digits": True, + "require_special": False, + "allowed_special": "!@#$%^&*()", + "disallowed_chars": "" + }, + "default": { + "min_length": 8, + "max_length": 16, + "require_uppercase": True, + "require_lowercase": True, + "require_digits": True, + "require_special": True, + "allowed_special": "!@#$%^&*()", + "disallowed_chars": "" + } + } + + def get_platform_policy(self, platform: str) -> Dict[str, Any]: + """ + Gibt die Passwort-Richtlinie für eine bestimmte Plattform zurück. + + Args: + platform: Name der Plattform + + Returns: + Dictionary mit der Passwort-Richtlinie + """ + platform = platform.lower() + return self.platform_policies.get(platform, self.platform_policies["default"]) + + def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None: + """ + Setzt oder aktualisiert die Passwort-Richtlinie für eine Plattform. + + Args: + platform: Name der Plattform + policy: Dictionary mit der Passwort-Richtlinie + """ + platform = platform.lower() + self.platform_policies[platform] = policy + logger.info(f"Passwort-Richtlinie für '{platform}' aktualisiert") + + def generate_password(self, platform: str = "default", length: Optional[int] = None, + custom_policy: Optional[Dict[str, Any]] = None) -> str: + """ + Generiert ein Passwort gemäß den Richtlinien. + + Args: + platform: Name der Plattform + length: Optionale Länge des Passworts (überschreibt die Plattformrichtlinie) + custom_policy: Optionale benutzerdefinierte Richtlinie + + Returns: + Generiertes Passwort + """ + # Richtlinie bestimmen + if custom_policy: + policy = custom_policy + else: + policy = self.get_platform_policy(platform) + + # Länge bestimmen + if length: + if length < policy["min_length"]: + logger.warning(f"Angeforderte Länge ({length}) ist kleiner als das Minimum " + f"({policy['min_length']}). Verwende Minimum.") + length = policy["min_length"] + elif length > policy["max_length"]: + logger.warning(f"Angeforderte Länge ({length}) ist größer als das Maximum " + f"({policy['max_length']}). Verwende Maximum.") + length = policy["max_length"] + else: + # Zufällige Länge im erlaubten Bereich + length = random.randint(policy["min_length"], policy["max_length"]) + + # Verfügbare Zeichen bestimmen + available_chars = "" + + if policy["require_lowercase"] or not (policy["require_uppercase"] or + policy["require_digits"] or + policy["require_special"]): + available_chars += string.ascii_lowercase + + if policy["require_uppercase"]: + available_chars += string.ascii_uppercase + + if policy["require_digits"]: + available_chars += string.digits + + if policy["require_special"] and policy["allowed_special"]: + available_chars += policy["allowed_special"] + + # Entferne nicht erlaubte Zeichen + if policy["disallowed_chars"]: + available_chars = "".join(char for char in available_chars + if char not in policy["disallowed_chars"]) + + # Sicherstellen, dass keine leere Zeichenmenge vorliegt + if not available_chars: + logger.error("Keine Zeichen für die Passwortgenerierung verfügbar") + available_chars = string.ascii_lowercase + + # Passwort generieren + password = "".join(random.choice(available_chars) for _ in range(length)) + + # Überprüfen, ob die Anforderungen erfüllt sind + meets_requirements = True + + if policy["require_lowercase"] and not any(char.islower() for char in password): + meets_requirements = False + + if policy["require_uppercase"] and not any(char.isupper() for char in password): + meets_requirements = False + + if policy["require_digits"] and not any(char.isdigit() for char in password): + meets_requirements = False + + if policy["require_special"] and not any(char in policy["allowed_special"] for char in password): + meets_requirements = False + + # Falls die Anforderungen nicht erfüllt sind, erneut generieren + if not meets_requirements: + logger.debug("Generiertes Passwort erfüllt nicht alle Anforderungen, generiere neu") + return self.generate_password(platform, length, custom_policy) + + logger.info(f"Passwort für '{platform}' generiert (Länge: {length})") + + return password + + def generate_platform_password(self, platform: str) -> str: + """ + Generiert ein Passwort für eine bestimmte Plattform. + + Args: + platform: Name der Plattform + + Returns: + Generiertes Passwort + """ + return self.generate_password(platform) + + def generate_strong_password(self, length: int = 16) -> str: + """ + Generiert ein starkes Passwort. + + Args: + length: Länge des Passworts + + Returns: + Generiertes Passwort + """ + custom_policy = { + "min_length": length, + "max_length": length, + "require_uppercase": True, + "require_lowercase": True, + "require_digits": True, + "require_special": True, + "allowed_special": "!@#$%^&*()-_=+[]{}<>,.;:/?|", + "disallowed_chars": "" + } + + return self.generate_password(custom_policy=custom_policy) + + def generate_memorable_password(self, num_words: int = 3, separator: str = "-") -> str: + """ + Generiert ein einprägsames Passwort aus Wörtern und Zahlen. + + Args: + num_words: Anzahl der Wörter + separator: Trennzeichen zwischen den Wörtern + + Returns: + Generiertes Passwort + """ + # Liste von einfachen Wörtern (kann erweitert/angepasst werden) + words = [ + "time", "year", "people", "way", "day", "man", "thing", "woman", "life", "child", + "world", "school", "state", "family", "student", "group", "country", "problem", + "hand", "part", "place", "case", "week", "company", "system", "program", "question", + "work", "government", "number", "night", "point", "home", "water", "room", "mother", + "area", "money", "story", "fact", "month", "lot", "right", "study", "book", "eye", + "job", "word", "business", "issue", "side", "kind", "head", "house", "service", + "friend", "father", "power", "hour", "game", "line", "end", "member", "law", "car", + "city", "name", "team", "minute", "idea", "kid", "body", "back", "parent", "face", + "level", "office", "door", "health", "person", "art", "war", "history", "party", + "result", "change", "morning", "reason", "research", "girl", "guy", "moment", "air", + "teacher", "force", "education" + ] + + # Zufällige Wörter auswählen + selected_words = random.sample(words, num_words) + + # Groß- und Kleinschreibung variieren und Zahlen hinzufügen + for i in range(len(selected_words)): + if random.choice([True, False]): + selected_words[i] = selected_words[i].capitalize() + + # Mit 50% Wahrscheinlichkeit eine Zahl anhängen + if random.choice([True, False]): + selected_words[i] += str(random.randint(0, 9)) + + # Passwort zusammensetzen + password = separator.join(selected_words) + + logger.info(f"Einprägsames Passwort generiert (Länge: {len(password)})") + + return password + + def validate_password(self, password: str, platform: str = "default", + custom_policy: Optional[Dict[str, Any]] = None) -> Tuple[bool, str]: + """ + Überprüft, ob ein Passwort den Richtlinien entspricht. + + Args: + password: Zu überprüfendes Passwort + platform: Name der Plattform + custom_policy: Optionale benutzerdefinierte Richtlinie + + Returns: + (Gültigkeit, Fehlermeldung) + """ + # Richtlinie bestimmen + if custom_policy: + policy = custom_policy + else: + policy = self.get_platform_policy(platform) + + # Prüfungen durchführen + if len(password) < policy["min_length"]: + return False, f"Passwort ist zu kurz (mindestens {policy['min_length']} Zeichen erforderlich)" + + if len(password) > policy["max_length"]: + return False, f"Passwort ist zu lang (maximal {policy['max_length']} Zeichen erlaubt)" + + if policy["require_lowercase"] and not any(char.islower() for char in password): + return False, "Passwort muss mindestens einen Kleinbuchstaben enthalten" + + if policy["require_uppercase"] and not any(char.isupper() for char in password): + return False, "Passwort muss mindestens einen Großbuchstaben enthalten" + + if policy["require_digits"] and not any(char.isdigit() for char in password): + return False, "Passwort muss mindestens eine Ziffer enthalten" + + if policy["require_special"] and not any(char in policy["allowed_special"] for char in password): + return False, f"Passwort muss mindestens ein Sonderzeichen enthalten ({policy['allowed_special']})" + + if policy["disallowed_chars"] and any(char in policy["disallowed_chars"] for char in password): + return False, f"Passwort enthält nicht erlaubte Zeichen ({policy['disallowed_chars']})" + + return True, "Passwort ist gültig" + + +# Kompatibilitätsfunktion für Legacy-Code, der direkt generate_password() importiert +def generate_password(platform: str = "instagram", length: Optional[int] = None) -> str: + """ + Kompatibilitätsfunktion für ältere Codeversionen. + Generiert ein Passwort für die angegebene Plattform. + + Args: + platform: Name der Plattform + length: Optionale Länge des Passworts + + Returns: + Generiertes Passwort + """ + # Einmalige Logger-Warnung, wenn die Legacy-Funktion verwendet wird + logger.warning("Die Funktion generate_password() ist veraltet, bitte verwende stattdessen die PasswordGenerator-Klasse.") + + # Eine Instanz der Generator-Klasse erstellen und die Methode aufrufen + generator = PasswordGenerator() + return generator.generate_password(platform, length) + + +# Weitere Legacy-Funktionen für Kompatibilität +def generate_strong_password(length: int = 16) -> str: + """Legacy-Funktion für Kompatibilität.""" + generator = PasswordGenerator() + return generator.generate_strong_password(length) + + +def generate_memorable_password(num_words: int = 3, separator: str = "-") -> str: + """Legacy-Funktion für Kompatibilität.""" + generator = PasswordGenerator() + return generator.generate_memorable_password(num_words, separator) \ No newline at end of file diff --git a/utils/proxy_rotator.py b/utils/proxy_rotator.py new file mode 100644 index 0000000..40ef930 --- /dev/null +++ b/utils/proxy_rotator.py @@ -0,0 +1,413 @@ +# Path: p:/Chimaira/Code-Playwright/utils/proxy_rotator.py + +""" +Proxy-Rotations- und Verwaltungsfunktionalität. +""" + +import os +import json +import random +import logging +import requests +import time +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("proxy_rotator") + +class ProxyRotator: + """Klasse zur Verwaltung und Rotation von Proxies.""" + + CONFIG_FILE = os.path.join("config", "proxies.json") + + def __init__(self): + """Initialisiert den ProxyRotator und lädt die Konfiguration.""" + self.config = self.load_config() + self.current_proxy = None + self.last_rotation_time = 0 + + # Stelle sicher, dass das Konfigurationsverzeichnis existiert + os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True) + + def load_config(self) -> Dict[str, Any]: + """Lädt die Proxy-Konfiguration aus der Konfigurationsdatei.""" + if not os.path.exists(self.CONFIG_FILE): + return { + "ipv4": [], + "ipv6": [], + "mobile": [], + "mobile_api": { + "marsproxies": "", + "iproyal": "" + }, + "rotation_interval": 300 # 5 Minuten + } + + try: + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + config = json.load(f) + + logger.info(f"Proxy-Konfiguration geladen: {len(config.get('ipv4', []))} IPv4, " + f"{len(config.get('ipv6', []))} IPv6, {len(config.get('mobile', []))} Mobile") + + return config + except Exception as e: + logger.error(f"Fehler beim Laden der Proxy-Konfiguration: {e}") + return { + "ipv4": [], + "ipv6": [], + "mobile": [], + "mobile_api": { + "marsproxies": "", + "iproyal": "" + }, + "rotation_interval": 300 + } + + def save_config(self) -> bool: + """Speichert die Proxy-Konfiguration in die Konfigurationsdatei.""" + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.config, f, indent=2) + + logger.info("Proxy-Konfiguration gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Proxy-Konfiguration: {e}") + return False + + def get_config(self) -> Dict[str, Any]: + """Gibt die aktuelle Konfiguration zurück.""" + return self.config + + def update_config(self, new_config: Dict[str, Any]) -> bool: + """Aktualisiert die Konfiguration mit den neuen Werten.""" + try: + # Aktualisiere nur die bereitgestellten Schlüssel + for key, value in new_config.items(): + self.config[key] = value + + # Konfiguration speichern + return self.save_config() + except Exception as e: + logger.error(f"Fehler beim Aktualisieren der Proxy-Konfiguration: {e}") + return False + + def get_proxies_by_type(self, proxy_type: str) -> List[str]: + """Gibt eine Liste von Proxies des angegebenen Typs zurück.""" + if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]: + logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}") + return [] + + return self.config.get(proxy_type.lower(), []) + + def get_random_proxy(self, proxy_type: str) -> Optional[str]: + """Gibt einen zufälligen Proxy des angegebenen Typs zurück.""" + proxies = self.get_proxies_by_type(proxy_type) + + if not proxies: + logger.warning(f"Keine Proxies vom Typ '{proxy_type}' verfügbar") + return None + + return random.choice(proxies) + + def get_proxy(self, proxy_type=None): + """ + Gibt eine Proxy-Konfiguration für den angegebenen Typ zurück. + + Args: + proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufälligen Typ + + Returns: + Dict mit Proxy-Konfiguration oder None, wenn kein Proxy verfügbar ist + """ + try: + # Wenn kein Proxy-Typ angegeben ist, einen zufälligen verwenden + if proxy_type is None: + available_types = [] + if self.config.get("ipv4"): + available_types.append("ipv4") + if self.config.get("ipv6"): + available_types.append("ipv6") + if self.config.get("mobile"): + available_types.append("mobile") + + if not available_types: + logger.warning("Keine Proxies verfügbar") + return None + + proxy_type = random.choice(available_types) + + # Proxy vom angegebenen Typ holen + proxy_list = self.get_proxies_by_type(proxy_type) + + if not proxy_list: + logger.warning(f"Keine Proxies vom Typ '{proxy_type}' verfügbar") + return None + + # Zufälligen Proxy aus der Liste auswählen + proxy = random.choice(proxy_list) + + # Proxy-URL parsen + parts = proxy.split(":") + + if len(parts) >= 4: + # Format: host:port:username:password + host, port, username, password = parts[:4] + + return { + "server": f"http://{host}:{port}", + "username": username, + "password": password + } + elif len(parts) >= 2: + # Format: host:port + host, port = parts[:2] + + return { + "server": f"http://{host}:{port}" + } + else: + logger.warning(f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}") + return None + + except Exception as e: + logger.error(f"Fehler beim Abrufen des Proxys: {e}") + return None + + def get_next_proxy(self, proxy_type: str, force_rotation: bool = False) -> Optional[str]: + """ + Gibt den nächsten zu verwendenden Proxy zurück, unter Berücksichtigung des Rotationsintervalls. + + Args: + proxy_type: Typ des Proxys (ipv4, ipv6, mobile) + force_rotation: Erzwingt eine Rotation, unabhängig vom Zeitintervall + + Returns: + Proxy-String oder None, wenn kein Proxy verfügbar ist + """ + current_time = time.time() + interval = self.config.get("rotation_interval", 300) # Standardintervall: 5 Minuten + + # Rotation durchführen, wenn das Intervall abgelaufen ist oder erzwungen wird + if force_rotation or self.current_proxy is None or (current_time - self.last_rotation_time) > interval: + self.current_proxy = self.get_random_proxy(proxy_type) + self.last_rotation_time = current_time + + if self.current_proxy: + logger.info(f"Proxy rotiert zu: {self.mask_proxy_credentials(self.current_proxy)}") + + return self.current_proxy + + def test_proxy(self, proxy_type: str) -> Dict[str, Any]: + """ + Testet einen Proxy des angegebenen Typs. + + Args: + proxy_type: Typ des zu testenden Proxys + + Returns: + Dictionary mit Testergebnissen + """ + proxy = self.get_random_proxy(proxy_type) + + if not proxy: + return { + "success": False, + "error": f"Keine Proxies vom Typ '{proxy_type}' verfügbar" + } + + try: + # Proxy-URL parsen + parts = proxy.split(":") + + if len(parts) >= 4: + # Format: host:port:username:password + host, port, username, password = parts[:4] + proxy_url = f"http://{username}:{password}@{host}:{port}" + elif len(parts) >= 2: + # Format: host:port + host, port = parts[:2] + proxy_url = f"http://{host}:{port}" + else: + return { + "success": False, + "error": f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}" + } + + # Proxy-Konfiguration für requests + proxies = { + "http": proxy_url, + "https": proxy_url + } + + # Startzeit für Antwortzeit-Messung + start_time = time.time() + + # Test-Anfrage über den Proxy + response = requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=10) + + # Antwortzeit berechnen + response_time = time.time() - start_time + + if response.status_code == 200: + data = response.json() + ip = data.get("ip", "Unbekannt") + + # Länderinformationen abrufen (optional) + country = self.get_country_for_ip(ip) + + return { + "success": True, + "ip": ip, + "country": country, + "response_time": response_time, + "proxy_type": proxy_type + } + else: + return { + "success": False, + "error": f"Ungültige Antwort: HTTP {response.status_code}" + } + + except requests.exceptions.Timeout: + return { + "success": False, + "error": "Zeitüberschreitung bei der Verbindung" + } + except requests.exceptions.ProxyError: + return { + "success": False, + "error": "Proxy-Fehler: Verbindung abgelehnt oder fehlgeschlagen" + } + except Exception as e: + logger.error(f"Fehler beim Testen des Proxys: {e}") + return { + "success": False, + "error": str(e) + } + + def get_country_for_ip(self, ip: str) -> Optional[str]: + """ + Ermittelt das Land für eine IP-Adresse. + + Args: + ip: IP-Adresse + + Returns: + Ländername oder None im Fehlerfall + """ + try: + response = requests.get(f"https://ipapi.co/{ip}/json/", timeout=5) + + if response.status_code == 200: + data = response.json() + return data.get("country_name") + + return None + except Exception: + return None + + def mask_proxy_credentials(self, proxy: str) -> str: + """ + Maskiert die Anmeldeinformationen in einem Proxy-String für die Protokollierung. + + Args: + proxy: Original-Proxy-String + + Returns: + Maskierter Proxy-String + """ + parts = proxy.split(":") + + if len(parts) >= 4: + # Format: host:port:username:password + host, port = parts[0], parts[1] + return f"{host}:{port}:***:***" + + return proxy + + def add_proxy(self, proxy: str, proxy_type: str) -> bool: + """ + Fügt einen neuen Proxy zur Konfiguration hinzu. + + Args: + proxy: Proxy-String im Format host:port:username:password + proxy_type: Typ des Proxys (ipv4, ipv6, mobile) + + Returns: + True bei Erfolg, False bei Fehler + """ + if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]: + logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}") + return False + + proxy_list = self.config.get(proxy_type.lower(), []) + + if proxy not in proxy_list: + proxy_list.append(proxy) + self.config[proxy_type.lower()] = proxy_list + self.save_config() + + logger.info(f"Proxy hinzugefügt: {self.mask_proxy_credentials(proxy)} (Typ: {proxy_type})") + return True + + return False + + def remove_proxy(self, proxy: str, proxy_type: str) -> bool: + """ + Entfernt einen Proxy aus der Konfiguration. + + Args: + proxy: Proxy-String + proxy_type: Typ des Proxys (ipv4, ipv6, mobile) + + Returns: + True bei Erfolg, False bei Fehler + """ + if proxy_type.lower() not in ["ipv4", "ipv6", "mobile"]: + logger.warning(f"Ungültiger Proxy-Typ: {proxy_type}") + return False + + proxy_list = self.config.get(proxy_type.lower(), []) + + if proxy in proxy_list: + proxy_list.remove(proxy) + self.config[proxy_type.lower()] = proxy_list + self.save_config() + + logger.info(f"Proxy entfernt: {self.mask_proxy_credentials(proxy)} (Typ: {proxy_type})") + return True + + return False + + def format_proxy_for_playwright(self, proxy: str) -> Dict[str, str]: + """ + Formatiert einen Proxy-String für die Verwendung mit Playwright. + + Args: + proxy: Proxy-String im Format host:port:username:password + + Returns: + Dictionary mit Playwright-Proxy-Konfiguration + """ + parts = proxy.split(":") + + if len(parts) >= 4: + # Format: host:port:username:password + host, port, username, password = parts[:4] + + return { + "server": f"{host}:{port}", + "username": username, + "password": password + } + elif len(parts) >= 2: + # Format: host:port + host, port = parts[:2] + + return { + "server": f"{host}:{port}" + } + else: + logger.warning(f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}") + return {} \ No newline at end of file diff --git a/utils/text_similarity.py b/utils/text_similarity.py new file mode 100644 index 0000000..d509551 --- /dev/null +++ b/utils/text_similarity.py @@ -0,0 +1,558 @@ +""" +Textähnlichkeits-Funktionen für robustes UI-Element-Matching. +Ermöglicht flexibles Auffinden von UI-Elementen auch bei leichten Textänderungen. +""" + +import re +import logging +from typing import List, Dict, Any, Optional, Tuple, Union, Callable +from difflib import SequenceMatcher + +logger = logging.getLogger("text_similarity") + +class TextSimilarity: + """Klasse für Textähnlichkeitsfunktionen zum robusten UI-Element-Matching.""" + + def __init__(self, default_threshold: float = 0.8): + """ + Initialisiert die TextSimilarity-Klasse. + + Args: + default_threshold: Standardschwellenwert für Ähnlichkeitsprüfungen (0-1) + """ + self.default_threshold = max(0.0, min(1.0, default_threshold)) + + def levenshtein_distance(self, s1: str, s2: str) -> int: + """ + Berechnet die Levenshtein-Distanz zwischen zwei Strings. + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Die Levenshtein-Distanz (kleinere Werte = ähnlichere Strings) + """ + if s1 == s2: + return 0 + + # Strings für die Berechnung vorbereiten + s1 = s1.lower().strip() + s2 = s2.lower().strip() + + # Spezialfall: leere Strings + if len(s1) == 0: return len(s2) + if len(s2) == 0: return len(s1) + + # Initialisiere die Distanzmatrix + matrix = [[0 for x in range(len(s2) + 1)] for x in range(len(s1) + 1)] + + # Fülle die erste Zeile und Spalte + for i in range(len(s1) + 1): + matrix[i][0] = i + for j in range(len(s2) + 1): + matrix[0][j] = j + + # Fülle die Matrix + for i in range(1, len(s1) + 1): + for j in range(1, len(s2) + 1): + cost = 0 if s1[i-1] == s2[j-1] else 1 + matrix[i][j] = min( + matrix[i-1][j] + 1, # Löschen + matrix[i][j-1] + 1, # Einfügen + matrix[i-1][j-1] + cost # Ersetzen + ) + + return matrix[len(s1)][len(s2)] + + def similarity_ratio(self, s1: str, s2: str) -> float: + """ + Berechnet das Ähnlichkeitsverhältnis zwischen zwei Strings (0-1). + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Ähnlichkeitsverhältnis zwischen 0 (unähnlich) und 1 (identisch) + """ + # Strings für Vergleich normalisieren + s1 = s1.lower().strip() + s2 = s2.lower().strip() + + # Leere Strings behandeln + if len(s1) == 0 and len(s2) == 0: + return 1.0 + + # Maximale mögliche Distanz = Summe der Längen beider Strings + max_distance = max(len(s1), len(s2)) + if max_distance == 0: + return 1.0 + + # Levenshtein-Distanz berechnen + distance = self.levenshtein_distance(s1, s2) + + # Ähnlichkeitsverhältnis berechnen + similarity = 1.0 - (distance / max_distance) + + return similarity + + def sequence_matcher_ratio(self, s1: str, s2: str) -> float: + """ + Berechnet das Ähnlichkeitsverhältnis mit Pythons SequenceMatcher. + Oft genauer als einfaches Levenshtein für längere Texte. + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Ähnlichkeitsverhältnis zwischen 0 (unähnlich) und 1 (identisch) + """ + # Strings für Vergleich normalisieren + s1 = s1.lower().strip() + s2 = s2.lower().strip() + + # SequenceMatcher verwenden + return SequenceMatcher(None, s1, s2).ratio() + + def jaro_winkler_similarity(self, s1: str, s2: str) -> float: + """ + Berechnet die Jaro-Winkler-Ähnlichkeit, die Präfixübereinstimmungen berücksichtigt. + Gut für kurze Strings wie Namen oder IDs. + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Ähnlichkeitswert zwischen 0 (unähnlich) und 1 (identisch) + """ + # Strings für Vergleich normalisieren + s1 = s1.lower().strip() + s2 = s2.lower().strip() + + # Identische Strings + if s1 == s2: + return 1.0 + + # Leere Strings behandeln + if len(s1) == 0 or len(s2) == 0: + return 0.0 + + # Berechne die Jaro-Ähnlichkeit + + # Suche nach übereinstimmenden Zeichen innerhalb des Suchradius + search_range = max(len(s1), len(s2)) // 2 - 1 + search_range = max(0, search_range) + + # Initialisiere Übereinstimmungs- und Transpositionszähler + matches = 0 + transpositions = 0 + + # Markiere übereinstimmende Zeichen + s1_matches = [False] * len(s1) + s2_matches = [False] * len(s2) + + # Finde Übereinstimmungen + for i in range(len(s1)): + start = max(0, i - search_range) + end = min(i + search_range + 1, len(s2)) + + for j in range(start, end): + if not s2_matches[j] and s1[i] == s2[j]: + s1_matches[i] = True + s2_matches[j] = True + matches += 1 + break + + # Wenn keine Übereinstimmungen gefunden wurden + if matches == 0: + return 0.0 + + # Zähle Transpositionszeichen + k = 0 + for i in range(len(s1)): + if s1_matches[i]: + while not s2_matches[k]: + k += 1 + if s1[i] != s2[k]: + transpositions += 1 + k += 1 + + # Berechne Jaro-Ähnlichkeit + jaro = ( + matches / len(s1) + + matches / len(s2) + + (matches - transpositions // 2) / matches + ) / 3.0 + + # Berechne Jaro-Winkler-Ähnlichkeit mit Präfixbonus + prefix_len = 0 + max_prefix_len = min(4, min(len(s1), len(s2))) + + # Zähle übereinstimmende Präfixzeichen + while prefix_len < max_prefix_len and s1[prefix_len] == s2[prefix_len]: + prefix_len += 1 + + # Skalierungsfaktor für Präfixanpassung (Standard: 0.1) + scaling_factor = 0.1 + + # Berechne Jaro-Winkler-Ähnlichkeit + jaro_winkler = jaro + prefix_len * scaling_factor * (1 - jaro) + + return jaro_winkler + + def is_similar(self, s1: str, s2: str, threshold: float = None, method: str = "sequence") -> bool: + """ + Prüft, ob zwei Strings ähnlich genug sind, basierend auf einem Schwellenwert. + + Args: + s1: Erster String + s2: Zweiter String + threshold: Ähnlichkeitsschwellenwert (0-1), oder None für Standardwert + method: Ähnlichkeitsmethode ("levenshtein", "sequence", "jaro_winkler") + + Returns: + True, wenn die Strings ähnlich genug sind, False sonst + """ + if threshold is None: + threshold = self.default_threshold + + # Leere oder None-Strings behandeln + s1 = "" if s1 is None else str(s1) + s2 = "" if s2 is None else str(s2) + + # Wenn beide Strings identisch sind + if s1 == s2: + return True + + # Ähnlichkeitsmethode auswählen + if method == "levenshtein": + similarity = self.similarity_ratio(s1, s2) + elif method == "jaro_winkler": + similarity = self.jaro_winkler_similarity(s1, s2) + else: # "sequence" oder andere + similarity = self.sequence_matcher_ratio(s1, s2) + + return similarity >= threshold + + def find_most_similar(self, target: str, candidates: List[str], + method: str = "sequence") -> Tuple[str, float]: + """ + Findet den ähnlichsten String in einer Liste von Kandidaten. + + Args: + target: Zieltext, zu dem der ähnlichste String gefunden werden soll + candidates: Liste von Kandidatenstrings + method: Ähnlichkeitsmethode ("levenshtein", "sequence", "jaro_winkler") + + Returns: + Tuple (ähnlichster String, Ähnlichkeitswert) + """ + if not candidates: + return "", 0.0 + + # Ähnlichkeitsfunktion auswählen + if method == "levenshtein": + similarity_func = self.similarity_ratio + elif method == "jaro_winkler": + similarity_func = self.jaro_winkler_similarity + else: # "sequence" oder andere + similarity_func = self.sequence_matcher_ratio + + # Finde den ähnlichsten Kandidaten + similarities = [(candidate, similarity_func(target, candidate)) for candidate in candidates] + most_similar = max(similarities, key=lambda x: x[1]) + + return most_similar + + def get_similarity_scores(self, target: str, candidates: List[str], + method: str = "sequence") -> Dict[str, float]: + """ + Berechnet Ähnlichkeitswerte für alle Kandidaten. + + Args: + target: Zieltext + candidates: Liste von Kandidatenstrings + method: Ähnlichkeitsmethode + + Returns: + Dictionary mit {Kandidat: Ähnlichkeitswert} + """ + # Ähnlichkeitsfunktion auswählen + if method == "levenshtein": + similarity_func = self.similarity_ratio + elif method == "jaro_winkler": + similarity_func = self.jaro_winkler_similarity + else: # "sequence" oder andere + similarity_func = self.sequence_matcher_ratio + + # Berechne Ähnlichkeiten für alle Kandidaten + return {candidate: similarity_func(target, candidate) for candidate in candidates} + + def words_similarity(self, s1: str, s2: str) -> float: + """ + Berechnet die Ähnlichkeit basierend auf gemeinsamen Wörtern. + + Args: + s1: Erster String + s2: Zweiter String + + Returns: + Ähnlichkeitswert zwischen 0 (unähnlich) und 1 (identisch) + """ + # Strings in Wörter zerlegen + words1 = set(re.findall(r'\w+', s1.lower())) + words2 = set(re.findall(r'\w+', s2.lower())) + + # Leere Wortmengen behandeln + if not words1 and not words2: + return 1.0 + if not words1 or not words2: + return 0.0 + + # Berechne Jaccard-Ähnlichkeit + intersection = len(words1.intersection(words2)) + union = len(words1.union(words2)) + + return intersection / union + + def contains_similar_text(self, text: str, patterns: List[str], + threshold: float = None, method: str = "sequence") -> bool: + """ + Prüft, ob ein Text einen der Muster ähnlich enthält. + + Args: + text: Zu durchsuchender Text + patterns: Liste von zu suchenden Mustern + threshold: Ähnlichkeitsschwellenwert + method: Ähnlichkeitsmethode + + Returns: + True, wenn mindestens ein Muster ähnlich genug ist + """ + if threshold is None: + threshold = self.default_threshold + + # Wenn patterns leer ist oder Text None ist + if not patterns or text is None: + return False + + text = str(text).lower() + + for pattern in patterns: + pattern = str(pattern).lower() + + # Prüfe, ob der Text das Muster enthält + if pattern in text: + return True + + # Prüfe Ähnlichkeit mit Wörtern im Text + words = re.findall(r'\w+', text) + for word in words: + if self.is_similar(word, pattern, threshold, method): + return True + + return False + +def fuzzy_find_element(page, text_or_patterns, selector_type="button", threshold=0.8, + method="sequence", wait_time=5000) -> Optional[Any]: + """ + Findet ein Element basierend auf Textähnlichkeit. + + Args: + page: Playwright Page-Objekt + text_or_patterns: Zieltext oder Liste von Texten + selector_type: Art des Elements ("button", "link", "input", "any") + threshold: Ähnlichkeitsschwellenwert + method: Ähnlichkeitsmethode + wait_time: Wartezeit in Millisekunden + + Returns: + Gefundenes Element oder None + """ + similarity = TextSimilarity(threshold) + patterns = [text_or_patterns] if isinstance(text_or_patterns, str) else text_or_patterns + + try: + # Warte, bis die Seite geladen ist + page.wait_for_load_state("domcontentloaded", timeout=wait_time) + + # Selektoren basierend auf dem Element-Typ + if selector_type == "button": + elements = page.query_selector_all("button, input[type='button'], input[type='submit'], [role='button']") + elif selector_type == "link": + elements = page.query_selector_all("a, [role='link']") + elif selector_type == "input": + elements = page.query_selector_all("input, textarea, select") + else: # "any" + elements = page.query_selector_all("*") + + # Keine Elemente gefunden + if not elements: + logger.debug(f"Keine {selector_type}-Elemente auf der Seite gefunden") + return None + + # Für jedes Element den Text und die Ähnlichkeit prüfen + best_match = None + best_similarity = -1 + + for element in elements: + # Text aus verschiedenen Attributen extrahieren + element_text = "" + + # Inneren Text prüfen + inner_text = element.inner_text() + if inner_text and inner_text.strip(): + element_text = inner_text.strip() + + # Value-Attribut prüfen (für Eingabefelder) + if not element_text: + try: + value = element.get_attribute("value") + if value and value.strip(): + element_text = value.strip() + except: + pass + + # Placeholder prüfen + if not element_text: + try: + placeholder = element.get_attribute("placeholder") + if placeholder and placeholder.strip(): + element_text = placeholder.strip() + except: + pass + + # Aria-Label prüfen + if not element_text: + try: + aria_label = element.get_attribute("aria-label") + if aria_label and aria_label.strip(): + element_text = aria_label.strip() + except: + pass + + # Title-Attribut prüfen + if not element_text: + try: + title = element.get_attribute("title") + if title and title.strip(): + element_text = title.strip() + except: + pass + + # Wenn immer noch kein Text gefunden wurde, überspringen + if not element_text: + continue + + # Ähnlichkeit für jeden Pattern prüfen + for pattern in patterns: + sim_score = similarity.sequence_matcher_ratio(pattern, element_text) + + # Ist dieser Match besser als der bisherige beste? + if sim_score > best_similarity and sim_score >= threshold: + best_similarity = sim_score + best_match = element + + # Bei perfekter Übereinstimmung sofort zurückgeben + if sim_score >= 0.99: + logger.info(f"Element mit perfekter Übereinstimmung gefunden: '{element_text}'") + return element + + # Bestes Ergebnis zurückgeben, wenn es über dem Schwellenwert liegt + if best_match: + try: + match_text = best_match.inner_text() or best_match.get_attribute("value") or best_match.get_attribute("placeholder") + logger.info(f"Element mit Ähnlichkeit {best_similarity:.2f} gefunden: '{match_text}'") + except: + logger.info(f"Element mit Ähnlichkeit {best_similarity:.2f} gefunden") + + return best_match + + logger.debug(f"Kein passendes Element für die angegebenen Muster gefunden: {patterns}") + return None + + except Exception as e: + logger.error(f"Fehler beim Suchen nach ähnlichem Element: {e}") + return None + +def find_element_by_text(page, text, exact=False, selector="*", timeout=5000) -> Optional[Any]: + """ + Findet ein Element, das den angegebenen Text enthält oder ihm ähnlich ist. + + Args: + page: Playwright Page-Objekt + text: Zu suchender Text + exact: Ob exakte Übereinstimmung erforderlich ist + selector: CSS-Selektor zum Einschränken der Suche + timeout: Timeout in Millisekunden + + Returns: + Gefundenes Element oder None + """ + try: + if exact: + # Bei exakter Suche XPath verwenden + xpath = f"//{selector}[contains(text(), '{text}') or contains(@value, '{text}') or contains(@placeholder, '{text}')]" + return page.wait_for_selector(xpath, timeout=timeout) + else: + # Bei Ähnlichkeitssuche alle passenden Elemente finden + similarity = TextSimilarity(0.8) # 80% Schwellenwert + + # Warten auf DOM-Bereitschaft + page.wait_for_load_state("domcontentloaded", timeout=timeout) + + # Alle Elemente mit dem angegebenen Selektor finden + elements = page.query_selector_all(selector) + + for element in elements: + # Verschiedene Textattribute prüfen + element_text = element.inner_text() + if not element_text: + element_text = element.get_attribute("value") or "" + if not element_text: + element_text = element.get_attribute("placeholder") or "" + + # Ähnlichkeit prüfen + if similarity.is_similar(text, element_text): + return element + + return None + + except Exception as e: + logger.error(f"Fehler beim Suchen nach Element mit Text '{text}': {e}") + return None + +def click_fuzzy_button(page, button_text, threshold=0.7, timeout=5000) -> bool: + """ + Klickt auf einen Button basierend auf Textähnlichkeit. + + Args: + page: Playwright Page-Objekt + button_text: Text oder Textmuster des Buttons + threshold: Ähnlichkeitsschwellenwert + timeout: Timeout in Millisekunden + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + # Versuche, das Element zu finden + button = fuzzy_find_element(page, button_text, selector_type="button", + threshold=threshold, wait_time=timeout) + + if button: + # Scrolle zum Button und klicke + button.scroll_into_view_if_needed() + button.click() + logger.info(f"Auf Button mit Text ähnlich zu '{button_text}' geklickt") + return True + else: + logger.warning(f"Kein Button mit Text ähnlich zu '{button_text}' gefunden") + return False + + except Exception as e: + logger.error(f"Fehler beim Klicken auf Button mit Text '{button_text}': {e}") + return False \ No newline at end of file diff --git a/utils/theme_manager.py b/utils/theme_manager.py new file mode 100644 index 0000000..38cbe74 --- /dev/null +++ b/utils/theme_manager.py @@ -0,0 +1,133 @@ +""" +Theme Manager - Verwaltet das Erscheinungsbild der Anwendung (nur Light Mode) +""" + +import os +import json +import logging +from typing import Dict, Any, Optional +from PyQt5.QtWidgets import QApplication +from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtCore import Qt, QSettings + +logger = logging.getLogger("theme_manager") + +class ThemeManager: + """ + Verwaltet das Erscheinungsbild der Anwendung. + """ + + # Themennamen + LIGHT_THEME = "light" + + def __init__(self, app: QApplication): + """ + Initialisiert den ThemeManager. + + Args: + app: Die QApplication-Instanz + """ + self.app = app + self.settings = QSettings("Chimaira", "SocialMediaAccountGenerator") + self.current_theme = self.LIGHT_THEME + + # Basisverzeichnis ermitteln + self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # Stelle sicher, dass die Verzeichnisse existieren + os.makedirs(os.path.join(self.base_dir, "resources", "themes"), exist_ok=True) + os.makedirs(os.path.join(self.base_dir, "resources", "icons"), exist_ok=True) + + # Lade QSS-Dateien für Themes + self.theme_stylesheets = { + self.LIGHT_THEME: self._load_stylesheet("light.qss") + } + + # Wende das Light Theme an + self.apply_theme(self.LIGHT_THEME) + + logger.info(f"ThemeManager initialisiert mit Theme: {self.current_theme}") + + def _load_stylesheet(self, filename: str) -> str: + """Lädt ein QSS-Stylesheet aus einer Datei.""" + try: + stylesheet_path = os.path.join(self.base_dir, "resources", "themes", filename) + if os.path.exists(stylesheet_path): + with open(stylesheet_path, 'r', encoding='utf-8') as f: + return f.read() + else: + logger.warning(f"Stylesheet-Datei nicht gefunden: {stylesheet_path}") + # Erzeuge eine leere Stylesheet-Datei, wenn sie nicht existiert + with open(stylesheet_path, 'w', encoding='utf-8') as f: + f.write("/* Auto-generated empty stylesheet */\n") + return "" + except Exception as e: + logger.error(f"Fehler beim Laden des Stylesheets {filename}: {e}") + return "" + + def apply_theme(self, theme_name: str) -> bool: + """ + Wendet das Light Theme auf die Anwendung an. + + Args: + theme_name: Wird ignoriert, immer Light Theme verwendet + + Returns: + bool: True, wenn das Theme erfolgreich angewendet wurde, sonst False + """ + try: + # Palette für das Light Theme erstellen + palette = QPalette() + + # Light Theme Palette + palette.setColor(QPalette.Window, QColor(240, 240, 240)) + palette.setColor(QPalette.WindowText, Qt.black) + palette.setColor(QPalette.Base, Qt.white) + palette.setColor(QPalette.AlternateBase, QColor(245, 245, 245)) + palette.setColor(QPalette.ToolTipBase, Qt.white) + palette.setColor(QPalette.ToolTipText, Qt.black) + palette.setColor(QPalette.Text, Qt.black) + palette.setColor(QPalette.Button, QColor(240, 240, 240)) + palette.setColor(QPalette.ButtonText, Qt.black) + palette.setColor(QPalette.BrightText, Qt.red) + palette.setColor(QPalette.Link, QColor(0, 0, 255)) + palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) + palette.setColor(QPalette.HighlightedText, Qt.white) + + # Palette auf die Anwendung anwenden + self.app.setPalette(palette) + + # Stylesheet anwenden + self.app.setStyleSheet(self.theme_stylesheets.get(self.LIGHT_THEME, "")) + + # Aktuelles Theme speichern + self.current_theme = self.LIGHT_THEME + self.settings.setValue("theme", self.LIGHT_THEME) + + logger.info(f"Theme '{self.LIGHT_THEME}' erfolgreich angewendet") + return True + + except Exception as e: + logger.error(f"Fehler beim Anwenden des Themes '{self.LIGHT_THEME}': {e}") + return False + + def get_current_theme(self) -> str: + """Gibt den Namen des aktuellen Themes zurück.""" + return self.LIGHT_THEME + + def get_icon_path(self, icon_name: str) -> str: + """ + Gibt den Pfad zum Icon zurück. + + Args: + icon_name: Name des Icons (ohne Dateierweiterung) + + Returns: + str: Pfad zum Icon + """ + # Social Media Icons bleiben unverändert (immer farbig) + if icon_name in ["instagram", "facebook", "twitter", "tiktok", "vk"]: + return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg") + + # Für andere Icons, die möglicherweise Theme-spezifisch sind + return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg") \ No newline at end of file diff --git a/utils/update_checker.py b/utils/update_checker.py new file mode 100644 index 0000000..8ace01a --- /dev/null +++ b/utils/update_checker.py @@ -0,0 +1,733 @@ + +Alle Projekte +Chimaira +Privat +Projektziel Das Hauptziel des Projekts ist die Entwicklung einer benutzerfreundlichen Software zur automatisierten Erstellung von Social-Media-Accounts. Die Anwendung ermöglicht es Benutzern, Konten für verschiedene Plattformen (Instagram, Facebook, Twitter, TikTok) mit minimaler manueller Intervention zu erstellen, zu verwalten und zu exportieren. Kernfunktionalitäten Automatisierte Account-Erstellung: Erstellen von Benutzerkonten für verschiedene Social-Media-Plattformen Proxy-Unterstützung: Verwendung von Proxies für anonyme Verbindungen und zur Umgehung von Einschränkungen E-Mail-Integration: Automatische Verarbeitung von Bestätigungscodes Datenbankintegration: Speichern und Verwalten erstellter Konten Benutzerfreundliche GUI: Intuitive Benutzeroberfläche mit Dark Mode Robuste Automatisierung: OCR-Fallback-Mechanismen für UI-Änderungen Code-Playwright/ # Social-Media-Account-Generator/ ├── config/ # Konfigurationsverzeichnis │ ├── browser_config.json │ ├── email_config.json │ ├── facebook_config.json │ ├── instagram_config.json │ ├── license_config.json │ ├── proxy_config.json │ ├── stealth_config.json │ ├── tiktok_config.json │ ├── twitter_config.json │ ├── update_config.json │ └── user_agents.json ├── controllers/ # Controller-Logik │ ├── account_controller.py │ ├── main_controller.py │ ├── settings_controller.py │ └── platform_controllers/ │ ├── base_controller.py │ ├── instagram_controller.py │ ├── facebook_controller.py (nicht implementiert) │ ├── twitter_controller.py (nicht implementiert) │ └── tiktok_controller.py (nicht implementiert) ├── database/ # Datenbankfunktionalität │ └── db_manager.py ├── licensing/ # Lizenzverwaltung │ ├── license_manager.py │ └── license_validator.py ├── logs/ # Log-Verzeichnis │ └── screenshots/ # Screenshots für OCR-Fallbacks ├── ocr/ # OCR-Funktionalität │ ├── fallback_actions.py │ ├── screenshot.py │ └── text_detector.py ├── resources/ # Ressourcen │ ├── icons/ # Icons für die UI │ │ ├── instagram.svg │ │ ├── facebook.svg │ │ ├── twitter.svg │ │ ├── tiktok.svg │ │ └── [andere Icons] │ └── themes/ # Theme-Ressourcen │ ├── dark.qss │ └── light.qss ├── social_networks/ # Social-Media-Automatisierung │ ├── base_automation.py │ ├── instagram/ │ │ ├── instagram_automation.py │ │ ├── instagram_selectors.py │ │ └── instagram_workflow.py │ ├── facebook/ # Noch nicht implementiert │ ├── twitter/ # Noch nicht implementiert │ └── tiktok/ # Noch nicht implementiert ├── updates/ # Update-Funktionalität │ └── update_checker.py ├── utils/ # Hilfsfunktionen │ ├── birthday_generator.py │ ├── email_handler.py │ ├── human_behavior.py │ ├── logger.py │ ├── password_generator.py │ ├── proxy_rotator.py │ ├── theme_manager.py │ └── username_generator.py ├── views/ # UI-Komponenten │ ├── main_window.py │ ├── platform_selector.py │ └── tabs/ │ ├── about_tab.py │ ├── accounts_tab.py │ ├── generator_tab.py │ └── settings_tab.py ├── browser/ # Browser-Automatisierung │ ├── playwright_manager.py │ └── stealth_config.py └── main.py # Haupteinstiegspunkt Kernstruktur und MVC-Framework: Grundlegendes MVC-Muster mit klarer Trennung von Daten, Ansicht und Logik Signale und Slots für die Kommunikation zwischen Komponenten Zentrale Logging-Funktionalität Benutzeroberfläche: Hauptfenster mit Plattformauswahl Plattformspezifische Tabs (Generator, Konten, Einstellungen, Über) Dark Mode für alle UI-Komponenten Utility-Klassen: Logger mit GUI-Integration Proxy-Rotator mit Testoption E-Mail-Handler für Verifizierungscodes Passwort-, Benutzernamen- und Geburtsdatumsgeneratoren Human-Behavior-Simulation für natürliche Verzögerungen Lizenzmanager Update-Checker Datenbankintegration: SQLite-Datenbankmanager für Account-Speicherung Import- und Exportfunktionen Suchfunktionen Instagram-Integration: Basis-Automation und Instagram-spezifische Logik Account-Generator-Workflow Stealth-Funktionalität zur Umgehung von Bot-Erkennung Noch ausstehende Aufgaben Plattform-Integration: Implementierung der Facebook-Automatisierung Implementierung der Twitter-Automatisierung Implementierung der TikTok-Automatisierung Model-Klassen: Entwicklung der Datenmodelle für Accounts und Plattformen Integration in die Controller-Logik OCR-Integration: Anpassung der OCR-Komponenten an die neue MVC-Struktur Verbesserung der Fallback-Mechanismen für UI-Änderungen Plattformspezifische Controller: Implementierung der Facebook-, Twitter- und TikTok-Controller Anpassung an spezifische Anforderungen jeder Plattform Erweiterte Funktionen: CAPTCHA-Behandlung mit externen Diensten oder manueller Eingabe SMS-Verifizierung mit SMS-Empfangsdiensten Verbesserte Fehlerbehandlung und Wiederherstellung Tests: Entwicklung von Unit-Tests für die Kernkomponenten End-to-End-Tests für den gesamten Workflow Performance-Optimierungen: Multi-Threading für parallele Account-Erstellung Optimierte Ressourcennutzung bei längeren Automatisierungen Zusammenfassung der Refaktorierung Das ursprüngliche, monolithische Design wurde zu einer modularen MVC-Architektur umgestaltet, die folgende Vorteile bietet: Verbesserte Wartbarkeit: Kleinere, spezialisierte Dateien statt einer großen main.py Einfachere Erweiterbarkeit: Neue Plattformen können durch Ableitung von Basisklassen hinzugefügt werden Bessere Testbarkeit: Komponenten können isoliert getestet werden Wiederverwendbarkeit: Gemeinsame Funktionalität in Basisklassen extrahiert Klare Verantwortlichkeiten: Jede Komponente hat eine spezifische Aufgabe Die größte Verbesserung ist die klare Trennung von Benutzeroberfläche, Geschäftslogik und Datenmanagement, was die Wartung und Erweiterung erheblich erleichtert und einen strukturierten Rahmen für die Implementierung weiterer Plattformen schafft. Nächste Schritte Die nächsten unmittelbaren Schritte sind: Implementierung der Datenmodelle zur Vervollständigung der MVC-Struktur Entwicklung der weiteren plattformspezifischen Controller Anpassung der bestehenden Automatisierungslogik an die neue Struktur Erstellung von Grundtests für die Kernfunktionalität. Nimm für die Pfade NIEMALS absolute Pfade, sondern IMMER relative Pfade + + + + +Unbenannt +Letzte Nachricht vor 21 Sekunden +Deprecation of generate_birthday() function +Letzte Nachricht vor 10 Minuten +Playwright Cookie Banner and Date Parsing Issues +Letzte Nachricht vor 30 Minuten +Troubleshooting Python script error with HumanBehavior class +Letzte Nachricht vor 2 Stunden +Troubleshooting Python code error with Instagram automation +Letzte Nachricht vor 2 Stunden +Troubleshooting Instagram Automation Error +Letzte Nachricht vor 3 Stunden +Modular Localization System for Multilingual App +Letzte Nachricht vor 4 Stunden +Instagram Account Creation Error +Letzte Nachricht vor 1 Tag +Code and Icon Structure Review for Social Media Account Generator +Letzte Nachricht vor 1 Tag +Projektwissen +57 % der Kapazität der Wissensdatenbank genutzt + +instagram_automation.py +1.080 Zeilen + +py + + + +instagram_automation.py +1.075 Zeilen + +py + + + +human_behavior.py +488 Zeilen + +py + + + +main_window.py +168 Zeilen + +py + + + +platform_selector.py +96 Zeilen + +py + + + +platform_button.py +77 Zeilen + +py + + + +theme_manager.py +133 Zeilen + +py + + + +main.py +49 Zeilen + +py + + + +main_controller.py +226 Zeilen + +py + + + +light.qss +255 Zeilen + +text + + + +dark.qss +190 Zeilen + +text + + + +theme.json +47 Zeilen + +json + + + +birthday_generator.py +299 Zeilen + +py + + + +username_generator.py +426 Zeilen + +py + + + +stealth_config.py +216 Zeilen + +py + + + +playwright_manager.py +517 Zeilen + +py + + + +user_agents.json +31 Zeilen + +json + + + +update_config.json +9 Zeilen + +json + + + +stealth_config.json +14 Zeilen + +json + + + +proxy_config.json +15 Zeilen + +json + + + +license_config.json +9 Zeilen + +json + + + +email_config.json +6 Zeilen + +json + + + +instagram_controller.py +186 Zeilen + +py + + + +base_controller.py +130 Zeilen + +py + + + +settings_controller.py +295 Zeilen + +py + + + +account_controller.py +150 Zeilen + +py + + + +license_validator.py +304 Zeilen + +py + + + +license_manager.py +450 Zeilen + +py + + + +instagram_workflow.py +315 Zeilen + +py + + + +instagram_selectors.py +113 Zeilen + +py + + + +base_automation.py +329 Zeilen + +py + + + +version.py +193 Zeilen + +py + + + +update_checker.py +411 Zeilen + +py + + + +proxy_rotator.py +347 Zeilen + +py + + + +password_generator.py +338 Zeilen + +py + + + +logger.py +70 Zeilen + +py + + + +email_handler.py +410 Zeilen + +py + + + +about_tab.py +64 Zeilen + +py + + + +settings_tab.py +302 Zeilen + +py + + + +generator_tab.py +278 Zeilen + +py + + + +accounts_tab.py +138 Zeilen + +py + + +update_checker.py + +14.92 KB •411 Zeilen +• +Die Formatierung kann von der Quelle abweichen + +""" +Update-Checking-Funktionalität für den Social Media Account Generator. +""" + +import os +import json +import logging +import requests +import shutil +from datetime import datetime +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("update_checker") + +class UpdateChecker: + """Klasse zum Überprüfen und Herunterladen von Updates.""" + + CONFIG_FILE = os.path.join("config", "app_version.json") + UPDATE_SERVER_URL = "https://api.example.com/updates" # Platzhalter - in der Produktion anpassen + + def __init__(self): + """Initialisiert den UpdateChecker und lädt die Konfiguration.""" + self.version_info = self.load_version_info() + + # Stelle sicher, dass das Konfigurationsverzeichnis existiert + os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True) + + # Updates-Verzeichnis für Downloads + os.makedirs("updates", exist_ok=True) + + def load_version_info(self) -> Dict[str, Any]: + """Lädt die Versionsinformationen aus der Konfigurationsdatei.""" + if not os.path.exists(self.CONFIG_FILE): + default_info = { + "current_version": "1.0.0", + "last_check": "", + "channel": "stable", + "auto_check": True, + "auto_download": False + } + + # Standardwerte speichern + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(default_info, f, indent=2) + except Exception as e: + logger.error(f"Fehler beim Speichern der Standardversionsinformationen: {e}") + + return default_info + + try: + with open(self.CONFIG_FILE, "r", encoding="utf-8") as f: + version_info = json.load(f) + + logger.info(f"Versionsinformationen geladen: {version_info.get('current_version', 'unbekannt')}") + + return version_info + except Exception as e: + logger.error(f"Fehler beim Laden der Versionsinformationen: {e}") + return { + "current_version": "1.0.0", + "last_check": "", + "channel": "stable", + "auto_check": True, + "auto_download": False + } + + def save_version_info(self) -> bool: + """Speichert die Versionsinformationen in die Konfigurationsdatei.""" + try: + with open(self.CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(self.version_info, f, indent=2) + + logger.info("Versionsinformationen gespeichert") + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der Versionsinformationen: {e}") + return False + + def compare_versions(self, version1: str, version2: str) -> int: + """ + Vergleicht zwei Versionsstrings (semver). + + Args: + version1: Erste Version + version2: Zweite Version + + Returns: + -1, wenn version1 < version2 + 0, wenn version1 == version2 + 1, wenn version1 > version2 + """ + v1_parts = [int(part) for part in version1.split(".")] + v2_parts = [int(part) for part in version2.split(".")] + + # Fülle fehlende Teile mit Nullen auf + while len(v1_parts) < 3: + v1_parts.append(0) + while len(v2_parts) < 3: + v2_parts.append(0) + + # Vergleiche die Teile + for i in range(3): + if v1_parts[i] < v2_parts[i]: + return -1 + elif v1_parts[i] > v2_parts[i]: + return 1 + + return 0 + + def check_for_updates(self, force: bool = False) -> Dict[str, Any]: + """ + Überprüft, ob Updates verfügbar sind. + + Args: + force: Erzwingt eine Überprüfung, auch wenn erst kürzlich geprüft wurde + + Returns: + Dictionary mit Update-Informationen + """ + result = { + "has_update": False, + "current_version": self.version_info["current_version"], + "latest_version": self.version_info["current_version"], + "release_date": "", + "release_notes": "", + "download_url": "", + "error": "" + } + + # Prüfe, ob seit der letzten Überprüfung genügend Zeit vergangen ist (24 Stunden) + if not force and self.version_info.get("last_check"): + try: + last_check = datetime.fromisoformat(self.version_info["last_check"]) + now = datetime.now() + + # Wenn weniger als 24 Stunden seit der letzten Überprüfung vergangen sind + if (now - last_check).total_seconds() < 86400: + logger.info("Update-Überprüfung übersprungen (letzte Überprüfung vor weniger als 24 Stunden)") + return result + except Exception as e: + logger.warning(f"Fehler beim Parsen des letzten Überprüfungsdatums: {e}") + + try: + # Simuliere eine Online-Überprüfung für Entwicklungszwecke + # In der Produktion sollte eine echte API-Anfrage implementiert werden + # response = requests.get( + # f"{self.UPDATE_SERVER_URL}/check", + # params={ + # "version": self.version_info["current_version"], + # "channel": self.version_info["channel"] + # }, + # timeout=10 + # ) + + # For demonstration purposes only + latest_version = "1.1.0" + has_update = self.compare_versions(self.version_info["current_version"], latest_version) < 0 + + if has_update: + result["has_update"] = True + result["latest_version"] = latest_version + result["release_date"] = "2025-05-01" + result["release_notes"] = ( + "Version 1.1.0:\n" + "- Unterstützung für Facebook-Accounts hinzugefügt\n" + "- Verbesserte Proxy-Rotation\n" + "- Bessere Fehlerbehandlung bei der Account-Erstellung\n" + "- Verschiedene Bugfixes und Leistungsverbesserungen" + ) + result["download_url"] = f"{self.UPDATE_SERVER_URL}/download/v1.1.0" + + # Update der letzten Überprüfung + self.version_info["last_check"] = datetime.now().isoformat() + self.save_version_info() + + logger.info(f"Update-Überprüfung abgeschlossen: {result['latest_version']} verfügbar") + + return result + + except requests.RequestException as e: + error_msg = f"Netzwerkfehler bei der Update-Überprüfung: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + except Exception as e: + error_msg = f"Unerwarteter Fehler bei der Update-Überprüfung: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + + def download_update(self, download_url: str, version: str) -> Dict[str, Any]: + """ + Lädt ein Update herunter. + + Args: + download_url: URL zum Herunterladen des Updates + version: Version des Updates + + Returns: + Dictionary mit Download-Informationen + """ + result = { + "success": False, + "file_path": "", + "version": version, + "error": "" + } + + try: + # Zieldateiname erstellen + file_name = f"update_v{version}.zip" + file_path = os.path.join("updates", file_name) + + # Simuliere einen Download für Entwicklungszwecke + # In der Produktion sollte ein echter Download implementiert werden + + # response = requests.get(download_url, stream=True, timeout=60) + # if response.status_code == 200: + # with open(file_path, "wb") as f: + # shutil.copyfileobj(response.raw, f) + + # Simulierter Download (erstelle eine leere Datei) + with open(file_path, "w") as f: + f.write(f"Placeholder for version {version} update") + + result["success"] = True + result["file_path"] = file_path + + logger.info(f"Update v{version} heruntergeladen: {file_path}") + + return result + + except requests.RequestException as e: + error_msg = f"Netzwerkfehler beim Herunterladen des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + except Exception as e: + error_msg = f"Unerwarteter Fehler beim Herunterladen des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result + + def is_update_available(self) -> bool: + """ + Überprüft, ob ein Update verfügbar ist. + + Returns: + True, wenn ein Update verfügbar ist, sonst False + """ + update_info = self.check_for_updates() + return update_info["has_update"] + + def get_current_version(self) -> str: + """ + Gibt die aktuelle Version zurück. + + Returns: + Aktuelle Version + """ + return self.version_info["current_version"] + + def set_current_version(self, version: str) -> bool: + """ + Setzt die aktuelle Version. + + Args: + version: Neue Version + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["current_version"] = version + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen der aktuellen Version: {e}") + return False + + def set_update_channel(self, channel: str) -> bool: + """ + Setzt den Update-Kanal (stable, beta, dev). + + Args: + channel: Update-Kanal + + Returns: + True bei Erfolg, False im Fehlerfall + """ + if channel not in ["stable", "beta", "dev"]: + logger.warning(f"Ungültiger Update-Kanal: {channel}") + return False + + try: + self.version_info["channel"] = channel + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen des Update-Kanals: {e}") + return False + + def get_update_channel(self) -> str: + """ + Gibt den aktuellen Update-Kanal zurück. + + Returns: + Update-Kanal (stable, beta, dev) + """ + return self.version_info.get("channel", "stable") + + def set_auto_check(self, auto_check: bool) -> bool: + """ + Aktiviert oder deaktiviert die automatische Update-Überprüfung. + + Args: + auto_check: True, um automatische Updates zu aktivieren, False zum Deaktivieren + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["auto_check"] = bool(auto_check) + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen der automatischen Update-Überprüfung: {e}") + return False + + def is_auto_check_enabled(self) -> bool: + """ + Überprüft, ob die automatische Update-Überprüfung aktiviert ist. + + Returns: + True, wenn die automatische Update-Überprüfung aktiviert ist, sonst False + """ + return self.version_info.get("auto_check", True) + + def set_auto_download(self, auto_download: bool) -> bool: + """ + Aktiviert oder deaktiviert den automatischen Download von Updates. + + Args: + auto_download: True, um automatische Downloads zu aktivieren, False zum Deaktivieren + + Returns: + True bei Erfolg, False im Fehlerfall + """ + try: + self.version_info["auto_download"] = bool(auto_download) + return self.save_version_info() + except Exception as e: + logger.error(f"Fehler beim Setzen des automatischen Downloads: {e}") + return False + + def is_auto_download_enabled(self) -> bool: + """ + Überprüft, ob der automatische Download von Updates aktiviert ist. + + Returns: + True, wenn der automatische Download aktiviert ist, sonst False + """ + return self.version_info.get("auto_download", False) + + def apply_update(self, update_file: str) -> Dict[str, Any]: + """ + Wendet ein heruntergeladenes Update an. + + Args: + update_file: Pfad zur Update-Datei + + Returns: + Dictionary mit Informationen über die Anwendung des Updates + """ + result = { + "success": False, + "version": "", + "error": "" + } + + if not os.path.exists(update_file): + result["error"] = f"Update-Datei nicht gefunden: {update_file}" + logger.error(result["error"]) + return result + + try: + # In der Produktion sollte hier die tatsächliche Update-Logik implementiert werden + # 1. Extrahieren des Updates + # 2. Sichern der aktuellen Version + # 3. Anwenden der Änderungen + # 4. Aktualisieren der Versionsinformationen + + # Simuliere ein erfolgreiches Update + logger.info(f"Update aus {update_file} erfolgreich angewendet (simuliert)") + + # Extrahiere Version aus dem Dateinamen + file_name = os.path.basename(update_file) + version_match = re.search(r"v([0-9.]+)", file_name) + + if version_match: + new_version = version_match.group(1) + self.set_current_version(new_version) + result["version"] = new_version + + result["success"] = True + + return result + + except Exception as e: + error_msg = f"Fehler beim Anwenden des Updates: {e}" + logger.error(error_msg) + result["error"] = error_msg + return result diff --git a/utils/username_generator.py b/utils/username_generator.py new file mode 100644 index 0000000..bba87ac --- /dev/null +++ b/utils/username_generator.py @@ -0,0 +1,465 @@ +# utils/username_generator.py + +""" +Benutzernamen-Generator für den Social Media Account Generator. +""" + +import random +import string +import re +import logging +from typing import Dict, List, Any, Optional, Tuple, Union + +logger = logging.getLogger("username_generator") + +class UsernameGenerator: + """Klasse zur Generierung von Benutzernamen für verschiedene Plattformen.""" + + def __init__(self): + """Initialisiert den UsernameGenerator.""" + # Plattformspezifische Richtlinien + self.platform_policies = { + "instagram": { + "min_length": 1, + "max_length": 30, + "allowed_chars": string.ascii_letters + string.digits + "._", + "allowed_start_chars": string.ascii_letters + string.digits, + "allowed_end_chars": string.ascii_letters + string.digits + ".", + "allowed_consecutive_special": False, + "disallowed_words": ["instagram", "admin", "official"], + "auto_suggestions": True + }, + "facebook": { + "min_length": 5, + "max_length": 50, + "allowed_chars": string.ascii_letters + string.digits + ".-", + "allowed_start_chars": string.ascii_letters, + "allowed_end_chars": string.ascii_letters + string.digits, + "allowed_consecutive_special": True, + "disallowed_words": ["facebook", "meta", "admin"], + "auto_suggestions": False + }, + "twitter": { + "min_length": 4, + "max_length": 15, + "allowed_chars": string.ascii_letters + string.digits + "_", + "allowed_start_chars": string.ascii_letters + string.digits, + "allowed_end_chars": string.ascii_letters + string.digits + "_", + "allowed_consecutive_special": True, + "disallowed_words": ["twitter", "admin", "official"], + "auto_suggestions": True + }, + "tiktok": { + "min_length": 2, + "max_length": 24, + "allowed_chars": string.ascii_letters + string.digits + "._", + "allowed_start_chars": string.ascii_letters + string.digits, + "allowed_end_chars": string.ascii_letters + string.digits, + "allowed_consecutive_special": False, + "disallowed_words": ["tiktok", "admin", "official"], + "auto_suggestions": True + }, + "default": { + "min_length": 3, + "max_length": 20, + "allowed_chars": string.ascii_letters + string.digits + "._-", + "allowed_start_chars": string.ascii_letters, + "allowed_end_chars": string.ascii_letters + string.digits, + "allowed_consecutive_special": False, + "disallowed_words": ["admin", "root", "system"], + "auto_suggestions": True + } + } + + # Liste von Adjektiven und Substantiven für zufällige Benutzernamen + self.adjectives = [ + "happy", "sunny", "clever", "brave", "mighty", "gentle", "wild", "calm", "bright", + "quiet", "swift", "bold", "wise", "fancy", "little", "big", "smart", "cool", "hot", + "super", "mega", "epic", "magic", "golden", "silver", "bronze", "shiny", "dark", + "light", "fast", "slow", "strong", "soft", "hard", "sweet", "sour", "tasty", "fresh", + "green", "blue", "red", "purple", "yellow", "orange", "pink", "white", "black" + ] + + self.nouns = [ + "tiger", "eagle", "lion", "wolf", "bear", "fox", "owl", "hawk", "falcon", "dolphin", + "shark", "whale", "turtle", "panda", "koala", "monkey", "cat", "dog", "horse", "pony", + "unicorn", "dragon", "phoenix", "wizard", "knight", "warrior", "ninja", "samurai", + "queen", "king", "prince", "princess", "hero", "legend", "star", "moon", "sun", "sky", + "ocean", "river", "mountain", "forest", "tree", "flower", "rose", "tulip", "daisy" + ] + + # Internationaler Wortschatz (nur mit ASCII-Zeichen) für verschiedene Sprachen + # Jeweils 100 kurze, neutrale Begriffe pro Sprache + self.international_words = { + # Deutsch - 100 kurze, neutrale Begriffe ohne Umlaute oder Sonderzeichen + "de": [ + "wald", "berg", "fluss", "tal", "see", "meer", "boot", "schiff", "haus", "dach", + "tuer", "fenster", "glas", "holz", "stein", "sand", "erde", "weg", "pfad", "strasse", + "auto", "rad", "ball", "spiel", "tisch", "stuhl", "bett", "kissen", "lampe", "licht", + "tag", "nacht", "sonne", "mond", "stern", "himmel", "wolke", "regen", "schnee", "wind", + "baum", "blume", "gras", "blatt", "frucht", "apfel", "brot", "wasser", "milch", "kaffee", + "buch", "brief", "stift", "musik", "lied", "tanz", "film", "bild", "farbe", "kunst", + "hand", "fuss", "kopf", "auge", "ohr", "nase", "mund", "zahn", "haar", "herz", + "zeit", "jahr", "monat", "woche", "tag", "stunde", "minute", "uhr", "zahl", "wort", + "name", "freund", "kind", "tier", "vogel", "fisch", "stadt", "land", "dorf", "garten", + "feld", "werk", "kraft", "geld", "gold", "bank", "markt", "preis", "karte", "punkt" + ], + + # Englisch - 100 kurze, neutrale Begriffe + "en": [ + "wood", "hill", "river", "valley", "lake", "sea", "boat", "ship", "house", "roof", + "door", "window", "glass", "wood", "stone", "sand", "earth", "way", "path", "road", + "car", "wheel", "ball", "game", "table", "chair", "bed", "pillow", "lamp", "light", + "day", "night", "sun", "moon", "star", "sky", "cloud", "rain", "snow", "wind", + "tree", "flower", "grass", "leaf", "fruit", "apple", "bread", "water", "milk", "coffee", + "book", "letter", "pen", "music", "song", "dance", "film", "image", "color", "art", + "hand", "foot", "head", "eye", "ear", "nose", "mouth", "tooth", "hair", "heart", + "time", "year", "month", "week", "day", "hour", "minute", "clock", "number", "word", + "name", "friend", "child", "animal", "bird", "fish", "city", "country", "village", "garden", + "field", "work", "power", "money", "gold", "bank", "market", "price", "card", "point" + ], + + # Französisch - 100 kurze, neutrale Begriffe (ohne Akzente oder Sonderzeichen) + "fr": [ + "bois", "mont", "fleuve", "vallee", "lac", "mer", "bateau", "navire", "maison", "toit", + "porte", "fenetre", "verre", "bois", "pierre", "sable", "terre", "voie", "sentier", "route", + "auto", "roue", "balle", "jeu", "table", "chaise", "lit", "coussin", "lampe", "lumiere", + "jour", "nuit", "soleil", "lune", "etoile", "ciel", "nuage", "pluie", "neige", "vent", + "arbre", "fleur", "herbe", "feuille", "fruit", "pomme", "pain", "eau", "lait", "cafe", + "livre", "lettre", "stylo", "musique", "chanson", "danse", "film", "image", "couleur", "art", + "main", "pied", "tete", "oeil", "oreille", "nez", "bouche", "dent", "cheveu", "coeur", + "temps", "annee", "mois", "semaine", "jour", "heure", "minute", "horloge", "nombre", "mot", + "nom", "ami", "enfant", "animal", "oiseau", "poisson", "ville", "pays", "village", "jardin", + "champ", "travail", "force", "argent", "or", "banque", "marche", "prix", "carte", "point" + ], + + # Spanisch - 100 kurze, neutrale Begriffe (ohne Akzente oder Sonderzeichen) + "es": [ + "bosque", "monte", "rio", "valle", "lago", "mar", "barco", "nave", "casa", "techo", + "puerta", "ventana", "vidrio", "madera", "piedra", "arena", "tierra", "via", "ruta", "calle", + "coche", "rueda", "bola", "juego", "mesa", "silla", "cama", "cojin", "lampara", "luz", + "dia", "noche", "sol", "luna", "estrella", "cielo", "nube", "lluvia", "nieve", "viento", + "arbol", "flor", "hierba", "hoja", "fruta", "manzana", "pan", "agua", "leche", "cafe", + "libro", "carta", "pluma", "musica", "cancion", "baile", "pelicula", "imagen", "color", "arte", + "mano", "pie", "cabeza", "ojo", "oreja", "nariz", "boca", "diente", "pelo", "corazon", + "tiempo", "ano", "mes", "semana", "dia", "hora", "minuto", "reloj", "numero", "palabra", + "nombre", "amigo", "nino", "animal", "ave", "pez", "ciudad", "pais", "pueblo", "jardin", + "campo", "trabajo", "fuerza", "dinero", "oro", "banco", "mercado", "precio", "carta", "punto" + ], + + # Japanisch - 100 kurze, neutrale Begriffe (in romanisierter Form) + "ja": [ + "ki", "yama", "kawa", "tani", "mizu", "umi", "fune", "ie", "yane", "kado", + "mado", "garasu", "ki", "ishi", "suna", "tsuchi", "michi", "kuruma", "wa", "tama", + "asobi", "tsukue", "isu", "neru", "makura", "akari", "hikari", "hi", "yoru", "taiyou", + "tsuki", "hoshi", "sora", "kumo", "ame", "yuki", "kaze", "ki", "hana", "kusa", + "ha", "kudamono", "ringo", "pan", "mizu", "gyunyu", "kohi", "hon", "tegami", "pen", + "ongaku", "uta", "odori", "eiga", "e", "iro", "geijutsu", "te", "ashi", "atama", + "me", "mimi", "hana", "kuchi", "ha", "kami", "kokoro", "jikan", "toshi", "tsuki", + "shukan", "hi", "jikan", "fun", "tokei", "kazu", "kotoba", "namae", "tomodachi", "kodomo", + "doubutsu", "tori", "sakana", "machi", "kuni", "mura", "niwa", "hatake", "shigoto", "chikara", + "okane", "kin", "ginko", "ichiba", "nedan", "kado", "ten", "ai", "heiwa", "yume" + ] + } + + def get_platform_policy(self, platform: str) -> Dict[str, Any]: + """ + Gibt die Benutzernamen-Richtlinie für eine bestimmte Plattform zurück. + + Args: + platform: Name der Plattform + + Returns: + Dictionary mit der Benutzernamen-Richtlinie + """ + platform = platform.lower() + return self.platform_policies.get(platform, self.platform_policies["default"]) + + def set_platform_policy(self, platform: str, policy: Dict[str, Any]) -> None: + """ + Setzt oder aktualisiert die Benutzernamen-Richtlinie für eine Plattform. + + Args: + platform: Name der Plattform + policy: Dictionary mit der Benutzernamen-Richtlinie + """ + platform = platform.lower() + self.platform_policies[platform] = policy + logger.info(f"Benutzernamen-Richtlinie für '{platform}' aktualisiert") + + def generate_username(self, platform: str = "default", name: Optional[str] = None, + custom_policy: Optional[Dict[str, Any]] = None) -> str: + """ + Generiert einen Benutzernamen gemäß den Richtlinien. + + Args: + platform: Name der Plattform + name: Optionaler vollständiger Name für die Generierung + custom_policy: Optionale benutzerdefinierte Richtlinie + + Returns: + Generierter Benutzername + """ + # Richtlinie bestimmen + if custom_policy: + policy = custom_policy + else: + policy = self.get_platform_policy(platform) + + # Wenn ein Name angegeben ist, versuche einen darauf basierenden Benutzernamen zu erstellen + if name: + return self.generate_from_name(name, policy) + else: + # Zufälligen Benutzernamen erstellen + return self.generate_random_username(policy) + + def generate_from_name(self, name: str, policy: Dict[str, Any]) -> str: + """ + Generiert einen Benutzernamen aus einem vollständigen Namen im Format + Vorname_RandomBegriffAusDerAusgewähltenSprache_GeburtsjahrXX. + + Args: + name: Vollständiger Name + policy: Benutzernamen-Richtlinie + + Returns: + Generierter Benutzername + """ + # Name in Teile zerlegen + parts = name.lower().split() + + # Sonderzeichen und Leerzeichen entfernen + parts = [re.sub(r'[^a-z0-9]', '', part) for part in parts] + parts = [part for part in parts if part] + + if not parts: + # Falls keine gültigen Teile, zufälligen Benutzernamen generieren + return self.generate_random_username(policy) + + # Vorname nehmen + firstname = parts[0] + + # Zufällige Sprache auswählen + available_languages = list(self.international_words.keys()) + chosen_language = random.choice(available_languages) + + # Zufälliges Wort aus der gewählten Sprache wählen + random_word = random.choice(self.international_words[chosen_language]) + + # Geburtsjahr simulieren (zwischen 18 und 40 Jahre alt) + current_year = 2025 # Aktuelle Jahresangabe im Code + birth_year = current_year - random.randint(18, 40) + + # Letzte zwei Ziffern vom Geburtsjahr plus eine Zufallszahl + year_suffix = str(birth_year)[-2:] + str(random.randint(0, 9)) + + # Benutzernamen im neuen Format zusammensetzen + username = f"{firstname}_{random_word}_{year_suffix}" + + # Länge prüfen und anpassen + if len(username) > policy["max_length"]: + # Bei Überlänge, kürze den Mittelteil + max_word_length = policy["max_length"] - len(firstname) - len(year_suffix) - 2 # 2 für die Unterstriche + if max_word_length < 3: # Zu kurz für ein sinnvolles Wort + # Fallback: Nur Vorname + Jahreszahl + username = f"{firstname}_{year_suffix}" + else: + random_word = random_word[:max_word_length] + username = f"{firstname}_{random_word}_{year_suffix}" + + # Überprüfen, ob die Richtlinien erfüllt sind + valid, error_msg = self.validate_username(username, policy=policy) + if not valid: + # Wenn nicht gültig, generiere einen alternativen Namen + logger.debug(f"Generierter Name '{username}' nicht gültig: {error_msg}") + + # Einfachere Variante versuchen + username = f"{firstname}{year_suffix}" + + valid, _ = self.validate_username(username, policy=policy) + if not valid: + # Wenn immer noch nicht gültig, Fallback auf Standard-Generator + return self.generate_random_username(policy) + + logger.info(f"Aus Name generierter Benutzername: {username}") + return username + + def generate_random_username(self, policy: Dict[str, Any]) -> str: + """ + Generiert einen zufälligen Benutzernamen. + + Args: + policy: Benutzernamen-Richtlinie + + Returns: + Generierter Benutzername + """ + # Verschiedene Muster für zufällige Benutzernamen + patterns = [ + # Adjektiv + Substantiv + lambda: random.choice(self.adjectives) + random.choice(self.nouns), + + # Substantiv + Zahlen + lambda: random.choice(self.nouns) + "".join(random.choices(string.digits, k=random.randint(1, 4))), + + # Adjektiv + Substantiv + Zahlen + lambda: random.choice(self.adjectives) + random.choice(self.nouns) + "".join(random.choices(string.digits, k=random.randint(1, 3))), + + # Substantiv + Unterstrich + Substantiv + lambda: random.choice(self.nouns) + ("_" if "_" in policy["allowed_chars"] else "") + random.choice(self.nouns), + + # Benutzer + Zahlen + lambda: "user" + "".join(random.choices(string.digits, k=random.randint(3, 6))) + ] + + # Zufälliges Muster auswählen und Benutzernamen generieren + max_attempts = 10 + for _ in range(max_attempts): + pattern_func = random.choice(patterns) + username = pattern_func() + + # Zu lange Benutzernamen kürzen + if len(username) > policy["max_length"]: + username = username[:policy["max_length"]] + + # Zu kurze Benutzernamen verlängern + if len(username) < policy["min_length"]: + username += "".join(random.choices(string.digits, k=policy["min_length"] - len(username))) + + # Überprüfen, ob der Benutzername den Richtlinien entspricht + valid, _ = self.validate_username(username, policy=policy) + if valid: + logger.info(f"Zufälliger Benutzername generiert: {username}") + return username + + # Fallback: Einfachen Benutzernamen mit Zufallsbuchstaben und Zahlen generieren + length = random.randint(policy["min_length"], min(policy["max_length"], policy["min_length"] + 5)) + username = random.choice(string.ascii_lowercase) # Erster Buchstabe + + allowed_chars = [c for c in policy["allowed_chars"] if c in (string.ascii_lowercase + string.digits)] + username += "".join(random.choice(allowed_chars) for _ in range(length - 1)) + + logger.info(f"Fallback-Benutzername generiert: {username}") + return username + + def suggest_alternatives(self, username: str, platform: str = "default") -> List[str]: + """ + Schlägt alternative Benutzernamen vor, wenn der gewünschte bereits vergeben ist. + + Args: + username: Gewünschter Benutzername + platform: Name der Plattform + + Returns: + Liste mit alternativen Benutzernamen + """ + policy = self.get_platform_policy(platform) + + # Wenn Auto-Suggestions deaktiviert sind, leere Liste zurückgeben + if not policy.get("auto_suggestions", True): + return [] + + alternatives = [] + base_username = username + + # Verschiedene Modifikationen ausprobieren + + # Anhängen von Zahlen + for i in range(5): + suffix = str(random.randint(1, 999)) + alt = base_username + suffix + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Sonderzeichen einfügen + for special in ["_", ".", "-"]: + if special in policy["allowed_chars"]: + alt = base_username + special + str(random.randint(1, 99)) + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Adjektiv voranstellen + for _ in range(2): + prefix = random.choice(self.adjectives) + alt = prefix + base_username + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Buchstaben ersetzen (z.B. 'o' durch '0') + if "0" in policy["allowed_chars"] and "o" in base_username.lower(): + alt = base_username.lower().replace("o", "0") + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Zufällige Buchstaben voranstellen + for _ in range(2): + prefix = "".join(random.choices(string.ascii_lowercase, k=random.randint(1, 3))) + alt = prefix + base_username + if len(alt) <= policy["max_length"]: + alternatives.append(alt) + + # Validiere die alternativen Benutzernamen + valid_alternatives = [] + for alt in alternatives: + valid, _ = self.validate_username(alt, policy=policy) + if valid: + valid_alternatives.append(alt) + + # Zufällige Auswahl aus den gültigen Alternativen (maximal 5) + if len(valid_alternatives) > 5: + valid_alternatives = random.sample(valid_alternatives, 5) + + logger.info(f"{len(valid_alternatives)} alternative Benutzernamen generiert für '{username}'") + + return valid_alternatives + + def validate_username(self, username: str, platform: str = "default", + policy: Optional[Dict[str, Any]] = None) -> Tuple[bool, str]: + """ + Überprüft, ob ein Benutzername den Richtlinien entspricht. + + Args: + username: Zu überprüfender Benutzername + platform: Name der Plattform + policy: Optionale Richtlinie (sonst wird die der Plattform verwendet) + + Returns: + (Gültigkeit, Fehlermeldung) + """ + # Richtlinie bestimmen + if not policy: + policy = self.get_platform_policy(platform) + + # Länge prüfen + if len(username) < policy["min_length"]: + return False, f"Benutzername ist zu kurz (mindestens {policy['min_length']} Zeichen erforderlich)" + + if len(username) > policy["max_length"]: + return False, f"Benutzername ist zu lang (maximal {policy['max_length']} Zeichen erlaubt)" + + # Erlaubte Zeichen prüfen + for char in username: + if char not in policy["allowed_chars"]: + return False, f"Unerlaubtes Zeichen: '{char}'" + + # Anfangszeichen prüfen + if username[0] not in policy["allowed_start_chars"]: + return False, f"Benutzername darf nicht mit '{username[0]}' beginnen" + + # Endzeichen prüfen + if username[-1] not in policy["allowed_end_chars"]: + return False, f"Benutzername darf nicht mit '{username[-1]}' enden" + + # Aufeinanderfolgende Sonderzeichen prüfen + if not policy["allowed_consecutive_special"]: + special_chars = set(policy["allowed_chars"]) - set(string.ascii_letters + string.digits) + for i in range(len(username) - 1): + if username[i] in special_chars and username[i+1] in special_chars: + return False, "Keine aufeinanderfolgenden Sonderzeichen erlaubt" + + # Disallowed words + for word in policy["disallowed_words"]: + if word.lower() in username.lower(): + return False, f"Der Benutzername darf '{word}' nicht enthalten" + + return True, "Benutzername ist gültig" \ No newline at end of file diff --git a/views/__pycache__/about_dialog.cpython-310.pyc b/views/__pycache__/about_dialog.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a72930964b59caddcfeda705dc75d760b0f5d635 GIT binary patch literal 2252 zcmZ8i&2HO95aup_MM<{fIH`-E1$)$?N}D!41cs5sMiQW@8^fss5C#aEyOt>Pue-}Q z5#*BtiIx(dq|~RW?(3=H8wBMvF;mO8YFV*M%7u*cEJ8Q)4~B}5ya9x3K4F?q8S!=2dZ`@{rZeM3%ZGrN^+NMaW-|wN>!oIB zz72u7Rr}69gwRAa{<_&cH4K#}hxw88;_M(7X{g}sUWlfkUVrDUQ77Rc>W4Q)o2=ph|B(-!RR?52xa zrtQ?LN49$H%9LfnMdbuRoW&{#ss+i_lQ?7f(Y+)bNWXn_eh?N!<6mpk8+oSqGxdNc zMU(Fbai*%JB+Le7IN(7VX0VB>6@bUG2+DX)84baVv&xS0EaQ=?I%UB^#RGIHdwOL^ zo*ak{ysxr~Q1DE>A>b;Al2FPZc%!}lX5-f04-%@nH{=-;dz)Mys=U}!T&mC0FwXX# z#{5X`)hQ2H4Vr%OTp)6B$t55ia+O-dAr@I89b)`5UYRRq|12pH0n@XG0)yq*fF%nc zkaj>grdUB4PRKD)^aP}V4yo6thB8N1Qvf*GtSKXzI;gF0wU~Z_1$41gqxQ4|d$+zp zMrh@t2WI?4o8tA%E#Iued|c7*zwHsf6(;c@OCd?Ug}DMgj#a`1!bo7im5WFWlDbhk zP?S4ox9YkAj`6$!Ubyhv2!mX(Cn7AWR>;_sm_Zu}6CHJ;B$qtcuiFf22tYpMOe})V zx56mmh3Zjp2~Dk}ZcuRr4h^}ATo=d+EV&HCg#tKq0SeF|E?pv5$@t2JGMoYELLF?l ziL0)|ufRa2G_inmN*G}jtME#DO;2fNo}Z2((thc-DaS9<4F3c)a5xzG*J`$aBPxdz$0_wKFmb{c_q2)!L1s+dFL zc{?%p%1punVBGR%mu~ZCVOk`-ALZ!>ZUGKFlf1;)wHb5Cy`B6(9YGg+u~c3Xk9jsO z4!~@v;sN+&9t*{RmzBaxW91Fry-h&Dh4-*F_254rL639g9dW^U)*l;d!!Osz=Gt}K z^tECgB-e}e@*{X9oPK%Z-T30hO>biu!=D!+>M9K~!Lzbor=Ub2zx#QgLw*_@%YJ?+ z@P5x}=8l7(4V*^;VJ1EXQrQ5-#~`YVQ66Vv6&0W09lM6R-tXa-rJj=R(qDBpsQ=i;O1+v;Y28@@R>U!=hC zdOove^XANGBNx2+2rV{$MV|FTQzP^`7e*ZrhEX|d&0kx`NiNZB6=r{juIT>(H3?#h literal 0 HcmV?d00001 diff --git a/views/__pycache__/main_window.cpython-310.pyc b/views/__pycache__/main_window.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c1155ef95cbbb4e6dd6c5275c5a37c3fe1ac46d GIT binary patch literal 6216 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 literal 0 HcmV?d00001 diff --git a/views/__pycache__/platform_selector.cpython-313.pyc b/views/__pycache__/platform_selector.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29a6a0502f66a6c227edd733b1dbf1bdfd687e4a GIT binary patch literal 6025 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", + f"

{version_text}

", + "

© 2025 Chimaira

", + f"

{support}

", + f"

{license_text}

", + ] + self.info_label.setText("".join(lines)) + if lm: + self.setWindowTitle(lm.get_text("menu.about", "Über")) + self.close_button.setText(lm.get_text("buttons.ok", "OK")) + diff --git a/views/main_window.py b/views/main_window.py new file mode 100644 index 0000000..9036dc1 --- /dev/null +++ b/views/main_window.py @@ -0,0 +1,235 @@ +# Path: views/main_window.py + +""" +Hauptfenster der Social Media Account Generator Anwendung. +""" + +import os +import logging +from PyQt5.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QStackedWidget, QTabWidget, + QAction, QMessageBox +) +from PyQt5.QtCore import Qt, pyqtSignal, QSize, QFile +from PyQt5.QtGui import QIcon, QFont +from localization.language_manager import LanguageManager + +from views.platform_selector import PlatformSelector +from views.about_dialog import AboutDialog +from utils.logger import add_gui_handler + +logger = logging.getLogger("main") + +class MainWindow(QMainWindow): + """Hauptfenster der Anwendung.""" + + # Signale + platform_selected = pyqtSignal(str) + back_to_selector_requested = pyqtSignal() + theme_toggled = pyqtSignal() + + def __init__(self, theme_manager=None, language_manager=None, db_manager=None): + super().__init__() + + # Theme Manager + self.theme_manager = theme_manager + + # Language Manager + self.language_manager = language_manager + self.db_manager = db_manager + + # Fenstereigenschaften setzen + self.setWindowTitle("Social Media Account Generator") + # Größere Mindest- und Startgröße, damit Plattformnamen + # (z.B. "Twitter" und "VK") nicht abgeschnitten werden und + # Tabelleninhalte genügend Platz haben + self.setMinimumSize(1200, 700) + self.resize(1200, 700) + + # Hauptwidget und Layout + self.central_widget = QWidget() + self.setCentralWidget(self.central_widget) + + # Haupt-Layout + self.main_layout = QVBoxLayout(self.central_widget) + + # Gestapeltes Widget für Anzeige von Plattformwahl und Hauptfunktionen + self.stacked_widget = QStackedWidget() + self.main_layout.addWidget(self.stacked_widget) + + # Plattform-Auswahl-Widget + self.platform_selector = PlatformSelector(self.language_manager, self.db_manager) + self.stacked_widget.addWidget(self.platform_selector) + + # Container für Plattform-spezifische Tabs + self.platform_container = QWidget() + self.platform_layout = QVBoxLayout(self.platform_container) + + # Header-Bereich mit Titel und Zurück-Button + self.header_widget = QWidget() + self.header_layout = QHBoxLayout(self.header_widget) + self.header_layout.setContentsMargins(0, 0, 0, 0) + + # Zurück-Button + self.back_button = QPushButton("↩ Zurück") + self.back_button.setFixedWidth(100) + self.header_layout.addWidget(self.back_button) + + # Plattform-Titel + self.platform_title = QLabel() + title_font = QFont() + title_font.setPointSize(14) + title_font.setBold(True) + self.platform_title.setFont(title_font) + self.platform_title.setAlignment(Qt.AlignCenter) + self.header_layout.addWidget(self.platform_title) + + # Platzhalter für die rechte Seite, um die Zentrierung zu erhalten + spacer = QLabel() + spacer.setFixedWidth(100) + self.header_layout.addWidget(spacer) + + self.platform_layout.addWidget(self.header_widget) + + # Tabs für die Plattform + self.tabs = QTabWidget() + self.platform_layout.addWidget(self.tabs) + + # Stacked Widget hinzufügen + self.stacked_widget.addWidget(self.platform_container) + + # Anfänglich Platform-Selektor anzeigen + self.stacked_widget.setCurrentWidget(self.platform_selector) + + # Statusleiste + self.statusBar().showMessage("Bereit") + + # "Über"-Menü erstellen + if self.language_manager: + self._create_menus() + # Verbinde das Sprachänderungssignal mit der UI-Aktualisierung + self.language_manager.language_changed.connect(self.refresh_language_ui) + + # Verbinde Signale + self.connect_signals() + + def connect_signals(self): + """Verbindet die internen Signale.""" + # Platform-Selector-Signal verbinden + self.platform_selector.platform_selected.connect(self.platform_selected) + + # Zurück-Button-Signal verbinden + self.back_button.clicked.connect(self.back_to_selector_requested) + + def init_platform_ui(self, platform: str, platform_controller): + """Initialisiert die plattformspezifische UI.""" + # Tabs entfernen (falls vorhanden) + while self.tabs.count() > 0: + self.tabs.removeTab(0) + + # Plattform-Titel setzen + gen_text = self.language_manager.get_text("tabs.generator", "Account Generator") if self.language_manager else "Account Generator" + self.platform_title.setText(f"{platform.title()} {gen_text}") + + # Icon laden und anzeigen + if self.theme_manager: + icon_path = self.theme_manager.get_icon_path(platform.lower()) + if os.path.exists(icon_path): + self.setWindowTitle(f"{platform.title()} {gen_text}") + self.setWindowIcon(QIcon(icon_path)) + + # Tabs von den Plattform-Controllern holen und hinzufügen + self.add_platform_tabs(platform_controller) + + def _create_menus(self): + """Erstellt die Menüeinträge für "Über" und Sprachen.""" + + # "Über"-Aktion + self.about_action = QAction(self.language_manager.get_text("menu.about", "Über"), self) + self.menuBar().addAction(self.about_action) + self.about_action.triggered.connect(self._show_about_dialog) + + + def _show_about_dialog(self): + """Öffnet den Über-Dialog.""" + dialog = AboutDialog(self.language_manager, self) + dialog.exec_() + + + def refresh_language_ui(self): + """ + Aktualisiert alle UI-Texte nach einem Sprachwechsel. + Diese Methode wird beim Language-Changed-Signal aufgerufen. + """ + if not self.language_manager: + return + + # Fenstername aktualisieren + self.setWindowTitle(self.language_manager.get_text("main.title", "Social Media Account Generator")) + + # Status-Nachricht aktualisieren + self.statusBar().showMessage(self.language_manager.get_text("status.ready", "Bereit")) + + # Den Zurück-Button aktualisieren + self.back_button.setText(self.language_manager.get_text("buttons.back", "↩ Zurück")) + + # Menüs aktualisieren + self.about_action.setText(self.language_manager.get_text("menu.about", "Über")) + + # Die Platform Selector-View aktualisieren + if hasattr(self.platform_selector, "update_texts"): + self.platform_selector.update_texts() + + # Die aktuelle Plattform-UI aktualisieren, falls vorhanden + current_platform = self.platform_title.text().split()[0].lower() if self.platform_title.text() else None + if current_platform: + gen_text = self.language_manager.get_text("tabs.generator", "Account Generator") + self.platform_title.setText(f"{current_platform.title()} {gen_text}") + + # Tabs aktualisieren + tab_names = { + 0: self.language_manager.get_text("tabs.generator", "Account Generator"), + 1: self.language_manager.get_text("tabs.settings", "Einstellungen") + } + + for i in range(self.tabs.count()): + self.tabs.setTabText(i, tab_names.get(i, self.tabs.tabText(i))) + + # Aktualisierung erzwingen + self.repaint() + + def add_platform_tabs(self, platform_controller): + """Fügt die Tabs vom Plattform-Controller hinzu.""" + # Generator-Tab + if hasattr(platform_controller, "get_generator_tab"): + generator_tab = platform_controller.get_generator_tab() + gen_text = self.language_manager.get_text("tabs.generator", "Account Generator") if self.language_manager else "Account Generator" + self.tabs.addTab(generator_tab, gen_text) + + # Einstellungen-Tab + if hasattr(platform_controller, "get_settings_tab"): + settings_tab = platform_controller.get_settings_tab() + settings_text = self.language_manager.get_text("tabs.settings", "Einstellungen") if self.language_manager else "Einstellungen" + self.tabs.addTab(settings_tab, settings_text) + + + def show_platform_ui(self): + """Zeigt die plattformspezifische UI an.""" + self.stacked_widget.setCurrentWidget(self.platform_container) + + def show_platform_selector(self): + """Zeigt den Plattform-Selektor an.""" + self.stacked_widget.setCurrentWidget(self.platform_selector) + self.setWindowTitle("Social Media Account Generator") + + # Standard-Icon zurücksetzen + self.setWindowIcon(QIcon()) + + def set_status_message(self, message: str): + """Setzt eine Nachricht in der Statusleiste.""" + self.statusBar().showMessage(message) + + def add_log_widget(self, text_widget): + """Fügt einen GUI-Handler zum Logger hinzu.""" + add_gui_handler(logger, text_widget) \ No newline at end of file diff --git a/views/platform_selector.py b/views/platform_selector.py new file mode 100644 index 0000000..be2395e --- /dev/null +++ b/views/platform_selector.py @@ -0,0 +1,148 @@ +# Path: views/platform_selector.py + +""" +Plattformauswahl-Widget für die Social Media Account Generator Anwendung. +""" + +import os +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QGridLayout, QLabel, + QHBoxLayout +) +from PyQt5.QtCore import pyqtSignal, Qt, QSize +from PyQt5.QtGui import QFont + +from views.widgets.platform_button import PlatformButton +from views.widgets.language_dropdown import LanguageDropdown +from views.tabs.accounts_tab import AccountsTab + +class PlatformSelector(QWidget): + """Widget zur Auswahl der Plattform.""" + + # Signal wird ausgelöst, wenn eine Plattform ausgewählt wird + platform_selected = pyqtSignal(str) + + def __init__(self, language_manager=None, db_manager=None): + super().__init__() + self.language_manager = language_manager + self.db_manager = db_manager + self.init_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + main_layout = QHBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + + # ----------------------------- + # Linke Seite: Plattformwahl + # ----------------------------- + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + + self.title_label = QLabel("Social Media Account Generator") + self.title_label.setAlignment(Qt.AlignCenter) + title_font = QFont() + title_font.setPointSize(18) + title_font.setBold(True) + self.title_label.setFont(title_font) + left_layout.addWidget(self.title_label) + + self.subtitle_label = QLabel("Wählen Sie eine Plattform") + self.subtitle_label.setAlignment(Qt.AlignCenter) + subtitle_font = QFont() + subtitle_font.setPointSize(12) + self.subtitle_label.setFont(subtitle_font) + left_layout.addWidget(self.subtitle_label) + + platforms_container = QWidget() + grid_layout = QGridLayout(platforms_container) + grid_layout.setSpacing(40) + + # Definiere verfügbare Plattformen + platforms = [ + {"name": "Instagram", "enabled": True}, + {"name": "Facebook", "enabled": True}, + {"name": "TikTok", "enabled": True}, + {"name": "Twitter", "enabled": True}, + {"name": "VK", "enabled": True} + ] + + # Relativer Pfad zu den Icons - vom aktuellen Verzeichnis aus + current_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(current_dir) + icons_dir = os.path.join(parent_dir, "resources", "icons") + + # Platziere Buttons in einem 2x3 Grid + for i, platform in enumerate(platforms): + row = i // 3 + col = i % 3 + + # Icon-Pfad erstellen + icon_path = os.path.join(icons_dir, f"{platform['name'].lower()}.svg") + + # Verwende das Icon nur, wenn die Datei existiert + if not os.path.exists(icon_path): + icon_path = None + + button = PlatformButton( + platform["name"], + icon_path, + platform["enabled"] + ) + button.clicked.connect(lambda checked=False, p=platform["name"]: self.platform_selected.emit(p.lower())) + grid_layout.addWidget(button, row, col, Qt.AlignCenter) + + left_layout.addWidget(platforms_container) + left_layout.addStretch() + + + # ----------------------------- + # Rechte Seite: Übersicht + # ----------------------------- + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + header_right_layout = QHBoxLayout() + header_right_layout.addStretch() + if self.language_manager: + self.language_dropdown = LanguageDropdown(self.language_manager) + header_right_layout.addWidget(self.language_dropdown) + right_layout.addLayout(header_right_layout) + + self.overview_label = QLabel("Übersicht") + self.overview_label.setAlignment(Qt.AlignCenter) + ov_font = QFont() + ov_font.setPointSize(14) + ov_font.setBold(True) + self.overview_label.setFont(ov_font) + right_layout.addWidget(self.overview_label) + + self.accounts_tab = AccountsTab(None, self.db_manager, self.language_manager) + right_layout.addWidget(self.accounts_tab) + + main_layout.addWidget(left_widget) + main_layout.addWidget(right_widget) + + def load_accounts(self): + """Lädt die Konten in der Übersicht neu.""" + if hasattr(self, "accounts_tab"): + self.accounts_tab.load_accounts() + + def update_texts(self): + """Aktualisiert die Texte gemäß der aktuellen Sprache.""" + if not self.language_manager: + return + + self.title_label.setText( + self.language_manager.get_text("main.title", "Social Media Account Generator") + ) + self.subtitle_label.setText( + self.language_manager.get_text("main.subtitle", "Wählen Sie eine Plattform") + ) + self.overview_label.setText( + self.language_manager.get_text("main.overview", "Übersicht") + ) diff --git a/views/tabs/__pycache__/accounts_tab.cpython-310.pyc b/views/tabs/__pycache__/accounts_tab.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab2d08bc295925eaff854b17d4d8242f3d3d1e44 GIT binary patch literal 6010 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 literal 0 HcmV?d00001 diff --git a/views/tabs/__pycache__/generator_tab.cpython-313.pyc b/views/tabs/__pycache__/generator_tab.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba31e57372d23a6e87adf014c356c34b85cf2a49 GIT binary patch literal 17410 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 literal 0 HcmV?d00001 diff --git a/views/tabs/accounts_tab.py b/views/tabs/accounts_tab.py new file mode 100644 index 0000000..e49c169 --- /dev/null +++ b/views/tabs/accounts_tab.py @@ -0,0 +1,207 @@ +""" +Tab zur Verwaltung der erstellten Social-Media-Accounts. +""" + +import logging +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, + QTableWidgetItem, QPushButton, QHeaderView, QMessageBox +) +from PyQt5.QtCore import pyqtSignal, Qt + +logger = logging.getLogger("accounts_tab") + +class AccountsTab(QWidget): + """Widget für den Konten-Tab.""" + + # Signale + refresh_requested = pyqtSignal() + export_requested = pyqtSignal() + delete_requested = pyqtSignal(int) # account_id + + def __init__(self, platform_name=None, db_manager=None, language_manager=None): + super().__init__() + self.platform_name = platform_name + self.db_manager = db_manager + self.language_manager = language_manager + self.init_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + # Konten laden, falls db_manager vorhanden + if self.db_manager: + self.load_accounts() + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + layout = QVBoxLayout(self) + + # Konten-Tabelle + self.accounts_table = QTableWidget() + self.accounts_table.setColumnCount(8) + self.accounts_table.setHorizontalHeaderLabels([ + "ID", + "Benutzername", + "Passwort", + "E-Mail", + "Handynummer", + "Name", + "Plattform", + "Erstellt am", + ]) + self.accounts_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + # ID-Spalte verstecken + self.accounts_table.setColumnHidden(0, True) + + layout.addWidget(self.accounts_table) + + # Button-Leiste + button_layout = QHBoxLayout() + + self.refresh_button = QPushButton("Aktualisieren") + self.refresh_button.clicked.connect(self.on_refresh_clicked) + + self.export_button = QPushButton("Exportieren") + self.export_button.clicked.connect(self.on_export_clicked) + + self.delete_button = QPushButton("Löschen") + self.delete_button.clicked.connect(self.on_delete_clicked) + + button_layout.addWidget(self.refresh_button) + button_layout.addWidget(self.export_button) + button_layout.addWidget(self.delete_button) + + layout.addLayout(button_layout) + + def load_accounts(self): + """Lädt Konten aus der Datenbank und zeigt sie in der Tabelle an.""" + try: + if (self.platform_name and str(self.platform_name).lower() not in ["all", ""] + and hasattr(self.db_manager, "get_accounts_by_platform")): + accounts = self.db_manager.get_accounts_by_platform(self.platform_name.lower()) + else: + accounts = self.db_manager.get_all_accounts() + if self.platform_name and str(self.platform_name).lower() not in ["all", ""]: + accounts = [ + acc for acc in accounts + if acc.get("platform", "").lower() == str(self.platform_name).lower() + ] + + self.display_accounts(accounts) + except Exception as e: + logger.error(f"Fehler beim Laden der Konten: {e}") + QMessageBox.critical(self, "Fehler", f"Fehler beim Laden der Konten:\n{str(e)}") + + def display_accounts(self, accounts): + """Zeigt die Konten in der Tabelle an.""" + self.accounts_table.setRowCount(len(accounts)) + + for row, account in enumerate(accounts): + self.accounts_table.setItem(row, 0, QTableWidgetItem(str(account.get("id", "")))) + self.accounts_table.setItem(row, 1, QTableWidgetItem(account.get("username", ""))) + self.accounts_table.setItem(row, 2, QTableWidgetItem(account.get("password", ""))) + self.accounts_table.setItem(row, 3, QTableWidgetItem(account.get("email", ""))) + self.accounts_table.setItem(row, 4, QTableWidgetItem(account.get("phone", ""))) + self.accounts_table.setItem(row, 5, QTableWidgetItem(account.get("full_name", ""))) + self.accounts_table.setItem(row, 6, QTableWidgetItem(account.get("platform", ""))) + self.accounts_table.setItem(row, 7, QTableWidgetItem(account.get("created_at", ""))) + + def on_refresh_clicked(self): + """Wird aufgerufen, wenn der Aktualisieren-Button geklickt wird.""" + self.refresh_requested.emit() + + # Direkt aktualisieren, falls db_manager vorhanden + if self.db_manager: + self.load_accounts() + + def on_export_clicked(self): + """Wird aufgerufen, wenn der Exportieren-Button geklickt wird.""" + self.export_requested.emit() + + def on_delete_clicked(self): + """Wird aufgerufen, wenn der Löschen-Button geklickt wird.""" + selected_rows = self.accounts_table.selectionModel().selectedRows() + if not selected_rows: + title = "Kein Konto ausgewählt" + text = "Bitte wählen Sie ein Konto zum Löschen aus." + if self.language_manager: + title = self.language_manager.get_text( + "accounts_tab.no_selection_title", title + ) + text = self.language_manager.get_text( + "accounts_tab.no_selection_text", text + ) + QMessageBox.warning(self, title, text) + return + + account_id = int(self.accounts_table.item(selected_rows[0].row(), 0).text()) + username = self.accounts_table.item(selected_rows[0].row(), 1).text() + + q_title = "Konto löschen" + q_text = f"Möchten Sie das Konto '{username}' wirklich löschen?" + if self.language_manager: + q_title = self.language_manager.get_text("accounts_tab.delete_title", q_title) + q_text = self.language_manager.get_text( + "accounts_tab.delete_text", q_text + ).format(username=username) + reply = QMessageBox.question( + self, + q_title, + q_text, + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + + if reply == QMessageBox.Yes: + self.delete_requested.emit(account_id) + + # Direkt aktualisieren, falls db_manager vorhanden + if self.db_manager: + success = self.db_manager.delete_account(account_id) + if success: + self.load_accounts() + suc_title = "Erfolg" + suc_text = f"Konto '{username}' wurde gelöscht." + if self.language_manager: + suc_title = self.language_manager.get_text( + "accounts_tab.delete_success_title", suc_title + ) + suc_text = self.language_manager.get_text( + "accounts_tab.delete_success_text", suc_text + ).format(username=username) + QMessageBox.information(self, suc_title, suc_text) + else: + err_title = "Fehler" + err_text = f"Konto '{username}' konnte nicht gelöscht werden." + if self.language_manager: + err_title = self.language_manager.get_text( + "accounts_tab.delete_error_title", err_title + ) + err_text = self.language_manager.get_text( + "accounts_tab.delete_error_text", err_text + ).format(username=username) + QMessageBox.critical(self, err_title, err_text) + + def update_texts(self): + """Aktualisiert UI-Texte gemäß aktueller Sprache.""" + if not self.language_manager: + return + lm = self.language_manager + self.refresh_button.setText(lm.get_text("buttons.refresh", "Aktualisieren")) + self.export_button.setText(lm.get_text("buttons.export", "Exportieren")) + self.delete_button.setText(lm.get_text("buttons.delete", "Löschen")) + headers = lm.get_text( + "accounts_tab.headers", + [ + "ID", + "Benutzername", + "Passwort", + "E-Mail", + "Handynummer", + "Name", + "Plattform", + "Erstellt am", + ], + ) + self.accounts_table.setHorizontalHeaderLabels(headers) diff --git a/views/tabs/generator_tab.py b/views/tabs/generator_tab.py new file mode 100644 index 0000000..bdce6b8 --- /dev/null +++ b/views/tabs/generator_tab.py @@ -0,0 +1,500 @@ +# Pfad: views/tabs/generator_tab.py + +""" +Tab zur Erstellung von Social-Media-Accounts. +""" + +import logging +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, + QGroupBox, QLabel, QLineEdit, QSpinBox, QRadioButton, + QCheckBox, QComboBox, QPushButton, QTextEdit, QProgressBar, + QMessageBox +) +from PyQt5.QtCore import Qt, pyqtSignal + +from utils.logger import add_gui_handler + +logger = logging.getLogger("generator_tab") + +class GeneratorTab(QWidget): + """Widget für den Account-Generator-Tab.""" + + # Signale + start_requested = pyqtSignal(dict) + stop_requested = pyqtSignal() + account_created = pyqtSignal(str, dict) # (platform, account_data) + + def __init__(self, platform_name, language_manager=None): + super().__init__() + self.platform_name = platform_name + self.language_manager = language_manager + self.init_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + layout = QVBoxLayout(self) + + # Formularbereich + self.form_group = QGroupBox() + form_layout = QFormLayout() + self.form_group.setLayout(form_layout) + + # Vorname und Nachname statt vollständigem Namen + self.first_name_label = QLabel() + self.first_name_input = QLineEdit() + form_layout.addRow(self.first_name_label, self.first_name_input) + + self.last_name_label = QLabel() + self.last_name_input = QLineEdit() + form_layout.addRow(self.last_name_label, self.last_name_input) + + # Alter (jetzt mit QLineEdit statt QSpinBox) + self.age_label = QLabel() + self.age_input = QLineEdit() + form_layout.addRow(self.age_label, self.age_input) + + # Registrierungsmethode + self.reg_method_group = QWidget() + reg_method_layout = QHBoxLayout(self.reg_method_group) + reg_method_layout.setContentsMargins(0, 0, 0, 0) + + self.email_radio = QRadioButton() + self.phone_radio = QRadioButton() + self.email_radio.setChecked(True) + + reg_method_layout.addWidget(self.email_radio) + reg_method_layout.addWidget(self.phone_radio) + + self.reg_method_label = QLabel() + form_layout.addRow(self.reg_method_label, self.reg_method_group) + + # Telefonnummer (nur sichtbar, wenn Telefon ausgewählt) + self.phone_label = QLabel() + self.phone_input = QLineEdit() + self.phone_input.setEnabled(False) + form_layout.addRow(self.phone_label, self.phone_input) + + # E-Mail-Domain + self.email_domain_label = QLabel() + self.email_domain_input = QLineEdit("z5m7q9dk3ah2v1plx6ju.com") + form_layout.addRow(self.email_domain_label, self.email_domain_input) + + # Proxy verwenden + self.use_proxy_check = QCheckBox() + self.use_proxy_check.setChecked(True) + + # Proxy-Typ + self.proxy_type_combo = QComboBox() + + proxy_widget = QWidget() + proxy_layout = QHBoxLayout(proxy_widget) + proxy_layout.setContentsMargins(0, 0, 0, 0) + proxy_layout.addWidget(self.use_proxy_check) + self.proxy_type_label = QLabel() + proxy_layout.addWidget(self.proxy_type_label) + proxy_layout.addWidget(self.proxy_type_combo) + proxy_layout.addStretch() + + self.proxy_label = QLabel() + form_layout.addRow(self.proxy_label, proxy_widget) + + # Headless-Modus + self.headless_check = QCheckBox() + form_layout.addRow("", self.headless_check) + + # Debug-Modus + self.debug_check = QCheckBox() + form_layout.addRow("", self.debug_check) + + # Plattformspezifische Parameter hinzufügen + self.add_platform_specific_fields(form_layout) + + # Formular zum Layout hinzufügen + layout.addWidget(self.form_group) + + # Fortschrittsanzeige + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + layout.addWidget(self.progress_bar) + + # Log-Bereich + self.log_group = QGroupBox() + log_layout = QVBoxLayout(self.log_group) + + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + log_layout.addWidget(self.log_text) + + # Log-Handler für das TextEdit + add_gui_handler(logger, self.log_text) + + layout.addWidget(self.log_group) + + # Buttons + button_layout = QHBoxLayout() + + self.start_button = QPushButton() + self.start_button.clicked.connect(self.on_start_clicked) + + self.stop_button = QPushButton() + self.stop_button.clicked.connect(self.on_stop_clicked) + self.stop_button.setEnabled(False) + + button_layout.addWidget(self.start_button) + button_layout.addWidget(self.stop_button) + + layout.addLayout(button_layout) + + # Event-Verbindungen + self.email_radio.toggled.connect(self.toggle_phone_input) + self.phone_radio.toggled.connect(self.toggle_phone_input) + self.use_proxy_check.toggled.connect(self.toggle_proxy_combo) + + def add_platform_specific_fields(self, form_layout): + """ + Fügt plattformspezifische Felder hinzu. + Diese Methode kann in abgeleiteten Klassen überschrieben werden. + + Args: + form_layout: Das Formular-Layout, zu dem die Felder hinzugefügt werden sollen + """ + # In plattformspezifischen Unterklassen überschreiben + platform = self.platform_name.lower() + + if platform == "tiktok": + # Beispiel: Kategorie/Nische für TikTok + self.category_label = QLabel() + self.category_combo = QComboBox() + + categories = [ + "Allgemein", + "Gaming", + "Mode", + "Fitness", + "Reisen", + "Kochen", + "Technologie", + "Bildung", + ] + + if self.language_manager: + categories = [ + self.language_manager.get_text( + "generator_tab.tiktok_category_general", "Allgemein" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_gaming", "Gaming" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_fashion", "Mode" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_fitness", "Fitness" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_travel", "Reisen" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_cooking", "Kochen" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_technology", "Technologie" + ), + self.language_manager.get_text( + "generator_tab.tiktok_category_education", "Bildung" + ), + ] + + self.category_label.setText( + self.language_manager.get_text( + "generator_tab.tiktok_category_label", "Kategorie/Nische:" + ) + ) + else: + self.category_label.setText("Kategorie/Nische:") + + self.category_combo.addItems(categories) + form_layout.addRow(self.category_label, self.category_combo) + + elif platform == "twitter": + # Beispiel: Interessen für Twitter + self.interests_combo = QComboBox() + interests = ["Allgemein", "Politik", "Technologie", "Wirtschaft", "Sport", "Unterhaltung", "Wissenschaft"] + self.interests_combo.addItems(interests) + form_layout.addRow("Interessen:", self.interests_combo) + + elif platform == "facebook": + # Beispiel: Datenschutzeinstellungen für Facebook + self.privacy_combo = QComboBox() + privacy_options = ["Öffentlich", "Nur Freunde", "Freunde außer...", "Nur ich"] + self.privacy_combo.addItems(privacy_options) + form_layout.addRow("Datenschutz:", self.privacy_combo) + + def toggle_phone_input(self): + """Aktiviert/Deaktiviert das Telefoneingabefeld basierend auf der Radiobutton-Auswahl.""" + self.phone_input.setEnabled(self.phone_radio.isChecked()) + + def toggle_proxy_combo(self): + """Aktiviert/Deaktiviert die Proxy-Typ-Combobox basierend auf der Checkbox.""" + self.proxy_type_combo.setEnabled(self.use_proxy_check.isChecked()) + + def on_start_clicked(self): + """Wird aufgerufen, wenn der Start-Button geklickt wird.""" + # Parameter sammeln + params = self.get_params() + + # Eingaben validieren + valid, error_msg = self.validate_inputs(params) + if not valid: + self.show_error(error_msg) + return + + # Signal auslösen + self.start_requested.emit(params) + + def on_stop_clicked(self): + """Wird aufgerufen, wenn der Stop-Button geklickt wird.""" + self.stop_requested.emit() + + def get_params(self): + """Sammelt alle Parameter für die Account-Erstellung.""" + # Vorname und Nachname kombinieren für den vollständigen Namen + first_name = self.first_name_input.text().strip() + last_name = self.last_name_input.text().strip() + full_name = f"{first_name} {last_name}" + + params = { + "first_name": first_name, + "last_name": last_name, + "full_name": full_name, + "age_text": self.age_input.text().strip(), # Speichere den Rohtext + "registration_method": "email" if self.email_radio.isChecked() else "phone", + "headless": self.headless_check.isChecked(), + "debug": self.debug_check.isChecked(), + "email_domain": self.email_domain_input.text().strip() + } + + # Telefonnummer (wenn ausgewählt) + if self.phone_radio.isChecked(): + params["phone_number"] = self.phone_input.text().strip() + + # Proxy (wenn aktiviert) + if self.use_proxy_check.isChecked(): + proxy_type = self.proxy_type_combo.currentText().lower() + params["use_proxy"] = True + params["proxy_type"] = proxy_type + else: + params["use_proxy"] = False + + # Plattformspezifische Parameter + additional_params = self.get_platform_specific_params() + if additional_params: + params["additional_params"] = additional_params + + return params + + def validate_inputs(self, params): + """ + Validiert die Eingaben für die Account-Erstellung. + + Args: + params: Die gesammelten Parameter + + Returns: + tuple: (gültig, Fehlermeldung) + """ + # Namen prüfen + if not params.get("first_name"): + msg = "Bitte geben Sie einen Vornamen ein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.first_name_error", + "Bitte geben Sie einen Vornamen ein.", + ) + return False, msg + + if not params.get("last_name"): + msg = "Bitte geben Sie einen Nachnamen ein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.last_name_error", + "Bitte geben Sie einen Nachnamen ein.", + ) + return False, msg + + # Alter prüfen + age_text = params.get("age_text", "") + if not age_text: + msg = "Bitte geben Sie ein Alter ein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.age_empty_error", + "Bitte geben Sie ein Alter ein.", + ) + return False, msg + + # Alter muss eine Zahl sein + try: + age = int(age_text) + params["age"] = age # Füge das konvertierte Alter zu den Parametern hinzu + except ValueError: + msg = "Das Alter muss eine ganze Zahl sein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.age_int_error", + "Das Alter muss eine ganze Zahl sein.", + ) + return False, msg + + # Alter-Bereich prüfen - hier ist die allgemeine Prüfung für 13-99 + # Die plattformspezifische Validierung kann später in den Controllern erfolgen + if age < 13 or age > 99: + msg = "Das Alter muss zwischen 13 und 99 liegen." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.age_range_error", + "Das Alter muss zwischen 13 und 99 liegen.", + ) + return False, msg + + # Telefonnummer prüfen, falls erforderlich + if params.get("registration_method") == "phone" and not params.get("phone_number"): + msg = "Bitte geben Sie eine Telefonnummer ein." + if self.language_manager: + msg = self.language_manager.get_text( + "generator_tab.phone_error", + "Bitte geben Sie eine Telefonnummer ein.", + ) + return False, msg + + return True, "" + + def get_platform_specific_params(self): + """ + Gibt plattformspezifische Parameter zurück. + Diese Methode kann in abgeleiteten Klassen überschrieben werden. + + Returns: + dict: Plattformspezifische Parameter + """ + # In plattformspezifischen Unterklassen überschreiben + platform = self.platform_name.lower() + additional_params = {} + + if platform == "tiktok" and hasattr(self, "category_combo"): + additional_params["category"] = self.category_combo.currentText() + + elif platform == "twitter" and hasattr(self, "interests_combo"): + additional_params["interests"] = self.interests_combo.currentText() + + elif platform == "facebook" and hasattr(self, "privacy_combo"): + additional_params["privacy"] = self.privacy_combo.currentText() + + return additional_params + + def set_running(self, running: bool): + """Setzt den Status auf 'Wird ausgeführt' oder 'Bereit'.""" + self.start_button.setEnabled(not running) + self.stop_button.setEnabled(running) + + def clear_log(self): + """Löscht den Log-Bereich.""" + self.log_text.clear() + + def add_log(self, message: str): + """Fügt eine Nachricht zum Log-Bereich hinzu.""" + self.log_text.append(message) + # Scrolle nach unten + # Korrigierter Code für die Qt-Version + try: + self.log_text.moveCursor(Qt.MoveOperation.MoveEnd) + except AttributeError: + # Fallback für ältere Qt-Versionen + try: + self.log_text.moveCursor(Qt.MoveEnd) + except AttributeError: + # Weitere Fallbacks + try: + from PyQt5.QtGui import QTextCursor + self.log_text.moveCursor(QTextCursor.End) + except: + pass # Im Notfall einfach ignorieren + + def set_progress(self, value: int): + """Setzt den Fortschritt der Fortschrittsanzeige.""" + self.progress_bar.setValue(value) + + def set_status(self, message: str): + """Setzt die Statusnachricht.""" + # Diese Methode könnte später um eine Statusleiste erweitert werden + self.add_log(message) + + def show_error(self, message: str): + """Zeigt eine Fehlermeldung an.""" + title = "Fehler" + if self.language_manager: + title = self.language_manager.get_text( + "generator_tab.error_title", "Fehler" + ) + QMessageBox.critical(self, title, message) + + def update_texts(self): + """Aktualisiert UI-Texte gemäß der aktuellen Sprache.""" + if not self.language_manager: + return + + lm = self.language_manager + + self.form_group.setTitle(lm.get_text("generator_tab.form_title", "Account-Informationen")) + self.first_name_label.setText(lm.get_text("generator_tab.first_name_label", "Vorname:")) + self.first_name_input.setPlaceholderText(lm.get_text("generator_tab.first_name_placeholder", "z.B. Max")) + self.last_name_label.setText(lm.get_text("generator_tab.last_name_label", "Nachname:")) + self.last_name_input.setPlaceholderText(lm.get_text("generator_tab.last_name_placeholder", "z.B. Mustermann")) + self.age_label.setText(lm.get_text("generator_tab.age_label", "Alter:")) + self.age_input.setPlaceholderText(lm.get_text("generator_tab.age_placeholder", "Alter zwischen 13 und 99")) + self.reg_method_label.setText(lm.get_text("generator_tab.registration_method_label", "Registrierungsmethode:")) + self.email_radio.setText(lm.get_text("generator_tab.email_radio", "E-Mail")) + self.phone_radio.setText(lm.get_text("generator_tab.phone_radio", "Telefon")) + self.phone_label.setText(lm.get_text("generator_tab.phone_label", "Telefonnummer:")) + self.phone_input.setPlaceholderText(lm.get_text("generator_tab.phone_placeholder", "z.B. +49123456789")) + self.email_domain_label.setText(lm.get_text("generator_tab.email_domain_label", "E-Mail-Domain:")) + self.use_proxy_check.setText(lm.get_text("generator_tab.proxy_use", "Proxy verwenden")) + self.proxy_type_label.setText(lm.get_text("generator_tab.proxy_type_label", "Typ:")) + self.proxy_type_combo.clear() + self.proxy_type_combo.addItems([ + lm.get_text("generator_tab.proxy_type_ipv4", "IPv4"), + lm.get_text("generator_tab.proxy_type_ipv6", "IPv6"), + lm.get_text("generator_tab.proxy_type_mobile", "Mobile"), + ]) + self.proxy_label.setText(lm.get_text("generator_tab.proxy_label", "Proxy:")) + self.headless_check.setText(lm.get_text("generator_tab.headless", "Browser im Hintergrund ausführen")) + self.debug_check.setText(lm.get_text("generator_tab.debug", "Debug-Modus (detaillierte Protokollierung)")) + + platform = self.platform_name.lower() + if platform == "tiktok" and hasattr(self, "category_combo"): + self.category_label.setText( + lm.get_text("generator_tab.tiktok_category_label", "Kategorie/Nische:") + ) + categories = [ + lm.get_text("generator_tab.tiktok_category_general", "Allgemein"), + lm.get_text("generator_tab.tiktok_category_gaming", "Gaming"), + lm.get_text("generator_tab.tiktok_category_fashion", "Mode"), + lm.get_text("generator_tab.tiktok_category_fitness", "Fitness"), + lm.get_text("generator_tab.tiktok_category_travel", "Reisen"), + lm.get_text("generator_tab.tiktok_category_cooking", "Kochen"), + lm.get_text("generator_tab.tiktok_category_technology", "Technologie"), + lm.get_text("generator_tab.tiktok_category_education", "Bildung"), + ] + current = self.category_combo.currentText() + self.category_combo.clear() + self.category_combo.addItems(categories) + if current in categories: + self.category_combo.setCurrentIndex(categories.index(current)) + self.log_group.setTitle(lm.get_text("generator_tab.log_title", "Log")) + self.start_button.setText(lm.get_text("buttons.create", "Account erstellen")) + self.stop_button.setText(lm.get_text("buttons.cancel", "Abbrechen")) diff --git a/views/tabs/settings_tab.py b/views/tabs/settings_tab.py new file mode 100644 index 0000000..479e433 --- /dev/null +++ b/views/tabs/settings_tab.py @@ -0,0 +1,315 @@ +""" +Tab für die Einstellungen der Anwendung. +""" + +import logging +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, + QGroupBox, QLabel, QLineEdit, QSpinBox, QTextEdit, + QPushButton, QCheckBox, QComboBox +) +from PyQt5.QtCore import pyqtSignal, Qt + +logger = logging.getLogger("settings_tab") + +class SettingsTab(QWidget): + """Widget für den Einstellungen-Tab.""" + + # Signale + proxy_settings_saved = pyqtSignal(dict) + proxy_tested = pyqtSignal(str) # proxy_type + email_settings_saved = pyqtSignal(dict) + email_tested = pyqtSignal(dict) # email_settings + license_activated = pyqtSignal(str) # license_key + + def __init__(self, platform_name, proxy_rotator=None, email_handler=None, license_manager=None, language_manager=None): + super().__init__() + self.platform_name = platform_name + self.proxy_rotator = proxy_rotator + self.email_handler = email_handler + self.license_manager = license_manager + self.language_manager = language_manager + self.init_ui() + if self.language_manager: + self.language_manager.language_changed.connect(self.update_texts) + self.update_texts() + + # Einstellungen laden, falls Handler vorhanden + self.load_settings() + + def init_ui(self): + """Initialisiert die Benutzeroberfläche.""" + layout = QVBoxLayout(self) + + # Proxy-Einstellungen + proxy_group = QGroupBox("Proxy-Einstellungen") + proxy_layout = QVBoxLayout(proxy_group) + + # IPv4 Proxies + ipv4_form = QFormLayout() + self.ipv4_proxy_input = QTextEdit() + self.ipv4_proxy_input.setPlaceholderText("Ein Proxy pro Zeile im Format: host:port:username:password") + ipv4_form.addRow("IPv4 Proxies:", self.ipv4_proxy_input) + proxy_layout.addLayout(ipv4_form) + + # IPv6 Proxies + ipv6_form = QFormLayout() + self.ipv6_proxy_input = QTextEdit() + self.ipv6_proxy_input.setPlaceholderText("Ein Proxy pro Zeile im Format: host:port:username:password") + ipv6_form.addRow("IPv6 Proxies:", self.ipv6_proxy_input) + proxy_layout.addLayout(ipv6_form) + + # Mobile Proxies + mobile_form = QFormLayout() + self.mobile_proxy_input = QTextEdit() + self.mobile_proxy_input.setPlaceholderText("Ein Proxy pro Zeile im Format: host:port:username:password") + mobile_form.addRow("Mobile Proxies:", self.mobile_proxy_input) + proxy_layout.addLayout(mobile_form) + + # Mobile Proxy API Keys + api_key_layout = QFormLayout() + self.marsproxy_api_input = QLineEdit() + api_key_layout.addRow("MarsProxies API Key:", self.marsproxy_api_input) + + self.iproyal_api_input = QLineEdit() + api_key_layout.addRow("IPRoyal API Key:", self.iproyal_api_input) + + proxy_layout.addLayout(api_key_layout) + + # Test-Button + proxy_button_layout = QHBoxLayout() + + self.test_proxy_button = QPushButton("Proxy testen") + self.test_proxy_button.clicked.connect(self.on_test_proxy_clicked) + + self.save_proxy_button = QPushButton("Proxy-Einstellungen speichern") + self.save_proxy_button.clicked.connect(self.on_save_proxy_clicked) + + proxy_button_layout.addWidget(self.test_proxy_button) + proxy_button_layout.addWidget(self.save_proxy_button) + + proxy_layout.addLayout(proxy_button_layout) + + layout.addWidget(proxy_group) + + # E-Mail-Einstellungen + email_group = QGroupBox("E-Mail-Einstellungen") + email_layout = QFormLayout(email_group) + + self.imap_server_input = QLineEdit("imap.ionos.de") + email_layout.addRow("IMAP-Server:", self.imap_server_input) + + self.imap_port_input = QSpinBox() + self.imap_port_input.setRange(1, 65535) + self.imap_port_input.setValue(993) + email_layout.addRow("IMAP-Port:", self.imap_port_input) + + self.imap_user_input = QLineEdit() + email_layout.addRow("IMAP-Benutzername:", self.imap_user_input) + + self.imap_pass_input = QLineEdit() + self.imap_pass_input.setEchoMode(QLineEdit.Password) + email_layout.addRow("IMAP-Passwort:", self.imap_pass_input) + + email_button_layout = QHBoxLayout() + + self.test_email_button = QPushButton("E-Mail testen") + self.test_email_button.clicked.connect(self.on_test_email_clicked) + + self.save_email_button = QPushButton("E-Mail-Einstellungen speichern") + self.save_email_button.clicked.connect(self.on_save_email_clicked) + + email_button_layout.addWidget(self.test_email_button) + email_button_layout.addWidget(self.save_email_button) + + email_layout.addRow("", email_button_layout) + + layout.addWidget(email_group) + + # Plattformspezifische Einstellungen + if self.platform_name.lower() != "instagram": + self.add_platform_specific_settings(layout) + + # Lizenz-Gruppe + license_group = QGroupBox("Lizenz") + license_layout = QFormLayout(license_group) + + self.license_key_input = QLineEdit() + license_layout.addRow("Lizenzschlüssel:", self.license_key_input) + + self.activate_license_button = QPushButton("Lizenz aktivieren") + self.activate_license_button.clicked.connect(self.on_activate_license_clicked) + license_layout.addRow("", self.activate_license_button) + + layout.addWidget(license_group) + + # Stretch am Ende hinzufügen + layout.addStretch(1) + + def add_platform_specific_settings(self, layout): + """ + Fügt plattformspezifische Einstellungen hinzu. + Diese Methode kann in abgeleiteten Klassen überschrieben werden. + + Args: + layout: Das Layout, zu dem die Einstellungen hinzugefügt werden sollen + """ + platform = self.platform_name.lower() + + platform_settings_group = QGroupBox(f"{self.platform_name}-spezifische Einstellungen") + platform_settings_layout = QFormLayout(platform_settings_group) + + # Je nach Plattform unterschiedliche Einstellungen + if platform == "twitter": + self.twitter_api_key = QLineEdit() + platform_settings_layout.addRow("Twitter API Key:", self.twitter_api_key) + + self.twitter_api_secret = QLineEdit() + platform_settings_layout.addRow("Twitter API Secret:", self.twitter_api_secret) + + elif platform == "tiktok": + self.video_upload = QCheckBox("Video automatisch hochladen") + platform_settings_layout.addRow("", self.video_upload) + + self.follower_action = QCheckBox("Automatisch anderen Nutzern folgen") + platform_settings_layout.addRow("", self.follower_action) + + elif platform == "facebook": + self.page_creation = QCheckBox("Seite automatisch erstellen") + platform_settings_layout.addRow("", self.page_creation) + + self.privacy_level = QComboBox() + self.privacy_level.addItems(["Öffentlich", "Freunde", "Nur ich"]) + platform_settings_layout.addRow("Datenschutzeinstellung:", self.privacy_level) + + # Speichern-Button für plattformspezifische Einstellungen + self.platform_save_button = QPushButton(f"{self.platform_name}-Einstellungen speichern") + self.platform_save_button.clicked.connect(self.on_save_platform_settings_clicked) + platform_settings_layout.addRow("", self.platform_save_button) + + layout.addWidget(platform_settings_group) + + def load_settings(self): + """Lädt die Einstellungen aus den Handlern.""" + # Proxy-Einstellungen laden + if self.proxy_rotator: + try: + proxy_config = self.proxy_rotator.get_config() or {} + + # IPv4 Proxies + ipv4_proxies = proxy_config.get("ipv4", []) + self.ipv4_proxy_input.setPlainText("\n".join(ipv4_proxies)) + + # IPv6 Proxies + ipv6_proxies = proxy_config.get("ipv6", []) + self.ipv6_proxy_input.setPlainText("\n".join(ipv6_proxies)) + + # Mobile Proxies + mobile_proxies = proxy_config.get("mobile", []) + self.mobile_proxy_input.setPlainText("\n".join(mobile_proxies)) + + # API Keys + mobile_api = proxy_config.get("mobile_api", {}) + self.marsproxy_api_input.setText(mobile_api.get("marsproxies", "")) + self.iproyal_api_input.setText(mobile_api.get("iproyal", "")) + + except Exception as e: + logger.error(f"Fehler beim Laden der Proxy-Einstellungen: {e}") + + # E-Mail-Einstellungen laden + if self.email_handler: + try: + email_config = self.email_handler.get_config() or {} + + self.imap_server_input.setText(email_config.get("imap_server", "imap.ionos.de")) + self.imap_port_input.setValue(email_config.get("imap_port", 993)) + self.imap_user_input.setText(email_config.get("imap_user", "")) + self.imap_pass_input.setText(email_config.get("imap_pass", "")) + + except Exception as e: + logger.error(f"Fehler beim Laden der E-Mail-Einstellungen: {e}") + + # Lizenzeinstellungen laden + if self.license_manager: + try: + license_info = self.license_manager.get_license_info() + self.license_key_input.setText(license_info.get("key", "")) + + except Exception as e: + logger.error(f"Fehler beim Laden der Lizenzeinstellungen: {e}") + + def on_save_proxy_clicked(self): + """Wird aufgerufen, wenn der Proxy-Speichern-Button geklickt wird.""" + # Proxy-Einstellungen sammeln + settings = { + "ipv4_proxies": self.ipv4_proxy_input.toPlainText(), + "ipv6_proxies": self.ipv6_proxy_input.toPlainText(), + "mobile_proxies": self.mobile_proxy_input.toPlainText(), + "mobile_api": { + "marsproxies": self.marsproxy_api_input.text().strip(), + "iproyal": self.iproyal_api_input.text().strip() + } + } + + # Signal auslösen + self.proxy_settings_saved.emit(settings) + + def on_test_proxy_clicked(self): + """Wird aufgerufen, wenn der Proxy-Test-Button geklickt wird.""" + # Proxy-Typ aus der Combobox in der Generator-Tab holen + # Da wir keine direkte Referenz haben, nehmen wir den ersten Eintrag + proxy_type = "ipv4" + + # Signal auslösen + self.proxy_tested.emit(proxy_type) + + def on_save_email_clicked(self): + """Wird aufgerufen, wenn der E-Mail-Speichern-Button geklickt wird.""" + # E-Mail-Einstellungen sammeln + settings = { + "imap_server": self.imap_server_input.text().strip(), + "imap_port": self.imap_port_input.value(), + "imap_user": self.imap_user_input.text().strip(), + "imap_pass": self.imap_pass_input.text() + } + + # Signal auslösen + self.email_settings_saved.emit(settings) + + def on_test_email_clicked(self): + """Wird aufgerufen, wenn der E-Mail-Test-Button geklickt wird.""" + # E-Mail-Einstellungen sammeln + settings = { + "imap_server": self.imap_server_input.text().strip(), + "imap_port": self.imap_port_input.value(), + "imap_user": self.imap_user_input.text().strip(), + "imap_pass": self.imap_pass_input.text() + } + + # Signal auslösen + self.email_tested.emit(settings) + + def on_save_platform_settings_clicked(self): + """Wird aufgerufen, wenn der Plattform-Einstellungen-Speichern-Button geklickt wird.""" + # Hier könnte ein plattformspezifisches Signal ausgelöst werden + logger.info(f"{self.platform_name}-Einstellungen gespeichert") + + def on_activate_license_clicked(self): + """Wird aufgerufen, wenn der Lizenz-Aktivieren-Button geklickt wird.""" + license_key = self.license_key_input.text().strip() + + if not license_key: + return + + # Signal auslösen + self.license_activated.emit(license_key) + + def update_texts(self): + """Aktualisiert UI-Texte gemäß aktueller Sprache.""" + if not self.language_manager: + return + self.test_proxy_button.setText(self.language_manager.get_text("buttons.test_proxy", "Proxy testen")) + self.save_proxy_button.setText(self.language_manager.get_text("buttons.save_proxy", "Proxy-Einstellungen speichern")) + self.test_email_button.setText(self.language_manager.get_text("buttons.test_email", "E-Mail testen")) + self.save_email_button.setText(self.language_manager.get_text("buttons.save_email", "E-Mail-Einstellungen speichern")) + self.activate_license_button.setText(self.language_manager.get_text("buttons.activate_license", "Lizenz aktivieren")) diff --git a/views/widgets/__pycache__/language_dropdown.cpython-310.pyc b/views/widgets/__pycache__/language_dropdown.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c795a99af65ee131593200ac24993c190dc5020c GIT binary patch literal 6259 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 aPW&e`^bIrggFE4L1b)5zCkDR>?f(S_dJ_cz literal 0 HcmV?d00001 diff --git a/views/widgets/__pycache__/platform_button.cpython-310.pyc b/views/widgets/__pycache__/platform_button.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d105f167555023c8c40bfc847c0f2f0f678e388 GIT binary patch literal 2184 zcma)7TW=dh6rS0uy}l%v(hH%~yr5blQHm5%QiNy%El6z?D+Q#HrPanWu{Yg|J2Tr{ zaGzX>f55Zzg4F-O8xrC-%o7q%`3dj>XJ(zoDOBoOdv?xk&Uen7InEo60KxMY*&hB; zMd(k>%s&n=U%)GRKqw-JVrnIrVEEdJmD)(MQK5bCrXVy*dSTG(I@5;sfx2CPJb%J@< zXy2BX#?}NIz4m-ZUWU3j6R^+-Kmt{wZixrDE7UvF>|dQ;1BTl@^wxk3Z& zLD7zRJGHIT-=Vmt`o26mJ=DvefJDDRv4i&9GuMIbA1-LL(Qed#Jm7he(dB-g?;YFj@qZx4~1fy?`=hhnFIVZPY#q;`5G#1fr^1re2QPhv+(RzDz zW$ksyPwQ}=$Bo&s>?UwhcQ~9b$S$~dK2WYGMvSXU7{ZW}VW`|BKhT2F?24E!QBKVP6`LleJ!dPeiL}uss;K@7(c*?hY=sdncx;!ky;G* zKioAlk@#SB4`Pz4HZQoyIcaU@aVFzz@O6~YQl_k2C}$MOq4L;aETk}=dXjJ$`>-jv zYffY3dYGsNNOt5=!ghv?N#*yGxc`_@Smas8`jYGMLxL-Rr(8E+!BT8oj*i(*R$sTE(6)n`8sjA7(VD(v9G2$g=h?A-bw+9=&z^WRC{RC!b z7`{M%-`!mAeJdCjy&=me?|sR{W0{Y7k_ow-Mse1A8nXk@J21mv^j^j+3%N2n;w`|h zUvV2qfCH_CO!tI{aF83p*fj{yDy)XZHD&UpZ@$ zj=ph#)Z_2CoE20AT1!a6Uvr z^Bm#k!z4T(ArXPDb62ACnBe&siOt7J+@*!Dbjm;Em5uLvX&0Q!uLyt4&W_Aw@RLq-q`dQ?OufW4aiFYK|T-(iUAT5gM=%DQz+Av zAQ3T~L$5=&j-vPR`vDRaIf8TOLV(1EQ99yu(nx3^p2OQb2-eQQFr!zLs%)Bm^kbga zUJ_nRfUu%k0(E)3#5HYG5P^T{@b2$nGcN`{L3v7bzw414dJm|~RRQG!S+?bj2_Rd~ zA&rHOawr>XVQHeU`GHK(n}9Zy?f5!RE|du{j(mcx=MbanT#m~o8Qq8ZLk!P$G0cZd z8mw~JBR-vBV$l1`G;<@Md=Fb|iH|1V{lNELhWjvk%*Pofw#0LQ)N#h^!Zb@nQZn!^MagVW;S&n%bShs2FAP2SzfZz=B^&jg|inJ)rVq?c?7u&XTR&O zFzkgB-_Fw+#_PjexJ7UK_}cAByhV!3&iVQn*8a|k9W6JkUA<^lGKldYt!?7^AGC(p zeJg`?@UdNNydPTyRZu#yu>Cg(YnwjE*K4%ii3+@fZdLb+{#tNa*uF)1ehY*ASE!_M zq2+6N`Ggo!x*J7?^ZJSN5;|WFK#f z(dlL5BY3R8iD^g6R!#WkeNvDH{^Qlx-mxNvRX5zg~^HPt^G^~ds8KfX)5J! z?$2Zf{PBmgFZy$^sj_OyMXjwZqbiqFYjs+f7|$H!Iq8J`TuU^!rKrJ9ohS4qT}$i} zPp6!)S*t3pTyKF*Cq~(8#56$i znxX2Js#o5Y^>ULIG)#x9%GRwaauO_e5>@8J+%R32ql%X}C!%>hWXv_UOhEPTKh7W@u9ce_Q-HVdE1! zC^^bMN6A=XXBfp2On|!Iz@s)E-RyqU|DeB)&p@EZhBl6G9X$<6Og59&z{55kzMuW$ zjX&I|#5B0bS z8L;_*^`HHnKfMzI(k>hflnxQvdy-e_K>OWvaN&A7PHTsIiX|s5Nh?OVrYTTIcuD%O zCTq==q$Dk=#I!V3S9AlGqmooMN|Hoq%SGt(<{ix=aQrRVQcDv2A|omsTm^1~lW1O! zlA>vnlnxNuc$l>H{~kF*g=z2a4z%eu`(XI$=jb2Y+!qm)?E8gw`|^Ld^Wljv5e$UB z#V&6;NodbPhG1}#*H#y;%=n_^-Da;wxfRZ>421snad^fwXArNhxCPIR!s%d(musq9 zh1?3_T6%yytJGHqzu{%+B6$<0GzjxG7