use actix_web::{web, web::Data}; use lemmy_api_structs::{ blocking, comment::*, community::*, post::*, site::*, user::*, websocket::*, }; use lemmy_db_queries::{ source::{ community::{CommunityModerator_, Community_}, site::Site_, user::UserSafeSettings_, }, Crud, DbPool, }; use lemmy_db_schema::source::{ community::{Community, CommunityModerator}, post::Post, site::Site, user::{UserSafeSettings, User_}, }; use lemmy_db_views_actor::{ community_user_ban_view::CommunityUserBanView, community_view::CommunityView, }; use lemmy_utils::{ claims::Claims, settings::structs::Settings, ApiError, ConnectionId, LemmyError, }; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; use serde::Deserialize; use std::{env, process::Command}; use url::Url; pub mod comment; pub mod community; pub mod post; pub mod routes; pub mod site; pub mod user; pub mod websocket; #[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(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| { CommunityView::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(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(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()); } // if user's token was issued before user's password reset. let user_validation_time = user.validator_time.timestamp_millis() / 1000; if user_validation_time > claims.iat { return Err(ApiError::err("not_logged_in").into()); } Ok(user) } pub(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(crate) async fn get_user_safe_settings_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| UserSafeSettings::read(conn, user_id)).await??; // Check for a site ban if user.banned { return Err(ApiError::err("site_ban").into()); } // if user's token was issued before user's password reset. let user_validation_time = user.validator_time.timestamp_millis() / 1000; if user_validation_time > claims.iat { return Err(ApiError::err("not_logged_in").into()); } Ok(user) } pub(crate) async fn get_user_safe_settings_from_jwt_opt( jwt: &Option, pool: &DbPool, ) -> Result, LemmyError> { match jwt { Some(jwt) => Ok(Some(get_user_safe_settings_from_jwt(jwt, pool).await?)), None => Ok(None), } } pub(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(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> { if score == -1 { let site = blocking(pool, move |conn| Site::read_simple(conn)).await??; if !site.enable_downvotes { return Err(ApiError::err("downvotes_disabled").into()); } } Ok(()) } /// Returns a list of communities that the user moderates /// or if a community_id is supplied validates the user is a moderator /// of that community and returns the community id in a vec /// /// * `user_id` - the user id of the moderator /// * `community_id` - optional community id to check for moderator privileges /// * `pool` - the diesel db pool pub(crate) async fn collect_moderated_communities( user_id: i32, community_id: Option, pool: &DbPool, ) -> Result, LemmyError> { if let Some(community_id) = community_id { // if the user provides a community_id, just check for mod/admin privileges is_mod_or_admin(pool, user_id, community_id).await?; Ok(vec![community_id]) } else { let ids = blocking(pool, move |conn: &'_ _| { CommunityModerator::get_user_moderated_communities(conn, user_id) }) .await??; Ok(ids) } } pub(crate) async fn build_federated_instances( pool: &DbPool, ) -> Result, LemmyError> { if Settings::get().federation().enabled { let distinct_communities = blocking(pool, move |conn| { Community::distinct_federated_communities(conn) }) .await??; let allowed = Settings::get().get_allowed_instances(); let blocked = Settings::get().get_blocked_instances(); let mut linked = distinct_communities .iter() .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string())) .collect::, LemmyError>>()?; if let Some(allowed) = allowed.as_ref() { linked.extend_from_slice(allowed); } if let Some(blocked) = blocked.as_ref() { linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname())); } // Sort and remove dupes linked.sort_unstable(); linked.dedup(); Ok(Some(FederatedInstances { linked, allowed, blocked, })) } else { Ok(None) } } 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::ModJoin => do_websocket_operation::(context, id, op, data).await, UserOperation::SaveUserSettings => { do_websocket_operation::(context, id, op, data).await } UserOperation::GetReportCount => { 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 } // 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, UserOperation::CreatePostReport => { do_websocket_operation::(context, id, op, data).await } UserOperation::ListPostReports => { do_websocket_operation::(context, id, op, data).await } UserOperation::ResolvePostReport => { 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 } UserOperation::CreateCommentReport => { do_websocket_operation::(context, id, op, data).await } UserOperation::ListCommentReports => { do_websocket_operation::(context, id, op, data).await } UserOperation::ResolveCommentReport => { 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!( "{}/lemmy_espeak_{}.wav", env::temp_dir().to_string_lossy(), &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) } /// Checks the password length pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> { if pass.len() > 60 { Err(ApiError::err("invalid_password").into()) } else { Ok(()) } } #[cfg(test)] mod tests { use crate::{captcha_espeak_wav_base64, get_user_from_jwt, get_user_safe_settings_from_jwt}; use lemmy_db_queries::{ establish_pooled_connection, source::user::User, Crud, ListingType, SortType, }; use lemmy_db_schema::source::user::{UserForm, User_}; use lemmy_utils::claims::Claims; use std::{ env::{current_dir, set_current_dir}, path::PathBuf, }; #[actix_rt::test] async fn test_should_not_validate_user_token_after_password_change() { struct CwdGuard(PathBuf); impl Drop for CwdGuard { fn drop(&mut self) { let _ = set_current_dir(&self.0); } } let _dir_bkp = CwdGuard(current_dir().unwrap()); // so configs could be read let _ = set_current_dir("../.."); let conn = establish_pooled_connection(); let new_user = UserForm { name: "user_df342sgf".into(), preferred_username: None, password_encrypted: "nope".into(), email: None, matrix_user_id: None, avatar: None, banner: None, admin: false, banned: Some(false), published: None, updated: None, show_nsfw: false, theme: "browser".into(), default_sort_type: SortType::Hot as i16, default_listing_type: ListingType::Subscribed as i16, lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, actor_id: None, bio: None, local: true, private_key: None, public_key: None, last_refreshed_at: None, inbox_url: None, shared_inbox_url: None, }; let inserted_user: User_ = User_::create(&conn.get().unwrap(), &new_user).unwrap(); let jwt_token = Claims::jwt(inserted_user.id, String::from("my-host.com")).unwrap(); get_user_from_jwt(&jwt_token, &conn) .await .expect("User should be decoded"); get_user_safe_settings_from_jwt(&jwt_token, &conn) .await .expect("User should be decoded"); std::thread::sleep(std::time::Duration::from_secs(1)); User_::update_password(&conn.get().unwrap(), inserted_user.id, &"password111").unwrap(); get_user_from_jwt(&jwt_token, &conn) .await .expect_err("JWT decode should fail after password change"); get_user_safe_settings_from_jwt(&jwt_token, &conn) .await .expect_err("JWT decode should fail after password change"); } #[test] fn test_espeak() { assert!(captcha_espeak_wav_base64("WxRt2l").is_ok()) } }