]> Untitled Git - lemmy.git/blob - src/lib.rs
Isomorphic docker (#1124)
[lemmy.git] / src / lib.rs
1 #![recursion_limit = "512"]
2 #[macro_use]
3 extern crate strum_macros;
4 #[macro_use]
5 extern crate lazy_static;
6 extern crate actix;
7 extern crate actix_web;
8 extern crate base64;
9 extern crate bcrypt;
10 extern crate captcha;
11 extern crate chrono;
12 extern crate diesel;
13 extern crate dotenv;
14 extern crate jsonwebtoken;
15 extern crate log;
16 extern crate openssl;
17 extern crate reqwest;
18 extern crate rss;
19 extern crate serde;
20 extern crate serde_json;
21 extern crate sha2;
22 extern crate strum;
23
24 pub mod api;
25 pub mod apub;
26 pub mod code_migrations;
27 pub mod request;
28 pub mod routes;
29 pub mod version;
30 pub mod websocket;
31
32 use crate::{
33   request::{retry, RecvError},
34   websocket::chat_server::ChatServer,
35 };
36 use actix::Addr;
37 use anyhow::anyhow;
38 use background_jobs::QueueHandle;
39 use lemmy_db::DbPool;
40 use lemmy_utils::{apub::get_apub_protocol_string, settings::Settings, LemmyError};
41 use log::error;
42 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
43 use reqwest::Client;
44 use serde::Deserialize;
45 use std::process::Command;
46
47 pub struct LemmyContext {
48   pub pool: DbPool,
49   pub chat_server: Addr<ChatServer>,
50   pub client: Client,
51   pub activity_queue: QueueHandle,
52 }
53
54 impl LemmyContext {
55   pub fn create(
56     pool: DbPool,
57     chat_server: Addr<ChatServer>,
58     client: Client,
59     activity_queue: QueueHandle,
60   ) -> LemmyContext {
61     LemmyContext {
62       pool,
63       chat_server,
64       client,
65       activity_queue,
66     }
67   }
68   pub fn pool(&self) -> &DbPool {
69     &self.pool
70   }
71   pub fn chat_server(&self) -> &Addr<ChatServer> {
72     &self.chat_server
73   }
74   pub fn client(&self) -> &Client {
75     &self.client
76   }
77   pub fn activity_queue(&self) -> &QueueHandle {
78     &self.activity_queue
79   }
80 }
81
82 impl Clone for LemmyContext {
83   fn clone(&self) -> Self {
84     LemmyContext {
85       pool: self.pool.clone(),
86       chat_server: self.chat_server.clone(),
87       client: self.client.clone(),
88       activity_queue: self.activity_queue.clone(),
89     }
90   }
91 }
92
93 #[derive(Deserialize, Debug)]
94 pub struct IframelyResponse {
95   title: Option<String>,
96   description: Option<String>,
97   thumbnail_url: Option<String>,
98   html: Option<String>,
99 }
100
101 pub async fn fetch_iframely(client: &Client, url: &str) -> Result<IframelyResponse, LemmyError> {
102   let fetch_url = format!("http://iframely/oembed?url={}", url);
103
104   let response = retry(|| client.get(&fetch_url).send()).await?;
105
106   let res: IframelyResponse = response
107     .json()
108     .await
109     .map_err(|e| RecvError(e.to_string()))?;
110   Ok(res)
111 }
112
113 #[derive(Deserialize, Debug, Clone)]
114 pub struct PictrsResponse {
115   files: Vec<PictrsFile>,
116   msg: String,
117 }
118
119 #[derive(Deserialize, Debug, Clone)]
120 pub struct PictrsFile {
121   file: String,
122   delete_token: String,
123 }
124
125 pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result<PictrsResponse, LemmyError> {
126   is_image_content_type(client, image_url).await?;
127
128   let fetch_url = format!(
129     "http://pictrs:8080/image/download?url={}",
130     utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
131   );
132
133   let response = retry(|| client.get(&fetch_url).send()).await?;
134
135   let response: PictrsResponse = response
136     .json()
137     .await
138     .map_err(|e| RecvError(e.to_string()))?;
139
140   if response.msg == "ok" {
141     Ok(response)
142   } else {
143     Err(anyhow!("{}", &response.msg).into())
144   }
145 }
146
147 async fn fetch_iframely_and_pictrs_data(
148   client: &Client,
149   url: Option<String>,
150 ) -> (
151   Option<String>,
152   Option<String>,
153   Option<String>,
154   Option<String>,
155 ) {
156   match &url {
157     Some(url) => {
158       // Fetch iframely data
159       let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) =
160         match fetch_iframely(client, url).await {
161           Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
162           Err(e) => {
163             error!("iframely err: {}", e);
164             (None, None, None, None)
165           }
166         };
167
168       // Fetch pictrs thumbnail
169       let pictrs_hash = match iframely_thumbnail_url {
170         Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
171           Ok(res) => Some(res.files[0].file.to_owned()),
172           Err(e) => {
173             error!("pictrs err: {}", e);
174             None
175           }
176         },
177         // Try to generate a small thumbnail if iframely is not supported
178         None => match fetch_pictrs(client, &url).await {
179           Ok(res) => Some(res.files[0].file.to_owned()),
180           Err(e) => {
181             error!("pictrs err: {}", e);
182             None
183           }
184         },
185       };
186
187       // The full urls are necessary for federation
188       let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash {
189         Some(format!(
190           "{}://{}/pictrs/image/{}",
191           get_apub_protocol_string(),
192           Settings::get().hostname,
193           pictrs_hash
194         ))
195       } else {
196         None
197       };
198
199       (
200         iframely_title,
201         iframely_description,
202         iframely_html,
203         pictrs_thumbnail,
204       )
205     }
206     None => (None, None, None, None),
207   }
208 }
209
210 pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
211   let response = retry(|| client.get(test).send()).await?;
212
213   if response
214     .headers()
215     .get("Content-Type")
216     .ok_or_else(|| anyhow!("No Content-Type header"))?
217     .to_str()?
218     .starts_with("image/")
219   {
220     Ok(())
221   } else {
222     Err(anyhow!("Not an image type.").into())
223   }
224 }
225
226 pub fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
227   let mut built_text = String::new();
228
229   // Building proper speech text for espeak
230   for mut c in captcha.chars() {
231     let new_str = if c.is_alphabetic() {
232       if c.is_lowercase() {
233         c.make_ascii_uppercase();
234         format!("lower case {} ... ", c)
235       } else {
236         c.make_ascii_uppercase();
237         format!("capital {} ... ", c)
238       }
239     } else {
240       format!("{} ...", c)
241     };
242
243     built_text.push_str(&new_str);
244   }
245
246   espeak_wav_base64(&built_text)
247 }
248
249 pub fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
250   // Make a temp file path
251   let uuid = uuid::Uuid::new_v4().to_string();
252   let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
253
254   // Write the wav file
255   Command::new("espeak")
256     .arg("-w")
257     .arg(&file_path)
258     .arg(text)
259     .status()?;
260
261   // Read the wav file bytes
262   let bytes = std::fs::read(&file_path)?;
263
264   // Delete the file
265   std::fs::remove_file(file_path)?;
266
267   // Convert to base64
268   let base64 = base64::encode(bytes);
269
270   Ok(base64)
271 }
272
273 #[cfg(test)]
274 mod tests {
275   use crate::{captcha_espeak_wav_base64, is_image_content_type};
276
277   #[test]
278   fn test_image() {
279     actix_rt::System::new("tset_image").block_on(async move {
280       let client = reqwest::Client::default();
281       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());
282       assert!(is_image_content_type(&client,
283                                     "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
284       )
285         .await.is_err()
286       );
287     });
288   }
289
290   #[test]
291   fn test_espeak() {
292     assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
293   }
294
295   // These helped with testing
296   // #[test]
297   // fn test_iframely() {
298   //   let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
299   //   assert!(res.is_ok());
300   // }
301
302   // #[test]
303   // fn test_pictshare() {
304   //   let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
305   //   assert!(res.is_ok());
306   //   let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
307   //   assert!(res_other.is_err());
308   // }
309 }