1use reqwest::{
2 Client, Method, RequestBuilder, StatusCode, Url,
3 header::{ACCEPT, ACCEPT_LANGUAGE},
4 multipart::Form,
5};
6use serde::de::DeserializeOwned;
7
8use crate::CyberdropError;
9use crate::config::{DEFAULT_BASE_URL, DEFAULT_TIMEOUT};
10use crate::token::AuthToken;
11
12#[derive(Debug, Clone)]
14pub struct CyberdropClient {
15 pub(crate) client: Client,
16 pub(crate) base_url: Url,
17 pub(crate) auth_token: Option<AuthToken>,
18}
19
20impl CyberdropClient {
21 pub fn new() -> Result<Self, CyberdropError> {
23 let client = Client::builder()
24 .timeout(DEFAULT_TIMEOUT)
25 .user_agent(default_user_agent())
26 .build()?;
27
28 Ok(Self {
29 client,
30 base_url: Url::parse(DEFAULT_BASE_URL)?,
31 auth_token: None,
32 })
33 }
34
35 pub fn new_with_token(token: impl Into<String>) -> Result<Self, CyberdropError> {
37 Ok(Self::new()?.with_auth_token(token))
38 }
39
40 pub fn auth_token(&self) -> Option<&str> {
42 self.auth_token.as_ref().map(AuthToken::as_str)
43 }
44
45 pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
49 self.auth_token = Some(AuthToken::new(token));
50 self
51 }
52
53 pub(crate) async fn get_json<T>(
54 &self,
55 path: &str,
56 requires_auth: bool,
57 ) -> Result<T, CyberdropError>
58 where
59 T: DeserializeOwned,
60 {
61 let builder = self.build_request(Method::GET, path, requires_auth)?;
62 self.send_json(builder).await
63 }
64
65 pub(crate) async fn get_json_with_header<T>(
66 &self,
67 path: &str,
68 requires_auth: bool,
69 header_name: &'static str,
70 header_value: &'static str,
71 ) -> Result<T, CyberdropError>
72 where
73 T: DeserializeOwned,
74 {
75 let builder = self
76 .build_request(Method::GET, path, requires_auth)?
77 .header(header_name, header_value);
78 self.send_json(builder).await
79 }
80
81 pub(crate) async fn post_json<B, T>(
82 &self,
83 path: &str,
84 body: &B,
85 requires_auth: bool,
86 ) -> Result<T, CyberdropError>
87 where
88 B: serde::Serialize + ?Sized,
89 T: DeserializeOwned,
90 {
91 let builder = self
92 .build_request(Method::POST, path, requires_auth)?
93 .json(body);
94 self.send_json(builder).await
95 }
96
97 pub(crate) async fn post_upload_json_url<B, T>(
98 &self,
99 url: Url,
100 body: &B,
101 ) -> Result<T, CyberdropError>
102 where
103 B: serde::Serialize + ?Sized,
104 T: DeserializeOwned,
105 {
106 let builder = self
107 .upload_headers(self.build_request_url(Method::POST, url, true)?)
108 .json(body);
109
110 self.send_json(builder).await
111 }
112
113 pub(crate) async fn post_upload_multipart_url<T>(
114 &self,
115 url: Url,
116 form: Form,
117 album_id: Option<u64>,
118 ) -> Result<T, CyberdropError>
119 where
120 T: DeserializeOwned,
121 {
122 let mut builder = self.upload_headers(self.build_request_url(Method::POST, url, true)?);
123 if let Some(id) = album_id {
124 builder = builder.header("albumid", id);
125 }
126
127 self.send_json(builder.multipart(form)).await
128 }
129
130 async fn send_json<T>(&self, builder: RequestBuilder) -> Result<T, CyberdropError>
131 where
132 T: DeserializeOwned,
133 {
134 let response = builder.send().await?;
135 Self::map_status(response.status())?;
136 Ok(response.json().await?)
137 }
138
139 fn build_request(
140 &self,
141 method: Method,
142 path: &str,
143 requires_auth: bool,
144 ) -> Result<RequestBuilder, CyberdropError> {
145 let url = self.base_url.join(path)?;
146 self.build_request_url(method, url, requires_auth)
147 }
148
149 fn build_request_url(
150 &self,
151 method: Method,
152 url: Url,
153 requires_auth: bool,
154 ) -> Result<RequestBuilder, CyberdropError> {
155 let builder = self
156 .client
157 .request(method, url)
158 .header(ACCEPT, "application/json, text/plain, */*")
159 .header(ACCEPT_LANGUAGE, "nl,en-US;q=0.9,en;q=0.8");
160
161 if requires_auth {
162 self.apply_auth(builder)
163 } else {
164 Ok(builder)
165 }
166 }
167
168 fn map_status(status: StatusCode) -> Result<(), CyberdropError> {
169 if status.is_success() {
170 return Ok(());
171 }
172
173 if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN {
174 Err(CyberdropError::AuthenticationFailed(status))
175 } else {
176 Err(CyberdropError::RequestFailed(status))
177 }
178 }
179
180 fn apply_auth(&self, builder: RequestBuilder) -> Result<RequestBuilder, CyberdropError> {
181 let token = self
182 .auth_token
183 .as_ref()
184 .ok_or(CyberdropError::MissingAuthToken)?;
185
186 Ok(builder.header("token", token.as_str()))
187 }
188
189 fn upload_headers(&self, builder: RequestBuilder) -> RequestBuilder {
190 let origin = self.base_url.origin().ascii_serialization();
191 let referer = self.base_url.as_str();
192
193 builder
194 .header("X-Requested-With", "XMLHttpRequest")
195 .header("striptags", "undefined")
196 .header("Origin", origin)
197 .header("Referer", referer)
198 .header("Cache-Control", "no-cache")
199 .header("Pragma", "no-cache")
200 }
201}
202
203fn default_user_agent() -> String {
204 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0".into()
206}