Skip to main content

capmonstercloud_client/requests/
friendly.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/// Friendly Captcha parameters.
9#[derive(Debug, Clone, Serialize)]
10pub struct FriendlyMetadata {
11    /// URL of the JS file, used to detect the captcha version.
12    /// V1: `https://cdn.jsdelivr.net/npm/friendly-challenge@X.Y.Z/widget.module.min.js`
13    /// (`X.Y.Z` is the client version from the `x-frc-client` header).
14    /// V2: URL of the `site.min.js` file loaded on the page, e.g.
15    /// `https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk@X.Y.Z/site.min.js`.
16    #[serde(rename = "apiGetLib")]
17    pub api_get_lib: String,
18}
19
20impl FriendlyMetadata {
21    pub fn new(api_get_lib: impl Into<String>) -> Self {
22        Self {
23            api_get_lib: api_get_lib.into(),
24        }
25    }
26}
27
28/// Friendly Captcha (`CustomTask`, class `friendly`).
29#[derive(Debug, Clone, Serialize)]
30pub struct FriendlyCustomTaskRequest {
31    /// Full URL of the page with the captcha.
32    #[serde(rename = "websiteURL")]
33    pub website_url: String,
34    /// Friendly Captcha site key.
35    #[serde(rename = "websiteKey")]
36    pub website_key: String,
37    pub metadata: FriendlyMetadata,
38    #[serde(flatten, skip_serializing_if = "Option::is_none")]
39    pub proxy: Option<ProxyInfo>,
40    /// Browser User-Agent (current Windows UA supported by CapMonster Cloud).
41    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
42    pub user_agent: Option<String>,
43}
44
45impl FriendlyCustomTaskRequest {
46    pub fn new(
47        website_url: impl Into<String>,
48        website_key: impl Into<String>,
49        metadata: FriendlyMetadata,
50    ) -> Self {
51        Self {
52            website_url: website_url.into(),
53            website_key: website_key.into(),
54            metadata,
55            proxy: None,
56            user_agent: None,
57        }
58    }
59
60    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
61        self.proxy = Some(proxy);
62        self
63    }
64
65    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
66        self.user_agent = Some(value.into());
67        self
68    }
69}
70
71impl CaptchaRequest for FriendlyCustomTaskRequest {
72    const TASK_TYPE: &'static str = "CustomTask";
73    const CLASS: Option<&'static str> = Some("friendly");
74
75    fn timeouts(&self) -> GetResultTimeouts {
76        GetResultTimeouts::secs(1.0, 0.0, 3.0, 180.0)
77    }
78
79    fn validate(&self) -> Result<()> {
80        require_non_empty("websiteURL", &self.website_url)?;
81        require_non_empty("websiteKey", &self.website_key)?;
82        require_non_empty("metadata.apiGetLib", &self.metadata.api_get_lib)?;
83        Ok(())
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::proxy::ProxyType;
91    use serde_json::json;
92
93    #[test]
94    fn minimal_task() {
95        let task = FriendlyCustomTaskRequest::new(
96            "https://example.com",
97            "FC123",
98            FriendlyMetadata::new("https://x/site.min.js"),
99        )
100        .to_task()
101        .unwrap();
102        assert_eq!(
103            task,
104            json!({
105                "type": "CustomTask", "class": "friendly", "websiteURL": "https://example.com", "websiteKey": "FC123",
106                "metadata": {"apiGetLib": "https://x/site.min.js"}
107            })
108        );
109    }
110
111    #[test]
112    fn full_task() {
113        let task = FriendlyCustomTaskRequest::new(
114            "https://example.com",
115            "FC123",
116            FriendlyMetadata::new("L"),
117        )
118        .with_proxy(ProxyInfo::new(ProxyType::Socks4, "1.2.3.4", 1080).with_auth("u", "p"))
119        .with_user_agent("UA")
120        .to_task()
121        .unwrap();
122        assert_eq!(
123            task,
124            json!({
125                "type": "CustomTask", "class": "friendly", "websiteURL": "https://example.com", "websiteKey": "FC123",
126                "metadata": {"apiGetLib": "L"},
127                "proxyType": "socks4", "proxyAddress": "1.2.3.4", "proxyPort": 1080,
128                "proxyLogin": "u", "proxyPassword": "p", "userAgent": "UA"
129            })
130        );
131    }
132
133    #[test]
134    fn empty_required_strings_are_rejected() {
135        let err = FriendlyCustomTaskRequest::new("", "K", FriendlyMetadata::new("L"))
136            .to_task()
137            .unwrap_err();
138        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
139        let err =
140            FriendlyCustomTaskRequest::new("https://example.com", "", FriendlyMetadata::new("L"))
141                .to_task()
142                .unwrap_err();
143        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteKey")));
144        let err =
145            FriendlyCustomTaskRequest::new("https://example.com", "K", FriendlyMetadata::new(""))
146                .to_task()
147                .unwrap_err();
148        assert!(
149            matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.apiGetLib"))
150        );
151    }
152}