diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 21b07420a..66dcea19b 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -18,7 +18,7 @@ doctest = false workspace = true [dependencies] -lemmy_utils = { workspace = true, features = ["default"] } +lemmy_utils = { workspace = true } lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] } lemmy_db_views_moderator = { workspace = true, features = ["full"] } 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?; 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/Cargo.toml b/crates/api_common/Cargo.toml index 9d144ddb4..0192099f3 100644 --- a/crates/api_common/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -23,7 +23,7 @@ full = [ "lemmy_db_views/full", "lemmy_db_views_actor/full", "lemmy_db_views_moderator/full", - "lemmy_utils/default", + "lemmy_utils/full", "activitypub_federation", "encoding", "reqwest-middleware", @@ -44,7 +44,7 @@ lemmy_db_views = { workspace = true } lemmy_db_views_moderator = { workspace = true } lemmy_db_views_actor = { workspace = true } lemmy_db_schema = { workspace = true } -lemmy_utils = { workspace = true, features = ["error-type"] } +lemmy_utils = { workspace = true } activitypub_federation = { workspace = true, optional = true } serde = { workspace = true } serde_with = { workspace = true } 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/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml index 2211d84ad..af50c5648 100644 --- a/crates/api_crud/Cargo.toml +++ b/crates/api_crud/Cargo.toml @@ -13,7 +13,7 @@ repository.workspace = true workspace = true [dependencies] -lemmy_utils = { workspace = true, features = ["default"] } +lemmy_utils = { workspace = true, features = ["full"] } lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] } lemmy_db_views_actor = { workspace = true, features = ["full"] } diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index 33778a1b0..4c3189a09 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -18,7 +18,7 @@ doctest = false workspace = true [dependencies] -lemmy_utils = { workspace = true, features = ["default"] } +lemmy_utils = { workspace = true, features = ["full"] } lemmy_db_schema = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true, features = ["full"] } lemmy_db_views_actor = { workspace = true, features = ["full"] } 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_perf/Cargo.toml b/crates/db_perf/Cargo.toml index f1e8faba3..ebadde00b 100644 --- a/crates/db_perf/Cargo.toml +++ b/crates/db_perf/Cargo.toml @@ -19,6 +19,6 @@ diesel = { workspace = true } diesel-async = { workspace = true } lemmy_db_schema = { workspace = true } lemmy_db_views = { workspace = true, features = ["full"] } -lemmy_utils = { workspace = true, features = ["default"] } +lemmy_utils = { workspace = true, features = ["full"] } tokio = { workspace = true } url = { workspace = true } diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml index 6d227ad40..d0d66d69f 100644 --- a/crates/db_schema/Cargo.toml +++ b/crates/db_schema/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [features] full = [ + "lemmy_utils/full", "diesel", "diesel-derive-newtype", "diesel-derive-enum", @@ -48,7 +49,7 @@ strum = { workspace = true } strum_macros = { workspace = true } serde_json = { workspace = true, optional = true } activitypub_federation = { workspace = true, optional = true } -lemmy_utils = { workspace = true, optional = true, features = ["default"] } +lemmy_utils = { workspace = true, optional = true } bcrypt = { workspace = true, optional = true } diesel = { workspace = true, features = [ "postgres", 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 f0756ab7d..271367528 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -736,6 +736,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, @@ -989,6 +997,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)); @@ -1060,6 +1070,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/Cargo.toml b/crates/db_views/Cargo.toml index e70030b64..cdd44869c 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -29,7 +29,7 @@ full = [ [dependencies] lemmy_db_schema = { workspace = true } -lemmy_utils = { workspace = true, optional = true, features = ["default"] } +lemmy_utils = { workspace = true, optional = true } diesel = { workspace = true, optional = true } diesel-async = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true } 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/routes/Cargo.toml b/crates/routes/Cargo.toml index 324894ab9..1403f92e7 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -16,7 +16,7 @@ doctest = false workspace = true [dependencies] -lemmy_utils = { workspace = true, features = ["default"] } +lemmy_utils = { workspace = true, features = ["full"] } lemmy_db_views = { workspace = true } lemmy_db_views_actor = { workspace = true } lemmy_db_schema = { workspace = true } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 326cbf2e4..2a832bec0 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -16,23 +16,24 @@ doctest = false [[bin]] name = "lemmy_util_bin" path = "src/main.rs" -required-features = ["default"] +required-features = ["full"] [lints] workspace = true [features] -default = [ - "error-type", - "dep:serde_json", - "dep:anyhow", - "dep:tracing-error", +full = [ + "dep:ts-rs", "dep:diesel", - "dep:http", + "dep:rosetta-i18n", "dep:actix-web", "dep:reqwest-middleware", "dep:tracing", "dep:actix-web", + "dep:serde_json", + "dep:anyhow", + "dep:tracing-error", + "dep:http", "dep:deser-hjson", "dep:regex", "dep:urlencoding", @@ -47,27 +48,23 @@ default = [ "dep:html2text", "dep:lettre", "dep:uuid", - "dep:rosetta-i18n", "dep:itertools", "dep:markdown-it", - ] -full = ["default", "dep:ts-rs"] -error-type = ["dep:serde", "dep:strum"] [dependencies] regex = { workspace = true, optional = true } tracing = { workspace = true, optional = true } tracing-error = { workspace = true, optional = true } itertools = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } serde_json = { workspace = true, optional = true } once_cell = { workspace = true, optional = true } url = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } reqwest-middleware = { workspace = true, optional = true } -strum = { workspace = true, optional = true } +strum = { workspace = true } strum_macros = { workspace = true } futures = { workspace = true, optional = true } diesel = { workspace = true, features = ["chrono"], optional = true } diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 0f68d70da..31f0707be 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -2,12 +2,10 @@ use cfg_if::cfg_if; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use strum_macros::{Display, EnumIter}; -#[cfg(feature = "ts-rs")] -use ts_rs::TS; #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)] -#[cfg_attr(feature = "ts-rs", derive(TS))] -#[cfg_attr(feature = "ts-rs", ts(export))] +#[cfg_attr(feature = "full", derive(ts_rs::TS))] +#[cfg_attr(feature = "full", ts(export))] #[serde(tag = "error", content = "message", rename_all = "snake_case")] #[non_exhaustive] // TODO: order these based on the crate they belong to (utils, federation, db, api) @@ -124,6 +122,7 @@ pub enum LemmyErrorType { CouldntLikePost, CouldntSavePost, CouldntMarkPostAsRead, + CouldntHidePost, CouldntUpdateCommunity, CouldntUpdateReplies, CouldntUpdatePersonMentions, @@ -168,7 +167,7 @@ pub enum LemmyErrorType { } cfg_if! { - if #[cfg(feature = "default")] { + if #[cfg(feature = "full")] { use tracing_error::SpanTrace; use std::fmt; @@ -276,52 +275,52 @@ cfg_if! { self.map_err(|e| e.inner) } } - } -} -#[cfg(test)] -mod tests { - #![allow(clippy::unwrap_used)] - #![allow(clippy::indexing_slicing)] - use super::*; - use actix_web::{body::MessageBody, ResponseError}; - use pretty_assertions::assert_eq; - use std::fs::read_to_string; - use strum::IntoEnumIterator; + #[cfg(test)] + mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + use super::*; + use actix_web::{body::MessageBody, ResponseError}; + use pretty_assertions::assert_eq; + use std::fs::read_to_string; + use strum::IntoEnumIterator; - #[test] - fn deserializes_no_message() { - let err = LemmyError::from(LemmyErrorType::Banned).error_response(); - let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); - assert_eq!(&json, "{\"error\":\"banned\"}") - } + #[test] + fn deserializes_no_message() { + let err = LemmyError::from(LemmyErrorType::Banned).error_response(); + let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); + assert_eq!(&json, "{\"error\":\"banned\"}") + } - #[test] - fn deserializes_with_message() { - let reg_banned = LemmyErrorType::PersonIsBannedFromSite(String::from("reason")); - let err = LemmyError::from(reg_banned).error_response(); - let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); - assert_eq!( - &json, - "{\"error\":\"person_is_banned_from_site\",\"message\":\"reason\"}" - ) - } + #[test] + fn deserializes_with_message() { + let reg_banned = LemmyErrorType::PersonIsBannedFromSite(String::from("reason")); + let err = LemmyError::from(reg_banned).error_response(); + let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap(); + assert_eq!( + &json, + "{\"error\":\"person_is_banned_from_site\",\"message\":\"reason\"}" + ) + } - /// Check if errors match translations. Disabled because many are not translated at all. - #[test] - #[ignore] - fn test_translations_match() { - #[derive(Deserialize)] - struct Err { - error: String, + /// Check if errors match translations. Disabled because many are not translated at all. + #[test] + #[ignore] + fn test_translations_match() { + #[derive(Deserialize)] + struct Err { + error: String, + } + + let translations = read_to_string("translations/translations/en.json").unwrap(); + LemmyErrorType::iter().for_each(|e| { + let msg = serde_json::to_string(&e).unwrap(); + let msg: Err = serde_json::from_str(&msg).unwrap(); + let msg = msg.error; + assert!(translations.contains(&format!("\"{msg}\"")), "{msg}"); + }); + } } - - let translations = read_to_string("translations/translations/en.json").unwrap(); - LemmyErrorType::iter().for_each(|e| { - let msg = serde_json::to_string(&e).unwrap(); - let msg: Err = serde_json::from_str(&msg).unwrap(); - let msg = msg.error; - assert!(translations.contains(&format!("\"{msg}\"")), "{msg}"); - }); } } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 65dbaaa45..c2760d9d9 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,28 +1,21 @@ use cfg_if::cfg_if; cfg_if! { - if #[cfg(feature = "default")] { + if #[cfg(feature = "full")] { pub mod apub; pub mod cache_header; pub mod email; - pub mod error; pub mod rate_limit; pub mod request; pub mod response; pub mod settings; pub mod utils; pub mod version; - } else { - mod error; - } -} - -cfg_if! { - if #[cfg(feature = "error-type")] { - pub use error::LemmyErrorType; } } +pub mod error; +pub use error::LemmyErrorType; use std::time::Duration; pub type ConnectionId = usize; @@ -41,7 +34,7 @@ macro_rules! location_info { }; } -#[cfg(feature = "default")] +#[cfg(feature = "full")] /// tokio::spawn, but accepts a future that may fail and also /// * logs errors /// * attaches the spawned task to the tracing span of the caller for better logging diff --git a/crates/utils/src/main.rs b/crates/utils/src/main.rs index c2365f233..ed658b097 100644 --- a/crates/utils/src/main.rs +++ b/crates/utils/src/main.rs @@ -1,16 +1,24 @@ -use doku::json::{AutoComments, CommentsStyle, Formatting, ObjectsStyle}; -use lemmy_utils::settings::structs::Settings; +use cfg_if::cfg_if; + fn main() { - let fmt = Formatting { - auto_comments: AutoComments::none(), - comments_style: CommentsStyle { - separator: "#".to_owned(), - }, - objects_style: ObjectsStyle { - surround_keys_with_quotes: false, - use_comma_as_separator: false, - }, - ..Default::default() - }; - println!("{}", doku::to_json_fmt_val(&fmt, &Settings::default())); + cfg_if! { + if #[cfg(feature = "full")] { + use doku::json::{AutoComments, CommentsStyle, Formatting, ObjectsStyle}; + use lemmy_utils::settings::structs::Settings; + let fmt = Formatting { + auto_comments: AutoComments::none(), + comments_style: CommentsStyle { + separator: "#".to_owned(), + }, + objects_style: ObjectsStyle { + surround_keys_with_quotes: false, + use_comma_as_separator: false, + }, + ..Default::default() + }; + println!("{}", doku::to_json_fmt_val(&fmt, &Settings::default())); + } else { + + } + } } 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/scripts/update_config_defaults.sh b/scripts/update_config_defaults.sh index 0984c247c..de7fa6479 100755 --- a/scripts/update_config_defaults.sh +++ b/scripts/update_config_defaults.sh @@ -3,4 +3,4 @@ set -e dest=${1-config/defaults.hjson} -cargo run --manifest-path crates/utils/Cargo.toml > "$dest" +cargo run --manifest-path crates/utils/Cargo.toml --features full > "$dest" 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))