X-Git-Url: http://these/git/?a=blobdiff_plain;f=crates%2Fapi%2Fsrc%2Flib.rs;h=1b7b3215454ec197aca3ae37be6f8cac6f8a8c76;hb=70fae9d68d65b1e4d153e30d3c065cc315b75eaf;hp=d71f4b6eed088e66a8e86d0d07de8251e84f2dfd;hpb=9cb4dad4b401ea0d322acd680147f5c6c831d73e;p=lemmy.git diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index d71f4b6e..1b7b3215 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,505 +1,144 @@ -use actix_web::{web, web::Data}; -use lemmy_api_structs::{ - blocking, - comment::*, - community::*, - person::*, - post::*, - site::*, - websocket::*, -}; -use lemmy_db_queries::{ - source::{ - community::{CommunityModerator_, Community_}, - site::Site_, - }, - Crud, - DbPool, -}; -use lemmy_db_schema::source::{ - community::{Community, CommunityModerator}, - post::Post, - site::Site, -}; -use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView}; -use lemmy_db_views_actor::{ - community_person_ban_view::CommunityPersonBanView, - community_view::CommunityView, -}; +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::{ - claims::Claims, - settings::structs::Settings, - ApiError, - ConnectionId, - LemmyError, + error::{LemmyError, LemmyErrorExt, LemmyErrorType}, + utils::slurs::check_slurs, }; -use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; -use serde::Deserialize; -use std::process::Command; -use url::Url; +use std::io::Cursor; pub mod comment; +pub mod comment_report; pub mod community; pub mod local_user; pub mod post; -pub mod routes; +pub mod post_report; +pub mod private_message; +pub mod private_message_report; pub mod site; -pub mod websocket; #[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; -} - -pub(crate) async fn is_mod_or_admin( - pool: &DbPool, - person_id: i32, - community_id: i32, -) -> Result<(), LemmyError> { - let is_mod_or_admin = blocking(pool, move |conn| { - CommunityView::is_mod_or_admin(conn, person_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(ApiError::err("not_a_mod_or_admin").into()); - } - Ok(()) + async fn perform(&self, context: &Data) -> Result; } -// TODO this probably isn't necessary anymore -// pub async fn is_admin(pool: &DbPool, person_id: i32) -> Result<(), LemmyError> { -// let user = blocking(pool, move |conn| LocalUser::read(conn, person_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 fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> { - if !local_user_view.local_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()), + // 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 { + Err(LemmyErrorType::CouldntCreateAudioCaptcha)?; + } } -} -pub(crate) async fn get_local_user_view_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 person_id = claims.id; - let local_user_view = blocking(pool, move |conn| { - LocalUserView::read_person(conn, person_id) - }) - .await??; - // Check for a site ban - if local_user_view.person.banned { - return Err(ApiError::err("site_ban").into()); - } - Ok(local_user_view) -} + wav::write( + header, + &wav::BitDepth::Sixteen(concat_samples), + &mut output_buffer, + ) + .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; -pub(crate) async fn get_local_user_view_from_jwt_opt( - jwt: &Option, - pool: &DbPool, -) -> Result, LemmyError> { - match jwt { - Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)), - None => Ok(None), - } -} - -pub(crate) async fn get_local_user_settings_view_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 person_id = claims.id; - let local_user_view = blocking(pool, move |conn| { - LocalUserSettingsView::read(conn, person_id) - }) - .await??; - // Check for a site ban - if local_user_view.person.banned { - return Err(ApiError::err("site_ban").into()); - } - Ok(local_user_view) + Ok(base64.encode(output_buffer.into_inner())) } -pub(crate) async fn get_local_user_settings_view_from_jwt_opt( - jwt: &Option, - pool: &DbPool, -) -> Result, LemmyError> { - match jwt { - Some(jwt) => Ok(Some( - get_local_user_settings_view_from_jwt(jwt, pool).await?, - )), - None => Ok(None), - } -} +/// Check size of report +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( - person_id: i32, - community_id: i32, - pool: &DbPool, -) -> Result<(), LemmyError> { - let is_banned = - move |conn: &'_ _| CommunityPersonBanView::get(conn, person_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() { + 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 { + 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_person_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::GetPersonDetails => { - 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::BanPerson => do_websocket_operation::(context, id, op, data).await, - UserOperation::GetPersonMentions => { - do_websocket_operation::(context, id, op, data).await - } - UserOperation::MarkPersonMentionAsRead => { - 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) -} - -/// 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; - - #[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); } }