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