]> Untitled Git - lemmy.git/blob - crates/utils/src/request.rs
Move most code into crates/ subfolder
[lemmy.git] / crates / utils / src / request.rs
1 use crate::{settings::Settings, LemmyError};
2 use anyhow::anyhow;
3 use log::error;
4 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
5 use reqwest::Client;
6 use serde::Deserialize;
7 use std::future::Future;
8 use thiserror::Error;
9
10 #[derive(Clone, Debug, Error)]
11 #[error("Error sending request, {0}")]
12 struct SendError(pub String);
13
14 #[derive(Clone, Debug, Error)]
15 #[error("Error receiving response, {0}")]
16 pub struct RecvError(pub String);
17
18 pub async fn retry<F, Fut, T>(f: F) -> Result<T, reqwest::Error>
19 where
20   F: Fn() -> Fut,
21   Fut: Future<Output = Result<T, reqwest::Error>>,
22 {
23   retry_custom(|| async { Ok((f)().await) }).await
24 }
25
26 async fn retry_custom<F, Fut, T>(f: F) -> Result<T, reqwest::Error>
27 where
28   F: Fn() -> Fut,
29   Fut: Future<Output = Result<Result<T, reqwest::Error>, reqwest::Error>>,
30 {
31   let mut response: Option<Result<T, reqwest::Error>> = None;
32
33   for _ in 0u8..3 {
34     match (f)().await? {
35       Ok(t) => return Ok(t),
36       Err(e) => {
37         if e.is_timeout() {
38           response = Some(Err(e));
39           continue;
40         }
41         return Err(e);
42       }
43     }
44   }
45
46   response.unwrap()
47 }
48
49 #[derive(Deserialize, Debug)]
50 pub(crate) struct IframelyResponse {
51   title: Option<String>,
52   description: Option<String>,
53   thumbnail_url: Option<String>,
54   html: Option<String>,
55 }
56
57 pub(crate) async fn fetch_iframely(
58   client: &Client,
59   url: &str,
60 ) -> Result<IframelyResponse, LemmyError> {
61   let fetch_url = format!("{}/oembed?url={}", Settings::get().iframely_url, url);
62
63   let response = retry(|| client.get(&fetch_url).send()).await?;
64
65   let res: IframelyResponse = response
66     .json()
67     .await
68     .map_err(|e| RecvError(e.to_string()))?;
69   Ok(res)
70 }
71
72 #[derive(Deserialize, Debug, Clone)]
73 pub(crate) struct PictrsResponse {
74   files: Vec<PictrsFile>,
75   msg: String,
76 }
77
78 #[derive(Deserialize, Debug, Clone)]
79 pub(crate) struct PictrsFile {
80   file: String,
81   delete_token: String,
82 }
83
84 pub(crate) async fn fetch_pictrs(
85   client: &Client,
86   image_url: &str,
87 ) -> Result<PictrsResponse, LemmyError> {
88   is_image_content_type(client, image_url).await?;
89
90   let fetch_url = format!(
91     "{}/image/download?url={}",
92     Settings::get().pictrs_url,
93     utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
94   );
95
96   let response = retry(|| client.get(&fetch_url).send()).await?;
97
98   let response: PictrsResponse = response
99     .json()
100     .await
101     .map_err(|e| RecvError(e.to_string()))?;
102
103   if response.msg == "ok" {
104     Ok(response)
105   } else {
106     Err(anyhow!("{}", &response.msg).into())
107   }
108 }
109
110 pub async fn fetch_iframely_and_pictrs_data(
111   client: &Client,
112   url: Option<String>,
113 ) -> (
114   Option<String>,
115   Option<String>,
116   Option<String>,
117   Option<String>,
118 ) {
119   match &url {
120     Some(url) => {
121       // Fetch iframely data
122       let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) =
123         match fetch_iframely(client, url).await {
124           Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
125           Err(e) => {
126             error!("iframely err: {}", e);
127             (None, None, None, None)
128           }
129         };
130
131       // Fetch pictrs thumbnail
132       let pictrs_hash = match iframely_thumbnail_url {
133         Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
134           Ok(res) => Some(res.files[0].file.to_owned()),
135           Err(e) => {
136             error!("pictrs err: {}", e);
137             None
138           }
139         },
140         // Try to generate a small thumbnail if iframely is not supported
141         None => match fetch_pictrs(client, &url).await {
142           Ok(res) => Some(res.files[0].file.to_owned()),
143           Err(e) => {
144             error!("pictrs err: {}", e);
145             None
146           }
147         },
148       };
149
150       // The full urls are necessary for federation
151       let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash {
152         Some(format!(
153           "{}/pictrs/image/{}",
154           Settings::get().get_protocol_and_hostname(),
155           pictrs_hash
156         ))
157       } else {
158         None
159       };
160
161       (
162         iframely_title,
163         iframely_description,
164         iframely_html,
165         pictrs_thumbnail,
166       )
167     }
168     None => (None, None, None, None),
169   }
170 }
171
172 async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
173   let response = retry(|| client.get(test).send()).await?;
174
175   if response
176     .headers()
177     .get("Content-Type")
178     .ok_or_else(|| anyhow!("No Content-Type header"))?
179     .to_str()?
180     .starts_with("image/")
181   {
182     Ok(())
183   } else {
184     Err(anyhow!("Not an image type.").into())
185   }
186 }
187
188 #[cfg(test)]
189 mod tests {
190   use crate::request::is_image_content_type;
191
192   #[test]
193   fn test_image() {
194     actix_rt::System::new("tset_image").block_on(async move {
195       let client = reqwest::Client::default();
196       assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
197       assert!(is_image_content_type(&client,
198                                     "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
199       )
200         .await.is_err()
201       );
202     });
203   }
204
205   // These helped with testing
206   // #[test]
207   // fn test_iframely() {
208   //   let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
209   //   assert!(res.is_ok());
210   // }
211
212   // #[test]
213   // fn test_pictshare() {
214   //   let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
215   //   assert!(res.is_ok());
216   //   let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
217   //   assert!(res_other.is_err());
218   // }
219 }