Skip to main content

bobby_browser_client/
challenges.rs

1//! Challenge detection and solving types for captchas, MFA, and verification flows.
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[serde(rename_all = "camelCase")]
8pub enum ChallengeType {
9    RecaptchaV2Checkbox,
10    RecaptchaV3,
11    TextCaptcha,
12    ImageGridCaptcha,
13    MfaCodeEntry,
14}
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18pub struct ChallengeDetection {
19    pub challenge_type: ChallengeType,
20    pub confidence: f32,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub region: Option<ChallengeRegion>,
23    #[serde(default)]
24    pub blocking: bool,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub hints: Option<ChallengeDetectionHints>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31pub struct ChallengeRegion {
32    pub x: f32,
33    pub y: f32,
34    pub width: f32,
35    pub height: f32,
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40pub struct ChallengeDetectionHints {
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub target_field_purpose: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub instruction_text: Option<String>,
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
49pub struct DetectChallengeIntent {
50    pub purpose: String,
51    #[serde(default)]
52    pub hints: DetectChallengeHints,
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[serde(rename_all = "camelCase")]
58pub struct DetectChallengeHints {
59    /// Restrict classification to a page region (CSS pixels, viewport-relative).
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub region: Option<ChallengeRegion>,
62    /// Deadline for the whole detection, including provider retries.
63    #[serde(default = "default_detect_timeout_ms")]
64    pub timeout_ms: u64,
65}
66
67impl Default for DetectChallengeHints {
68    fn default() -> Self {
69        Self {
70            region: None,
71            timeout_ms: default_detect_timeout_ms(),
72        }
73    }
74}
75
76fn default_detect_timeout_ms() -> u64 {
77    15_000
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82pub struct SolveChallengeIntent {
83    pub purpose: String,
84    #[serde(default)]
85    pub hints: SolveChallengeHints,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[serde(rename_all = "camelCase")]
91pub struct SolveChallengeHints {
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub region: Option<ChallengeRegion>,
94    #[serde(default = "default_solve_timeout_ms")]
95    pub timeout_ms: u64,
96}
97
98impl Default for SolveChallengeHints {
99    fn default() -> Self {
100        Self {
101            region: None,
102            timeout_ms: default_solve_timeout_ms(),
103        }
104    }
105}
106
107fn default_solve_timeout_ms() -> u64 {
108    30_000
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113#[serde(tag = "kind", rename_all = "camelCase")]
114pub enum SolveStep {
115    Click {
116        x: f32,
117        y: f32,
118    },
119    TypeText {
120        text: String,
121        target_field_purpose: String,
122    },
123    WaitAndReassess {
124        ms: u64,
125    },
126}