1 use crate::claims::Claims;
2 use actix_web::{web, web::Data};
6 community_view::CommunityUserBanView,
12 use lemmy_structs::{blocking, comment::*, community::*, post::*, site::*, user::*};
14 apub::get_apub_protocol_string,
15 request::{retry, RecvError},
21 use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
23 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
25 use serde::Deserialize;
26 use std::process::Command;
36 #[async_trait::async_trait(?Send)]
38 type Response: serde::ser::Serialize + Send;
42 context: &Data<LemmyContext>,
43 websocket_id: Option<ConnectionId>,
44 ) -> Result<Self::Response, LemmyError>;
47 pub(in crate) async fn is_mod_or_admin(
51 ) -> Result<(), LemmyError> {
52 let is_mod_or_admin = blocking(pool, move |conn| {
53 Community::is_mod_or_admin(conn, user_id, community_id)
57 return Err(APIError::err("not_a_mod_or_admin").into());
61 pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
62 let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
64 return Err(APIError::err("not_an_admin").into());
69 pub(in crate) async fn get_post(post_id: i32, pool: &DbPool) -> Result<Post, LemmyError> {
70 match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
72 Err(_e) => Err(APIError::err("couldnt_find_post").into()),
76 pub(in crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result<User_, LemmyError> {
77 let claims = match Claims::decode(&jwt) {
78 Ok(claims) => claims.claims,
79 Err(_e) => return Err(APIError::err("not_logged_in").into()),
81 let user_id = claims.id;
82 let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
83 // Check for a site ban
85 return Err(APIError::err("site_ban").into());
90 pub(in crate) async fn get_user_from_jwt_opt(
93 ) -> Result<Option<User_>, LemmyError> {
95 Some(jwt) => Ok(Some(get_user_from_jwt(jwt, pool).await?)),
100 pub(in crate) async fn check_community_ban(
104 ) -> Result<(), LemmyError> {
105 let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
106 if blocking(pool, is_banned).await? {
107 Err(APIError::err("community_ban").into())
113 pub async fn match_websocket_operation(
114 context: LemmyContext,
118 ) -> Result<String, LemmyError> {
121 UserOperation::Login => do_websocket_operation::<Login>(context, id, op, data).await,
122 UserOperation::Register => do_websocket_operation::<Register>(context, id, op, data).await,
123 UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
124 UserOperation::GetUserDetails => {
125 do_websocket_operation::<GetUserDetails>(context, id, op, data).await
127 UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
128 UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
129 UserOperation::BanUser => do_websocket_operation::<BanUser>(context, id, op, data).await,
130 UserOperation::GetUserMentions => {
131 do_websocket_operation::<GetUserMentions>(context, id, op, data).await
133 UserOperation::MarkUserMentionAsRead => {
134 do_websocket_operation::<MarkUserMentionAsRead>(context, id, op, data).await
136 UserOperation::MarkAllAsRead => {
137 do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
139 UserOperation::DeleteAccount => {
140 do_websocket_operation::<DeleteAccount>(context, id, op, data).await
142 UserOperation::PasswordReset => {
143 do_websocket_operation::<PasswordReset>(context, id, op, data).await
145 UserOperation::PasswordChange => {
146 do_websocket_operation::<PasswordChange>(context, id, op, data).await
148 UserOperation::UserJoin => do_websocket_operation::<UserJoin>(context, id, op, data).await,
149 UserOperation::PostJoin => do_websocket_operation::<PostJoin>(context, id, op, data).await,
150 UserOperation::CommunityJoin => {
151 do_websocket_operation::<CommunityJoin>(context, id, op, data).await
153 UserOperation::SaveUserSettings => {
154 do_websocket_operation::<SaveUserSettings>(context, id, op, data).await
157 // Private Message ops
158 UserOperation::CreatePrivateMessage => {
159 do_websocket_operation::<CreatePrivateMessage>(context, id, op, data).await
161 UserOperation::EditPrivateMessage => {
162 do_websocket_operation::<EditPrivateMessage>(context, id, op, data).await
164 UserOperation::DeletePrivateMessage => {
165 do_websocket_operation::<DeletePrivateMessage>(context, id, op, data).await
167 UserOperation::MarkPrivateMessageAsRead => {
168 do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await
170 UserOperation::GetPrivateMessages => {
171 do_websocket_operation::<GetPrivateMessages>(context, id, op, data).await
175 UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await,
176 UserOperation::CreateSite => do_websocket_operation::<CreateSite>(context, id, op, data).await,
177 UserOperation::EditSite => do_websocket_operation::<EditSite>(context, id, op, data).await,
178 UserOperation::GetSite => do_websocket_operation::<GetSite>(context, id, op, data).await,
179 UserOperation::GetSiteConfig => {
180 do_websocket_operation::<GetSiteConfig>(context, id, op, data).await
182 UserOperation::SaveSiteConfig => {
183 do_websocket_operation::<SaveSiteConfig>(context, id, op, data).await
185 UserOperation::Search => do_websocket_operation::<Search>(context, id, op, data).await,
186 UserOperation::TransferCommunity => {
187 do_websocket_operation::<TransferCommunity>(context, id, op, data).await
189 UserOperation::TransferSite => {
190 do_websocket_operation::<TransferSite>(context, id, op, data).await
192 UserOperation::ListCategories => {
193 do_websocket_operation::<ListCategories>(context, id, op, data).await
197 UserOperation::GetCommunity => {
198 do_websocket_operation::<GetCommunity>(context, id, op, data).await
200 UserOperation::ListCommunities => {
201 do_websocket_operation::<ListCommunities>(context, id, op, data).await
203 UserOperation::CreateCommunity => {
204 do_websocket_operation::<CreateCommunity>(context, id, op, data).await
206 UserOperation::EditCommunity => {
207 do_websocket_operation::<EditCommunity>(context, id, op, data).await
209 UserOperation::DeleteCommunity => {
210 do_websocket_operation::<DeleteCommunity>(context, id, op, data).await
212 UserOperation::RemoveCommunity => {
213 do_websocket_operation::<RemoveCommunity>(context, id, op, data).await
215 UserOperation::FollowCommunity => {
216 do_websocket_operation::<FollowCommunity>(context, id, op, data).await
218 UserOperation::GetFollowedCommunities => {
219 do_websocket_operation::<GetFollowedCommunities>(context, id, op, data).await
221 UserOperation::BanFromCommunity => {
222 do_websocket_operation::<BanFromCommunity>(context, id, op, data).await
224 UserOperation::AddModToCommunity => {
225 do_websocket_operation::<AddModToCommunity>(context, id, op, data).await
229 UserOperation::CreatePost => do_websocket_operation::<CreatePost>(context, id, op, data).await,
230 UserOperation::GetPost => do_websocket_operation::<GetPost>(context, id, op, data).await,
231 UserOperation::GetPosts => do_websocket_operation::<GetPosts>(context, id, op, data).await,
232 UserOperation::EditPost => do_websocket_operation::<EditPost>(context, id, op, data).await,
233 UserOperation::DeletePost => do_websocket_operation::<DeletePost>(context, id, op, data).await,
234 UserOperation::RemovePost => do_websocket_operation::<RemovePost>(context, id, op, data).await,
235 UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
236 UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
237 UserOperation::CreatePostLike => {
238 do_websocket_operation::<CreatePostLike>(context, id, op, data).await
240 UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
243 UserOperation::CreateComment => {
244 do_websocket_operation::<CreateComment>(context, id, op, data).await
246 UserOperation::EditComment => {
247 do_websocket_operation::<EditComment>(context, id, op, data).await
249 UserOperation::DeleteComment => {
250 do_websocket_operation::<DeleteComment>(context, id, op, data).await
252 UserOperation::RemoveComment => {
253 do_websocket_operation::<RemoveComment>(context, id, op, data).await
255 UserOperation::MarkCommentAsRead => {
256 do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
258 UserOperation::SaveComment => {
259 do_websocket_operation::<SaveComment>(context, id, op, data).await
261 UserOperation::GetComments => {
262 do_websocket_operation::<GetComments>(context, id, op, data).await
264 UserOperation::CreateCommentLike => {
265 do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
270 async fn do_websocket_operation<'a, 'b, Data>(
271 context: LemmyContext,
275 ) -> Result<String, LemmyError>
277 for<'de> Data: Deserialize<'de> + 'a,
280 let parsed_data: Data = serde_json::from_str(&data)?;
281 let res = parsed_data
282 .perform(&web::Data::new(context), Some(id))
284 serialize_websocket_message(&op, &res)
287 pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
288 let mut built_text = String::new();
290 // Building proper speech text for espeak
291 for mut c in captcha.chars() {
292 let new_str = if c.is_alphabetic() {
293 if c.is_lowercase() {
294 c.make_ascii_uppercase();
295 format!("lower case {} ... ", c)
297 c.make_ascii_uppercase();
298 format!("capital {} ... ", c)
304 built_text.push_str(&new_str);
307 espeak_wav_base64(&built_text)
310 pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
311 // Make a temp file path
312 let uuid = uuid::Uuid::new_v4().to_string();
313 let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
315 // Write the wav file
316 Command::new("espeak")
322 // Read the wav file bytes
323 let bytes = std::fs::read(&file_path)?;
326 std::fs::remove_file(file_path)?;
329 let base64 = base64::encode(bytes);
334 #[derive(Deserialize, Debug)]
335 pub(crate) struct IframelyResponse {
336 title: Option<String>,
337 description: Option<String>,
338 thumbnail_url: Option<String>,
339 html: Option<String>,
342 pub(crate) async fn fetch_iframely(
345 ) -> Result<IframelyResponse, LemmyError> {
346 let fetch_url = format!("http://iframely/oembed?url={}", url);
348 let response = retry(|| client.get(&fetch_url).send()).await?;
350 let res: IframelyResponse = response
353 .map_err(|e| RecvError(e.to_string()))?;
357 #[derive(Deserialize, Debug, Clone)]
358 pub(crate) struct PictrsResponse {
359 files: Vec<PictrsFile>,
363 #[derive(Deserialize, Debug, Clone)]
364 pub(crate) struct PictrsFile {
366 delete_token: String,
369 pub(crate) async fn fetch_pictrs(
372 ) -> Result<PictrsResponse, LemmyError> {
373 is_image_content_type(client, image_url).await?;
375 let fetch_url = format!(
376 "http://pictrs:8080/image/download?url={}",
377 utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
380 let response = retry(|| client.get(&fetch_url).send()).await?;
382 let response: PictrsResponse = response
385 .map_err(|e| RecvError(e.to_string()))?;
387 if response.msg == "ok" {
390 Err(anyhow!("{}", &response.msg).into())
394 async fn fetch_iframely_and_pictrs_data(
405 // Fetch iframely data
406 let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) =
407 match fetch_iframely(client, url).await {
408 Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
410 error!("iframely err: {}", e);
411 (None, None, None, None)
415 // Fetch pictrs thumbnail
416 let pictrs_hash = match iframely_thumbnail_url {
417 Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
418 Ok(res) => Some(res.files[0].file.to_owned()),
420 error!("pictrs err: {}", e);
424 // Try to generate a small thumbnail if iframely is not supported
425 None => match fetch_pictrs(client, &url).await {
426 Ok(res) => Some(res.files[0].file.to_owned()),
428 error!("pictrs err: {}", e);
434 // The full urls are necessary for federation
435 let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash {
437 "{}://{}/pictrs/image/{}",
438 get_apub_protocol_string(),
439 Settings::get().hostname,
448 iframely_description,
453 None => (None, None, None, None),
457 pub(crate) async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
458 let response = retry(|| client.get(test).send()).await?;
463 .ok_or_else(|| anyhow!("No Content-Type header"))?
465 .starts_with("image/")
469 Err(anyhow!("Not an image type.").into())
475 use crate::{captcha_espeak_wav_base64, is_image_content_type};
479 actix_rt::System::new("tset_image").block_on(async move {
480 let client = reqwest::Client::default();
481 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());
482 assert!(is_image_content_type(&client,
483 "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
492 assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
495 // These helped with testing
497 // fn test_iframely() {
498 // let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
499 // assert!(res.is_ok());
503 // fn test_pictshare() {
504 // let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
505 // assert!(res.is_ok());
506 // let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
507 // assert!(res_other.is_err());