mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-30 11:49:50 +02:00
62 lines
3.0 KiB
Python
62 lines
3.0 KiB
Python
import re
|
|
from typing import Optional
|
|
|
|
from typeguard import typechecked
|
|
|
|
# A bot-detection challenge in the page's own words. We NEVER solve one of these: defeating
|
|
# bot-detection is off-limits, and an agent that tries produces exactly the confident nonsense this
|
|
# codebase exists to prevent (it cannot hear an audio clip, and "press and hold" against a
|
|
# behavioural detector is a coin flip it will report as a success either way).
|
|
#
|
|
# Named separately from the login wall because the remedy is different. A login wall is fixed by
|
|
# signing in once and it stays fixed; a challenge is a one-shot the human has to clear right now,
|
|
# in this card, before anything else can proceed.
|
|
P_CAPTCHA_RE = re.compile(
|
|
r"enter (?:the )?(?:code|characters) you hear|play the audio|"
|
|
r"press (?:and|&) hold|click and hold|hold to (?:verify|continue)|"
|
|
r"drag (?:the )?(?:slider|puzzle|piece|handle)|slide to (?:verify|complete)|"
|
|
r"i'?m not a robot|verify you are (?:a )?human|are you a human|"
|
|
r"security check|complete the (?:security )?(?:check|challenge)|"
|
|
r"recaptcha|hcaptcha|are you a robot|unusual traffic|"
|
|
r"select all (?:images|squares) (?:with|containing)",
|
|
re.I,
|
|
)
|
|
# Vendor iframes and containers, for challenges that render no readable copy at all.
|
|
P_CAPTCHA_DOM_RE = re.compile(
|
|
r"recaptcha|hcaptcha|turnstile|arkose|funcaptcha|px-captcha|geetest|captcha-?(?:container|frame)",
|
|
re.I,
|
|
)
|
|
|
|
|
|
@typechecked
|
|
def captcha_kind(state_text: str) -> Optional[str]:
|
|
"""Which challenge is on the page, or None.
|
|
|
|
The KIND is the whole point of returning a string: "solve the captcha" tells a user nothing,
|
|
while "it wants you to play an audio clip and type what you hear" tells them what they are
|
|
about to do. Measured on tiktok 2026-08-04, where the challenge was an audio one and the run
|
|
burned its whole budget clicking at a page it could never get past.
|
|
"""
|
|
text = state_text or ""
|
|
if re.search(r"enter (?:the )?(?:code|characters) you hear|play the audio", text, re.I):
|
|
return "an audio challenge (play the clip and type what you hear)"
|
|
if re.search(r"press (?:and|&) hold|click and hold|hold to (?:verify|continue)", text, re.I):
|
|
return "a press-and-hold challenge"
|
|
if re.search(r"drag (?:the )?(?:slider|puzzle|piece|handle)|slide to (?:verify|complete)", text, re.I):
|
|
return "a slider/puzzle challenge"
|
|
if re.search(r"select all (?:images|squares) (?:with|containing)", text, re.I):
|
|
return "an image-selection challenge"
|
|
if P_CAPTCHA_RE.search(text) or P_CAPTCHA_DOM_RE.search(text):
|
|
return "a bot-detection challenge"
|
|
return None
|
|
|
|
|
|
@typechecked
|
|
def prompt_copy(kind: str, host: str) -> tuple[str, str]:
|
|
"""(problem, instruction) for the human handoff. Says plainly that we will not attempt it."""
|
|
return (
|
|
f"{host or 'This site'} is showing {kind}.",
|
|
"I can't solve these, and guessing at one would be worse than stopping. Please complete it "
|
|
"in the browser card, then tell me to continue.",
|
|
)
|