]> Untitled Git - lemmy.git/blob - crates/utils/src/request.rs
Simplify config using macros (#1686)
[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.expect("retry http request")
48 }
49
50 #[derive(Deserialize, Debug)]
51 pub struct IframelyResponse {
52   pub title: Option<String>,
53   pub description: Option<String>,
54   thumbnail_url: Option<Url>,
55   pub html: Option<String>,
56 }
57
58 pub(crate) async fn fetch_iframely(
59   client: &Client,
60   url: &Url,
61 ) -> Result<IframelyResponse, LemmyError> {
62   if let Some(iframely_url) = Settings::get().iframely_url {
63     let fetch_url = format!("{}/oembed?url={}", iframely_url, url);
64
65     let response = retry(|| client.get(&fetch_url).send()).await?;
66
67     let res: IframelyResponse = response
68       .json()
69       .await
70       .map_err(|e| RecvError(e.to_string()))?;
71     Ok(res)
72   } else {
73     Err(anyhow!("Missing Iframely URL in config.").into())
74   }
75 }
76
77 #[derive(Deserialize, Debug, Clone)]
78 pub(crate) struct PictrsResponse {
79   files: Vec<PictrsFile>,
80   msg: String,
81 }
82
83 #[derive(Deserialize, Debug, Clone)]
84 pub(crate) struct PictrsFile {
85   file: String,
86   delete_token: String,
87 }
88
89 pub(crate) async fn fetch_pictrs(
90   client: &Client,
91   image_url: &Url,
92 ) -> Result<Option<PictrsResponse>, LemmyError> {
93   if let Some(pictrs_url) = Settings::get().pictrs_url {
94     is_image_content_type(client, image_url).await?;
95
96     let fetch_url = format!(
97       "{}/image/download?url={}",
98       pictrs_url,
99       utf8_percent_encode(image_url.as_str(), NON_ALPHANUMERIC) // TODO this might not be needed
100     );
101
102     let response = retry(|| client.get(&fetch_url).send()).await?;
103
104     let response: PictrsResponse = response
105       .json()
106       .await
107       .map_err(|e| RecvError(e.to_string()))?;
108
109     if response.msg == "ok" {
110       Ok(Some(response))
111     } else {
112       Err(anyhow!("{}", &response.msg).into())
113     }
114   } else {
115     Ok(None)
116   }
117 }
118
119 pub async fn fetch_iframely_and_pictrs_data(
120   client: &Client,
121   url: Option<&Url>,
122 ) -> Result<(Option<IframelyResponse>, Option<Url>), LemmyError> {
123   match &url {
124     Some(url) => {
125       // Fetch iframely data
126       let iframely_res_option = fetch_iframely(client, url).await.ok();
127
128       // Fetch pictrs thumbnail
129       let pictrs_hash = match &iframely_res_option {
130         Some(iframely_res) => match &iframely_res.thumbnail_url {
131           Some(iframely_thumbnail_url) => fetch_pictrs(client, iframely_thumbnail_url)
132             .await?
133             .map(|r| r.files[0].file.to_owned()),
134           // Try to generate a small thumbnail if iframely is not supported
135           None => fetch_pictrs(client, url)
136             .await?
137             .map(|r| r.files[0].file.to_owned()),
138         },
139         None => fetch_pictrs(client, url)
140           .await?
141           .map(|r| r.files[0].file.to_owned()),
142       };
143
144       // The full urls are necessary for federation
145       let pictrs_thumbnail = pictrs_hash
146         .map(|p| {
147           Url::parse(&format!(
148             "{}/pictrs/image/{}",
149             Settings::get().get_protocol_and_hostname(),
150             p
151           ))
152           .ok()
153         })
154         .flatten();
155
156       Ok((iframely_res_option, pictrs_thumbnail))
157     }
158     None => Ok((None, None)),
159   }
160 }
161
162 async fn is_image_content_type(client: &Client, test: &Url) -> Result<(), LemmyError> {
163   let response = retry(|| client.get(test.to_owned()).send()).await?;
164   if response
165     .headers()
166     .get("Content-Type")
167     .ok_or_else(|| anyhow!("No Content-Type header"))?
168     .to_str()?
169     .starts_with("image/")
170   {
171     Ok(())
172   } else {
173     Err(anyhow!("Not an image type.").into())
174   }
175 }
176
177 #[cfg(test)]
178 mod tests {
179   // These helped with testing
180   // #[test]
181   // fn test_iframely() {
182   //   let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
183   //   assert!(res.is_ok());
184   // }
185
186   // #[test]
187   // fn test_pictshare() {
188   //   let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
189   //   assert!(res.is_ok());
190   //   let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
191   //   assert!(res_other.is_err());
192   // }
193 }