diff --git a/Cargo.lock b/Cargo.lock index 881efdcd0..1c687495e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1662,6 +1662,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" +[[package]] +name = "enum-map" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "988f0d17a0fa38291e5f41f71ea8d46a5d5497b9054d5a759fae2cbb819f2356" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4da76b3b6116d758c7ba93f7ec6a35d2e2cf24feda76c6e38a375f4d5c59f2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.103", +] + [[package]] name = "enum_delegate" version = "0.2.0" @@ -2764,6 +2784,7 @@ dependencies = [ "deser-hjson", "diesel", "doku", + "enum-map", "futures", "html2text", "http", diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index a0fd7bf18..c5d8999ec 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -42,7 +42,7 @@ impl Perform for BanFromCommunity { // Verify that only mods or admins can ban is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?; - is_valid_body_field(&data.reason)?; + is_valid_body_field(data.reason.as_deref())?; let community_user_ban_form = CommunityPersonBanForm { community_id: data.community_id, diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 452557d2c..f7dc4525e 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -30,7 +30,7 @@ impl Perform for BanPerson { // Make sure user is an admin is_admin(&local_user_view)?; - is_valid_body_field(&data.reason)?; + is_valid_body_field(data.reason.as_deref())?; let ban = data.ban; let banned_person_id = data.person_id; diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 8e5bcde21..e0e942c3e 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -45,11 +45,10 @@ impl PerformCrud for CreateComment { let local_user_view = local_user_view_from_jwt(&data.auth, context).await?; let local_site = LocalSite::read(context.pool()).await?; - let content_slurs_removed = remove_slurs( - &data.content.clone(), - &local_site_to_slur_regex(&local_site), - ); - is_valid_body_field(&Some(content_slurs_removed.clone()))?; + let content_slurs_removed = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site)); + is_valid_body_field(Some(&content_slurs_removed))?; + + let mentions = scrape_text_for_mentions(&content_slurs_removed); // Check for a community ban let post_id = data.post_id; @@ -96,7 +95,7 @@ impl PerformCrud for CreateComment { .await?; let comment_form = CommentInsertForm::builder() - .content(content_slurs_removed.clone()) + .content(content_slurs_removed.into_owned()) .post_id(data.post_id) .creator_id(local_user_view.person.id) .language_id(Some(language_id)) @@ -126,7 +125,6 @@ impl PerformCrud for CreateComment { .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_comment"))?; // Scan the comment for user mentions, add those rows - let mentions = scrape_text_for_mentions(&content_slurs_removed); let recipient_ids = send_local_notifs( mentions, &updated_comment, diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 86bdb52e2..730f6b3b8 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -64,11 +64,11 @@ impl PerformCrud for EditComment { .as_ref() .map(|c| remove_slurs(c, &local_site_to_slur_regex(&local_site))); - is_valid_body_field(&content_slurs_removed)?; + is_valid_body_field(content_slurs_removed.as_deref())?; let comment_id = data.comment_id; let form = CommentUpdateForm::builder() - .content(content_slurs_removed) + .content(content_slurs_removed.map(|s| s.into_owned())) .language_id(data.language_id) .updated(Some(Some(naive_now()))) .build(); diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index 850e9f2f5..ee05f6163 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -67,7 +67,7 @@ impl PerformCrud for CreateCommunity { check_slurs_opt(&data.description, &slur_regex)?; is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?; - is_valid_body_field(&data.description)?; + is_valid_body_field(data.description.as_deref())?; // Double check for duplicate community actor_ids let community_actor_id = generate_local_apub_endpoint( diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index f4e0c8c94..fed1393c2 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -38,7 +38,7 @@ impl PerformCrud for EditCommunity { let slur_regex = local_site_to_slur_regex(&local_site); check_slurs_opt(&data.title, &slur_regex)?; check_slurs_opt(&data.description, &slur_regex)?; - is_valid_body_field(&data.description)?; + is_valid_body_field(data.description.as_deref())?; // Verify its a mod (only mods can edit it) let community_id = data.community_id; diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index cd2cf1c3d..abc4b7305 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -57,7 +57,7 @@ impl PerformCrud for CreatePost { let url = data_url.map(clean_url_params).map(Into::into); // TODO no good way to handle a "clear" is_valid_post_title(&data.name)?; - is_valid_body_field(&data.body)?; + is_valid_body_field(data.body.as_deref())?; check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?; check_community_deleted_or_removed(data.community_id, context.pool()).await?; diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index af2c63c50..f340a5025 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -49,7 +49,7 @@ impl PerformCrud for EditPost { is_valid_post_title(name)?; } - is_valid_body_field(&data.body)?; + is_valid_body_field(data.body.as_deref())?; let post_id = data.post_id; let orig_post = Post::read(context.pool(), post_id).await?; diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 3f1d4ef89..b5557ff2e 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -39,16 +39,13 @@ impl PerformCrud for CreatePrivateMessage { let local_user_view = local_user_view_from_jwt(&data.auth, context).await?; let local_site = LocalSite::read(context.pool()).await?; - let content_slurs_removed = remove_slurs( - &data.content.clone(), - &local_site_to_slur_regex(&local_site), - ); - is_valid_body_field(&Some(content_slurs_removed.clone()))?; + let content_slurs_removed = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site)); + is_valid_body_field(Some(&content_slurs_removed))?; check_person_block(local_user_view.person.id, data.recipient_id, context.pool()).await?; let private_message_form = PrivateMessageInsertForm::builder() - .content(content_slurs_removed.clone()) + .content(content_slurs_removed.clone().into_owned()) .creator_id(local_user_view.person.id) .recipient_id(data.recipient_id) .build(); diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index cc3c377b8..d997f506b 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -41,14 +41,14 @@ impl PerformCrud for EditPrivateMessage { // Doing the update let content_slurs_removed = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site)); - is_valid_body_field(&Some(content_slurs_removed.clone()))?; + is_valid_body_field(Some(&content_slurs_removed))?; let private_message_id = data.private_message_id; PrivateMessage::update( context.pool(), private_message_id, &PrivateMessageUpdateForm::builder() - .content(Some(content_slurs_removed)) + .content(Some(content_slurs_removed.into_owned())) .updated(Some(Some(naive_now()))) .build(), ) diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index af8540669..09eb457ba 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -66,7 +66,7 @@ impl PerformCrud for CreateSite { site_description_length_check(desc)?; } - is_valid_body_field(&data.sidebar)?; + is_valid_body_field(data.sidebar.as_deref())?; let application_question = diesel_option_overwrite(&data.application_question); check_application_question( diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index b3e865759..82407939d 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -57,7 +57,7 @@ impl PerformCrud for EditSite { site_description_length_check(desc)?; } - is_valid_body_field(&data.sidebar)?; + is_valid_body_field(data.sidebar.as_deref())?; let application_question = diesel_option_overwrite(&data.application_question); check_application_question( diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index e2a03b8b3..3c4e1c9c3 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -167,7 +167,7 @@ impl Object for ApubComment { let form = CommentInsertForm { creator_id: creator.id, post_id: post.id, - content: content_slurs_removed, + content: content_slurs_removed.into_owned(), removed: None, published: note.published.map(|u| u.naive_local()), updated: note.updated.map(|u| u.naive_local()), diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index b255ffb9b..34ecef4fa 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -211,15 +211,14 @@ impl Object for ApubPost { let local_site = LocalSite::read(context.pool()).await.ok(); let slur_regex = &local_site_opt_to_slur_regex(&local_site); - let body_slurs_removed = - read_from_string_or_source_opt(&page.content, &page.media_type, &page.source) - .map(|s| remove_slurs(&s, slur_regex)); + let body = read_from_string_or_source_opt(&page.content, &page.media_type, &page.source); + let body_slurs_removed = body.as_deref().map(|s| remove_slurs(s, slur_regex)); let language_id = LanguageTag::to_language_id_single(page.language, context.pool()).await?; PostInsertForm { name, url: url.map(Into::into), - body: body_slurs_removed, + body: body_slurs_removed.map(|s| s.into_owned()), creator_id: creator.id, community_id: community.id, removed: None, diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 4e80c5df3..fcb6ba4c1 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -45,6 +45,7 @@ jsonwebtoken = "8.1.1" lettre = "0.10.1" comrak = { version = "0.14.0", default-features = false } totp-rs = { version = "4.2.0", features = ["gen_secret", "otpauth"] } +enum-map = "2.5" [dev-dependencies] reqwest = { workspace = true } diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index d40db5239..5351e4c82 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -1,6 +1,6 @@ use crate::IpAddr; +use enum_map::{enum_map, EnumMap}; use std::{collections::HashMap, time::Instant}; -use strum::IntoEnumIterator; use tracing::debug; #[derive(Debug, Clone)] @@ -9,7 +9,7 @@ struct RateLimitBucket { allowance: f64, } -#[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone, AsRefStr)] +#[derive(Eq, PartialEq, Hash, Debug, enum_map::Enum, Copy, Clone, AsRefStr)] pub(crate) enum RateLimitType { Message, Register, @@ -22,30 +22,10 @@ pub(crate) enum RateLimitType { /// Rate limiting based on rate type and IP addr #[derive(Debug, Clone, Default)] pub struct RateLimitStorage { - buckets: HashMap>, + buckets: HashMap>, } impl RateLimitStorage { - fn insert_ip(&mut self, ip: &IpAddr) { - for rate_limit_type in RateLimitType::iter() { - if self.buckets.get(&rate_limit_type).is_none() { - self.buckets.insert(rate_limit_type, HashMap::new()); - } - - if let Some(bucket) = self.buckets.get_mut(&rate_limit_type) { - if bucket.get(ip).is_none() { - bucket.insert( - ip.clone(), - RateLimitBucket { - last_checked: Instant::now(), - allowance: -2f64, - }, - ); - } - } - } - } - /// Rate limiting Algorithm described here: https://stackoverflow.com/a/668327/1655478 /// /// Returns true if the request passed the rate limit, false if it failed and should be rejected. @@ -57,40 +37,39 @@ impl RateLimitStorage { rate: i32, per: i32, ) -> bool { - self.insert_ip(ip); - if let Some(bucket) = self.buckets.get_mut(&type_) { - if let Some(rate_limit) = bucket.get_mut(ip) { - let current = Instant::now(); - let time_passed = current.duration_since(rate_limit.last_checked).as_secs() as f64; + let current = Instant::now(); + let ip_buckets = self.buckets.entry(ip.clone()).or_insert(enum_map! { + _ => RateLimitBucket { + last_checked: current, + allowance: -2f64, + }, + }); + #[allow(clippy::indexing_slicing)] // `EnumMap` has no `get` funciton + let rate_limit = &mut ip_buckets[type_]; + let time_passed = current.duration_since(rate_limit.last_checked).as_secs() as f64; - // The initial value - if rate_limit.allowance == -2f64 { - rate_limit.allowance = f64::from(rate); - }; + // The initial value + if rate_limit.allowance == -2f64 { + rate_limit.allowance = f64::from(rate); + }; - rate_limit.last_checked = current; - rate_limit.allowance += time_passed * (f64::from(rate) / f64::from(per)); - if rate_limit.allowance > f64::from(rate) { - rate_limit.allowance = f64::from(rate); - } + rate_limit.last_checked = current; + rate_limit.allowance += time_passed * (f64::from(rate) / f64::from(per)); + if rate_limit.allowance > f64::from(rate) { + rate_limit.allowance = f64::from(rate); + } - if rate_limit.allowance < 1.0 { - debug!( - "Rate limited type: {}, IP: {}, time_passed: {}, allowance: {}", - type_.as_ref(), - ip, - time_passed, - rate_limit.allowance - ); - false - } else { - rate_limit.allowance -= 1.0; - true - } - } else { - true - } + if rate_limit.allowance < 1.0 { + debug!( + "Rate limited type: {}, IP: {}, time_passed: {}, allowance: {}", + type_.as_ref(), + ip, + time_passed, + rate_limit.allowance + ); + false } else { + rate_limit.allowance -= 1.0; true } } diff --git a/crates/utils/src/utils/slurs.rs b/crates/utils/src/utils/slurs.rs index b92650ea3..6d17aa710 100644 --- a/crates/utils/src/utils/slurs.rs +++ b/crates/utils/src/utils/slurs.rs @@ -1,11 +1,12 @@ use crate::error::LemmyError; use regex::{Regex, RegexBuilder}; +use std::borrow::Cow; -pub fn remove_slurs(test: &str, slur_regex: &Option) -> String { +pub fn remove_slurs<'a>(test: &'a str, slur_regex: &Option) -> Cow<'a, str> { if let Some(slur_regex) = slur_regex { - slur_regex.replace_all(test, "*removed*").to_string() + slur_regex.replace_all(test, "*removed*") } else { - test.to_string() + Cow::Borrowed(test) } } diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index c4feb467b..9860b0b31 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -68,7 +68,7 @@ pub fn is_valid_post_title(title: &str) -> LemmyResult<()> { } /// This could be post bodies, comments, or any description field -pub fn is_valid_body_field(body: &Option) -> LemmyResult<()> { +pub fn is_valid_body_field(body: Option<&str>) -> LemmyResult<()> { if let Some(body) = body { let check = body.chars().count() <= BODY_MAX_LENGTH; if !check {