1mod note_list_query_builder;
2mod post_list_query_builder;
3mod tag_list_query_builder;
4
5pub use self::{
6 note_list_query_builder::NotesListQueryBuilder,
7 post_list_query_builder::PostListQueryBuilder,
8 tag_list_query_builder::TagListQueryBuilder,
9};
10#[cfg(feature = "scrape")]
11use crate::HtmlPost;
12use crate::{
13 DeletedImageList,
14 Error,
15};
16use reqwest::header::{
17 HeaderMap,
18 HeaderValue,
19};
20#[cfg(feature = "scrape")]
21use scraper::Html;
22use std::{
23 num::NonZeroU64,
24 sync::{
25 Arc,
26 Mutex,
27 },
28 time::Duration,
29};
30use url::Url;
31
32static USER_AGENT_VALUE: HeaderValue = HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4514.0 Safari/537.36");
34static REFERER_VALUE: HeaderValue = HeaderValue::from_static("https://rule34.xxx/");
35static ACCEPT_LANGUAGE_VALUE: HeaderValue = HeaderValue::from_static("en,en-US;q=0,5");
36static ACCEPT_VALUE: HeaderValue = HeaderValue::from_static("*/*");
37
38#[derive(Debug)]
39struct AuthState {
40 user_id: u64,
41 api_key: String,
42}
43
44#[derive(Debug, Clone)]
46pub struct Client {
47 pub client: reqwest::Client,
51
52 auth_state: Arc<Mutex<Option<AuthState>>>,
54}
55
56impl Client {
57 pub fn new() -> Self {
59 let mut default_headers = HeaderMap::new();
60 default_headers.insert(reqwest::header::USER_AGENT, USER_AGENT_VALUE.clone());
61 default_headers.insert(
62 reqwest::header::ACCEPT_LANGUAGE,
63 ACCEPT_LANGUAGE_VALUE.clone(),
64 );
65 default_headers.insert(reqwest::header::ACCEPT, ACCEPT_VALUE.clone());
66 default_headers.insert(reqwest::header::REFERER, REFERER_VALUE.clone());
67
68 let client = reqwest::Client::builder()
69 .default_headers(default_headers)
70 .connect_timeout(Duration::from_secs(10))
71 .build()
72 .expect("failed to build rule34 client");
73
74 Client {
75 client,
76 auth_state: Arc::new(Mutex::new(None)),
77 }
78 }
79
80 pub fn set_auth(&self, user_id: u64, api_key: &str) {
82 let mut auth_state = self
83 .auth_state
84 .lock()
85 .unwrap_or_else(|error| error.into_inner());
86
87 *auth_state = Some(AuthState {
88 user_id,
89 api_key: api_key.into(),
90 });
91 }
92
93 fn get_auth(&self) -> std::sync::MutexGuard<'_, Option<AuthState>> {
95 self.auth_state
96 .lock()
97 .unwrap_or_else(|error| error.into_inner())
98 }
99
100 async fn get_text(&self, url: &str) -> Result<String, Error> {
102 Ok(self
103 .client
104 .get(url)
105 .timeout(Duration::from_secs(90))
106 .send()
107 .await?
108 .error_for_status()?
109 .text()
110 .await?)
111 }
112
113 #[cfg(feature = "scrape")]
116 async fn get_html<F, T>(&self, uri: &str, f: F) -> Result<T, Error>
117 where
118 F: FnOnce(Html) -> T + Send + 'static,
119 T: Send + 'static,
120 {
121 let text = self.get_text(uri).await?;
122 let ret =
123 tokio::task::spawn_blocking(move || f(Html::parse_document(text.as_str()))).await?;
124 Ok(ret)
125 }
126
127 async fn get_xml<T>(&self, uri: &str) -> Result<T, Error>
129 where
130 T: serde::de::DeserializeOwned + Send + 'static,
131 {
132 let text = self.get_text(uri).await?;
133 let ret = tokio::task::spawn_blocking(move || quick_xml::de::from_str(&text)).await??;
134 Ok(ret)
135 }
136
137 pub fn list_posts(&self) -> PostListQueryBuilder<'_> {
139 PostListQueryBuilder::new(self)
140 }
141
142 #[cfg(feature = "scrape")]
144 pub async fn get_html_post(&self, id: NonZeroU64) -> Result<HtmlPost, Error> {
145 let url = crate::post_id_to_html_post_url(id);
146 let ret = self
147 .get_html(url.as_str(), |html| HtmlPost::from_html(&html))
148 .await??;
149
150 Ok(ret)
151 }
152
153 pub async fn list_deleted_images(
163 &self,
164 last_id: Option<NonZeroU64>,
165 ) -> Result<DeletedImageList, Error> {
166 let mut url = Url::parse_with_params(
167 crate::API_BASE_URL,
168 &[
169 ("page", "dapi"),
170 ("s", "post"),
171 ("q", "index"),
172 ("deleted", "show"),
173 ],
174 )?;
175 if let Some(last_id) = last_id {
176 let mut last_id_buf = itoa::Buffer::new();
177 url.query_pairs_mut()
178 .append_pair("last_id", last_id_buf.format(last_id.get()));
179 }
180
181 {
182 let auth = self.get_auth();
183 let auth = auth.as_ref().ok_or(Error::MissingAuth)?;
184
185 let mut query_pairs_mut = url.query_pairs_mut();
186
187 query_pairs_mut.append_pair("user_id", itoa::Buffer::new().format(auth.user_id));
188 query_pairs_mut.append_pair("api_key", &auth.api_key);
189 }
190
191 self.get_xml(url.as_str()).await
195 }
196
197 pub fn list_tags(&self) -> TagListQueryBuilder<'_> {
199 TagListQueryBuilder::new(self)
200 }
201
202 pub fn list_notes(&self) -> NotesListQueryBuilder<'_> {
206 NotesListQueryBuilder::new(self)
207 }
208}
209
210impl Default for Client {
211 fn default() -> Self {
212 Self::new()
213 }
214}