Update changes
Dieser Commit ist enthalten in:
@ -9,7 +9,14 @@
|
|||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(pkill:*)",
|
"Bash(pkill:*)",
|
||||||
"Bash(lsof:*)",
|
"Bash(lsof:*)",
|
||||||
"Bash(ls:*)"
|
"Bash(ls:*)",
|
||||||
|
"Bash(Remove-Item -Path \"A:\\\\GiTea\\\\AccountForger\\\\debug_video_issue.py\" -Force)",
|
||||||
|
"Bash(Remove-Item -Path \"A:\\\\GiTea\\\\AccountForger\\\\utils\\\\modal_test.py\" -Force)",
|
||||||
|
"Bash(Remove-Item -Path \"A:\\\\GiTea\\\\AccountForger\\\\browser\\\\instagram_video_bypass.py\" -Force)",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(python -m py_compile:*)",
|
||||||
|
"WebSearch",
|
||||||
|
"Bash(grep:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": [],
|
"ask": [],
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
- **Path**: `A:\GiTea\AccountForger`
|
- **Path**: `A:\GiTea\AccountForger`
|
||||||
- **Files**: 1394 files
|
- **Files**: 1574 files
|
||||||
- **Size**: 257.7 MB
|
- **Size**: 269.4 MB
|
||||||
- **Last Modified**: 2025-11-27 19:34
|
- **Last Modified**: 2025-12-20 22:41
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
@ -78,8 +78,7 @@ controllers/
|
|||||||
│ ├── method_rotation_worker_mixin.py
|
│ ├── method_rotation_worker_mixin.py
|
||||||
│ ├── ok_ru_controller.py
|
│ ├── ok_ru_controller.py
|
||||||
│ ├── rotation_error_handler.py
|
│ ├── rotation_error_handler.py
|
||||||
│ ├── safe_imports.py
|
│ └── safe_imports.py
|
||||||
│ └── mixins
|
|
||||||
database/
|
database/
|
||||||
│ ├── accounts.db
|
│ ├── accounts.db
|
||||||
│ ├── account_repository.py
|
│ ├── account_repository.py
|
||||||
@ -106,7 +105,6 @@ domain/
|
|||||||
│ │ ├── platform.py
|
│ │ ├── platform.py
|
||||||
│ │ ├── rate_limit_policy.py
|
│ │ ├── rate_limit_policy.py
|
||||||
│ │ └── __init__.py
|
│ │ └── __init__.py
|
||||||
│ ├── enums
|
|
||||||
│ ├── repositories/
|
│ ├── repositories/
|
||||||
│ │ ├── analytics_repository.py
|
│ │ ├── analytics_repository.py
|
||||||
│ │ ├── fingerprint_repository.py
|
│ │ ├── fingerprint_repository.py
|
||||||
@ -439,3 +437,6 @@ This project is managed with Claude Project Manager. To work with this project:
|
|||||||
- README updated on 2025-11-27 19:33:36
|
- README updated on 2025-11-27 19:33:36
|
||||||
- README updated on 2025-11-27 19:34:18
|
- README updated on 2025-11-27 19:34:18
|
||||||
- README updated on 2025-11-27 19:41:04
|
- README updated on 2025-11-27 19:41:04
|
||||||
|
- README updated on 2025-12-20 22:29:45
|
||||||
|
- README updated on 2025-12-20 22:41:18
|
||||||
|
- README updated on 2025-12-31 01:00:22
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Cookie Consent Handler für Browser-Sessions
|
Cookie Consent Handler für Browser-Sessions
|
||||||
|
|
||||||
Behandelt Cookie-Consent-Seiten bei der Session-Wiederherstellung
|
Behandelt Cookie-Consent-Seiten bei der Session-Wiederherstellung.
|
||||||
|
Enthält Anti-Detection-Maßnahmen wie Lese-Pausen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
import time
|
||||||
|
import random
|
||||||
|
from typing import Optional, Any
|
||||||
from playwright.sync_api import Page
|
from playwright.sync_api import Page
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -15,20 +18,25 @@ class CookieConsentHandler:
|
|||||||
"""Behandelt Cookie-Consent-Dialoge verschiedener Plattformen"""
|
"""Behandelt Cookie-Consent-Dialoge verschiedener Plattformen"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_instagram_consent(page: Page) -> bool:
|
def handle_instagram_consent(page: Page, human_behavior: Any = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Behandelt Instagram's Cookie-Consent-Seite
|
Behandelt Instagram's Cookie-Consent-Seite mit realistischer Lese-Zeit.
|
||||||
|
|
||||||
|
Diese Methode simuliert menschliches Verhalten durch eine Pause
|
||||||
|
bevor der Cookie-Dialog geklickt wird. Echte Menschen lesen den
|
||||||
|
Cookie-Text bevor sie auf einen Button klicken.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
page: Playwright Page-Objekt
|
page: Playwright Page-Objekt
|
||||||
|
human_behavior: Optional HumanBehavior-Instanz für realistische Delays
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True wenn Consent behandelt wurde, False sonst
|
bool: True wenn Consent behandelt wurde, False sonst
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Warte kurz auf Seitenladung
|
# Warte kurz auf Seitenladung
|
||||||
page.wait_for_load_state('networkidle', timeout=5000)
|
page.wait_for_load_state('networkidle', timeout=5000)
|
||||||
|
|
||||||
# Prüfe ob wir auf der Cookie-Consent-Seite sind
|
# Prüfe ob wir auf der Cookie-Consent-Seite sind
|
||||||
consent_indicators = [
|
consent_indicators = [
|
||||||
# Deutsche Texte
|
# Deutsche Texte
|
||||||
@ -57,7 +65,28 @@ class CookieConsentHandler:
|
|||||||
button = page.locator(button_selector).first
|
button = page.locator(button_selector).first
|
||||||
if button.is_visible():
|
if button.is_visible():
|
||||||
logger.info(f"Found consent decline button: {button_selector}")
|
logger.info(f"Found consent decline button: {button_selector}")
|
||||||
|
|
||||||
|
# ANTI-DETECTION: Realistische Lese-Pause bevor Cookie-Dialog geklickt wird
|
||||||
|
# Simuliert das Lesen der Cookie-Informationen (3-8 Sekunden)
|
||||||
|
if human_behavior and hasattr(human_behavior, 'anti_detection_delay'):
|
||||||
|
logger.debug("Cookie-Banner erkannt - simuliere Lesen...")
|
||||||
|
human_behavior.anti_detection_delay("cookie_reading")
|
||||||
|
else:
|
||||||
|
# Fallback ohne HumanBehavior
|
||||||
|
read_delay = random.uniform(3.0, 8.0)
|
||||||
|
logger.debug(f"Cookie-Banner Lese-Pause: {read_delay:.1f}s")
|
||||||
|
time.sleep(read_delay)
|
||||||
|
|
||||||
|
# Gelegentlich etwas scrollen um "mehr zu lesen" (30% Chance)
|
||||||
|
if random.random() < 0.3:
|
||||||
|
try:
|
||||||
|
page.evaluate("window.scrollBy(0, 50)")
|
||||||
|
time.sleep(random.uniform(0.8, 1.5))
|
||||||
|
page.evaluate("window.scrollBy(0, -50)")
|
||||||
|
time.sleep(random.uniform(0.3, 0.6))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# Verwende robuste Click-Methoden für Cookie-Consent
|
# Verwende robuste Click-Methoden für Cookie-Consent
|
||||||
success = False
|
success = False
|
||||||
try:
|
try:
|
||||||
@ -233,19 +262,21 @@ class CookieConsentHandler:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_and_handle_consent(page: Page, platform: str = "instagram") -> bool:
|
def check_and_handle_consent(page: Page, platform: str = "instagram",
|
||||||
|
human_behavior: Any = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Prüft und behandelt Cookie-Consent für die angegebene Plattform
|
Prüft und behandelt Cookie-Consent für die angegebene Plattform.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
page: Playwright Page-Objekt
|
page: Playwright Page-Objekt
|
||||||
platform: Plattform-Name (default: "instagram")
|
platform: Plattform-Name (default: "instagram")
|
||||||
|
human_behavior: Optional HumanBehavior-Instanz für realistische Delays
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True wenn Consent behandelt wurde, False sonst
|
bool: True wenn Consent behandelt wurde, False sonst
|
||||||
"""
|
"""
|
||||||
if platform.lower() == "instagram":
|
if platform.lower() == "instagram":
|
||||||
return CookieConsentHandler.handle_instagram_consent(page)
|
return CookieConsentHandler.handle_instagram_consent(page, human_behavior)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"No consent handler implemented for platform: {platform}")
|
logger.warning(f"No consent handler implemented for platform: {platform}")
|
||||||
return False
|
return False
|
||||||
@ -1,521 +0,0 @@
|
|||||||
# Instagram Video Bypass - Emergency Deep Level Fixes
|
|
||||||
"""
|
|
||||||
Tiefgreifende Instagram Video Bypass Techniken
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
|
|
||||||
logger = logging.getLogger("instagram_video_bypass")
|
|
||||||
|
|
||||||
class InstagramVideoBypass:
|
|
||||||
"""Deep-level Instagram video bypass techniques"""
|
|
||||||
|
|
||||||
def __init__(self, page: Any):
|
|
||||||
self.page = page
|
|
||||||
|
|
||||||
def apply_emergency_bypass(self) -> None:
|
|
||||||
"""Wendet Emergency Deep-Level Bypass an"""
|
|
||||||
|
|
||||||
# 1. Complete Automation Signature Removal
|
|
||||||
automation_removal_script = """
|
|
||||||
() => {
|
|
||||||
// Remove ALL automation signatures
|
|
||||||
|
|
||||||
// 1. Navigator properties cleanup
|
|
||||||
delete navigator.__webdriver_script_fn;
|
|
||||||
delete navigator.__fxdriver_evaluate;
|
|
||||||
delete navigator.__driver_unwrapped;
|
|
||||||
delete navigator.__webdriver_unwrapped;
|
|
||||||
delete navigator.__driver_evaluate;
|
|
||||||
delete navigator.__selenium_unwrapped;
|
|
||||||
delete navigator.__fxdriver_unwrapped;
|
|
||||||
|
|
||||||
// 2. Window properties cleanup
|
|
||||||
delete window.navigator.webdriver;
|
|
||||||
delete window.webdriver;
|
|
||||||
delete window.chrome.webdriver;
|
|
||||||
delete window.callPhantom;
|
|
||||||
delete window._phantom;
|
|
||||||
delete window.__nightmare;
|
|
||||||
delete window._selenium;
|
|
||||||
delete window.calledSelenium;
|
|
||||||
delete window.$cdc_asdjflasutopfhvcZLmcfl_;
|
|
||||||
delete window.$chrome_asyncScriptInfo;
|
|
||||||
delete window.__webdriver_evaluate;
|
|
||||||
delete window.__selenium_evaluate;
|
|
||||||
delete window.__webdriver_script_function;
|
|
||||||
delete window.__webdriver_script_func;
|
|
||||||
delete window.__webdriver_script_fn;
|
|
||||||
delete window.__fxdriver_evaluate;
|
|
||||||
delete window.__driver_unwrapped;
|
|
||||||
delete window.__webdriver_unwrapped;
|
|
||||||
delete window.__driver_evaluate;
|
|
||||||
delete window.__selenium_unwrapped;
|
|
||||||
delete window.__fxdriver_unwrapped;
|
|
||||||
|
|
||||||
// 3. Document cleanup
|
|
||||||
delete document.__webdriver_script_fn;
|
|
||||||
delete document.__selenium_unwrapped;
|
|
||||||
delete document.__webdriver_unwrapped;
|
|
||||||
delete document.__driver_evaluate;
|
|
||||||
delete document.__webdriver_evaluate;
|
|
||||||
delete document.__fxdriver_evaluate;
|
|
||||||
delete document.__fxdriver_unwrapped;
|
|
||||||
delete document.__driver_unwrapped;
|
|
||||||
|
|
||||||
// 4. Chrome object enhancement
|
|
||||||
if (!window.chrome) {
|
|
||||||
window.chrome = {};
|
|
||||||
}
|
|
||||||
if (!window.chrome.runtime) {
|
|
||||||
window.chrome.runtime = {
|
|
||||||
onConnect: {addListener: function() {}},
|
|
||||||
onMessage: {addListener: function() {}},
|
|
||||||
connect: function() { return {postMessage: function() {}, onMessage: {addListener: function() {}}} }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!window.chrome.app) {
|
|
||||||
window.chrome.app = {
|
|
||||||
isInstalled: false,
|
|
||||||
InstallState: {DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed'},
|
|
||||||
RunningState: {CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running'}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Plugin array enhancement
|
|
||||||
const fakePlugins = [
|
|
||||||
{name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format'},
|
|
||||||
{name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: 'Portable Document Format'},
|
|
||||||
{name: 'Native Client', filename: 'internal-nacl-plugin', description: 'Native Client'}
|
|
||||||
];
|
|
||||||
|
|
||||||
Object.defineProperty(navigator, 'plugins', {
|
|
||||||
get: () => {
|
|
||||||
const pluginArray = [...fakePlugins];
|
|
||||||
pluginArray.length = fakePlugins.length;
|
|
||||||
pluginArray.item = function(index) { return this[index] || null; };
|
|
||||||
pluginArray.namedItem = function(name) { return this.find(p => p.name === name) || null; };
|
|
||||||
pluginArray.refresh = function() {};
|
|
||||||
return pluginArray;
|
|
||||||
},
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 2. Instagram-specific video API spoofing
|
|
||||||
instagram_video_api_script = """
|
|
||||||
() => {
|
|
||||||
// Instagram Video API Deep Spoofing
|
|
||||||
|
|
||||||
// 1. MSE (Media Source Extensions) proper support
|
|
||||||
if (window.MediaSource) {
|
|
||||||
const originalIsTypeSupported = MediaSource.isTypeSupported;
|
|
||||||
MediaSource.isTypeSupported = function(type) {
|
|
||||||
const supportedTypes = [
|
|
||||||
'video/mp4; codecs="avc1.42E01E"',
|
|
||||||
'video/mp4; codecs="avc1.4D401F"',
|
|
||||||
'video/mp4; codecs="avc1.640028"',
|
|
||||||
'video/webm; codecs="vp8"',
|
|
||||||
'video/webm; codecs="vp9"',
|
|
||||||
'audio/mp4; codecs="mp4a.40.2"',
|
|
||||||
'audio/webm; codecs="opus"'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (supportedTypes.includes(type)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return originalIsTypeSupported.call(this, type);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Encrypted Media Extensions deep spoofing
|
|
||||||
if (navigator.requestMediaKeySystemAccess) {
|
|
||||||
const originalRequestAccess = navigator.requestMediaKeySystemAccess;
|
|
||||||
navigator.requestMediaKeySystemAccess = function(keySystem, supportedConfigurations) {
|
|
||||||
if (keySystem === 'com.widevine.alpha') {
|
|
||||||
return Promise.resolve({
|
|
||||||
keySystem: 'com.widevine.alpha',
|
|
||||||
getConfiguration: () => ({
|
|
||||||
initDataTypes: ['cenc', 'keyids', 'webm'],
|
|
||||||
audioCapabilities: [
|
|
||||||
{contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: 'SW_SECURE_CRYPTO'},
|
|
||||||
{contentType: 'audio/webm; codecs="opus"', robustness: 'SW_SECURE_CRYPTO'}
|
|
||||||
],
|
|
||||||
videoCapabilities: [
|
|
||||||
{contentType: 'video/mp4; codecs="avc1.42E01E"', robustness: 'SW_SECURE_DECODE'},
|
|
||||||
{contentType: 'video/mp4; codecs="avc1.4D401F"', robustness: 'SW_SECURE_DECODE'},
|
|
||||||
{contentType: 'video/webm; codecs="vp9"', robustness: 'SW_SECURE_DECODE'}
|
|
||||||
],
|
|
||||||
distinctiveIdentifier: 'optional',
|
|
||||||
persistentState: 'required',
|
|
||||||
sessionTypes: ['temporary', 'persistent-license']
|
|
||||||
}),
|
|
||||||
createMediaKeys: () => Promise.resolve({
|
|
||||||
createSession: (sessionType) => {
|
|
||||||
const session = {
|
|
||||||
sessionId: 'session_' + Math.random().toString(36).substr(2, 9),
|
|
||||||
expiration: NaN,
|
|
||||||
closed: Promise.resolve(),
|
|
||||||
keyStatuses: new Map(),
|
|
||||||
addEventListener: function() {},
|
|
||||||
removeEventListener: function() {},
|
|
||||||
generateRequest: function(initDataType, initData) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.onmessage) {
|
|
||||||
this.onmessage({
|
|
||||||
type: 'message',
|
|
||||||
message: new ArrayBuffer(8)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
load: function() { return Promise.resolve(false); },
|
|
||||||
update: function(response) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.onkeystatuseschange) {
|
|
||||||
this.onkeystatuseschange();
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
close: function() { return Promise.resolve(); },
|
|
||||||
remove: function() { return Promise.resolve(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add event target methods
|
|
||||||
session.dispatchEvent = function() {};
|
|
||||||
|
|
||||||
return session;
|
|
||||||
},
|
|
||||||
setServerCertificate: () => Promise.resolve(true)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return originalRequestAccess.apply(this, arguments);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Hardware media key handling
|
|
||||||
if (navigator.mediaSession) {
|
|
||||||
navigator.mediaSession.setActionHandler = function() {};
|
|
||||||
navigator.mediaSession.playbackState = 'playing';
|
|
||||||
} else {
|
|
||||||
navigator.mediaSession = {
|
|
||||||
metadata: null,
|
|
||||||
playbackState: 'playing',
|
|
||||||
setActionHandler: function() {},
|
|
||||||
setPositionState: function() {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Picture-in-Picture API
|
|
||||||
if (!document.pictureInPictureEnabled) {
|
|
||||||
Object.defineProperty(document, 'pictureInPictureEnabled', {
|
|
||||||
get: () => true,
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Web Audio API enhancement for video
|
|
||||||
if (window.AudioContext || window.webkitAudioContext) {
|
|
||||||
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
||||||
const originalAudioContext = AudioCtx;
|
|
||||||
|
|
||||||
window.AudioContext = function(...args) {
|
|
||||||
const ctx = new originalAudioContext(...args);
|
|
||||||
|
|
||||||
// Override audio context properties for consistency
|
|
||||||
Object.defineProperty(ctx, 'baseLatency', {
|
|
||||||
get: () => 0.01,
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperty(ctx, 'outputLatency', {
|
|
||||||
get: () => 0.02,
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Copy static methods
|
|
||||||
Object.keys(originalAudioContext).forEach(key => {
|
|
||||||
window.AudioContext[key] = originalAudioContext[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 3. Network request interception for video
|
|
||||||
network_interception_script = """
|
|
||||||
() => {
|
|
||||||
// Advanced network request interception for Instagram videos
|
|
||||||
|
|
||||||
const originalFetch = window.fetch;
|
|
||||||
window.fetch = function(input, init) {
|
|
||||||
const url = typeof input === 'string' ? input : input.url;
|
|
||||||
|
|
||||||
// Instagram video CDN requests
|
|
||||||
if (url.includes('instagram.com') || url.includes('fbcdn.net') || url.includes('cdninstagram.com')) {
|
|
||||||
const enhancedInit = {
|
|
||||||
...init,
|
|
||||||
headers: {
|
|
||||||
...init?.headers,
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Accept-Encoding': 'identity;q=1, *;q=0',
|
|
||||||
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'DNT': '1',
|
|
||||||
'Origin': 'https://www.instagram.com',
|
|
||||||
'Pragma': 'no-cache',
|
|
||||||
'Referer': 'https://www.instagram.com/',
|
|
||||||
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
||||||
'Sec-Ch-Ua-Mobile': '?0',
|
|
||||||
'Sec-Ch-Ua-Platform': '"Windows"',
|
|
||||||
'Sec-Fetch-Dest': 'video',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
'Sec-Fetch-Site': 'cross-site',
|
|
||||||
'User-Agent': navigator.userAgent,
|
|
||||||
'X-Asbd-Id': '129477',
|
|
||||||
'X-Fb-Lsd': document.querySelector('[name="fb_dtsg"]')?.value || '',
|
|
||||||
'X-Instagram-Ajax': '1'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove problematic headers that might indicate automation
|
|
||||||
delete enhancedInit.headers['sec-ch-ua-arch'];
|
|
||||||
delete enhancedInit.headers['sec-ch-ua-bitness'];
|
|
||||||
delete enhancedInit.headers['sec-ch-ua-full-version'];
|
|
||||||
delete enhancedInit.headers['sec-ch-ua-full-version-list'];
|
|
||||||
delete enhancedInit.headers['sec-ch-ua-model'];
|
|
||||||
delete enhancedInit.headers['sec-ch-ua-wow64'];
|
|
||||||
|
|
||||||
return originalFetch.call(this, input, enhancedInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalFetch.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// XMLHttpRequest interception
|
|
||||||
const originalXHROpen = XMLHttpRequest.prototype.open;
|
|
||||||
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
|
|
||||||
this._url = url;
|
|
||||||
return originalXHROpen.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
const originalXHRSend = XMLHttpRequest.prototype.send;
|
|
||||||
XMLHttpRequest.prototype.send = function(body) {
|
|
||||||
if (this._url && (this._url.includes('instagram.com') || this._url.includes('fbcdn.net'))) {
|
|
||||||
// Add video-specific headers
|
|
||||||
this.setRequestHeader('Accept', '*/*');
|
|
||||||
this.setRequestHeader('Accept-Language', 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7');
|
|
||||||
this.setRequestHeader('Cache-Control', 'no-cache');
|
|
||||||
this.setRequestHeader('Pragma', 'no-cache');
|
|
||||||
this.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
||||||
}
|
|
||||||
return originalXHRSend.apply(this, arguments);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 4. Timing and behavior normalization
|
|
||||||
timing_script = """
|
|
||||||
() => {
|
|
||||||
// Normalize timing functions to avoid detection
|
|
||||||
|
|
||||||
// Performance timing spoofing
|
|
||||||
if (window.performance && window.performance.timing) {
|
|
||||||
const timing = performance.timing;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
Object.defineProperty(performance.timing, 'navigationStart', {
|
|
||||||
get: () => now - Math.floor(Math.random() * 1000) - 1000,
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperty(performance.timing, 'loadEventEnd', {
|
|
||||||
get: () => now - Math.floor(Math.random() * 500),
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date/Time consistency
|
|
||||||
const originalDate = Date;
|
|
||||||
const startTime = originalDate.now();
|
|
||||||
|
|
||||||
Date.now = function() {
|
|
||||||
return startTime + (originalDate.now() - startTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove timing inconsistencies that indicate automation
|
|
||||||
const originalSetTimeout = window.setTimeout;
|
|
||||||
window.setTimeout = function(fn, delay, ...args) {
|
|
||||||
// Add slight randomization to timing
|
|
||||||
const randomDelay = delay + Math.floor(Math.random() * 10) - 5;
|
|
||||||
return originalSetTimeout.call(this, fn, Math.max(0, randomDelay), ...args);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Apply all scripts in sequence
|
|
||||||
scripts = [
|
|
||||||
automation_removal_script,
|
|
||||||
instagram_video_api_script,
|
|
||||||
network_interception_script,
|
|
||||||
timing_script
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, script in enumerate(scripts):
|
|
||||||
try:
|
|
||||||
self.page.add_init_script(script)
|
|
||||||
logger.info(f"Applied emergency bypass script {i+1}/4")
|
|
||||||
time.sleep(0.1) # Small delay between scripts
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to apply emergency bypass script {i+1}: {e}")
|
|
||||||
|
|
||||||
logger.info("Emergency Instagram video bypass applied")
|
|
||||||
|
|
||||||
def inject_video_session_data(self) -> None:
|
|
||||||
"""Injiziert realistische Video-Session-Daten"""
|
|
||||||
|
|
||||||
session_script = """
|
|
||||||
() => {
|
|
||||||
// Inject realistic video session data
|
|
||||||
|
|
||||||
// 1. Video viewing history
|
|
||||||
localStorage.setItem('instagram_video_history', JSON.stringify({
|
|
||||||
last_viewed: Date.now() - Math.floor(Math.random() * 86400000),
|
|
||||||
view_count: Math.floor(Math.random() * 50) + 10,
|
|
||||||
preferences: {
|
|
||||||
autoplay: true,
|
|
||||||
quality: 'auto',
|
|
||||||
captions: false
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 2. Media session state
|
|
||||||
localStorage.setItem('media_session_state', JSON.stringify({
|
|
||||||
hasInteracted: true,
|
|
||||||
lastInteraction: Date.now() - Math.floor(Math.random() * 3600000),
|
|
||||||
playbackRate: 1,
|
|
||||||
volume: 0.8
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 3. DRM license cache simulation
|
|
||||||
sessionStorage.setItem('drm_licenses', JSON.stringify({
|
|
||||||
widevine: {
|
|
||||||
version: '4.10.2449.0',
|
|
||||||
lastUpdate: Date.now() - Math.floor(Math.random() * 604800000),
|
|
||||||
status: 'valid'
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 4. Instagram session tokens
|
|
||||||
const csrfToken = document.querySelector('[name="csrfmiddlewaretoken"]')?.value ||
|
|
||||||
document.querySelector('meta[name="csrf-token"]')?.content ||
|
|
||||||
'missing';
|
|
||||||
|
|
||||||
if (csrfToken !== 'missing') {
|
|
||||||
sessionStorage.setItem('csrf_token', csrfToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.page.evaluate(session_script)
|
|
||||||
logger.info("Video session data injected successfully")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to inject video session data: {e}")
|
|
||||||
|
|
||||||
def simulate_user_interaction(self) -> None:
|
|
||||||
"""Simuliert authentische Benutzerinteraktion"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Random mouse movements
|
|
||||||
for _ in range(3):
|
|
||||||
x = random.randint(100, 800)
|
|
||||||
y = random.randint(100, 600)
|
|
||||||
self.page.mouse.move(x, y)
|
|
||||||
time.sleep(random.uniform(0.1, 0.3))
|
|
||||||
|
|
||||||
# Random scroll
|
|
||||||
self.page.mouse.wheel(0, random.randint(-200, 200))
|
|
||||||
time.sleep(random.uniform(0.2, 0.5))
|
|
||||||
|
|
||||||
# Click somewhere safe (not on video)
|
|
||||||
self.page.click('body', position={'x': random.randint(50, 100), 'y': random.randint(50, 100)})
|
|
||||||
time.sleep(random.uniform(0.3, 0.7))
|
|
||||||
|
|
||||||
logger.info("User interaction simulation completed")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to simulate user interaction: {e}")
|
|
||||||
|
|
||||||
def check_video_errors(self) -> Dict[str, Any]:
|
|
||||||
"""Überprüft Video-Fehler und DRM-Status"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.page.evaluate("""
|
|
||||||
() => {
|
|
||||||
const errors = [];
|
|
||||||
const diagnostics = {
|
|
||||||
drm_support: false,
|
|
||||||
media_source: false,
|
|
||||||
codec_support: {},
|
|
||||||
video_elements: 0,
|
|
||||||
error_messages: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check for DRM support
|
|
||||||
if (navigator.requestMediaKeySystemAccess) {
|
|
||||||
diagnostics.drm_support = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Media Source Extensions
|
|
||||||
if (window.MediaSource) {
|
|
||||||
diagnostics.media_source = true;
|
|
||||||
diagnostics.codec_support = {
|
|
||||||
h264: MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E"'),
|
|
||||||
vp9: MediaSource.isTypeSupported('video/webm; codecs="vp9"'),
|
|
||||||
aac: MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count video elements
|
|
||||||
diagnostics.video_elements = document.querySelectorAll('video').length;
|
|
||||||
|
|
||||||
// Look for error messages
|
|
||||||
const errorElements = document.querySelectorAll('[class*="error"], [class*="fail"]');
|
|
||||||
errorElements.forEach(el => {
|
|
||||||
if (el.textContent.includes('Video') || el.textContent.includes('video')) {
|
|
||||||
diagnostics.error_messages.push(el.textContent.trim());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Console errors
|
|
||||||
const consoleErrors = [];
|
|
||||||
const originalConsoleError = console.error;
|
|
||||||
console.error = function(...args) {
|
|
||||||
consoleErrors.push(args.join(' '));
|
|
||||||
originalConsoleError.apply(console, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
diagnostics,
|
|
||||||
console_errors: consoleErrors,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
logger.info(f"Video diagnostics: {result}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Video error check failed: {e}")
|
|
||||||
return {}
|
|
||||||
@ -17,6 +17,7 @@ from infrastructure.services.browser_protection_service import BrowserProtection
|
|||||||
# Konfiguriere Logger
|
# Konfiguriere Logger
|
||||||
logger = logging.getLogger("playwright_manager")
|
logger = logging.getLogger("playwright_manager")
|
||||||
|
|
||||||
|
|
||||||
class PlaywrightManager:
|
class PlaywrightManager:
|
||||||
"""
|
"""
|
||||||
Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen.
|
Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen.
|
||||||
@ -25,6 +26,9 @@ class PlaywrightManager:
|
|||||||
# Klassen-Variable: Zählt aktive Browser-Instanzen (Feature 5: Browser-Instanz Schutz)
|
# Klassen-Variable: Zählt aktive Browser-Instanzen (Feature 5: Browser-Instanz Schutz)
|
||||||
_active_count = 0
|
_active_count = 0
|
||||||
|
|
||||||
|
# Klassen-Variable: Referenz auf die zuletzt gestartete Instanz (für Cleanup)
|
||||||
|
_current_instance: "PlaywrightManager" = None
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
headless: bool = False,
|
headless: bool = False,
|
||||||
proxy: Optional[Dict[str, str]] = None,
|
proxy: Optional[Dict[str, str]] = None,
|
||||||
@ -149,24 +153,30 @@ class PlaywrightManager:
|
|||||||
|
|
||||||
# Feature 5: Browser-Instanz Schutz - Nur eine Instanz gleichzeitig
|
# Feature 5: Browser-Instanz Schutz - Nur eine Instanz gleichzeitig
|
||||||
if PlaywrightManager._active_count >= 1:
|
if PlaywrightManager._active_count >= 1:
|
||||||
# Safety-Check: Prüfe ob Counter "hängt" (Absturz-Schutz)
|
# Es gibt noch einen alten Browser - prüfe ob er geschlossen werden kann
|
||||||
# Wenn ProcessGuard NICHT locked ist, aber Counter > 0, dann ist Counter "tot"
|
old_instance = PlaywrightManager._current_instance
|
||||||
from utils.process_guard import get_guard
|
|
||||||
guard = get_guard()
|
|
||||||
|
|
||||||
if not guard.is_locked():
|
if old_instance is not None and old_instance is not self:
|
||||||
# Counter hängt! Process Guard ist frei, aber Counter sagt Browser läuft
|
# Alte Instanz existiert und ist nicht diese - schließen
|
||||||
|
logger.warning(
|
||||||
|
f"⚠️ BROWSER-CLEANUP: Alte Browser-Instanz wird geschlossen "
|
||||||
|
f"(Counter war {PlaywrightManager._active_count})"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
old_instance.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Fehler beim Schließen alter Browser-Instanz: {e}")
|
||||||
|
|
||||||
|
# Counter und Referenz zurücksetzen
|
||||||
|
PlaywrightManager._active_count = 0
|
||||||
|
PlaywrightManager._current_instance = None
|
||||||
|
else:
|
||||||
|
# Keine alte Instanz gefunden, aber Counter > 0 - Safety Reset
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"⚠️ SAFETY-RESET: _active_count war {PlaywrightManager._active_count}, "
|
f"⚠️ SAFETY-RESET: _active_count war {PlaywrightManager._active_count}, "
|
||||||
f"aber ProcessGuard ist nicht locked. Counter wird zurückgesetzt."
|
f"aber keine alte Instanz gefunden. Counter wird zurückgesetzt."
|
||||||
)
|
)
|
||||||
PlaywrightManager._active_count = 0
|
PlaywrightManager._active_count = 0
|
||||||
else:
|
|
||||||
# Guard ist locked UND Counter ist > 0 → echte parallele Instanz
|
|
||||||
raise RuntimeError(
|
|
||||||
"Browser bereits aktiv. Nur eine Browser-Instanz gleichzeitig erlaubt. "
|
|
||||||
"Beenden Sie den aktuellen Prozess."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.playwright = sync_playwright().start()
|
self.playwright = sync_playwright().start()
|
||||||
@ -281,8 +291,9 @@ class PlaywrightManager:
|
|||||||
self.browser.on("disconnected", self._on_browser_disconnected)
|
self.browser.on("disconnected", self._on_browser_disconnected)
|
||||||
logger.debug("Browser-Disconnect-Handler registriert")
|
logger.debug("Browser-Disconnect-Handler registriert")
|
||||||
|
|
||||||
# Feature 5: Browser-Instanz Counter erhöhen
|
# Feature 5: Browser-Instanz Counter erhöhen und aktuelle Instanz speichern
|
||||||
PlaywrightManager._active_count += 1
|
PlaywrightManager._active_count += 1
|
||||||
|
PlaywrightManager._current_instance = self
|
||||||
logger.info(f"Browser gestartet (aktive Instanzen: {PlaywrightManager._active_count})")
|
logger.info(f"Browser gestartet (aktive Instanzen: {PlaywrightManager._active_count})")
|
||||||
|
|
||||||
return self.page
|
return self.page
|
||||||
@ -524,15 +535,22 @@ class PlaywrightManager:
|
|||||||
key = f"fill_{selector}"
|
key = f"fill_{selector}"
|
||||||
return self._retry_action(key, lambda: self.fill_form_field(selector, value, timeout))
|
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:
|
def click_element(self, selector: str, force: bool = False, timeout: int = 5000,
|
||||||
|
use_bezier_mouse: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Klickt auf ein Element mit Anti-Bot-Bypass-Strategien.
|
Klickt auf ein Element mit Anti-Bot-Bypass-Strategien.
|
||||||
|
|
||||||
|
Diese Methode simuliert menschliches Klick-Verhalten durch:
|
||||||
|
- Bézier-Kurven-Mausbewegungen zum Element
|
||||||
|
- Natürliches Scrolling (auch bei sichtbaren Elementen)
|
||||||
|
- Variable Verzögerungen
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
selector: Selektor für das Element
|
selector: Selektor für das Element
|
||||||
force: Force-Click verwenden
|
force: Force-Click verwenden
|
||||||
timeout: Timeout in Millisekunden
|
timeout: Timeout in Millisekunden
|
||||||
|
use_bezier_mouse: Ob Bézier-Mausbewegung vor dem Klick verwendet werden soll
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True bei Erfolg, False bei Fehler
|
bool: True bei Erfolg, False bei Fehler
|
||||||
"""
|
"""
|
||||||
@ -541,24 +559,164 @@ class PlaywrightManager:
|
|||||||
element = self.wait_for_selector(selector, timeout)
|
element = self.wait_for_selector(selector, timeout)
|
||||||
if not element:
|
if not element:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Scroll zum Element
|
# Verbessertes Scrolling mit menschlichem Verhalten
|
||||||
self.page.evaluate("element => element.scrollIntoView({ behavior: 'smooth', block: 'center' })", element)
|
self._human_scroll_to_element(element)
|
||||||
time.sleep(random.uniform(0.3, 0.7))
|
|
||||||
|
# Bézier-Mausbewegung zum Element (Anti-Detection)
|
||||||
# Menschenähnliches Verhalten - leichte Verzögerung vor dem Klick
|
if use_bezier_mouse:
|
||||||
time.sleep(random.uniform(0.2, 0.5))
|
self._move_mouse_to_element_bezier(element)
|
||||||
|
|
||||||
# Element klicken
|
# Menschenähnliches Verhalten - variable Verzögerung vor dem Klick
|
||||||
element.click(force=force, delay=random.uniform(20, 100))
|
time.sleep(random.uniform(0.15, 0.4))
|
||||||
|
|
||||||
|
# Element klicken mit variablem Delay
|
||||||
|
element.click(force=force, delay=random.uniform(30, 120))
|
||||||
|
|
||||||
logger.info(f"Element geklickt: {selector}")
|
logger.info(f"Element geklickt: {selector}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Klicken auf {selector}: {e}")
|
logger.error(f"Fehler beim Klicken auf {selector}: {e}")
|
||||||
# Bei Fehlern verwende robuste Click-Strategien
|
# Bei Fehlern verwende robuste Click-Strategien
|
||||||
return self.robust_click(selector, timeout)
|
return self.robust_click(selector, timeout)
|
||||||
|
|
||||||
|
def _human_scroll_to_element(self, element) -> None:
|
||||||
|
"""
|
||||||
|
Scrollt zum Element mit menschlichem Verhalten.
|
||||||
|
|
||||||
|
Simuliert natürliches Scrollverhalten:
|
||||||
|
- Gelegentlich erst in falsche Richtung scrollen
|
||||||
|
- Auch bei sichtbaren Elementen leicht scrollen
|
||||||
|
- Variable Scroll-Geschwindigkeit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: Das Playwright ElementHandle
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Gelegentlich erst in "falsche" Richtung scrollen (15% Chance)
|
||||||
|
if random.random() < 0.15:
|
||||||
|
wrong_direction = random.choice(['up', 'down'])
|
||||||
|
scroll_amount = random.randint(50, 150)
|
||||||
|
if wrong_direction == 'up':
|
||||||
|
self.page.evaluate(f"window.scrollBy(0, -{scroll_amount})")
|
||||||
|
else:
|
||||||
|
self.page.evaluate(f"window.scrollBy(0, {scroll_amount})")
|
||||||
|
time.sleep(random.uniform(0.2, 0.5))
|
||||||
|
logger.debug(f"Korrektur-Scroll: erst {wrong_direction}")
|
||||||
|
|
||||||
|
# Scroll-Verhalten zufällig wählen
|
||||||
|
scroll_behavior = random.choice(['smooth', 'smooth', 'auto']) # 66% smooth
|
||||||
|
scroll_block = random.choice(['center', 'center', 'nearest']) # 66% center
|
||||||
|
|
||||||
|
# Hauptscroll zum Element
|
||||||
|
scroll_script = f"""
|
||||||
|
(element) => {{
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const isFullyVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
|
||||||
|
|
||||||
|
// Auch wenn sichtbar, leicht scrollen für natürliches Verhalten (60% Chance)
|
||||||
|
if (isFullyVisible && Math.random() < 0.6) {{
|
||||||
|
const smallScroll = Math.floor(Math.random() * 60) - 30;
|
||||||
|
window.scrollBy(0, smallScroll);
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Zum Element scrollen
|
||||||
|
element.scrollIntoView({{ behavior: '{scroll_behavior}', block: '{scroll_block}' }});
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
self.page.evaluate(scroll_script, element)
|
||||||
|
|
||||||
|
# Variable Wartezeit nach Scroll
|
||||||
|
time.sleep(random.uniform(0.4, 1.0))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Fehler beim Human-Scroll: {e}")
|
||||||
|
# Fallback: einfaches Scroll
|
||||||
|
try:
|
||||||
|
self.page.evaluate("element => element.scrollIntoView({ behavior: 'smooth', block: 'center' })", element)
|
||||||
|
time.sleep(random.uniform(0.3, 0.7))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _move_mouse_to_element_bezier(self, element) -> None:
|
||||||
|
"""
|
||||||
|
Bewegt die Maus mit Bézier-Kurve zum Element.
|
||||||
|
|
||||||
|
Simuliert realistische menschliche Mausbewegungen:
|
||||||
|
- Kubische Bézier-Kurve mit 2 Kontrollpunkten
|
||||||
|
- Variable Geschwindigkeit (langsamer am Anfang/Ende)
|
||||||
|
- Leichtes Zittern für Natürlichkeit
|
||||||
|
- Gelegentliche Mikro-Pausen
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: Das Playwright ElementHandle
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Aktuelle Mausposition oder zufälliger Startpunkt
|
||||||
|
viewport = self.page.viewport_size
|
||||||
|
if viewport:
|
||||||
|
current_x = random.randint(100, viewport['width'] - 100)
|
||||||
|
current_y = random.randint(100, viewport['height'] - 100)
|
||||||
|
else:
|
||||||
|
current_x = random.randint(100, 1820)
|
||||||
|
current_y = random.randint(100, 980)
|
||||||
|
|
||||||
|
# Zielpunkt: Mitte des Elements mit leichter Variation
|
||||||
|
box = element.bounding_box()
|
||||||
|
if not box:
|
||||||
|
logger.debug("Kein Bounding-Box für Element, überspringe Bézier-Bewegung")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Zielposition mit leichter Variation (nicht exakt Mitte)
|
||||||
|
target_x = box['x'] + box['width'] / 2 + random.uniform(-5, 5)
|
||||||
|
target_y = box['y'] + box['height'] / 2 + random.uniform(-5, 5)
|
||||||
|
|
||||||
|
# Entfernung berechnen
|
||||||
|
distance = ((target_x - current_x)**2 + (target_y - current_y)**2)**0.5
|
||||||
|
|
||||||
|
# Anzahl der Schritte basierend auf Entfernung (mehr Schritte = flüssiger)
|
||||||
|
steps = max(25, int(distance / 8))
|
||||||
|
|
||||||
|
# Kontrollpunkte für kubische Bézier-Kurve
|
||||||
|
ctrl_variance = distance / 4
|
||||||
|
ctrl1_x = current_x + (target_x - current_x) * random.uniform(0.2, 0.4) + random.uniform(-ctrl_variance, ctrl_variance)
|
||||||
|
ctrl1_y = current_y + (target_y - current_y) * random.uniform(0.1, 0.3) + random.uniform(-ctrl_variance, ctrl_variance)
|
||||||
|
ctrl2_x = current_x + (target_x - current_x) * random.uniform(0.6, 0.8) + random.uniform(-ctrl_variance, ctrl_variance)
|
||||||
|
ctrl2_y = current_y + (target_y - current_y) * random.uniform(0.7, 0.9) + random.uniform(-ctrl_variance, ctrl_variance)
|
||||||
|
|
||||||
|
# Mausbewegung ausführen
|
||||||
|
for i in range(steps + 1):
|
||||||
|
t = i / steps
|
||||||
|
|
||||||
|
# Kubische Bézier-Formel: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
|
||||||
|
x = (1-t)**3 * current_x + 3*(1-t)**2*t * ctrl1_x + 3*(1-t)*t**2 * ctrl2_x + t**3 * target_x
|
||||||
|
y = (1-t)**3 * current_y + 3*(1-t)**2*t * ctrl1_y + 3*(1-t)*t**2 * ctrl2_y + t**3 * target_y
|
||||||
|
|
||||||
|
# Leichtes Zittern hinzufügen für Realismus
|
||||||
|
x += random.uniform(-1, 1)
|
||||||
|
y += random.uniform(-1, 1)
|
||||||
|
|
||||||
|
# Maus bewegen
|
||||||
|
self.page.mouse.move(x, y)
|
||||||
|
|
||||||
|
# Variable Geschwindigkeit: langsamer am Anfang/Ende, schneller in der Mitte
|
||||||
|
if i < steps * 0.15 or i > steps * 0.85:
|
||||||
|
# Langsamer am Anfang und Ende (Beschleunigung/Abbremsen)
|
||||||
|
time.sleep(random.uniform(0.008, 0.018))
|
||||||
|
else:
|
||||||
|
# Schneller in der Mitte
|
||||||
|
time.sleep(random.uniform(0.003, 0.008))
|
||||||
|
|
||||||
|
# Gelegentliche Mikro-Pause (5% Chance) - simuliert Zögern
|
||||||
|
if random.random() < 0.05:
|
||||||
|
time.sleep(random.uniform(0.02, 0.08))
|
||||||
|
|
||||||
|
logger.debug(f"Bézier-Mausbewegung: ({current_x:.0f},{current_y:.0f}) -> ({target_x:.0f},{target_y:.0f})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Bézier-Mausbewegung fehlgeschlagen: {e}")
|
||||||
|
# Kein Fallback nötig - Klick funktioniert auch ohne Mausbewegung
|
||||||
|
|
||||||
def robust_click(self, selector: str, timeout: int = 5000) -> bool:
|
def robust_click(self, selector: str, timeout: int = 5000) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -1017,6 +1175,10 @@ class PlaywrightManager:
|
|||||||
else:
|
else:
|
||||||
logger.warning("Browser disconnected aber Counter war bereits 0")
|
logger.warning("Browser disconnected aber Counter war bereits 0")
|
||||||
|
|
||||||
|
# Aktuelle Instanz-Referenz löschen wenn diese Instanz die aktuelle war
|
||||||
|
if PlaywrightManager._current_instance is self:
|
||||||
|
PlaywrightManager._current_instance = None
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Schließt den Browser und gibt Ressourcen frei."""
|
"""Schließt den Browser und gibt Ressourcen frei."""
|
||||||
try:
|
try:
|
||||||
@ -1072,6 +1234,10 @@ class PlaywrightManager:
|
|||||||
else:
|
else:
|
||||||
logger.debug("Counter wurde bereits durch disconnected-Event dekrementiert")
|
logger.debug("Counter wurde bereits durch disconnected-Event dekrementiert")
|
||||||
|
|
||||||
|
# Aktuelle Instanz-Referenz löschen wenn diese Instanz die aktuelle war
|
||||||
|
if PlaywrightManager._current_instance is self:
|
||||||
|
PlaywrightManager._current_instance = None
|
||||||
|
|
||||||
logger.info("Browser-Sitzung erfolgreich geschlossen")
|
logger.info("Browser-Sitzung erfolgreich geschlossen")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -1,154 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Quick check script to verify method rotation system status.
|
|
||||||
Run this to ensure everything is working before starting main.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Add project root to path
|
|
||||||
project_root = Path(__file__).parent
|
|
||||||
sys.path.insert(0, str(project_root))
|
|
||||||
|
|
||||||
def check_imports():
|
|
||||||
"""Check if all rotation system imports work"""
|
|
||||||
print("🔍 Checking imports...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from domain.entities.method_rotation import MethodStrategy, RotationSession
|
|
||||||
print("✅ Domain entities: OK")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Domain entities: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
from application.use_cases.method_rotation_use_case import MethodRotationUseCase
|
|
||||||
print("✅ Use cases: OK")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Use cases: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
from controllers.platform_controllers.method_rotation_mixin import MethodRotationMixin
|
|
||||||
print("✅ Controller mixin: OK")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Controller mixin: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def check_database():
|
|
||||||
"""Check database and tables"""
|
|
||||||
print("\n🗄️ Checking database...")
|
|
||||||
|
|
||||||
db_path = project_root / "database" / "accounts.db"
|
|
||||||
if not db_path.exists():
|
|
||||||
print(f"❌ Database not found: {db_path}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"✅ Database found: {db_path}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
import sqlite3
|
|
||||||
conn = sqlite3.connect(str(db_path))
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# Check for rotation tables
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT name FROM sqlite_master
|
|
||||||
WHERE type='table' AND (
|
|
||||||
name = 'method_strategies' OR
|
|
||||||
name = 'rotation_sessions' OR
|
|
||||||
name = 'platform_method_states'
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
tables = [row[0] for row in cursor.fetchall()]
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if len(tables) >= 3:
|
|
||||||
print(f"✅ Rotation tables found: {tables}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"⚠️ Missing rotation tables. Found: {tables}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Database check failed: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def check_config():
|
|
||||||
"""Check configuration files"""
|
|
||||||
print("\n⚙️ Checking configuration...")
|
|
||||||
|
|
||||||
config_path = project_root / "config" / "method_rotation_config.json"
|
|
||||||
if config_path.exists():
|
|
||||||
print("✅ Rotation config found")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("⚠️ Rotation config not found (will use defaults)")
|
|
||||||
return True # Not critical
|
|
||||||
|
|
||||||
def check_controllers():
|
|
||||||
"""Check if controllers can be imported"""
|
|
||||||
print("\n🎮 Checking controllers...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
from controllers.platform_controllers.base_controller import BasePlatformController
|
|
||||||
print("✅ Base controller: OK")
|
|
||||||
|
|
||||||
from controllers.platform_controllers.instagram_controller import InstagramController
|
|
||||||
print("✅ Instagram controller: OK")
|
|
||||||
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Controller check failed: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main check function"""
|
|
||||||
print("🔧 Method Rotation System - Status Check")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
checks = [
|
|
||||||
("Imports", check_imports),
|
|
||||||
("Database", check_database),
|
|
||||||
("Config", check_config),
|
|
||||||
("Controllers", check_controllers)
|
|
||||||
]
|
|
||||||
|
|
||||||
all_good = True
|
|
||||||
for name, check_func in checks:
|
|
||||||
try:
|
|
||||||
result = check_func()
|
|
||||||
if not result:
|
|
||||||
all_good = False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {name} check crashed: {e}")
|
|
||||||
all_good = False
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
if all_good:
|
|
||||||
print("✅ Method rotation system is ready!")
|
|
||||||
print("🚀 You can safely start main.py")
|
|
||||||
print("\n💡 Expected behavior:")
|
|
||||||
print(" - Account creation works as before")
|
|
||||||
print(" - Additional rotation logs will appear")
|
|
||||||
print(" - Automatic method switching on failures")
|
|
||||||
print(" - Graceful fallback if any issues occur")
|
|
||||||
else:
|
|
||||||
print("⚠️ Some issues detected, but main.py should still work")
|
|
||||||
print("🔄 Rotation system will fall back to original behavior")
|
|
||||||
print("\n🛠️ To fix issues:")
|
|
||||||
print(" 1. Run: python3 run_migration.py")
|
|
||||||
print(" 2. Check file permissions")
|
|
||||||
print(" 3. Restart main.py")
|
|
||||||
|
|
||||||
print("\n📝 To test rotation manually:")
|
|
||||||
print(" - Create an account on any platform")
|
|
||||||
print(" - Check logs for rotation messages")
|
|
||||||
print(" - Simulate failures to see rotation in action")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
"""
|
|
||||||
Einfacher Switch zwischen alter und neuer Implementation für schnelles Rollback
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ImplementationSwitch:
|
|
||||||
"""Einfacher Switch zwischen alter und neuer Implementation"""
|
|
||||||
|
|
||||||
# Direkt aktivieren im Testbetrieb
|
|
||||||
USE_REFACTORED_CODE = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def rollback_to_legacy(cls):
|
|
||||||
"""Schneller Rollback wenn nötig"""
|
|
||||||
cls.USE_REFACTORED_CODE = False
|
|
||||||
print("WARNUNG: Rollback zu Legacy-Implementation aktiviert!")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def use_refactored_code(cls):
|
|
||||||
"""Aktiviert die refaktorierte Implementation"""
|
|
||||||
cls.USE_REFACTORED_CODE = True
|
|
||||||
print("INFO: Refaktorierte Implementation aktiviert")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_refactored_active(cls) -> bool:
|
|
||||||
"""Prüft ob refaktorierte Implementation aktiv ist"""
|
|
||||||
return cls.USE_REFACTORED_CODE
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_status(cls) -> str:
|
|
||||||
"""Gibt den aktuellen Status zurück"""
|
|
||||||
if cls.USE_REFACTORED_CODE:
|
|
||||||
return "Refaktorierte Implementation (NEU)"
|
|
||||||
else:
|
|
||||||
return "Legacy Implementation (ALT)"
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"key": "",
|
|
||||||
"status": "inactive",
|
|
||||||
"hardware_id": "",
|
|
||||||
"activation_date": null,
|
|
||||||
"expiry_date": null,
|
|
||||||
"features": [],
|
|
||||||
"last_check": null,
|
|
||||||
"session_ip_mode": "auto",
|
|
||||||
"ip_fallback": "0.0.0.0"
|
|
||||||
}
|
|
||||||
@ -255,21 +255,7 @@ class BasePlatformController(QObject):
|
|||||||
|
|
||||||
# Normale Verarbeitung
|
# Normale Verarbeitung
|
||||||
self.handle_account_created(result)
|
self.handle_account_created(result)
|
||||||
|
|
||||||
def stop_account_creation(self):
|
|
||||||
"""Stoppt die 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(f"{self.platform_name}-Account-Erstellung wurde abgebrochen")
|
|
||||||
generator_tab.set_running(False)
|
|
||||||
generator_tab.set_progress(0)
|
|
||||||
|
|
||||||
# Forge-Dialog schließen falls vorhanden
|
|
||||||
if hasattr(self, 'forge_dialog') and self.forge_dialog:
|
|
||||||
self.forge_dialog.close()
|
|
||||||
self.forge_dialog = None
|
|
||||||
|
|
||||||
def handle_account_created(self, result):
|
def handle_account_created(self, result):
|
||||||
"""
|
"""
|
||||||
Verarbeitet erfolgreich erstellte Accounts.
|
Verarbeitet erfolgreich erstellte Accounts.
|
||||||
@ -287,19 +273,14 @@ class BasePlatformController(QObject):
|
|||||||
|
|
||||||
Diese Methode kann von Unterklassen überschrieben werden für spezielle Anforderungen.
|
Diese Methode kann von Unterklassen überschrieben werden für spezielle Anforderungen.
|
||||||
Sie stellt sicher dass:
|
Sie stellt sicher dass:
|
||||||
1. Der Process Guard freigegeben wird
|
1. Der Worker-Thread gestoppt wird (Worker gibt Guard frei mit release())
|
||||||
2. Der Worker-Thread gestoppt wird
|
2. Die UI zurückgesetzt wird
|
||||||
3. Die UI zurückgesetzt wird
|
3. Dialoge geschlossen werden
|
||||||
4. Dialoge geschlossen werden
|
|
||||||
"""
|
|
||||||
# Guard-Freigabe (wichtig: VOR Worker-Stop)
|
|
||||||
from utils.process_guard import get_guard
|
|
||||||
guard = get_guard()
|
|
||||||
if guard.is_locked():
|
|
||||||
guard.end(success=False)
|
|
||||||
self.logger.info("Guard freigegeben bei Stop (BaseController)")
|
|
||||||
|
|
||||||
# Worker stoppen falls vorhanden
|
WICHTIG: Guard-Freigabe erfolgt im Worker.stop() mit release().
|
||||||
|
User-Abbruch zählt NICHT als Failure.
|
||||||
|
"""
|
||||||
|
# Worker stoppen falls vorhanden (Worker.stop() gibt Guard frei)
|
||||||
if self.worker_thread and self.worker_thread.isRunning():
|
if self.worker_thread and self.worker_thread.isRunning():
|
||||||
self.worker_thread.stop()
|
self.worker_thread.stop()
|
||||||
generator_tab = self.get_generator_tab()
|
generator_tab = self.get_generator_tab()
|
||||||
|
|||||||
@ -8,19 +8,23 @@ from typing import Dict, Any, Optional
|
|||||||
from utils.text_similarity import TextSimilarity
|
from utils.text_similarity import TextSimilarity
|
||||||
from domain.value_objects.browser_protection_style import BrowserProtectionStyle, ProtectionLevel
|
from domain.value_objects.browser_protection_style import BrowserProtectionStyle, ProtectionLevel
|
||||||
import traceback
|
import traceback
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountCreationWorkerThread(QThread):
|
class BaseAccountCreationWorkerThread(QThread):
|
||||||
"""Basis-Klasse für alle Platform Worker Threads"""
|
"""Basis-Klasse für alle Platform Worker Threads"""
|
||||||
|
|
||||||
# Signals MÜSSEN identisch zu bestehenden sein
|
# Signals MÜSSEN identisch zu bestehenden sein
|
||||||
update_signal = pyqtSignal(str)
|
update_signal = pyqtSignal(str)
|
||||||
log_signal = pyqtSignal(str)
|
log_signal = pyqtSignal(str)
|
||||||
progress_signal = pyqtSignal(int)
|
progress_signal = pyqtSignal(int)
|
||||||
finished_signal = pyqtSignal(dict)
|
finished_signal = pyqtSignal(dict)
|
||||||
error_signal = pyqtSignal(str)
|
error_signal = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, params: Dict[str, Any], platform_name: str,
|
def __init__(self, params: Dict[str, Any], platform_name: str,
|
||||||
session_controller: Optional[Any] = None,
|
session_controller: Optional[Any] = None,
|
||||||
generator_tab: Optional[Any] = None):
|
generator_tab: Optional[Any] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -29,10 +33,19 @@ class BaseAccountCreationWorkerThread(QThread):
|
|||||||
self.session_controller = session_controller
|
self.session_controller = session_controller
|
||||||
self.generator_tab = generator_tab
|
self.generator_tab = generator_tab
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
|
# Thread-Safety Lock für Guard-Flags
|
||||||
|
self._guard_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Flag: Wurde der Worker durch User-Abbruch gestoppt?
|
||||||
|
self._was_cancelled = False
|
||||||
|
|
||||||
|
# Flag: Wurde Guard bereits vom Worker freigegeben?
|
||||||
|
self._guard_released = False
|
||||||
|
|
||||||
# TextSimilarity für robustes Fehler-Matching
|
# TextSimilarity für robustes Fehler-Matching
|
||||||
self.text_similarity = TextSimilarity(default_threshold=0.8)
|
self.text_similarity = TextSimilarity(default_threshold=0.8)
|
||||||
|
|
||||||
# Platform-spezifische Error-Patterns (überschreibbar)
|
# Platform-spezifische Error-Patterns (überschreibbar)
|
||||||
self.error_interpretations = self.get_error_interpretations()
|
self.error_interpretations = self.get_error_interpretations()
|
||||||
|
|
||||||
@ -209,10 +222,8 @@ class BaseAccountCreationWorkerThread(QThread):
|
|||||||
self.progress_signal.emit(0) # Reset progress on error
|
self.progress_signal.emit(0) # Reset progress on error
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Feature 5: Process Guard freigeben
|
# Process Guard freigeben (thread-safe)
|
||||||
from utils.process_guard import get_guard
|
self._release_guard_if_needed(success)
|
||||||
guard = get_guard()
|
|
||||||
guard.end(success)
|
|
||||||
|
|
||||||
def _interpret_error(self, error_message: str) -> str:
|
def _interpret_error(self, error_message: str) -> str:
|
||||||
"""Interpretiert Fehler mit Fuzzy-Matching"""
|
"""Interpretiert Fehler mit Fuzzy-Matching"""
|
||||||
@ -282,24 +293,47 @@ class BaseAccountCreationWorkerThread(QThread):
|
|||||||
|
|
||||||
return save_result
|
return save_result
|
||||||
|
|
||||||
|
def _release_guard_if_needed(self, success: bool = False, is_cancel: bool = False):
|
||||||
|
"""
|
||||||
|
Thread-safe Guard-Freigabe.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
success: War der Prozess erfolgreich? (nur relevant bei end())
|
||||||
|
is_cancel: Wird von stop() aufgerufen? (erzwingt release())
|
||||||
|
"""
|
||||||
|
with self._guard_lock:
|
||||||
|
if self._guard_released:
|
||||||
|
# Bereits freigegeben - nichts zu tun
|
||||||
|
return
|
||||||
|
|
||||||
|
# Wenn von stop() aufgerufen, IMMER als Cancel markieren
|
||||||
|
if is_cancel:
|
||||||
|
self._was_cancelled = True
|
||||||
|
|
||||||
|
from utils.process_guard import get_guard
|
||||||
|
guard = get_guard()
|
||||||
|
|
||||||
|
if self._was_cancelled:
|
||||||
|
# User-Abbruch: release() zählt nicht als Failure
|
||||||
|
guard.release()
|
||||||
|
logger.info(f"{self.platform_name}: Guard released (User-Abbruch)")
|
||||||
|
else:
|
||||||
|
# Normale Beendigung: end() mit Erfolgs-Status
|
||||||
|
guard.end(success)
|
||||||
|
logger.info(f"{self.platform_name}: Guard ended (success={success})")
|
||||||
|
|
||||||
|
self._guard_released = True
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""
|
||||||
Stoppt den Thread sauber mit Guard-Freigabe.
|
Stoppt den Thread sauber mit Guard-Freigabe.
|
||||||
|
|
||||||
WICHTIG: Guard wird SOFORT freigegeben, da terminate() den finally-Block überspringt.
|
User-Abbruch wird mit release() behandelt (zählt NICHT als Failure).
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
# Guard SOFORT freigeben bevor terminate()
|
# Guard thread-safe freigeben (is_cancel=True setzt _was_cancelled im Lock)
|
||||||
# Grund: terminate() überspringt den finally-Block in run()
|
self._release_guard_if_needed(is_cancel=True)
|
||||||
from utils.process_guard import get_guard
|
|
||||||
guard = get_guard()
|
|
||||||
if guard.is_locked():
|
|
||||||
guard.end(success=False)
|
|
||||||
logger.info("Guard freigegeben bei Worker-Stop (vor terminate)")
|
|
||||||
|
|
||||||
# Jetzt Thread beenden
|
# Jetzt Thread beenden
|
||||||
self.terminate()
|
self.terminate()
|
||||||
|
|||||||
@ -307,15 +307,13 @@ class FacebookController(BasePlatformController):
|
|||||||
self.forge_dialog = None
|
self.forge_dialog = None
|
||||||
|
|
||||||
def stop_account_creation(self):
|
def stop_account_creation(self):
|
||||||
"""Stoppt die Facebook-Account-Erstellung mit Guard-Freigabe."""
|
"""
|
||||||
# Guard-Freigabe (wichtig: VOR Worker-Stop)
|
Stoppt die Facebook-Account-Erstellung.
|
||||||
from utils.process_guard import get_guard
|
|
||||||
guard = get_guard()
|
|
||||||
if guard.is_locked():
|
|
||||||
guard.end(success=False)
|
|
||||||
self.logger.info("Guard freigegeben bei Facebook Stop")
|
|
||||||
|
|
||||||
# Worker stoppen
|
WICHTIG: Guard-Freigabe erfolgt im Worker.stop() mit release().
|
||||||
|
User-Abbruch zählt NICHT als Failure.
|
||||||
|
"""
|
||||||
|
# Worker stoppen (Worker.stop() gibt Guard frei mit release())
|
||||||
if self.worker_thread and self.worker_thread.isRunning():
|
if self.worker_thread and self.worker_thread.isRunning():
|
||||||
self.worker_thread.stop()
|
self.worker_thread.stop()
|
||||||
generator_tab = self.get_generator_tab()
|
generator_tab = self.get_generator_tab()
|
||||||
|
|||||||
@ -198,17 +198,15 @@ class GmailController(BasePlatformController):
|
|||||||
logger.info(f"[GMAIL] start_account_creation abgeschlossen")
|
logger.info(f"[GMAIL] start_account_creation abgeschlossen")
|
||||||
|
|
||||||
def stop_account_creation(self):
|
def stop_account_creation(self):
|
||||||
"""Stoppt die laufende Account-Erstellung mit Guard-Freigabe"""
|
"""
|
||||||
|
Stoppt die Gmail-Account-Erstellung.
|
||||||
|
|
||||||
|
WICHTIG: Guard-Freigabe erfolgt im Worker.stop() mit release().
|
||||||
|
User-Abbruch zählt NICHT als Failure.
|
||||||
|
"""
|
||||||
logger.info("[GMAIL] Stoppe Account-Erstellung")
|
logger.info("[GMAIL] Stoppe Account-Erstellung")
|
||||||
|
|
||||||
# Guard-Freigabe (wichtig: VOR Worker-Stop)
|
# Worker stoppen (Worker.stop() gibt Guard frei mit release())
|
||||||
from utils.process_guard import get_guard
|
|
||||||
guard = get_guard()
|
|
||||||
if guard.is_locked():
|
|
||||||
guard.end(success=False)
|
|
||||||
logger.info("Guard freigegeben bei Gmail Stop")
|
|
||||||
|
|
||||||
# Worker stoppen
|
|
||||||
if self.worker_thread and self.worker_thread.isRunning():
|
if self.worker_thread and self.worker_thread.isRunning():
|
||||||
self.worker_thread.stop()
|
self.worker_thread.stop()
|
||||||
self.worker_thread.wait()
|
self.worker_thread.wait()
|
||||||
|
|||||||
@ -198,8 +198,9 @@ class InstagramController(BasePlatformController):
|
|||||||
is_valid, error_msg = self.validate_inputs(params)
|
is_valid, error_msg = self.validate_inputs(params)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
# Guard freigeben da Worker nicht gestartet wird
|
# Guard freigeben da Worker nicht gestartet wird
|
||||||
|
# release() statt end() - Validierungsfehler ist kein "echter" Failure
|
||||||
from utils.process_guard import get_guard
|
from utils.process_guard import get_guard
|
||||||
get_guard().end(success=False)
|
get_guard().release()
|
||||||
self.get_generator_tab().show_error(error_msg)
|
self.get_generator_tab().show_error(error_msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -213,8 +214,10 @@ class InstagramController(BasePlatformController):
|
|||||||
# Schmiedeanimation-Dialog erstellen und anzeigen
|
# Schmiedeanimation-Dialog erstellen und anzeigen
|
||||||
parent_widget = generator_tab.window() # Hauptfenster als Parent
|
parent_widget = generator_tab.window() # Hauptfenster als Parent
|
||||||
self.forge_dialog = ForgeAnimationDialog(parent_widget, "Instagram")
|
self.forge_dialog = ForgeAnimationDialog(parent_widget, "Instagram")
|
||||||
self.forge_dialog.cancel_clicked.connect(self.stop_account_creation)
|
|
||||||
self.forge_dialog.closed.connect(self.stop_account_creation)
|
# NUR cancel_clicked verbinden - closed wird durch close() in stop_account_creation
|
||||||
|
# getriggert und würde sonst zu Doppelaufrufen führen
|
||||||
|
self.forge_dialog.cancel_clicked.connect(self._on_user_cancel)
|
||||||
|
|
||||||
# Fensterposition vom Hauptfenster holen
|
# Fensterposition vom Hauptfenster holen
|
||||||
if parent_widget:
|
if parent_widget:
|
||||||
@ -276,28 +279,35 @@ class InstagramController(BasePlatformController):
|
|||||||
# Kritischer Fehler VOR Worker-Start → Guard freigeben!
|
# Kritischer Fehler VOR Worker-Start → Guard freigeben!
|
||||||
logger.error(f"Fehler beim Start der Account-Erstellung: {e}", exc_info=True)
|
logger.error(f"Fehler beim Start der Account-Erstellung: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# release() - technischer Fehler vor Start ist kein User-Failure
|
||||||
from utils.process_guard import get_guard
|
from utils.process_guard import get_guard
|
||||||
get_guard().end(success=False)
|
get_guard().release()
|
||||||
|
|
||||||
# Dialog schließen falls vorhanden
|
# Dialog schließen falls vorhanden
|
||||||
if hasattr(self, 'forge_dialog') and self.forge_dialog:
|
if hasattr(self, 'forge_dialog') and self.forge_dialog:
|
||||||
self.forge_dialog.close()
|
self.forge_dialog.close()
|
||||||
|
self.forge_dialog = None
|
||||||
|
|
||||||
# UI zurücksetzen
|
# UI zurücksetzen
|
||||||
generator_tab = self.get_generator_tab()
|
generator_tab = self.get_generator_tab()
|
||||||
generator_tab.set_running(False)
|
generator_tab.set_running(False)
|
||||||
generator_tab.show_error(f"Fehler beim Start: {str(e)}")
|
generator_tab.show_error(f"Fehler beim Start: {str(e)}")
|
||||||
|
|
||||||
def stop_account_creation(self):
|
def _on_user_cancel(self):
|
||||||
"""Stoppt die Instagram-Account-Erstellung mit Guard-Freigabe."""
|
"""
|
||||||
# Guard-Freigabe (wichtig: VOR Worker-Stop)
|
Handler für User-Abbruch (Cancel-Button im Dialog).
|
||||||
from utils.process_guard import get_guard
|
Ruft stop_account_creation auf und verhindert Doppelaufrufe.
|
||||||
guard = get_guard()
|
"""
|
||||||
if guard.is_locked():
|
self.stop_account_creation()
|
||||||
guard.end(success=False)
|
|
||||||
self.logger.info("Guard freigegeben bei Instagram Stop")
|
|
||||||
|
|
||||||
# Worker stoppen
|
def stop_account_creation(self):
|
||||||
|
"""
|
||||||
|
Stoppt die Instagram-Account-Erstellung.
|
||||||
|
|
||||||
|
WICHTIG: Guard-Freigabe erfolgt im Worker.stop() mit release().
|
||||||
|
Der Controller macht KEINE eigene Guard-Freigabe mehr.
|
||||||
|
"""
|
||||||
|
# Worker stoppen (Worker.stop() gibt Guard frei mit release())
|
||||||
if self.worker_thread and self.worker_thread.isRunning():
|
if self.worker_thread and self.worker_thread.isRunning():
|
||||||
self.worker_thread.stop()
|
self.worker_thread.stop()
|
||||||
generator_tab = self.get_generator_tab()
|
generator_tab = self.get_generator_tab()
|
||||||
|
|||||||
@ -267,17 +267,28 @@ class MethodRotationMixin:
|
|||||||
def _create_rotation_context(self, params: Dict[str, Any]) -> RotationContext:
|
def _create_rotation_context(self, params: Dict[str, Any]) -> RotationContext:
|
||||||
"""
|
"""
|
||||||
Create rotation context from account creation parameters.
|
Create rotation context from account creation parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
params: Account creation parameters
|
params: Account creation parameters
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
RotationContext for method selection
|
RotationContext for method selection
|
||||||
"""
|
"""
|
||||||
|
# Handle both BrowserFingerprint objects and dictionaries
|
||||||
|
fingerprint_data = params.get('fingerprint')
|
||||||
|
fingerprint_id = None
|
||||||
|
if fingerprint_data:
|
||||||
|
if hasattr(fingerprint_data, 'fingerprint_id'):
|
||||||
|
# BrowserFingerprint object
|
||||||
|
fingerprint_id = fingerprint_data.fingerprint_id
|
||||||
|
elif isinstance(fingerprint_data, dict):
|
||||||
|
# Dictionary
|
||||||
|
fingerprint_id = fingerprint_data.get('fingerprint_id')
|
||||||
|
|
||||||
return RotationContext(
|
return RotationContext(
|
||||||
platform=self.platform_name.lower(),
|
platform=self.platform_name.lower(),
|
||||||
account_id=params.get('account_id'),
|
account_id=params.get('account_id'),
|
||||||
fingerprint_id=params.get('fingerprint', {}).get('fingerprint_id'),
|
fingerprint_id=fingerprint_id,
|
||||||
excluded_methods=params.get('_excluded_methods', []),
|
excluded_methods=params.get('_excluded_methods', []),
|
||||||
max_risk_level=RiskLevel(params.get('_max_risk_level', 'HIGH')),
|
max_risk_level=RiskLevel(params.get('_max_risk_level', 'HIGH')),
|
||||||
emergency_mode=params.get('_emergency_mode', False),
|
emergency_mode=params.get('_emergency_mode', False),
|
||||||
|
|||||||
@ -131,15 +131,13 @@ class OkRuController(BasePlatformController):
|
|||||||
self.forge_dialog.show()
|
self.forge_dialog.show()
|
||||||
|
|
||||||
def stop_account_creation(self):
|
def stop_account_creation(self):
|
||||||
"""Stoppt die OK.ru-Account-Erstellung mit Guard-Freigabe."""
|
"""
|
||||||
# Guard-Freigabe (wichtig: VOR Worker-Stop)
|
Stoppt die OK.ru-Account-Erstellung.
|
||||||
from utils.process_guard import get_guard
|
|
||||||
guard = get_guard()
|
|
||||||
if guard.is_locked():
|
|
||||||
guard.end(success=False)
|
|
||||||
self.logger.info("Guard freigegeben bei OK.ru Stop")
|
|
||||||
|
|
||||||
# Worker stoppen
|
WICHTIG: Guard-Freigabe erfolgt im Worker.stop() mit release().
|
||||||
|
User-Abbruch zählt NICHT als Failure.
|
||||||
|
"""
|
||||||
|
# Worker stoppen (Worker.stop() gibt Guard frei mit release())
|
||||||
if self.worker_thread and self.worker_thread.isRunning():
|
if self.worker_thread and self.worker_thread.isRunning():
|
||||||
self.worker_thread.stop()
|
self.worker_thread.stop()
|
||||||
generator_tab = self.get_generator_tab()
|
generator_tab = self.get_generator_tab()
|
||||||
|
|||||||
@ -273,15 +273,13 @@ class TikTokController(BasePlatformController):
|
|||||||
self.forge_dialog.show()
|
self.forge_dialog.show()
|
||||||
|
|
||||||
def stop_account_creation(self):
|
def stop_account_creation(self):
|
||||||
"""Stoppt die TikTok-Account-Erstellung mit Guard-Freigabe."""
|
"""
|
||||||
# Guard-Freigabe (wichtig: VOR Worker-Stop)
|
Stoppt die TikTok-Account-Erstellung.
|
||||||
from utils.process_guard import get_guard
|
|
||||||
guard = get_guard()
|
|
||||||
if guard.is_locked():
|
|
||||||
guard.end(success=False)
|
|
||||||
self.logger.info("Guard freigegeben bei TikTok Stop")
|
|
||||||
|
|
||||||
# Worker stoppen
|
WICHTIG: Guard-Freigabe erfolgt im Worker.stop() mit release().
|
||||||
|
User-Abbruch zählt NICHT als Failure.
|
||||||
|
"""
|
||||||
|
# Worker stoppen (Worker.stop() gibt Guard frei mit release())
|
||||||
if self.worker_thread and self.worker_thread.isRunning():
|
if self.worker_thread and self.worker_thread.isRunning():
|
||||||
self.worker_thread.stop()
|
self.worker_thread.stop()
|
||||||
generator_tab = self.get_generator_tab()
|
generator_tab = self.get_generator_tab()
|
||||||
|
|||||||
@ -271,15 +271,13 @@ class XController(BasePlatformController):
|
|||||||
self.forge_dialog.show()
|
self.forge_dialog.show()
|
||||||
|
|
||||||
def stop_account_creation(self):
|
def stop_account_creation(self):
|
||||||
"""Stoppt die X-Account-Erstellung mit Guard-Freigabe."""
|
"""
|
||||||
# Guard-Freigabe (wichtig: VOR Worker-Stop)
|
Stoppt die X-Account-Erstellung.
|
||||||
from utils.process_guard import get_guard
|
|
||||||
guard = get_guard()
|
|
||||||
if guard.is_locked():
|
|
||||||
guard.end(success=False)
|
|
||||||
self.logger.info("Guard freigegeben bei X Stop")
|
|
||||||
|
|
||||||
# Worker stoppen
|
WICHTIG: Guard-Freigabe erfolgt im Worker.stop() mit release().
|
||||||
|
User-Abbruch zählt NICHT als Failure.
|
||||||
|
"""
|
||||||
|
# Worker stoppen (Worker.stop() gibt Guard frei mit release())
|
||||||
if self.worker_thread and self.worker_thread.isRunning():
|
if self.worker_thread and self.worker_thread.isRunning():
|
||||||
self.worker_thread.stop()
|
self.worker_thread.stop()
|
||||||
generator_tab = self.get_generator_tab()
|
generator_tab = self.get_generator_tab()
|
||||||
|
|||||||
@ -118,27 +118,28 @@ class SessionController(QObject):
|
|||||||
else:
|
else:
|
||||||
error_msg = f"Account mit ID {account_id} nicht gefunden"
|
error_msg = f"Account mit ID {account_id} nicht gefunden"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
# Feature 5: Guard freigeben da Worker nicht gestartet wird
|
# Guard freigeben - kein User-Fehler, daher release()
|
||||||
guard.end(success=False)
|
guard.release()
|
||||||
self.login_failed.emit(account_id, error_msg)
|
self.login_failed.emit(account_id, error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Ein-Klick-Login: {e}")
|
logger.error(f"Fehler beim Ein-Klick-Login: {e}")
|
||||||
# Feature 5: Guard freigeben bei Fehler vor Worker-Start
|
# Guard freigeben - technischer Fehler vor Start, daher release()
|
||||||
guard.end(success=False)
|
guard.release()
|
||||||
self.login_failed.emit(account_id, str(e))
|
self.login_failed.emit(account_id, str(e))
|
||||||
|
|
||||||
def _cancel_login(self, account_id: str):
|
def _cancel_login(self, account_id: str):
|
||||||
"""Bricht den Login-Prozess ab mit Guard-Freigabe"""
|
"""
|
||||||
|
Bricht den Login-Prozess ab.
|
||||||
|
User-Abbruch zählt NICHT als Failure.
|
||||||
|
"""
|
||||||
logger.info(f"Login für Account {account_id} wurde abgebrochen")
|
logger.info(f"Login für Account {account_id} wurde abgebrochen")
|
||||||
|
|
||||||
# Guard IMMER freigeben bei Cancel (einfacher + robuster)
|
# Guard freigeben - User-Abbruch, daher release()
|
||||||
# Doppelte Freigabe ist kein Problem (wird von ProcessGuard ignoriert)
|
|
||||||
from utils.process_guard import get_guard
|
from utils.process_guard import get_guard
|
||||||
guard = get_guard()
|
guard = get_guard()
|
||||||
if guard.is_locked():
|
guard.release() # Idempotent - mehrfacher Aufruf ist sicher
|
||||||
guard.end(success=False)
|
logger.info("Guard released bei Login-Cancel")
|
||||||
logger.info("Guard freigegeben bei Login-Cancel")
|
|
||||||
|
|
||||||
# Dialog schließen
|
# Dialog schließen
|
||||||
if hasattr(self, 'login_dialog') and self.login_dialog:
|
if hasattr(self, 'login_dialog') and self.login_dialog:
|
||||||
|
|||||||
Binäre Datei nicht angezeigt.
@ -1,267 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Debug Video Issue - Final Diagnostic Script
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from browser.playwright_manager import PlaywrightManager
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
logger = logging.getLogger("video_debug")
|
|
||||||
|
|
||||||
async def debug_video_issue():
|
|
||||||
"""Comprehensive video issue debugging"""
|
|
||||||
|
|
||||||
print("🔍 STARTING COMPREHENSIVE VIDEO DEBUG ANALYSIS")
|
|
||||||
|
|
||||||
# Test with fresh manager
|
|
||||||
manager = PlaywrightManager(headless=False)
|
|
||||||
|
|
||||||
try:
|
|
||||||
page = manager.start()
|
|
||||||
|
|
||||||
print("📋 STEP 1: Navigating to Instagram...")
|
|
||||||
success = manager.navigate_to("https://www.instagram.com")
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
print("❌ Failed to navigate to Instagram")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("📋 STEP 2: Checking browser capabilities...")
|
|
||||||
|
|
||||||
# Check all video-related capabilities
|
|
||||||
capabilities = page.evaluate("""
|
|
||||||
() => {
|
|
||||||
const results = {
|
|
||||||
// Basic video support
|
|
||||||
video_element: !!document.createElement('video'),
|
|
||||||
video_can_play_mp4: document.createElement('video').canPlayType('video/mp4'),
|
|
||||||
video_can_play_webm: document.createElement('video').canPlayType('video/webm'),
|
|
||||||
|
|
||||||
// DRM Support
|
|
||||||
widevine_support: !!navigator.requestMediaKeySystemAccess,
|
|
||||||
media_source: !!window.MediaSource,
|
|
||||||
encrypted_media: !!window.MediaKeys,
|
|
||||||
|
|
||||||
// Chrome APIs
|
|
||||||
chrome_present: !!window.chrome,
|
|
||||||
chrome_runtime: !!(window.chrome && window.chrome.runtime),
|
|
||||||
chrome_app: window.chrome ? window.chrome.app : 'missing',
|
|
||||||
chrome_csi: !!(window.chrome && window.chrome.csi),
|
|
||||||
chrome_loadtimes: !!(window.chrome && window.chrome.loadTimes),
|
|
||||||
|
|
||||||
// Media Devices
|
|
||||||
media_devices: !!(navigator.mediaDevices),
|
|
||||||
enumerate_devices: !!(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices),
|
|
||||||
get_user_media: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
|
||||||
|
|
||||||
// Performance API
|
|
||||||
performance_now: !!performance.now,
|
|
||||||
performance_timing: !!performance.timing,
|
|
||||||
|
|
||||||
// Automation markers
|
|
||||||
webdriver_present: !!navigator.webdriver,
|
|
||||||
automation_markers: {
|
|
||||||
webdriver_script_fn: !!navigator.__webdriver_script_fn,
|
|
||||||
webdriver_evaluate: !!window.__webdriver_evaluate,
|
|
||||||
selenium_unwrapped: !!document.__selenium_unwrapped,
|
|
||||||
chrome_webdriver: !!(window.chrome && window.chrome.webdriver)
|
|
||||||
},
|
|
||||||
|
|
||||||
// User agent analysis
|
|
||||||
user_agent: navigator.userAgent,
|
|
||||||
platform: navigator.platform,
|
|
||||||
vendor: navigator.vendor,
|
|
||||||
languages: navigator.languages,
|
|
||||||
|
|
||||||
// Screen info
|
|
||||||
screen_width: screen.width,
|
|
||||||
screen_height: screen.height,
|
|
||||||
device_pixel_ratio: devicePixelRatio,
|
|
||||||
|
|
||||||
// Timing
|
|
||||||
page_load_time: performance.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
print("📊 BROWSER CAPABILITIES:")
|
|
||||||
for key, value in capabilities.items():
|
|
||||||
print(f" {key}: {value}")
|
|
||||||
|
|
||||||
print("\n📋 STEP 3: Testing video element creation...")
|
|
||||||
|
|
||||||
video_test = page.evaluate("""
|
|
||||||
() => {
|
|
||||||
// Create video element and test
|
|
||||||
const video = document.createElement('video');
|
|
||||||
video.style.display = 'none';
|
|
||||||
document.body.appendChild(video);
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
video_created: true,
|
|
||||||
video_properties: {
|
|
||||||
autoplay: video.autoplay,
|
|
||||||
controls: video.controls,
|
|
||||||
muted: video.muted,
|
|
||||||
preload: video.preload,
|
|
||||||
crossOrigin: video.crossOrigin
|
|
||||||
},
|
|
||||||
video_methods: {
|
|
||||||
canPlayType: typeof video.canPlayType,
|
|
||||||
play: typeof video.play,
|
|
||||||
pause: typeof video.pause,
|
|
||||||
load: typeof video.load
|
|
||||||
},
|
|
||||||
codec_support: {
|
|
||||||
mp4_h264: video.canPlayType('video/mp4; codecs="avc1.42E01E"'),
|
|
||||||
mp4_h265: video.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"'),
|
|
||||||
webm_vp8: video.canPlayType('video/webm; codecs="vp8"'),
|
|
||||||
webm_vp9: video.canPlayType('video/webm; codecs="vp9"'),
|
|
||||||
audio_aac: video.canPlayType('audio/mp4; codecs="mp4a.40.2"'),
|
|
||||||
audio_opus: video.canPlayType('audio/webm; codecs="opus"')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.body.removeChild(video);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
print("\n📊 VIDEO ELEMENT TEST:")
|
|
||||||
for key, value in video_test.items():
|
|
||||||
print(f" {key}: {value}")
|
|
||||||
|
|
||||||
print("\n📋 STEP 4: Checking console errors...")
|
|
||||||
|
|
||||||
# Wait a bit for any console errors
|
|
||||||
await asyncio.sleep(2)
|
|
||||||
|
|
||||||
# Check for specific Instagram video errors
|
|
||||||
print("\n📋 STEP 5: Looking for Instagram-specific issues...")
|
|
||||||
|
|
||||||
# Try to find any video elements or error messages
|
|
||||||
video_status = page.evaluate("""
|
|
||||||
() => {
|
|
||||||
const results = {
|
|
||||||
video_elements_count: document.querySelectorAll('video').length,
|
|
||||||
error_messages: [],
|
|
||||||
instagram_classes: {
|
|
||||||
video_error_present: !!document.querySelector('.x6s0dn4.xatbrnm.x9f619'),
|
|
||||||
video_containers: document.querySelectorAll('[class*="video"]').length,
|
|
||||||
error_spans: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Look for error messages
|
|
||||||
const errorSpans = document.querySelectorAll('span');
|
|
||||||
errorSpans.forEach(span => {
|
|
||||||
const text = span.textContent.trim();
|
|
||||||
if (text.includes('Video') || text.includes('video') || text.includes('abgespielt') || text.includes('richtig')) {
|
|
||||||
results.instagram_classes.error_spans.push({
|
|
||||||
text: text,
|
|
||||||
classes: span.className
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
print("\n📊 INSTAGRAM VIDEO STATUS:")
|
|
||||||
for key, value in video_status.items():
|
|
||||||
print(f" {key}: {value}")
|
|
||||||
|
|
||||||
print("\n📋 STEP 6: Testing DRM capabilities...")
|
|
||||||
|
|
||||||
drm_test = page.evaluate("""
|
|
||||||
() => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (!navigator.requestMediaKeySystemAccess) {
|
|
||||||
resolve({drm_support: false, error: 'No requestMediaKeySystemAccess'});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{
|
|
||||||
initDataTypes: ['cenc'],
|
|
||||||
videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E"'}]
|
|
||||||
}]).then(access => {
|
|
||||||
resolve({
|
|
||||||
drm_support: true,
|
|
||||||
key_system: access.keySystem,
|
|
||||||
configuration: access.getConfiguration()
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
|
||||||
resolve({
|
|
||||||
drm_support: false,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
print("\n📊 DRM TEST RESULTS:")
|
|
||||||
print(f" {drm_test}")
|
|
||||||
|
|
||||||
print("\n🎯 FINAL DIAGNOSIS:")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Analyze results
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
if not capabilities.get('video_element'):
|
|
||||||
issues.append("❌ Video elements not supported")
|
|
||||||
|
|
||||||
if capabilities.get('webdriver_present'):
|
|
||||||
issues.append("❌ Webdriver detection present")
|
|
||||||
|
|
||||||
if not capabilities.get('widevine_support'):
|
|
||||||
issues.append("❌ Widevine DRM not supported")
|
|
||||||
|
|
||||||
if video_status.get('instagram_classes', {}).get('video_error_present'):
|
|
||||||
issues.append("❌ Instagram video error message detected")
|
|
||||||
|
|
||||||
if not drm_test.get('drm_support'):
|
|
||||||
issues.append(f"❌ DRM test failed: {drm_test.get('error', 'Unknown')}")
|
|
||||||
|
|
||||||
automation_markers = capabilities.get('automation_markers', {})
|
|
||||||
detected_markers = [k for k, v in automation_markers.items() if v]
|
|
||||||
if detected_markers:
|
|
||||||
issues.append(f"❌ Automation markers detected: {detected_markers}")
|
|
||||||
|
|
||||||
if issues:
|
|
||||||
print("🚨 CRITICAL ISSUES FOUND:")
|
|
||||||
for issue in issues:
|
|
||||||
print(f" {issue}")
|
|
||||||
else:
|
|
||||||
print("✅ No obvious technical issues detected")
|
|
||||||
print("🤔 The problem might be:")
|
|
||||||
print(" - Account-specific restrictions")
|
|
||||||
print(" - Geographic blocking")
|
|
||||||
print(" - Instagram A/B testing")
|
|
||||||
print(" - Specific video content restrictions")
|
|
||||||
|
|
||||||
print("\n📋 RECOMMENDATION:")
|
|
||||||
if len(issues) > 3:
|
|
||||||
print(" 🔄 Technical fixes needed - automation still detectable")
|
|
||||||
elif len(issues) > 0:
|
|
||||||
print(" 🔧 Some technical issues remain")
|
|
||||||
else:
|
|
||||||
print(" 💡 Technical setup appears correct - likely policy/account issue")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Debug failed: {e}")
|
|
||||||
print(f"❌ Debug script failed: {e}")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
manager.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(debug_video_issue())
|
|
||||||
2224
docs/overview.md
Normale Datei
2224
docs/overview.md
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
503
docs/production-architecture.md
Normale Datei
503
docs/production-architecture.md
Normale Datei
@ -0,0 +1,503 @@
|
|||||||
|
# AccountForger - Produktionsarchitektur
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
| Aspekt | Entscheidung |
|
||||||
|
|--------|--------------|
|
||||||
|
| **Hosting** | Eigener VPS/Root-Server (Docker Compose) |
|
||||||
|
| **Multi-Tenant** | Ja - mehrere Clients mit eigenen API-Keys |
|
||||||
|
| **eSIM-Strategie** | Pool mit Rotation (mehrere Nummern pro Kunde) |
|
||||||
|
| **Router-Standort** | Beim Kunden (dezentral) |
|
||||||
|
| **eSIM-Verwaltung** | Zentral durch Anbieter |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business-Modell
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ VERKAUF AN KUNDEN │
|
||||||
|
│ │
|
||||||
|
│ Paket beinhaltet: │
|
||||||
|
│ ├── Software-Lizenz (AccountForger Desktop) │
|
||||||
|
│ ├── Vorkonfigurierter RUTX11 Router │
|
||||||
|
│ │ ├── Webhook bereits eingerichtet │
|
||||||
|
│ │ ├── Auth-Token basierend auf Lizenz │
|
||||||
|
│ │ └── eSIMs bereits eingelegt │
|
||||||
|
│ └── Telefonnummern im System registriert │
|
||||||
|
│ │
|
||||||
|
│ Kunde muss nur: │
|
||||||
|
│ ├── Router mit Strom + Internet verbinden │
|
||||||
|
│ └── Software installieren + Lizenz aktivieren │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Dezentrale Router beim Kunden, zentraler Server für Verifikation.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ KUNDE A │
|
||||||
|
│ ┌─────────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ Desktop Client │ │ RUTX11 Router │ │
|
||||||
|
│ │ (Lizenz: A123) │ │ (Token: A123) │ │
|
||||||
|
│ └────────┬────────┘ └────────┬─────────┘ │
|
||||||
|
└───────────┼────────────────────────────────┼────────────────────┘
|
||||||
|
│ │
|
||||||
|
┌───────────┼────────────────────────────────┼────────────────────┐
|
||||||
|
│ KUNDE B │
|
||||||
|
│ ┌─────────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ Desktop Client │ │ RUTX11 Router │ │
|
||||||
|
│ │ (Lizenz: B456) │ │ (Token: B456) │ │
|
||||||
|
│ └────────┬────────┘ └────────┬─────────┘ │
|
||||||
|
└───────────┼────────────────────────────────┼────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ HTTPS (API) │ HTTPS (Webhook)
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ZENTRALER SERVER (VPS) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
|
||||||
|
│ │ Email │ │ SMS │ │ Client/Router │ │
|
||||||
|
│ │ Service │ │ Service │ │ Registry │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ IMAP Polling│ │ Webhook │ │ Lizenz → Router → Tel. │ │
|
||||||
|
│ └─────────────┘ │ Empfänger │ └─────────────────────────┘ │
|
||||||
|
│ └─────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ PostgreSQL FastAPI │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Server-Komponenten
|
||||||
|
|
||||||
|
### Technologie-Stack
|
||||||
|
| Komponente | Technologie |
|
||||||
|
|------------|-------------|
|
||||||
|
| Framework | FastAPI (Python) |
|
||||||
|
| Datenbank | PostgreSQL |
|
||||||
|
| ORM | SQLAlchemy 2.0 |
|
||||||
|
| Background Jobs | Celery + Redis |
|
||||||
|
| Cache | Redis |
|
||||||
|
| Auth | API-Keys (JWT optional) |
|
||||||
|
|
||||||
|
### API-Endpunkte
|
||||||
|
|
||||||
|
| Endpunkt | Methode | Beschreibung |
|
||||||
|
|----------|---------|--------------|
|
||||||
|
| `/api/v1/verification/email/request` | POST | Email-Adresse anfordern |
|
||||||
|
| `/api/v1/verification/email/code/{id}` | GET | Email-Code abfragen |
|
||||||
|
| `/api/v1/verification/sms/request` | POST | Telefonnummer anfordern |
|
||||||
|
| `/api/v1/verification/sms/code/{id}` | GET | SMS-Code abfragen |
|
||||||
|
| `/api/v1/webhook/rutx11/sms` | POST | SMS-Webhook vom Router |
|
||||||
|
| `/api/v1/phone/available` | GET | Verfügbare Nummern |
|
||||||
|
|
||||||
|
### Datenbank-Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Clients (Lizenznehmer)
|
||||||
|
CREATE TABLE clients (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
license_key VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
name VARCHAR(255),
|
||||||
|
api_key_hash VARCHAR(255) NOT NULL,
|
||||||
|
tier VARCHAR(20) DEFAULT 'standard',
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Router (dezentral beim Kunden)
|
||||||
|
CREATE TABLE routers (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
client_id UUID NOT NULL REFERENCES clients(id),
|
||||||
|
router_token VARCHAR(100) UNIQUE NOT NULL, -- = Lizenzschlüssel oder abgeleitet
|
||||||
|
model VARCHAR(50) DEFAULT 'RUTX11',
|
||||||
|
serial_number VARCHAR(100),
|
||||||
|
is_online BOOLEAN DEFAULT false,
|
||||||
|
last_seen_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Telefonnummern (pro Router/Kunde)
|
||||||
|
CREATE TABLE phone_numbers (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
router_id UUID NOT NULL REFERENCES routers(id),
|
||||||
|
phone_number VARCHAR(20) UNIQUE NOT NULL,
|
||||||
|
esim_slot INTEGER NOT NULL, -- 1 oder 2
|
||||||
|
carrier VARCHAR(100),
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
cooldown_until TIMESTAMP,
|
||||||
|
usage_count INTEGER DEFAULT 0,
|
||||||
|
last_used_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Verifikations-Anfragen
|
||||||
|
CREATE TABLE verification_requests (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
client_id UUID NOT NULL REFERENCES clients(id),
|
||||||
|
platform VARCHAR(50) NOT NULL,
|
||||||
|
verification_type VARCHAR(20) NOT NULL, -- 'email' / 'sms'
|
||||||
|
email_address VARCHAR(255),
|
||||||
|
phone_number_id UUID REFERENCES phone_numbers(id),
|
||||||
|
status VARCHAR(20) DEFAULT 'pending',
|
||||||
|
verification_code VARCHAR(20),
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Email-Konten (zentral Server-verwaltet)
|
||||||
|
CREATE TABLE email_accounts (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
email_address VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
imap_server VARCHAR(255) NOT NULL,
|
||||||
|
imap_port INTEGER DEFAULT 993,
|
||||||
|
password_encrypted BYTEA NOT NULL,
|
||||||
|
domain VARCHAR(255) NOT NULL,
|
||||||
|
is_active BOOLEAN DEFAULT true
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Empfangene SMS
|
||||||
|
CREATE TABLE sms_messages (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
router_id UUID NOT NULL REFERENCES routers(id),
|
||||||
|
phone_number VARCHAR(20) NOT NULL,
|
||||||
|
sender VARCHAR(50),
|
||||||
|
message_body TEXT NOT NULL,
|
||||||
|
received_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
processed BOOLEAN DEFAULT false,
|
||||||
|
matched_request_id UUID REFERENCES verification_requests(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entity-Relationship
|
||||||
|
|
||||||
|
```
|
||||||
|
clients (1) ──────< routers (1) ──────< phone_numbers (n)
|
||||||
|
│ │
|
||||||
|
│ └──────< sms_messages (n)
|
||||||
|
│
|
||||||
|
└──────< verification_requests (n)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
accountforger-server/
|
||||||
|
├── app/
|
||||||
|
│ ├── main.py # FastAPI Entry
|
||||||
|
│ ├── config.py # Konfiguration
|
||||||
|
│ ├── api/v1/
|
||||||
|
│ │ ├── verification.py # Verifikations-Endpunkte
|
||||||
|
│ │ ├── webhooks.py # RUTX11 Webhook
|
||||||
|
│ │ └── auth.py # Authentifizierung
|
||||||
|
│ ├── services/
|
||||||
|
│ │ ├── email_service.py # IMAP-Polling
|
||||||
|
│ │ ├── sms_service.py # SMS-Verarbeitung
|
||||||
|
│ │ └── phone_rotation.py # eSIM-Rotation
|
||||||
|
│ └── infrastructure/
|
||||||
|
│ ├── database.py
|
||||||
|
│ └── imap_client.py
|
||||||
|
├── docker-compose.yml
|
||||||
|
└── requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. RUTX11 Integration
|
||||||
|
|
||||||
|
### SMS-Forwarding Konfiguration
|
||||||
|
|
||||||
|
1. **Im RUTX11 WebUI:**
|
||||||
|
- Services → Mobile Utilities → SMS Gateway → SMS Forwarding
|
||||||
|
- Neue Regel: HTTP POST an `https://server.domain/api/v1/webhook/rutx11/sms`
|
||||||
|
|
||||||
|
2. **Webhook-Payload vom Router:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sender": "+49123456789",
|
||||||
|
"message": "123456 is your verification code",
|
||||||
|
"timestamp": "2026-01-17T10:30:00Z",
|
||||||
|
"sim_slot": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dezentrale Router-Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Verification Server │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────┐ │
|
||||||
|
│ │ Router Registry │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ - Client ↔ Router Map │ │
|
||||||
|
│ │ - Telefonnummern/Client│ │
|
||||||
|
│ │ - Health Monitoring │ │
|
||||||
|
│ └───────────┬─────────────┘ │
|
||||||
|
└──────────────┼─────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────┼────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||||
|
│ Kunde A Router │ │ Kunde B Router │ │ Kunde C Router │
|
||||||
|
│ Token: A123 │ │ Token: B456 │ │ Token: C789 │
|
||||||
|
│ Tel: +49... │ │ Tel: +49... │ │ Tel: +49... │
|
||||||
|
└────────────────┘ └────────────────┘ └────────────────┘
|
||||||
|
│ │ │
|
||||||
|
└────────────────────┴────────────────────┘
|
||||||
|
│
|
||||||
|
Webhooks an zentralen Server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Router-Vorkonfiguration (vor Versand)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# RUTX11 SMS Forwarding Konfiguration
|
||||||
|
# Services → Mobile Utilities → SMS Gateway → SMS Forwarding
|
||||||
|
|
||||||
|
Webhook URL: https://verify.domain.com/api/v1/webhook/sms
|
||||||
|
HTTP Method: POST
|
||||||
|
Headers: X-Router-Token: {LIZENZ_TOKEN}
|
||||||
|
Payload Format: JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
### eSIM-Rotationsstrategie
|
||||||
|
|
||||||
|
| Strategie | Beschreibung |
|
||||||
|
|-----------|--------------|
|
||||||
|
| Cooldown | 30 Min. Pause nach Nutzung pro Nummer |
|
||||||
|
| Round Robin | Abwechselnd durch alle verfügbaren Nummern |
|
||||||
|
| Platform-Aware | Tracken welche Nummer für welche Platform verwendet wurde |
|
||||||
|
| Load Balancing | Gleichmäßige Verteilung über alle Router/SIM-Banks |
|
||||||
|
| Health Check | Automatische Deaktivierung bei Problemen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Client-Änderungen
|
||||||
|
|
||||||
|
### Was bleibt im Client
|
||||||
|
- PyQt5 UI
|
||||||
|
- Playwright Browser-Automation
|
||||||
|
- Fingerprint-Management
|
||||||
|
- Lokale SQLite-Datenbank
|
||||||
|
|
||||||
|
### Was entfernt wird
|
||||||
|
- `utils/email_handler.py` → ersetzt durch API-Calls
|
||||||
|
- `config/email_config.json` → keine Credentials mehr im Client
|
||||||
|
- Hardcoded `"123456"` SMS-Placeholder
|
||||||
|
|
||||||
|
### Neuer VerificationClient
|
||||||
|
|
||||||
|
**Datei:** `utils/verification_client.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class VerificationClient:
|
||||||
|
def __init__(self, server_url: str, api_key: str):
|
||||||
|
self.server_url = server_url
|
||||||
|
self.api_key = api_key
|
||||||
|
|
||||||
|
def request_email(self, platform: str) -> dict:
|
||||||
|
"""Fordert Email-Adresse vom Server an."""
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/api/v1/verification/email/request",
|
||||||
|
json={"platform": platform},
|
||||||
|
headers={"X-API-Key": self.api_key}
|
||||||
|
)
|
||||||
|
return response.json() # {"request_id": "...", "email_address": "..."}
|
||||||
|
|
||||||
|
def request_sms(self, platform: str) -> dict:
|
||||||
|
"""Fordert Telefonnummer vom Server an."""
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/api/v1/verification/sms/request",
|
||||||
|
json={"platform": platform},
|
||||||
|
headers={"X-API-Key": self.api_key}
|
||||||
|
)
|
||||||
|
return response.json() # {"request_id": "...", "phone_number": "..."}
|
||||||
|
|
||||||
|
def poll_for_code(self, request_id: str, type: str, timeout: int = 120) -> str:
|
||||||
|
"""Pollt Server bis Code empfangen wurde."""
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zu ändernde Dateien
|
||||||
|
|
||||||
|
| Datei | Änderung |
|
||||||
|
|-------|----------|
|
||||||
|
| `social_networks/base_automation.py` | `verification_client` Parameter hinzufügen |
|
||||||
|
| `social_networks/*/verification.py` | API-Calls statt direktem IMAP |
|
||||||
|
| `controllers/platform_controllers/base_worker_thread.py` | VerificationClient initialisieren |
|
||||||
|
| `config/server_config.json` (NEU) | Server-URL und API-Key |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Multi-Tenant Architektur
|
||||||
|
|
||||||
|
### Client-Verwaltung
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Clients (Tenants)
|
||||||
|
CREATE TABLE clients (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
license_key VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
api_key_hash VARCHAR(255) NOT NULL,
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
tier VARCHAR(20) DEFAULT 'standard', -- standard, premium, enterprise
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Rate Limits pro Tier
|
||||||
|
CREATE TABLE tier_limits (
|
||||||
|
tier VARCHAR(20) PRIMARY KEY,
|
||||||
|
email_requests_per_hour INTEGER,
|
||||||
|
sms_requests_per_hour INTEGER,
|
||||||
|
max_concurrent_verifications INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO tier_limits VALUES
|
||||||
|
('standard', 50, 20, 5),
|
||||||
|
('premium', 200, 100, 20),
|
||||||
|
('enterprise', 1000, 500, 100);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tenant-Isolation
|
||||||
|
|
||||||
|
| Aspekt | Implementierung |
|
||||||
|
|--------|-----------------|
|
||||||
|
| API-Keys | Eindeutig pro Client |
|
||||||
|
| Rate Limits | Per-Tenant basierend auf Tier |
|
||||||
|
| Logging | Client-ID in allen Logs |
|
||||||
|
| Ressourcen | Shared Pool mit Fair-Use |
|
||||||
|
| Abrechnung | Usage-Tracking pro Client |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sicherheit
|
||||||
|
|
||||||
|
### Authentifizierung
|
||||||
|
- API-Key pro Client-Installation
|
||||||
|
- Gehashed in Server-DB (bcrypt)
|
||||||
|
- Gebunden an Lizenzschlüssel
|
||||||
|
|
||||||
|
### Credentials-Management
|
||||||
|
| Credential | Speicherort | Schutz |
|
||||||
|
|------------|-------------|--------|
|
||||||
|
| IMAP-Passwörter | Server DB | AES-256 verschlüsselt |
|
||||||
|
| API-Keys | Server DB | bcrypt Hash |
|
||||||
|
| RUTX11 Webhook | Server .env | Umgebungsvariable |
|
||||||
|
| Client API-Key | Client Config | Verschlüsselt lokal |
|
||||||
|
|
||||||
|
### Netzwerk
|
||||||
|
- HTTPS-Only (TLS 1.3)
|
||||||
|
- IP-Whitelist für RUTX11 Webhook
|
||||||
|
- Rate Limiting (10 Email-Requests/Min, 5 SMS-Requests/Min)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Deployment
|
||||||
|
|
||||||
|
### Docker Compose (empfohlen)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://user:pass@db:5432/verify
|
||||||
|
- REDIS_URL=redis://redis:6379
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
|
||||||
|
celery:
|
||||||
|
build: .
|
||||||
|
command: celery -A app.worker worker -l info
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Verifikation / Testing
|
||||||
|
|
||||||
|
1. **Server starten:** `docker-compose up`
|
||||||
|
2. **Email-Test:**
|
||||||
|
- POST `/api/v1/verification/email/request` mit `{"platform": "instagram"}`
|
||||||
|
- GET `/api/v1/verification/email/code/{id}` → Code erhalten
|
||||||
|
3. **SMS-Test:**
|
||||||
|
- Webhook simulieren: POST an `/api/v1/webhook/rutx11/sms`
|
||||||
|
- Prüfen ob SMS in DB landet und Request gematcht wird
|
||||||
|
4. **Client-Integration:**
|
||||||
|
- `VerificationClient` instanziieren
|
||||||
|
- Account-Erstellung mit Server-Verifikation testen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Onboarding-Prozess (Neuer Kunde)
|
||||||
|
|
||||||
|
### Admin-Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Neue Lizenz generieren
|
||||||
|
└── license_key: "AF-2026-XXXXX"
|
||||||
|
|
||||||
|
2. Client in DB anlegen
|
||||||
|
└── INSERT INTO clients (license_key, name, api_key_hash, tier)
|
||||||
|
|
||||||
|
3. Router vorkonfigurieren
|
||||||
|
├── RUTX11 Webhook URL setzen
|
||||||
|
├── X-Router-Token Header = license_key
|
||||||
|
└── eSIMs einlegen
|
||||||
|
|
||||||
|
4. Telefonnummern registrieren
|
||||||
|
├── INSERT INTO routers (client_id, router_token)
|
||||||
|
└── INSERT INTO phone_numbers (router_id, phone_number, esim_slot)
|
||||||
|
|
||||||
|
5. Paket versenden
|
||||||
|
├── Router (Plug & Play ready)
|
||||||
|
└── Software-Download Link + Lizenzschlüssel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kunde-Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Router auspacken und anschließen
|
||||||
|
└── Strom + Ethernet/WLAN
|
||||||
|
|
||||||
|
2. Software installieren
|
||||||
|
└── AccountForger.exe
|
||||||
|
|
||||||
|
3. Lizenz aktivieren
|
||||||
|
└── Lizenzschlüssel eingeben → Server validiert
|
||||||
|
|
||||||
|
4. Fertig!
|
||||||
|
└── SMS-Verifikation funktioniert automatisch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Implementierungsreihenfolge
|
||||||
|
|
||||||
|
1. **Server Basis** - FastAPI Setup, DB-Schema, Email-Service
|
||||||
|
2. **Router Registry** - Client/Router/Telefonnummer Verwaltung
|
||||||
|
3. **RUTX11 Webhook** - SMS empfangen, Router-Token validieren, Request matchen
|
||||||
|
4. **Client Integration** - VerificationClient implementieren
|
||||||
|
5. **Plattform-Migration** - Alle verification.py Dateien umstellen
|
||||||
|
6. **Admin-Panel** - Kunden/Router/Nummern verwalten (optional)
|
||||||
|
7. **Security & Monitoring** - Rate Limiting, Logging, Health Checks
|
||||||
@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Install script for AccountForger dependencies.
|
|
||||||
Handles PyQt5 installation across different platforms.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
import platform
|
|
||||||
|
|
||||||
def install_package(package):
|
|
||||||
"""Install a package using pip"""
|
|
||||||
try:
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
|
|
||||||
return True
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def install_pyqt5():
|
|
||||||
"""Install PyQt5 with platform-specific handling"""
|
|
||||||
print("Installing PyQt5...")
|
|
||||||
|
|
||||||
# Try different PyQt5 variants
|
|
||||||
packages_to_try = [
|
|
||||||
"PyQt5",
|
|
||||||
"PyQt5-Qt5",
|
|
||||||
"PyQt5-sip"
|
|
||||||
]
|
|
||||||
|
|
||||||
for package in packages_to_try:
|
|
||||||
print(f"Trying to install {package}...")
|
|
||||||
if install_package(package):
|
|
||||||
print(f"✅ Successfully installed {package}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"❌ Failed to install {package}")
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def check_pyqt5():
|
|
||||||
"""Check if PyQt5 is available"""
|
|
||||||
try:
|
|
||||||
import PyQt5
|
|
||||||
print("✅ PyQt5 is already installed")
|
|
||||||
return True
|
|
||||||
except ImportError:
|
|
||||||
print("❌ PyQt5 not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main installation function"""
|
|
||||||
print("AccountForger - Dependency Installer")
|
|
||||||
print("=" * 40)
|
|
||||||
|
|
||||||
# Check Python version
|
|
||||||
python_version = sys.version_info
|
|
||||||
if python_version < (3, 7):
|
|
||||||
print(f"❌ Python 3.7+ required, found {python_version.major}.{python_version.minor}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"✅ Python {python_version.major}.{python_version.minor} detected")
|
|
||||||
|
|
||||||
# Check/install PyQt5
|
|
||||||
if not check_pyqt5():
|
|
||||||
print("\nInstalling PyQt5...")
|
|
||||||
if not install_pyqt5():
|
|
||||||
print("\n⚠️ PyQt5 installation failed!")
|
|
||||||
print("Manual installation options:")
|
|
||||||
print("1. pip install PyQt5")
|
|
||||||
print("2. conda install pyqt (if using Anaconda)")
|
|
||||||
print("3. Use system package manager (Linux)")
|
|
||||||
print("\nAccountForger will still work with limited functionality")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Install other requirements
|
|
||||||
other_packages = [
|
|
||||||
"requests",
|
|
||||||
"selenium",
|
|
||||||
"playwright",
|
|
||||||
"beautifulsoup4",
|
|
||||||
"lxml"
|
|
||||||
]
|
|
||||||
|
|
||||||
print("\nInstalling other dependencies...")
|
|
||||||
failed_packages = []
|
|
||||||
|
|
||||||
for package in other_packages:
|
|
||||||
print(f"Installing {package}...")
|
|
||||||
if install_package(package):
|
|
||||||
print(f"✅ {package} installed")
|
|
||||||
else:
|
|
||||||
print(f"❌ {package} failed")
|
|
||||||
failed_packages.append(package)
|
|
||||||
|
|
||||||
if failed_packages:
|
|
||||||
print(f"\n⚠️ Some packages failed to install: {failed_packages}")
|
|
||||||
print("Try installing them manually with:")
|
|
||||||
for package in failed_packages:
|
|
||||||
print(f" pip install {package}")
|
|
||||||
|
|
||||||
print("\n🚀 Installation complete!")
|
|
||||||
print("You can now run: python main.py")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
success = main()
|
|
||||||
sys.exit(0 if success else 1)
|
|
||||||
20
resources/icons/aegissight-logo-dark.svg
Normale Datei
20
resources/icons/aegissight-logo-dark.svg
Normale Datei
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1231 385" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g id="Logo-Schrift" serif:id="Logo+Schrift" transform="matrix(1.075028,0,0,0.631883,0,-494.958122)">
|
||||||
|
<rect x="0" y="783.307" width="1144.799" height="607.865" style="fill:none;"/>
|
||||||
|
<g transform="matrix(1.521788,0,0,2.589032,114.348241,790.79828)">
|
||||||
|
<g transform="matrix(104.166667,0,0,104.166667,636.063765,201.582585)">
|
||||||
|
</g>
|
||||||
|
<text x="120.491px" y="201.583px" style="font-family:'Lato-Bold', 'Lato', sans-serif;font-weight:700;font-size:104.167px;fill:#FFFFFF;">Aegis<tspan x="375.595px 432.522px " y="201.583px 201.583px ">Si</tspan>gh<tspan x="572.001px " y="201.583px ">t</tspan></text>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.738271,0,0,1.256027,162.661946,1094.789099)">
|
||||||
|
<g transform="matrix(1,0,0,1,-200,-248.484848)">
|
||||||
|
<g id="svgg">
|
||||||
|
<path id="rechts" d="M212.575,238.576C212.984,240.67 223.048,241.002 270.154,240.533C349.694,239.739 344.481,239.31 346.236,243.942C347.823,248.13 347.264,250.927 338.778,272.292C333.041,286.737 321.692,301.671 304.569,327.057C262.704,389.124 258.243,380.556 257.465,379.844C256.548,379.007 256.695,378.153 256.7,377.409C256.827,359.293 254.573,273.452 254.549,270.937C254.525,268.422 254.116,268.891 229.156,268.982C211.282,269.047 211.756,268.669 211.925,271.847C211.971,272.701 212.094,316.69 212.2,369.6C212.306,422.51 212.487,468.568 212.604,469.063C213.014,470.81 224.336,462 224.6,462C224.864,462 237.107,453.265 241.4,450.384C242.5,449.646 244.343,448.313 245.496,447.421C246.648,446.53 248.865,444.9 250.421,443.8C251.978,442.7 255.169,440.115 257.513,438.055C259.857,435.996 262.771,433.605 263.988,432.743C267.489,430.261 269.974,428.216 270.637,427.269C270.973,426.789 271.767,426.127 272.4,425.8C273.034,425.472 273.862,424.68 274.24,424.04C274.618,423.399 275.574,422.512 276.364,422.067C277.741,421.292 287.002,412.973 290.077,409.749C290.89,408.897 293.68,406.009 296.277,403.331C303.179,396.216 308.766,389.886 310.684,387.009C311.611,385.619 312.782,384.149 313.286,383.741C313.791,383.334 314.523,382.55 314.913,382C315.304,381.45 316.113,380.353 316.711,379.562C317.31,378.771 318.552,377.132 319.471,375.919C320.389,374.706 321.709,373.103 322.403,372.357C324.097,370.534 325.597,368.32 327.217,365.252C327.957,363.85 329.057,362.338 329.66,361.892C330.264,361.446 331.622,359.655 332.679,357.912C333.735,356.168 335.453,353.696 336.496,352.417C337.539,351.139 338.935,348.947 339.599,347.546C341.424,343.695 344.598,338.004 345.689,336.626C347.172,334.754 348.692,331.944 348.986,330.528C349.132,329.828 349.51,329.041 349.826,328.779C350.142,328.517 350.4,328.069 350.4,327.784C350.4,327.499 351.048,326.045 351.84,324.552C352.632,323.059 353.784,320.479 354.401,318.819C355.017,317.159 356.416,314.072 357.509,311.96C358.602,309.848 359.894,306.968 360.38,305.56C360.866,304.152 361.593,302.46 361.995,301.8C362.398,301.14 362.941,299.795 363.203,298.812C363.464,297.828 363.931,296.663 364.239,296.223C364.548,295.782 364.8,295.078 364.8,294.658C364.8,293.56 367.089,287.051 368.23,284.904C368.764,283.901 369.201,282.793 369.202,282.44C369.204,282.088 369.46,281.312 369.771,280.715C370.082,280.118 370.552,278.588 370.814,277.315C371.076,276.042 371.715,273.867 372.234,272.482C372.753,271.097 373.442,268.667 373.765,267.082C374.657,262.705 375.074,261.226 376.185,258.503C376.746,257.13 377.395,254.61 377.628,252.903C377.861,251.196 386.4,207.294 386.4,202.415C386.4,200.114 384.943,198.138 382.973,197.769C382.197,197.623 390.698,196.027 262.4,197.136L256.297,196.493C254.923,195.188 254.409,193.392 254.634,190.691C255.021,186.052 255.075,102.153 254.699,90.2C254.256,76.132 254.359,75.232 256.566,73.785C257.5,73.174 257.724,73.166 258.9,73.706C259.615,74.035 343.437,105.997 345.2,108.641L346.2,110.142L346.246,163.984L347.17,164.968L348.095,165.953L367.317,165.835L386.539,165.718L387.711,164.406L388.883,163.095L388.646,155.847C388.515,151.861 388.304,143.29 388.176,136.8C387.97,126.347 389.116,102.223 388.883,92.984C388.587,81.212 385.041,79.623 381.162,77.313C378.036,75.451 212.403,10.83 212.49,12.505" style="fill:rgb(200,168,81);"/>
|
||||||
|
<path id="links" d="M31.8,72.797C19.193,77.854 16.869,77.149 16.354,86.093C16.177,89.171 13.694,109.47 13.373,112C11.292,128.389 11.075,175.356 12.999,192.8C13.326,195.77 15.755,217.626 17.524,225.4C17.975,227.38 21.242,245.556 21.798,247.6C23.196,252.741 27.444,269.357 28.368,273C29.454,277.277 33.845,288.636 34.632,290.326C35.42,292.017 39.017,301.259 39.364,301.931C39.973,303.107 41.279,306.405 42.799,310.6C43.879,313.58 46.904,319.091 47.546,320.62C48.78,323.561 51.339,328.992 51.965,330C52.17,330.33 53.466,332.67 54.845,335.2C56.223,337.73 65.855,353.259 67.765,356.052C72.504,362.981 75.544,366.754 76.46,368.119C78.119,370.593 79.488,372.185 85.821,379C87.66,380.98 89.758,383.356 90.483,384.279C92.003,386.218 92.035,386.23 93.151,385.3C94.267,384.37 94.041,384.013 94.036,382.593C94.015,376.905 94.025,351.182 94.025,351.182C94.062,315.081 94.745,313.16 93.752,308.626C92.302,301.997 88.001,300.043 80.439,284.793C71.474,266.714 65.169,255.803 62.016,248.485C61.011,246.153 59.289,240.91 61.521,240.882C65.215,240.836 143.575,240.107 144.382,240.673C145.808,241.671 146.494,243.516 146.346,245.959C146.058,250.736 146.217,438.282 146.511,439.663C146.825,441.137 153.946,447.096 162.193,452.924C177.223,463.547 187.111,469.578 187.956,468.458C189.091,466.954 188.058,10.288 188.006,12.482M146.001,134.292C145.999,164.821 146.043,190.718 146.099,191.84C146.336,196.617 147.019,196.45 127.622,196.354C106.312,196.249 58.054,196.89 58.054,196.89L57.06,195.896C55.315,194.152 55.678,132.49 55.766,126C56.004,108.467 56.656,110.707 66.745,106.586C70.345,105.116 134.261,79.128 135.708,78.566C146.998,74.183 145.972,74.295 146.055,76.768" style="fill:#FFFFFF;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Nachher Breite: | Höhe: | Größe: 6.2 KiB |
20
resources/icons/aegissight-logo.svg
Normale Datei
20
resources/icons/aegissight-logo.svg
Normale Datei
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1231 385" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g id="Logo-Schrift" serif:id="Logo+Schrift" transform="matrix(1.075028,0,0,0.631883,0,-494.958122)">
|
||||||
|
<rect x="0" y="783.307" width="1144.799" height="607.865" style="fill:none;"/>
|
||||||
|
<g transform="matrix(1.521788,0,0,2.589032,114.348241,790.79828)">
|
||||||
|
<g transform="matrix(104.166667,0,0,104.166667,636.063765,201.582585)">
|
||||||
|
</g>
|
||||||
|
<text x="120.491px" y="201.583px" style="font-family:'Lato-Bold', 'Lato', sans-serif;font-weight:700;font-size:104.167px;fill:rgb(10,24,50);">Aegis<tspan x="375.595px 432.522px " y="201.583px 201.583px ">Si</tspan>gh<tspan x="572.001px " y="201.583px ">t</tspan></text>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.738271,0,0,1.256027,162.661946,1094.789099)">
|
||||||
|
<g transform="matrix(1,0,0,1,-200,-248.484848)">
|
||||||
|
<g id="svgg">
|
||||||
|
<path id="rechts" d="M212.575,238.576C212.984,240.67 223.048,241.002 270.154,240.533C349.694,239.739 344.481,239.31 346.236,243.942C347.823,248.13 347.264,250.927 338.778,272.292C333.041,286.737 321.692,301.671 304.569,327.057C262.704,389.124 258.243,380.556 257.465,379.844C256.548,379.007 256.695,378.153 256.7,377.409C256.827,359.293 254.573,273.452 254.549,270.937C254.525,268.422 254.116,268.891 229.156,268.982C211.282,269.047 211.756,268.669 211.925,271.847C211.971,272.701 212.094,316.69 212.2,369.6C212.306,422.51 212.487,468.568 212.604,469.063C213.014,470.81 224.336,462 224.6,462C224.864,462 237.107,453.265 241.4,450.384C242.5,449.646 244.343,448.313 245.496,447.421C246.648,446.53 248.865,444.9 250.421,443.8C251.978,442.7 255.169,440.115 257.513,438.055C259.857,435.996 262.771,433.605 263.988,432.743C267.489,430.261 269.974,428.216 270.637,427.269C270.973,426.789 271.767,426.127 272.4,425.8C273.034,425.472 273.862,424.68 274.24,424.04C274.618,423.399 275.574,422.512 276.364,422.067C277.741,421.292 287.002,412.973 290.077,409.749C290.89,408.897 293.68,406.009 296.277,403.331C303.179,396.216 308.766,389.886 310.684,387.009C311.611,385.619 312.782,384.149 313.286,383.741C313.791,383.334 314.523,382.55 314.913,382C315.304,381.45 316.113,380.353 316.711,379.562C317.31,378.771 318.552,377.132 319.471,375.919C320.389,374.706 321.709,373.103 322.403,372.357C324.097,370.534 325.597,368.32 327.217,365.252C327.957,363.85 329.057,362.338 329.66,361.892C330.264,361.446 331.622,359.655 332.679,357.912C333.735,356.168 335.453,353.696 336.496,352.417C337.539,351.139 338.935,348.947 339.599,347.546C341.424,343.695 344.598,338.004 345.689,336.626C347.172,334.754 348.692,331.944 348.986,330.528C349.132,329.828 349.51,329.041 349.826,328.779C350.142,328.517 350.4,328.069 350.4,327.784C350.4,327.499 351.048,326.045 351.84,324.552C352.632,323.059 353.784,320.479 354.401,318.819C355.017,317.159 356.416,314.072 357.509,311.96C358.602,309.848 359.894,306.968 360.38,305.56C360.866,304.152 361.593,302.46 361.995,301.8C362.398,301.14 362.941,299.795 363.203,298.812C363.464,297.828 363.931,296.663 364.239,296.223C364.548,295.782 364.8,295.078 364.8,294.658C364.8,293.56 367.089,287.051 368.23,284.904C368.764,283.901 369.201,282.793 369.202,282.44C369.204,282.088 369.46,281.312 369.771,280.715C370.082,280.118 370.552,278.588 370.814,277.315C371.076,276.042 371.715,273.867 372.234,272.482C372.753,271.097 373.442,268.667 373.765,267.082C374.657,262.705 375.074,261.226 376.185,258.503C376.746,257.13 377.395,254.61 377.628,252.903C377.861,251.196 386.4,207.294 386.4,202.415C386.4,200.114 384.943,198.138 382.973,197.769C382.197,197.623 390.698,196.027 262.4,197.136L256.297,196.493C254.923,195.188 254.409,193.392 254.634,190.691C255.021,186.052 255.075,102.153 254.699,90.2C254.256,76.132 254.359,75.232 256.566,73.785C257.5,73.174 257.724,73.166 258.9,73.706C259.615,74.035 343.437,105.997 345.2,108.641L346.2,110.142L346.246,163.984L347.17,164.968L348.095,165.953L367.317,165.835L386.539,165.718L387.711,164.406L388.883,163.095L388.646,155.847C388.515,151.861 388.304,143.29 388.176,136.8C387.97,126.347 389.116,102.223 388.883,92.984C388.587,81.212 385.041,79.623 381.162,77.313C378.036,75.451 212.403,10.83 212.49,12.505" style="fill:rgb(200,168,81);"/>
|
||||||
|
<path id="links" d="M31.8,72.797C19.193,77.854 16.869,77.149 16.354,86.093C16.177,89.171 13.694,109.47 13.373,112C11.292,128.389 11.075,175.356 12.999,192.8C13.326,195.77 15.755,217.626 17.524,225.4C17.975,227.38 21.242,245.556 21.798,247.6C23.196,252.741 27.444,269.357 28.368,273C29.454,277.277 33.845,288.636 34.632,290.326C35.42,292.017 39.017,301.259 39.364,301.931C39.973,303.107 41.279,306.405 42.799,310.6C43.879,313.58 46.904,319.091 47.546,320.62C48.78,323.561 51.339,328.992 51.965,330C52.17,330.33 53.466,332.67 54.845,335.2C56.223,337.73 65.855,353.259 67.765,356.052C72.504,362.981 75.544,366.754 76.46,368.119C78.119,370.593 79.488,372.185 85.821,379C87.66,380.98 89.758,383.356 90.483,384.279C92.003,386.218 92.035,386.23 93.151,385.3C94.267,384.37 94.041,384.013 94.036,382.593C94.015,376.905 94.025,351.182 94.025,351.182C94.062,315.081 94.745,313.16 93.752,308.626C92.302,301.997 88.001,300.043 80.439,284.793C71.474,266.714 65.169,255.803 62.016,248.485C61.011,246.153 59.289,240.91 61.521,240.882C65.215,240.836 143.575,240.107 144.382,240.673C145.808,241.671 146.494,243.516 146.346,245.959C146.058,250.736 146.217,438.282 146.511,439.663C146.825,441.137 153.946,447.096 162.193,452.924C177.223,463.547 187.111,469.578 187.956,468.458C189.091,466.954 188.058,10.288 188.006,12.482M146.001,134.292C145.999,164.821 146.043,190.718 146.099,191.84C146.336,196.617 147.019,196.45 127.622,196.354C106.312,196.249 58.054,196.89 58.054,196.89L57.06,195.896C55.315,194.152 55.678,132.49 55.766,126C56.004,108.467 56.656,110.707 66.745,106.586C70.345,105.116 134.261,79.128 135.708,78.566C146.998,74.183 145.972,74.295 146.055,76.768" style="fill:rgb(10,24,50);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Nachher Breite: | Höhe: | Größe: 6.2 KiB |
@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="450" height="100" viewBox="0 0 450 100" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap');
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Accurate shield matching original -->
|
|
||||||
<g id="shield-eye-accurate-dark">
|
|
||||||
<!-- Angular shield shape -->
|
|
||||||
<path d="M 35 30
|
|
||||||
L 65 30
|
|
||||||
L 75 40
|
|
||||||
L 75 80
|
|
||||||
L 50 115
|
|
||||||
L 25 80
|
|
||||||
L 25 40
|
|
||||||
L 35 30 Z"
|
|
||||||
fill="none"
|
|
||||||
stroke="#FFFFFF"
|
|
||||||
stroke-width="3.5"
|
|
||||||
stroke-linejoin="miter"/>
|
|
||||||
|
|
||||||
<!-- Eye centered in shield -->
|
|
||||||
<g transform="translate(50, 65)">
|
|
||||||
<!-- Almond/football shaped eye -->
|
|
||||||
<ellipse cx="0" cy="0" rx="24" ry="13"
|
|
||||||
fill="none"
|
|
||||||
stroke="#FFFFFF"
|
|
||||||
stroke-width="3.5"/>
|
|
||||||
|
|
||||||
<!-- Circular iris -->
|
|
||||||
<circle cx="0" cy="0" r="10"
|
|
||||||
fill="none"
|
|
||||||
stroke="#FFFFFF"
|
|
||||||
stroke-width="3.5"/>
|
|
||||||
|
|
||||||
<!-- Pupil -->
|
|
||||||
<circle cx="0" cy="0" r="4" fill="#FFFFFF"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Dark version for dark theme -->
|
|
||||||
<g transform="translate(20, 50)">
|
|
||||||
<!-- Shield centered vertically with text -->
|
|
||||||
<g transform="translate(0, -72.5)">
|
|
||||||
<use href="#shield-eye-accurate-dark"/>
|
|
||||||
</g>
|
|
||||||
<!-- Text aligned with shield center -->
|
|
||||||
<text x="90" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="#FFFFFF">IntelSight</text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Vorher Breite: | Höhe: | Größe: 1.6 KiB |
@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="450" height="100" viewBox="0 0 450 100" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap');
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Accurate shield matching original -->
|
|
||||||
<g id="shield-eye-accurate">
|
|
||||||
<!-- Angular shield shape -->
|
|
||||||
<path d="M 35 30
|
|
||||||
L 65 30
|
|
||||||
L 75 40
|
|
||||||
L 75 80
|
|
||||||
L 50 115
|
|
||||||
L 25 80
|
|
||||||
L 25 40
|
|
||||||
L 35 30 Z"
|
|
||||||
fill="none"
|
|
||||||
stroke="#232D53"
|
|
||||||
stroke-width="3.5"
|
|
||||||
stroke-linejoin="miter"/>
|
|
||||||
|
|
||||||
<!-- Eye centered in shield -->
|
|
||||||
<g transform="translate(50, 65)">
|
|
||||||
<!-- Almond/football shaped eye -->
|
|
||||||
<ellipse cx="0" cy="0" rx="24" ry="13"
|
|
||||||
fill="none"
|
|
||||||
stroke="#232D53"
|
|
||||||
stroke-width="3.5"/>
|
|
||||||
|
|
||||||
<!-- Circular iris -->
|
|
||||||
<circle cx="0" cy="0" r="10"
|
|
||||||
fill="none"
|
|
||||||
stroke="#232D53"
|
|
||||||
stroke-width="3.5"/>
|
|
||||||
|
|
||||||
<!-- Pupil -->
|
|
||||||
<circle cx="0" cy="0" r="4" fill="#232D53"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Light version with IntelSight corporate colors -->
|
|
||||||
<g transform="translate(20, 50)">
|
|
||||||
<!-- Shield centered vertically with text -->
|
|
||||||
<g transform="translate(0, -72.5)">
|
|
||||||
<use href="#shield-eye-accurate"/>
|
|
||||||
</g>
|
|
||||||
<!-- Text aligned with shield center -->
|
|
||||||
<text x="90" y="5" font-family="'Poppins', sans-serif" font-size="46" font-weight="600" fill="#232D53">IntelSight</text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Vorher Breite: | Höhe: | Größe: 1.6 KiB |
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* AccountForger Dark Mode Theme
|
* AccountForger Dark Mode Theme
|
||||||
* Based on IntelSight Corporate Design System
|
* Based on AegisSight Corporate Design System
|
||||||
* Color Palette from CORPORATE_DESIGN_DARK_MODE.md
|
* Color Palette from CORPORATE_DESIGN_DARK_MODE.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* AccountForger Light Mode Theme
|
* AccountForger Light Mode Theme
|
||||||
* Based on IntelSight Corporate Design System
|
* Based on AegisSight Corporate Design System
|
||||||
* Adapted from Dark Mode guidelines for Light Mode usage
|
* Adapted from Dark Mode guidelines for Light Mode usage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@ -327,15 +327,140 @@ class FacebookUIHelper:
|
|||||||
def check_element_exists(self, selector: str, timeout: int = 1000) -> bool:
|
def check_element_exists(self, selector: str, timeout: int = 1000) -> bool:
|
||||||
"""
|
"""
|
||||||
Prüft ob ein Element existiert.
|
Prüft ob ein Element existiert.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
selector: CSS-Selektor
|
selector: CSS-Selektor
|
||||||
timeout: Maximale Wartezeit in ms
|
timeout: Maximale Wartezeit in ms
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True wenn Element existiert
|
bool: True wenn Element existiert
|
||||||
"""
|
"""
|
||||||
if not self._ensure_browser():
|
if not self._ensure_browser():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self.automation.browser.is_element_visible(selector, timeout=timeout)
|
return self.automation.browser.is_element_visible(selector, timeout=timeout)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# ANTI-DETECTION: Tab-Navigation Methoden
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
def navigate_to_next_field(self, use_tab: bool = None) -> bool:
|
||||||
|
"""
|
||||||
|
Navigiert zum nächsten Feld, entweder per Tab oder Maus-Klick.
|
||||||
|
|
||||||
|
Diese Methode simuliert menschliches Verhalten durch zufällige
|
||||||
|
Verwendung von Tab-Navigation (wie viele echte Benutzer es tun).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
use_tab: Explizit Tab verwenden. Wenn None, 50% Chance für Tab.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True bei Erfolg
|
||||||
|
"""
|
||||||
|
if not self._ensure_browser():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Entscheide zufällig ob Tab verwendet wird (50% Chance)
|
||||||
|
if use_tab is None:
|
||||||
|
use_tab = random.random() < 0.5
|
||||||
|
|
||||||
|
try:
|
||||||
|
if use_tab:
|
||||||
|
logger.debug("Verwende Tab-Navigation zum nächsten Feld")
|
||||||
|
self.automation.browser.page.keyboard.press("Tab")
|
||||||
|
|
||||||
|
# Variable Wartezeit nach Tab
|
||||||
|
time.sleep(random.uniform(0.15, 0.35))
|
||||||
|
|
||||||
|
# Gelegentlich Tab + Shift-Tab (versehentlich zu weit gegangen)
|
||||||
|
if random.random() < 0.08:
|
||||||
|
logger.debug("Simuliere Tab-Korrektur (zu weit)")
|
||||||
|
self.automation.browser.page.keyboard.press("Tab")
|
||||||
|
time.sleep(random.uniform(0.2, 0.4))
|
||||||
|
self.automation.browser.page.keyboard.press("Shift+Tab")
|
||||||
|
time.sleep(random.uniform(0.15, 0.3))
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.debug("Tab-Navigation nicht verwendet (Maus wird später genutzt)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei Tab-Navigation: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fill_field_with_tab_navigation(self, selector: str, value: str,
|
||||||
|
use_tab_navigation: bool = None,
|
||||||
|
error_rate: float = 0.15) -> bool:
|
||||||
|
"""
|
||||||
|
Füllt ein Feld aus mit optionaler Tab-Navigation.
|
||||||
|
|
||||||
|
Diese Methode kombiniert Tab-Navigation mit menschenähnlichem Tippen
|
||||||
|
für ein realistischeres Verhalten.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
selector: CSS-Selektor des Feldes
|
||||||
|
value: Einzugebender Wert
|
||||||
|
use_tab_navigation: Ob Tab verwendet werden soll (None = 50% Chance)
|
||||||
|
error_rate: Wahrscheinlichkeit für Tippfehler (10-20%)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True bei Erfolg
|
||||||
|
"""
|
||||||
|
if not self._ensure_browser():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Entscheide Navigationsmethode
|
||||||
|
if use_tab_navigation is None:
|
||||||
|
use_tab_navigation = random.random() < 0.5
|
||||||
|
|
||||||
|
if use_tab_navigation:
|
||||||
|
# Tab-Navigation verwenden
|
||||||
|
logger.debug(f"Fülle Feld {selector} mit Tab-Navigation")
|
||||||
|
self.automation.browser.page.keyboard.press("Tab")
|
||||||
|
time.sleep(random.uniform(0.15, 0.35))
|
||||||
|
else:
|
||||||
|
# Maus-Klick auf Feld
|
||||||
|
logger.debug(f"Fülle Feld {selector} mit Maus-Klick")
|
||||||
|
if not self.automation.browser.click_element(selector):
|
||||||
|
logger.warning(f"Konnte Feld nicht anklicken: {selector}")
|
||||||
|
return False
|
||||||
|
time.sleep(random.uniform(0.2, 0.5))
|
||||||
|
|
||||||
|
# Anti-Detection Delay vor Eingabe
|
||||||
|
if hasattr(self.automation, 'human_behavior'):
|
||||||
|
self.automation.human_behavior.anti_detection_delay("field_focus")
|
||||||
|
|
||||||
|
# Text mit Tippfehlern eingeben
|
||||||
|
for i, char in enumerate(value):
|
||||||
|
# Tippfehler simulieren
|
||||||
|
if random.random() < error_rate:
|
||||||
|
# Falsches Zeichen
|
||||||
|
wrong_char = random.choice('abcdefghijklmnopqrstuvwxyz0123456789')
|
||||||
|
self.automation.browser.page.keyboard.type(wrong_char)
|
||||||
|
time.sleep(random.uniform(0.1, 0.3))
|
||||||
|
|
||||||
|
# Korrektur mit Backspace
|
||||||
|
self.automation.browser.page.keyboard.press("Backspace")
|
||||||
|
time.sleep(random.uniform(0.08, 0.2))
|
||||||
|
|
||||||
|
# Korrektes Zeichen
|
||||||
|
self.automation.browser.page.keyboard.type(char)
|
||||||
|
|
||||||
|
# Variable Verzögerung
|
||||||
|
delay_ms = random.randint(50, 150)
|
||||||
|
if char in ' .,!?':
|
||||||
|
delay_ms *= random.uniform(1.3, 2.0)
|
||||||
|
time.sleep(delay_ms / 1000)
|
||||||
|
|
||||||
|
# Gelegentlich längere Pause (Denken)
|
||||||
|
if random.random() < 0.05:
|
||||||
|
time.sleep(random.uniform(0.3, 0.8))
|
||||||
|
|
||||||
|
logger.info(f"Feld {selector} erfolgreich ausgefüllt")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Ausfüllen mit Tab-Navigation: {e}")
|
||||||
|
return False
|
||||||
@ -258,87 +258,199 @@ class InstagramRegistration:
|
|||||||
def _navigate_to_signup_page(self) -> bool:
|
def _navigate_to_signup_page(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Navigiert zur Instagram-Registrierungsseite.
|
Navigiert zur Instagram-Registrierungsseite.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True bei Erfolg, False bei Fehler
|
bool: True bei Erfolg, False bei Fehler
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"Navigiere zur Registrierungsseite: {InstagramSelectors.SIGNUP_URL}")
|
||||||
|
|
||||||
# Zur Registrierungsseite navigieren
|
# Zur Registrierungsseite navigieren
|
||||||
self.automation.browser.navigate_to(InstagramSelectors.SIGNUP_URL)
|
self.automation.browser.navigate_to(InstagramSelectors.SIGNUP_URL)
|
||||||
|
|
||||||
# Warten, bis die Seite geladen ist
|
# Warten, bis die Seite geladen ist
|
||||||
|
logger.debug("Warte auf Seitenladung...")
|
||||||
self.automation.human_behavior.wait_for_page_load()
|
self.automation.human_behavior.wait_for_page_load()
|
||||||
|
|
||||||
|
# Aktuelle URL loggen (für Debugging bei Weiterleitungen)
|
||||||
|
try:
|
||||||
|
current_url = self.automation.browser.page.url
|
||||||
|
logger.info(f"Aktuelle URL nach Navigation: {current_url}")
|
||||||
|
|
||||||
|
# Prüfen ob wir umgeleitet wurden
|
||||||
|
if "login" in current_url.lower() and "signup" not in current_url.lower():
|
||||||
|
logger.warning("Wurde zur Login-Seite umgeleitet - versuche erneut zur Registrierung zu navigieren")
|
||||||
|
self.automation.browser.navigate_to(InstagramSelectors.SIGNUP_URL)
|
||||||
|
self.automation.human_behavior.wait_for_page_load()
|
||||||
|
current_url = self.automation.browser.page.url
|
||||||
|
logger.info(f"URL nach erneutem Navigieren: {current_url}")
|
||||||
|
except Exception as url_err:
|
||||||
|
logger.debug(f"Konnte aktuelle URL nicht abrufen: {url_err}")
|
||||||
|
|
||||||
# SOFORT Cookie-Banner behandeln BEVOR weitere Aktionen (TIMING-FIX)
|
# SOFORT Cookie-Banner behandeln BEVOR weitere Aktionen (TIMING-FIX)
|
||||||
logger.info("Behandle Cookie-Banner SOFORT nach Navigation für korrekte Session-Cookies")
|
logger.info("Behandle Cookie-Banner SOFORT nach Navigation für korrekte Session-Cookies")
|
||||||
cookie_handled = self._handle_cookie_banner()
|
cookie_handled = self._handle_cookie_banner()
|
||||||
if not cookie_handled:
|
if not cookie_handled:
|
||||||
logger.warning("Cookie-Banner konnte nicht behandelt werden - Session könnte beeinträchtigt sein")
|
logger.warning("Cookie-Banner konnte nicht behandelt werden - Session könnte beeinträchtigt sein")
|
||||||
|
|
||||||
# Kurz warten damit Cookies gesetzt werden können
|
# Kurz warten damit Cookies gesetzt werden können
|
||||||
|
logger.debug("Warte nach Cookie-Behandlung...")
|
||||||
self.automation.human_behavior.random_delay(1.0, 2.0)
|
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||||
|
|
||||||
# Screenshot erstellen
|
# Screenshot erstellen
|
||||||
self.automation._take_screenshot("signup_page")
|
self.automation._take_screenshot("signup_page")
|
||||||
|
|
||||||
# Prüfen, ob Registrierungsformular sichtbar ist
|
# Prüfen, ob Registrierungsformular sichtbar ist
|
||||||
|
logger.debug(f"Suche nach Registrierungsformular mit Selektor: {InstagramSelectors.EMAIL_PHONE_FIELD}")
|
||||||
|
|
||||||
if not self.automation.browser.is_element_visible(InstagramSelectors.EMAIL_PHONE_FIELD, timeout=5000):
|
if not self.automation.browser.is_element_visible(InstagramSelectors.EMAIL_PHONE_FIELD, timeout=5000):
|
||||||
logger.warning("Registrierungsformular nicht sichtbar")
|
logger.warning(f"Hauptselektor {InstagramSelectors.EMAIL_PHONE_FIELD} nicht gefunden - versuche Alternativen")
|
||||||
|
|
||||||
|
# Alternative Selektoren versuchen
|
||||||
|
alt_selectors = [
|
||||||
|
InstagramSelectors.ALT_EMAIL_FIELD,
|
||||||
|
"input[type='email']",
|
||||||
|
"input[type='text'][aria-label*='mail']",
|
||||||
|
"input[type='text'][aria-label*='Mail']",
|
||||||
|
"input[type='text'][placeholder*='mail']",
|
||||||
|
"input[name='email']",
|
||||||
|
"//input[contains(@aria-label, 'E-Mail') or contains(@aria-label, 'Email')]"
|
||||||
|
]
|
||||||
|
|
||||||
|
for alt_selector in alt_selectors:
|
||||||
|
logger.debug(f"Versuche alternativen Selektor: {alt_selector}")
|
||||||
|
if self.automation.browser.is_element_visible(alt_selector, timeout=1000):
|
||||||
|
logger.info(f"Formular mit alternativem Selektor gefunden: {alt_selector}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Debug: Logge sichtbare Elemente auf der Seite
|
||||||
|
try:
|
||||||
|
page_title = self.automation.browser.page.title()
|
||||||
|
logger.debug(f"Seitentitel: {page_title}")
|
||||||
|
|
||||||
|
# Prüfe auf bekannte Blockade-Elemente
|
||||||
|
if self.automation.browser.is_element_visible("div[role='dialog']", timeout=1000):
|
||||||
|
logger.warning("Ein Dialog ist noch sichtbar - könnte Cookie-Dialog oder anderer Modal sein")
|
||||||
|
|
||||||
|
# Screenshot vom aktuellen Zustand
|
||||||
|
self.automation._take_screenshot("signup_page_blocked")
|
||||||
|
|
||||||
|
# Prüfe auf Fehlermeldungen
|
||||||
|
if self.automation.browser.is_element_visible("p[class*='error'], div[class*='error']", timeout=1000):
|
||||||
|
logger.warning("Fehlermeldung auf der Seite erkannt")
|
||||||
|
|
||||||
|
# Prüfe ob wir auf einer anderen Seite sind
|
||||||
|
current_url = self.automation.browser.page.url
|
||||||
|
if "challenge" in current_url.lower():
|
||||||
|
logger.error("CHECKPOINT/CHALLENGE erkannt - Instagram verlangt Verifizierung!")
|
||||||
|
elif "suspended" in current_url.lower():
|
||||||
|
logger.error("Account wurde suspendiert oder IP blockiert!")
|
||||||
|
|
||||||
|
except Exception as debug_err:
|
||||||
|
logger.debug(f"Debug-Informationen konnten nicht abgerufen werden: {debug_err}")
|
||||||
|
|
||||||
|
logger.warning("Registrierungsformular nicht sichtbar - alle Selektoren fehlgeschlagen")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info("Erfolgreich zur Registrierungsseite navigiert und Cookies akzeptiert")
|
logger.info("Erfolgreich zur Registrierungsseite navigiert und Cookies akzeptiert")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Navigieren zur Registrierungsseite: {e}")
|
logger.error(f"Fehler beim Navigieren zur Registrierungsseite: {e}", exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _handle_cookie_banner(self) -> bool:
|
def _handle_cookie_banner(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Behandelt den Cookie-Banner, falls angezeigt.
|
Behandelt den Cookie-Banner, falls angezeigt.
|
||||||
Akzeptiert IMMER Cookies für vollständiges Session-Management bei der Registrierung.
|
Akzeptiert IMMER Cookies für vollständiges Session-Management bei der Registrierung.
|
||||||
|
|
||||||
|
ANTI-DETECTION: Wartet 3-8 Sekunden bevor der Cookie-Dialog geklickt wird,
|
||||||
|
um menschliches Leseverhalten zu simulieren.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
bool: True wenn Banner behandelt wurde oder nicht existiert, False bei Fehler
|
||||||
"""
|
"""
|
||||||
# Cookie-Dialog-Erkennung
|
# Cookie-Dialog-Erkennung
|
||||||
if self.automation.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=2000):
|
logger.debug(f"Prüfe auf Cookie-Dialog mit Selektor: {InstagramSelectors.COOKIE_DIALOG}")
|
||||||
logger.info("Cookie-Banner erkannt - akzeptiere alle Cookies für Session-Management")
|
|
||||||
|
if self.automation.browser.is_element_visible(InstagramSelectors.COOKIE_DIALOG, timeout=3000):
|
||||||
|
logger.info("Cookie-Banner erkannt - simuliere Lesen bevor geklickt wird")
|
||||||
|
|
||||||
|
# ANTI-DETECTION: Lese-Pause bevor Cookie-Dialog geklickt wird (3-8 Sekunden)
|
||||||
|
# Echte Menschen lesen den Cookie-Text bevor sie klicken
|
||||||
|
logger.debug("Starte Anti-Detection Lese-Pause für Cookie-Banner...")
|
||||||
|
self.automation.human_behavior.anti_detection_delay("cookie_reading")
|
||||||
|
logger.debug("Anti-Detection Lese-Pause abgeschlossen")
|
||||||
|
|
||||||
|
# Gelegentlich etwas scrollen um "mehr zu lesen" (30% Chance)
|
||||||
|
if random.random() < 0.3:
|
||||||
|
try:
|
||||||
|
logger.debug("Simuliere Scrollen im Cookie-Dialog...")
|
||||||
|
self.automation.browser.page.evaluate("window.scrollBy(0, 50)")
|
||||||
|
time.sleep(random.uniform(0.8, 1.5))
|
||||||
|
self.automation.browser.page.evaluate("window.scrollBy(0, -50)")
|
||||||
|
time.sleep(random.uniform(0.3, 0.6))
|
||||||
|
except Exception as scroll_err:
|
||||||
|
logger.debug(f"Cookie-Dialog Scroll übersprungen: {scroll_err}")
|
||||||
|
|
||||||
|
logger.info("Klicke Cookie-Banner - akzeptiere alle Cookies für Session-Management")
|
||||||
|
|
||||||
# Akzeptieren-Button suchen und klicken (PRIMÄR für Registrierung)
|
# Akzeptieren-Button suchen und klicken (PRIMÄR für Registrierung)
|
||||||
accept_success = self.automation.ui_helper.click_button_fuzzy(
|
accept_success = self.automation.ui_helper.click_button_fuzzy(
|
||||||
InstagramSelectors.get_button_texts("accept_cookies"),
|
InstagramSelectors.get_button_texts("accept_cookies"),
|
||||||
InstagramSelectors.COOKIE_ACCEPT_BUTTON
|
InstagramSelectors.COOKIE_ACCEPT_BUTTON
|
||||||
)
|
)
|
||||||
|
|
||||||
if accept_success:
|
if accept_success:
|
||||||
logger.info("Cookie-Banner erfolgreich akzeptiert - Session-Cookies werden gespeichert")
|
logger.info("Cookie-Banner erfolgreich akzeptiert - Session-Cookies werden gespeichert")
|
||||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
|
||||||
|
# WICHTIG: Nach Cookie-Klick warten bis Seite stabil ist
|
||||||
|
logger.debug("Warte auf Seitenstabilität nach Cookie-Akzeptierung...")
|
||||||
|
time.sleep(2.0) # Feste Wartezeit für Seiten-Reload
|
||||||
|
|
||||||
|
# Warte auf Network-Idle
|
||||||
|
try:
|
||||||
|
self.automation.browser.page.wait_for_load_state("networkidle", timeout=10000)
|
||||||
|
logger.debug("Seite ist nach Cookie-Akzeptierung stabil (networkidle)")
|
||||||
|
except Exception as wait_err:
|
||||||
|
logger.warning(f"Timeout beim Warten auf networkidle: {wait_err}")
|
||||||
|
|
||||||
|
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.warning("Konnte Cookie-Banner nicht akzeptieren, versuche alternativen Akzeptieren-Button")
|
logger.warning("Konnte Cookie-Banner nicht akzeptieren, versuche alternativen Akzeptieren-Button")
|
||||||
|
|
||||||
# Alternative Akzeptieren-Selektoren versuchen
|
# Alternative Akzeptieren-Selektoren versuchen
|
||||||
alternative_accept_selectors = [
|
alternative_accept_selectors = [
|
||||||
"//button[contains(text(), 'Alle akzeptieren')]",
|
"//button[contains(text(), 'Alle akzeptieren')]",
|
||||||
"//button[contains(text(), 'Accept All')]",
|
"//button[contains(text(), 'Alle Cookies erlauben')]",
|
||||||
|
"//button[contains(text(), 'Accept All')]",
|
||||||
"//button[contains(text(), 'Zulassen')]",
|
"//button[contains(text(), 'Zulassen')]",
|
||||||
"//button[contains(text(), 'Allow All')]",
|
"//button[contains(text(), 'Allow All')]",
|
||||||
"//button[contains(@aria-label, 'Accept')]",
|
"//button[contains(@aria-label, 'Accept')]",
|
||||||
"[data-testid='accept-all-button']"
|
"[data-testid='accept-all-button']"
|
||||||
]
|
]
|
||||||
|
|
||||||
for selector in alternative_accept_selectors:
|
for selector in alternative_accept_selectors:
|
||||||
|
logger.debug(f"Versuche alternativen Cookie-Selektor: {selector}")
|
||||||
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||||
if self.automation.browser.click_element(selector):
|
if self.automation.browser.click_element(selector):
|
||||||
logger.info("Cookie-Banner mit alternativem Selector akzeptiert")
|
logger.info(f"Cookie-Banner mit alternativem Selector akzeptiert: {selector}")
|
||||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
|
||||||
|
# Nach Cookie-Klick warten
|
||||||
|
time.sleep(2.0)
|
||||||
|
try:
|
||||||
|
self.automation.browser.page.wait_for_load_state("networkidle", timeout=10000)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.automation.human_behavior.random_delay(1.0, 2.0)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.error("Konnte Cookie-Banner nicht akzeptieren - Session-Management könnte beeinträchtigt sein")
|
logger.error("Konnte Cookie-Banner nicht akzeptieren - Session-Management könnte beeinträchtigt sein")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.debug("Kein Cookie-Banner erkannt")
|
logger.debug("Kein Cookie-Banner erkannt - fahre fort")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _select_registration_method(self, method: str) -> bool:
|
def _select_registration_method(self, method: str) -> bool:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ Instagram-UI-Helper - Hilfsmethoden für die Interaktion mit der Instagram-UI
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Dict, List, Any, Optional, Tuple, Union, Callable
|
from typing import Dict, List, Any, Optional, Tuple, Union, Callable
|
||||||
@ -786,38 +787,165 @@ class InstagramUIHelper:
|
|||||||
def wait_for_page_load(self, timeout: int = 30000, check_interval: int = 500) -> bool:
|
def wait_for_page_load(self, timeout: int = 30000, check_interval: int = 500) -> bool:
|
||||||
"""
|
"""
|
||||||
Wartet, bis die Seite vollständig geladen ist.
|
Wartet, bis die Seite vollständig geladen ist.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout: Zeitlimit in Millisekunden
|
timeout: Zeitlimit in Millisekunden
|
||||||
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
check_interval: Intervall zwischen den Prüfungen in Millisekunden
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True wenn die Seite geladen wurde, False bei Zeitüberschreitung
|
bool: True wenn die Seite geladen wurde, False bei Zeitüberschreitung
|
||||||
"""
|
"""
|
||||||
if not self._ensure_browser():
|
if not self._ensure_browser():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Warten auf Netzwerk-Idle
|
# Warten auf Netzwerk-Idle
|
||||||
self.automation.browser.page.wait_for_load_state("networkidle", timeout=timeout)
|
self.automation.browser.page.wait_for_load_state("networkidle", timeout=timeout)
|
||||||
|
|
||||||
# Zusätzlich auf das Verschwinden der Ladeindikatoren warten
|
# Zusätzlich auf das Verschwinden der Ladeindikatoren warten
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
end_time = start_time + (timeout / 1000)
|
end_time = start_time + (timeout / 1000)
|
||||||
|
|
||||||
while time.time() < end_time:
|
while time.time() < end_time:
|
||||||
if not self.is_page_loading():
|
if not self.is_page_loading():
|
||||||
# Noch eine kurze Pause für Animationen
|
# Noch eine kurze Pause für Animationen
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
logger.info("Seite vollständig geladen")
|
logger.info("Seite vollständig geladen")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Kurze Pause vor der nächsten Prüfung
|
# Kurze Pause vor der nächsten Prüfung
|
||||||
time.sleep(check_interval / 1000)
|
time.sleep(check_interval / 1000)
|
||||||
|
|
||||||
logger.warning("Zeitüberschreitung beim Warten auf das Laden der Seite")
|
logger.warning("Zeitüberschreitung beim Warten auf das Laden der Seite")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Warten auf das Laden der Seite: {e}")
|
logger.error(f"Fehler beim Warten auf das Laden der Seite: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# ANTI-DETECTION: Tab-Navigation Methoden
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
def navigate_to_next_field(self, use_tab: bool = None) -> bool:
|
||||||
|
"""
|
||||||
|
Navigiert zum nächsten Feld, entweder per Tab oder Maus-Klick.
|
||||||
|
|
||||||
|
Diese Methode simuliert menschliches Verhalten durch zufällige
|
||||||
|
Verwendung von Tab-Navigation (wie viele echte Benutzer es tun).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
use_tab: Explizit Tab verwenden. Wenn None, 50% Chance für Tab.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True bei Erfolg
|
||||||
|
"""
|
||||||
|
if not self._ensure_browser():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Entscheide zufällig ob Tab verwendet wird (50% Chance)
|
||||||
|
if use_tab is None:
|
||||||
|
use_tab = random.random() < 0.5
|
||||||
|
|
||||||
|
try:
|
||||||
|
if use_tab:
|
||||||
|
logger.debug("Verwende Tab-Navigation zum nächsten Feld")
|
||||||
|
self.automation.browser.page.keyboard.press("Tab")
|
||||||
|
|
||||||
|
# Variable Wartezeit nach Tab
|
||||||
|
time.sleep(random.uniform(0.15, 0.35))
|
||||||
|
|
||||||
|
# Gelegentlich Tab + Shift-Tab (versehentlich zu weit gegangen)
|
||||||
|
if random.random() < 0.08:
|
||||||
|
logger.debug("Simuliere Tab-Korrektur (zu weit)")
|
||||||
|
self.automation.browser.page.keyboard.press("Tab")
|
||||||
|
time.sleep(random.uniform(0.2, 0.4))
|
||||||
|
self.automation.browser.page.keyboard.press("Shift+Tab")
|
||||||
|
time.sleep(random.uniform(0.15, 0.3))
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.debug("Tab-Navigation nicht verwendet (Maus wird später genutzt)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei Tab-Navigation: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fill_field_with_tab_navigation(self, selector: str, value: str,
|
||||||
|
use_tab_navigation: bool = None,
|
||||||
|
error_rate: float = 0.15) -> bool:
|
||||||
|
"""
|
||||||
|
Füllt ein Feld aus mit optionaler Tab-Navigation.
|
||||||
|
|
||||||
|
Diese Methode kombiniert Tab-Navigation mit menschenähnlichem Tippen
|
||||||
|
für ein realistischeres Verhalten.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
selector: CSS-Selektor des Feldes (nur für Maus-Klick verwendet)
|
||||||
|
value: Einzugebender Wert
|
||||||
|
use_tab_navigation: Ob Tab verwendet werden soll (None = 50% Chance)
|
||||||
|
error_rate: Wahrscheinlichkeit für Tippfehler (10-20%)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True bei Erfolg
|
||||||
|
"""
|
||||||
|
if not self._ensure_browser():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Entscheide Navigationsmethode
|
||||||
|
if use_tab_navigation is None:
|
||||||
|
use_tab_navigation = random.random() < 0.5
|
||||||
|
|
||||||
|
if use_tab_navigation:
|
||||||
|
# Tab-Navigation verwenden
|
||||||
|
logger.debug(f"Fülle Feld mit Tab-Navigation")
|
||||||
|
self.automation.browser.page.keyboard.press("Tab")
|
||||||
|
time.sleep(random.uniform(0.15, 0.35))
|
||||||
|
else:
|
||||||
|
# Maus-Klick auf Feld
|
||||||
|
logger.debug(f"Fülle Feld {selector} mit Maus-Klick")
|
||||||
|
if not self.automation.browser.click_element(selector):
|
||||||
|
logger.warning(f"Konnte Feld nicht anklicken: {selector}")
|
||||||
|
return False
|
||||||
|
time.sleep(random.uniform(0.2, 0.5))
|
||||||
|
|
||||||
|
# Anti-Detection Delay vor Eingabe
|
||||||
|
if hasattr(self.automation, 'human_behavior') and hasattr(self.automation.human_behavior, 'anti_detection_delay'):
|
||||||
|
self.automation.human_behavior.anti_detection_delay("field_focus")
|
||||||
|
|
||||||
|
# Text mit Tippfehlern eingeben
|
||||||
|
for i, char in enumerate(value):
|
||||||
|
# Tippfehler simulieren
|
||||||
|
if random.random() < error_rate:
|
||||||
|
# Falsches Zeichen
|
||||||
|
wrong_char = random.choice('abcdefghijklmnopqrstuvwxyz0123456789')
|
||||||
|
self.automation.browser.page.keyboard.type(wrong_char)
|
||||||
|
time.sleep(random.uniform(0.1, 0.3))
|
||||||
|
|
||||||
|
# Korrektur mit Backspace
|
||||||
|
self.automation.browser.page.keyboard.press("Backspace")
|
||||||
|
time.sleep(random.uniform(0.08, 0.2))
|
||||||
|
|
||||||
|
# Korrektes Zeichen
|
||||||
|
self.automation.browser.page.keyboard.type(char)
|
||||||
|
|
||||||
|
# Variable Verzögerung
|
||||||
|
delay_ms = random.randint(50, 150)
|
||||||
|
if char in ' .,!?':
|
||||||
|
delay_ms *= random.uniform(1.3, 2.0)
|
||||||
|
time.sleep(delay_ms / 1000)
|
||||||
|
|
||||||
|
# Gelegentlich längere Pause (Denken)
|
||||||
|
if random.random() < 0.05:
|
||||||
|
time.sleep(random.uniform(0.3, 0.8))
|
||||||
|
|
||||||
|
logger.info(f"Feld erfolgreich ausgefüllt")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Ausfüllen mit Tab-Navigation: {e}")
|
||||||
return False
|
return False
|
||||||
@ -451,14 +451,97 @@ class TikTokRegistration:
|
|||||||
logger.debug("Kein Cookie-Banner erkannt")
|
logger.debug("Kein Cookie-Banner erkannt")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _close_video_overlay(self) -> bool:
|
||||||
|
"""
|
||||||
|
Schließt Video-Overlays, die Klicks blockieren können.
|
||||||
|
TikTok zeigt manchmal Promo-Videos, die den Login-Button überlagern.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Overlay geschlossen wurde oder nicht existiert
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Prüfe ob ein Video-Modal sichtbar ist
|
||||||
|
video_modal_selectors = [
|
||||||
|
"div[data-focus-lock-disabled]",
|
||||||
|
"div[class*='VideoPlayer']",
|
||||||
|
"div[class*='video-card']",
|
||||||
|
"div[class*='DivVideoContainer']"
|
||||||
|
]
|
||||||
|
|
||||||
|
for selector in video_modal_selectors:
|
||||||
|
if self.automation.browser.is_element_visible(selector, timeout=1000):
|
||||||
|
logger.info(f"Video-Overlay erkannt: {selector}")
|
||||||
|
|
||||||
|
# Versuche verschiedene Methoden zum Schließen
|
||||||
|
close_selectors = [
|
||||||
|
"button[aria-label='Close']",
|
||||||
|
"button[aria-label='Schließen']",
|
||||||
|
"div[data-focus-lock-disabled] button:has(svg)",
|
||||||
|
"[data-e2e='browse-close']",
|
||||||
|
"button.TUXButton:has-text('×')",
|
||||||
|
"button:has-text('×')"
|
||||||
|
]
|
||||||
|
|
||||||
|
for close_selector in close_selectors:
|
||||||
|
try:
|
||||||
|
if self.automation.browser.is_element_visible(close_selector, timeout=500):
|
||||||
|
self.automation.browser.click_element(close_selector)
|
||||||
|
logger.info(f"Video-Overlay geschlossen mit: {close_selector}")
|
||||||
|
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fallback: ESC-Taste drücken
|
||||||
|
try:
|
||||||
|
self.automation.browser.page.keyboard.press("Escape")
|
||||||
|
logger.info("Video-Overlay mit ESC-Taste geschlossen")
|
||||||
|
self.automation.human_behavior.random_delay(0.5, 1.0)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: JavaScript zum Entfernen des Video-Elements
|
||||||
|
try:
|
||||||
|
js_code = """
|
||||||
|
// Video-Elemente pausieren und verstecken
|
||||||
|
document.querySelectorAll('video').forEach(v => {
|
||||||
|
v.pause();
|
||||||
|
v.style.display = 'none';
|
||||||
|
});
|
||||||
|
// Modal-Container mit focus-lock entfernen
|
||||||
|
const modal = document.querySelector('div[data-focus-lock-disabled]');
|
||||||
|
if (modal && modal.querySelector('video')) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
"""
|
||||||
|
result = self.automation.browser.page.evaluate(js_code)
|
||||||
|
if result:
|
||||||
|
logger.info("Video-Overlay mit JavaScript versteckt")
|
||||||
|
self.automation.human_behavior.random_delay(0.3, 0.5)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"JavaScript-Entfernung fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
return True # Kein Overlay gefunden = OK
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Fehler beim Schließen des Video-Overlays: {e}")
|
||||||
|
return True # Trotzdem fortfahren
|
||||||
|
|
||||||
def _click_login_button(self) -> bool:
|
def _click_login_button(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Klickt auf den Anmelden-Button auf der Startseite.
|
Klickt auf den Anmelden-Button auf der Startseite.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True bei Erfolg, False bei Fehler
|
bool: True bei Erfolg, False bei Fehler
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# WICHTIG: Erst Video-Overlays schließen, die Klicks blockieren können
|
||||||
|
self._close_video_overlay()
|
||||||
|
|
||||||
# Liste aller Login-Button-Selektoren, die wir versuchen wollen
|
# Liste aller Login-Button-Selektoren, die wir versuchen wollen
|
||||||
login_selectors = [
|
login_selectors = [
|
||||||
self.selectors.LOGIN_BUTTON, # button#header-login-button
|
self.selectors.LOGIN_BUTTON, # button#header-login-button
|
||||||
@ -469,16 +552,34 @@ class TikTokRegistration:
|
|||||||
"button[aria-label*='Anmelden']", # Aria-Label
|
"button[aria-label*='Anmelden']", # Aria-Label
|
||||||
"button:has(.TUXButton-label:text('Anmelden'))" # Verschachtelte Struktur
|
"button:has(.TUXButton-label:text('Anmelden'))" # Verschachtelte Struktur
|
||||||
]
|
]
|
||||||
|
|
||||||
# Versuche jeden Selektor
|
# Versuche jeden Selektor mit force=True für blockierte Elemente
|
||||||
for i, selector in enumerate(login_selectors):
|
for i, selector in enumerate(login_selectors):
|
||||||
logger.debug(f"Versuche Login-Selektor {i+1}: {selector}")
|
logger.debug(f"Versuche Login-Selektor {i+1}: {selector}")
|
||||||
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
if self.automation.browser.is_element_visible(selector, timeout=3000):
|
||||||
result = self.automation.browser.click_element(selector)
|
# Erst normaler Klick
|
||||||
if result:
|
try:
|
||||||
logger.info(f"Anmelden-Button erfolgreich geklickt mit Selektor {i+1}")
|
result = self.automation.browser.click_element(selector)
|
||||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
if result:
|
||||||
return True
|
logger.info(f"Anmelden-Button erfolgreich geklickt mit Selektor {i+1}")
|
||||||
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||||
|
return True
|
||||||
|
except Exception as click_error:
|
||||||
|
# Bei Blockierung: Force-Click mit JavaScript
|
||||||
|
logger.debug(f"Normaler Klick blockiert, versuche JavaScript-Klick: {click_error}")
|
||||||
|
try:
|
||||||
|
escaped_selector = selector.replace("'", "\\'")
|
||||||
|
js_click = f"""
|
||||||
|
const el = document.querySelector('{escaped_selector}');
|
||||||
|
if (el) {{ el.click(); return true; }}
|
||||||
|
return false;
|
||||||
|
"""
|
||||||
|
if self.automation.browser.page.evaluate(js_click):
|
||||||
|
logger.info(f"Anmelden-Button mit JavaScript geklickt (Selektor {i+1})")
|
||||||
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
# Versuche es mit Fuzzy-Button-Matching
|
# Versuche es mit Fuzzy-Button-Matching
|
||||||
result = self.automation.ui_helper.click_button_fuzzy(
|
result = self.automation.ui_helper.click_button_fuzzy(
|
||||||
@ -501,35 +602,50 @@ class TikTokRegistration:
|
|||||||
def _click_register_link(self) -> bool:
|
def _click_register_link(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Klickt auf den Registrieren-Link im Login-Dialog.
|
Klickt auf den Registrieren-Link im Login-Dialog.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True bei Erfolg, False bei Fehler
|
bool: True bei Erfolg, False bei Fehler
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Warten, bis der Login-Dialog angezeigt wird
|
# Warten, bis der Login-Dialog angezeigt wird
|
||||||
self.automation.human_behavior.random_delay(2.0, 3.0)
|
self.automation.human_behavior.random_delay(2.0, 3.0)
|
||||||
|
|
||||||
|
# Video-Overlay schließen falls vorhanden (blockiert oft Klicks)
|
||||||
|
self._close_video_overlay()
|
||||||
|
|
||||||
# Screenshot für Debugging
|
# Screenshot für Debugging
|
||||||
self.automation._take_screenshot("after_login_button_click")
|
self.automation._take_screenshot("after_login_button_click")
|
||||||
|
|
||||||
# Verschiedene Registrieren-Selektoren versuchen
|
# Verschiedene Registrieren-Selektoren versuchen (prioritätsbezogen sortiert)
|
||||||
register_selectors = [
|
register_selectors = [
|
||||||
"a:text('Registrieren')", # Direkter Text-Match
|
# Primäre Selektoren (data-e2e Attribute sind am stabilsten)
|
||||||
"button:text('Registrieren')", # Button-Text
|
"span[data-e2e='bottom-sign-up']", # Offizieller TikTok-Selektor
|
||||||
"div:text('Registrieren')", # Div-Text
|
"[data-e2e='bottom-sign-up']", # Allgemeiner
|
||||||
"span:text('Registrieren')", # Span-Text
|
"[data-e2e*='sign-up']", # Partial match
|
||||||
"[data-e2e*='signup']", # Data-Attribute
|
"[data-e2e*='signup']", # Data-Attribute
|
||||||
"[data-e2e*='register']", # Data-Attribute
|
"[data-e2e*='register']", # Data-Attribute
|
||||||
|
# Dialog-bezogene Selektoren
|
||||||
|
"div[role='dialog'] a:has-text('Registrieren')", # Link im Dialog
|
||||||
|
"div[role='dialog'] span:has-text('Registrieren')", # Span im Dialog
|
||||||
|
"div[role='dialog'] div:has-text('Registrieren')", # Div im Dialog
|
||||||
|
# Text-basierte Selektoren
|
||||||
|
"a:text('Registrieren')", # Direkter Text-Match
|
||||||
|
"button:text('Registrieren')", # Button-Text
|
||||||
|
"span:text('Registrieren')", # Span-Text
|
||||||
|
"div:text('Registrieren')", # Div-Text
|
||||||
|
# Href-basierte Selektoren
|
||||||
"a[href*='signup']", # Signup-Link
|
"a[href*='signup']", # Signup-Link
|
||||||
"//a[contains(text(), 'Registrieren')]", # XPath
|
"a[href*='/signup']", # Mit Slash
|
||||||
"//button[contains(text(), 'Registrieren')]", # XPath Button
|
# XPath als Fallback
|
||||||
"//span[contains(text(), 'Registrieren')]", # XPath Span
|
"//a[contains(text(), 'Registrieren')]", # XPath
|
||||||
"//div[contains(text(), 'Konto erstellen')]", # Alternative Text
|
"//span[contains(text(), 'Registrieren')]", # XPath Span
|
||||||
"//a[contains(text(), 'Sign up')]", # Englisch
|
"//div[contains(text(), 'Konto erstellen')]", # Alternative Text
|
||||||
".signup-link", # CSS-Klasse
|
"//a[contains(text(), 'Sign up')]", # Englisch
|
||||||
".register-link" # CSS-Klasse
|
# CSS-Klassen als letzter Fallback
|
||||||
|
".signup-link", # CSS-Klasse
|
||||||
|
".register-link" # CSS-Klasse
|
||||||
]
|
]
|
||||||
|
|
||||||
# Versuche jeden Selektor
|
# Versuche jeden Selektor
|
||||||
for i, selector in enumerate(register_selectors):
|
for i, selector in enumerate(register_selectors):
|
||||||
logger.debug(f"Versuche Registrieren-Selektor {i+1}: {selector}")
|
logger.debug(f"Versuche Registrieren-Selektor {i+1}: {selector}")
|
||||||
@ -543,8 +659,37 @@ class TikTokRegistration:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Selektor {i+1} fehlgeschlagen: {e}")
|
logger.debug(f"Selektor {i+1} fehlgeschlagen: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Fallback: Fuzzy-Text-Suche
|
# JavaScript-Fallback: Element per JS klicken (umgeht Overlays)
|
||||||
|
logger.debug("Versuche JavaScript-Klick für Registrieren-Link")
|
||||||
|
try:
|
||||||
|
js_selectors = [
|
||||||
|
"span[data-e2e='bottom-sign-up']",
|
||||||
|
"[data-e2e*='sign-up']",
|
||||||
|
"a[href*='signup']"
|
||||||
|
]
|
||||||
|
for js_sel in js_selectors:
|
||||||
|
try:
|
||||||
|
clicked = self.automation.browser.page.evaluate(f'''
|
||||||
|
() => {{
|
||||||
|
const el = document.querySelector("{js_sel}");
|
||||||
|
if (el) {{
|
||||||
|
el.click();
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
''')
|
||||||
|
if clicked:
|
||||||
|
logger.info(f"Registrieren-Link per JavaScript geklickt: {js_sel}")
|
||||||
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"JavaScript-Klick fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
# Fallback: Fuzzy-Text-Suche mit Playwright Locator
|
||||||
try:
|
try:
|
||||||
page_content = self.automation.browser.page.content()
|
page_content = self.automation.browser.page.content()
|
||||||
if "Registrieren" in page_content or "Sign up" in page_content:
|
if "Registrieren" in page_content or "Sign up" in page_content:
|
||||||
@ -559,18 +704,26 @@ class TikTokRegistration:
|
|||||||
try:
|
try:
|
||||||
element = self.automation.browser.page.locator(text_sel).first
|
element = self.automation.browser.page.locator(text_sel).first
|
||||||
if element.is_visible():
|
if element.is_visible():
|
||||||
element.click()
|
# Versuche normalen Klick
|
||||||
logger.info(f"Auf Text geklickt: {text_sel}")
|
try:
|
||||||
self.automation.human_behavior.random_delay(0.5, 1.5)
|
element.click(timeout=3000)
|
||||||
return True
|
logger.info(f"Auf Text geklickt: {text_sel}")
|
||||||
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
# Falls blockiert, force-click
|
||||||
|
element.click(force=True)
|
||||||
|
logger.info(f"Auf Text force-geklickt: {text_sel}")
|
||||||
|
self.automation.human_behavior.random_delay(0.5, 1.5)
|
||||||
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Fallback-Text-Suche fehlgeschlagen: {e}")
|
logger.debug(f"Fallback-Text-Suche fehlgeschlagen: {e}")
|
||||||
|
|
||||||
logger.error("Konnte keinen Registrieren-Link finden")
|
logger.error("Konnte keinen Registrieren-Link finden")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Klicken auf den Registrieren-Link: {e}")
|
logger.error(f"Fehler beim Klicken auf den Registrieren-Link: {e}")
|
||||||
# Debug-Screenshot bei Fehler
|
# Debug-Screenshot bei Fehler
|
||||||
@ -931,7 +1084,8 @@ class TikTokRegistration:
|
|||||||
year_selected = False
|
year_selected = False
|
||||||
# Berechne den Index für das Jahr (normalerweise absteigend sortiert)
|
# Berechne den Index für das Jahr (normalerweise absteigend sortiert)
|
||||||
# Annahme: Jahre von aktuellem Jahr bis 1900, also Index = aktuelles_jahr - gewähltes_jahr
|
# Annahme: Jahre von aktuellem Jahr bis 1900, also Index = aktuelles_jahr - gewähltes_jahr
|
||||||
current_year = 2025 # oder datetime.now().year
|
from datetime import datetime
|
||||||
|
current_year = datetime.now().year
|
||||||
year_index = current_year - birthday['year']
|
year_index = current_year - birthday['year']
|
||||||
|
|
||||||
year_option_selectors = [
|
year_option_selectors = [
|
||||||
|
|||||||
@ -1,150 +0,0 @@
|
|||||||
"""
|
|
||||||
Unit-Tests für GeneratorTabFactory.
|
|
||||||
Validiert die Factory-Implementierung und plattform-spezifische Tab-Erstellung.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
# Füge Projekt-Root zum Path hinzu
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication, QWidget
|
|
||||||
from views.tabs.generator_tab_factory import GeneratorTabFactory, create_generator_tab
|
|
||||||
from views.tabs.generator_tab import GeneratorTab
|
|
||||||
|
|
||||||
|
|
||||||
class TestGeneratorTabFactory(unittest.TestCase):
|
|
||||||
"""
|
|
||||||
Test-Suite für GeneratorTabFactory.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
"""Erstelle QApplication für Qt-Widgets."""
|
|
||||||
if not QApplication.instance():
|
|
||||||
cls.app = QApplication([])
|
|
||||||
else:
|
|
||||||
cls.app = QApplication.instance()
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Setup vor jedem Test."""
|
|
||||||
# Registry zurücksetzen
|
|
||||||
GeneratorTabFactory.clear_registry()
|
|
||||||
self.language_manager = MagicMock()
|
|
||||||
|
|
||||||
def test_create_generic_tab_for_unknown_platform(self):
|
|
||||||
"""Test: Factory erstellt generischen Tab für unbekannte Plattform."""
|
|
||||||
tab = GeneratorTabFactory.create_tab("unknown_platform", self.language_manager)
|
|
||||||
|
|
||||||
self.assertIsInstance(tab, QWidget)
|
|
||||||
self.assertIsInstance(tab, GeneratorTab)
|
|
||||||
|
|
||||||
def test_create_facebook_tab(self):
|
|
||||||
"""Test: Factory erstellt FacebookGeneratorTab für Facebook."""
|
|
||||||
tab = GeneratorTabFactory.create_tab("facebook", self.language_manager)
|
|
||||||
|
|
||||||
self.assertIsInstance(tab, QWidget)
|
|
||||||
# Prüfe ob es der Facebook-spezifische Tab ist
|
|
||||||
# FacebookGeneratorTab hat gender_male, gender_female, gender_custom Attribute
|
|
||||||
self.assertTrue(hasattr(tab, 'gender_male'), "Facebook-Tab sollte gender_male haben")
|
|
||||||
self.assertTrue(hasattr(tab, 'gender_female'), "Facebook-Tab sollte gender_female haben")
|
|
||||||
self.assertTrue(hasattr(tab, 'gender_custom'), "Facebook-Tab sollte gender_custom haben")
|
|
||||||
|
|
||||||
def test_case_insensitive_platform_names(self):
|
|
||||||
"""Test: Plattform-Namen sind case-insensitive."""
|
|
||||||
tab1 = GeneratorTabFactory.create_tab("FACEBOOK", self.language_manager)
|
|
||||||
tab2 = GeneratorTabFactory.create_tab("Facebook", self.language_manager)
|
|
||||||
tab3 = GeneratorTabFactory.create_tab("facebook", self.language_manager)
|
|
||||||
|
|
||||||
# Alle sollten Facebook-Tabs sein
|
|
||||||
for tab in [tab1, tab2, tab3]:
|
|
||||||
self.assertTrue(hasattr(tab, 'gender_male'))
|
|
||||||
|
|
||||||
def test_registry_functionality(self):
|
|
||||||
"""Test: Tab-Registry funktioniert korrekt."""
|
|
||||||
# Erstelle Mock-Tab-Klasse
|
|
||||||
class MockTab(QWidget):
|
|
||||||
def __init__(self, platform, language_manager):
|
|
||||||
super().__init__()
|
|
||||||
self.platform = platform
|
|
||||||
self.language_manager = language_manager
|
|
||||||
|
|
||||||
# Registriere Mock-Tab
|
|
||||||
GeneratorTabFactory.register_tab("test_platform", MockTab)
|
|
||||||
|
|
||||||
# Erstelle Tab
|
|
||||||
tab = GeneratorTabFactory.create_tab("test_platform", self.language_manager)
|
|
||||||
|
|
||||||
self.assertIsInstance(tab, MockTab)
|
|
||||||
self.assertEqual(tab.platform, "test_platform")
|
|
||||||
|
|
||||||
def test_lazy_loading(self):
|
|
||||||
"""Test: Tabs werden lazy geladen."""
|
|
||||||
# Registry sollte initial leer sein
|
|
||||||
self.assertEqual(len(GeneratorTabFactory._tab_registry), 0)
|
|
||||||
|
|
||||||
# Erstelle Facebook-Tab
|
|
||||||
tab = GeneratorTabFactory.create_tab("facebook", self.language_manager)
|
|
||||||
|
|
||||||
# Jetzt sollte Facebook in Registry sein
|
|
||||||
self.assertIn("facebook", GeneratorTabFactory._tab_registry)
|
|
||||||
|
|
||||||
def test_get_supported_platforms(self):
|
|
||||||
"""Test: Liste der unterstützten Plattformen."""
|
|
||||||
platforms = GeneratorTabFactory.get_supported_platforms()
|
|
||||||
|
|
||||||
# Sollte bekannte Plattformen enthalten
|
|
||||||
self.assertIn("facebook", platforms)
|
|
||||||
self.assertIn("instagram", platforms)
|
|
||||||
self.assertIn("tiktok", platforms)
|
|
||||||
self.assertIn("x", platforms)
|
|
||||||
|
|
||||||
def test_is_platform_supported(self):
|
|
||||||
"""Test: Plattform-Support-Prüfung."""
|
|
||||||
self.assertTrue(GeneratorTabFactory.is_platform_supported("facebook"))
|
|
||||||
self.assertTrue(GeneratorTabFactory.is_platform_supported("FACEBOOK"))
|
|
||||||
self.assertFalse(GeneratorTabFactory.is_platform_supported("unknown"))
|
|
||||||
|
|
||||||
def test_convenience_function(self):
|
|
||||||
"""Test: Convenience-Funktion create_generator_tab."""
|
|
||||||
tab = create_generator_tab("facebook", self.language_manager)
|
|
||||||
|
|
||||||
self.assertIsInstance(tab, QWidget)
|
|
||||||
self.assertTrue(hasattr(tab, 'gender_male'))
|
|
||||||
|
|
||||||
def test_error_handling_fallback(self):
|
|
||||||
"""Test: Factory fällt auf generischen Tab zurück bei Fehlern."""
|
|
||||||
# Simuliere einen Fehler beim Tab-Erstellen
|
|
||||||
with patch('views.tabs.facebook_generator_tab.FacebookGeneratorTab.__init__',
|
|
||||||
side_effect=Exception("Test error")):
|
|
||||||
|
|
||||||
tab = GeneratorTabFactory.create_tab("facebook", self.language_manager)
|
|
||||||
|
|
||||||
# Sollte auf generischen Tab zurückfallen
|
|
||||||
self.assertIsInstance(tab, GeneratorTab)
|
|
||||||
|
|
||||||
def test_signal_compatibility(self):
|
|
||||||
"""Test: Alle Tabs haben die erforderlichen Signale."""
|
|
||||||
platforms = ["facebook", "instagram", "tiktok", "x"]
|
|
||||||
|
|
||||||
for platform in platforms:
|
|
||||||
tab = GeneratorTabFactory.create_tab(platform, self.language_manager)
|
|
||||||
|
|
||||||
# Prüfe erforderliche Signale
|
|
||||||
self.assertTrue(hasattr(tab, 'start_requested'),
|
|
||||||
f"{platform}-Tab sollte start_requested Signal haben")
|
|
||||||
self.assertTrue(hasattr(tab, 'stop_requested'),
|
|
||||||
f"{platform}-Tab sollte stop_requested Signal haben")
|
|
||||||
self.assertTrue(hasattr(tab, 'account_created'),
|
|
||||||
f"{platform}-Tab sollte account_created Signal haben")
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
"""Cleanup nach jedem Test."""
|
|
||||||
GeneratorTabFactory.clear_registry()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
@ -1,611 +0,0 @@
|
|||||||
"""
|
|
||||||
Comprehensive tests for the method rotation system.
|
|
||||||
Tests all components: entities, repositories, use cases, and integration.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import sqlite3
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from unittest.mock import Mock, patch, MagicMock
|
|
||||||
|
|
||||||
# Add project root to path
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
||||||
|
|
||||||
from domain.entities.method_rotation import (
|
|
||||||
MethodStrategy, RotationSession, RotationEvent, PlatformMethodState,
|
|
||||||
RiskLevel, RotationEventType, RotationStrategy
|
|
||||||
)
|
|
||||||
from application.use_cases.method_rotation_use_case import MethodRotationUseCase, RotationContext
|
|
||||||
from infrastructure.repositories.method_strategy_repository import MethodStrategyRepository
|
|
||||||
from infrastructure.repositories.rotation_session_repository import RotationSessionRepository
|
|
||||||
from infrastructure.repositories.platform_method_state_repository import PlatformMethodStateRepository
|
|
||||||
|
|
||||||
|
|
||||||
class MockDBManager:
|
|
||||||
"""Mock database manager for testing"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.db_path = tempfile.mktemp(suffix='.db')
|
|
||||||
self.connection = None
|
|
||||||
self._setup_test_database()
|
|
||||||
|
|
||||||
def _setup_test_database(self):
|
|
||||||
"""Create test database with rotation tables"""
|
|
||||||
conn = sqlite3.connect(self.db_path)
|
|
||||||
|
|
||||||
# Create rotation system tables
|
|
||||||
with open('database/migrations/add_method_rotation_system.sql', 'r') as f:
|
|
||||||
sql_script = f.read()
|
|
||||||
# Remove the INSERT statements for tests
|
|
||||||
sql_lines = sql_script.split('\n')
|
|
||||||
create_statements = [line for line in sql_lines if line.strip() and not line.strip().startswith('INSERT')]
|
|
||||||
clean_sql = '\n'.join(create_statements)
|
|
||||||
conn.executescript(clean_sql)
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def get_connection(self):
|
|
||||||
if not self.connection:
|
|
||||||
self.connection = sqlite3.connect(self.db_path)
|
|
||||||
return self.connection
|
|
||||||
|
|
||||||
def execute_query(self, query, params=None):
|
|
||||||
conn = self.get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
if params:
|
|
||||||
cursor.execute(query, params)
|
|
||||||
else:
|
|
||||||
cursor.execute(query)
|
|
||||||
conn.commit()
|
|
||||||
return cursor
|
|
||||||
|
|
||||||
def fetch_one(self, query, params=None):
|
|
||||||
conn = self.get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
if params:
|
|
||||||
cursor.execute(query, params)
|
|
||||||
else:
|
|
||||||
cursor.execute(query)
|
|
||||||
return cursor.fetchone()
|
|
||||||
|
|
||||||
def fetch_all(self, query, params=None):
|
|
||||||
conn = self.get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
if params:
|
|
||||||
cursor.execute(query, params)
|
|
||||||
else:
|
|
||||||
cursor.execute(query)
|
|
||||||
return cursor.fetchall()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.connection:
|
|
||||||
self.connection.close()
|
|
||||||
if os.path.exists(self.db_path):
|
|
||||||
os.unlink(self.db_path)
|
|
||||||
|
|
||||||
|
|
||||||
class TestMethodStrategy(unittest.TestCase):
|
|
||||||
"""Test MethodStrategy entity"""
|
|
||||||
|
|
||||||
def test_method_strategy_creation(self):
|
|
||||||
"""Test creating a method strategy"""
|
|
||||||
strategy = MethodStrategy(
|
|
||||||
strategy_id="test_id",
|
|
||||||
platform="instagram",
|
|
||||||
method_name="email",
|
|
||||||
priority=8,
|
|
||||||
risk_level=RiskLevel.LOW
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(strategy.strategy_id, "test_id")
|
|
||||||
self.assertEqual(strategy.platform, "instagram")
|
|
||||||
self.assertEqual(strategy.method_name, "email")
|
|
||||||
self.assertEqual(strategy.priority, 8)
|
|
||||||
self.assertEqual(strategy.risk_level, RiskLevel.LOW)
|
|
||||||
self.assertTrue(strategy.is_active)
|
|
||||||
|
|
||||||
def test_effectiveness_score_calculation(self):
|
|
||||||
"""Test effectiveness score calculation"""
|
|
||||||
strategy = MethodStrategy(
|
|
||||||
strategy_id="test_id",
|
|
||||||
platform="instagram",
|
|
||||||
method_name="email",
|
|
||||||
priority=8,
|
|
||||||
success_rate=0.9,
|
|
||||||
failure_rate=0.1,
|
|
||||||
risk_level=RiskLevel.LOW
|
|
||||||
)
|
|
||||||
|
|
||||||
score = strategy.effectiveness_score
|
|
||||||
self.assertGreater(score, 0.8) # High priority, high success rate should score well
|
|
||||||
|
|
||||||
def test_cooldown_functionality(self):
|
|
||||||
"""Test cooldown period functionality"""
|
|
||||||
strategy = MethodStrategy(
|
|
||||||
strategy_id="test_id",
|
|
||||||
platform="instagram",
|
|
||||||
method_name="email",
|
|
||||||
cooldown_period=300,
|
|
||||||
last_failure=datetime.now() - timedelta(seconds=100)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(strategy.is_on_cooldown)
|
|
||||||
self.assertGreater(strategy.cooldown_remaining_seconds, 0)
|
|
||||||
|
|
||||||
# Test expired cooldown
|
|
||||||
strategy.last_failure = datetime.now() - timedelta(seconds=400)
|
|
||||||
self.assertFalse(strategy.is_on_cooldown)
|
|
||||||
|
|
||||||
def test_performance_update(self):
|
|
||||||
"""Test performance metrics update"""
|
|
||||||
strategy = MethodStrategy(
|
|
||||||
strategy_id="test_id",
|
|
||||||
platform="instagram",
|
|
||||||
method_name="email",
|
|
||||||
success_rate=0.5,
|
|
||||||
failure_rate=0.5
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update with success
|
|
||||||
strategy.update_performance(True, 120.0)
|
|
||||||
self.assertGreater(strategy.success_rate, 0.5)
|
|
||||||
self.assertLess(strategy.failure_rate, 0.5)
|
|
||||||
self.assertIsNotNone(strategy.last_success)
|
|
||||||
|
|
||||||
# Update with failure
|
|
||||||
original_success_rate = strategy.success_rate
|
|
||||||
strategy.update_performance(False)
|
|
||||||
self.assertLess(strategy.success_rate, original_success_rate)
|
|
||||||
self.assertIsNotNone(strategy.last_failure)
|
|
||||||
|
|
||||||
|
|
||||||
class TestRotationSession(unittest.TestCase):
|
|
||||||
"""Test RotationSession entity"""
|
|
||||||
|
|
||||||
def test_rotation_session_creation(self):
|
|
||||||
"""Test creating a rotation session"""
|
|
||||||
session = RotationSession(
|
|
||||||
session_id="test_session",
|
|
||||||
platform="instagram",
|
|
||||||
current_method="email"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(session.session_id, "test_session")
|
|
||||||
self.assertEqual(session.platform, "instagram")
|
|
||||||
self.assertEqual(session.current_method, "email")
|
|
||||||
self.assertTrue(session.is_active)
|
|
||||||
self.assertEqual(session.rotation_count, 0)
|
|
||||||
|
|
||||||
def test_session_metrics(self):
|
|
||||||
"""Test session metrics calculation"""
|
|
||||||
session = RotationSession(
|
|
||||||
session_id="test_session",
|
|
||||||
platform="instagram",
|
|
||||||
current_method="email"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add some attempts
|
|
||||||
session.add_attempt("email", True)
|
|
||||||
session.add_attempt("email", False)
|
|
||||||
session.add_attempt("phone", True)
|
|
||||||
|
|
||||||
self.assertEqual(session.success_count, 2)
|
|
||||||
self.assertEqual(session.failure_count, 1)
|
|
||||||
self.assertAlmostEqual(session.success_rate, 2/3, places=2)
|
|
||||||
|
|
||||||
def test_rotation_logic(self):
|
|
||||||
"""Test rotation decision logic"""
|
|
||||||
session = RotationSession(
|
|
||||||
session_id="test_session",
|
|
||||||
platform="instagram",
|
|
||||||
current_method="email"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add failures to trigger rotation
|
|
||||||
session.add_attempt("email", False)
|
|
||||||
session.add_attempt("email", False)
|
|
||||||
|
|
||||||
self.assertTrue(session.should_rotate)
|
|
||||||
|
|
||||||
# Test rotation
|
|
||||||
session.rotate_to_method("phone", "consecutive_failures")
|
|
||||||
self.assertEqual(session.current_method, "phone")
|
|
||||||
self.assertEqual(session.rotation_count, 1)
|
|
||||||
self.assertEqual(session.rotation_reason, "consecutive_failures")
|
|
||||||
|
|
||||||
|
|
||||||
class TestMethodStrategyRepository(unittest.TestCase):
|
|
||||||
"""Test MethodStrategyRepository"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.db_manager = MockDBManager()
|
|
||||||
self.repo = MethodStrategyRepository(self.db_manager)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.db_manager.close()
|
|
||||||
|
|
||||||
def test_save_and_find_strategy(self):
|
|
||||||
"""Test saving and finding strategies"""
|
|
||||||
strategy = MethodStrategy(
|
|
||||||
strategy_id="test_strategy",
|
|
||||||
platform="instagram",
|
|
||||||
method_name="email",
|
|
||||||
priority=8,
|
|
||||||
risk_level=RiskLevel.LOW
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save strategy
|
|
||||||
self.repo.save(strategy)
|
|
||||||
|
|
||||||
# Find by ID
|
|
||||||
found_strategy = self.repo.find_by_id("test_strategy")
|
|
||||||
self.assertIsNotNone(found_strategy)
|
|
||||||
self.assertEqual(found_strategy.strategy_id, "test_strategy")
|
|
||||||
self.assertEqual(found_strategy.platform, "instagram")
|
|
||||||
self.assertEqual(found_strategy.method_name, "email")
|
|
||||||
|
|
||||||
def test_find_active_by_platform(self):
|
|
||||||
"""Test finding active strategies by platform"""
|
|
||||||
# Create multiple strategies
|
|
||||||
strategies = [
|
|
||||||
MethodStrategy("s1", "instagram", "email", 8, risk_level=RiskLevel.LOW, success_rate=0.9),
|
|
||||||
MethodStrategy("s2", "instagram", "phone", 6, risk_level=RiskLevel.MEDIUM, success_rate=0.7),
|
|
||||||
MethodStrategy("s3", "instagram", "social", 4, risk_level=RiskLevel.HIGH, success_rate=0.3, is_active=False),
|
|
||||||
MethodStrategy("s4", "tiktok", "email", 8, risk_level=RiskLevel.LOW, success_rate=0.8)
|
|
||||||
]
|
|
||||||
|
|
||||||
for strategy in strategies:
|
|
||||||
self.repo.save(strategy)
|
|
||||||
|
|
||||||
# Find active Instagram strategies
|
|
||||||
active_strategies = self.repo.find_active_by_platform("instagram")
|
|
||||||
|
|
||||||
self.assertEqual(len(active_strategies), 2) # Only active ones
|
|
||||||
self.assertEqual(active_strategies[0].method_name, "email") # Highest effectiveness
|
|
||||||
|
|
||||||
def test_get_next_available_method(self):
|
|
||||||
"""Test getting next available method"""
|
|
||||||
# Create strategies
|
|
||||||
strategies = [
|
|
||||||
MethodStrategy("s1", "instagram", "email", 8, risk_level=RiskLevel.LOW, success_rate=0.9),
|
|
||||||
MethodStrategy("s2", "instagram", "phone", 6, risk_level=RiskLevel.MEDIUM, success_rate=0.7),
|
|
||||||
]
|
|
||||||
|
|
||||||
for strategy in strategies:
|
|
||||||
self.repo.save(strategy)
|
|
||||||
|
|
||||||
# Get next method excluding email
|
|
||||||
next_method = self.repo.get_next_available_method("instagram", ["email"])
|
|
||||||
self.assertIsNotNone(next_method)
|
|
||||||
self.assertEqual(next_method.method_name, "phone")
|
|
||||||
|
|
||||||
# Get next method with no exclusions
|
|
||||||
best_method = self.repo.get_next_available_method("instagram")
|
|
||||||
self.assertIsNotNone(best_method)
|
|
||||||
self.assertEqual(best_method.method_name, "email") # Best strategy
|
|
||||||
|
|
||||||
def test_platform_statistics(self):
|
|
||||||
"""Test platform statistics calculation"""
|
|
||||||
# Create strategies with different metrics
|
|
||||||
strategies = [
|
|
||||||
MethodStrategy("s1", "instagram", "email", 8, risk_level=RiskLevel.LOW,
|
|
||||||
success_rate=0.9, last_success=datetime.now()),
|
|
||||||
MethodStrategy("s2", "instagram", "phone", 6, risk_level=RiskLevel.MEDIUM,
|
|
||||||
success_rate=0.6, last_failure=datetime.now()),
|
|
||||||
]
|
|
||||||
|
|
||||||
for strategy in strategies:
|
|
||||||
self.repo.save(strategy)
|
|
||||||
|
|
||||||
stats = self.repo.get_platform_statistics("instagram")
|
|
||||||
|
|
||||||
self.assertEqual(stats['total_methods'], 2)
|
|
||||||
self.assertEqual(stats['active_methods'], 2)
|
|
||||||
self.assertGreater(stats['avg_success_rate'], 0)
|
|
||||||
self.assertEqual(stats['recent_successes_24h'], 1)
|
|
||||||
|
|
||||||
|
|
||||||
class TestRotationUseCase(unittest.TestCase):
|
|
||||||
"""Test MethodRotationUseCase"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.db_manager = MockDBManager()
|
|
||||||
self.strategy_repo = MethodStrategyRepository(self.db_manager)
|
|
||||||
self.session_repo = RotationSessionRepository(self.db_manager)
|
|
||||||
self.state_repo = PlatformMethodStateRepository(self.db_manager)
|
|
||||||
|
|
||||||
self.use_case = MethodRotationUseCase(
|
|
||||||
self.strategy_repo, self.session_repo, self.state_repo
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setup test data
|
|
||||||
self._setup_test_strategies()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.db_manager.close()
|
|
||||||
|
|
||||||
def _setup_test_strategies(self):
|
|
||||||
"""Setup test strategies"""
|
|
||||||
strategies = [
|
|
||||||
MethodStrategy("instagram_email", "instagram", "email", 8,
|
|
||||||
risk_level=RiskLevel.LOW, success_rate=0.9),
|
|
||||||
MethodStrategy("instagram_phone", "instagram", "phone", 6,
|
|
||||||
risk_level=RiskLevel.MEDIUM, success_rate=0.7),
|
|
||||||
MethodStrategy("tiktok_email", "tiktok", "email", 8,
|
|
||||||
risk_level=RiskLevel.LOW, success_rate=0.8),
|
|
||||||
]
|
|
||||||
|
|
||||||
for strategy in strategies:
|
|
||||||
self.strategy_repo.save(strategy)
|
|
||||||
|
|
||||||
def test_start_rotation_session(self):
|
|
||||||
"""Test starting a rotation session"""
|
|
||||||
context = RotationContext(
|
|
||||||
platform="instagram",
|
|
||||||
account_id="test_account"
|
|
||||||
)
|
|
||||||
|
|
||||||
session = self.use_case.start_rotation_session(context)
|
|
||||||
|
|
||||||
self.assertIsNotNone(session)
|
|
||||||
self.assertEqual(session.platform, "instagram")
|
|
||||||
self.assertEqual(session.current_method, "email") # Best method
|
|
||||||
self.assertTrue(session.is_active)
|
|
||||||
|
|
||||||
def test_get_optimal_method(self):
|
|
||||||
"""Test getting optimal method"""
|
|
||||||
context = RotationContext(platform="instagram")
|
|
||||||
|
|
||||||
method = self.use_case.get_optimal_method(context)
|
|
||||||
|
|
||||||
self.assertIsNotNone(method)
|
|
||||||
self.assertEqual(method.method_name, "email") # Best strategy
|
|
||||||
|
|
||||||
# Test with exclusions
|
|
||||||
context.excluded_methods = ["email"]
|
|
||||||
method = self.use_case.get_optimal_method(context)
|
|
||||||
self.assertEqual(method.method_name, "phone")
|
|
||||||
|
|
||||||
def test_method_rotation(self):
|
|
||||||
"""Test method rotation"""
|
|
||||||
# Start session
|
|
||||||
context = RotationContext(platform="instagram")
|
|
||||||
session = self.use_case.start_rotation_session(context)
|
|
||||||
|
|
||||||
# Record failure to trigger rotation
|
|
||||||
self.use_case.record_method_result(
|
|
||||||
session.session_id, "email", False, 0.0,
|
|
||||||
{'error_type': 'rate_limit', 'message': 'Rate limited'}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if rotation should occur
|
|
||||||
should_rotate = self.use_case.should_rotate_method(session.session_id)
|
|
||||||
|
|
||||||
if should_rotate:
|
|
||||||
# Attempt rotation
|
|
||||||
next_method = self.use_case.rotate_method(session.session_id, "rate_limit")
|
|
||||||
self.assertIsNotNone(next_method)
|
|
||||||
self.assertEqual(next_method.method_name, "phone")
|
|
||||||
|
|
||||||
def test_emergency_mode(self):
|
|
||||||
"""Test emergency mode functionality"""
|
|
||||||
# Enable emergency mode
|
|
||||||
self.use_case.enable_emergency_mode("instagram", "test_emergency")
|
|
||||||
|
|
||||||
# Check that platform state reflects emergency mode
|
|
||||||
state = self.state_repo.find_by_platform("instagram")
|
|
||||||
self.assertTrue(state.emergency_mode)
|
|
||||||
|
|
||||||
# Disable emergency mode
|
|
||||||
self.use_case.disable_emergency_mode("instagram")
|
|
||||||
state = self.state_repo.find_by_platform("instagram")
|
|
||||||
self.assertFalse(state.emergency_mode)
|
|
||||||
|
|
||||||
def test_performance_tracking(self):
|
|
||||||
"""Test performance tracking and metrics"""
|
|
||||||
context = RotationContext(platform="instagram")
|
|
||||||
session = self.use_case.start_rotation_session(context)
|
|
||||||
|
|
||||||
# Record success
|
|
||||||
self.use_case.record_method_result(
|
|
||||||
session.session_id, "email", True, 120.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get recommendations
|
|
||||||
recommendations = self.use_case.get_platform_method_recommendations("instagram")
|
|
||||||
|
|
||||||
self.assertIn('platform', recommendations)
|
|
||||||
self.assertIn('recommended_methods', recommendations)
|
|
||||||
self.assertGreater(len(recommendations['recommended_methods']), 0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegration(unittest.TestCase):
|
|
||||||
"""Integration tests for the complete rotation system"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.db_manager = MockDBManager()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.db_manager.close()
|
|
||||||
|
|
||||||
def test_complete_rotation_workflow(self):
|
|
||||||
"""Test complete rotation workflow from start to finish"""
|
|
||||||
# Initialize components
|
|
||||||
strategy_repo = MethodStrategyRepository(self.db_manager)
|
|
||||||
session_repo = RotationSessionRepository(self.db_manager)
|
|
||||||
state_repo = PlatformMethodStateRepository(self.db_manager)
|
|
||||||
use_case = MethodRotationUseCase(strategy_repo, session_repo, state_repo)
|
|
||||||
|
|
||||||
# Setup strategies
|
|
||||||
strategies = [
|
|
||||||
MethodStrategy("instagram_email", "instagram", "email", 8,
|
|
||||||
risk_level=RiskLevel.LOW, success_rate=0.9, max_daily_attempts=20),
|
|
||||||
MethodStrategy("instagram_phone", "instagram", "phone", 6,
|
|
||||||
risk_level=RiskLevel.MEDIUM, success_rate=0.7, max_daily_attempts=10),
|
|
||||||
]
|
|
||||||
|
|
||||||
for strategy in strategies:
|
|
||||||
strategy_repo.save(strategy)
|
|
||||||
|
|
||||||
# 1. Start rotation session
|
|
||||||
context = RotationContext(platform="instagram", account_id="test_account")
|
|
||||||
session = use_case.start_rotation_session(context)
|
|
||||||
|
|
||||||
self.assertIsNotNone(session)
|
|
||||||
self.assertEqual(session.current_method, "email")
|
|
||||||
|
|
||||||
# 2. Simulate failure and rotation
|
|
||||||
use_case.record_method_result(
|
|
||||||
session.session_id, "email", False, 0.0,
|
|
||||||
{'error_type': 'rate_limit', 'message': 'Rate limited'}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check rotation trigger
|
|
||||||
if use_case.should_rotate_method(session.session_id):
|
|
||||||
next_method = use_case.rotate_method(session.session_id, "rate_limit")
|
|
||||||
self.assertEqual(next_method.method_name, "phone")
|
|
||||||
|
|
||||||
# 3. Simulate success with new method
|
|
||||||
use_case.record_method_result(
|
|
||||||
session.session_id, "phone", True, 180.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. Verify session is completed
|
|
||||||
session_status = use_case.get_session_status(session.session_id)
|
|
||||||
self.assertIsNotNone(session_status)
|
|
||||||
|
|
||||||
def test_error_handling_and_fallback(self):
|
|
||||||
"""Test error handling and fallback mechanisms"""
|
|
||||||
# Test with invalid platform
|
|
||||||
strategy_repo = MethodStrategyRepository(self.db_manager)
|
|
||||||
session_repo = RotationSessionRepository(self.db_manager)
|
|
||||||
state_repo = PlatformMethodStateRepository(self.db_manager)
|
|
||||||
use_case = MethodRotationUseCase(strategy_repo, session_repo, state_repo)
|
|
||||||
|
|
||||||
# Try to get method for platform with no strategies
|
|
||||||
context = RotationContext(platform="nonexistent")
|
|
||||||
method = use_case.get_optimal_method(context)
|
|
||||||
|
|
||||||
self.assertIsNone(method) # Should handle gracefully
|
|
||||||
|
|
||||||
def test_concurrent_sessions(self):
|
|
||||||
"""Test handling multiple concurrent sessions"""
|
|
||||||
strategy_repo = MethodStrategyRepository(self.db_manager)
|
|
||||||
session_repo = RotationSessionRepository(self.db_manager)
|
|
||||||
state_repo = PlatformMethodStateRepository(self.db_manager)
|
|
||||||
use_case = MethodRotationUseCase(strategy_repo, session_repo, state_repo)
|
|
||||||
|
|
||||||
# Setup strategy
|
|
||||||
strategy = MethodStrategy("instagram_email", "instagram", "email", 8,
|
|
||||||
risk_level=RiskLevel.LOW, success_rate=0.9)
|
|
||||||
strategy_repo.save(strategy)
|
|
||||||
|
|
||||||
# Start multiple sessions
|
|
||||||
sessions = []
|
|
||||||
for i in range(3):
|
|
||||||
context = RotationContext(platform="instagram", account_id=f"account_{i}")
|
|
||||||
session = use_case.start_rotation_session(context)
|
|
||||||
sessions.append(session)
|
|
||||||
|
|
||||||
# Verify all sessions are active and distinct
|
|
||||||
self.assertEqual(len(sessions), 3)
|
|
||||||
session_ids = [s.session_id for s in sessions]
|
|
||||||
self.assertEqual(len(set(session_ids)), 3) # All unique
|
|
||||||
|
|
||||||
|
|
||||||
class TestMixinIntegration(unittest.TestCase):
|
|
||||||
"""Test mixin integration with controllers"""
|
|
||||||
|
|
||||||
def test_controller_mixin_integration(self):
|
|
||||||
"""Test that controller mixins work correctly"""
|
|
||||||
from controllers.platform_controllers.method_rotation_mixin import MethodRotationMixin
|
|
||||||
|
|
||||||
# Create mock controller with mixin
|
|
||||||
class MockController(MethodRotationMixin):
|
|
||||||
def __init__(self):
|
|
||||||
self.platform_name = "instagram"
|
|
||||||
self.db_manager = MockDBManager()
|
|
||||||
self.logger = Mock()
|
|
||||||
self._init_method_rotation_system()
|
|
||||||
|
|
||||||
controller = MockController()
|
|
||||||
|
|
||||||
# Test that rotation system is initialized
|
|
||||||
self.assertIsNotNone(controller.method_rotation_use_case)
|
|
||||||
|
|
||||||
# Test availability check
|
|
||||||
self.assertTrue(controller._should_use_rotation_system())
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
controller.db_manager.close()
|
|
||||||
|
|
||||||
def test_worker_mixin_integration(self):
|
|
||||||
"""Test worker thread mixin integration"""
|
|
||||||
from controllers.platform_controllers.method_rotation_worker_mixin import MethodRotationWorkerMixin
|
|
||||||
|
|
||||||
# Create mock worker with mixin
|
|
||||||
class MockWorker(MethodRotationWorkerMixin):
|
|
||||||
def __init__(self):
|
|
||||||
self.params = {'registration_method': 'email'}
|
|
||||||
self.log_signal = Mock()
|
|
||||||
self.rotation_retry_count = 0
|
|
||||||
self.max_rotation_retries = 3
|
|
||||||
self.controller_instance = None
|
|
||||||
|
|
||||||
worker = MockWorker()
|
|
||||||
|
|
||||||
# Test initialization
|
|
||||||
worker._init_rotation_support()
|
|
||||||
|
|
||||||
# Test availability check
|
|
||||||
available = worker._is_rotation_available()
|
|
||||||
self.assertFalse(available) # No controller instance
|
|
||||||
|
|
||||||
# Test error classification
|
|
||||||
error_type = worker._classify_error("Rate limit exceeded")
|
|
||||||
self.assertEqual(error_type, "rate_limit")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Create test suite
|
|
||||||
test_suite = unittest.TestSuite()
|
|
||||||
|
|
||||||
# Add test cases
|
|
||||||
test_cases = [
|
|
||||||
TestMethodStrategy,
|
|
||||||
TestRotationSession,
|
|
||||||
TestMethodStrategyRepository,
|
|
||||||
TestRotationUseCase,
|
|
||||||
TestIntegration,
|
|
||||||
TestMixinIntegration
|
|
||||||
]
|
|
||||||
|
|
||||||
for test_case in test_cases:
|
|
||||||
tests = unittest.TestLoader().loadTestsFromTestCase(test_case)
|
|
||||||
test_suite.addTests(tests)
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
runner = unittest.TextTestRunner(verbosity=2)
|
|
||||||
result = runner.run(test_suite)
|
|
||||||
|
|
||||||
# Print summary
|
|
||||||
print(f"\nTest Summary:")
|
|
||||||
print(f"Tests run: {result.testsRun}")
|
|
||||||
print(f"Failures: {len(result.failures)}")
|
|
||||||
print(f"Errors: {len(result.errors)}")
|
|
||||||
|
|
||||||
if result.failures:
|
|
||||||
print("\nFailures:")
|
|
||||||
for test, traceback in result.failures:
|
|
||||||
print(f"- {test}: {traceback}")
|
|
||||||
|
|
||||||
if result.errors:
|
|
||||||
print("\nErrors:")
|
|
||||||
for test, traceback in result.errors:
|
|
||||||
print(f"- {test}: {traceback}")
|
|
||||||
|
|
||||||
# Exit with appropriate code
|
|
||||||
sys.exit(0 if result.wasSuccessful() else 1)
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
Theme Configuration - Single Source of Truth for all UI Colors and Styles
|
Theme Configuration - Single Source of Truth for all UI Colors and Styles
|
||||||
Based on IntelSight Corporate Design System
|
Based on AegisSight Corporate Design System
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class ThemeConfig:
|
class ThemeConfig:
|
||||||
@ -94,7 +94,7 @@ class ThemeConfig:
|
|||||||
'scrollbar_handle_hover': '#0078A3',
|
'scrollbar_handle_hover': '#0078A3',
|
||||||
|
|
||||||
# ========== LOGO ==========
|
# ========== LOGO ==========
|
||||||
'logo_path': 'intelsight-logo.svg',
|
'logo_path': 'aegissight-logo.svg',
|
||||||
},
|
},
|
||||||
|
|
||||||
'dark': {
|
'dark': {
|
||||||
@ -180,7 +180,7 @@ class ThemeConfig:
|
|||||||
'scrollbar_handle_hover': '#00B8E6',
|
'scrollbar_handle_hover': '#00B8E6',
|
||||||
|
|
||||||
# ========== LOGO ==========
|
# ========== LOGO ==========
|
||||||
'logo_path': 'intelsight-dark.svg',
|
'logo_path': 'aegissight-logo-dark.svg',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,18 +24,21 @@ class HumanBehavior:
|
|||||||
self.speed_factor = max(0.1, min(10.0, speed_factor)) # Begrenzung auf 0.1-10.0
|
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
|
self.randomness = max(0.0, min(1.0, randomness)) # Begrenzung auf 0.0-1.0
|
||||||
|
|
||||||
# Typische Verzögerungen (in Sekunden)
|
# Typische Verzögerungen (in Sekunden) - ERHÖHT für Anti-Detection
|
||||||
self.delays = {
|
self.delays = {
|
||||||
"typing_per_char": 0.05, # Verzögerung pro Zeichen beim Tippen
|
"typing_per_char": 0.08, # Verzögerung pro Zeichen beim Tippen (erhöht)
|
||||||
"mouse_movement": 0.5, # Verzögerung für Mausbewegung
|
"mouse_movement": 0.5, # Verzögerung für Mausbewegung
|
||||||
"click": 0.1, # Verzögerung für Mausklick
|
"click": 0.15, # Verzögerung für Mausklick (erhöht)
|
||||||
"page_load": 2.0, # Verzögerung für das Laden einer Seite
|
"page_load": 8.0, # Verzögerung für das Laden einer Seite (STARK erhöht: 5-15s)
|
||||||
"form_fill": 1.0, # Verzögerung zwischen Formularfeldern
|
"form_fill": 4.0, # Verzögerung zwischen Formularfeldern (STARK erhöht: 2-8s)
|
||||||
"decision": 1.5, # Verzögerung für Entscheidungen
|
"decision": 3.0, # Verzögerung für Entscheidungen (erhöht)
|
||||||
"scroll": 0.3, # Verzögerung für Scrollbewegungen
|
"scroll": 0.5, # Verzögerung für Scrollbewegungen (erhöht)
|
||||||
"verification": 5.0, # Verzögerung für Verifizierungsprozesse
|
"verification": 30.0, # Verzögerung für Verifizierungsprozesse (STARK erhöht: 15-45s)
|
||||||
"image_upload": 3.0, # Verzögerung für Bildupload
|
"image_upload": 5.0, # Verzögerung für Bildupload (erhöht)
|
||||||
"navigation": 1.0 # Verzögerung für Navigation
|
"navigation": 2.0, # Verzögerung für Navigation (erhöht)
|
||||||
|
"cookie_reading": 5.0, # NEU: Cookie-Banner lesen (3-8s)
|
||||||
|
"field_transition": 5.0, # NEU: Zwischen Formularfeldern (2-8s)
|
||||||
|
"thinking": 2.0 # NEU: Kurze Denkpause
|
||||||
}
|
}
|
||||||
|
|
||||||
def sleep(self, delay_type: str, multiplier: float = 1.0) -> None:
|
def sleep(self, delay_type: str, multiplier: float = 1.0) -> None:
|
||||||
@ -76,7 +79,7 @@ class HumanBehavior:
|
|||||||
def _random_delay(self, min_seconds: float = 1.0, max_seconds: float = 3.0) -> 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.
|
Führt eine zufällige Wartezeit aus, um menschliches Verhalten zu simulieren.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
min_seconds: Minimale Wartezeit in Sekunden
|
min_seconds: Minimale Wartezeit in Sekunden
|
||||||
max_seconds: Maximale Wartezeit in Sekunden
|
max_seconds: Maximale Wartezeit in Sekunden
|
||||||
@ -84,42 +87,96 @@ class HumanBehavior:
|
|||||||
delay = random.uniform(min_seconds, max_seconds)
|
delay = random.uniform(min_seconds, max_seconds)
|
||||||
logger.debug(f"Zufällige Wartezeit: {delay:.2f} Sekunden")
|
logger.debug(f"Zufällige Wartezeit: {delay:.2f} Sekunden")
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
def type_text(self, text: str, on_char_typed: Optional[Callable[[str], None]] = None,
|
def anti_detection_delay(self, action_type: str = "form_fill") -> None:
|
||||||
error_probability: float = 0.05, correction_probability: float = 0.9) -> str:
|
|
||||||
"""
|
"""
|
||||||
Simuliert menschliches Tippen mit möglichen Tippfehlern und Korrekturen.
|
Erzeugt eine realistische Anti-Detection-Verzögerung.
|
||||||
|
|
||||||
|
Diese Methode verwendet längere, zufälligere Wartezeiten um Bot-Erkennung
|
||||||
|
zu vermeiden. Die Verzögerungen sind bewusst lang um menschliches
|
||||||
|
Verhalten realistischer zu simulieren.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action_type: Art der Aktion:
|
||||||
|
- "form_fill": Zwischen Formularfeldern (2-8s)
|
||||||
|
- "page_load": Auf neuen Seiten (5-15s)
|
||||||
|
- "verification": Vor Code-Eingabe (15-45s)
|
||||||
|
- "cookie_reading": Cookie-Banner lesen (3-8s)
|
||||||
|
- "thinking": Kurze Denkpause (1-3s)
|
||||||
|
"""
|
||||||
|
delay_ranges = {
|
||||||
|
"form_fill": (2.0, 8.0), # Zwischen Formularfeldern
|
||||||
|
"page_load": (5.0, 15.0), # Auf neuen Seiten
|
||||||
|
"verification": (15.0, 45.0), # Vor Code-Eingabe
|
||||||
|
"cookie_reading": (3.0, 8.0), # Cookie-Banner lesen
|
||||||
|
"thinking": (1.0, 3.0), # Kurze Denkpause
|
||||||
|
"field_focus": (0.5, 1.5), # Vor Feldinteraktion
|
||||||
|
}
|
||||||
|
|
||||||
|
min_delay, max_delay = delay_ranges.get(action_type, (2.0, 5.0))
|
||||||
|
|
||||||
|
# Basis-Verzögerung
|
||||||
|
delay = random.uniform(min_delay, max_delay)
|
||||||
|
|
||||||
|
# Zusätzliche Variation basierend auf randomness
|
||||||
|
if self.randomness > 0:
|
||||||
|
variation = 1.0 + (random.random() * 2 - 1) * self.randomness * 0.3
|
||||||
|
delay *= variation
|
||||||
|
|
||||||
|
# Speed-Factor anwenden (aber nicht zu stark reduzieren)
|
||||||
|
delay = delay / max(self.speed_factor, 0.5)
|
||||||
|
|
||||||
|
# Gelegentlich extra lange Pause (simuliert Ablenkung/Nachdenken)
|
||||||
|
if random.random() < 0.1:
|
||||||
|
extra_delay = random.uniform(2.0, 5.0)
|
||||||
|
delay += extra_delay
|
||||||
|
logger.debug(f"Extra Denkpause: +{extra_delay:.2f}s")
|
||||||
|
|
||||||
|
logger.debug(f"Anti-Detection Delay ({action_type}): {delay:.2f}s")
|
||||||
|
time.sleep(max(0.5, delay)) # Minimum 0.5s
|
||||||
|
|
||||||
|
def type_text(self, text: str, on_char_typed: Optional[Callable[[str], None]] = None,
|
||||||
|
error_probability: float = 0.15, correction_probability: float = 0.95) -> str:
|
||||||
|
"""
|
||||||
|
Simuliert menschliches Tippen mit realistischen Tippfehlern und Korrekturen.
|
||||||
|
|
||||||
|
Die Fehlerrate wurde auf 15% erhöht (vorher 5%) um realistischeres
|
||||||
|
menschliches Verhalten zu simulieren. Echte Menschen machen häufig
|
||||||
|
Tippfehler und korrigieren diese sofort.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Zu tippender Text
|
text: Zu tippender Text
|
||||||
on_char_typed: Optionale Funktion, die für jedes getippte Zeichen aufgerufen wird
|
on_char_typed: Optionale Funktion, die für jedes getippte Zeichen aufgerufen wird
|
||||||
error_probability: Wahrscheinlichkeit für Tippfehler (0-1)
|
error_probability: Wahrscheinlichkeit für Tippfehler (0-1), Standard: 0.15 (15%)
|
||||||
correction_probability: Wahrscheinlichkeit, Tippfehler zu korrigieren (0-1)
|
correction_probability: Wahrscheinlichkeit, Tippfehler zu korrigieren (0-1)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Der tatsächlich getippte Text (mit oder ohne Fehler)
|
Der tatsächlich getippte Text (mit oder ohne Fehler)
|
||||||
"""
|
"""
|
||||||
# Anpassen der Fehlerwahrscheinlichkeit basierend auf Zufälligkeit
|
# Fehlerrate zwischen 10-20% halten für Realismus
|
||||||
adjusted_error_prob = error_probability * self.randomness
|
base_error_prob = max(0.10, min(0.20, error_probability))
|
||||||
|
# Anpassen basierend auf Zufälligkeit (aber nicht unter 10%)
|
||||||
|
adjusted_error_prob = max(0.10, base_error_prob * (0.8 + self.randomness * 0.4))
|
||||||
|
|
||||||
result = ""
|
result = ""
|
||||||
i = 0
|
i = 0
|
||||||
|
|
||||||
while i < len(text):
|
while i < len(text):
|
||||||
char = text[i]
|
char = text[i]
|
||||||
|
|
||||||
# Potentieller Tippfehler
|
# Potentieller Tippfehler
|
||||||
if random.random() < adjusted_error_prob:
|
if random.random() < adjusted_error_prob:
|
||||||
# Auswahl eines Fehlertyps:
|
# Auswahl eines Fehlertyps:
|
||||||
# - Falsches Zeichen (Tastatur-Nachbarn)
|
# - Falsches Zeichen (Tastatur-Nachbarn) - 50%
|
||||||
# - Ausgelassenes Zeichen
|
# - Transposition (Buchstaben vertauschen) - 15%
|
||||||
# - Doppeltes Zeichen
|
# - Ausgelassenes Zeichen - 15%
|
||||||
|
# - Doppeltes Zeichen - 20%
|
||||||
error_type = random.choices(
|
error_type = random.choices(
|
||||||
["wrong", "skip", "double"],
|
["wrong", "transposition", "skip", "double"],
|
||||||
weights=[0.6, 0.2, 0.2],
|
weights=[0.50, 0.15, 0.15, 0.20],
|
||||||
k=1
|
k=1
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
if error_type == "wrong":
|
if error_type == "wrong":
|
||||||
# Falsches Zeichen tippen (Tastatur-Nachbarn)
|
# Falsches Zeichen tippen (Tastatur-Nachbarn)
|
||||||
keyboard_neighbors = self.get_keyboard_neighbors(char)
|
keyboard_neighbors = self.get_keyboard_neighbors(char)
|
||||||
@ -129,15 +186,18 @@ class HumanBehavior:
|
|||||||
if on_char_typed:
|
if on_char_typed:
|
||||||
on_char_typed(wrong_char)
|
on_char_typed(wrong_char)
|
||||||
self.sleep("typing_per_char")
|
self.sleep("typing_per_char")
|
||||||
|
|
||||||
|
# Pause bevor Fehler "bemerkt" wird
|
||||||
|
time.sleep(random.uniform(0.1, 0.4))
|
||||||
|
|
||||||
# Entscheiden, ob der Fehler korrigiert wird
|
# Entscheiden, ob der Fehler korrigiert wird
|
||||||
if random.random() < correction_probability:
|
if random.random() < correction_probability:
|
||||||
# Löschen des falschen Zeichens
|
# Löschen des falschen Zeichens
|
||||||
result = result[:-1]
|
result = result[:-1]
|
||||||
if on_char_typed:
|
if on_char_typed:
|
||||||
on_char_typed("\b") # Backspace
|
on_char_typed("\b") # Backspace
|
||||||
self.sleep("typing_per_char", 1.5) # Längere Pause für Korrektur
|
self.sleep("typing_per_char", 1.8)
|
||||||
|
|
||||||
# Korrektes Zeichen tippen
|
# Korrektes Zeichen tippen
|
||||||
result += char
|
result += char
|
||||||
if on_char_typed:
|
if on_char_typed:
|
||||||
@ -149,35 +209,87 @@ class HumanBehavior:
|
|||||||
if on_char_typed:
|
if on_char_typed:
|
||||||
on_char_typed(char)
|
on_char_typed(char)
|
||||||
self.sleep("typing_per_char")
|
self.sleep("typing_per_char")
|
||||||
|
|
||||||
|
elif error_type == "transposition" and i < len(text) - 1:
|
||||||
|
# Buchstaben vertauschen (häufiger Tippfehler)
|
||||||
|
next_char = text[i + 1]
|
||||||
|
result += next_char + char # Vertauscht
|
||||||
|
if on_char_typed:
|
||||||
|
on_char_typed(next_char)
|
||||||
|
self.sleep("typing_per_char")
|
||||||
|
on_char_typed(char)
|
||||||
|
self.sleep("typing_per_char")
|
||||||
|
|
||||||
|
# Korrektur der Transposition
|
||||||
|
if random.random() < correction_probability:
|
||||||
|
time.sleep(random.uniform(0.2, 0.5)) # Bemerken des Fehlers
|
||||||
|
# Beide Zeichen löschen
|
||||||
|
result = result[:-2]
|
||||||
|
if on_char_typed:
|
||||||
|
on_char_typed("\b")
|
||||||
|
self.sleep("typing_per_char", 1.3)
|
||||||
|
on_char_typed("\b")
|
||||||
|
self.sleep("typing_per_char", 1.5)
|
||||||
|
|
||||||
|
# Korrekte Reihenfolge tippen
|
||||||
|
result += char + next_char
|
||||||
|
if on_char_typed:
|
||||||
|
on_char_typed(char)
|
||||||
|
self.sleep("typing_per_char")
|
||||||
|
on_char_typed(next_char)
|
||||||
|
self.sleep("typing_per_char")
|
||||||
|
|
||||||
|
i += 1 # Nächstes Zeichen überspringen (bereits verarbeitet)
|
||||||
|
|
||||||
elif error_type == "skip":
|
elif error_type == "skip":
|
||||||
# Zeichen auslassen (nichts tun)
|
# Zeichen auslassen (nichts tun)
|
||||||
|
# In 50% der Fälle später bemerken und nachtippen
|
||||||
|
if random.random() < 0.5 and i < len(text) - 1:
|
||||||
|
# Nächstes Zeichen normal tippen
|
||||||
|
pass # Wird übersprungen
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif error_type == "double":
|
elif error_type == "double":
|
||||||
# Zeichen doppelt tippen
|
# Zeichen doppelt tippen
|
||||||
result += char + char
|
result += char + char
|
||||||
if on_char_typed:
|
if on_char_typed:
|
||||||
on_char_typed(char)
|
on_char_typed(char)
|
||||||
|
self.sleep("typing_per_char", 0.3) # Sehr kurz zwischen Doppel
|
||||||
on_char_typed(char)
|
on_char_typed(char)
|
||||||
self.sleep("typing_per_char")
|
self.sleep("typing_per_char")
|
||||||
|
|
||||||
|
# Pause bevor Fehler bemerkt wird
|
||||||
|
time.sleep(random.uniform(0.15, 0.35))
|
||||||
|
|
||||||
# Entscheiden, ob der Fehler korrigiert wird
|
# Entscheiden, ob der Fehler korrigiert wird
|
||||||
if random.random() < correction_probability:
|
if random.random() < correction_probability:
|
||||||
# Löschen des doppelten Zeichens
|
# Löschen des doppelten Zeichens
|
||||||
result = result[:-1]
|
result = result[:-1]
|
||||||
if on_char_typed:
|
if on_char_typed:
|
||||||
on_char_typed("\b") # Backspace
|
on_char_typed("\b") # Backspace
|
||||||
self.sleep("typing_per_char", 1.2)
|
self.sleep("typing_per_char", 1.3)
|
||||||
else:
|
else:
|
||||||
# Normales Tippen ohne Fehler
|
# Normales Tippen ohne Fehler
|
||||||
result += char
|
result += char
|
||||||
if on_char_typed:
|
if on_char_typed:
|
||||||
on_char_typed(char)
|
on_char_typed(char)
|
||||||
self.sleep("typing_per_char")
|
|
||||||
|
# Variable Tippgeschwindigkeit basierend auf Zeichen
|
||||||
|
if char in ' .,!?;:':
|
||||||
|
# Längere Pause nach Satzzeichen/Leerzeichen
|
||||||
|
self.sleep("typing_per_char", random.uniform(1.2, 1.8))
|
||||||
|
elif char.isupper():
|
||||||
|
# Leicht länger für Großbuchstaben (Shift-Taste)
|
||||||
|
self.sleep("typing_per_char", random.uniform(1.0, 1.3))
|
||||||
|
else:
|
||||||
|
self.sleep("typing_per_char", random.uniform(0.8, 1.2))
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
# Gelegentliche längere Pause (Nachdenken)
|
||||||
|
if random.random() < 0.05:
|
||||||
|
time.sleep(random.uniform(0.3, 0.8))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_keyboard_neighbors(self, char: str) -> List[str]:
|
def get_keyboard_neighbors(self, char: str) -> List[str]:
|
||||||
|
|||||||
@ -1,195 +0,0 @@
|
|||||||
"""
|
|
||||||
Modal System Test - Test-Funktionen für das Modal-System
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from typing import Optional
|
|
||||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
|
|
||||||
from utils.modal_manager import ModalManager
|
|
||||||
from views.widgets.progress_modal import ProgressModal
|
|
||||||
from views.widgets.account_creation_modal import AccountCreationModal
|
|
||||||
from views.widgets.login_process_modal import LoginProcessModal
|
|
||||||
|
|
||||||
logger = logging.getLogger("modal_test")
|
|
||||||
|
|
||||||
|
|
||||||
class ModalTestWindow(QMainWindow):
|
|
||||||
"""Test-Fenster für Modal-System Tests"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.setWindowTitle("AccountForger Modal System Test")
|
|
||||||
self.setGeometry(100, 100, 600, 400)
|
|
||||||
|
|
||||||
# Modal Manager
|
|
||||||
self.modal_manager = ModalManager(parent_window=self)
|
|
||||||
|
|
||||||
# Test UI
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Erstellt Test-UI"""
|
|
||||||
central_widget = QWidget()
|
|
||||||
self.setCentralWidget(central_widget)
|
|
||||||
|
|
||||||
layout = QVBoxLayout(central_widget)
|
|
||||||
|
|
||||||
# Test Buttons
|
|
||||||
btn_account_creation = QPushButton("Test Account Creation Modal")
|
|
||||||
btn_account_creation.clicked.connect(self.test_account_creation_modal)
|
|
||||||
layout.addWidget(btn_account_creation)
|
|
||||||
|
|
||||||
btn_login_process = QPushButton("Test Login Process Modal")
|
|
||||||
btn_login_process.clicked.connect(self.test_login_process_modal)
|
|
||||||
layout.addWidget(btn_login_process)
|
|
||||||
|
|
||||||
btn_generic_modal = QPushButton("Test Generic Progress Modal")
|
|
||||||
btn_generic_modal.clicked.connect(self.test_generic_modal)
|
|
||||||
layout.addWidget(btn_generic_modal)
|
|
||||||
|
|
||||||
btn_error_modal = QPushButton("Test Error Modal")
|
|
||||||
btn_error_modal.clicked.connect(self.test_error_modal)
|
|
||||||
layout.addWidget(btn_error_modal)
|
|
||||||
|
|
||||||
btn_modal_manager = QPushButton("Test Modal Manager")
|
|
||||||
btn_modal_manager.clicked.connect(self.test_modal_manager)
|
|
||||||
layout.addWidget(btn_modal_manager)
|
|
||||||
|
|
||||||
def test_account_creation_modal(self):
|
|
||||||
"""Testet Account Creation Modal"""
|
|
||||||
logger.info("Testing Account Creation Modal")
|
|
||||||
|
|
||||||
modal = AccountCreationModal(parent=self, platform="Instagram")
|
|
||||||
|
|
||||||
# Steps setzen
|
|
||||||
steps = [
|
|
||||||
"Browser wird vorbereitet",
|
|
||||||
"Formular wird ausgefüllt",
|
|
||||||
"Account wird erstellt",
|
|
||||||
"E-Mail wird verifiziert"
|
|
||||||
]
|
|
||||||
modal.set_steps(steps)
|
|
||||||
|
|
||||||
# Modal anzeigen
|
|
||||||
modal.show_platform_specific_process()
|
|
||||||
|
|
||||||
# Simuliere Steps
|
|
||||||
QTimer.singleShot(1000, lambda: modal.start_step("Browser wird vorbereitet"))
|
|
||||||
QTimer.singleShot(2000, lambda: modal.complete_step("Browser wird vorbereitet", "Formular wird ausgefüllt"))
|
|
||||||
QTimer.singleShot(3000, lambda: modal.start_step("Formular wird ausgefüllt"))
|
|
||||||
QTimer.singleShot(4000, lambda: modal.complete_step("Formular wird ausgefüllt", "Account wird erstellt"))
|
|
||||||
QTimer.singleShot(5000, lambda: modal.start_step("Account wird erstellt"))
|
|
||||||
QTimer.singleShot(6000, lambda: modal.complete_step("Account wird erstellt", "E-Mail wird verifiziert"))
|
|
||||||
QTimer.singleShot(7000, lambda: modal.start_step("E-Mail wird verifiziert"))
|
|
||||||
QTimer.singleShot(8000, lambda: modal.show_success({"username": "test_user", "platform": "Instagram"}))
|
|
||||||
|
|
||||||
def test_login_process_modal(self):
|
|
||||||
"""Testet Login Process Modal"""
|
|
||||||
logger.info("Testing Login Process Modal")
|
|
||||||
|
|
||||||
modal = LoginProcessModal(parent=self, platform="TikTok")
|
|
||||||
|
|
||||||
# Session Login testen
|
|
||||||
modal.show_session_login("test_account", "TikTok")
|
|
||||||
|
|
||||||
# Simuliere Login-Prozess
|
|
||||||
QTimer.singleShot(1000, lambda: modal.update_login_progress("browser_init", "Browser wird gestartet"))
|
|
||||||
QTimer.singleShot(2000, lambda: modal.update_login_progress("session_restore", "Session wird wiederhergestellt"))
|
|
||||||
QTimer.singleShot(3000, lambda: modal.update_login_progress("verification", "Login wird geprüft"))
|
|
||||||
QTimer.singleShot(4000, lambda: modal.show_session_restored())
|
|
||||||
|
|
||||||
def test_generic_modal(self):
|
|
||||||
"""Testet Generic Progress Modal"""
|
|
||||||
logger.info("Testing Generic Progress Modal")
|
|
||||||
|
|
||||||
modal = ProgressModal(parent=self, modal_type="verification")
|
|
||||||
modal.show_process()
|
|
||||||
|
|
||||||
# Simuliere Updates
|
|
||||||
QTimer.singleShot(1000, lambda: modal.update_status("Verbindung wird hergestellt...", "Server wird kontaktiert"))
|
|
||||||
QTimer.singleShot(2000, lambda: modal.update_status("Daten werden verarbeitet...", "Bitte warten"))
|
|
||||||
QTimer.singleShot(3000, lambda: modal.update_status("✅ Vorgang abgeschlossen!", "Erfolgreich"))
|
|
||||||
QTimer.singleShot(4000, lambda: modal.hide_process())
|
|
||||||
|
|
||||||
def test_error_modal(self):
|
|
||||||
"""Testet Error Modal"""
|
|
||||||
logger.info("Testing Error Modal")
|
|
||||||
|
|
||||||
modal = ProgressModal(parent=self, modal_type="generic")
|
|
||||||
modal.show_process()
|
|
||||||
|
|
||||||
# Nach kurzer Zeit Fehler anzeigen
|
|
||||||
QTimer.singleShot(1500, lambda: modal.show_error("Netzwerkfehler aufgetreten", auto_close_seconds=3))
|
|
||||||
|
|
||||||
def test_modal_manager(self):
|
|
||||||
"""Testet Modal Manager"""
|
|
||||||
logger.info("Testing Modal Manager")
|
|
||||||
|
|
||||||
# Zeige Account Creation Modal über Manager
|
|
||||||
self.modal_manager.show_modal(
|
|
||||||
'account_creation',
|
|
||||||
title="🔄 Test Account wird erstellt",
|
|
||||||
status="Modal Manager Test läuft...",
|
|
||||||
detail="Über ModalManager aufgerufen"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Simuliere Updates über Manager
|
|
||||||
QTimer.singleShot(1000, lambda: self.modal_manager.update_modal_status(
|
|
||||||
'account_creation',
|
|
||||||
"Browser wird initialisiert...",
|
|
||||||
"Schritt 1 von 3"
|
|
||||||
))
|
|
||||||
|
|
||||||
QTimer.singleShot(2000, lambda: self.modal_manager.update_modal_status(
|
|
||||||
'account_creation',
|
|
||||||
"Formular wird ausgefüllt...",
|
|
||||||
"Schritt 2 von 3"
|
|
||||||
))
|
|
||||||
|
|
||||||
QTimer.singleShot(3000, lambda: self.modal_manager.update_modal_status(
|
|
||||||
'account_creation',
|
|
||||||
"Account wird finalisiert...",
|
|
||||||
"Schritt 3 von 3"
|
|
||||||
))
|
|
||||||
|
|
||||||
QTimer.singleShot(4000, lambda: self.modal_manager.update_modal_status(
|
|
||||||
'account_creation',
|
|
||||||
"✅ Account erfolgreich erstellt!",
|
|
||||||
"Test abgeschlossen"
|
|
||||||
))
|
|
||||||
|
|
||||||
QTimer.singleShot(5000, lambda: self.modal_manager.hide_modal('account_creation'))
|
|
||||||
|
|
||||||
|
|
||||||
def run_modal_test():
|
|
||||||
"""Führt den Modal-Test aus"""
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# QApplication erstellen falls nicht vorhanden
|
|
||||||
app = QApplication.instance()
|
|
||||||
if app is None:
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
|
|
||||||
# Test-Fenster erstellen
|
|
||||||
test_window = ModalTestWindow()
|
|
||||||
test_window.show()
|
|
||||||
|
|
||||||
# App ausführen
|
|
||||||
if hasattr(app, 'exec'):
|
|
||||||
return app.exec()
|
|
||||||
else:
|
|
||||||
return app.exec_()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Logging konfigurieren
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test ausführen
|
|
||||||
run_modal_test()
|
|
||||||
@ -1,412 +0,0 @@
|
|||||||
"""
|
|
||||||
Performance Monitor - Non-intrusive monitoring for race condition detection
|
|
||||||
Debug-only monitoring without production performance impact
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import functools
|
|
||||||
import traceback
|
|
||||||
from typing import Dict, Any, Optional, Callable, List
|
|
||||||
from collections import defaultdict, deque
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class OperationMetrics:
|
|
||||||
"""Metriken für eine einzelne Operation"""
|
|
||||||
operation_name: str
|
|
||||||
thread_id: int
|
|
||||||
thread_name: str
|
|
||||||
start_time: float
|
|
||||||
end_time: Optional[float] = None
|
|
||||||
duration: Optional[float] = None
|
|
||||||
success: bool = True
|
|
||||||
error_message: Optional[str] = None
|
|
||||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
||||||
stack_trace: Optional[str] = None
|
|
||||||
|
|
||||||
def complete(self, success: bool = True, error_message: Optional[str] = None):
|
|
||||||
"""Markiert Operation als abgeschlossen"""
|
|
||||||
self.end_time = time.time()
|
|
||||||
self.duration = self.end_time - self.start_time
|
|
||||||
self.success = success
|
|
||||||
self.error_message = error_message
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
|
||||||
"""Konvertiert zu Dictionary für Serialisierung"""
|
|
||||||
return {
|
|
||||||
'operation_name': self.operation_name,
|
|
||||||
'thread_id': self.thread_id,
|
|
||||||
'thread_name': self.thread_name,
|
|
||||||
'start_time': self.start_time,
|
|
||||||
'end_time': self.end_time,
|
|
||||||
'duration': self.duration,
|
|
||||||
'success': self.success,
|
|
||||||
'error_message': self.error_message,
|
|
||||||
'metadata': self.metadata,
|
|
||||||
'has_stack_trace': self.stack_trace is not None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PerformanceMonitor:
|
|
||||||
"""
|
|
||||||
Performance-Monitor mit race condition detection
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, enabled: bool = None, max_history: int = 1000):
|
|
||||||
# Auto-detect based on debug settings oder environment
|
|
||||||
if enabled is None:
|
|
||||||
enabled = (
|
|
||||||
os.getenv('DEBUG_RACE_CONDITIONS', '').lower() in ['true', '1', 'yes'] or
|
|
||||||
os.getenv('PERFORMANCE_MONITORING', '').lower() in ['true', '1', 'yes']
|
|
||||||
)
|
|
||||||
|
|
||||||
self.enabled = enabled
|
|
||||||
self.max_history = max_history
|
|
||||||
|
|
||||||
# Monitoring data
|
|
||||||
self._operation_history: deque = deque(maxlen=max_history)
|
|
||||||
self._active_operations: Dict[str, OperationMetrics] = {}
|
|
||||||
self._operation_stats: Dict[str, Dict[str, Any]] = defaultdict(lambda: {
|
|
||||||
'total_calls': 0,
|
|
||||||
'successful_calls': 0,
|
|
||||||
'failed_calls': 0,
|
|
||||||
'total_duration': 0.0,
|
|
||||||
'min_duration': float('inf'),
|
|
||||||
'max_duration': 0.0,
|
|
||||||
'concurrent_executions': 0,
|
|
||||||
'max_concurrent': 0
|
|
||||||
})
|
|
||||||
|
|
||||||
# Thread safety
|
|
||||||
self._lock = threading.RLock()
|
|
||||||
|
|
||||||
# Race condition detection
|
|
||||||
self._potential_races: List[Dict[str, Any]] = []
|
|
||||||
self._long_operations: List[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
# Thresholds
|
|
||||||
self.long_operation_threshold = 2.0 # seconds
|
|
||||||
self.race_detection_window = 0.1 # seconds
|
|
||||||
|
|
||||||
if self.enabled:
|
|
||||||
logger.info("Performance monitoring enabled")
|
|
||||||
|
|
||||||
def monitor_operation(self, operation_name: str, capture_stack: bool = False):
|
|
||||||
"""
|
|
||||||
Decorator für Operation-Monitoring
|
|
||||||
"""
|
|
||||||
def decorator(func: Callable) -> Callable:
|
|
||||||
if not self.enabled:
|
|
||||||
return func # No overhead when disabled
|
|
||||||
|
|
||||||
@functools.wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return self._execute_monitored(
|
|
||||||
operation_name or func.__name__,
|
|
||||||
func,
|
|
||||||
capture_stack,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
wrapper.original = func
|
|
||||||
wrapper.is_monitored = True
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def _execute_monitored(self, operation_name: str, func: Callable,
|
|
||||||
capture_stack: bool, *args, **kwargs) -> Any:
|
|
||||||
"""Führt eine überwachte Operation aus"""
|
|
||||||
if not self.enabled:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
thread_id = threading.current_thread().ident
|
|
||||||
thread_name = threading.current_thread().name
|
|
||||||
operation_key = f"{operation_name}_{thread_id}_{time.time()}"
|
|
||||||
|
|
||||||
# Metrics-Objekt erstellen
|
|
||||||
metrics = OperationMetrics(
|
|
||||||
operation_name=operation_name,
|
|
||||||
thread_id=thread_id,
|
|
||||||
thread_name=thread_name,
|
|
||||||
start_time=time.time(),
|
|
||||||
stack_trace=traceback.format_stack() if capture_stack else None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Race condition detection
|
|
||||||
self._detect_potential_race(operation_name, metrics.start_time)
|
|
||||||
|
|
||||||
with self._lock:
|
|
||||||
# Concurrent execution tracking
|
|
||||||
concurrent_count = sum(
|
|
||||||
1 for op in self._active_operations.values()
|
|
||||||
if op.operation_name == operation_name
|
|
||||||
)
|
|
||||||
|
|
||||||
stats = self._operation_stats[operation_name]
|
|
||||||
stats['concurrent_executions'] = concurrent_count
|
|
||||||
stats['max_concurrent'] = max(stats['max_concurrent'], concurrent_count)
|
|
||||||
|
|
||||||
# Operation zu aktiven hinzufügen
|
|
||||||
self._active_operations[operation_key] = metrics
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Operation ausführen
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
|
|
||||||
# Erfolg markieren
|
|
||||||
metrics.complete(success=True)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Fehler markieren
|
|
||||||
metrics.complete(success=False, error_message=str(e))
|
|
||||||
raise
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Cleanup und Statistik-Update
|
|
||||||
with self._lock:
|
|
||||||
self._active_operations.pop(operation_key, None)
|
|
||||||
self._update_statistics(metrics)
|
|
||||||
self._operation_history.append(metrics)
|
|
||||||
|
|
||||||
# Long operation detection
|
|
||||||
if metrics.duration and metrics.duration > self.long_operation_threshold:
|
|
||||||
self._record_long_operation(metrics)
|
|
||||||
|
|
||||||
def _detect_potential_race(self, operation_name: str, start_time: float):
|
|
||||||
"""Erkennt potentielle Race Conditions"""
|
|
||||||
if not self.enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Prüfe ob ähnliche Operationen zeitgleich laufen
|
|
||||||
concurrent_ops = []
|
|
||||||
with self._lock:
|
|
||||||
for op in self._active_operations.values():
|
|
||||||
if (op.operation_name == operation_name and
|
|
||||||
abs(op.start_time - start_time) < self.race_detection_window):
|
|
||||||
concurrent_ops.append(op)
|
|
||||||
|
|
||||||
if len(concurrent_ops) > 0:
|
|
||||||
race_info = {
|
|
||||||
'operation_name': operation_name,
|
|
||||||
'detected_at': start_time,
|
|
||||||
'concurrent_threads': [op.thread_id for op in concurrent_ops],
|
|
||||||
'time_window': self.race_detection_window,
|
|
||||||
'severity': 'high' if len(concurrent_ops) > 2 else 'medium'
|
|
||||||
}
|
|
||||||
|
|
||||||
self._potential_races.append(race_info)
|
|
||||||
|
|
||||||
logger.warning(f"Potential race condition detected: {operation_name} "
|
|
||||||
f"running on {len(concurrent_ops)} threads simultaneously")
|
|
||||||
|
|
||||||
def _record_long_operation(self, metrics: OperationMetrics):
|
|
||||||
"""Zeichnet lange Operationen auf"""
|
|
||||||
long_op_info = {
|
|
||||||
'operation_name': metrics.operation_name,
|
|
||||||
'duration': metrics.duration,
|
|
||||||
'thread_id': metrics.thread_id,
|
|
||||||
'start_time': metrics.start_time,
|
|
||||||
'success': metrics.success,
|
|
||||||
'metadata': metrics.metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
self._long_operations.append(long_op_info)
|
|
||||||
|
|
||||||
logger.warning(f"Long operation detected: {metrics.operation_name} "
|
|
||||||
f"took {metrics.duration:.3f}s (threshold: {self.long_operation_threshold}s)")
|
|
||||||
|
|
||||||
def _update_statistics(self, metrics: OperationMetrics):
|
|
||||||
"""Aktualisiert Operation-Statistiken"""
|
|
||||||
stats = self._operation_stats[metrics.operation_name]
|
|
||||||
|
|
||||||
stats['total_calls'] += 1
|
|
||||||
if metrics.success:
|
|
||||||
stats['successful_calls'] += 1
|
|
||||||
else:
|
|
||||||
stats['failed_calls'] += 1
|
|
||||||
|
|
||||||
if metrics.duration:
|
|
||||||
stats['total_duration'] += metrics.duration
|
|
||||||
stats['min_duration'] = min(stats['min_duration'], metrics.duration)
|
|
||||||
stats['max_duration'] = max(stats['max_duration'], metrics.duration)
|
|
||||||
|
|
||||||
def get_statistics(self) -> Dict[str, Any]:
|
|
||||||
"""Gibt vollständige Monitoring-Statistiken zurück"""
|
|
||||||
if not self.enabled:
|
|
||||||
return {'monitoring_enabled': False}
|
|
||||||
|
|
||||||
with self._lock:
|
|
||||||
# Statistiken aufbereiten
|
|
||||||
processed_stats = {}
|
|
||||||
for op_name, stats in self._operation_stats.items():
|
|
||||||
processed_stats[op_name] = {
|
|
||||||
**stats,
|
|
||||||
'average_duration': (
|
|
||||||
stats['total_duration'] / stats['total_calls']
|
|
||||||
if stats['total_calls'] > 0 else 0
|
|
||||||
),
|
|
||||||
'success_rate': (
|
|
||||||
stats['successful_calls'] / stats['total_calls']
|
|
||||||
if stats['total_calls'] > 0 else 0
|
|
||||||
),
|
|
||||||
'min_duration': stats['min_duration'] if stats['min_duration'] != float('inf') else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'monitoring_enabled': True,
|
|
||||||
'operation_statistics': processed_stats,
|
|
||||||
'race_conditions': {
|
|
||||||
'detected_count': len(self._potential_races),
|
|
||||||
'recent_races': self._potential_races[-10:], # Last 10
|
|
||||||
},
|
|
||||||
'long_operations': {
|
|
||||||
'detected_count': len(self._long_operations),
|
|
||||||
'threshold': self.long_operation_threshold,
|
|
||||||
'recent_long_ops': self._long_operations[-10:], # Last 10
|
|
||||||
},
|
|
||||||
'active_operations': len(self._active_operations),
|
|
||||||
'history_size': len(self._operation_history),
|
|
||||||
'thresholds': {
|
|
||||||
'long_operation_threshold': self.long_operation_threshold,
|
|
||||||
'race_detection_window': self.race_detection_window
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_race_condition_report(self) -> Dict[str, Any]:
|
|
||||||
"""Gibt detaillierten Race Condition Report zurück"""
|
|
||||||
if not self.enabled:
|
|
||||||
return {'monitoring_enabled': False}
|
|
||||||
|
|
||||||
with self._lock:
|
|
||||||
# Gruppiere Race Conditions nach Operation
|
|
||||||
races_by_operation = defaultdict(list)
|
|
||||||
for race in self._potential_races:
|
|
||||||
races_by_operation[race['operation_name']].append(race)
|
|
||||||
|
|
||||||
# Analysiere Patterns
|
|
||||||
analysis = {}
|
|
||||||
for op_name, races in races_by_operation.items():
|
|
||||||
high_severity = sum(1 for r in races if r['severity'] == 'high')
|
|
||||||
analysis[op_name] = {
|
|
||||||
'total_races': len(races),
|
|
||||||
'high_severity_races': high_severity,
|
|
||||||
'affected_threads': len(set(
|
|
||||||
thread_id for race in races
|
|
||||||
for thread_id in race['concurrent_threads']
|
|
||||||
)),
|
|
||||||
'first_detected': min(r['detected_at'] for r in races),
|
|
||||||
'last_detected': max(r['detected_at'] for r in races),
|
|
||||||
'recommendation': self._get_race_recommendation(op_name, races)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'monitoring_enabled': True,
|
|
||||||
'total_race_conditions': len(self._potential_races),
|
|
||||||
'affected_operations': len(races_by_operation),
|
|
||||||
'analysis_by_operation': analysis,
|
|
||||||
'raw_detections': self._potential_races
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_race_recommendation(self, operation_name: str, races: List[Dict]) -> str:
|
|
||||||
"""Gibt Empfehlungen für Race Condition Behebung"""
|
|
||||||
race_count = len(races)
|
|
||||||
high_severity_count = sum(1 for r in races if r['severity'] == 'high')
|
|
||||||
|
|
||||||
if high_severity_count > 5:
|
|
||||||
return f"CRITICAL: {operation_name} has {high_severity_count} high-severity race conditions. Implement ThreadSafetyMixin immediately."
|
|
||||||
elif race_count > 10:
|
|
||||||
return f"HIGH: {operation_name} frequently encounters race conditions. Consider adding thread synchronization."
|
|
||||||
elif race_count > 3:
|
|
||||||
return f"MEDIUM: {operation_name} occasionally has race conditions. Monitor and consider thread safety measures."
|
|
||||||
else:
|
|
||||||
return f"LOW: {operation_name} has minimal race condition risk."
|
|
||||||
|
|
||||||
def export_report(self, filename: Optional[str] = None) -> str:
|
|
||||||
"""Exportiert vollständigen Report als JSON"""
|
|
||||||
if not self.enabled:
|
|
||||||
return "Monitoring not enabled"
|
|
||||||
|
|
||||||
if filename is None:
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
filename = f"performance_report_{timestamp}.json"
|
|
||||||
|
|
||||||
report = {
|
|
||||||
'timestamp': datetime.now().isoformat(),
|
|
||||||
'statistics': self.get_statistics(),
|
|
||||||
'race_condition_report': self.get_race_condition_report(),
|
|
||||||
'operation_history': [op.to_dict() for op in list(self._operation_history)[-100:]] # Last 100
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filename, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(report, f, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
logger.info(f"Performance report exported to: {filename}")
|
|
||||||
return filename
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to export performance report: {e}")
|
|
||||||
return f"Export failed: {e}"
|
|
||||||
|
|
||||||
def reset_statistics(self):
|
|
||||||
"""Setzt alle Statistiken zurück"""
|
|
||||||
with self._lock:
|
|
||||||
self._operation_history.clear()
|
|
||||||
self._operation_stats.clear()
|
|
||||||
self._potential_races.clear()
|
|
||||||
self._long_operations.clear()
|
|
||||||
# Aktive Operationen nicht löschen - könnten noch laufen
|
|
||||||
|
|
||||||
if self.enabled:
|
|
||||||
logger.info("Performance monitoring statistics reset")
|
|
||||||
|
|
||||||
|
|
||||||
# Global Monitor Instance
|
|
||||||
_global_monitor: Optional[PerformanceMonitor] = None
|
|
||||||
_monitor_init_lock = threading.RLock()
|
|
||||||
|
|
||||||
|
|
||||||
def get_performance_monitor() -> PerformanceMonitor:
|
|
||||||
"""Holt die globale Monitor-Instanz (Singleton)"""
|
|
||||||
global _global_monitor
|
|
||||||
|
|
||||||
if _global_monitor is None:
|
|
||||||
with _monitor_init_lock:
|
|
||||||
if _global_monitor is None:
|
|
||||||
_global_monitor = PerformanceMonitor()
|
|
||||||
|
|
||||||
return _global_monitor
|
|
||||||
|
|
||||||
|
|
||||||
# Convenience Decorators
|
|
||||||
def monitor_if_enabled(operation_name: str = None, capture_stack: bool = False):
|
|
||||||
"""Convenience decorator für conditional monitoring"""
|
|
||||||
monitor = get_performance_monitor()
|
|
||||||
return monitor.monitor_operation(operation_name, capture_stack)
|
|
||||||
|
|
||||||
|
|
||||||
def monitor_race_conditions(operation_name: str = None):
|
|
||||||
"""Speziell für Race Condition Detection"""
|
|
||||||
return monitor_if_enabled(operation_name, capture_stack=True)
|
|
||||||
|
|
||||||
|
|
||||||
def monitor_fingerprint_operations(operation_name: str = None):
|
|
||||||
"""Speziell für Fingerprint-Operationen"""
|
|
||||||
return monitor_if_enabled(f"fingerprint_{operation_name}", capture_stack=False)
|
|
||||||
|
|
||||||
|
|
||||||
def monitor_session_operations(operation_name: str = None):
|
|
||||||
"""Speziell für Session-Operationen"""
|
|
||||||
return monitor_if_enabled(f"session_{operation_name}", capture_stack=False)
|
|
||||||
@ -7,6 +7,12 @@ Dieser Guard verhindert:
|
|||||||
- Mehrere Browser-Instanzen gleichzeitig
|
- Mehrere Browser-Instanzen gleichzeitig
|
||||||
|
|
||||||
Clean Code & YAGNI: Nur das Nötigste, keine Über-Engineering.
|
Clean Code & YAGNI: Nur das Nötigste, keine Über-Engineering.
|
||||||
|
|
||||||
|
WICHTIG - Korrekte Verwendung:
|
||||||
|
- start() → Prozess beginnt
|
||||||
|
- end(success=True/False) → Prozess endet normal (zählt für Failure-Tracking)
|
||||||
|
- release() → Prozess wird abgebrochen (zählt NICHT als Failure)
|
||||||
|
- Alle Methoden sind idempotent (mehrfacher Aufruf ist sicher)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -14,6 +20,7 @@ import logging
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
import threading
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -26,6 +33,9 @@ class ProcessGuard:
|
|||||||
- Process Lock Management (nur ein Prozess gleichzeitig)
|
- Process Lock Management (nur ein Prozess gleichzeitig)
|
||||||
- Fehler-Tracking (Zwangspause nach 3 Fehlern)
|
- Fehler-Tracking (Zwangspause nach 3 Fehlern)
|
||||||
- Persistierung der Pause-Zeit über Neustarts
|
- Persistierung der Pause-Zeit über Neustarts
|
||||||
|
|
||||||
|
Thread-Safety:
|
||||||
|
- Alle öffentlichen Methoden sind thread-safe durch Lock
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Konfiguration
|
# Konfiguration
|
||||||
@ -35,11 +45,15 @@ class ProcessGuard:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialisiert den Process Guard."""
|
"""Initialisiert den Process Guard."""
|
||||||
|
# Thread-Safety Lock
|
||||||
|
self._thread_lock = threading.Lock()
|
||||||
|
|
||||||
# Process Lock
|
# Process Lock
|
||||||
self._is_locked = False
|
self._is_locked = False
|
||||||
self._current_process = None
|
self._current_process = None
|
||||||
self._current_platform = None
|
self._current_platform = None
|
||||||
self._lock_started_at = None # Timestamp für Auto-Timeout
|
self._lock_started_at = None # Timestamp für Auto-Timeout
|
||||||
|
self._lock_id = None # Eindeutige ID für jeden Lock
|
||||||
|
|
||||||
# Error Tracking
|
# Error Tracking
|
||||||
self._failure_count = 0
|
self._failure_count = 0
|
||||||
@ -48,6 +62,9 @@ class ProcessGuard:
|
|||||||
# Config File
|
# Config File
|
||||||
self._config_file = Path("config/.process_guard")
|
self._config_file = Path("config/.process_guard")
|
||||||
|
|
||||||
|
# Counter für Lock-IDs
|
||||||
|
self._lock_counter = 0
|
||||||
|
|
||||||
def can_start(self, process_type: str, platform: str) -> Tuple[bool, Optional[str]]:
|
def can_start(self, process_type: str, platform: str) -> Tuple[bool, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Prüft ob ein Prozess gestartet werden darf.
|
Prüft ob ein Prozess gestartet werden darf.
|
||||||
@ -61,98 +78,172 @@ class ProcessGuard:
|
|||||||
- (True, None) wenn erlaubt
|
- (True, None) wenn erlaubt
|
||||||
- (False, "Fehlermeldung") wenn blockiert
|
- (False, "Fehlermeldung") wenn blockiert
|
||||||
"""
|
"""
|
||||||
# 1. Prüfe Zwangspause
|
with self._thread_lock:
|
||||||
if self._is_paused():
|
# 1. Prüfe Zwangspause
|
||||||
remaining_min = self._get_pause_remaining_minutes()
|
if self._is_paused():
|
||||||
error_msg = (
|
remaining_min = self._get_pause_remaining_minutes()
|
||||||
f"⏸ Zwangspause aktiv\n\n"
|
error_msg = (
|
||||||
f"Nach 3 fehlgeschlagenen Versuchen ist eine Pause erforderlich.\n"
|
f"⏸ Zwangspause aktiv\n\n"
|
||||||
f"Verbleibende Zeit: {remaining_min} Minuten\n\n"
|
f"Nach 3 fehlgeschlagenen Versuchen ist eine Pause erforderlich.\n"
|
||||||
f"Empfehlung:\n"
|
f"Verbleibende Zeit: {remaining_min} Minuten\n\n"
|
||||||
f"• Proxy-Einstellungen prüfen\n"
|
f"Empfehlung:\n"
|
||||||
f"• Internetverbindung prüfen\n"
|
f"• Proxy-Einstellungen prüfen\n"
|
||||||
f"• Plattform-Status überprüfen"
|
f"• Internetverbindung prüfen\n"
|
||||||
)
|
f"• Plattform-Status überprüfen"
|
||||||
return False, error_msg
|
)
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
# 2. Prüfe Process Lock
|
# 2. Prüfe Process Lock (mit Auto-Timeout-Check)
|
||||||
if self._is_locked:
|
if self._is_locked_with_timeout_check():
|
||||||
error_msg = (
|
error_msg = (
|
||||||
f"⚠ Prozess läuft bereits\n\n"
|
f"⚠ Prozess läuft bereits\n\n"
|
||||||
f"Aktuell aktiv: {self._current_process} ({self._current_platform})\n\n"
|
f"Aktuell aktiv: {self._current_process} ({self._current_platform})\n\n"
|
||||||
f"Bitte warten Sie bis der aktuelle Vorgang abgeschlossen ist."
|
f"Bitte warten Sie bis der aktuelle Vorgang abgeschlossen ist."
|
||||||
)
|
)
|
||||||
return False, error_msg
|
return False, error_msg
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
def start(self, process_type: str, platform: str):
|
def start(self, process_type: str, platform: str) -> int:
|
||||||
"""
|
"""
|
||||||
Startet einen Prozess (setzt den Lock).
|
Startet einen Prozess (setzt den Lock).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
process_type: Art des Prozesses
|
process_type: Art des Prozesses
|
||||||
platform: Plattform
|
platform: Plattform
|
||||||
"""
|
|
||||||
self._is_locked = True
|
|
||||||
self._current_process = process_type
|
|
||||||
self._current_platform = platform
|
|
||||||
self._lock_started_at = datetime.now() # Timestamp für Auto-Timeout
|
|
||||||
logger.info(f"Process locked: {process_type} ({platform})")
|
|
||||||
|
|
||||||
def end(self, success: bool):
|
Returns:
|
||||||
|
int: Lock-ID für diesen Prozess (für spätere Freigabe)
|
||||||
|
"""
|
||||||
|
with self._thread_lock:
|
||||||
|
self._lock_counter += 1
|
||||||
|
self._lock_id = self._lock_counter
|
||||||
|
self._is_locked = True
|
||||||
|
self._current_process = process_type
|
||||||
|
self._current_platform = platform
|
||||||
|
self._lock_started_at = datetime.now()
|
||||||
|
logger.info(f"Process locked [ID={self._lock_id}]: {process_type} ({platform})")
|
||||||
|
return self._lock_id
|
||||||
|
|
||||||
|
def end(self, success: bool) -> bool:
|
||||||
"""
|
"""
|
||||||
Beendet einen Prozess (gibt den Lock frei).
|
Beendet einen Prozess (gibt den Lock frei).
|
||||||
|
|
||||||
|
Diese Methode ist IDEMPOTENT - mehrfacher Aufruf ist sicher.
|
||||||
|
Der Failure-Counter wird nur erhöht wenn der Lock aktiv war.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
success: War der Prozess erfolgreich?
|
success: War der Prozess erfolgreich?
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Lock freigegeben wurde, False wenn kein Lock aktiv war
|
||||||
"""
|
"""
|
||||||
# Lock freigeben
|
with self._thread_lock:
|
||||||
process_info = f"{self._current_process} ({self._current_platform})"
|
# IDEMPOTENZ: Prüfe ob Lock überhaupt aktiv ist
|
||||||
self._is_locked = False
|
if not self._is_locked:
|
||||||
self._current_process = None
|
logger.debug("end() aufgerufen, aber kein Lock aktiv - ignoriere")
|
||||||
self._current_platform = None
|
return False
|
||||||
self._lock_started_at = None # Timestamp zurücksetzen
|
|
||||||
|
|
||||||
# Fehler-Tracking
|
# Lock-Info für Logging speichern
|
||||||
if success:
|
process_info = f"{self._current_process} ({self._current_platform})"
|
||||||
if self._failure_count > 0:
|
lock_id = self._lock_id
|
||||||
logger.info(f"Fehler-Counter zurückgesetzt nach Erfolg (war: {self._failure_count})")
|
|
||||||
self._failure_count = 0
|
|
||||||
self._save_pause_state()
|
|
||||||
else:
|
|
||||||
self._failure_count += 1
|
|
||||||
logger.warning(f"Fehlschlag #{self._failure_count} bei {process_info}")
|
|
||||||
|
|
||||||
if self._failure_count >= self.MAX_FAILURES:
|
# Lock freigeben
|
||||||
self._activate_pause()
|
self._is_locked = False
|
||||||
|
self._current_process = None
|
||||||
|
self._current_platform = None
|
||||||
|
self._lock_started_at = None
|
||||||
|
self._lock_id = None
|
||||||
|
|
||||||
logger.info(f"Process unlocked: {process_info} (success={success})")
|
# Fehler-Tracking (nur wenn Lock aktiv war)
|
||||||
|
if success:
|
||||||
|
if self._failure_count > 0:
|
||||||
|
logger.info(f"Fehler-Counter zurückgesetzt nach Erfolg (war: {self._failure_count})")
|
||||||
|
self._failure_count = 0
|
||||||
|
self._save_pause_state()
|
||||||
|
else:
|
||||||
|
self._failure_count += 1
|
||||||
|
logger.warning(f"Fehlschlag #{self._failure_count} bei {process_info}")
|
||||||
|
|
||||||
|
if self._failure_count >= self.MAX_FAILURES:
|
||||||
|
self._activate_pause()
|
||||||
|
|
||||||
|
logger.info(f"Process unlocked [ID={lock_id}]: {process_info} (success={success})")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def release(self) -> bool:
|
||||||
|
"""
|
||||||
|
Gibt den Lock frei OHNE den Failure-Counter zu beeinflussen.
|
||||||
|
|
||||||
|
Verwendung:
|
||||||
|
- User-Abbruch (Cancel-Button)
|
||||||
|
- Validierungsfehler VOR Prozessstart
|
||||||
|
- Cleanup bei App-Schließung
|
||||||
|
|
||||||
|
Diese Methode ist IDEMPOTENT - mehrfacher Aufruf ist sicher.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Lock freigegeben wurde, False wenn kein Lock aktiv war
|
||||||
|
"""
|
||||||
|
with self._thread_lock:
|
||||||
|
# IDEMPOTENZ: Prüfe ob Lock überhaupt aktiv ist
|
||||||
|
if not self._is_locked:
|
||||||
|
logger.debug("release() aufgerufen, aber kein Lock aktiv - ignoriere")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Lock-Info für Logging speichern
|
||||||
|
process_info = f"{self._current_process} ({self._current_platform})"
|
||||||
|
lock_id = self._lock_id
|
||||||
|
|
||||||
|
# Lock freigeben (OHNE Failure-Tracking)
|
||||||
|
self._is_locked = False
|
||||||
|
self._current_process = None
|
||||||
|
self._current_platform = None
|
||||||
|
self._lock_started_at = None
|
||||||
|
self._lock_id = None
|
||||||
|
|
||||||
|
logger.info(f"Process released [ID={lock_id}]: {process_info} (kein Failure gezählt)")
|
||||||
|
return True
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""
|
"""
|
||||||
Reset beim App-Start.
|
Reset beim App-Start.
|
||||||
Lädt Pause-State, resettet aber Lock (da Lock nicht über Neustarts persistiert).
|
Lädt Pause-State, resettet aber Lock (da Lock nicht über Neustarts persistiert).
|
||||||
"""
|
"""
|
||||||
self._is_locked = False
|
with self._thread_lock:
|
||||||
self._current_process = None
|
self._is_locked = False
|
||||||
self._current_platform = None
|
self._current_process = None
|
||||||
self._lock_started_at = None # Timestamp zurücksetzen
|
self._current_platform = None
|
||||||
self._load_pause_state()
|
self._lock_started_at = None
|
||||||
|
self._lock_id = None
|
||||||
|
self._load_pause_state()
|
||||||
|
|
||||||
if self._is_paused():
|
if self._is_paused():
|
||||||
remaining = self._get_pause_remaining_minutes()
|
remaining = self._get_pause_remaining_minutes()
|
||||||
logger.warning(f"Zwangspause aktiv: noch {remaining} Minuten")
|
logger.warning(f"Zwangspause aktiv: noch {remaining} Minuten")
|
||||||
|
|
||||||
logger.info("Process Guard initialisiert")
|
logger.info("Process Guard initialisiert")
|
||||||
|
|
||||||
def is_locked(self) -> bool:
|
def is_locked(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Gibt zurück ob aktuell ein Prozess läuft (mit Auto-Timeout-Check).
|
Gibt zurück ob aktuell ein Prozess läuft (mit Auto-Timeout-Check).
|
||||||
|
|
||||||
|
Thread-safe Methode.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True wenn ein Prozess aktiv ist
|
True wenn ein Prozess aktiv ist
|
||||||
"""
|
"""
|
||||||
|
with self._thread_lock:
|
||||||
|
return self._is_locked_with_timeout_check()
|
||||||
|
|
||||||
|
def _is_locked_with_timeout_check(self) -> bool:
|
||||||
|
"""
|
||||||
|
Interne Methode: Prüft Lock-Status mit Auto-Timeout.
|
||||||
|
MUSS innerhalb eines _thread_lock aufgerufen werden!
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn Lock aktiv ist
|
||||||
|
"""
|
||||||
if not self._is_locked:
|
if not self._is_locked:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -162,14 +253,15 @@ class ProcessGuard:
|
|||||||
|
|
||||||
if elapsed_minutes > self.LOCK_TIMEOUT_MINUTES:
|
if elapsed_minutes > self.LOCK_TIMEOUT_MINUTES:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"⏰ AUTO-TIMEOUT: Lock nach {int(elapsed_minutes)} Minuten freigegeben. "
|
f"⏰ AUTO-TIMEOUT: Lock [ID={self._lock_id}] nach {int(elapsed_minutes)} Minuten freigegeben. "
|
||||||
f"Prozess: {self._current_process} ({self._current_platform})"
|
f"Prozess: {self._current_process} ({self._current_platform})"
|
||||||
)
|
)
|
||||||
# Lock automatisch freigeben
|
# Lock automatisch freigeben (OHNE Failure-Zählung - Timeout ist kein User-Fehler)
|
||||||
self._is_locked = False
|
self._is_locked = False
|
||||||
self._current_process = None
|
self._current_process = None
|
||||||
self._current_platform = None
|
self._current_platform = None
|
||||||
self._lock_started_at = None
|
self._lock_started_at = None
|
||||||
|
self._lock_id = None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -178,26 +270,44 @@ class ProcessGuard:
|
|||||||
"""
|
"""
|
||||||
Gibt zurück ob Zwangspause aktiv ist.
|
Gibt zurück ob Zwangspause aktiv ist.
|
||||||
|
|
||||||
|
Thread-safe Methode.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True wenn Pause aktiv ist
|
True wenn Pause aktiv ist
|
||||||
"""
|
"""
|
||||||
return self._is_paused()
|
with self._thread_lock:
|
||||||
|
return self._is_paused()
|
||||||
|
|
||||||
def get_status_message(self) -> Optional[str]:
|
def get_status_message(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Gibt Status-Nachricht zurück wenn blockiert.
|
Gibt Status-Nachricht zurück wenn blockiert.
|
||||||
|
|
||||||
|
Thread-safe Methode.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None wenn nicht blockiert, sonst Nachricht
|
None wenn nicht blockiert, sonst Nachricht
|
||||||
"""
|
"""
|
||||||
if self._is_paused():
|
with self._thread_lock:
|
||||||
remaining = self._get_pause_remaining_minutes()
|
if self._is_paused():
|
||||||
return f"Zwangspause aktiv (noch {remaining} Min)"
|
remaining = self._get_pause_remaining_minutes()
|
||||||
|
return f"Zwangspause aktiv (noch {remaining} Min)"
|
||||||
|
|
||||||
if self._is_locked:
|
if self._is_locked:
|
||||||
return f"'{self._current_process}' läuft"
|
return f"'{self._current_process}' läuft"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_failure_count(self) -> int:
|
||||||
|
"""
|
||||||
|
Gibt den aktuellen Failure-Counter zurück.
|
||||||
|
|
||||||
|
Thread-safe Methode.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Anzahl der Fehlschläge seit letztem Erfolg
|
||||||
|
"""
|
||||||
|
with self._thread_lock:
|
||||||
|
return self._failure_count
|
||||||
|
|
||||||
# Private Methoden
|
# Private Methoden
|
||||||
|
|
||||||
@ -273,18 +383,24 @@ class ProcessGuard:
|
|||||||
logger.error(f"Fehler beim Laden des Pause-State: {e}")
|
logger.error(f"Fehler beim Laden des Pause-State: {e}")
|
||||||
|
|
||||||
|
|
||||||
# Globale Instanz (YAGNI: Kein komplexes Singleton-Pattern nötig)
|
# Globale Instanz mit Thread-Safety
|
||||||
_guard_instance = None
|
_guard_instance = None
|
||||||
|
_guard_instance_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def get_guard() -> ProcessGuard:
|
def get_guard() -> ProcessGuard:
|
||||||
"""
|
"""
|
||||||
Gibt die globale ProcessGuard-Instanz zurück.
|
Gibt die globale ProcessGuard-Instanz zurück.
|
||||||
|
|
||||||
|
Thread-safe Singleton-Pattern.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ProcessGuard: Die globale Guard-Instanz
|
ProcessGuard: Die globale Guard-Instanz
|
||||||
"""
|
"""
|
||||||
global _guard_instance
|
global _guard_instance
|
||||||
if _guard_instance is None:
|
if _guard_instance is None:
|
||||||
_guard_instance = ProcessGuard()
|
with _guard_instance_lock:
|
||||||
|
# Double-check locking
|
||||||
|
if _guard_instance is None:
|
||||||
|
_guard_instance = ProcessGuard()
|
||||||
return _guard_instance
|
return _guard_instance
|
||||||
|
|||||||
@ -206,8 +206,8 @@ class ProfileExportService:
|
|||||||
spaceBefore=5*mm
|
spaceBefore=5*mm
|
||||||
)
|
)
|
||||||
|
|
||||||
# IntelSight Logo versuchen zu laden
|
# AegisSight Logo versuchen zu laden
|
||||||
logo_path = Path("resources/icons/intelsight-logo.svg")
|
logo_path = Path("resources/icons/aegissight-logo.svg")
|
||||||
if logo_path.exists():
|
if logo_path.exists():
|
||||||
try:
|
try:
|
||||||
# SVG zu reportlab Image (mit svglib falls verfügbar)
|
# SVG zu reportlab Image (mit svglib falls verfügbar)
|
||||||
|
|||||||
@ -383,19 +383,19 @@ class ProxyRotator:
|
|||||||
def format_proxy_for_playwright(self, proxy: str) -> Dict[str, str]:
|
def format_proxy_for_playwright(self, proxy: str) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Formatiert einen Proxy-String für die Verwendung mit Playwright.
|
Formatiert einen Proxy-String für die Verwendung mit Playwright.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
proxy: Proxy-String im Format host:port:username:password
|
proxy: Proxy-String im Format host:port:username:password
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary mit Playwright-Proxy-Konfiguration
|
Dictionary mit Playwright-Proxy-Konfiguration
|
||||||
"""
|
"""
|
||||||
parts = proxy.split(":")
|
parts = proxy.split(":")
|
||||||
|
|
||||||
if len(parts) >= 4:
|
if len(parts) >= 4:
|
||||||
# Format: host:port:username:password
|
# Format: host:port:username:password
|
||||||
host, port, username, password = parts[:4]
|
host, port, username, password = parts[:4]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"server": f"{host}:{port}",
|
"server": f"{host}:{port}",
|
||||||
"username": username,
|
"username": username,
|
||||||
@ -404,10 +404,110 @@ class ProxyRotator:
|
|||||||
elif len(parts) >= 2:
|
elif len(parts) >= 2:
|
||||||
# Format: host:port
|
# Format: host:port
|
||||||
host, port = parts[:2]
|
host, port = parts[:2]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"server": f"{host}:{port}"
|
"server": f"{host}:{port}"
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}")
|
logger.warning(f"Ungültiges Proxy-Format: {self.mask_proxy_credentials(proxy)}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# ANTI-DETECTION: Erzwungene Proxy-Rotation für Account-Registrierung
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
def force_rotation(self, proxy_type: str = None) -> Optional[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Erzwingt eine sofortige Proxy-Rotation.
|
||||||
|
|
||||||
|
Diese Methode sollte VOR jeder neuen Account-Registrierung aufgerufen
|
||||||
|
werden, um sicherzustellen, dass ein frischer Proxy verwendet wird.
|
||||||
|
Dies verhindert, dass mehrere Accounts von derselben IP erstellt werden.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
proxy_type: Proxy-Typ ("ipv4", "ipv6", "mobile") oder None für zufällig
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Neue Proxy-Konfiguration (Dict) oder None wenn kein Proxy verfügbar
|
||||||
|
"""
|
||||||
|
logger.info("ERZWINGE Proxy-Rotation für neue Registrierung")
|
||||||
|
|
||||||
|
# Vorherigen Proxy vergessen
|
||||||
|
old_proxy = self.current_proxy
|
||||||
|
self.current_proxy = None
|
||||||
|
self.last_rotation_time = 0
|
||||||
|
|
||||||
|
# Neuen Proxy holen
|
||||||
|
new_proxy = self.get_proxy(proxy_type)
|
||||||
|
|
||||||
|
if new_proxy:
|
||||||
|
self.current_proxy = new_proxy.get('server', '')
|
||||||
|
self.last_rotation_time = time.time()
|
||||||
|
|
||||||
|
# Log mit maskierten Credentials
|
||||||
|
masked_server = self.mask_proxy_credentials(self.current_proxy)
|
||||||
|
logger.info(f"Proxy rotiert: {masked_server}")
|
||||||
|
|
||||||
|
if old_proxy:
|
||||||
|
logger.debug(f"Vorheriger Proxy: {self.mask_proxy_credentials(old_proxy)}")
|
||||||
|
else:
|
||||||
|
logger.warning("Kein Proxy verfügbar für erzwungene Rotation")
|
||||||
|
|
||||||
|
return new_proxy
|
||||||
|
|
||||||
|
def get_proxy_for_registration(self, proxy_type: str = None,
|
||||||
|
force_new: bool = True) -> Optional[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Holt einen Proxy speziell für Account-Registrierung.
|
||||||
|
|
||||||
|
Diese Methode ist ein Wrapper um force_rotation() mit zusätzlicher
|
||||||
|
Logik für Registrierungen.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
proxy_type: Gewünschter Proxy-Typ oder None für zufällig
|
||||||
|
force_new: Ob ein neuer Proxy erzwungen werden soll (Standard: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Proxy-Konfiguration für Playwright oder None
|
||||||
|
"""
|
||||||
|
if force_new:
|
||||||
|
proxy_config = self.force_rotation(proxy_type)
|
||||||
|
else:
|
||||||
|
proxy_config = self.get_proxy(proxy_type)
|
||||||
|
|
||||||
|
if not proxy_config:
|
||||||
|
logger.warning("Kein Proxy für Registrierung verfügbar - Registrierung ohne Proxy")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(f"Proxy für Registrierung bereit: {self.mask_proxy_credentials(proxy_config.get('server', ''))}")
|
||||||
|
return proxy_config
|
||||||
|
|
||||||
|
def should_rotate_for_registration(self) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob eine Proxy-Rotation vor der nächsten Registrierung empfohlen wird.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn Rotation empfohlen, False sonst
|
||||||
|
"""
|
||||||
|
# Immer True - jede Registrierung sollte einen neuen Proxy verwenden
|
||||||
|
# Dies ist die sicherste Anti-Detection-Strategie
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_rotation_stats(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Gibt Statistiken über Proxy-Rotationen zurück.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mit Rotations-Statistiken
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"current_proxy": self.mask_proxy_credentials(self.current_proxy) if self.current_proxy else None,
|
||||||
|
"last_rotation_time": self.last_rotation_time,
|
||||||
|
"time_since_last_rotation": time.time() - self.last_rotation_time if self.last_rotation_time > 0 else None,
|
||||||
|
"rotation_interval": self.config.get("rotation_interval", 300),
|
||||||
|
"available_proxies": {
|
||||||
|
"ipv4": len(self.config.get("ipv4", [])),
|
||||||
|
"ipv6": len(self.config.get("ipv6", [])),
|
||||||
|
"mobile": len(self.config.get("mobile", []))
|
||||||
|
}
|
||||||
|
}
|
||||||
351
utils/rate_limit_handler.py
Normale Datei
351
utils/rate_limit_handler.py
Normale Datei
@ -0,0 +1,351 @@
|
|||||||
|
"""
|
||||||
|
Rate Limit Handler für HTTP 429 und ähnliche Fehler.
|
||||||
|
|
||||||
|
Dieses Modul implementiert exponentielles Backoff für Rate-Limiting,
|
||||||
|
um automatisch auf zu viele Anfragen zu reagieren und Sperren zu vermeiden.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from typing import Callable, Any, Optional, List
|
||||||
|
|
||||||
|
logger = logging.getLogger("rate_limit_handler")
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitHandler:
|
||||||
|
"""
|
||||||
|
Behandelt Rate-Limits mit exponentiellem Backoff.
|
||||||
|
|
||||||
|
Diese Klasse implementiert eine robuste Strategie zum Umgang mit
|
||||||
|
Rate-Limiting durch soziale Netzwerke. Bei Erkennung eines Rate-Limits
|
||||||
|
wird exponentiell länger gewartet, um Sperren zu vermeiden.
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
handler = RateLimitHandler()
|
||||||
|
|
||||||
|
# Option 1: Manuelles Handling
|
||||||
|
if rate_limit_detected:
|
||||||
|
handler.handle_rate_limit()
|
||||||
|
|
||||||
|
# Option 2: Automatisches Retry
|
||||||
|
result = handler.execute_with_backoff(my_function, arg1, arg2)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Bekannte Rate-Limit-Indikatoren
|
||||||
|
RATE_LIMIT_INDICATORS = [
|
||||||
|
# HTTP Status Codes
|
||||||
|
'429',
|
||||||
|
'rate limit',
|
||||||
|
'rate_limit',
|
||||||
|
'ratelimit',
|
||||||
|
# Englische Meldungen
|
||||||
|
'too many requests',
|
||||||
|
'too many attempts',
|
||||||
|
'slow down',
|
||||||
|
'try again later',
|
||||||
|
'temporarily blocked',
|
||||||
|
'please wait',
|
||||||
|
'request blocked',
|
||||||
|
# Deutsche Meldungen
|
||||||
|
'zu viele anfragen',
|
||||||
|
'zu viele versuche',
|
||||||
|
'später erneut versuchen',
|
||||||
|
'vorübergehend gesperrt',
|
||||||
|
'bitte warten',
|
||||||
|
# Plattform-spezifische Meldungen
|
||||||
|
'challenge_required', # Instagram
|
||||||
|
'checkpoint_required', # Instagram/Facebook
|
||||||
|
'feedback_required', # Instagram
|
||||||
|
'spam', # Generisch
|
||||||
|
'suspicious activity', # Generisch
|
||||||
|
'unusual activity', # Generisch
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
initial_delay: float = 60.0,
|
||||||
|
max_delay: float = 600.0,
|
||||||
|
backoff_multiplier: float = 2.0,
|
||||||
|
max_retries: int = 5,
|
||||||
|
jitter_factor: float = 0.2):
|
||||||
|
"""
|
||||||
|
Initialisiert den Rate-Limit-Handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
initial_delay: Anfängliche Wartezeit in Sekunden (Standard: 60s = 1 Minute)
|
||||||
|
max_delay: Maximale Wartezeit in Sekunden (Standard: 600s = 10 Minuten)
|
||||||
|
backoff_multiplier: Multiplikator für exponentielles Backoff (Standard: 2.0)
|
||||||
|
max_retries: Maximale Anzahl an Wiederholungsversuchen (Standard: 5)
|
||||||
|
jitter_factor: Faktor für zufällige Variation (Standard: 0.2 = ±20%)
|
||||||
|
"""
|
||||||
|
self.initial_delay = initial_delay
|
||||||
|
self.max_delay = max_delay
|
||||||
|
self.backoff_multiplier = backoff_multiplier
|
||||||
|
self.max_retries = max_retries
|
||||||
|
self.jitter_factor = jitter_factor
|
||||||
|
|
||||||
|
# Status-Tracking
|
||||||
|
self.current_retry = 0
|
||||||
|
self.last_rate_limit_time = 0
|
||||||
|
self.total_rate_limits = 0
|
||||||
|
self.consecutive_successes = 0
|
||||||
|
|
||||||
|
def is_rate_limited(self, response_text: str) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob eine Antwort auf ein Rate-Limit hindeutet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response_text: Text der Antwort (z.B. Seiteninhalt, Fehlermeldung)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn Rate-Limit erkannt wurde, sonst False
|
||||||
|
"""
|
||||||
|
if not response_text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
response_lower = response_text.lower()
|
||||||
|
|
||||||
|
for indicator in self.RATE_LIMIT_INDICATORS:
|
||||||
|
if indicator in response_lower:
|
||||||
|
logger.warning(f"Rate-Limit Indikator gefunden: '{indicator}'")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def calculate_delay(self, retry_count: int = None) -> float:
|
||||||
|
"""
|
||||||
|
Berechnet die Backoff-Verzögerung.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
retry_count: Aktueller Wiederholungsversuch (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Verzögerung in Sekunden
|
||||||
|
"""
|
||||||
|
if retry_count is None:
|
||||||
|
retry_count = self.current_retry
|
||||||
|
|
||||||
|
# Exponentielles Backoff berechnen
|
||||||
|
delay = self.initial_delay * (self.backoff_multiplier ** retry_count)
|
||||||
|
|
||||||
|
# Jitter hinzufügen (zufällige Variation)
|
||||||
|
jitter = delay * random.uniform(-self.jitter_factor, self.jitter_factor)
|
||||||
|
delay = delay + jitter
|
||||||
|
|
||||||
|
# Maximum nicht überschreiten
|
||||||
|
delay = min(delay, self.max_delay)
|
||||||
|
|
||||||
|
return delay
|
||||||
|
|
||||||
|
def handle_rate_limit(self, retry_count: int = None,
|
||||||
|
on_waiting: Optional[Callable[[float, int], None]] = None) -> float:
|
||||||
|
"""
|
||||||
|
Behandelt ein erkanntes Rate-Limit mit Backoff.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
retry_count: Aktueller Wiederholungsversuch
|
||||||
|
on_waiting: Optionaler Callback während des Wartens (delay, retry)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tatsächlich gewartete Zeit in Sekunden
|
||||||
|
"""
|
||||||
|
if retry_count is None:
|
||||||
|
retry_count = self.current_retry
|
||||||
|
|
||||||
|
delay = self.calculate_delay(retry_count)
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"Rate-Limit erkannt! Warte {delay:.1f}s "
|
||||||
|
f"(Versuch {retry_count + 1}/{self.max_retries})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Callback aufrufen falls vorhanden
|
||||||
|
if on_waiting:
|
||||||
|
on_waiting(delay, retry_count + 1)
|
||||||
|
|
||||||
|
# Warten
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
# Status aktualisieren
|
||||||
|
self.current_retry = retry_count + 1
|
||||||
|
self.last_rate_limit_time = time.time()
|
||||||
|
self.total_rate_limits += 1
|
||||||
|
self.consecutive_successes = 0
|
||||||
|
|
||||||
|
return delay
|
||||||
|
|
||||||
|
def execute_with_backoff(self, func: Callable, *args,
|
||||||
|
on_retry: Optional[Callable[[int, Exception], None]] = None,
|
||||||
|
**kwargs) -> Any:
|
||||||
|
"""
|
||||||
|
Führt eine Funktion mit automatischem Backoff bei Rate-Limits aus.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: Auszuführende Funktion
|
||||||
|
*args: Positionsargumente für die Funktion
|
||||||
|
on_retry: Optionaler Callback bei Retry (retry_count, exception)
|
||||||
|
**kwargs: Keyword-Argumente für die Funktion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Rückgabewert der Funktion oder None bei Fehler
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Wenn max_retries erreicht oder nicht-Rate-Limit-Fehler
|
||||||
|
"""
|
||||||
|
last_exception = None
|
||||||
|
|
||||||
|
for attempt in range(self.max_retries):
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
|
||||||
|
# Erfolg - Reset Retry-Zähler
|
||||||
|
self.current_retry = 0
|
||||||
|
self.consecutive_successes += 1
|
||||||
|
|
||||||
|
# Nach mehreren Erfolgen: Backoff-Zähler langsam reduzieren
|
||||||
|
if self.consecutive_successes >= 3:
|
||||||
|
self.total_rate_limits = max(0, self.total_rate_limits - 1)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
last_exception = e
|
||||||
|
error_str = str(e).lower()
|
||||||
|
|
||||||
|
# Prüfe auf Rate-Limit-Indikatoren
|
||||||
|
is_rate_limit = any(
|
||||||
|
indicator in error_str
|
||||||
|
for indicator in self.RATE_LIMIT_INDICATORS
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_rate_limit:
|
||||||
|
logger.warning(f"Rate-Limit Exception erkannt: {e}")
|
||||||
|
|
||||||
|
if on_retry:
|
||||||
|
on_retry(attempt, e)
|
||||||
|
|
||||||
|
self.handle_rate_limit(attempt)
|
||||||
|
else:
|
||||||
|
# Anderer Fehler - nicht durch Backoff lösbar
|
||||||
|
logger.error(f"Nicht-Rate-Limit Fehler: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Maximum erreicht
|
||||||
|
logger.error(
|
||||||
|
f"Maximale Wiederholungsversuche ({self.max_retries}) erreicht. "
|
||||||
|
f"Letzter Fehler: {last_exception}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def should_slow_down(self) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob die Geschwindigkeit reduziert werden sollte.
|
||||||
|
|
||||||
|
Basierend auf der Anzahl der kürzlichen Rate-Limits wird empfohlen,
|
||||||
|
ob zusätzliche Verzögerungen eingebaut werden sollten.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn Verlangsamung empfohlen, sonst False
|
||||||
|
"""
|
||||||
|
# Wenn kürzlich (< 5 min) ein Rate-Limit war
|
||||||
|
time_since_last = time.time() - self.last_rate_limit_time
|
||||||
|
if time_since_last < 300 and self.last_rate_limit_time > 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Wenn viele Rate-Limits insgesamt
|
||||||
|
if self.total_rate_limits >= 3:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_recommended_delay(self) -> float:
|
||||||
|
"""
|
||||||
|
Gibt eine empfohlene zusätzliche Verzögerung zurück.
|
||||||
|
|
||||||
|
Basierend auf dem aktuellen Status wird eine Verzögerung empfohlen,
|
||||||
|
die zwischen Aktionen eingefügt werden sollte.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Empfohlene Verzögerung in Sekunden
|
||||||
|
"""
|
||||||
|
if not self.should_slow_down():
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Basis-Verzögerung basierend auf Anzahl der Rate-Limits
|
||||||
|
base_delay = 5.0 * self.total_rate_limits
|
||||||
|
|
||||||
|
# Zusätzliche Verzögerung wenn kürzlich Rate-Limit war
|
||||||
|
time_since_last = time.time() - self.last_rate_limit_time
|
||||||
|
if time_since_last < 300:
|
||||||
|
# Je kürzer her, desto länger warten
|
||||||
|
recency_factor = 1.0 - (time_since_last / 300)
|
||||||
|
base_delay += 10.0 * recency_factor
|
||||||
|
|
||||||
|
return min(base_delay, 30.0) # Maximum 30 Sekunden
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Setzt den Handler auf Anfangszustand zurück."""
|
||||||
|
self.current_retry = 0
|
||||||
|
self.last_rate_limit_time = 0
|
||||||
|
self.total_rate_limits = 0
|
||||||
|
self.consecutive_successes = 0
|
||||||
|
logger.info("Rate-Limit Handler zurückgesetzt")
|
||||||
|
|
||||||
|
def get_status(self) -> dict:
|
||||||
|
"""
|
||||||
|
Gibt den aktuellen Status des Handlers zurück.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mit Status-Informationen
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"current_retry": self.current_retry,
|
||||||
|
"total_rate_limits": self.total_rate_limits,
|
||||||
|
"consecutive_successes": self.consecutive_successes,
|
||||||
|
"last_rate_limit_time": self.last_rate_limit_time,
|
||||||
|
"should_slow_down": self.should_slow_down(),
|
||||||
|
"recommended_delay": self.get_recommended_delay(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Globale Instanz für einfache Verwendung
|
||||||
|
_default_handler: Optional[RateLimitHandler] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_handler() -> RateLimitHandler:
|
||||||
|
"""
|
||||||
|
Gibt die globale Standard-Instanz des Rate-Limit-Handlers zurück.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RateLimitHandler-Instanz
|
||||||
|
"""
|
||||||
|
global _default_handler
|
||||||
|
if _default_handler is None:
|
||||||
|
_default_handler = RateLimitHandler()
|
||||||
|
return _default_handler
|
||||||
|
|
||||||
|
|
||||||
|
def handle_rate_limit(retry_count: int = None) -> float:
|
||||||
|
"""
|
||||||
|
Convenience-Funktion für Rate-Limit-Handling mit Standard-Handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
retry_count: Aktueller Wiederholungsversuch
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Gewartete Zeit in Sekunden
|
||||||
|
"""
|
||||||
|
return get_default_handler().handle_rate_limit(retry_count)
|
||||||
|
|
||||||
|
|
||||||
|
def is_rate_limited(response_text: str) -> bool:
|
||||||
|
"""
|
||||||
|
Convenience-Funktion für Rate-Limit-Erkennung.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response_text: Zu prüfender Text
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn Rate-Limit erkannt
|
||||||
|
"""
|
||||||
|
return get_default_handler().is_rate_limited(response_text)
|
||||||
@ -191,9 +191,9 @@ class ThemeManager(QObject):
|
|||||||
return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg")
|
return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg")
|
||||||
|
|
||||||
# Logo is theme-specific
|
# Logo is theme-specific
|
||||||
if icon_name == "intelsight-logo":
|
if icon_name == "aegissight-logo":
|
||||||
theme = ThemeConfig.get_theme(self.current_theme)
|
theme = ThemeConfig.get_theme(self.current_theme)
|
||||||
logo_name = theme.get('logo_path', 'intelsight-logo.svg').replace('.svg', '')
|
logo_name = theme.get('logo_path', 'aegissight-logo.svg').replace('.svg', '')
|
||||||
return os.path.join(self.base_dir, "resources", "icons", f"{logo_name}.svg")
|
return os.path.join(self.base_dir, "resources", "icons", f"{logo_name}.svg")
|
||||||
|
|
||||||
# For other icons
|
# For other icons
|
||||||
|
|||||||
@ -295,21 +295,22 @@ class UsernameGenerator:
|
|||||||
Generierter Benutzername
|
Generierter Benutzername
|
||||||
"""
|
"""
|
||||||
# Verschiedene Muster für zufällige Benutzernamen
|
# Verschiedene Muster für zufällige Benutzernamen
|
||||||
|
# ANTI-DETECTION: Keine verdächtigen Patterns wie "user" + Zahlen
|
||||||
patterns = [
|
patterns = [
|
||||||
# Adjektiv + Substantiv
|
# Adjektiv + Substantiv (z.B. "happytiger")
|
||||||
lambda: random.choice(self.adjectives) + random.choice(self.nouns),
|
lambda: random.choice(self.adjectives) + random.choice(self.nouns),
|
||||||
|
|
||||||
# Substantiv + Zahlen
|
# Substantiv + Jahr (z.B. "eagle1995")
|
||||||
lambda: random.choice(self.nouns) + "".join(random.choices(string.digits, k=random.randint(1, 4))),
|
lambda: random.choice(self.nouns) + str(random.randint(1985, 2005)),
|
||||||
|
|
||||||
# Adjektiv + Substantiv + Zahlen
|
# Adjektiv + Substantiv + 2 Ziffern (z.B. "coolwolf42")
|
||||||
lambda: random.choice(self.adjectives) + random.choice(self.nouns) + "".join(random.choices(string.digits, k=random.randint(1, 3))),
|
lambda: random.choice(self.adjectives) + random.choice(self.nouns) + str(random.randint(10, 99)),
|
||||||
|
|
||||||
# Substantiv + Unterstrich + Substantiv
|
# Substantiv + Unterstrich + Adjektiv (z.B. "tiger_happy")
|
||||||
lambda: random.choice(self.nouns) + ("_" if "_" in policy["allowed_chars"] else "") + random.choice(self.nouns),
|
lambda: random.choice(self.nouns) + ("_" if "_" in policy["allowed_chars"] else "") + random.choice(self.adjectives),
|
||||||
|
|
||||||
# Benutzer + Zahlen
|
# Adjektiv + Substantiv mit Punkt (z.B. "happy.tiger") - falls erlaubt
|
||||||
lambda: "user" + "".join(random.choices(string.digits, k=random.randint(3, 6)))
|
lambda: random.choice(self.adjectives) + ("." if "." in policy["allowed_chars"] else "") + random.choice(self.nouns),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Zufälliges Muster auswählen und Benutzernamen generieren
|
# Zufälliges Muster auswählen und Benutzernamen generieren
|
||||||
@ -417,49 +418,221 @@ class UsernameGenerator:
|
|||||||
policy: Optional[Dict[str, Any]] = None) -> Tuple[bool, str]:
|
policy: Optional[Dict[str, Any]] = None) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Überprüft, ob ein Benutzername den Richtlinien entspricht.
|
Überprüft, ob ein Benutzername den Richtlinien entspricht.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
username: Zu überprüfender Benutzername
|
username: Zu überprüfender Benutzername
|
||||||
platform: Name der Plattform
|
platform: Name der Plattform
|
||||||
policy: Optionale Richtlinie (sonst wird die der Plattform verwendet)
|
policy: Optionale Richtlinie (sonst wird die der Plattform verwendet)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(Gültigkeit, Fehlermeldung)
|
(Gültigkeit, Fehlermeldung)
|
||||||
"""
|
"""
|
||||||
# Richtlinie bestimmen
|
# Richtlinie bestimmen
|
||||||
if not policy:
|
if not policy:
|
||||||
policy = self.get_platform_policy(platform)
|
policy = self.get_platform_policy(platform)
|
||||||
|
|
||||||
# Länge prüfen
|
# Länge prüfen
|
||||||
if len(username) < policy["min_length"]:
|
if len(username) < policy["min_length"]:
|
||||||
return False, f"Benutzername ist zu kurz (mindestens {policy['min_length']} Zeichen erforderlich)"
|
return False, f"Benutzername ist zu kurz (mindestens {policy['min_length']} Zeichen erforderlich)"
|
||||||
|
|
||||||
if len(username) > policy["max_length"]:
|
if len(username) > policy["max_length"]:
|
||||||
return False, f"Benutzername ist zu lang (maximal {policy['max_length']} Zeichen erlaubt)"
|
return False, f"Benutzername ist zu lang (maximal {policy['max_length']} Zeichen erlaubt)"
|
||||||
|
|
||||||
# Erlaubte Zeichen prüfen
|
# Erlaubte Zeichen prüfen
|
||||||
for char in username:
|
for char in username:
|
||||||
if char not in policy["allowed_chars"]:
|
if char not in policy["allowed_chars"]:
|
||||||
return False, f"Unerlaubtes Zeichen: '{char}'"
|
return False, f"Unerlaubtes Zeichen: '{char}'"
|
||||||
|
|
||||||
# Anfangszeichen prüfen
|
# Anfangszeichen prüfen
|
||||||
if username[0] not in policy["allowed_start_chars"]:
|
if username[0] not in policy["allowed_start_chars"]:
|
||||||
return False, f"Benutzername darf nicht mit '{username[0]}' beginnen"
|
return False, f"Benutzername darf nicht mit '{username[0]}' beginnen"
|
||||||
|
|
||||||
# Endzeichen prüfen
|
# Endzeichen prüfen
|
||||||
if username[-1] not in policy["allowed_end_chars"]:
|
if username[-1] not in policy["allowed_end_chars"]:
|
||||||
return False, f"Benutzername darf nicht mit '{username[-1]}' enden"
|
return False, f"Benutzername darf nicht mit '{username[-1]}' enden"
|
||||||
|
|
||||||
# Aufeinanderfolgende Sonderzeichen prüfen
|
# Aufeinanderfolgende Sonderzeichen prüfen
|
||||||
if not policy["allowed_consecutive_special"]:
|
if not policy["allowed_consecutive_special"]:
|
||||||
special_chars = set(policy["allowed_chars"]) - set(string.ascii_letters + string.digits)
|
special_chars = set(policy["allowed_chars"]) - set(string.ascii_letters + string.digits)
|
||||||
for i in range(len(username) - 1):
|
for i in range(len(username) - 1):
|
||||||
if username[i] in special_chars and username[i+1] in special_chars:
|
if username[i] in special_chars and username[i+1] in special_chars:
|
||||||
return False, "Keine aufeinanderfolgenden Sonderzeichen erlaubt"
|
return False, "Keine aufeinanderfolgenden Sonderzeichen erlaubt"
|
||||||
|
|
||||||
# Disallowed words
|
# Disallowed words
|
||||||
for word in policy["disallowed_words"]:
|
for word in policy["disallowed_words"]:
|
||||||
if word.lower() in username.lower():
|
if word.lower() in username.lower():
|
||||||
return False, f"Der Benutzername darf '{word}' nicht enthalten"
|
return False, f"Der Benutzername darf '{word}' nicht enthalten"
|
||||||
|
|
||||||
return True, "Benutzername ist gültig"
|
# ANTI-DETECTION: Prüfe auf verdächtige Bot-Patterns
|
||||||
|
if self._has_suspicious_pattern(username):
|
||||||
|
return False, "Benutzername enthält verdächtiges Bot-Pattern"
|
||||||
|
|
||||||
|
return True, "Benutzername ist gültig"
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# ANTI-DETECTION: Verdächtige Pattern-Erkennung
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
|
def _has_suspicious_pattern(self, username: str) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob ein Benutzername verdächtige Bot-Patterns enthält.
|
||||||
|
|
||||||
|
Diese Methode erkennt Benutzernamen-Muster, die häufig von Bots
|
||||||
|
verwendet werden und daher von Plattformen leicht erkannt werden.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: Zu prüfender Benutzername
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn verdächtig, False wenn ok
|
||||||
|
"""
|
||||||
|
username_lower = username.lower()
|
||||||
|
|
||||||
|
# Liste verdächtiger Patterns (Regex)
|
||||||
|
suspicious_patterns = [
|
||||||
|
# Plattform-spezifische Bot-Prefixe
|
||||||
|
r'^fb_', # Facebook Bot-Pattern
|
||||||
|
r'^ig_', # Instagram Bot-Pattern
|
||||||
|
r'^tw_', # Twitter Bot-Pattern
|
||||||
|
r'^tt_', # TikTok Bot-Pattern
|
||||||
|
|
||||||
|
# Offensichtliche Bot/Test-Prefixe
|
||||||
|
r'^bot_', # Offensichtlicher Bot
|
||||||
|
r'^test_', # Test-Account
|
||||||
|
r'^temp_', # Temporär
|
||||||
|
r'^fake_', # Offensichtlich fake
|
||||||
|
r'^new_', # Neu (suspekt)
|
||||||
|
r'^auto_', # Automatisierung
|
||||||
|
|
||||||
|
# Notfall/Backup-Patterns (aus altem Code)
|
||||||
|
r'_emergency_', # Notfall-Pattern
|
||||||
|
r'_backup_', # Backup-Pattern
|
||||||
|
r'^emergency_', # Emergency am Anfang
|
||||||
|
r'^backup_', # Backup am Anfang
|
||||||
|
|
||||||
|
# Generische Bot-Patterns
|
||||||
|
r'^user\d{4,}$', # user + 4+ Ziffern am Ende (z.B. user12345)
|
||||||
|
r'^account\d+', # account + Zahlen
|
||||||
|
r'^profile\d+', # profile + Zahlen
|
||||||
|
|
||||||
|
# Verdächtige Zahlenfolgen
|
||||||
|
r'\d{8,}', # 8+ aufeinanderfolgende Ziffern
|
||||||
|
r'^[a-z]{1,2}\d{6,}$', # 1-2 Buchstaben + 6+ Ziffern
|
||||||
|
|
||||||
|
# Timestamp-basierte Patterns
|
||||||
|
r'\d{10,}', # Unix-Timestamp-ähnlich (10+ Ziffern)
|
||||||
|
r'_\d{13}_', # Millisekunden-Timestamp in der Mitte
|
||||||
|
|
||||||
|
# Generische Suffixe die auf Bots hindeuten
|
||||||
|
r'_gen$', # Generator-Suffix
|
||||||
|
r'_bot$', # Bot-Suffix
|
||||||
|
r'_auto$', # Auto-Suffix
|
||||||
|
r'_spam$', # Spam-Suffix
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in suspicious_patterns:
|
||||||
|
if re.search(pattern, username_lower):
|
||||||
|
logger.debug(f"Verdächtiges Pattern gefunden: {pattern} in '{username}'")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Zusätzliche Heuristiken
|
||||||
|
|
||||||
|
# Prüfe auf zu viele Unterstriche (>2 ist verdächtig)
|
||||||
|
if username_lower.count('_') > 2:
|
||||||
|
logger.debug(f"Zu viele Unterstriche in '{username}'")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Prüfe auf repetitive Zeichen (z.B. "aaaa" oder "1111")
|
||||||
|
for i in range(len(username_lower) - 3):
|
||||||
|
if username_lower[i] == username_lower[i+1] == username_lower[i+2] == username_lower[i+3]:
|
||||||
|
logger.debug(f"Repetitive Zeichen in '{username}'")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def generate_realistic_username(self, first_name: str = "", last_name: str = "",
|
||||||
|
platform: str = "default") -> str:
|
||||||
|
"""
|
||||||
|
Generiert einen realistischen Benutzernamen ohne verdächtige Patterns.
|
||||||
|
|
||||||
|
Diese Methode ist speziell für Anti-Detection optimiert und generiert
|
||||||
|
Benutzernamen, die wie echte menschliche Benutzernamen aussehen.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
first_name: Vorname (optional)
|
||||||
|
last_name: Nachname (optional)
|
||||||
|
platform: Zielplattform
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Realistischer Benutzername
|
||||||
|
"""
|
||||||
|
policy = self.get_platform_policy(platform)
|
||||||
|
|
||||||
|
# Realistische Patterns (wie echte Menschen sie wählen)
|
||||||
|
realistic_patterns = []
|
||||||
|
|
||||||
|
if first_name:
|
||||||
|
first_name_clean = re.sub(r'[^a-z]', '', first_name.lower())
|
||||||
|
|
||||||
|
# Pattern 1: vorname + Geburtsjahr (z.B. "max1995")
|
||||||
|
realistic_patterns.append(
|
||||||
|
lambda fn=first_name_clean: f"{fn}{random.randint(1985, 2005)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern 2: vorname + Nachname-Initial + 2 Ziffern (z.B. "maxm92")
|
||||||
|
if last_name:
|
||||||
|
last_initial = last_name[0].lower() if last_name else ''
|
||||||
|
realistic_patterns.append(
|
||||||
|
lambda fn=first_name_clean, li=last_initial: f"{fn}{li}{random.randint(10, 99)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern 3: vorname.nachname (z.B. "max.mustermann")
|
||||||
|
if last_name:
|
||||||
|
last_name_clean = re.sub(r'[^a-z]', '', last_name.lower())
|
||||||
|
realistic_patterns.append(
|
||||||
|
lambda fn=first_name_clean, ln=last_name_clean: f"{fn}.{ln}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern 4: vorname_adjektiv (z.B. "max_sunny")
|
||||||
|
realistic_patterns.append(
|
||||||
|
lambda fn=first_name_clean: f"{fn}_{random.choice(self.adjectives)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pattern 5: adjektiv_vorname_jahr (z.B. "sunny_max_93")
|
||||||
|
realistic_patterns.append(
|
||||||
|
lambda fn=first_name_clean: f"{random.choice(self.adjectives)}_{fn}_{random.randint(85, 99)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fallback-Patterns ohne Namen
|
||||||
|
realistic_patterns.extend([
|
||||||
|
# adjektiv + tier (z.B. "happytiger")
|
||||||
|
lambda: f"{random.choice(self.adjectives)}{random.choice(self.nouns)}",
|
||||||
|
|
||||||
|
# adjektiv + tier + 2 Ziffern (z.B. "coolwolf42")
|
||||||
|
lambda: f"{random.choice(self.adjectives)}{random.choice(self.nouns)}{random.randint(10, 99)}",
|
||||||
|
|
||||||
|
# tier + jahr (z.B. "eagle1995")
|
||||||
|
lambda: f"{random.choice(self.nouns)}{random.randint(1985, 2005)}",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Versuche bis zu 20 mal einen gültigen, nicht-verdächtigen Namen zu generieren
|
||||||
|
for _ in range(20):
|
||||||
|
pattern_func = random.choice(realistic_patterns)
|
||||||
|
username = pattern_func()
|
||||||
|
|
||||||
|
# Länge anpassen
|
||||||
|
if len(username) > policy["max_length"]:
|
||||||
|
username = username[:policy["max_length"]]
|
||||||
|
if len(username) < policy["min_length"]:
|
||||||
|
username += str(random.randint(10, 99))
|
||||||
|
|
||||||
|
# Validieren (inkl. Pattern-Check)
|
||||||
|
valid, _ = self.validate_username(username, policy=policy)
|
||||||
|
if valid:
|
||||||
|
logger.info(f"Realistischer Benutzername generiert: {username}")
|
||||||
|
return username
|
||||||
|
|
||||||
|
# Absoluter Fallback
|
||||||
|
fallback = f"{random.choice(self.adjectives)}{random.choice(self.nouns)}{random.randint(10, 99)}"
|
||||||
|
logger.warning(f"Fallback-Benutzername verwendet: {fallback}")
|
||||||
|
return fallback
|
||||||
@ -41,12 +41,12 @@ class AboutDialog(QDialog):
|
|||||||
# Get the theme-aware logo path
|
# Get the theme-aware logo path
|
||||||
if self.theme_manager:
|
if self.theme_manager:
|
||||||
# Use theme manager to get correct logo based on current theme
|
# Use theme manager to get correct logo based on current theme
|
||||||
logo_path = self.theme_manager.get_icon_path("intelsight-logo")
|
logo_path = self.theme_manager.get_icon_path("aegissight-logo")
|
||||||
else:
|
else:
|
||||||
# Fallback to light logo if no theme manager
|
# Fallback to light logo if no theme manager
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
parent_dir = os.path.dirname(current_dir)
|
parent_dir = os.path.dirname(current_dir)
|
||||||
logo_path = os.path.join(parent_dir, "resources", "icons", "intelsight-logo.svg")
|
logo_path = os.path.join(parent_dir, "resources", "icons", "aegissight-logo.svg")
|
||||||
|
|
||||||
if os.path.exists(logo_path):
|
if os.path.exists(logo_path):
|
||||||
# Load logo and display it at a smaller size for corner placement
|
# Load logo and display it at a smaller size for corner placement
|
||||||
@ -61,7 +61,7 @@ class AboutDialog(QDialog):
|
|||||||
logo_label.setFixedSize(scaled_pixmap.size())
|
logo_label.setFixedSize(scaled_pixmap.size())
|
||||||
else:
|
else:
|
||||||
# Fallback if logo not found
|
# Fallback if logo not found
|
||||||
logo_label.setText("IntelSight")
|
logo_label.setText("AegisSight")
|
||||||
logo_label.setStyleSheet("font-size: 18px; font-weight: bold;")
|
logo_label.setStyleSheet("font-size: 18px; font-weight: bold;")
|
||||||
|
|
||||||
# Logo in top-left corner
|
# Logo in top-left corner
|
||||||
@ -111,7 +111,7 @@ class AboutDialog(QDialog):
|
|||||||
lines = [
|
lines = [
|
||||||
f"<h2>{title}</h2>",
|
f"<h2>{title}</h2>",
|
||||||
f"<p>{version_text}</p>",
|
f"<p>{version_text}</p>",
|
||||||
"<p>© 2025 IntelSight UG (haftungsbeschränkt)</p>",
|
"<p>© 2025 AegisSight UG (haftungsbeschränkt)</p>",
|
||||||
f"<p>{support}</p>",
|
f"<p>{support}</p>",
|
||||||
f"<p>{license_text}</p>",
|
f"<p>{license_text}</p>",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -156,11 +156,11 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Get the correct logo based on current theme
|
# Get the correct logo based on current theme
|
||||||
if self.theme_manager:
|
if self.theme_manager:
|
||||||
logo_path = self.theme_manager.get_icon_path("intelsight-logo")
|
logo_path = self.theme_manager.get_icon_path("aegissight-logo")
|
||||||
else:
|
else:
|
||||||
# Fallback if no theme manager
|
# Fallback if no theme manager
|
||||||
logo_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
logo_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
"resources", "icons", "intelsight-logo.svg")
|
"resources", "icons", "aegissight-logo.svg")
|
||||||
|
|
||||||
self.logo_widget.setIcon(QIcon(logo_path))
|
self.logo_widget.setIcon(QIcon(logo_path))
|
||||||
self.logo_widget.setIconSize(QSize(120, 40))
|
self.logo_widget.setIconSize(QSize(120, 40))
|
||||||
@ -322,7 +322,7 @@ class MainWindow(QMainWindow):
|
|||||||
if hasattr(self, 'logo_widget') and self.logo_widget and self.theme_manager:
|
if hasattr(self, 'logo_widget') and self.logo_widget and self.theme_manager:
|
||||||
# Get the new logo path from theme manager based on current theme
|
# Get the new logo path from theme manager based on current theme
|
||||||
current_theme = self.theme_manager.get_current_theme()
|
current_theme = self.theme_manager.get_current_theme()
|
||||||
logo_path = self.theme_manager.get_icon_path("intelsight-logo")
|
logo_path = self.theme_manager.get_icon_path("aegissight-logo")
|
||||||
|
|
||||||
print(f"DEBUG: Updating logo for theme '{current_theme}'")
|
print(f"DEBUG: Updating logo for theme '{current_theme}'")
|
||||||
print(f"DEBUG: Logo path: {logo_path}")
|
print(f"DEBUG: Logo path: {logo_path}")
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren