From 6d815db375a074b2b7dd9b01cb942bd7bdcaeeda Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 29 Feb 2024 15:12:45 +0100 Subject: [PATCH 1/2] Require verified email to reset password (#4482) --- crates/api/src/local_user/login.rs | 12 ++---------- crates/api/src/local_user/mod.rs | 15 +++++++++++++++ crates/api/src/local_user/reset_password.rs | 5 ++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index 956dcbba1..1fe337f3c 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -1,4 +1,4 @@ -use crate::check_totp_2fa_valid; +use crate::{check_totp_2fa_valid, local_user::check_email_verified}; use actix_web::{ web::{Data, Json}, HttpRequest, @@ -43,15 +43,7 @@ pub async fn login( Err(LemmyErrorType::IncorrectLogin)? } check_user_valid(&local_user_view.person)?; - - // Check if the user's email is verified if email verification is turned on - // However, skip checking verification if the user is an admin - if !local_user_view.local_user.admin - && site_view.local_site.require_email_verification - && !local_user_view.local_user.email_verified - { - Err(LemmyErrorType::EmailNotVerified)? - } + check_email_verified(&local_user_view, &site_view)?; check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool()) .await?; diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index 98e023fa5..8bf2e5327 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -1,3 +1,6 @@ +use lemmy_db_views::structs::{LocalUserView, SiteView}; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + pub mod add_admin; pub mod ban_person; pub mod block; @@ -16,3 +19,15 @@ pub mod save_settings; pub mod update_totp; pub mod validate_auth; pub mod verify_email; + +/// Check if the user's email is verified if email verification is turned on +/// However, skip checking verification if the user is an admin +fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> { + if !local_user_view.local_user.admin + && site_view.local_site.require_email_verification + && !local_user_view.local_user.email_verified + { + Err(LemmyErrorType::EmailNotVerified)? + } + Ok(()) +} diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs index 90aa910e0..414f506ba 100644 --- a/crates/api/src/local_user/reset_password.rs +++ b/crates/api/src/local_user/reset_password.rs @@ -1,3 +1,4 @@ +use crate::local_user::check_email_verified; use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, @@ -6,7 +7,7 @@ use lemmy_api_common::{ SuccessResponse, }; use lemmy_db_schema::source::password_reset_request::PasswordResetRequest; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] @@ -29,6 +30,8 @@ pub async fn reset_password( if recent_resets_count >= 3 { Err(LemmyErrorType::PasswordResetLimitReached)? } + let site_view = SiteView::read_local(&mut context.pool()).await?; + check_email_verified(&local_user_view, &site_view)?; // Email the pure token to the user. send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await?; From 87b577467be74637881e1a22eba39349232f0a77 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 29 Feb 2024 10:42:34 -0500 Subject: [PATCH 2/2] Adding ability to hide posts. (#4480) * Adding ability to hide posts. - Adds an post/hide API route. - Adds a `show_hidden` (default false) to `GetPosts`. - Adds a `hidden` field to `PostView`. - Removes the single `post_id` from MarkPostAsRead. - Fixes #1403 * Add a check to make sure hidden field is true. * Fixing test. * Add back semicolon --- crates/api/src/post/hide.rs | 34 ++++ crates/api/src/post/mark_read.rs | 9 +- crates/api/src/post/mod.rs | 1 + crates/api_common/src/post.rs | 15 +- crates/apub/src/api/list_posts.rs | 2 + crates/db_schema/src/impls/post.rs | 181 ++++++++++-------- crates/db_schema/src/schema.rs | 11 ++ crates/db_schema/src/source/post.rs | 24 ++- crates/db_views/src/post_view.rs | 71 ++++++- crates/db_views/src/structs.rs | 1 + crates/utils/src/error.rs | 1 + .../2024-02-28-144211_hide_posts/down.sql | 2 + .../2024-02-28-144211_hide_posts/up.sql | 7 + src/api_routes_http.rs | 2 + 14 files changed, 266 insertions(+), 95 deletions(-) create mode 100644 crates/api/src/post/hide.rs create mode 100644 migrations/2024-02-28-144211_hide_posts/down.sql create mode 100644 migrations/2024-02-28-144211_hide_posts/up.sql diff --git a/crates/api/src/post/hide.rs b/crates/api/src/post/hide.rs new file mode 100644 index 000000000..1adfa110d --- /dev/null +++ b/crates/api/src/post/hide.rs @@ -0,0 +1,34 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{context::LemmyContext, post::HidePost, SuccessResponse}; +use lemmy_db_schema::source::post::PostHide; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, MAX_API_PARAM_ELEMENTS}; +use std::collections::HashSet; + +#[tracing::instrument(skip(context))] +pub async fn hide_post( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + let post_ids = HashSet::from_iter(data.post_ids.clone()); + + if post_ids.len() > MAX_API_PARAM_ELEMENTS { + Err(LemmyErrorType::TooManyItems)?; + } + + let person_id = local_user_view.person.id; + + // Mark the post as hidden / unhidden + if data.hide { + PostHide::hide(&mut context.pool(), post_ids, person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntHidePost)?; + } else { + PostHide::unhide(&mut context.pool(), post_ids, person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntHidePost)?; + } + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/post/mark_read.rs b/crates/api/src/post/mark_read.rs index a46e949fa..bfc455f4f 100644 --- a/crates/api/src/post/mark_read.rs +++ b/crates/api/src/post/mark_read.rs @@ -11,14 +11,7 @@ pub async fn mark_post_as_read( context: Data, local_user_view: LocalUserView, ) -> Result, LemmyError> { - let mut post_ids = HashSet::new(); - if let Some(post_ids_) = &data.post_ids { - post_ids.extend(post_ids_.iter().cloned()); - } - - if let Some(post_id) = data.post_id { - post_ids.insert(post_id); - } + let post_ids = HashSet::from_iter(data.post_ids.clone()); if post_ids.len() > MAX_API_PARAM_ELEMENTS { Err(LemmyErrorType::TooManyItems)?; diff --git a/crates/api/src/post/mod.rs b/crates/api/src/post/mod.rs index 6a6ed9d21..7287010f7 100644 --- a/crates/api/src/post/mod.rs +++ b/crates/api/src/post/mod.rs @@ -1,5 +1,6 @@ pub mod feature; pub mod get_link_metadata; +pub mod hide; pub mod like; pub mod list_post_likes; pub mod lock; diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 1db07e451..69d1258e3 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -79,6 +79,7 @@ pub struct GetPosts { pub saved_only: Option, pub liked_only: Option, pub disliked_only: Option, + pub show_hidden: Option, pub page_cursor: Option, } @@ -148,12 +149,20 @@ pub struct RemovePost { #[cfg_attr(feature = "full", ts(export))] /// Mark a post as read. pub struct MarkPostAsRead { - /// TODO: deprecated, send `post_ids` instead - pub post_id: Option, - pub post_ids: Option>, + pub post_ids: Vec, pub read: bool, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Hide a post from list views +pub struct HidePost { + pub post_ids: Vec, + pub hide: bool, +} + #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 5285acaa7..b2ca95648 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -36,6 +36,7 @@ pub async fn list_posts( data.community_id }; let saved_only = data.saved_only.unwrap_or_default(); + let show_hidden = data.show_hidden.unwrap_or_default(); let liked_only = data.liked_only.unwrap_or_default(); let disliked_only = data.disliked_only.unwrap_or_default(); @@ -75,6 +76,7 @@ pub async fn list_posts( page, page_after, limit, + show_hidden, ..Default::default() } .list(&local_site.site, &mut context.pool()) diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index f49af6226..7e2eec22b 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -1,23 +1,10 @@ use crate::{ newtypes::{CommunityId, DbUrl, PersonId, PostId}, - schema::post::dsl::{ - ap_id, - body, - community_id, - creator_id, - deleted, - featured_community, - local, - name, - post, - published, - removed, - thumbnail_url, - updated, - url, - }, + schema::{post, post_hide, post_like, post_read, post_saved}, source::post::{ Post, + PostHide, + PostHideForm, PostInsertForm, PostLike, PostLikeForm, @@ -53,9 +40,9 @@ impl Crud for Post { async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post) + insert_into(post::table) .values(form) - .on_conflict(ap_id) + .on_conflict(post::ap_id) .do_update() .set(form) .get_result::(conn) @@ -68,7 +55,7 @@ impl Crud for Post { new_post: &Self::UpdateForm, ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(post.find(post_id)) + diesel::update(post::table.find(post_id)) .set(new_post) .get_result::(conn) .await @@ -81,12 +68,12 @@ impl Post { the_community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - post - .filter(community_id.eq(the_community_id)) - .filter(deleted.eq(false)) - .filter(removed.eq(false)) - .then_order_by(featured_community.desc()) - .then_order_by(published.desc()) + post::table + .filter(post::community_id.eq(the_community_id)) + .filter(post::deleted.eq(false)) + .filter(post::removed.eq(false)) + .then_order_by(post::featured_community.desc()) + .then_order_by(post::published.desc()) .limit(FETCH_LIMIT_MAX) .load::(conn) .await @@ -97,12 +84,12 @@ impl Post { the_community_id: CommunityId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - post - .filter(community_id.eq(the_community_id)) - .filter(deleted.eq(false)) - .filter(removed.eq(false)) - .filter(featured_community.eq(true)) - .then_order_by(published.desc()) + post::table + .filter(post::community_id.eq(the_community_id)) + .filter(post::deleted.eq(false)) + .filter(post::removed.eq(false)) + .filter(post::featured_community.eq(true)) + .then_order_by(post::published.desc()) .limit(FETCH_LIMIT_MAX) .load::(conn) .await @@ -112,13 +99,13 @@ impl Post { pool: &mut DbPool<'_>, ) -> Result)>, Error> { let conn = &mut get_conn(pool).await?; - post - .select((ap_id, coalesce(updated, published))) - .filter(local.eq(true)) - .filter(deleted.eq(false)) - .filter(removed.eq(false)) - .filter(published.ge(Utc::now().naive_utc() - Duration::days(SITEMAP_DAYS))) - .order(published.desc()) + post::table + .select((post::ap_id, coalesce(post::updated, post::published))) + .filter(post::local.eq(true)) + .filter(post::deleted.eq(false)) + .filter(post::removed.eq(false)) + .filter(post::published.ge(Utc::now().naive_utc() - Duration::days(SITEMAP_DAYS))) + .order(post::published.desc()) .limit(SITEMAP_LIMIT) .load::<(DbUrl, chrono::DateTime)>(conn) .await @@ -130,13 +117,13 @@ impl Post { ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - diesel::update(post.filter(creator_id.eq(for_creator_id))) + diesel::update(post::table.filter(post::creator_id.eq(for_creator_id))) .set(( - name.eq(DELETED_REPLACEMENT_TEXT), - url.eq(Option::<&str>::None), - body.eq(DELETED_REPLACEMENT_TEXT), - deleted.eq(true), - updated.eq(naive_now()), + post::name.eq(DELETED_REPLACEMENT_TEXT), + post::url.eq(Option::<&str>::None), + post::body.eq(DELETED_REPLACEMENT_TEXT), + post::deleted.eq(true), + post::updated.eq(naive_now()), )) .get_results::(conn) .await @@ -150,15 +137,15 @@ impl Post { ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - let mut update = diesel::update(post).into_boxed(); - update = update.filter(creator_id.eq(for_creator_id)); + let mut update = diesel::update(post::table).into_boxed(); + update = update.filter(post::creator_id.eq(for_creator_id)); if let Some(for_community_id) = for_community_id { - update = update.filter(community_id.eq(for_community_id)); + update = update.filter(post::community_id.eq(for_community_id)); } update - .set((removed.eq(new_removed), updated.eq(naive_now()))) + .set((post::removed.eq(new_removed), post::updated.eq(naive_now()))) .get_results::(conn) .await } @@ -174,8 +161,8 @@ impl Post { let conn = &mut get_conn(pool).await?; let object_id: DbUrl = object_id.into(); Ok( - post - .filter(ap_id.eq(object_id)) + post::table + .filter(post::ap_id.eq(object_id)) .first::(conn) .await .ok() @@ -190,9 +177,9 @@ impl Post { let conn = &mut get_conn(pool).await?; let pictrs_search = "%pictrs/image%"; - post - .filter(creator_id.eq(for_creator_id)) - .filter(url.like(pictrs_search)) + post::table + .filter(post::creator_id.eq(for_creator_id)) + .filter(post::url.like(pictrs_search)) .load::(conn) .await } @@ -206,13 +193,13 @@ impl Post { let pictrs_search = "%pictrs/image%"; diesel::update( - post - .filter(creator_id.eq(for_creator_id)) - .filter(url.like(pictrs_search)), + post::table + .filter(post::creator_id.eq(for_creator_id)) + .filter(post::url.like(pictrs_search)), ) .set(( - url.eq::>(None), - thumbnail_url.eq::>(None), + post::url.eq::>(None), + post::thumbnail_url.eq::>(None), )) .get_results::(conn) .await @@ -224,9 +211,9 @@ impl Post { ) -> Result, Error> { let conn = &mut get_conn(pool).await?; let pictrs_search = "%pictrs/image%"; - post - .filter(community_id.eq(for_community_id)) - .filter(url.like(pictrs_search)) + post::table + .filter(post::community_id.eq(for_community_id)) + .filter(post::url.like(pictrs_search)) .load::(conn) .await } @@ -240,13 +227,13 @@ impl Post { let pictrs_search = "%pictrs/image%"; diesel::update( - post - .filter(community_id.eq(for_community_id)) - .filter(url.like(pictrs_search)), + post::table + .filter(post::community_id.eq(for_community_id)) + .filter(post::url.like(pictrs_search)), ) .set(( - url.eq::>(None), - thumbnail_url.eq::>(None), + post::url.eq::>(None), + post::thumbnail_url.eq::>(None), )) .get_results::(conn) .await @@ -258,11 +245,10 @@ impl Likeable for PostLike { type Form = PostLikeForm; type IdType = PostId; async fn like(pool: &mut DbPool<'_>, post_like_form: &PostLikeForm) -> Result { - use crate::schema::post_like::dsl::{person_id, post_id, post_like}; let conn = &mut get_conn(pool).await?; - insert_into(post_like) + insert_into(post_like::table) .values(post_like_form) - .on_conflict((post_id, person_id)) + .on_conflict((post_like::post_id, post_like::person_id)) .do_update() .set(post_like_form) .get_result::(conn) @@ -273,9 +259,8 @@ impl Likeable for PostLike { person_id: PersonId, post_id: PostId, ) -> Result { - use crate::schema::post_like::dsl; let conn = &mut get_conn(pool).await?; - diesel::delete(dsl::post_like.find((person_id, post_id))) + diesel::delete(post_like::table.find((person_id, post_id))) .execute(conn) .await } @@ -285,20 +270,18 @@ impl Likeable for PostLike { impl Saveable for PostSaved { type Form = PostSavedForm; async fn save(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { - use crate::schema::post_saved::dsl::{person_id, post_id, post_saved}; let conn = &mut get_conn(pool).await?; - insert_into(post_saved) + insert_into(post_saved::table) .values(post_saved_form) - .on_conflict((post_id, person_id)) + .on_conflict((post_saved::post_id, post_saved::person_id)) .do_update() .set(post_saved_form) .get_result::(conn) .await } async fn unsave(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result { - use crate::schema::post_saved::dsl::post_saved; let conn = &mut get_conn(pool).await?; - diesel::delete(post_saved.find((post_saved_form.person_id, post_saved_form.post_id))) + diesel::delete(post_saved::table.find((post_saved_form.person_id, post_saved_form.post_id))) .execute(conn) .await } @@ -310,14 +293,13 @@ impl PostRead { post_ids: HashSet, person_id: PersonId, ) -> Result { - use crate::schema::post_read::dsl::post_read; let conn = &mut get_conn(pool).await?; let forms = post_ids .into_iter() .map(|post_id| PostReadForm { post_id, person_id }) .collect::>(); - insert_into(post_read) + insert_into(post_read::table) .values(forms) .on_conflict_do_nothing() .execute(conn) @@ -329,13 +311,48 @@ impl PostRead { post_id_: HashSet, person_id_: PersonId, ) -> Result { - use crate::schema::post_read::dsl::{person_id, post_id, post_read}; let conn = &mut get_conn(pool).await?; diesel::delete( - post_read - .filter(post_id.eq_any(post_id_)) - .filter(person_id.eq(person_id_)), + post_read::table + .filter(post_read::post_id.eq_any(post_id_)) + .filter(post_read::person_id.eq(person_id_)), + ) + .execute(conn) + .await + } +} + +impl PostHide { + pub async fn hide( + pool: &mut DbPool<'_>, + post_ids: HashSet, + person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + let forms = post_ids + .into_iter() + .map(|post_id| PostHideForm { post_id, person_id }) + .collect::>(); + insert_into(post_hide::table) + .values(forms) + .on_conflict_do_nothing() + .execute(conn) + .await + } + + pub async fn unhide( + pool: &mut DbPool<'_>, + post_id_: HashSet, + person_id_: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + diesel::delete( + post_hide::table + .filter(post_hide::post_id.eq_any(post_id_)) + .filter(post_hide::person_id.eq(person_id_)), ) .execute(conn) .await diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 19f9183a5..bdde25566 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -730,6 +730,14 @@ diesel::table! { } } +diesel::table! { + post_hide (person_id, post_id) { + post_id -> Int4, + person_id -> Int4, + published -> Timestamptz, + } +} + diesel::table! { post_like (person_id, post_id) { post_id -> Int4, @@ -983,6 +991,8 @@ diesel::joinable!(post_aggregates -> community (community_id)); diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); +diesel::joinable!(post_hide -> person (person_id)); +diesel::joinable!(post_hide -> post (post_id)); diesel::joinable!(post_like -> person (person_id)); diesel::joinable!(post_like -> post (post_id)); diesel::joinable!(post_read -> person (person_id)); @@ -1054,6 +1064,7 @@ diesel::allow_tables_to_appear_in_same_query!( person_post_aggregates, post, post_aggregates, + post_hide, post_like, post_read, post_report, diff --git a/crates/db_schema/src/source/post.rs b/crates/db_schema/src/source/post.rs index 4ac3e2a65..115c90eef 100644 --- a/crates/db_schema/src/source/post.rs +++ b/crates/db_schema/src/source/post.rs @@ -1,6 +1,6 @@ use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId}; #[cfg(feature = "full")] -use crate::schema::{post, post_like, post_read, post_saved}; +use crate::schema::{post, post_hide, post_like, post_read, post_saved}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -182,3 +182,25 @@ pub(crate) struct PostReadForm { pub post_id: PostId, pub person_id: PersonId, } + +#[derive(PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, Associations) +)] +#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] +#[cfg_attr(feature = "full", diesel(table_name = post_hide))] +#[cfg_attr(feature = "full", diesel(primary_key(post_id, person_id)))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct PostHide { + pub post_id: PostId, + pub person_id: PersonId, + pub published: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = post_hide))] +pub(crate) struct PostHideForm { + pub post_id: PostId, + pub person_id: PersonId, +} diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 6e15d1678..04e3e4d3c 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -35,6 +35,7 @@ use lemmy_db_schema::{ person_post_aggregates, post, post_aggregates, + post_hide, post_like, post_read, post_saved, @@ -107,6 +108,16 @@ fn queries<'a>() -> Queries< ) }; + let is_hidden = |person_id| { + exists( + post_hide::table.filter( + post_aggregates::post_id + .eq(post_hide::post_id) + .and(post_hide::person_id.eq(person_id)), + ), + ) + }; + let is_creator_blocked = |person_id| { exists( person_block::table.filter( @@ -147,6 +158,13 @@ fn queries<'a>() -> Queries< Box::new(false.into_sql::()) }; + let is_hidden_selection: Box> = + if let Some(person_id) = my_person_id { + Box::new(is_hidden(person_id)) + } else { + Box::new(false.into_sql::()) + }; + let is_creator_blocked_selection: Box> = if let Some(person_id) = my_person_id { Box::new(is_creator_blocked(person_id)) @@ -211,6 +229,7 @@ fn queries<'a>() -> Queries< subscribed_type_selection, is_saved_selection, is_read_selection, + is_hidden_selection, is_creator_blocked_selection, score_selection, coalesce( @@ -406,6 +425,13 @@ fn queries<'a>() -> Queries< } } + if !options.show_hidden { + // If a creator id isn't given (IE its on home or community pages), hide the hidden posts + if let (None, Some(person_id)) = (options.creator_id, my_person_id) { + query = query.filter(not(is_hidden(person_id))); + } + } + if let Some(person_id) = my_person_id { if options.liked_only { query = query.filter(score(person_id).eq(1)); @@ -593,6 +619,7 @@ pub struct PostQuery<'a> { pub page_after: Option, pub page_before_or_equal: Option, pub page_back: bool, + pub show_hidden: bool, } impl<'a> PostQuery<'a> { @@ -726,7 +753,7 @@ mod tests { local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, person::{Person, PersonInsertForm}, person_block::{PersonBlock, PersonBlockForm}, - post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm}, + post::{Post, PostHide, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm}, site::Site, }, traits::{Blockable, Crud, Joinable, Likeable}, @@ -1463,6 +1490,47 @@ mod tests { cleanup(data, pool).await } + #[tokio::test] + #[serial] + async fn post_listings_hide_hidden() -> LemmyResult<()> { + let pool = &build_db_pool().await?; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Mark a post as hidden + PostHide::hide( + pool, + HashSet::from([data.inserted_bot_post.id]), + data.local_user_view.person.id, + ) + .await?; + + // Make sure you don't see the hidden post in the results + let post_listings_hide_hidden = data.default_post_query().list(&data.site, pool).await?; + assert_eq!(vec![POST], names(&post_listings_hide_hidden)); + + // Make sure it does come back with the show_hidden option + let post_listings_show_hidden = PostQuery { + sort: Some(SortType::New), + local_user: Some(&data.local_user_view), + show_hidden: true, + ..Default::default() + } + .list(&data.site, pool) + .await?; + assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_hidden)); + + // Make sure that hidden field is true. + assert!( + &post_listings_show_hidden + .first() + .expect("first post should exist") + .hidden + ); + + cleanup(data, pool).await + } + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { let num_deleted = Post::delete(pool, data.inserted_post.id).await?; Community::delete(pool, data.inserted_community.id).await?; @@ -1584,6 +1652,7 @@ mod tests { }, subscribed: SubscribedType::NotSubscribed, read: false, + hidden: false, saved: false, creator_blocked: false, }) diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index e05d33242..9b2d8d602 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -120,6 +120,7 @@ pub struct PostView { pub subscribed: SubscribedType, pub saved: bool, pub read: bool, + pub hidden: bool, pub creator_blocked: bool, pub my_vote: Option, pub unread_comments: i64, diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index ad3f4371f..31f0707be 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -122,6 +122,7 @@ pub enum LemmyErrorType { CouldntLikePost, CouldntSavePost, CouldntMarkPostAsRead, + CouldntHidePost, CouldntUpdateCommunity, CouldntUpdateReplies, CouldntUpdatePersonMentions, diff --git a/migrations/2024-02-28-144211_hide_posts/down.sql b/migrations/2024-02-28-144211_hide_posts/down.sql new file mode 100644 index 000000000..72729838c --- /dev/null +++ b/migrations/2024-02-28-144211_hide_posts/down.sql @@ -0,0 +1,2 @@ +DROP TABLE post_hide; + diff --git a/migrations/2024-02-28-144211_hide_posts/up.sql b/migrations/2024-02-28-144211_hide_posts/up.sql new file mode 100644 index 000000000..922dddd66 --- /dev/null +++ b/migrations/2024-02-28-144211_hide_posts/up.sql @@ -0,0 +1,7 @@ +CREATE TABLE post_hide ( + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + person_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + published timestamp with time zone NOT NULL DEFAULT now(), + PRIMARY KEY (person_id, post_id) +); + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 912dcfbf9..966862fa5 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -49,6 +49,7 @@ use lemmy_api::{ post::{ feature::feature_post, get_link_metadata::get_link_metadata, + hide::hide_post, like::like_post, list_post_likes::list_post_likes, lock::lock_post, @@ -206,6 +207,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/delete", web::post().to(delete_post)) .route("/remove", web::post().to(remove_post)) .route("/mark_as_read", web::post().to(mark_post_as_read)) + .route("/hide", web::post().to(hide_post)) .route("/lock", web::post().to(lock_post)) .route("/feature", web::post().to(feature_post)) .route("/list", web::get().to(list_posts))