eoka_protocol/
datadome.rs1use 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}