1 use crate::{settings::structs::Settings, LemmyError};
4 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
6 use serde::Deserialize;
7 use std::future::Future;
11 #[derive(Clone, Debug, Error)]
12 #[error("Error sending request, {0}")]
13 struct SendError(pub String);
15 #[derive(Clone, Debug, Error)]
16 #[error("Error receiving response, {0}")]
17 pub struct RecvError(pub String);
19 pub async fn retry<F, Fut, T>(f: F) -> Result<T, reqwest::Error>
22 Fut: Future<Output = Result<T, reqwest::Error>>,
24 retry_custom(|| async { Ok((f)().await) }).await
27 async fn retry_custom<F, Fut, T>(f: F) -> Result<T, reqwest::Error>
30 Fut: Future<Output = Result<Result<T, reqwest::Error>, reqwest::Error>>,
32 let mut response: Option<Result<T, reqwest::Error>> = None;
36 Ok(t) => return Ok(t),
39 response = Some(Err(e));
47 response.expect("retry http request")
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>,
58 pub(crate) async fn fetch_iframely(
61 ) -> Result<IframelyResponse, LemmyError> {
62 if let Some(iframely_url) = Settings::get().iframely_url {
63 let fetch_url = format!("{}/oembed?url={}", iframely_url, url);
65 let response = retry(|| client.get(&fetch_url).send()).await?;
67 let res: IframelyResponse = response
70 .map_err(|e| RecvError(e.to_string()))?;
73 Err(anyhow!("Missing Iframely URL in config.").into())
77 #[derive(Deserialize, Debug, Clone)]
78 pub(crate) struct PictrsResponse {
79 files: Vec<PictrsFile>,
83 #[derive(Deserialize, Debug, Clone)]
84 pub(crate) struct PictrsFile {
89 pub(crate) async fn fetch_pictrs(
92 ) -> Result<Option<PictrsResponse>, LemmyError> {
93 if let Some(pictrs_url) = Settings::get().pictrs_url {
94 is_image_content_type(client, image_url).await?;
96 let fetch_url = format!(
97 "{}/image/download?url={}",
99 utf8_percent_encode(image_url.as_str(), NON_ALPHANUMERIC) // TODO this might not be needed
102 let response = retry(|| client.get(&fetch_url).send()).await?;
104 let response: PictrsResponse = response
107 .map_err(|e| RecvError(e.to_string()))?;
109 if response.msg == "ok" {
112 Err(anyhow!("{}", &response.msg).into())
119 pub async fn fetch_iframely_and_pictrs_data(
122 ) -> Result<(Option<IframelyResponse>, Option<Url>), LemmyError> {
125 // Fetch iframely data
126 let iframely_res_option = fetch_iframely(client, url).await.ok();
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)
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)
137 .map(|r| r.files[0].file.to_owned()),
139 None => fetch_pictrs(client, url)
141 .map(|r| r.files[0].file.to_owned()),
144 // The full urls are necessary for federation
145 let pictrs_thumbnail = pictrs_hash
148 "{}/pictrs/image/{}",
149 Settings::get().get_protocol_and_hostname(),
156 Ok((iframely_res_option, pictrs_thumbnail))
158 None => Ok((None, None)),
162 async fn is_image_content_type(client: &Client, test: &Url) -> Result<(), LemmyError> {
163 let response = retry(|| client.get(test.to_owned()).send()).await?;
167 .ok_or_else(|| anyhow!("No Content-Type header"))?
169 .starts_with("image/")
173 Err(anyhow!("Not an image type.").into())
179 // These helped with testing
181 // fn test_iframely() {
182 // let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
183 // assert!(res.is_ok());
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());