use crate::claims::Claims; use actix_web::{web, web::Data}; use anyhow::anyhow; use lemmy_db::{ community::Community, community_view::CommunityUserBanView, post::Post, user::User_, Crud, DbPool, }; use lemmy_structs::{blocking, comment::*, community::*, post::*, site::*, user::*}; use lemmy_utils::{ apub::get_apub_protocol_string, request::{retry, RecvError}, settings::Settings, APIError, ConnectionId, LemmyError, }; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; use log::error; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use reqwest::Client; use serde::Deserialize; use std::process::Command; pub mod claims; pub mod comment; pub mod community; pub mod post; pub mod site; pub mod user; pub mod version; #[async_trait::async_trait(?Send)] pub trait Perform { type Response: serde::ser::Serialize + Send; async fn perform( &self, context: &Data, websocket_id: Option, ) -> Result; } pub(in crate) async fn is_mod_or_admin( pool: &DbPool, user_id: i32, community_id: i32, ) -> Result<(), LemmyError> { let is_mod_or_admin = blocking(pool, move |conn| { Community::is_mod_or_admin(conn, user_id, community_id) }) .await?; if !is_mod_or_admin { return Err(APIError::err("not_a_mod_or_admin").into()); } Ok(()) } pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> { let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if !user.admin { return Err(APIError::err("not_an_admin").into()); } Ok(()) } pub(in crate) async fn get_post(post_id: i32, pool: &DbPool) -> Result { match blocking(pool, move |conn| Post::read(conn, post_id)).await? { Ok(post) => Ok(post), Err(_e) => Err(APIError::err("couldnt_find_post").into()), } } pub(in crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result { let claims = match Claims::decode(&jwt) { Ok(claims) => claims.claims, Err(_e) => return Err(APIError::err("not_logged_in").into()), }; let user_id = claims.id; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; // Check for a site ban if user.banned { return Err(APIError::err("site_ban").into()); } Ok(user) } pub(in crate) async fn get_user_from_jwt_opt( jwt: &Option, pool: &DbPool, ) -> Result, LemmyError> { match jwt { Some(jwt) => Ok(Some(get_user_from_jwt(jwt, pool).await?)), None => Ok(None), } } pub(in crate) async fn check_community_ban( user_id: i32, community_id: i32, pool: &DbPool, ) -> Result<(), LemmyError> { let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); if blocking(pool, is_banned).await? { Err(APIError::err("community_ban").into()) } else { Ok(()) } } pub async fn match_websocket_operation( context: LemmyContext, id: ConnectionId, op: UserOperation, data: &str, ) -> Result { match op { // User ops UserOperation::Login => do_websocket_operation::(context, id, op, data).await, UserOperation::Register => do_websocket_operation::(context, id, op, data).await, UserOperation::GetCaptcha => do_websocket_operation::(context, id, op, data).await, UserOperation::GetUserDetails => { do_websocket_operation::(context, id, op, data).await } UserOperation::GetReplies => do_websocket_operation::(context, id, op, data).await, UserOperation::AddAdmin => do_websocket_operation::(context, id, op, data).await, UserOperation::BanUser => do_websocket_operation::(context, id, op, data).await, UserOperation::GetUserMentions => { do_websocket_operation::(context, id, op, data).await } UserOperation::MarkUserMentionAsRead => { do_websocket_operation::(context, id, op, data).await } UserOperation::MarkAllAsRead => { do_websocket_operation::(context, id, op, data).await } UserOperation::DeleteAccount => { do_websocket_operation::(context, id, op, data).await } UserOperation::PasswordReset => { do_websocket_operation::(context, id, op, data).await } UserOperation::PasswordChange => { do_websocket_operation::(context, id, op, data).await } UserOperation::UserJoin => do_websocket_operation::(context, id, op, data).await, UserOperation::PostJoin => do_websocket_operation::(context, id, op, data).await, UserOperation::CommunityJoin => { do_websocket_operation::(context, id, op, data).await } UserOperation::SaveUserSettings => { do_websocket_operation::(context, id, op, data).await } // Private Message ops UserOperation::CreatePrivateMessage => { do_websocket_operation::(context, id, op, data).await } UserOperation::EditPrivateMessage => { do_websocket_operation::(context, id, op, data).await } UserOperation::DeletePrivateMessage => { do_websocket_operation::(context, id, op, data).await } UserOperation::MarkPrivateMessageAsRead => { do_websocket_operation::(context, id, op, data).await } UserOperation::GetPrivateMessages => { do_websocket_operation::(context, id, op, data).await } // Site ops UserOperation::GetModlog => do_websocket_operation::(context, id, op, data).await, UserOperation::CreateSite => do_websocket_operation::(context, id, op, data).await, UserOperation::EditSite => do_websocket_operation::(context, id, op, data).await, UserOperation::GetSite => do_websocket_operation::(context, id, op, data).await, UserOperation::GetSiteConfig => { do_websocket_operation::(context, id, op, data).await } UserOperation::SaveSiteConfig => { do_websocket_operation::(context, id, op, data).await } UserOperation::Search => do_websocket_operation::(context, id, op, data).await, UserOperation::TransferCommunity => { do_websocket_operation::(context, id, op, data).await } UserOperation::TransferSite => { do_websocket_operation::(context, id, op, data).await } UserOperation::ListCategories => { do_websocket_operation::(context, id, op, data).await } // Community ops UserOperation::GetCommunity => { do_websocket_operation::(context, id, op, data).await } UserOperation::ListCommunities => { do_websocket_operation::(context, id, op, data).await } UserOperation::CreateCommunity => { do_websocket_operation::(context, id, op, data).await } UserOperation::EditCommunity => { do_websocket_operation::(context, id, op, data).await } UserOperation::DeleteCommunity => { do_websocket_operation::(context, id, op, data).await } UserOperation::RemoveCommunity => { do_websocket_operation::(context, id, op, data).await } UserOperation::FollowCommunity => { do_websocket_operation::(context, id, op, data).await } UserOperation::GetFollowedCommunities => { do_websocket_operation::(context, id, op, data).await } UserOperation::BanFromCommunity => { do_websocket_operation::(context, id, op, data).await } UserOperation::AddModToCommunity => { do_websocket_operation::(context, id, op, data).await } // Post ops UserOperation::CreatePost => do_websocket_operation::(context, id, op, data).await, UserOperation::GetPost => do_websocket_operation::(context, id, op, data).await, UserOperation::GetPosts => do_websocket_operation::(context, id, op, data).await, UserOperation::EditPost => do_websocket_operation::(context, id, op, data).await, UserOperation::DeletePost => do_websocket_operation::(context, id, op, data).await, UserOperation::RemovePost => do_websocket_operation::(context, id, op, data).await, UserOperation::LockPost => do_websocket_operation::(context, id, op, data).await, UserOperation::StickyPost => do_websocket_operation::(context, id, op, data).await, UserOperation::CreatePostLike => { do_websocket_operation::(context, id, op, data).await } UserOperation::SavePost => do_websocket_operation::(context, id, op, data).await, // Comment ops UserOperation::CreateComment => { do_websocket_operation::(context, id, op, data).await } UserOperation::EditComment => { do_websocket_operation::(context, id, op, data).await } UserOperation::DeleteComment => { do_websocket_operation::(context, id, op, data).await } UserOperation::RemoveComment => { do_websocket_operation::(context, id, op, data).await } UserOperation::MarkCommentAsRead => { do_websocket_operation::(context, id, op, data).await } UserOperation::SaveComment => { do_websocket_operation::(context, id, op, data).await } UserOperation::GetComments => { do_websocket_operation::(context, id, op, data).await } UserOperation::CreateCommentLike => { do_websocket_operation::(context, id, op, data).await } } } async fn do_websocket_operation<'a, 'b, Data>( context: LemmyContext, id: ConnectionId, op: UserOperation, data: &str, ) -> Result where for<'de> Data: Deserialize<'de> + 'a, Data: Perform, { let parsed_data: Data = serde_json::from_str(&data)?; let res = parsed_data .perform(&web::Data::new(context), Some(id)) .await?; serialize_websocket_message(&op, &res) } pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result { let mut built_text = String::new(); // Building proper speech text for espeak for mut c in captcha.chars() { let new_str = if c.is_alphabetic() { if c.is_lowercase() { c.make_ascii_uppercase(); format!("lower case {} ... ", c) } else { c.make_ascii_uppercase(); format!("capital {} ... ", c) } } else { format!("{} ...", c) }; built_text.push_str(&new_str); } espeak_wav_base64(&built_text) } pub(crate) fn espeak_wav_base64(text: &str) -> Result { // Make a temp file path let uuid = uuid::Uuid::new_v4().to_string(); let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid); // Write the wav file Command::new("espeak") .arg("-w") .arg(&file_path) .arg(text) .status()?; // Read the wav file bytes let bytes = std::fs::read(&file_path)?; // Delete the file std::fs::remove_file(file_path)?; // Convert to base64 let base64 = base64::encode(bytes); Ok(base64) } #[derive(Deserialize, Debug)] pub(crate) struct IframelyResponse { title: Option, description: Option, thumbnail_url: Option, html: Option, } pub(crate) async fn fetch_iframely( client: &Client, url: &str, ) -> Result { let fetch_url = format!("http://iframely/oembed?url={}", url); let response = retry(|| client.get(&fetch_url).send()).await?; let res: IframelyResponse = response .json() .await .map_err(|e| RecvError(e.to_string()))?; Ok(res) } #[derive(Deserialize, Debug, Clone)] pub(crate) struct PictrsResponse { files: Vec, msg: String, } #[derive(Deserialize, Debug, Clone)] pub(crate) struct PictrsFile { file: String, delete_token: String, } pub(crate) async fn fetch_pictrs( client: &Client, image_url: &str, ) -> Result { is_image_content_type(client, image_url).await?; let fetch_url = format!( "http://pictrs:8080/image/download?url={}", utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed ); let response = retry(|| client.get(&fetch_url).send()).await?; let response: PictrsResponse = response .json() .await .map_err(|e| RecvError(e.to_string()))?; if response.msg == "ok" { Ok(response) } else { Err(anyhow!("{}", &response.msg).into()) } } async fn fetch_iframely_and_pictrs_data( client: &Client, url: Option, ) -> ( Option, Option, Option, Option, ) { match &url { Some(url) => { // Fetch iframely data let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) = match fetch_iframely(client, url).await { Ok(res) => (res.title, res.description, res.thumbnail_url, res.html), Err(e) => { error!("iframely err: {}", e); (None, None, None, None) } }; // Fetch pictrs thumbnail let pictrs_hash = match iframely_thumbnail_url { Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await { Ok(res) => Some(res.files[0].file.to_owned()), Err(e) => { error!("pictrs err: {}", e); None } }, // Try to generate a small thumbnail if iframely is not supported None => match fetch_pictrs(client, &url).await { Ok(res) => Some(res.files[0].file.to_owned()), Err(e) => { error!("pictrs err: {}", e); None } }, }; // The full urls are necessary for federation let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash { Some(format!( "{}://{}/pictrs/image/{}", get_apub_protocol_string(), Settings::get().hostname, pictrs_hash )) } else { None }; ( iframely_title, iframely_description, iframely_html, pictrs_thumbnail, ) } None => (None, None, None, None), } } pub(crate) async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> { let response = retry(|| client.get(test).send()).await?; if response .headers() .get("Content-Type") .ok_or_else(|| anyhow!("No Content-Type header"))? .to_str()? .starts_with("image/") { Ok(()) } else { Err(anyhow!("Not an image type.").into()) } } #[cfg(test)] mod tests { use crate::{captcha_espeak_wav_base64, is_image_content_type}; #[test] fn test_image() { actix_rt::System::new("tset_image").block_on(async move { let client = reqwest::Client::default(); 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()); assert!(is_image_content_type(&client, "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20" ) .await.is_err() ); }); } #[test] fn test_espeak() { assert!(captcha_espeak_wav_base64("WxRt2l").is_ok()) } // These helped with testing // #[test] // fn test_iframely() { // let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await; // assert!(res.is_ok()); // } // #[test] // fn test_pictshare() { // let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg"); // assert!(res.is_ok()); // let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu"); // assert!(res_other.is_err()); // } }