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