Skip to main content

eoka_protocol/
datadome.rs

1use serde::{Deserialize, Serialize};
2
3pub const DATADOME_DEFAULT_ATTEMPTS: u32 = 1;
4pub const DATADOME_MAX_ATTEMPTS: u32 = 4;
5pub const DATADOME_DEFAULT_TIMEOUT_MS: u64 = 60_000;
6pub const DATADOME_MIN_TIMEOUT_MS: u64 = 5_000;
7pub const DATADOME_MAX_TIMEOUT_MS: u64 = 120_000;
8
9#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
10#[serde(default, deny_unknown_fields)]
11pub struct CaptchaDatadomeArgs {
12    #[serde(deserialize_with = "deserialize_attempts")]
13    #[schemars(range(min = 1, max = DATADOME_MAX_ATTEMPTS))]
14    pub max_attempts: u32,
15    #[serde(deserialize_with = "deserialize_timeout")]
16    #[schemars(range(min = DATADOME_MIN_TIMEOUT_MS, max = DATADOME_MAX_TIMEOUT_MS))]
17    pub timeout_ms: u64,
18}
19
20fn deserialize_attempts<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
21    let value = u32::deserialize(d)?;
22    validate_attempts(value).map_err(serde::de::Error::custom)?;
23    Ok(value)
24}
25
26fn deserialize_timeout<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
27    let value = u64::deserialize(d)?;
28    validate_timeout(value).map_err(serde::de::Error::custom)?;
29    Ok(value)
30}
31
32fn validate_attempts(value: u32) -> Result<(), &'static str> {
33    if !(1..=DATADOME_MAX_ATTEMPTS).contains(&value) {
34        return Err("max_attempts must be between 1 and 4");
35    }
36    Ok(())
37}
38
39fn validate_timeout(value: u64) -> Result<(), &'static str> {
40    if !(DATADOME_MIN_TIMEOUT_MS..=DATADOME_MAX_TIMEOUT_MS).contains(&value) {
41        return Err("timeout_ms must be between 5000 and 120000");
42    }
43    Ok(())
44}
45
46impl Default for CaptchaDatadomeArgs {
47    fn default() -> Self {
48        Self {
49            max_attempts: DATADOME_DEFAULT_ATTEMPTS,
50            timeout_ms: DATADOME_DEFAULT_TIMEOUT_MS,
51        }
52    }
53}
54
55impl CaptchaDatadomeArgs {
56    pub fn validate(&self) -> Result<(), &'static str> {
57        validate_attempts(self.max_attempts)?;
58        validate_timeout(self.timeout_ms)
59    }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
63#[serde(rename_all = "snake_case")]
64pub enum DataDomeOutcome {
65    ChallengeCleared,
66    NotPresent,
67    Blocked,
68    IpBanned,
69    Failed,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
73pub struct CaptchaDatadomeResult {
74    pub outcome: DataDomeOutcome,
75    pub tab_id: String,
76    pub elapsed_ms: u64,
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::{input_schema_for_cmd, request_from_cmd, Request};
83    use serde_json::json;
84
85    #[test]
86    fn defaults_ranges_and_roundtrip_are_canonical() {
87        let request = request_from_cmd("captcha_datadome", json!({})).unwrap();
88        assert_eq!(
89            request.args_json(),
90            json!({"max_attempts":1,"timeout_ms":60000})
91        );
92        let decoded: Request =
93            serde_json::from_value(serde_json::to_value(&request).unwrap()).unwrap();
94        assert_eq!(decoded.cmd(), "captcha_datadome");
95        assert_eq!(decoded.args_json(), request.args_json());
96        for input in [
97            json!({"max_attempts":0}),
98            json!({"max_attempts":5}),
99            json!({"timeout_ms":4999}),
100            json!({"timeout_ms":120001}),
101            json!({"api_key":"not-used"}),
102            json!({"max_attempts":null}),
103            json!({"max_attempts":-1}),
104            json!({"max_attempts":1.5}),
105            json!({"timeout_ms":"60000"}),
106            json!({"timeout_ms":null}),
107        ] {
108            assert!(request_from_cmd("captcha_datadome", input).is_err());
109        }
110        for (input, expected) in [
111            (
112                json!({"max_attempts":2}),
113                json!({"max_attempts":2,"timeout_ms":60000}),
114            ),
115            (
116                json!({"timeout_ms":5000}),
117                json!({"max_attempts":1,"timeout_ms":5000}),
118            ),
119        ] {
120            assert_eq!(
121                request_from_cmd("captcha_datadome", input)
122                    .unwrap()
123                    .args_json(),
124                expected
125            );
126        }
127        let schema = input_schema_for_cmd("captcha_datadome");
128        assert_eq!(
129            schema["properties"]["max_attempts"]["default"],
130            DATADOME_DEFAULT_ATTEMPTS
131        );
132        assert_eq!(
133            schema["properties"]["timeout_ms"]["default"],
134            DATADOME_DEFAULT_TIMEOUT_MS
135        );
136        assert_eq!(schema["properties"]["max_attempts"]["minimum"], 1);
137        assert_eq!(
138            schema["properties"]["max_attempts"]["maximum"],
139            DATADOME_MAX_ATTEMPTS
140        );
141        assert_eq!(
142            schema["properties"]["timeout_ms"]["minimum"],
143            DATADOME_MIN_TIMEOUT_MS
144        );
145        assert_eq!(
146            schema["properties"]["timeout_ms"]["maximum"],
147            DATADOME_MAX_TIMEOUT_MS
148        );
149        for timeout_ms in [DATADOME_MIN_TIMEOUT_MS, DATADOME_MAX_TIMEOUT_MS] {
150            let args = CaptchaDatadomeArgs {
151                max_attempts: DATADOME_MAX_ATTEMPTS,
152                timeout_ms,
153            };
154            assert!(args.validate().is_ok());
155            assert!(
156                request_from_cmd("captcha_datadome", serde_json::to_value(args).unwrap()).is_ok()
157            );
158        }
159        assert_eq!(schema["additionalProperties"], false);
160        assert!(CaptchaDatadomeArgs {
161            max_attempts: 0,
162            ..Default::default()
163        }
164        .validate()
165        .is_err());
166    }
167}