fix: Handle email verification dialog (#782)

This commit is contained in:
Heavenfighter
2026-01-28 12:50:40 +01:00
committed by GitHub
parent b4cb979164
commit 23c27157d0
6 changed files with 39 additions and 13 deletions

View File

@@ -317,6 +317,7 @@ timeouts:
page_load: 15.0 # Timeout for web_open page loads page_load: 15.0 # Timeout for web_open page loads
captcha_detection: 2.0 # Timeout for captcha iframe detection captcha_detection: 2.0 # Timeout for captcha iframe detection
sms_verification: 4.0 # Timeout for SMS verification banners sms_verification: 4.0 # Timeout for SMS verification banners
email_verification: 4.0 # Timeout for email verification banners
gdpr_prompt: 10.0 # Timeout when handling GDPR dialogs gdpr_prompt: 10.0 # Timeout when handling GDPR dialogs
login_detection: 10.0 # Timeout for DOM-based login detection fallback (auth probe is tried first) login_detection: 10.0 # Timeout for DOM-based login detection fallback (auth probe is tried first)
publishing_result: 300.0 # Timeout for publishing status checks publishing_result: 300.0 # Timeout for publishing status checks

View File

@@ -518,6 +518,13 @@
"title": "Sms Verification", "title": "Sms Verification",
"type": "number" "type": "number"
}, },
"email_verification": {
"default": 4.0,
"description": "Timeout for eMail verification prompts.",
"minimum": 0.1,
"title": "Email Verification",
"type": "number"
},
"gdpr_prompt": { "gdpr_prompt": {
"default": 10.0, "default": 10.0,
"description": "Timeout for GDPR/consent dialogs.", "description": "Timeout for GDPR/consent dialogs.",

View File

@@ -863,11 +863,22 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
LOG.warning("############################################") LOG.warning("############################################")
LOG.warning("# Device verification message detected. Please follow the instruction displayed in the Browser.") LOG.warning("# Device verification message detected. Please follow the instruction displayed in the Browser.")
LOG.warning("############################################") LOG.warning("############################################")
await ainput("Press ENTER when done...") await ainput(_("Press ENTER when done..."))
except TimeoutError: except TimeoutError:
# No SMS verification prompt detected. # No SMS verification prompt detected.
pass pass
try:
email_timeout = self._timeout("email_verification")
await self.web_find(By.TEXT, "Um dein Konto zu schützen haben wir dir eine E-Mail geschickt", timeout = email_timeout)
LOG.warning("############################################")
LOG.warning("# Device verification message detected. Please follow the instruction displayed in the Browser.")
LOG.warning("############################################")
await ainput(_("Press ENTER when done..."))
except TimeoutError:
# No email verification prompt detected.
pass
try: try:
LOG.info("Handling GDPR disclaimer...") LOG.info("Handling GDPR disclaimer...")
gdpr_timeout = self._timeout("gdpr_prompt") gdpr_timeout = self._timeout("gdpr_prompt")

View File

@@ -153,6 +153,7 @@ class TimeoutConfig(ContextualModel):
page_load:float = Field(default = 15.0, ge = 1.0, description = "Page load timeout for web_open.") page_load:float = Field(default = 15.0, ge = 1.0, description = "Page load timeout for web_open.")
captcha_detection:float = Field(default = 2.0, ge = 0.1, description = "Timeout for captcha iframe detection.") captcha_detection:float = Field(default = 2.0, ge = 0.1, description = "Timeout for captcha iframe detection.")
sms_verification:float = Field(default = 4.0, ge = 0.1, description = "Timeout for SMS verification prompts.") sms_verification:float = Field(default = 4.0, ge = 0.1, description = "Timeout for SMS verification prompts.")
email_verification:float = Field(default = 4.0, ge = 0.1, description = "Timeout for email verification prompts.")
gdpr_prompt:float = Field(default = 10.0, ge = 1.0, description = "Timeout for GDPR/consent dialogs.") gdpr_prompt:float = Field(default = 10.0, ge = 1.0, description = "Timeout for GDPR/consent dialogs.")
login_detection:float = Field(default = 10.0, ge = 1.0, description = "Timeout for detecting existing login session via DOM elements.") login_detection:float = Field(default = 10.0, ge = 1.0, description = "Timeout for detecting existing login session via DOM elements.")
publishing_result:float = Field(default = 300.0, ge = 10.0, description = "Timeout for publishing result checks.") publishing_result:float = Field(default = 300.0, ge = 10.0, description = "Timeout for publishing result checks.")
@@ -218,7 +219,7 @@ class DiagnosticsConfig(ContextualModel):
def _validate_glob_pattern(v:str) -> str: def _validate_glob_pattern(v:str) -> str:
if not v.strip(): if not v.strip():
raise ValueError("must be a non-empty, non-blank glob pattern") raise ValueError(_("must be a non-empty, non-blank glob pattern"))
return v return v

View File

@@ -603,6 +603,8 @@ kleinanzeigen_bot/model/config_model.py:
"amount must be specified when auto_price_reduction is enabled": "amount muss angegeben werden, wenn auto_price_reduction aktiviert ist" "amount must be specified when auto_price_reduction is enabled": "amount muss angegeben werden, wenn auto_price_reduction aktiviert ist"
"min_price must be specified when auto_price_reduction is enabled": "min_price muss angegeben werden, wenn auto_price_reduction aktiviert ist" "min_price must be specified when auto_price_reduction is enabled": "min_price muss angegeben werden, wenn auto_price_reduction aktiviert ist"
"Percentage reduction amount must not exceed %s": "Prozentuale Reduktionsmenge darf %s nicht überschreiten" "Percentage reduction amount must not exceed %s": "Prozentuale Reduktionsmenge darf %s nicht überschreiten"
_validate_glob_pattern:
"must be a non-empty, non-blank glob pattern": "muss ein nicht-leeres Glob-Muster sein"
_validate_pause_requires_capture: _validate_pause_requires_capture:
"pause_on_login_detection_failure requires login_detection_capture to be enabled": "pause_on_login_detection_failure erfordert, dass login_detection_capture aktiviert ist" "pause_on_login_detection_failure requires login_detection_capture to be enabled": "pause_on_login_detection_failure erfordert, dass login_detection_capture aktiviert ist"

View File

@@ -509,17 +509,21 @@ class TestKleinanzeigenBotAuthentication:
# First login attempt: # First login attempt:
# 1. Captcha iframe found (in check_and_wait_for_captcha) # 1. Captcha iframe found (in check_and_wait_for_captcha)
# 2. Phone verification not found (in handle_after_login_logic) # 2. Phone verification not found (in handle_after_login_logic)
# 3. GDPR banner not found (in handle_after_login_logic) # 3. Email verification not found (in handle_after_login_logic)
# 4. GDPR banner not found (in handle_after_login_logic)
# Second login attempt: # Second login attempt:
# 4. Captcha iframe found (in check_and_wait_for_captcha) # 5. Captcha iframe found (in check_and_wait_for_captcha)
# 5. Phone verification not found (in handle_after_login_logic) # 6. Phone verification not found (in handle_after_login_logic)
# 6. GDPR banner not found (in handle_after_login_logic) # 7. Email verification not found (in handle_after_login_logic)
# 8. GDPR banner not found (in handle_after_login_logic)
mock_find.side_effect = [ mock_find.side_effect = [
AsyncMock(), # Captcha iframe (first login) AsyncMock(), # Captcha iframe (first login)
TimeoutError(), # Phone verification (first login) TimeoutError(), # Phone verification (first login)
TimeoutError(), # Email verification (first login)
TimeoutError(), # GDPR banner (first login) TimeoutError(), # GDPR banner (first login)
AsyncMock(), # Captcha iframe (second login) AsyncMock(), # Captcha iframe (second login)
TimeoutError(), # Phone verification (second login) TimeoutError(), # Phone verification (second login)
TimeoutError(), # Email verification (second login)
TimeoutError(), # GDPR banner (second login) TimeoutError(), # GDPR banner (second login)
] ]
mock_ainput.return_value = "" mock_ainput.return_value = ""
@@ -529,7 +533,7 @@ class TestKleinanzeigenBotAuthentication:
await test_bot.login() await test_bot.login()
# Verify the complete flow # Verify the complete flow
assert mock_find.call_count == 6 # Exactly 6 web_find calls assert mock_find.call_count == 8 # Exactly 8 web_find calls
assert mock_ainput.call_count == 2 # Two captcha prompts assert mock_ainput.call_count == 2 # Two captcha prompts
assert mock_input.call_count == 6 # Two login attempts with username, clear password, and set password assert mock_input.call_count == 6 # Two login attempts with username, clear password, and set password
assert mock_click.call_count == 2 # Two submit button clicks assert mock_click.call_count == 2 # Two submit button clicks
@@ -583,13 +587,13 @@ class TestKleinanzeigenBotAuthentication:
patch("kleinanzeigen_bot.ainput", new_callable = AsyncMock) as mock_ainput, patch("kleinanzeigen_bot.ainput", new_callable = AsyncMock) as mock_ainput,
): ):
# Test case 1: No special handling needed # Test case 1: No special handling needed
mock_find.side_effect = [TimeoutError(), TimeoutError()] # No phone verification, no GDPR mock_find.side_effect = [TimeoutError(), TimeoutError(), TimeoutError()] # No phone verification, no email verification, no GDPR
mock_click.return_value = AsyncMock() mock_click.return_value = AsyncMock()
mock_ainput.return_value = "" mock_ainput.return_value = ""
await test_bot.handle_after_login_logic() await test_bot.handle_after_login_logic()
assert mock_find.call_count == 2 assert mock_find.call_count == 3
assert mock_click.call_count == 0 assert mock_click.call_count == 0
assert mock_ainput.call_count == 0 assert mock_ainput.call_count == 0
@@ -597,11 +601,11 @@ class TestKleinanzeigenBotAuthentication:
mock_find.reset_mock() mock_find.reset_mock()
mock_click.reset_mock() mock_click.reset_mock()
mock_ainput.reset_mock() mock_ainput.reset_mock()
mock_find.side_effect = [AsyncMock(), TimeoutError()] # Phone verification found, no GDPR mock_find.side_effect = [AsyncMock(), TimeoutError(), TimeoutError()] # Phone verification found, no email verification, no GDPR
await test_bot.handle_after_login_logic() await test_bot.handle_after_login_logic()
assert mock_find.call_count == 2 assert mock_find.call_count == 3
assert mock_click.call_count == 0 # No click needed, just wait for user assert mock_click.call_count == 0 # No click needed, just wait for user
assert mock_ainput.call_count == 1 # Wait for user to complete verification assert mock_ainput.call_count == 1 # Wait for user to complete verification
@@ -609,11 +613,11 @@ class TestKleinanzeigenBotAuthentication:
mock_find.reset_mock() mock_find.reset_mock()
mock_click.reset_mock() mock_click.reset_mock()
mock_ainput.reset_mock() mock_ainput.reset_mock()
mock_find.side_effect = [TimeoutError(), AsyncMock()] # No phone verification, GDPR found mock_find.side_effect = [TimeoutError(), TimeoutError(), AsyncMock()] # No phone verification, no email verification, GDPR found
await test_bot.handle_after_login_logic() await test_bot.handle_after_login_logic()
assert mock_find.call_count == 2 assert mock_find.call_count == 3
assert mock_click.call_count == 2 # Click to accept GDPR and continue assert mock_click.call_count == 2 # Click to accept GDPR and continue
assert mock_ainput.call_count == 0 assert mock_ainput.call_count == 0