1 #![recursion_limit = "512"]
3 extern crate lazy_static;
5 extern crate actix_web;
12 extern crate jsonwebtoken;
18 extern crate serde_json;
24 pub mod code_migrations;
31 request::{retry, RecvError},
32 websocket::chat_server::ChatServer,
36 use background_jobs::QueueHandle;
38 use lemmy_utils::{apub::get_apub_protocol_string, settings::Settings, LemmyError};
40 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
42 use serde::Deserialize;
43 use std::process::Command;
45 pub struct LemmyContext {
47 pub chat_server: Addr<ChatServer>,
49 pub activity_queue: QueueHandle,
55 chat_server: Addr<ChatServer>,
57 activity_queue: QueueHandle,
66 pub fn pool(&self) -> &DbPool {
69 pub fn chat_server(&self) -> &Addr<ChatServer> {
72 pub fn client(&self) -> &Client {
75 pub fn activity_queue(&self) -> &QueueHandle {
80 impl Clone for LemmyContext {
81 fn clone(&self) -> Self {
83 pool: self.pool.clone(),
84 chat_server: self.chat_server.clone(),
85 client: self.client.clone(),
86 activity_queue: self.activity_queue.clone(),
91 #[derive(Deserialize, Debug)]
92 pub struct IframelyResponse {
93 title: Option<String>,
94 description: Option<String>,
95 thumbnail_url: Option<String>,
99 pub async fn fetch_iframely(client: &Client, url: &str) -> Result<IframelyResponse, LemmyError> {
100 let fetch_url = format!("http://iframely/oembed?url={}", url);
102 let response = retry(|| client.get(&fetch_url).send()).await?;
104 let res: IframelyResponse = response
107 .map_err(|e| RecvError(e.to_string()))?;
111 #[derive(Deserialize, Debug, Clone)]
112 pub struct PictrsResponse {
113 files: Vec<PictrsFile>,
117 #[derive(Deserialize, Debug, Clone)]
118 pub struct PictrsFile {
120 delete_token: String,
123 pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result<PictrsResponse, LemmyError> {
124 is_image_content_type(client, image_url).await?;
126 let fetch_url = format!(
127 "http://pictrs:8080/image/download?url={}",
128 utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
131 let response = retry(|| client.get(&fetch_url).send()).await?;
133 let response: PictrsResponse = response
136 .map_err(|e| RecvError(e.to_string()))?;
138 if response.msg == "ok" {
141 Err(anyhow!("{}", &response.msg).into())
145 async fn fetch_iframely_and_pictrs_data(
156 // Fetch iframely data
157 let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) =
158 match fetch_iframely(client, url).await {
159 Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
161 error!("iframely err: {}", e);
162 (None, None, None, None)
166 // Fetch pictrs thumbnail
167 let pictrs_hash = match iframely_thumbnail_url {
168 Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
169 Ok(res) => Some(res.files[0].file.to_owned()),
171 error!("pictrs err: {}", e);
175 // Try to generate a small thumbnail if iframely is not supported
176 None => match fetch_pictrs(client, &url).await {
177 Ok(res) => Some(res.files[0].file.to_owned()),
179 error!("pictrs err: {}", e);
185 // The full urls are necessary for federation
186 let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash {
188 "{}://{}/pictrs/image/{}",
189 get_apub_protocol_string(),
190 Settings::get().hostname,
199 iframely_description,
204 None => (None, None, None, None),
208 pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
209 let response = retry(|| client.get(test).send()).await?;
214 .ok_or_else(|| anyhow!("No Content-Type header"))?
216 .starts_with("image/")
220 Err(anyhow!("Not an image type.").into())
224 pub fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
225 let mut built_text = String::new();
227 // Building proper speech text for espeak
228 for mut c in captcha.chars() {
229 let new_str = if c.is_alphabetic() {
230 if c.is_lowercase() {
231 c.make_ascii_uppercase();
232 format!("lower case {} ... ", c)
234 c.make_ascii_uppercase();
235 format!("capital {} ... ", c)
241 built_text.push_str(&new_str);
244 espeak_wav_base64(&built_text)
247 pub fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
248 // Make a temp file path
249 let uuid = uuid::Uuid::new_v4().to_string();
250 let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
252 // Write the wav file
253 Command::new("espeak")
259 // Read the wav file bytes
260 let bytes = std::fs::read(&file_path)?;
263 std::fs::remove_file(file_path)?;
266 let base64 = base64::encode(bytes);
273 use crate::{captcha_espeak_wav_base64, is_image_content_type};
277 actix_rt::System::new("tset_image").block_on(async move {
278 let client = reqwest::Client::default();
279 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());
280 assert!(is_image_content_type(&client,
281 "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
290 assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
293 // These helped with testing
295 // fn test_iframely() {
296 // let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
297 // assert!(res.is_ok());
301 // fn test_pictshare() {
302 // let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
303 // assert!(res.is_ok());
304 // let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
305 // assert!(res_other.is_err());