Skip to main content

capmonstercloud_client/requests/
altcha.rs

1use serde::Serialize;
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::error::Result;
5use crate::proxy::ProxyInfo;
6use crate::timeouts::GetResultTimeouts;
7
8/// Altcha challenge parameters taken from the page (all four are required by the API).
9#[derive(Debug, Clone, Serialize)]
10pub struct AltchaMetadata {
11    pub challenge: String,
12    pub iterations: String,
13    pub salt: String,
14    pub signature: String,
15}
16
17/// Altcha proof-of-work captcha (`CustomTask`, class `altcha`).
18#[derive(Debug, Clone, Serialize)]
19pub struct AltchaCustomTaskRequest {
20    #[serde(rename = "websiteURL")]
21    pub website_url: String,
22    /// Site key; an empty string is allowed.
23    #[serde(rename = "websiteKey")]
24    pub website_key: String,
25    pub metadata: AltchaMetadata,
26    #[serde(flatten, skip_serializing_if = "Option::is_none")]
27    pub proxy: Option<ProxyInfo>,
28    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
29    pub user_agent: Option<String>,
30}
31
32impl AltchaCustomTaskRequest {
33    pub fn new(
34        website_url: impl Into<String>,
35        website_key: impl Into<String>,
36        metadata: AltchaMetadata,
37    ) -> Self {
38        Self {
39            website_url: website_url.into(),
40            website_key: website_key.into(),
41            metadata,
42            proxy: None,
43            user_agent: None,
44        }
45    }
46
47    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
48        self.proxy = Some(proxy);
49        self
50    }
51
52    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
53        self.user_agent = Some(value.into());
54        self
55    }
56}
57
58impl CaptchaRequest for AltchaCustomTaskRequest {
59    const TASK_TYPE: &'static str = "CustomTask";
60    const CLASS: Option<&'static str> = Some("altcha");
61
62    fn timeouts(&self) -> GetResultTimeouts {
63        GetResultTimeouts::secs(1.0, 0.0, 1.0, 50.0)
64    }
65
66    fn validate(&self) -> Result<()> {
67        require_non_empty("websiteURL", &self.website_url)?;
68        require_non_empty("metadata.challenge", &self.metadata.challenge)?;
69        require_non_empty("metadata.iterations", &self.metadata.iterations)?;
70        require_non_empty("metadata.salt", &self.metadata.salt)?;
71        require_non_empty("metadata.signature", &self.metadata.signature)?;
72        Ok(())
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use serde_json::json;
80
81    #[test]
82    fn task_has_class_and_metadata() {
83        let meta = AltchaMetadata {
84            challenge: "c".into(),
85            iterations: "1".into(),
86            salt: "s".into(),
87            signature: "g".into(),
88        };
89        let task = AltchaCustomTaskRequest::new("https://example.com", "", meta)
90            .to_task()
91            .unwrap();
92        assert_eq!(
93            task,
94            json!({
95                "type": "CustomTask", "class": "altcha",
96                "websiteURL": "https://example.com", "websiteKey": "",
97                "metadata": {"challenge": "c", "iterations": "1", "salt": "s", "signature": "g"}
98            })
99        );
100    }
101
102    #[test]
103    fn empty_required_strings_are_rejected() {
104        let err = AltchaCustomTaskRequest::new(
105            "",
106            "",
107            AltchaMetadata {
108                challenge: "c".into(),
109                iterations: "1".into(),
110                salt: "s".into(),
111                signature: "g".into(),
112            },
113        )
114        .to_task()
115        .unwrap_err();
116        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
117        let err = AltchaCustomTaskRequest::new(
118            "https://example.com",
119            "",
120            AltchaMetadata {
121                challenge: "".into(),
122                iterations: "1".into(),
123                salt: "s".into(),
124                signature: "g".into(),
125            },
126        )
127        .to_task()
128        .unwrap_err();
129        assert!(
130            matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.challenge"))
131        );
132        let err = AltchaCustomTaskRequest::new(
133            "https://example.com",
134            "",
135            AltchaMetadata {
136                challenge: "c".into(),
137                iterations: "".into(),
138                salt: "s".into(),
139                signature: "g".into(),
140            },
141        )
142        .to_task()
143        .unwrap_err();
144        assert!(
145            matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.iterations"))
146        );
147        let err = AltchaCustomTaskRequest::new(
148            "https://example.com",
149            "",
150            AltchaMetadata {
151                challenge: "c".into(),
152                iterations: "1".into(),
153                salt: "".into(),
154                signature: "g".into(),
155            },
156        )
157        .to_task()
158        .unwrap_err();
159        assert!(
160            matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.salt"))
161        );
162        let err = AltchaCustomTaskRequest::new(
163            "https://example.com",
164            "",
165            AltchaMetadata {
166                challenge: "c".into(),
167                iterations: "1".into(),
168                salt: "s".into(),
169                signature: "".into(),
170            },
171        )
172        .to_task()
173        .unwrap_err();
174        assert!(
175            matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.signature"))
176        );
177    }
178}