X-Git-Url: http://these/git/?a=blobdiff_plain;f=crates%2Fapi%2Fsrc%2Flib.rs;h=9d3cf211c233ef5da1df63408310b18cb69ef840;hb=92568956353f21649ed9aff68b42699c9d036f30;hp=5642c4b9c7a31f0a9a744af2ac70e8a974a2062d;hpb=aba32917bd49176ab2897e88dae1de5b54cc2a39;p=lemmy.git diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 5642c4b9..9d3cf211 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,466 +1,144 @@ -use actix_web::{web, web::Data}; -use lemmy_db_queries::{ - source::{ - community::{CommunityModerator_, Community_}, - site::Site_, - user::UserSafeSettings_, - }, - Crud, - DbPool, +use actix_web::web::Data; +use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine}; +use captcha::Captcha; +use lemmy_api_common::{context::LemmyContext, utils::local_site_to_slur_regex}; +use lemmy_db_schema::source::local_site::LocalSite; +use lemmy_utils::{ + error::{LemmyError, LemmyErrorExt, LemmyErrorType}, + utils::slurs::check_slurs, }; -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_structs::{blocking, comment::*, community::*, post::*, site::*, user::*, websocket::*}; -use lemmy_utils::{claims::Claims, settings::Settings, ApiError, ConnectionId, LemmyError}; -use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; -use serde::Deserialize; -use std::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; +use std::io::Cursor; + +mod comment; +mod comment_report; +mod community; +mod local_user; +mod post; +mod post_report; +mod private_message; +mod private_message_report; +mod site; #[async_trait::async_trait(?Send)] pub trait Perform { - type Response: serde::ser::Serialize + Send; + type Response: serde::ser::Serialize + Send + Clone + Sync; - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result; + async fn perform(&self, context: &Data) -> 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(()) -} +/// Converts the captcha to a base64 encoded wav audio file +pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> Result { + let letters = captcha.as_wav(); -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()), + // Decode each wav file, concatenate the samples + let mut concat_samples: Vec = Vec::new(); + let mut any_header: Option = None; + for letter in letters { + let mut cursor = Cursor::new(letter.unwrap_or_default()); + let (header, samples) = wav::read(&mut cursor)?; + any_header = Some(header); + if let Some(samples16) = samples.as_sixteen() { + concat_samples.extend(samples16); + } else { + return Err(LemmyErrorType::CouldntCreateAudioCaptcha)?; + } } -} -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()), + // Encode the concatenated result as a wav file + let mut output_buffer = Cursor::new(vec![]); + let header = match any_header { + Some(header) => header, + None => return Err(LemmyErrorType::CouldntCreateAudioCaptcha)?, }; - 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) -} + wav::write( + header, + &wav::BitDepth::Sixteen(concat_samples), + &mut output_buffer, + ) + .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; -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), - } + Ok(base64.encode(output_buffer.into_inner())) } -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()); - } - 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), - } -} +/// Check size of report and remove whitespace +pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> { + let slur_regex = &local_site_to_slur_regex(local_site); -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(()) + check_slurs(reason, slur_regex)?; + if reason.is_empty() { + return Err(LemmyErrorType::ReportReasonRequired)?; } -} - -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()); - } + if reason.chars().count() > 1000 { + return Err(LemmyErrorType::ReportTooLong)?; } 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) fn check_optional_url(item: &Option>) -> Result<(), LemmyError> { - if let Some(Some(item)) = &item { - if Url::parse(item).is_err() { - return Err(ApiError::err("invalid_url").into()); - } - } - Ok(()) -} - -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>>()?; - - linked.extend_from_slice(&allowed); - linked.retain(|a| !blocked.contains(a) && !a.eq("") && !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!("/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) -} - #[cfg(test)] mod tests { - use crate::captcha_espeak_wav_base64; - - #[test] - fn test_espeak() { - assert!(captcha_espeak_wav_base64("WxRt2l").is_ok()) + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + + use lemmy_api_common::utils::check_validator_time; + use lemmy_db_schema::{ + source::{ + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + secret::Secret, + }, + traits::Crud, + utils::build_db_pool_for_tests, + }; + use lemmy_utils::{claims::Claims, settings::SETTINGS}; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn test_should_not_validate_user_token_after_password_change() { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + let secret = Secret::init(pool).await.unwrap(); + let settings = &SETTINGS.to_owned(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) + .await + .unwrap(); + + let new_person = PersonInsertForm::builder() + .name("Gerry9812".into()) + .public_key("pubkey".to_string()) + .instance_id(inserted_instance.id) + .build(); + + let inserted_person = Person::create(pool, &new_person).await.unwrap(); + + let local_user_form = LocalUserInsertForm::builder() + .person_id(inserted_person.id) + .password_encrypted("123456".to_string()) + .build(); + + let inserted_local_user = LocalUser::create(pool, &local_user_form).await.unwrap(); + + let jwt = Claims::jwt( + inserted_local_user.id.0, + &secret.jwt_secret, + &settings.hostname, + ) + .unwrap(); + let claims = Claims::decode(&jwt, &secret.jwt_secret).unwrap().claims; + let check = check_validator_time(&inserted_local_user.validator_time, &claims); + assert!(check.is_ok()); + + // The check should fail, since the validator time is now newer than the jwt issue time + let updated_local_user = + LocalUser::update_password(pool, inserted_local_user.id, "password111") + .await + .unwrap(); + let check_after = check_validator_time(&updated_local_user.validator_time, &claims); + assert!(check_after.is_err()); + + let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap(); + assert_eq!(1, num_deleted); } }