From 39b3ec2473c048f3a08e205826864864a39b40d4 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 26 Nov 2024 09:27:05 -0500 Subject: [PATCH 01/43] Combined tables try 2 --- crates/db_schema/src/newtypes.rs | 6 + crates/db_schema/src/schema.rs | 12 + crates/db_schema/src/source/combined/mod.rs | 1 + .../db_schema/src/source/combined/report.rs | 22 ++ crates/db_schema/src/source/mod.rs | 1 + crates/db_views/src/lib.rs | 2 + crates/db_views/src/post_report_view.rs | 8 +- crates/db_views/src/report_combined_view.rs | 238 ++++++++++++++++++ crates/db_views/src/structs.rs | 53 ++++ .../down.sql | 1 + .../up.sql | 14 ++ 11 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 crates/db_schema/src/source/combined/mod.rs create mode 100644 crates/db_schema/src/source/combined/report.rs create mode 100644 crates/db_views/src/report_combined_view.rs create mode 100644 migrations/2024-11-26-115042_add_combined_tables/down.sql create mode 100644 migrations/2024-11-26-115042_add_combined_tables/up.sql diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c28be8222d..fe84802d76 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -179,6 +179,12 @@ pub struct LtreeDef(pub String); #[cfg_attr(feature = "full", ts(export))] pub struct DbUrl(pub(crate) Box); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The report combined id +pub struct ReportCombinedId(i32); + impl DbUrl { pub fn inner(&self) -> &Url { &self.0 diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index f2b186d35d..1d3177b155 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -856,6 +856,15 @@ diesel::table! { } } +diesel::table! { + report_combined (id) { + id -> Int4, + published -> Timestamptz, + post_report_id -> Nullable, + comment_report_id -> Nullable, + } +} + diesel::table! { secret (id) { id -> Int4, @@ -1006,6 +1015,8 @@ diesel::joinable!(post_report -> post (post_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); +diesel::joinable!(report_combined -> comment_report (comment_report_id)); +diesel::joinable!(report_combined -> post_report (post_report_id)); diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); @@ -1072,6 +1083,7 @@ diesel::allow_tables_to_appear_in_same_query!( received_activity, registration_application, remote_image, + report_combined, secret, sent_activity, site, diff --git a/crates/db_schema/src/source/combined/mod.rs b/crates/db_schema/src/source/combined/mod.rs new file mode 100644 index 0000000000..7352eef8e0 --- /dev/null +++ b/crates/db_schema/src/source/combined/mod.rs @@ -0,0 +1 @@ +pub mod report; diff --git a/crates/db_schema/src/source/combined/report.rs b/crates/db_schema/src/source/combined/report.rs new file mode 100644 index 0000000000..7c55329e74 --- /dev/null +++ b/crates/db_schema/src/source/combined/report.rs @@ -0,0 +1,22 @@ +use crate::newtypes::{CommentReportId, PostReportId, ReportCombinedId}; +#[cfg(feature = "full")] +use crate::schema::report_combined; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "full", derive(Identifiable, Queryable, Selectable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = report_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A combined reports table. +pub struct ReportCombined { + pub id: ReportCombinedId, + pub published: DateTime, + pub post_report_id: Option, + pub comment_report_id: Option, +} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 5082ddbd19..17252c6035 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -5,6 +5,7 @@ use url::Url; pub mod activity; pub mod actor_language; pub mod captcha_answer; +pub mod combined; pub mod comment; pub mod comment_reply; pub mod comment_report; diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index e93c7409d1..e93741be8c 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -22,6 +22,8 @@ pub mod private_message_view; #[cfg(feature = "full")] pub mod registration_application_view; #[cfg(feature = "full")] +pub mod report_combined_view; +#[cfg(feature = "full")] pub mod site_view; pub mod structs; #[cfg(feature = "full")] diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index 9429c258f8..c530c97398 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -136,15 +136,15 @@ fn queries<'a>() -> Queries< query = query.order_by(post_report::published.desc()); } - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query.limit(limit).offset(offset); - // If its not an admin, get only the ones you mod if !user.local_user.admin { query = query.filter(community_actions::became_moderator.is_not_null()); } + let (limit, offset) = limit_and_offset(options.page, options.limit)?; + + query = query.limit(limit).offset(offset); + query.load::(&mut conn).await }; diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs new file mode 100644 index 0000000000..d34838c831 --- /dev/null +++ b/crates/db_views/src/report_combined_view.rs @@ -0,0 +1,238 @@ +use crate::structs::{ + LocalUserView, + PostOrCommentReportViewTemp, + PostReportView, + ReportCombinedView, +}; +use diesel::{ + pg::Pg, + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::{CommunityId, PersonId, PostReportId}, + schema::{ + comment_report, + community, + community_actions, + local_user, + person, + person_actions, + post, + post_actions, + post_aggregates, + post_report, + report_combined, + }, + source::community::CommunityFollower, + utils::{ + actions, + actions_alias, + functions::coalesce, + get_conn, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, +}; +use lemmy_utils::error::LemmyResult; + +impl ReportCombinedView { + /// returns the current unresolved report count for the communities you mod + pub async fn get_report_count( + pool: &mut DbPool<'_>, + my_person_id: PersonId, + admin: bool, + community_id: Option, + ) -> Result { + use diesel::dsl::count; + let conn = &mut get_conn(pool).await?; + let mut query = post_report::table + .inner_join(post::table) + .filter(post_report::resolved.eq(false)) + .into_boxed(); + + if let Some(community_id) = community_id { + query = query.filter(post::community_id.eq(community_id)) + } + + // If its not an admin, get only the ones you mod + if !admin { + query + .inner_join( + community_actions::table.on( + community_actions::community_id + .eq(post::community_id) + .and(community_actions::person_id.eq(my_person_id)) + .and(community_actions::became_moderator.is_not_null()), + ), + ) + .select(count(post_report::id)) + .first::(conn) + .await + } else { + query + .select(count(post_report::id)) + .first::(conn) + .await + } + } +} + +#[derive(Default)] +pub struct ReportCombinedQuery { + pub community_id: Option, + pub page: Option, + pub limit: Option, + pub unresolved_only: bool, +} + +impl ReportCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &LocalUserView, + ) -> LemmyResult> { + let options = self; + let conn = &mut get_conn(pool).await?; + let mut query = report_combined::table + .left_join(post_report::table) + .left_join(comment_report::table) + // .inner_join(post::table) + // .inner_join(community::table.on(post::community_id.eq(community::id))) + .left_join( + person::table.on( + post_report::creator_id + .eq(person::id) + .or(comment_report::creator_id.eq(person::id)), + ), + ) + // .inner_join(aliases::person1.on(post::creator_id.eq(aliases::person1.field(person::id)))) + // .left_join(actions_alias( + // creator_community_actions, + // post::creator_id, + // post::community_id, + // )) + // .left_join(actions( + // community_actions::table, + // Some(my_person_id), + // post::community_id, + // )) + // .left_join( + // local_user::table.on( + // post::creator_id + // .eq(local_user::person_id) + // .and(local_user::admin.eq(true)), + // ), + // ) + // .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + // .left_join(actions( + // person_actions::table, + // Some(my_person_id), + // post::creator_id, + // )) + // .inner_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) + // .left_join( + // aliases::person2 + // .on(post_report::resolver_id.eq(aliases::person2.field(person::id).nullable())), + // ) + .select(( + post_report::all_columns.nullable(), + comment_report::all_columns.nullable(), + // post::all_columns, + // community::all_columns, + person::all_columns.nullable(), + // aliases::person1.fields(person::all_columns), + // creator_community_actions + // .field(community_actions::received_ban) + // .nullable() + // .is_not_null(), + // creator_community_actions + // .field(community_actions::became_moderator) + // .nullable() + // .is_not_null(), + // local_user::admin.nullable().is_not_null(), + // CommunityFollower::select_subscribed_type(), + // post_actions::saved.nullable().is_not_null(), + // post_actions::read.nullable().is_not_null(), + // post_actions::hidden.nullable().is_not_null(), + // person_actions::blocked.nullable().is_not_null(), + // post_actions::like_score.nullable(), + // coalesce( + // post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + // post_aggregates::comments, + // ), + // post_aggregates::all_columns, + // aliases::person2.fields(person::all_columns.nullable()), + )) + .into_boxed(); + + // if let Some(community_id) = options.community_id { + // query = query.filter(post::community_id.eq(community_id)); + // } + + // if let Some(post_id) = options.post_id { + // query = query.filter(post::id.eq(post_id)); + // } + + // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) + // if options.unresolved_only { + // query = query + // .filter(post_report::resolved.eq(false)) + // .order_by(post_report::published.asc()); + // } else { + // query = query.order_by(post_report::published.desc()); + // } + + // If its not an admin, get only the ones you mod + // if !user.local_user.admin { + // query = query.filter(community_actions::became_moderator.is_not_null()); + // } + + let (limit, offset) = limit_and_offset(options.page, options.limit)?; + + query = query.limit(limit).offset(offset); + + let res = query.load::(conn).await?; + let out = res + .iter() + .filter_map(map_to_post_or_comment_view_tmp) + .collect(); + + Ok(out) + } +} + +fn map_to_post_or_comment_view_tmp( + view: &ReportCombinedView, +) -> Option { + // If it has post_report, you know the other fields are defined + if let (Some(post_report), Some(post_creator)) = (view.post_report.clone(), view.creator.clone()) + { + Some(PostOrCommentReportViewTemp::Post { + post_report, + post_creator, + }) + } else if let (Some(comment_report), Some(comment_creator)) = + (view.comment_report.clone(), view.creator.clone()) + { + Some(PostOrCommentReportViewTemp::Comment { + comment_report, + comment_creator, + }) + } else { + None + } +} + +// TODO add tests diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 4586fbcacd..e51aec67c3 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -237,3 +237,56 @@ pub struct LocalImageView { pub local_image: LocalImage, pub person: Person, } +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +// TODO TS shouldn't be necessary here, since this shouldn't be used externally +#[cfg_attr(feature = "full", ts(export))] +/// A combined report view +pub struct ReportCombinedView { + // Post-specific + pub post_report: Option, + // pub post_creator: Person, + // pub unread_comments: i64, + // pub post_counts: PostAggregates, + // #[cfg_attr(feature = "full", ts(optional))] + // pub resolver: Option, + // Comment-specific + pub comment_report: Option, + // pub comment_creator: Person, + // pub comment: Comment, + // pub comment_counts: CommentAggregates, + // Shared + // pub post: Post, + // pub community: Community, + // pub creator: Person, + // pub creator_banned_from_community: bool, + // pub creator_is_moderator: bool, + // pub creator_is_admin: bool, + // pub subscribed: SubscribedType, + // pub saved: bool, + // pub read: bool, + // pub hidden: bool, + // pub creator_blocked: bool, + // #[cfg_attr(feature = "full", ts(optional))] + // pub my_vote: Option, + // --- + pub creator: Option, +} + +pub enum PostOrCommentReportView { + Post(PostReportView), + Comment(CommentReportView), +} + +pub enum PostOrCommentReportViewTemp { + Post { + post_report: PostReport, + post_creator: Person, + }, + Comment { + comment_report: CommentReport, + comment_creator: Person, + }, +} diff --git a/migrations/2024-11-26-115042_add_combined_tables/down.sql b/migrations/2024-11-26-115042_add_combined_tables/down.sql new file mode 100644 index 0000000000..02c7477941 --- /dev/null +++ b/migrations/2024-11-26-115042_add_combined_tables/down.sql @@ -0,0 +1 @@ +DROP TABLE report_combined; diff --git a/migrations/2024-11-26-115042_add_combined_tables/up.sql b/migrations/2024-11-26-115042_add_combined_tables/up.sql new file mode 100644 index 0000000000..a7a0a74142 --- /dev/null +++ b/migrations/2024-11-26-115042_add_combined_tables/up.sql @@ -0,0 +1,14 @@ + +CREATE TABLE report_combined ( + id serial PRIMARY KEY, + published timestamptz not null, + post_report_id int REFERENCES post_report ON UPDATE CASCADE ON DELETE CASCADE, + comment_report_id int REFERENCES comment_report ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (post_report_id, comment_report_id) +); + +CREATE INDEX idx_report_combined_published on report_combined (published desc); + +-- TODO do history update +-- TODO do triggers in replaceable schema + From 1ff8ae06416e03cfc111884213593da5e45244f0 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 26 Nov 2024 16:53:01 -0500 Subject: [PATCH 02/43] Finishing up combined report table. --- crates/api/src/lib.rs | 4 +- .../{ => reports}/comment_report/create.rs | 2 +- .../src/{ => reports}/comment_report/list.rs | 2 +- .../src/{ => reports}/comment_report/mod.rs | 0 .../{ => reports}/comment_report/resolve.rs | 2 +- crates/api/src/reports/mod.rs | 4 + .../src/{ => reports}/post_report/create.rs | 2 +- .../api/src/{ => reports}/post_report/list.rs | 2 +- .../api/src/{ => reports}/post_report/mod.rs | 0 .../src/{ => reports}/post_report/resolve.rs | 2 +- .../private_message_report/create.rs | 2 +- .../private_message_report/list.rs | 2 +- .../private_message_report/mod.rs | 0 .../private_message_report/resolve.rs | 2 +- .../api/src/reports/report_combined/list.rs | 35 ++ crates/api/src/reports/report_combined/mod.rs | 1 + crates/api_common/src/comment.rs | 58 +--- crates/api_common/src/lib.rs | 1 + crates/api_common/src/post.rs | 59 +--- crates/api_common/src/private_message.rs | 54 +-- crates/api_common/src/reports/combined.rs | 32 ++ crates/api_common/src/reports/comment.rs | 60 ++++ crates/api_common/src/reports/mod.rs | 4 + crates/api_common/src/reports/post.rs | 61 ++++ .../api_common/src/reports/private_message.rs | 56 +++ crates/db_schema/src/schema.rs | 2 + crates/db_views/src/report_combined_view.rs | 319 ++++++++++++------ crates/db_views/src/structs.rs | 73 ++-- .../down.sql | 1 + .../up.sql | 9 +- src/api_routes_http.rs | 39 ++- 31 files changed, 541 insertions(+), 349 deletions(-) rename crates/api/src/{ => reports}/comment_report/create.rs (97%) rename crates/api/src/{ => reports}/comment_report/list.rs (94%) rename crates/api/src/{ => reports}/comment_report/mod.rs (100%) rename crates/api/src/{ => reports}/comment_report/resolve.rs (95%) create mode 100644 crates/api/src/reports/mod.rs rename crates/api/src/{ => reports}/post_report/create.rs (97%) rename crates/api/src/{ => reports}/post_report/list.rs (94%) rename crates/api/src/{ => reports}/post_report/mod.rs (100%) rename crates/api/src/{ => reports}/post_report/resolve.rs (96%) rename crates/api/src/{ => reports}/private_message_report/create.rs (96%) rename crates/api/src/{ => reports}/private_message_report/list.rs (90%) rename crates/api/src/{ => reports}/private_message_report/mod.rs (100%) rename crates/api/src/{ => reports}/private_message_report/resolve.rs (93%) create mode 100644 crates/api/src/reports/report_combined/list.rs create mode 100644 crates/api/src/reports/report_combined/mod.rs create mode 100644 crates/api_common/src/reports/combined.rs create mode 100644 crates/api_common/src/reports/comment.rs create mode 100644 crates/api_common/src/reports/mod.rs create mode 100644 crates/api_common/src/reports/post.rs create mode 100644 crates/api_common/src/reports/private_message.rs diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 83979212dd..e8d84542aa 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -33,13 +33,11 @@ use std::io::Cursor; use totp_rs::{Secret, TOTP}; pub mod comment; -pub mod comment_report; pub mod community; pub mod local_user; pub mod post; -pub mod post_report; pub mod private_message; -pub mod private_message_report; +pub mod reports; pub mod site; pub mod sitemap; diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/reports/comment_report/create.rs similarity index 97% rename from crates/api/src/comment_report/create.rs rename to crates/api/src/reports/comment_report/create.rs index 48066cfe66..a456ded369 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/reports/comment_report/create.rs @@ -2,8 +2,8 @@ use crate::check_report_reason; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ - comment::{CommentReportResponse, CreateCommentReport}, context::LemmyContext, + reports::comment::{CommentReportResponse, CreateCommentReport}, send_activity::{ActivityChannel, SendActivityData}, utils::{ check_comment_deleted_or_removed, diff --git a/crates/api/src/comment_report/list.rs b/crates/api/src/reports/comment_report/list.rs similarity index 94% rename from crates/api/src/comment_report/list.rs rename to crates/api/src/reports/comment_report/list.rs index d2f723819f..f4a6116989 100644 --- a/crates/api/src/comment_report/list.rs +++ b/crates/api/src/reports/comment_report/list.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json, Query}; use lemmy_api_common::{ - comment::{ListCommentReports, ListCommentReportsResponse}, context::LemmyContext, + reports::comment::{ListCommentReports, ListCommentReportsResponse}, utils::check_community_mod_of_any_or_admin_action, }; use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView}; diff --git a/crates/api/src/comment_report/mod.rs b/crates/api/src/reports/comment_report/mod.rs similarity index 100% rename from crates/api/src/comment_report/mod.rs rename to crates/api/src/reports/comment_report/mod.rs diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/reports/comment_report/resolve.rs similarity index 95% rename from crates/api/src/comment_report/resolve.rs rename to crates/api/src/reports/comment_report/resolve.rs index 58d5041dcf..5ab36054f2 100644 --- a/crates/api/src/comment_report/resolve.rs +++ b/crates/api/src/reports/comment_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ - comment::{CommentReportResponse, ResolveCommentReport}, context::LemmyContext, + reports::comment::{CommentReportResponse, ResolveCommentReport}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable}; diff --git a/crates/api/src/reports/mod.rs b/crates/api/src/reports/mod.rs new file mode 100644 index 0000000000..f23d1d71f2 --- /dev/null +++ b/crates/api/src/reports/mod.rs @@ -0,0 +1,4 @@ +pub mod comment_report; +pub mod post_report; +pub mod private_message_report; +pub mod report_combined; diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/reports/post_report/create.rs similarity index 97% rename from crates/api/src/post_report/create.rs rename to crates/api/src/reports/post_report/create.rs index b9edf35c57..bc85bdbe7d 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/reports/post_report/create.rs @@ -3,7 +3,7 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, - post::{CreatePostReport, PostReportResponse}, + reports::post::{CreatePostReport, PostReportResponse}, send_activity::{ActivityChannel, SendActivityData}, utils::{ check_community_user_action, diff --git a/crates/api/src/post_report/list.rs b/crates/api/src/reports/post_report/list.rs similarity index 94% rename from crates/api/src/post_report/list.rs rename to crates/api/src/reports/post_report/list.rs index 7d1d50b0b3..da3c0cd947 100644 --- a/crates/api/src/post_report/list.rs +++ b/crates/api/src/reports/post_report/list.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json, Query}; use lemmy_api_common::{ context::LemmyContext, - post::{ListPostReports, ListPostReportsResponse}, + reports::post::{ListPostReports, ListPostReportsResponse}, utils::check_community_mod_of_any_or_admin_action, }; use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView}; diff --git a/crates/api/src/post_report/mod.rs b/crates/api/src/reports/post_report/mod.rs similarity index 100% rename from crates/api/src/post_report/mod.rs rename to crates/api/src/reports/post_report/mod.rs diff --git a/crates/api/src/post_report/resolve.rs b/crates/api/src/reports/post_report/resolve.rs similarity index 96% rename from crates/api/src/post_report/resolve.rs rename to crates/api/src/reports/post_report/resolve.rs index 6523275131..26b182a456 100644 --- a/crates/api/src/post_report/resolve.rs +++ b/crates/api/src/reports/post_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - post::{PostReportResponse, ResolvePostReport}, + reports::post::{PostReportResponse, ResolvePostReport}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable}; diff --git a/crates/api/src/private_message_report/create.rs b/crates/api/src/reports/private_message_report/create.rs similarity index 96% rename from crates/api/src/private_message_report/create.rs rename to crates/api/src/reports/private_message_report/create.rs index de8ca390fa..17b5dceeb2 100644 --- a/crates/api/src/private_message_report/create.rs +++ b/crates/api/src/reports/private_message_report/create.rs @@ -2,7 +2,7 @@ use crate::check_report_reason; use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse}, + reports::private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse}, utils::send_new_report_email_to_admins, }; use lemmy_db_schema::{ diff --git a/crates/api/src/private_message_report/list.rs b/crates/api/src/reports/private_message_report/list.rs similarity index 90% rename from crates/api/src/private_message_report/list.rs rename to crates/api/src/reports/private_message_report/list.rs index 79ef53e1c1..61dbd6b975 100644 --- a/crates/api/src/private_message_report/list.rs +++ b/crates/api/src/reports/private_message_report/list.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json, Query}; use lemmy_api_common::{ context::LemmyContext, - private_message::{ListPrivateMessageReports, ListPrivateMessageReportsResponse}, + reports::private_message::{ListPrivateMessageReports, ListPrivateMessageReportsResponse}, utils::is_admin, }; use lemmy_db_views::{ diff --git a/crates/api/src/private_message_report/mod.rs b/crates/api/src/reports/private_message_report/mod.rs similarity index 100% rename from crates/api/src/private_message_report/mod.rs rename to crates/api/src/reports/private_message_report/mod.rs diff --git a/crates/api/src/private_message_report/resolve.rs b/crates/api/src/reports/private_message_report/resolve.rs similarity index 93% rename from crates/api/src/private_message_report/resolve.rs rename to crates/api/src/reports/private_message_report/resolve.rs index 7d821a60c9..3f812e4fe4 100644 --- a/crates/api/src/private_message_report/resolve.rs +++ b/crates/api/src/reports/private_message_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - private_message::{PrivateMessageReportResponse, ResolvePrivateMessageReport}, + reports::private_message::{PrivateMessageReportResponse, ResolvePrivateMessageReport}, utils::is_admin, }; use lemmy_db_schema::{source::private_message_report::PrivateMessageReport, traits::Reportable}; diff --git a/crates/api/src/reports/report_combined/list.rs b/crates/api/src/reports/report_combined/list.rs new file mode 100644 index 0000000000..7fab9919bf --- /dev/null +++ b/crates/api/src/reports/report_combined/list.rs @@ -0,0 +1,35 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + reports::combined::{ListReports, ListReportsResponse}, + utils::check_community_mod_of_any_or_admin_action, +}; +use lemmy_db_views::{report_combined_view::ReportCombinedQuery, structs::LocalUserView}; +use lemmy_utils::error::LemmyResult; + +/// Lists reports for a community if an id is supplied +/// or returns all reports for communities a user moderates +#[tracing::instrument(skip(context))] +pub async fn list_reports( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let community_id = data.community_id; + let unresolved_only = data.unresolved_only.unwrap_or_default(); + + check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; + + let page = data.page; + let limit = data.limit; + let reports = ReportCombinedQuery { + community_id, + unresolved_only, + page, + limit, + } + .list(&mut context.pool(), &local_user_view) + .await?; + + Ok(Json(ListReportsResponse { reports })) +} diff --git a/crates/api/src/reports/report_combined/mod.rs b/crates/api/src/reports/report_combined/mod.rs new file mode 100644 index 0000000000..d17e233fbf --- /dev/null +++ b/crates/api/src/reports/report_combined/mod.rs @@ -0,0 +1 @@ +pub mod list; diff --git a/crates/api_common/src/comment.rs b/crates/api_common/src/comment.rs index e083657893..bae9c4de4e 100644 --- a/crates/api_common/src/comment.rs +++ b/crates/api_common/src/comment.rs @@ -1,9 +1,9 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommentReportId, CommunityId, LanguageId, LocalUserId, PostId}, + newtypes::{CommentId, CommunityId, LanguageId, LocalUserId, PostId}, CommentSortType, ListingType, }; -use lemmy_db_views::structs::{CommentReportView, CommentView, VoteView}; +use lemmy_db_views::structs::{CommentView, VoteView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -146,60 +146,6 @@ pub struct GetCommentsResponse { pub comments: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Report a comment. -pub struct CreateCommentReport { - pub comment_id: CommentId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The comment report response. -pub struct CommentReportResponse { - pub comment_report_view: CommentReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a comment report (only doable by mods). -pub struct ResolveCommentReport { - pub report_id: CommentReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List comment reports. -pub struct ListCommentReports { - #[cfg_attr(feature = "full", ts(optional))] - pub comment_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, - /// if no community is given, it returns reports for all communities moderated by the auth user - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The comment report list response. -pub struct ListCommentReportsResponse { - pub comment_reports: Vec, -} - #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 6e09d904d6..8af1dec25c 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -11,6 +11,7 @@ pub mod oauth_provider; pub mod person; pub mod post; pub mod private_message; +pub mod reports; #[cfg(feature = "full")] pub mod request; #[cfg(feature = "full")] diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 405de3a92d..310c5c03e6 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -1,10 +1,10 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId}, + newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId}, ListingType, PostFeatureType, PostSortType, }; -use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView}; +use lemmy_db_views::structs::{PaginationCursor, PostView, VoteView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -247,61 +247,6 @@ pub struct SavePost { pub save: bool, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Create a post report. -pub struct CreatePostReport { - pub post_id: PostId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The post report response. -pub struct PostReportResponse { - pub post_report_view: PostReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a post report (mods only). -pub struct ResolvePostReport { - pub report_id: PostReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List post reports. -pub struct ListPostReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, - // TODO make into tagged enum at some point - /// if no community is given, it returns reports for all communities moderated by the auth user - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub post_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The post reports response. -pub struct ListPostReportsResponse { - pub post_reports: Vec, -} - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/api_common/src/private_message.rs b/crates/api_common/src/private_message.rs index 666fe38659..8bd417a8e4 100644 --- a/crates/api_common/src/private_message.rs +++ b/crates/api_common/src/private_message.rs @@ -1,5 +1,5 @@ -use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId, PrivateMessageReportId}; -use lemmy_db_views::structs::{PrivateMessageReportView, PrivateMessageView}; +use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId}; +use lemmy_db_views::structs::PrivateMessageView; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -72,53 +72,3 @@ pub struct PrivateMessagesResponse { pub struct PrivateMessageResponse { pub private_message_view: PrivateMessageView, } - -#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Create a report for a private message. -pub struct CreatePrivateMessageReport { - pub private_message_id: PrivateMessageId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// A private message report response. -pub struct PrivateMessageReportResponse { - pub private_message_report_view: PrivateMessageReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a private message report. -pub struct ResolvePrivateMessageReport { - pub report_id: PrivateMessageReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List private message reports. -// TODO , perhaps GetReports should be a tagged enum list too. -pub struct ListPrivateMessageReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The response for list private message reports. -pub struct ListPrivateMessageReportsResponse { - pub private_message_reports: Vec, -} diff --git a/crates/api_common/src/reports/combined.rs b/crates/api_common/src/reports/combined.rs new file mode 100644 index 0000000000..17d6a0505c --- /dev/null +++ b/crates/api_common/src/reports/combined.rs @@ -0,0 +1,32 @@ +use lemmy_db_schema::newtypes::CommunityId; +use lemmy_db_views::structs::ReportCombinedView; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// List reports. +pub struct ListReports { + #[cfg_attr(feature = "full", ts(optional))] + pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub limit: Option, + /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] + pub unresolved_only: Option, + /// if no community is given, it returns reports for all communities moderated by the auth user + #[cfg_attr(feature = "full", ts(optional))] + pub community_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The post reports response. +pub struct ListReportsResponse { + pub reports: Vec, +} diff --git a/crates/api_common/src/reports/comment.rs b/crates/api_common/src/reports/comment.rs new file mode 100644 index 0000000000..4324079e5f --- /dev/null +++ b/crates/api_common/src/reports/comment.rs @@ -0,0 +1,60 @@ +use lemmy_db_schema::newtypes::{CommentId, CommentReportId, CommunityId}; +use lemmy_db_views::structs::CommentReportView; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Report a comment. +pub struct CreateCommentReport { + pub comment_id: CommentId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The comment report response. +pub struct CommentReportResponse { + pub comment_report_view: CommentReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a comment report (only doable by mods). +pub struct ResolveCommentReport { + pub report_id: CommentReportId, + pub resolved: bool, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// List comment reports. +pub struct ListCommentReports { + #[cfg_attr(feature = "full", ts(optional))] + pub comment_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub limit: Option, + /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] + pub unresolved_only: Option, + /// if no community is given, it returns reports for all communities moderated by the auth user + #[cfg_attr(feature = "full", ts(optional))] + pub community_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The comment report list response. +pub struct ListCommentReportsResponse { + pub comment_reports: Vec, +} diff --git a/crates/api_common/src/reports/mod.rs b/crates/api_common/src/reports/mod.rs new file mode 100644 index 0000000000..6584de1bc4 --- /dev/null +++ b/crates/api_common/src/reports/mod.rs @@ -0,0 +1,4 @@ +pub mod combined; +pub mod comment; +pub mod post; +pub mod private_message; diff --git a/crates/api_common/src/reports/post.rs b/crates/api_common/src/reports/post.rs new file mode 100644 index 0000000000..25094a22a0 --- /dev/null +++ b/crates/api_common/src/reports/post.rs @@ -0,0 +1,61 @@ +use lemmy_db_schema::newtypes::{CommunityId, PostId, PostReportId}; +use lemmy_db_views::structs::PostReportView; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a post report. +pub struct CreatePostReport { + pub post_id: PostId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The post report response. +pub struct PostReportResponse { + pub post_report_view: PostReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a post report (mods only). +pub struct ResolvePostReport { + pub report_id: PostReportId, + pub resolved: bool, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// List post reports. +pub struct ListPostReports { + #[cfg_attr(feature = "full", ts(optional))] + pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub limit: Option, + /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] + pub unresolved_only: Option, + // TODO make into tagged enum at some point + /// if no community is given, it returns reports for all communities moderated by the auth user + #[cfg_attr(feature = "full", ts(optional))] + pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub post_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The post reports response. +pub struct ListPostReportsResponse { + pub post_reports: Vec, +} diff --git a/crates/api_common/src/reports/private_message.rs b/crates/api_common/src/reports/private_message.rs new file mode 100644 index 0000000000..0eb8100f0a --- /dev/null +++ b/crates/api_common/src/reports/private_message.rs @@ -0,0 +1,56 @@ +use lemmy_db_schema::newtypes::{PrivateMessageId, PrivateMessageReportId}; +use lemmy_db_views::structs::PrivateMessageReportView; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a report for a private message. +pub struct CreatePrivateMessageReport { + pub private_message_id: PrivateMessageId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A private message report response. +pub struct PrivateMessageReportResponse { + pub private_message_report_view: PrivateMessageReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a private message report. +pub struct ResolvePrivateMessageReport { + pub report_id: PrivateMessageReportId, + pub resolved: bool, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// List private message reports. +// TODO , perhaps GetReports should be a tagged enum list too. +pub struct ListPrivateMessageReports { + #[cfg_attr(feature = "full", ts(optional))] + pub page: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub limit: Option, + /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] + pub unresolved_only: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The response for list private message reports. +pub struct ListPrivateMessageReportsResponse { + pub private_message_reports: Vec, +} diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 1d3177b155..a64ade092d 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -862,6 +862,7 @@ diesel::table! { published -> Timestamptz, post_report_id -> Nullable, comment_report_id -> Nullable, + private_message_report_id -> Nullable, } } @@ -1017,6 +1018,7 @@ diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); diesel::joinable!(report_combined -> comment_report (comment_report_id)); diesel::joinable!(report_combined -> post_report (post_report_id)); +diesel::joinable!(report_combined -> private_message_report (private_message_report_id)); diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index d34838c831..04bcc0f38d 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -1,11 +1,12 @@ use crate::structs::{ + CommentReportView, LocalUserView, - PostOrCommentReportViewTemp, PostReportView, + PrivateMessageReportView, ReportCombinedView, + ReportCombinedViewInternal, }; use diesel::{ - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -16,8 +17,11 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::{CommunityId, PersonId, PostReportId}, + newtypes::{CommunityId, PersonId}, schema::{ + comment, + comment_actions, + comment_aggregates, comment_report, community, community_actions, @@ -28,25 +32,17 @@ use lemmy_db_schema::{ post_actions, post_aggregates, post_report, + private_message, + private_message_report, report_combined, }, source::community::CommunityFollower, - utils::{ - actions, - actions_alias, - functions::coalesce, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{actions, actions_alias, functions::coalesce, get_conn, limit_and_offset, DbPool}, }; use lemmy_utils::error::LemmyResult; -impl ReportCombinedView { +// TODO fix +impl ReportCombinedViewInternal { /// returns the current unresolved report count for the communities you mod pub async fn get_report_count( pool: &mut DbPool<'_>, @@ -96,140 +92,239 @@ pub struct ReportCombinedQuery { pub unresolved_only: bool, } +// TODO need to add private message impl ReportCombinedQuery { pub async fn list( self, pool: &mut DbPool<'_>, user: &LocalUserView, - ) -> LemmyResult> { + ) -> LemmyResult> { let options = self; + let my_person_id = user.local_user.person_id; + let item_creator = aliases::person1.field(person::id); let conn = &mut get_conn(pool).await?; + + // Notes: since the post_report_id and comment_report_id are optional columns, + // many joins must use an OR condition. + // For example, the report creator must be the person table joined to either: + // - post_report.creator_id + // - comment_report.creator_id let mut query = report_combined::table .left_join(post_report::table) .left_join(comment_report::table) - // .inner_join(post::table) - // .inner_join(community::table.on(post::community_id.eq(community::id))) - .left_join( + .left_join(private_message_report::table) + // The report creator + .inner_join( person::table.on( post_report::creator_id .eq(person::id) - .or(comment_report::creator_id.eq(person::id)), + .or(comment_report::creator_id.eq(person::id)) + .or(private_message_report::creator_id.eq(person::id)), + ), + ) + // The comment + .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) + // The private message + .left_join( + private_message::table + .on(private_message_report::private_message_id.eq(private_message::id)), + ) + // The post + .left_join( + post::table.on( + post_report::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)), ), ) - // .inner_join(aliases::person1.on(post::creator_id.eq(aliases::person1.field(person::id)))) - // .left_join(actions_alias( - // creator_community_actions, - // post::creator_id, - // post::community_id, - // )) - // .left_join(actions( - // community_actions::table, - // Some(my_person_id), - // post::community_id, - // )) - // .left_join( - // local_user::table.on( - // post::creator_id - // .eq(local_user::person_id) - // .and(local_user::admin.eq(true)), - // ), - // ) - // .left_join(actions(post_actions::table, Some(my_person_id), post::id)) - // .left_join(actions( - // person_actions::table, - // Some(my_person_id), - // post::creator_id, - // )) - // .inner_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) - // .left_join( - // aliases::person2 - // .on(post_report::resolver_id.eq(aliases::person2.field(person::id).nullable())), - // ) + // The item creator + // You can now use aliases::person1.field(person::id) / item_creator for all the item actions + .inner_join( + aliases::person1.on( + post::creator_id + .eq(item_creator) + .or(comment::creator_id.eq(item_creator)) + .or(private_message::creator_id.eq(item_creator)), + ), + ) + // The community + .left_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + .left_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) + .left_join( + comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), + ) + .left_join(aliases::person2.on(item_creator.eq(aliases::person2.field(person::id)))) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment_report::comment_id, + )) .select(( + // Post-specific post_report::all_columns.nullable(), + post::all_columns.nullable(), + post_aggregates::all_columns.nullable(), + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ) + .nullable(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + // Comment-specific comment_report::all_columns.nullable(), - // post::all_columns, - // community::all_columns, - person::all_columns.nullable(), - // aliases::person1.fields(person::all_columns), - // creator_community_actions - // .field(community_actions::received_ban) - // .nullable() - // .is_not_null(), - // creator_community_actions - // .field(community_actions::became_moderator) - // .nullable() - // .is_not_null(), - // local_user::admin.nullable().is_not_null(), - // CommunityFollower::select_subscribed_type(), - // post_actions::saved.nullable().is_not_null(), - // post_actions::read.nullable().is_not_null(), - // post_actions::hidden.nullable().is_not_null(), - // person_actions::blocked.nullable().is_not_null(), - // post_actions::like_score.nullable(), - // coalesce( - // post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), - // post_aggregates::comments, - // ), - // post_aggregates::all_columns, - // aliases::person2.fields(person::all_columns.nullable()), + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Private-message-specific + private_message_report::all_columns.nullable(), + private_message::all_columns.nullable(), + // Shared + person::all_columns, + aliases::person1.fields(person::all_columns), + community::all_columns.nullable(), + CommunityFollower::select_subscribed_type(), + aliases::person2.fields(person::all_columns.nullable()), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), )) .into_boxed(); - // if let Some(community_id) = options.community_id { - // query = query.filter(post::community_id.eq(community_id)); - // } - - // if let Some(post_id) = options.post_id { - // query = query.filter(post::id.eq(post_id)); - // } + if let Some(community_id) = options.community_id { + query = query.filter(community::id.eq(community_id)); + } // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest // first (FIFO) - // if options.unresolved_only { - // query = query - // .filter(post_report::resolved.eq(false)) - // .order_by(post_report::published.asc()); - // } else { - // query = query.order_by(post_report::published.desc()); - // } + if options.unresolved_only { + query = query + .filter(post_report::resolved.eq(false)) + .or_filter(comment_report::resolved.eq(false)) + .or_filter(private_message_report::resolved.eq(false)) + .order_by(report_combined::published.asc()); + } else { + query = query.order_by(report_combined::published.desc()); + } // If its not an admin, get only the ones you mod - // if !user.local_user.admin { - // query = query.filter(community_actions::became_moderator.is_not_null()); - // } + if !user.local_user.admin { + query = query.filter(community_actions::became_moderator.is_not_null()); + } let (limit, offset) = limit_and_offset(options.page, options.limit)?; query = query.limit(limit).offset(offset); - let res = query.load::(conn).await?; - let out = res - .iter() - .filter_map(map_to_post_or_comment_view_tmp) - .collect(); + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(map_to_enum).collect(); Ok(out) } } -fn map_to_post_or_comment_view_tmp( - view: &ReportCombinedView, -) -> Option { - // If it has post_report, you know the other fields are defined - if let (Some(post_report), Some(post_creator)) = (view.post_report.clone(), view.creator.clone()) - { - Some(PostOrCommentReportViewTemp::Post { +/// Maps the combined DB row to an enum +fn map_to_enum(view: ReportCombinedViewInternal) -> Option { + // Use for a short alias + let v = view; + + if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = ( + v.post_report, + v.post.clone(), + v.community.clone(), + v.post_unread_comments, + v.post_counts, + ) { + Some(ReportCombinedView::Post(PostReportView { post_report, - post_creator, - }) - } else if let (Some(comment_report), Some(comment_creator)) = - (view.comment_report.clone(), view.creator.clone()) - { - Some(PostOrCommentReportViewTemp::Comment { + post, + community, + unread_comments, + counts, + creator: v.report_creator, + post_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + resolver: v.resolver, + })) + } else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = ( + v.comment_report, + v.comment, + v.comment_counts, + v.post.clone(), + v.community.clone(), + ) { + Some(ReportCombinedView::Comment(CommentReportView { comment_report, - comment_creator, - }) + comment, + counts, + post, + community, + creator: v.report_creator, + comment_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + resolver: v.resolver, + })) + } else if let (Some(private_message_report), Some(private_message)) = + (v.private_message_report, v.private_message) + { + Some(ReportCombinedView::PrivateMessage( + PrivateMessageReportView { + private_message_report, + private_message, + creator: v.report_creator, + private_message_creator: v.item_creator, + resolver: v.resolver, + }, + )) } else { None } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index e51aec67c3..cbd879157a 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -237,56 +237,47 @@ pub struct LocalImageView { pub local_image: LocalImage, pub person: Person, } -#[skip_serializing_none] + #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", derive(Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -// TODO TS shouldn't be necessary here, since this shouldn't be used externally -#[cfg_attr(feature = "full", ts(export))] /// A combined report view -pub struct ReportCombinedView { +pub struct ReportCombinedViewInternal { // Post-specific pub post_report: Option, - // pub post_creator: Person, - // pub unread_comments: i64, - // pub post_counts: PostAggregates, - // #[cfg_attr(feature = "full", ts(optional))] - // pub resolver: Option, + pub post: Option, + pub post_counts: Option, + pub post_unread_comments: Option, + pub post_saved: bool, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, // Comment-specific pub comment_report: Option, - // pub comment_creator: Person, - // pub comment: Comment, - // pub comment_counts: CommentAggregates, - // Shared - // pub post: Post, - // pub community: Community, - // pub creator: Person, - // pub creator_banned_from_community: bool, - // pub creator_is_moderator: bool, - // pub creator_is_admin: bool, - // pub subscribed: SubscribedType, - // pub saved: bool, - // pub read: bool, - // pub hidden: bool, - // pub creator_blocked: bool, - // #[cfg_attr(feature = "full", ts(optional))] - // pub my_vote: Option, - // --- - pub creator: Option, + pub comment: Option, + pub comment_counts: Option, + pub comment_saved: bool, + pub my_comment_vote: Option, + // Private-message-specific + pub private_message_report: Option, + pub private_message: Option, + // // Shared + pub report_creator: Person, + pub item_creator: Person, + pub community: Option, + pub subscribed: SubscribedType, + pub resolver: Option, + pub item_creator_is_admin: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_is_moderator: bool, + pub item_creator_blocked: bool, } -pub enum PostOrCommentReportView { +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub enum ReportCombinedView { Post(PostReportView), Comment(CommentReportView), -} - -pub enum PostOrCommentReportViewTemp { - Post { - post_report: PostReport, - post_creator: Person, - }, - Comment { - comment_report: CommentReport, - comment_creator: Person, - }, + PrivateMessage(PrivateMessageReportView), } diff --git a/migrations/2024-11-26-115042_add_combined_tables/down.sql b/migrations/2024-11-26-115042_add_combined_tables/down.sql index 02c7477941..b27ba9bc43 100644 --- a/migrations/2024-11-26-115042_add_combined_tables/down.sql +++ b/migrations/2024-11-26-115042_add_combined_tables/down.sql @@ -1 +1,2 @@ DROP TABLE report_combined; + diff --git a/migrations/2024-11-26-115042_add_combined_tables/up.sql b/migrations/2024-11-26-115042_add_combined_tables/up.sql index a7a0a74142..2068355bf6 100644 --- a/migrations/2024-11-26-115042_add_combined_tables/up.sql +++ b/migrations/2024-11-26-115042_add_combined_tables/up.sql @@ -1,14 +1,13 @@ - CREATE TABLE report_combined ( id serial PRIMARY KEY, - published timestamptz not null, + published timestamptz NOT NULL, post_report_id int REFERENCES post_report ON UPDATE CASCADE ON DELETE CASCADE, comment_report_id int REFERENCES comment_report ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (post_report_id, comment_report_id) + private_message_report_id int REFERENCES private_message_report ON UPDATE CASCADE ON DELETE CASCADE, + UNIQUE (post_report_id, comment_report_id, private_message_report_id) ); -CREATE INDEX idx_report_combined_published on report_combined (published desc); +CREATE INDEX idx_report_combined_published ON report_combined (published DESC); -- TODO do history update -- TODO do triggers in replaceable schema - diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 2f431419c0..e4fcb7a8f1 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -6,11 +6,6 @@ use lemmy_api::{ list_comment_likes::list_comment_likes, save::save_comment, }, - comment_report::{ - create::create_comment_report, - list::list_comment_reports, - resolve::resolve_comment_report, - }, community::{ add_mod::add_mod_to_community, ban::ban_from_community, @@ -64,16 +59,24 @@ use lemmy_api::{ mark_read::mark_post_as_read, save::save_post, }, - post_report::{ - create::create_post_report, - list::list_post_reports, - resolve::resolve_post_report, - }, private_message::mark_read::mark_pm_as_read, - private_message_report::{ - create::create_pm_report, - list::list_pm_reports, - resolve::resolve_pm_report, + reports::{ + comment_report::{ + create::create_comment_report, + list::list_comment_reports, + resolve::resolve_comment_report, + }, + post_report::{ + create::create_post_report, + list::list_post_reports, + resolve::resolve_post_report, + }, + private_message_report::{ + create::create_pm_report, + list::list_pm_reports, + resolve::resolve_pm_report, + }, + report_combined::list::list_reports, }, site::{ block::block_instance, @@ -248,6 +251,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/like", web::post().to(like_post)) .route("/like/list", web::get().to(list_post_likes)) .route("/save", web::put().to(save_post)) + // TODO should these be moved into the new report heading? .route("/report", web::post().to(create_post_report)) .route("/report/resolve", web::put().to(resolve_post_report)) .route("/report/list", web::get().to(list_post_reports)) @@ -274,10 +278,16 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/like/list", web::get().to(list_comment_likes)) .route("/save", web::put().to(save_comment)) .route("/list", web::get().to(list_comments)) + // TODO should these be moved into the new report heading? .route("/report", web::post().to(create_comment_report)) .route("/report/resolve", web::put().to(resolve_comment_report)) .route("/report/list", web::get().to(list_comment_reports)), ) + .service( + web::scope("report") + .wrap(rate_limit.message()) + .route("/list", web::get().to(list_reports)), + ) // Private Message .service( web::scope("/private_message") @@ -287,6 +297,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("", web::put().to(update_private_message)) .route("/delete", web::post().to(delete_private_message)) .route("/mark_as_read", web::post().to(mark_pm_as_read)) + // TODO should these be moved into the new report heading? .route("/report", web::post().to(create_pm_report)) .route("/report/resolve", web::put().to(resolve_pm_report)) .route("/report/list", web::get().to(list_pm_reports)), From 2351c7a93b3bd35b2cb421aa88e097e925e14e9b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 26 Nov 2024 16:58:46 -0500 Subject: [PATCH 03/43] Fix ts optionals. --- crates/db_schema/src/source/combined/report.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/db_schema/src/source/combined/report.rs b/crates/db_schema/src/source/combined/report.rs index 7c55329e74..de0df8ad38 100644 --- a/crates/db_schema/src/source/combined/report.rs +++ b/crates/db_schema/src/source/combined/report.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{CommentReportId, PostReportId, ReportCombinedId}; +use crate::newtypes::{CommentReportId, PostReportId, PrivateMessageReportId, ReportCombinedId}; #[cfg(feature = "full")] use crate::schema::report_combined; use chrono::{DateTime, Utc}; @@ -17,6 +17,10 @@ use ts_rs::TS; pub struct ReportCombined { pub id: ReportCombinedId, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub post_report_id: Option, + #[cfg_attr(feature = "full", ts(optional))] pub comment_report_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub private_message_report_id: Option, } From 0ba961ff5cc0a80f2273ec19b2f8bc316fd21a28 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 27 Nov 2024 16:02:11 -0500 Subject: [PATCH 04/43] Adding tests, triggers, and history updates for report_combined. --- crates/api/src/local_user/report_count.rs | 36 +-- crates/api_common/src/person.rs | 7 +- .../db_schema/replaceable_schema/triggers.sql | 54 ++++ crates/db_views/src/report_combined_view.rs | 272 ++++++++++++++++-- .../up.sql | 32 ++- 5 files changed, 340 insertions(+), 61 deletions(-) diff --git a/crates/api/src/local_user/report_count.rs b/crates/api/src/local_user/report_count.rs index 32448dcaae..0d24a4de94 100644 --- a/crates/api/src/local_user/report_count.rs +++ b/crates/api/src/local_user/report_count.rs @@ -4,12 +4,7 @@ use lemmy_api_common::{ person::{GetReportCount, GetReportCountResponse}, utils::check_community_mod_of_any_or_admin_action, }; -use lemmy_db_views::structs::{ - CommentReportView, - LocalUserView, - PostReportView, - PrivateMessageReportView, -}; +use lemmy_db_views::structs::{LocalUserView, ReportCombinedViewInternal}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -18,29 +13,14 @@ pub async fn report_count( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let person_id = local_user_view.person.id; - let admin = local_user_view.local_user.admin; - let community_id = data.community_id; - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - let comment_reports = - CommentReportView::get_report_count(&mut context.pool(), person_id, admin, community_id) - .await?; - - let post_reports = - PostReportView::get_report_count(&mut context.pool(), person_id, admin, community_id).await?; - - let private_message_reports = if admin && community_id.is_none() { - Some(PrivateMessageReportView::get_report_count(&mut context.pool()).await?) - } else { - None - }; + let count = ReportCombinedViewInternal::get_report_count( + &mut context.pool(), + &local_user_view, + data.community_id, + ) + .await?; - Ok(Json(GetReportCountResponse { - community_id, - comment_reports, - post_reports, - private_message_reports, - })) + Ok(Json(GetReportCountResponse { count })) } diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index b95cf5e774..797946d656 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -448,12 +448,7 @@ pub struct GetReportCount { #[cfg_attr(feature = "full", ts(export))] /// A response for the number of reports. pub struct GetReportCountResponse { - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - pub comment_reports: i64, - pub post_reports: i64, - #[cfg_attr(feature = "full", ts(optional))] - pub private_message_reports: Option, + pub count: i64, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index e5b3e22d0d..58862e66d8 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -653,3 +653,57 @@ CREATE TRIGGER change_values FOR EACH ROW EXECUTE FUNCTION r.private_message_change_values (); +-- Combined tables triggers +-- These insert (published, item_id) into X_combined tables +-- Reports +-- Comment +CREATE FUNCTION r.report_combined_comment_report_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO report_combined (published, comment_report_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; +END +$$; + +CREATE TRIGGER report_combined_comment + AFTER INSERT ON comment_report + FOR EACH ROW + EXECUTE FUNCTION r.report_combined_comment_report_insert (); + +-- Post +CREATE FUNCTION r.report_combined_post_report_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO report_combined (published, post_report_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; +END +$$; + +CREATE TRIGGER report_combined_post + AFTER INSERT ON post_report + FOR EACH ROW + EXECUTE FUNCTION r.report_combined_post_report_insert (); + +-- Private message +CREATE FUNCTION r.report_combined_private_message_report_insert () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO report_combined (published, private_message_report_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; +END +$$; + +CREATE TRIGGER report_combined_private_message + AFTER INSERT ON private_message_report + FOR EACH ROW + EXECUTE FUNCTION r.report_combined_private_message_report_insert (); + diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 04bcc0f38d..994361d800 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -17,7 +17,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::{CommunityId, PersonId}, + newtypes::CommunityId, schema::{ comment, comment_actions, @@ -41,20 +41,41 @@ use lemmy_db_schema::{ }; use lemmy_utils::error::LemmyResult; -// TODO fix impl ReportCombinedViewInternal { /// returns the current unresolved report count for the communities you mod pub async fn get_report_count( pool: &mut DbPool<'_>, - my_person_id: PersonId, - admin: bool, + user: &LocalUserView, community_id: Option, ) -> Result { use diesel::dsl::count; + let conn = &mut get_conn(pool).await?; - let mut query = post_report::table - .inner_join(post::table) + let my_person_id = user.local_user.person_id; + + let mut query = report_combined::table + .left_join(post_report::table) + .left_join(comment_report::table) + .left_join(private_message_report::table) + // Need to join to comment and post to get the community + .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) + // The post + .left_join( + post::table.on( + post_report::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)), + ), + ) + .left_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) .filter(post_report::resolved.eq(false)) + .or_filter(comment_report::resolved.eq(false)) + .or_filter(private_message_report::resolved.eq(false)) .into_boxed(); if let Some(community_id) = community_id { @@ -62,25 +83,14 @@ impl ReportCombinedViewInternal { } // If its not an admin, get only the ones you mod - if !admin { - query - .inner_join( - community_actions::table.on( - community_actions::community_id - .eq(post::community_id) - .and(community_actions::person_id.eq(my_person_id)) - .and(community_actions::became_moderator.is_not_null()), - ), - ) - .select(count(post_report::id)) - .first::(conn) - .await - } else { - query - .select(count(post_report::id)) - .first::(conn) - .await + if !user.local_user.admin { + query = query.filter(community_actions::became_moderator.is_not_null()); } + + query + .select(count(report_combined::id)) + .first::(conn) + .await } } @@ -92,7 +102,6 @@ pub struct ReportCombinedQuery { pub unresolved_only: bool, } -// TODO need to add private message impl ReportCombinedQuery { pub async fn list( self, @@ -330,4 +339,215 @@ fn map_to_enum(view: ReportCombinedViewInternal) -> Option { } } -// TODO add tests +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + report_combined_view::ReportCombinedQuery, + structs::{LocalUserView, ReportCombinedView, ReportCombinedViewInternal}, + }; + use lemmy_db_schema::{ + assert_length, + source::{ + comment::{Comment, CommentInsertForm}, + comment_report::{CommentReport, CommentReportForm}, + community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + local_user_vote_display_mode::LocalUserVoteDisplayMode, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, + post_report::{PostReport, PostReportForm}, + private_message::{PrivateMessage, PrivateMessageInsertForm}, + private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, + }, + traits::{Crud, Joinable, Reportable}, + utils::build_db_pool_for_tests, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + #[tokio::test] + #[serial] + async fn test_crud() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rcv"); + let inserted_timmy = Person::create(pool, &timmy_form).await?; + let timmy_local_user_form = LocalUserInsertForm::test_form(inserted_timmy.id); + let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; + let timmy_view = LocalUserView { + local_user: timmy_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_timmy.clone(), + counts: Default::default(), + }; + + // Make an admin, to be able to see private message reports. + let admin_form = PersonInsertForm::test_form(inserted_instance.id, "admin_rcv"); + let inserted_admin = Person::create(pool, &admin_form).await?; + let admin_local_user_form = LocalUserInsertForm::test_form_admin(inserted_admin.id); + let admin_local_user = LocalUser::create(pool, &admin_local_user_form, vec![]).await?; + let admin_view = LocalUserView { + local_user: admin_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_admin.clone(), + counts: Default::default(), + }; + + let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rcv"); + let inserted_sara = Person::create(pool, &sara_form).await?; + + let community_form = CommunityInsertForm::new( + inserted_instance.id, + "test community crv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &community_form).await?; + + // Make timmy a mod + let timmy_moderator_form = CommunityModeratorForm { + community_id: inserted_community.id, + person_id: inserted_timmy.id, + }; + CommunityModerator::join(pool, &timmy_moderator_form).await?; + + let post_form = PostInsertForm::new( + "A test post crv".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &post_form).await?; + + // sara reports the post + let sara_report_post_form = PostReportForm { + creator_id: inserted_sara.id, + post_id: inserted_post.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from sara".into(), + }; + let inserted_post_report = PostReport::report(pool, &sara_report_post_form).await?; + + // Timmy creates a comment + let comment_form = CommentInsertForm::new( + inserted_timmy.id, + inserted_post.id, + "A test comment rv".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + // Sara reports the comment + let sara_report_comment_form = CommentReportForm { + creator_id: inserted_sara.id, + comment_id: inserted_comment.id, + original_comment_text: "A test comment rv".into(), + reason: "from sara".into(), + }; + CommentReport::report(pool, &sara_report_comment_form).await?; + + // Timmy creates a private message report + let pm_form = PrivateMessageInsertForm::new( + inserted_timmy.id, + inserted_sara.id, + "something offensive crv".to_string(), + ); + let inserted_pm = PrivateMessage::create(pool, &pm_form).await?; + + // sara reports private message + let pm_report_form = PrivateMessageReportForm { + creator_id: inserted_sara.id, + original_pm_text: inserted_pm.content.clone(), + private_message_id: inserted_pm.id, + reason: "its offensive".to_string(), + }; + PrivateMessageReport::report(pool, &pm_report_form).await?; + + // Do a batch read of admins reports + let reports = ReportCombinedQuery::default() + .list(pool, &admin_view) + .await?; + assert_eq!(3, reports.len()); + + // Make sure the report types are correct + if let ReportCombinedView::Post(v) = &reports[2] { + assert_eq!(inserted_post.id, v.post.id); + assert_eq!(inserted_sara.id, v.creator.id); + assert_eq!(inserted_timmy.id, v.post_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[1] { + assert_eq!(inserted_comment.id, v.comment.id); + assert_eq!(inserted_post.id, v.post.id); + assert_eq!(inserted_timmy.id, v.comment_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert_eq!(inserted_pm.id, v.private_message.id); + } else { + panic!("wrong type"); + } + + let report_count_admin = + ReportCombinedViewInternal::get_report_count(pool, &admin_view, None).await?; + assert_eq!(3, report_count_admin); + + // Timmy should only see 2 reports, since they're not an admin, + // but they do mod the community + let reports = ReportCombinedQuery::default() + .list(pool, &timmy_view) + .await?; + assert_eq!(2, reports.len()); + + // Make sure the report types are correct + if let ReportCombinedView::Post(v) = &reports[1] { + assert_eq!(inserted_post.id, v.post.id); + assert_eq!(inserted_sara.id, v.creator.id); + assert_eq!(inserted_timmy.id, v.post_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[0] { + assert_eq!(inserted_comment.id, v.comment.id); + assert_eq!(inserted_post.id, v.post.id); + assert_eq!(inserted_timmy.id, v.comment_creator.id); + } else { + panic!("wrong type"); + } + + let report_count_timmy = + ReportCombinedViewInternal::get_report_count(pool, &timmy_view, None).await?; + assert_eq!(2, report_count_timmy); + + // Resolve the post report + PostReport::resolve(pool, inserted_post_report.id, inserted_timmy.id).await?; + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: true, + ..Default::default() + } + .list(pool, &timmy_view) + .await?; + assert_length!(1, reports_after_resolve); + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + Instance::delete(pool, inserted_instance.id).await?; + + Ok(()) + } +} diff --git a/migrations/2024-11-26-115042_add_combined_tables/up.sql b/migrations/2024-11-26-115042_add_combined_tables/up.sql index 2068355bf6..1dd6490052 100644 --- a/migrations/2024-11-26-115042_add_combined_tables/up.sql +++ b/migrations/2024-11-26-115042_add_combined_tables/up.sql @@ -1,3 +1,12 @@ +-- Creates combined tables for the following: +-- +-- Reports: (comment, post, and private_message) +-- Inbox: (Comment replies, post replies, comment mentions, post mentions, private messages) +-- Modlog: (lots of types) +-- Search: (community, post, comment, user, url) +-- TODO not sure about these two: +-- Home: (comment, post) +-- Community: (comment, post) CREATE TABLE report_combined ( id serial PRIMARY KEY, published timestamptz NOT NULL, @@ -9,5 +18,26 @@ CREATE TABLE report_combined ( CREATE INDEX idx_report_combined_published ON report_combined (published DESC); --- TODO do history update +-- Updating the history +INSERT INTO report_combined (published, post_report_id) +SELECT + published, + id +FROM + post_report; + +INSERT INTO report_combined (published, comment_report_id) +SELECT + published, + id +FROM + comment_report; + +INSERT INTO report_combined (published, private_message_report_id) +SELECT + published, + id +FROM + private_message_report; + -- TODO do triggers in replaceable schema From d8dda440107acc53ef16bec09b31a290f33f7392 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 27 Nov 2024 16:37:05 -0500 Subject: [PATCH 05/43] Adding profile. --- crates/api_common/src/reports/post.rs | 1 - crates/api_common/src/reports/private_message.rs | 1 - crates/db_views/src/structs.rs | 2 +- migrations/2024-11-26-115042_add_combined_tables/up.sql | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/api_common/src/reports/post.rs b/crates/api_common/src/reports/post.rs index 25094a22a0..1ed62252c0 100644 --- a/crates/api_common/src/reports/post.rs +++ b/crates/api_common/src/reports/post.rs @@ -44,7 +44,6 @@ pub struct ListPostReports { /// Only shows the unresolved reports #[cfg_attr(feature = "full", ts(optional))] pub unresolved_only: Option, - // TODO make into tagged enum at some point /// if no community is given, it returns reports for all communities moderated by the auth user #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, diff --git a/crates/api_common/src/reports/private_message.rs b/crates/api_common/src/reports/private_message.rs index 0eb8100f0a..22cd4f9752 100644 --- a/crates/api_common/src/reports/private_message.rs +++ b/crates/api_common/src/reports/private_message.rs @@ -36,7 +36,6 @@ pub struct ResolvePrivateMessageReport { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// List private message reports. -// TODO , perhaps GetReports should be a tagged enum list too. pub struct ListPrivateMessageReports { #[cfg_attr(feature = "full", ts(optional))] pub page: Option, diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index cbd879157a..2ce42fd184 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -261,7 +261,7 @@ pub struct ReportCombinedViewInternal { // Private-message-specific pub private_message_report: Option, pub private_message: Option, - // // Shared + // Shared pub report_creator: Person, pub item_creator: Person, pub community: Option, diff --git a/migrations/2024-11-26-115042_add_combined_tables/up.sql b/migrations/2024-11-26-115042_add_combined_tables/up.sql index 1dd6490052..195856fcca 100644 --- a/migrations/2024-11-26-115042_add_combined_tables/up.sql +++ b/migrations/2024-11-26-115042_add_combined_tables/up.sql @@ -2,6 +2,7 @@ -- -- Reports: (comment, post, and private_message) -- Inbox: (Comment replies, post replies, comment mentions, post mentions, private messages) +-- Profile: (Posts and Comments) -- Modlog: (lots of types) -- Search: (community, post, comment, user, url) -- TODO not sure about these two: @@ -40,4 +41,3 @@ SELECT FROM private_message_report; --- TODO do triggers in replaceable schema From 588e1f6c0ae66ab8ab0a8cd8f58c62ad41af3c80 Mon Sep 17 00:00:00 2001 From: dullbananas Date: Mon, 2 Dec 2024 10:56:14 -0700 Subject: [PATCH 06/43] Add cursor pagination to report_combined view (#5244) * add pagination cursor * store timestamp instead of id in cursor (partial) * Revert "store timestamp instead of id in cursor (partial)" This reverts commit 89359dde4bc5fee39fdd2840828330f398444a36. * use paginated query builder --- crates/db_schema/src/newtypes.rs | 6 +- .../db_schema/src/source/combined/report.rs | 8 +- crates/db_views/src/report_combined_view.rs | 95 ++++++++++++++++--- crates/db_views/src/structs.rs | 6 ++ .../up.sql | 4 +- 5 files changed, 100 insertions(+), 19 deletions(-) diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index fe84802d76..c417ea2e45 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -89,19 +89,19 @@ pub struct PersonMentionId(i32); #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The comment report id. -pub struct CommentReportId(i32); +pub struct CommentReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The post report id. -pub struct PostReportId(i32); +pub struct PostReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The private message report id. -pub struct PrivateMessageReportId(i32); +pub struct PrivateMessageReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] diff --git a/crates/db_schema/src/source/combined/report.rs b/crates/db_schema/src/source/combined/report.rs index de0df8ad38..4085bddd69 100644 --- a/crates/db_schema/src/source/combined/report.rs +++ b/crates/db_schema/src/source/combined/report.rs @@ -2,6 +2,8 @@ use crate::newtypes::{CommentReportId, PostReportId, PrivateMessageReportId, Rep #[cfg(feature = "full")] use crate::schema::report_combined; use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -9,10 +11,14 @@ use ts_rs::TS; #[skip_serializing_none] #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] -#[cfg_attr(feature = "full", derive(Identifiable, Queryable, Selectable, TS))] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) +)] #[cfg_attr(feature = "full", diesel(table_name = report_combined))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] +#[cfg_attr(feature = "full", cursor_keys_module(name = report_combined_keys))] /// A combined reports table. pub struct ReportCombined { pub id: ReportCombinedId, diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 994361d800..4d0d3e3424 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -3,6 +3,7 @@ use crate::structs::{ LocalUserView, PostReportView, PrivateMessageReportView, + ReportCombinedPaginationCursor, ReportCombinedView, ReportCombinedViewInternal, }; @@ -13,8 +14,10 @@ use diesel::{ JoinOnDsl, NullableExpressionMethods, QueryDsl, + SelectableHelper, }; use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, newtypes::CommunityId, @@ -36,8 +39,19 @@ use lemmy_db_schema::{ private_message_report, report_combined, }, - source::community::CommunityFollower, - utils::{actions, actions_alias, functions::coalesce, get_conn, limit_and_offset, DbPool}, + source::{ + combined::report::{report_combined_keys as key, ReportCombined}, + community::CommunityFollower, + }, + utils::{ + actions, + actions_alias, + functions::coalesce, + get_conn, + limit_and_offset, + DbPool, + ReverseTimestampKey, + }, }; use lemmy_utils::error::LemmyResult; @@ -94,12 +108,47 @@ impl ReportCombinedViewInternal { } } +impl ReportCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &ReportCombinedView) -> ReportCombinedPaginationCursor { + let (prefix, id) = match view { + ReportCombinedView::Comment(v) => ('C', v.comment_report.id.0), + ReportCombinedView::Post(v) => ('P', v.post_report.id.0), + ReportCombinedView::PrivateMessage(v) => ('M', v.private_message_report.id.0), + }; + // hex encoding to prevent ossification + ReportCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = report_combined::table + .select(ReportCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(report_combined::comment_report_id.eq(id)), + "P" => query.filter(report_combined::post_report_id.eq(id)), + "M" => query.filter(report_combined::private_message_report_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(ReportCombined); + #[derive(Default)] pub struct ReportCombinedQuery { pub community_id: Option, pub page: Option, pub limit: Option, pub unresolved_only: bool, + pub page_after: Option, + pub page_back: bool, } impl ReportCombinedQuery { @@ -237,18 +286,6 @@ impl ReportCombinedQuery { query = query.filter(community::id.eq(community_id)); } - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(post_report::resolved.eq(false)) - .or_filter(comment_report::resolved.eq(false)) - .or_filter(private_message_report::resolved.eq(false)) - .order_by(report_combined::published.asc()); - } else { - query = query.order_by(report_combined::published.desc()); - } - // If its not an admin, get only the ones you mod if !user.local_user.admin { query = query.filter(community_actions::became_moderator.is_not_null()); @@ -258,6 +295,36 @@ impl ReportCombinedQuery { query = query.limit(limit).offset(offset); + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = options.page_after.map(|c| c.0); + + if options.page_back { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) + if options.unresolved_only { + query = query + .filter( + post_report::resolved + .eq(false) + .or(comment_report::resolved.eq(false)) + .or(private_message_report::resolved.eq(false)), + ) + // TODO: when a `then_asc` method is added, use it here, make the id sort direction match, + // and remove the separate index; unless additional columns are added to this sort + .then_desc(ReverseTimestampKey(key::published)); + } else { + query = query.then_desc(key::published); + } + + // Tie breaker + query = query.then_desc(key::id); + let res = query.load::(conn).await?; // Map the query results to the enum diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 2ce42fd184..cb80c5a2d1 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -126,6 +126,12 @@ pub struct PostReportView { #[cfg_attr(feature = "full", ts(export))] pub struct PaginationCursor(pub String); +/// like PaginationCursor but for the report_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(ts_rs::TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ReportCombinedPaginationCursor(pub String); + #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] diff --git a/migrations/2024-11-26-115042_add_combined_tables/up.sql b/migrations/2024-11-26-115042_add_combined_tables/up.sql index 195856fcca..ef7900ddd7 100644 --- a/migrations/2024-11-26-115042_add_combined_tables/up.sql +++ b/migrations/2024-11-26-115042_add_combined_tables/up.sql @@ -17,7 +17,9 @@ CREATE TABLE report_combined ( UNIQUE (post_report_id, comment_report_id, private_message_report_id) ); -CREATE INDEX idx_report_combined_published ON report_combined (published DESC); +CREATE INDEX idx_report_combined_published ON report_combined (published DESC, id DESC); + +CREATE INDEX idx_report_combined_published_asc ON report_combined (reverse_timestamp_sort (published) DESC, id DESC); -- Updating the history INSERT INTO report_combined (published, post_report_id) From 7fdbb58e98b52a805f4221f4a80527e6e972052c Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 13:28:26 -0500 Subject: [PATCH 07/43] Fixing migration and paged API. --- .../api/src/reports/report_combined/list.rs | 16 +++++++---- crates/api_common/src/post.rs | 2 ++ crates/api_common/src/reports/combined.rs | 12 ++++----- crates/db_views/src/report_combined_view.rs | 27 +++++-------------- .../down.sql | 0 .../up.sql | 0 src/api_routes_http.rs | 2 +- 7 files changed, 27 insertions(+), 32 deletions(-) rename migrations/{2024-11-26-115042_add_combined_tables => 2024-12-02-181601_add_combined_tables}/down.sql (100%) rename migrations/{2024-11-26-115042_add_combined_tables => 2024-12-02-181601_add_combined_tables}/up.sql (100%) diff --git a/crates/api/src/reports/report_combined/list.rs b/crates/api/src/reports/report_combined/list.rs index 7fab9919bf..12548d1891 100644 --- a/crates/api/src/reports/report_combined/list.rs +++ b/crates/api/src/reports/report_combined/list.rs @@ -16,17 +16,23 @@ pub async fn list_reports( local_user_view: LocalUserView, ) -> LemmyResult> { let community_id = data.community_id; - let unresolved_only = data.unresolved_only.unwrap_or_default(); + let unresolved_only = data.unresolved_only; check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - let page = data.page; - let limit = data.limit; + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + let reports = ReportCombinedQuery { community_id, unresolved_only, - page, - limit, + page_after, + page_back, } .list(&mut context.pool(), &local_user_view) .await?; diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 310c5c03e6..8d8d3e0802 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -116,6 +116,8 @@ pub struct GetPosts { pub no_comments_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, } #[skip_serializing_none] diff --git a/crates/api_common/src/reports/combined.rs b/crates/api_common/src/reports/combined.rs index 17d6a0505c..69d9288305 100644 --- a/crates/api_common/src/reports/combined.rs +++ b/crates/api_common/src/reports/combined.rs @@ -1,26 +1,26 @@ use lemmy_db_schema::newtypes::CommunityId; -use lemmy_db_views::structs::ReportCombinedView; +use lemmy_db_views::structs::{ReportCombinedPaginationCursor, ReportCombinedView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// List reports. pub struct ListReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, /// Only shows the unresolved reports #[cfg_attr(feature = "full", ts(optional))] pub unresolved_only: Option, /// if no community is given, it returns reports for all communities moderated by the auth user #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 4d0d3e3424..55bf533a9a 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -43,15 +43,7 @@ use lemmy_db_schema::{ combined::report::{report_combined_keys as key, ReportCombined}, community::CommunityFollower, }, - utils::{ - actions, - actions_alias, - functions::coalesce, - get_conn, - limit_and_offset, - DbPool, - ReverseTimestampKey, - }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, }; use lemmy_utils::error::LemmyResult; @@ -119,6 +111,7 @@ impl ReportCombinedPaginationCursor { // hex encoding to prevent ossification ReportCombinedPaginationCursor(format!("{prefix}{id:x}")) } + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); let mut query = report_combined::table @@ -144,11 +137,9 @@ pub struct PaginationCursorData(ReportCombined); #[derive(Default)] pub struct ReportCombinedQuery { pub community_id: Option, - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, + pub unresolved_only: Option, pub page_after: Option, - pub page_back: bool, + pub page_back: Option, } impl ReportCombinedQuery { @@ -291,15 +282,11 @@ impl ReportCombinedQuery { query = query.filter(community_actions::became_moderator.is_not_null()); } - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query.limit(limit).offset(offset); - let mut query = PaginatedQueryBuilder::new(query); let page_after = options.page_after.map(|c| c.0); - if options.page_back { + if options.page_back.unwrap_or_default() { query = query.before(page_after).limit_and_offset_from_end(); } else { query = query.after(page_after); @@ -307,7 +294,7 @@ impl ReportCombinedQuery { // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest // first (FIFO) - if options.unresolved_only { + if options.unresolved_only.unwrap_or_default() { query = query .filter( post_report::resolved @@ -601,7 +588,7 @@ mod tests { // Do a batch read of timmys reports // It should only show saras, which is unresolved let reports_after_resolve = ReportCombinedQuery { - unresolved_only: true, + unresolved_only: Some(true), ..Default::default() } .list(pool, &timmy_view) diff --git a/migrations/2024-11-26-115042_add_combined_tables/down.sql b/migrations/2024-12-02-181601_add_combined_tables/down.sql similarity index 100% rename from migrations/2024-11-26-115042_add_combined_tables/down.sql rename to migrations/2024-12-02-181601_add_combined_tables/down.sql diff --git a/migrations/2024-11-26-115042_add_combined_tables/up.sql b/migrations/2024-12-02-181601_add_combined_tables/up.sql similarity index 100% rename from migrations/2024-11-26-115042_add_combined_tables/up.sql rename to migrations/2024-12-02-181601_add_combined_tables/up.sql diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index baf45f5f90..452fb98707 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -286,7 +286,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .service( scope("report") .wrap(rate_limit.message()) - .route("/list", web::get().to(list_reports)), + .route("/list", get().to(list_reports)), ) // Private Message .service( From 00beccd272edd19174ef05bf31d596d90bb03245 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 13:46:51 -0500 Subject: [PATCH 08/43] Using dullbananas trigger procedure --- .../db_schema/replaceable_schema/triggers.sql | 72 +++++++------------ 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 58862e66d8..2d9b0df6e5 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -655,55 +655,33 @@ CREATE TRIGGER change_values -- Combined tables triggers -- These insert (published, item_id) into X_combined tables --- Reports --- Comment -CREATE FUNCTION r.report_combined_comment_report_insert () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - INSERT INTO report_combined (published, comment_report_id) - VALUES (NEW.published, NEW.id); - RETURN NEW; -END -$$; - -CREATE TRIGGER report_combined_comment - AFTER INSERT ON comment_report - FOR EACH ROW - EXECUTE FUNCTION r.report_combined_comment_report_insert (); - --- Post -CREATE FUNCTION r.report_combined_post_report_insert () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ +-- Reports (comment_report, post_report, private_message_report) +CREATE PROCEDURE r.create_report_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ BEGIN - INSERT INTO report_combined (published, post_report_id) - VALUES (NEW.published, NEW.id); - RETURN NEW; -END -$$; + EXECUTE replace($b$ CREATE FUNCTION r.report_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO report_combined (published, thing_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER report_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.report_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; -CREATE TRIGGER report_combined_post - AFTER INSERT ON post_report - FOR EACH ROW - EXECUTE FUNCTION r.report_combined_post_report_insert (); +CALL r.create_report_combined_trigger ('post_report'); --- Private message -CREATE FUNCTION r.report_combined_private_message_report_insert () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - INSERT INTO report_combined (published, private_message_report_id) - VALUES (NEW.published, NEW.id); - RETURN NEW; -END -$$; +CALL r.create_report_combined_trigger ('comment_report'); -CREATE TRIGGER report_combined_private_message - AFTER INSERT ON private_message_report - FOR EACH ROW - EXECUTE FUNCTION r.report_combined_private_message_report_insert (); +CALL r.create_report_combined_trigger ('private_message_report'); From 1776de2f45b698111ad412858aceffb38e754e56 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 16:10:59 -0500 Subject: [PATCH 09/43] Removing pointless list routes, reorganizing tests. --- crates/api/src/reports/comment_report/list.rs | 37 -- crates/api/src/reports/comment_report/mod.rs | 1 - crates/api/src/reports/post_report/list.rs | 37 -- crates/api/src/reports/post_report/mod.rs | 1 - .../reports/private_message_report/list.rs | 35 -- .../src/reports/private_message_report/mod.rs | 1 - crates/db_views/src/comment_report_view.rs | 489 +----------------- crates/db_views/src/post_report_view.rs | 356 +------------ .../src/private_message_report_view.rs | 195 +------ crates/db_views/src/report_combined_view.rs | 444 ++++++++++++++-- src/api_routes_http.rs | 25 +- 11 files changed, 462 insertions(+), 1159 deletions(-) delete mode 100644 crates/api/src/reports/comment_report/list.rs delete mode 100644 crates/api/src/reports/post_report/list.rs delete mode 100644 crates/api/src/reports/private_message_report/list.rs diff --git a/crates/api/src/reports/comment_report/list.rs b/crates/api/src/reports/comment_report/list.rs deleted file mode 100644 index f4a6116989..0000000000 --- a/crates/api/src/reports/comment_report/list.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - reports::comment::{ListCommentReports, ListCommentReportsResponse}, - utils::check_community_mod_of_any_or_admin_action, -}; -use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView}; -use lemmy_utils::error::LemmyResult; - -/// Lists comment reports for a community if an id is supplied -/// or returns all comment reports for communities a user moderates -#[tracing::instrument(skip(context))] -pub async fn list_comment_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let community_id = data.community_id; - let comment_id = data.comment_id; - let unresolved_only = data.unresolved_only.unwrap_or_default(); - - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - - let page = data.page; - let limit = data.limit; - let comment_reports = CommentReportQuery { - community_id, - comment_id, - unresolved_only, - page, - limit, - } - .list(&mut context.pool(), &local_user_view) - .await?; - - Ok(Json(ListCommentReportsResponse { comment_reports })) -} diff --git a/crates/api/src/reports/comment_report/mod.rs b/crates/api/src/reports/comment_report/mod.rs index 3bb1a9b46f..c85613aa60 100644 --- a/crates/api/src/reports/comment_report/mod.rs +++ b/crates/api/src/reports/comment_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/api/src/reports/post_report/list.rs b/crates/api/src/reports/post_report/list.rs deleted file mode 100644 index da3c0cd947..0000000000 --- a/crates/api/src/reports/post_report/list.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - reports::post::{ListPostReports, ListPostReportsResponse}, - utils::check_community_mod_of_any_or_admin_action, -}; -use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView}; -use lemmy_utils::error::LemmyResult; - -/// Lists post reports for a community if an id is supplied -/// or returns all post reports for communities a user moderates -#[tracing::instrument(skip(context))] -pub async fn list_post_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let community_id = data.community_id; - let post_id = data.post_id; - let unresolved_only = data.unresolved_only.unwrap_or_default(); - - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - - let page = data.page; - let limit = data.limit; - let post_reports = PostReportQuery { - community_id, - post_id, - unresolved_only, - page, - limit, - } - .list(&mut context.pool(), &local_user_view) - .await?; - - Ok(Json(ListPostReportsResponse { post_reports })) -} diff --git a/crates/api/src/reports/post_report/mod.rs b/crates/api/src/reports/post_report/mod.rs index 3bb1a9b46f..c85613aa60 100644 --- a/crates/api/src/reports/post_report/mod.rs +++ b/crates/api/src/reports/post_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/api/src/reports/private_message_report/list.rs b/crates/api/src/reports/private_message_report/list.rs deleted file mode 100644 index 61dbd6b975..0000000000 --- a/crates/api/src/reports/private_message_report/list.rs +++ /dev/null @@ -1,35 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - reports::private_message::{ListPrivateMessageReports, ListPrivateMessageReportsResponse}, - utils::is_admin, -}; -use lemmy_db_views::{ - private_message_report_view::PrivateMessageReportQuery, - structs::LocalUserView, -}; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn list_pm_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - is_admin(&local_user_view)?; - - let unresolved_only = data.unresolved_only.unwrap_or_default(); - let page = data.page; - let limit = data.limit; - let private_message_reports = PrivateMessageReportQuery { - unresolved_only, - page, - limit, - } - .list(&mut context.pool()) - .await?; - - Ok(Json(ListPrivateMessageReportsResponse { - private_message_reports, - })) -} diff --git a/crates/api/src/reports/private_message_report/mod.rs b/crates/api/src/reports/private_message_report/mod.rs index 3bb1a9b46f..c85613aa60 100644 --- a/crates/api/src/reports/private_message_report/mod.rs +++ b/crates/api/src/reports/private_message_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index b4a23a0da6..6154b9b565 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -1,7 +1,6 @@ -use crate::structs::{CommentReportView, LocalUserView}; +use crate::structs::CommentReportView; use diesel::{ dsl::now, - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -12,7 +11,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::{CommentId, CommentReportId, CommunityId, PersonId}, + newtypes::{CommentReportId, PersonId}, schema::{ comment, comment_actions, @@ -26,26 +25,21 @@ use lemmy_db_schema::{ post, }, source::community::CommunityFollower, - utils::{ - actions, - actions_alias, - functions::coalesce, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, CommentReportView, (CommentReportId, PersonId)>, - impl ListFn<'a, CommentReportView, (CommentReportQuery, &'a LocalUserView)>, -> { - let all_joins = |query: comment_report::BoxedQuery<'a, Pg>, my_person_id: PersonId| { - query +impl CommentReportView { + /// returns the CommentReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: CommentReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + comment_report::table + .find(report_id) .inner_join(comment::table) .inner_join(post::table.on(comment::post_id.eq(post::id))) .inner_join(community::table.on(post::community_id.eq(community::id))) @@ -117,456 +111,7 @@ fn queries<'a>() -> Queries< comment_actions::like_score.nullable(), aliases::person2.fields(person::all_columns).nullable(), )) - }; - - let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (CommentReportId, PersonId)| async move { - all_joins( - comment_report::table.find(report_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, - (options, user): (CommentReportQuery, &'a LocalUserView)| async move { - let mut query = all_joins(comment_report::table.into_boxed(), user.person.id); - - if let Some(community_id) = options.community_id { - query = query.filter(post::community_id.eq(community_id)); - } - - if let Some(comment_id) = options.comment_id { - query = query.filter(comment_report::comment_id.eq(comment_id)); - } - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(comment_report::resolved.eq(false)) - .order_by(comment_report::published.asc()); - } else { - query = query.order_by(comment_report::published.desc()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query.limit(limit).offset(offset); - - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); - } - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl CommentReportView { - /// returns the CommentReportView for the provided report_id - /// - /// * `report_id` - the report id to obtain - pub async fn read( - pool: &mut DbPool<'_>, - report_id: CommentReportId, - my_person_id: PersonId, - ) -> Result { - queries().read(pool, (report_id, my_person_id)).await - } - - /// Returns the current unresolved comment report count for the communities you mod - pub async fn get_report_count( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - admin: bool, - community_id: Option, - ) -> Result { - use diesel::dsl::count; - - let conn = &mut get_conn(pool).await?; - - let mut query = comment_report::table - .inner_join(comment::table) - .inner_join(post::table.on(comment::post_id.eq(post::id))) - .filter(comment_report::resolved.eq(false)) - .into_boxed(); - - if let Some(community_id) = community_id { - query = query.filter(post::community_id.eq(community_id)) - } - - // If its not an admin, get only the ones you mod - if !admin { - query - .inner_join( - community_actions::table.on( - community_actions::community_id - .eq(post::community_id) - .and(community_actions::person_id.eq(my_person_id)) - .and(community_actions::became_moderator.is_not_null()), - ), - ) - .select(count(comment_report::id)) - .first::(conn) - .await - } else { - query - .select(count(comment_report::id)) - .first::(conn) - .await - } - } -} - -#[derive(Default)] -pub struct CommentReportQuery { - pub community_id: Option, - pub comment_id: Option, - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl CommentReportQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - user: &LocalUserView, - ) -> Result, Error> { - queries().list(pool, (self, user)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::{ - comment_report_view::{CommentReportQuery, CommentReportView}, - structs::LocalUserView, - }; - use lemmy_db_schema::{ - aggregates::structs::CommentAggregates, - source::{ - comment::{Comment, CommentInsertForm}, - comment_report::{CommentReport, CommentReportForm}, - community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - }, - traits::{Crud, Joinable, Reportable}, - utils::{build_db_pool_for_tests, RANK_DEFAULT}, - CommunityVisibility, - SubscribedType, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_crv"); - - let inserted_timmy = Person::create(pool, &new_person).await?; - - let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; - let timmy_view = LocalUserView { - local_user: timmy_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_timmy.clone(), - counts: Default::default(), - }; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_crv"); - - let inserted_sara = Person::create(pool, &new_person_2).await?; - - // Add a third person, since new ppl can only report something once. - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_crv"); - - let inserted_jessica = Person::create(pool, &new_person_3).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test community crv".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - // Make timmy a mod - let timmy_moderator_form = CommunityModeratorForm { - community_id: inserted_community.id, - person_id: inserted_timmy.id, - }; - - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; - - let new_post = PostInsertForm::new( - "A test post crv".into(), - inserted_timmy.id, - inserted_community.id, - ); - - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_timmy.id, - inserted_post.id, - "A test comment 32".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - // sara reports - let sara_report_form = CommentReportForm { - creator_id: inserted_sara.id, - comment_id: inserted_comment.id, - original_comment_text: "this was it at time of creation".into(), - reason: "from sara".into(), - }; - - let inserted_sara_report = CommentReport::report(pool, &sara_report_form).await?; - - // jessica reports - let jessica_report_form = CommentReportForm { - creator_id: inserted_jessica.id, - comment_id: inserted_comment.id, - original_comment_text: "this was it at time of creation".into(), - reason: "from jessica".into(), - }; - - let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; - - let agg = CommentAggregates::read(pool, inserted_comment.id).await?; - - let read_jessica_report_view = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - let expected_jessica_report_view = CommentReportView { - comment_report: inserted_jessica_report.clone(), - comment: inserted_comment.clone(), - post: inserted_post, - creator_is_moderator: true, - creator_is_admin: false, - creator_blocked: false, - subscribed: SubscribedType::NotSubscribed, - saved: false, - community: Community { - id: inserted_community.id, - name: inserted_community.name, - icon: None, - removed: false, - deleted: false, - nsfw: false, - actor_id: inserted_community.actor_id.clone(), - local: true, - title: inserted_community.title, - sidebar: None, - description: None, - updated: None, - banner: None, - hidden: false, - posting_restricted_to_mods: false, - published: inserted_community.published, - private_key: inserted_community.private_key, - public_key: inserted_community.public_key, - last_refreshed_at: inserted_community.last_refreshed_at, - followers_url: inserted_community.followers_url, - inbox_url: inserted_community.inbox_url, - moderators_url: inserted_community.moderators_url, - featured_url: inserted_community.featured_url, - instance_id: inserted_instance.id, - visibility: CommunityVisibility::Public, - }, - creator: Person { - id: inserted_jessica.id, - name: inserted_jessica.name, - display_name: None, - published: inserted_jessica.published, - avatar: None, - actor_id: inserted_jessica.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_jessica.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_jessica.private_key, - public_key: inserted_jessica.public_key, - last_refreshed_at: inserted_jessica.last_refreshed_at, - }, - comment_creator: Person { - id: inserted_timmy.id, - name: inserted_timmy.name.clone(), - display_name: None, - published: inserted_timmy.published, - avatar: None, - actor_id: inserted_timmy.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_timmy.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_timmy.private_key.clone(), - public_key: inserted_timmy.public_key.clone(), - last_refreshed_at: inserted_timmy.last_refreshed_at, - }, - creator_banned_from_community: false, - counts: CommentAggregates { - comment_id: inserted_comment.id, - score: 0, - upvotes: 0, - downvotes: 0, - published: agg.published, - child_count: 0, - hot_rank: RANK_DEFAULT, - controversy_rank: 0.0, - report_count: 2, - unresolved_report_count: 2, - }, - my_vote: None, - resolver: None, - }; - - assert_eq!(read_jessica_report_view, expected_jessica_report_view); - - let mut expected_sara_report_view = expected_jessica_report_view.clone(); - expected_sara_report_view.comment_report = inserted_sara_report; - expected_sara_report_view.creator = Person { - id: inserted_sara.id, - name: inserted_sara.name, - display_name: None, - published: inserted_sara.published, - avatar: None, - actor_id: inserted_sara.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_sara.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_sara.private_key, - public_key: inserted_sara.public_key, - last_refreshed_at: inserted_sara.last_refreshed_at, - }; - - // Do a batch read of timmys reports - let reports = CommentReportQuery::default() - .list(pool, &timmy_view) - .await?; - - assert_eq!( - reports, - [ - expected_jessica_report_view.clone(), - expected_sara_report_view.clone(), - ] - ); - - // Make sure the counts are correct - let report_count = - CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(2, report_count); - - // Try to resolve the report - CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - let read_jessica_report_view_after_resolve = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - - let mut expected_jessica_report_view_after_resolve = expected_jessica_report_view; - expected_jessica_report_view_after_resolve - .comment_report - .resolved = true; - expected_jessica_report_view_after_resolve - .comment_report - .resolver_id = Some(inserted_timmy.id); - expected_jessica_report_view_after_resolve - .comment_report - .updated = read_jessica_report_view_after_resolve - .comment_report - .updated; - expected_jessica_report_view_after_resolve - .counts - .unresolved_report_count = 1; - expected_sara_report_view.counts.unresolved_report_count = 1; - expected_jessica_report_view_after_resolve.resolver = Some(Person { - id: inserted_timmy.id, - name: inserted_timmy.name.clone(), - display_name: None, - published: inserted_timmy.published, - avatar: None, - actor_id: inserted_timmy.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_timmy.inbox_url.clone(), - private_key: inserted_timmy.private_key.clone(), - public_key: inserted_timmy.public_key.clone(), - last_refreshed_at: inserted_timmy.last_refreshed_at, - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - }); - - assert_eq!( - read_jessica_report_view_after_resolve, - expected_jessica_report_view_after_resolve - ); - - // Do a batch read of timmys reports - // It should only show saras, which is unresolved - let reports_after_resolve = CommentReportQuery { - unresolved_only: (true), - ..Default::default() - } - .list(pool, &timmy_view) - .await?; - assert_eq!(reports_after_resolve[0], expected_sara_report_view); - assert_eq!(reports_after_resolve.len(), 1); - - // Make sure the counts are correct - let report_count_after_resolved = - CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(1, report_count_after_resolved); - - Person::delete(pool, inserted_timmy.id).await?; - Person::delete(pool, inserted_sara.id).await?; - Person::delete(pool, inserted_jessica.id).await?; - Community::delete(pool, inserted_community.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) + .first(conn) + .await } } diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index c530c97398..4c7fd676c4 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -1,6 +1,5 @@ -use crate::structs::{LocalUserView, PostReportView}; +use crate::structs::PostReportView; use diesel::{ - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -11,7 +10,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::{CommunityId, PersonId, PostId, PostReportId}, + newtypes::{PersonId, PostReportId}, schema::{ community, community_actions, @@ -24,26 +23,22 @@ use lemmy_db_schema::{ post_report, }, source::community::CommunityFollower, - utils::{ - actions, - actions_alias, - functions::coalesce, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, PostReportView, (PostReportId, PersonId)>, - impl ListFn<'a, PostReportView, (PostReportQuery, &'a LocalUserView)>, -> { - let all_joins = |query: post_report::BoxedQuery<'a, Pg>, my_person_id: PersonId| { - query +impl PostReportView { + /// returns the PostReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: PostReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + post_report::table + .find(report_id) .inner_join(post::table) .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(person::table.on(post_report::creator_id.eq(person::id))) @@ -104,322 +99,7 @@ fn queries<'a>() -> Queries< post_aggregates::all_columns, aliases::person2.fields(person::all_columns.nullable()), )) - }; - - let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (PostReportId, PersonId)| async move { - all_joins( - post_report::table.find(report_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, (options, user): (PostReportQuery, &'a LocalUserView)| async move { - let mut query = all_joins(post_report::table.into_boxed(), user.person.id); - - if let Some(community_id) = options.community_id { - query = query.filter(post::community_id.eq(community_id)); - } - - if let Some(post_id) = options.post_id { - query = query.filter(post::id.eq(post_id)); - } - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(post_report::resolved.eq(false)) - .order_by(post_report::published.asc()); - } else { - query = query.order_by(post_report::published.desc()); - } - - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query.limit(limit).offset(offset); - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl PostReportView { - /// returns the PostReportView for the provided report_id - /// - /// * `report_id` - the report id to obtain - pub async fn read( - pool: &mut DbPool<'_>, - report_id: PostReportId, - my_person_id: PersonId, - ) -> Result { - queries().read(pool, (report_id, my_person_id)).await - } - - /// returns the current unresolved post report count for the communities you mod - pub async fn get_report_count( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - admin: bool, - community_id: Option, - ) -> Result { - use diesel::dsl::count; - let conn = &mut get_conn(pool).await?; - let mut query = post_report::table - .inner_join(post::table) - .filter(post_report::resolved.eq(false)) - .into_boxed(); - - if let Some(community_id) = community_id { - query = query.filter(post::community_id.eq(community_id)) - } - - // If its not an admin, get only the ones you mod - if !admin { - query - .inner_join( - community_actions::table.on( - community_actions::community_id - .eq(post::community_id) - .and(community_actions::person_id.eq(my_person_id)) - .and(community_actions::became_moderator.is_not_null()), - ), - ) - .select(count(post_report::id)) - .first::(conn) - .await - } else { - query - .select(count(post_report::id)) - .first::(conn) - .await - } - } -} - -#[derive(Default)] -pub struct PostReportQuery { - pub community_id: Option, - pub post_id: Option, - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl PostReportQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - user: &LocalUserView, - ) -> Result, Error> { - queries().list(pool, (self, user)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::{ - post_report_view::{PostReportQuery, PostReportView}, - structs::LocalUserView, - }; - use lemmy_db_schema::{ - aggregates::structs::PostAggregates, - assert_length, - source::{ - community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - post_report::{PostReport, PostReportForm}, - }, - traits::{Crud, Joinable, Reportable}, - utils::build_db_pool_for_tests, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_prv"); - - let inserted_timmy = Person::create(pool, &new_person).await?; - - let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; - let timmy_view = LocalUserView { - local_user: timmy_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_timmy.clone(), - counts: Default::default(), - }; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_prv"); - - let inserted_sara = Person::create(pool, &new_person_2).await?; - - // Add a third person, since new ppl can only report something once. - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_prv"); - - let inserted_jessica = Person::create(pool, &new_person_3).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test community prv".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - // Make timmy a mod - let timmy_moderator_form = CommunityModeratorForm { - community_id: inserted_community.id, - person_id: inserted_timmy.id, - }; - - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; - - let new_post = PostInsertForm::new( - "A test post crv".into(), - inserted_timmy.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - // sara reports - let sara_report_form = PostReportForm { - creator_id: inserted_sara.id, - post_id: inserted_post.id, - original_post_name: "Orig post".into(), - original_post_url: None, - original_post_body: None, - reason: "from sara".into(), - }; - - PostReport::report(pool, &sara_report_form).await?; - - let new_post_2 = PostInsertForm::new( - "A test post crv 2".into(), - inserted_timmy.id, - inserted_community.id, - ); - let inserted_post_2 = Post::create(pool, &new_post_2).await?; - - // jessica reports - let jessica_report_form = PostReportForm { - creator_id: inserted_jessica.id, - post_id: inserted_post_2.id, - original_post_name: "Orig post".into(), - original_post_url: None, - original_post_body: None, - reason: "from jessica".into(), - }; - - let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; - - let read_jessica_report_view = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - - // Make sure the triggers are reading the aggregates correctly. - let agg_1 = PostAggregates::read(pool, inserted_post.id).await?; - let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?; - - assert_eq!( - read_jessica_report_view.post_report, - inserted_jessica_report - ); - assert_eq!(read_jessica_report_view.post, inserted_post_2); - assert_eq!(read_jessica_report_view.community.id, inserted_community.id); - assert_eq!(read_jessica_report_view.creator.id, inserted_jessica.id); - assert_eq!(read_jessica_report_view.post_creator.id, inserted_timmy.id); - assert_eq!(read_jessica_report_view.my_vote, None); - assert_eq!(read_jessica_report_view.resolver, None); - assert_eq!(agg_1.report_count, 1); - assert_eq!(agg_1.unresolved_report_count, 1); - assert_eq!(agg_2.report_count, 1); - assert_eq!(agg_2.unresolved_report_count, 1); - - // Do a batch read of timmys reports - let reports = PostReportQuery::default().list(pool, &timmy_view).await?; - - assert_eq!(reports[1].creator.id, inserted_sara.id); - assert_eq!(reports[0].creator.id, inserted_jessica.id); - - // Make sure the counts are correct - let report_count = - PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(2, report_count); - - // Pretend the post was removed, and resolve all reports for that object. - // This is called manually in the API for post removals - PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, inserted_timmy.id) - .await?; - - let read_jessica_report_view_after_resolve = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - assert!(read_jessica_report_view_after_resolve.post_report.resolved); - assert_eq!( - read_jessica_report_view_after_resolve - .post_report - .resolver_id, - Some(inserted_timmy.id) - ); - assert_eq!( - read_jessica_report_view_after_resolve - .resolver - .map(|r| r.id), - Some(inserted_timmy.id) - ); - - // Make sure the unresolved_post report got decremented in the trigger - let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?; - assert_eq!(agg_2.report_count, 1); - assert_eq!(agg_2.unresolved_report_count, 0); - - // Make sure the other unresolved report isn't changed - let agg_1 = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(agg_1.report_count, 1); - assert_eq!(agg_1.unresolved_report_count, 1); - - // Do a batch read of timmys reports - // It should only show saras, which is unresolved - let reports_after_resolve = PostReportQuery { - unresolved_only: true, - ..Default::default() - } - .list(pool, &timmy_view) - .await?; - assert_length!(1, reports_after_resolve); - assert_eq!(reports_after_resolve[0].creator.id, inserted_sara.id); - - // Make sure the counts are correct - let report_count_after_resolved = - PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(1, report_count_after_resolved); - - Person::delete(pool, inserted_timmy.id).await?; - Person::delete(pool, inserted_sara.id).await?; - Person::delete(pool, inserted_jessica.id).await?; - Community::delete(pool, inserted_community.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) + .first(conn) + .await } } diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index e59d996089..956ccf0e1b 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -1,76 +1,13 @@ use crate::structs::PrivateMessageReportView; -use diesel::{ - pg::Pg, - result::Error, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; +use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, newtypes::PrivateMessageReportId, schema::{person, private_message, private_message_report}, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, PrivateMessageReportView, PrivateMessageReportId>, - impl ListFn<'a, PrivateMessageReportView, PrivateMessageReportQuery>, -> { - let all_joins = - |query: private_message_report::BoxedQuery<'a, Pg>| { - query - .inner_join(private_message::table) - .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .inner_join( - aliases::person1 - .on(private_message_report::creator_id.eq(aliases::person1.field(person::id))), - ) - .left_join(aliases::person2.on( - private_message_report::resolver_id.eq(aliases::person2.field(person::id).nullable()), - )) - .select(( - private_message_report::all_columns, - private_message::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - aliases::person2.fields(person::all_columns).nullable(), - )) - }; - - let read = move |mut conn: DbConn<'a>, report_id: PrivateMessageReportId| async move { - all_joins(private_message_report::table.find(report_id).into_boxed()) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, options: PrivateMessageReportQuery| async move { - let mut query = all_joins(private_message_report::table.into_boxed()); - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(private_message_report::resolved.eq(false)) - .order_by(private_message_report::published.asc()); - } else { - query = query.order_by(private_message_report::published.desc()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query - .limit(limit) - .offset(offset) - .load::(&mut conn) - .await - }; - - Queries::new(read, list) -} - impl PrivateMessageReportView { /// returns the PrivateMessageReportView for the provided report_id /// @@ -79,118 +16,28 @@ impl PrivateMessageReportView { pool: &mut DbPool<'_>, report_id: PrivateMessageReportId, ) -> Result { - queries().read(pool, report_id).await - } - - /// Returns the current unresolved post report count for the communities you mod - pub async fn get_report_count(pool: &mut DbPool<'_>) -> Result { - use diesel::dsl::count; let conn = &mut get_conn(pool).await?; - private_message_report::table + .find(report_id) .inner_join(private_message::table) - .filter(private_message_report::resolved.eq(false)) - .into_boxed() - .select(count(private_message_report::id)) - .first::(conn) + .inner_join(person::table.on(private_message::creator_id.eq(person::id))) + .inner_join( + aliases::person1 + .on(private_message_report::creator_id.eq(aliases::person1.field(person::id))), + ) + .left_join( + aliases::person2.on( + private_message_report::resolver_id.eq(aliases::person2.field(person::id).nullable()), + ), + ) + .select(( + private_message_report::all_columns, + private_message::all_columns, + person::all_columns, + aliases::person1.fields(person::all_columns), + aliases::person2.fields(person::all_columns).nullable(), + )) + .first(conn) .await } } - -#[derive(Default)] -pub struct PrivateMessageReportQuery { - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl PrivateMessageReportQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, self).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::private_message_report_view::PrivateMessageReportQuery; - use lemmy_db_schema::{ - assert_length, - source::{ - instance::Instance, - person::{Person, PersonInsertForm}, - private_message::{PrivateMessage, PrivateMessageInsertForm}, - private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, - }, - traits::{Crud, Reportable}, - utils::build_db_pool_for_tests, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person_1 = PersonInsertForm::test_form(inserted_instance.id, "timmy_mrv"); - let inserted_timmy = Person::create(pool, &new_person_1).await?; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); - let inserted_jessica = Person::create(pool, &new_person_2).await?; - - // timmy sends private message to jessica - let pm_form = PrivateMessageInsertForm::new( - inserted_timmy.id, - inserted_jessica.id, - "something offensive".to_string(), - ); - let pm = PrivateMessage::create(pool, &pm_form).await?; - - // jessica reports private message - let pm_report_form = PrivateMessageReportForm { - creator_id: inserted_jessica.id, - original_pm_text: pm.content.clone(), - private_message_id: pm.id, - reason: "its offensive".to_string(), - }; - let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; - - let reports = PrivateMessageReportQuery::default().list(pool).await?; - assert_length!(1, reports); - assert!(!reports[0].private_message_report.resolved); - assert_eq!(inserted_timmy.name, reports[0].private_message_creator.name); - assert_eq!(inserted_jessica.name, reports[0].creator.name); - assert_eq!(pm_report.reason, reports[0].private_message_report.reason); - assert_eq!(pm.content, reports[0].private_message.content); - - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "admin_mrv"); - let inserted_admin = Person::create(pool, &new_person_3).await?; - - // admin resolves the report (after taking appropriate action) - PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id).await?; - - let reports = PrivateMessageReportQuery { - unresolved_only: (false), - ..Default::default() - } - .list(pool) - .await?; - assert_length!(1, reports); - assert!(reports[0].private_message_report.resolved); - assert!(reports[0].resolver.is_some()); - assert_eq!( - Some(&inserted_admin.name), - reports[0].resolver.as_ref().map(|r| &r.name) - ); - - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } -} diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 55bf533a9a..b1232940f4 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -151,6 +151,8 @@ impl ReportCombinedQuery { let options = self; let my_person_id = user.local_user.person_id; let item_creator = aliases::person1.field(person::id); + + let resolver = aliases::person2.field(person::id).nullable(); let conn = &mut get_conn(pool).await?; // Notes: since the post_report_id and comment_report_id are optional columns, @@ -225,7 +227,15 @@ impl ReportCombinedQuery { .left_join( comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), ) - .left_join(aliases::person2.on(item_creator.eq(aliases::person2.field(person::id)))) + // The resolver + .left_join( + aliases::person2.on( + private_message_report::resolver_id + .eq(resolver) + .or(post_report::resolver_id.eq(resolver)) + .or(comment_report::resolver_id.eq(resolver)), + ), + ) .left_join(actions( comment_actions::table, Some(my_person_id), @@ -399,9 +409,16 @@ mod tests { use crate::{ report_combined_view::ReportCombinedQuery, - structs::{LocalUserView, ReportCombinedView, ReportCombinedViewInternal}, + structs::{ + CommentReportView, + LocalUserView, + PostReportView, + ReportCombinedView, + ReportCombinedViewInternal, + }, }; use lemmy_db_schema::{ + aggregates::structs::{CommentAggregates, PostAggregates}, assert_length, source::{ comment::{Comment, CommentInsertForm}, @@ -417,18 +434,26 @@ mod tests { private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, }, traits::{Crud, Joinable, Reportable}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, DbPool}, }; use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); + struct Data { + instance: Instance, + timmy: Person, + sara: Person, + jessica: Person, + timmy_view: LocalUserView, + admin_view: LocalUserView, + community: Community, + post: Post, + post_2: Post, + comment: Comment, + } + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; let timmy_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rcv"); @@ -457,6 +482,9 @@ mod tests { let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rcv"); let inserted_sara = Person::create(pool, &sara_form).await?; + let jessica_form = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); + let inserted_jessica = Person::create(pool, &jessica_form).await?; + let community_form = CommunityInsertForm::new( inserted_instance.id, "test community crv".to_string(), @@ -479,16 +507,12 @@ mod tests { ); let inserted_post = Post::create(pool, &post_form).await?; - // sara reports the post - let sara_report_post_form = PostReportForm { - creator_id: inserted_sara.id, - post_id: inserted_post.id, - original_post_name: "Orig post".into(), - original_post_url: None, - original_post_body: None, - reason: "from sara".into(), - }; - let inserted_post_report = PostReport::report(pool, &sara_report_post_form).await?; + let new_post_2 = PostInsertForm::new( + "A test post crv 2".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post_2 = Post::create(pool, &new_post_2).await?; // Timmy creates a comment let comment_form = CommentInsertForm::new( @@ -498,10 +522,48 @@ mod tests { ); let inserted_comment = Comment::create(pool, &comment_form, None).await?; + Ok(Data { + instance: inserted_instance, + timmy: inserted_timmy, + sara: inserted_sara, + jessica: inserted_jessica, + admin_view, + timmy_view, + community: inserted_community, + post: inserted_post, + post_2: inserted_post_2, + comment: inserted_comment, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports the post + let sara_report_post_form = PostReportForm { + creator_id: data.sara.id, + post_id: data.post.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from sara".into(), + }; + let inserted_post_report = PostReport::report(pool, &sara_report_post_form).await?; + // Sara reports the comment let sara_report_comment_form = CommentReportForm { - creator_id: inserted_sara.id, - comment_id: inserted_comment.id, + creator_id: data.sara.id, + comment_id: data.comment.id, original_comment_text: "A test comment rv".into(), reason: "from sara".into(), }; @@ -509,15 +571,15 @@ mod tests { // Timmy creates a private message report let pm_form = PrivateMessageInsertForm::new( - inserted_timmy.id, - inserted_sara.id, + data.timmy.id, + data.sara.id, "something offensive crv".to_string(), ); let inserted_pm = PrivateMessage::create(pool, &pm_form).await?; // sara reports private message let pm_report_form = PrivateMessageReportForm { - creator_id: inserted_sara.id, + creator_id: data.sara.id, original_pm_text: inserted_pm.content.clone(), private_message_id: inserted_pm.id, reason: "its offensive".to_string(), @@ -526,22 +588,22 @@ mod tests { // Do a batch read of admins reports let reports = ReportCombinedQuery::default() - .list(pool, &admin_view) + .list(pool, &data.admin_view) .await?; assert_eq!(3, reports.len()); // Make sure the report types are correct if let ReportCombinedView::Post(v) = &reports[2] { - assert_eq!(inserted_post.id, v.post.id); - assert_eq!(inserted_sara.id, v.creator.id); - assert_eq!(inserted_timmy.id, v.post_creator.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy.id, v.post_creator.id); } else { panic!("wrong type"); } if let ReportCombinedView::Comment(v) = &reports[1] { - assert_eq!(inserted_comment.id, v.comment.id); - assert_eq!(inserted_post.id, v.post.id); - assert_eq!(inserted_timmy.id, v.comment_creator.id); + assert_eq!(data.comment.id, v.comment.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.comment_creator.id); } else { panic!("wrong type"); } @@ -552,38 +614,38 @@ mod tests { } let report_count_admin = - ReportCombinedViewInternal::get_report_count(pool, &admin_view, None).await?; + ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?; assert_eq!(3, report_count_admin); // Timmy should only see 2 reports, since they're not an admin, // but they do mod the community let reports = ReportCombinedQuery::default() - .list(pool, &timmy_view) + .list(pool, &data.timmy_view) .await?; assert_eq!(2, reports.len()); // Make sure the report types are correct if let ReportCombinedView::Post(v) = &reports[1] { - assert_eq!(inserted_post.id, v.post.id); - assert_eq!(inserted_sara.id, v.creator.id); - assert_eq!(inserted_timmy.id, v.post_creator.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy.id, v.post_creator.id); } else { panic!("wrong type"); } if let ReportCombinedView::Comment(v) = &reports[0] { - assert_eq!(inserted_comment.id, v.comment.id); - assert_eq!(inserted_post.id, v.post.id); - assert_eq!(inserted_timmy.id, v.comment_creator.id); + assert_eq!(data.comment.id, v.comment.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.comment_creator.id); } else { panic!("wrong type"); } let report_count_timmy = - ReportCombinedViewInternal::get_report_count(pool, &timmy_view, None).await?; + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; assert_eq!(2, report_count_timmy); // Resolve the post report - PostReport::resolve(pool, inserted_post_report.id, inserted_timmy.id).await?; + PostReport::resolve(pool, inserted_post_report.id, data.timmy.id).await?; // Do a batch read of timmys reports // It should only show saras, which is unresolved @@ -591,16 +653,312 @@ mod tests { unresolved_only: Some(true), ..Default::default() } - .list(pool, &timmy_view) + .list(pool, &data.timmy_view) .await?; assert_length!(1, reports_after_resolve); // Make sure the counts are correct let report_count_after_resolved = - ReportCombinedViewInternal::get_report_count(pool, &timmy_view, None).await?; + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_private_message_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // timmy sends private message to jessica + let pm_form = PrivateMessageInsertForm::new( + data.timmy.id, + data.jessica.id, + "something offensive".to_string(), + ); + let pm = PrivateMessage::create(pool, &pm_form).await?; + + // jessica reports private message + let pm_report_form = PrivateMessageReportForm { + creator_id: data.jessica.id, + original_pm_text: pm.content.clone(), + private_message_id: pm.id, + reason: "its offensive".to_string(), + }; + let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; + + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, reports); + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert!(!v.private_message_report.resolved); + assert_eq!(data.timmy.name, v.private_message_creator.name); + assert_eq!(data.jessica.name, v.creator.name); + assert_eq!(pm_report.reason, v.private_message_report.reason); + assert_eq!(pm.content, v.private_message.content); + } else { + panic!("wrong type"); + } + + // admin resolves the report (after taking appropriate action) + PrivateMessageReport::resolve(pool, pm_report.id, data.admin_view.person.id).await?; + + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, reports); + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert!(v.private_message_report.resolved); + assert!(v.resolver.is_some()); + assert_eq!( + Some(&data.admin_view.person.name), + v.resolver.as_ref().map(|r| &r.name) + ); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_post_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports + let sara_report_form = PostReportForm { + creator_id: data.sara.id, + post_id: data.post.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from sara".into(), + }; + + PostReport::report(pool, &sara_report_form).await?; + + // jessica reports + let jessica_report_form = PostReportForm { + creator_id: data.jessica.id, + post_id: data.post_2.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from jessica".into(), + }; + + let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; + + let read_jessica_report_view = + PostReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + + // Make sure the triggers are reading the aggregates correctly. + let agg_1 = PostAggregates::read(pool, data.post.id).await?; + let agg_2 = PostAggregates::read(pool, data.post_2.id).await?; + + assert_eq!( + read_jessica_report_view.post_report, + inserted_jessica_report + ); + assert_eq!(read_jessica_report_view.post, data.post_2); + assert_eq!(read_jessica_report_view.community.id, data.community.id); + assert_eq!(read_jessica_report_view.creator.id, data.jessica.id); + assert_eq!(read_jessica_report_view.post_creator.id, data.timmy.id); + assert_eq!(read_jessica_report_view.my_vote, None); + assert_eq!(read_jessica_report_view.resolver, None); + assert_eq!(agg_1.report_count, 1); + assert_eq!(agg_1.unresolved_report_count, 1); + assert_eq!(agg_2.report_count, 1); + assert_eq!(agg_2.unresolved_report_count, 1); + + // Do a batch read of timmys reports + let reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Post(v) = &reports[1] { + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Post(v) = &reports[0] { + assert_eq!(v.creator.id, data.jessica.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count); + + // Pretend the post was removed, and resolve all reports for that object. + // This is called manually in the API for post removals + PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, data.timmy.id) + .await?; + + let read_jessica_report_view_after_resolve = + PostReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + assert!(read_jessica_report_view_after_resolve.post_report.resolved); + assert_eq!( + read_jessica_report_view_after_resolve + .post_report + .resolver_id, + Some(data.timmy.id) + ); + assert_eq!( + read_jessica_report_view_after_resolve + .resolver + .map(|r| r.id), + Some(data.timmy.id) + ); + + // Make sure the unresolved_post report got decremented in the trigger + let agg_2 = PostAggregates::read(pool, data.post_2.id).await?; + assert_eq!(agg_2.report_count, 1); + assert_eq!(agg_2.unresolved_report_count, 0); + + // Make sure the other unresolved report isn't changed + let agg_1 = PostAggregates::read(pool, data.post.id).await?; + assert_eq!(agg_1.report_count, 1); + assert_eq!(agg_1.unresolved_report_count, 1); + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Post(v) = &reports_after_resolve[0] { + assert_length!(1, reports_after_resolve); + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_comment_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports + let sara_report_form = CommentReportForm { + creator_id: data.sara.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from sara".into(), + }; + + CommentReport::report(pool, &sara_report_form).await?; + + // jessica reports + let jessica_report_form = CommentReportForm { + creator_id: data.jessica.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from jessica".into(), + }; + + let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; + + let agg = CommentAggregates::read(pool, data.comment.id).await?; + assert_eq!(agg.report_count, 2); + + let read_jessica_report_view = + CommentReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + assert_eq!(read_jessica_report_view.counts.unresolved_report_count, 2); + + // Do a batch read of timmys reports + let reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Comment(v) = &reports[0] { + assert_eq!(v.creator.id, data.jessica.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[1] { + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count); + + // Resolve the report + CommentReport::resolve(pool, inserted_jessica_report.id, data.timmy.id).await?; + let read_jessica_report_view_after_resolve = + CommentReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + + assert!( + read_jessica_report_view_after_resolve + .comment_report + .resolved + ); + assert_eq!( + read_jessica_report_view_after_resolve + .comment_report + .resolver_id, + Some(data.timmy.id) + ); + assert_eq!( + read_jessica_report_view_after_resolve + .resolver + .map(|r| r.id), + Some(data.timmy.id) + ); + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Comment(v) = &reports_after_resolve[0] { + assert_length!(1, reports_after_resolve); + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; assert_eq!(1, report_count_after_resolved); - Instance::delete(pool, inserted_instance.id).await?; + cleanup(data, pool).await?; Ok(()) } diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 452fb98707..3f61534359 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -61,21 +61,9 @@ use lemmy_api::{ }, private_message::mark_read::mark_pm_as_read, reports::{ - comment_report::{ - create::create_comment_report, - list::list_comment_reports, - resolve::resolve_comment_report, - }, - post_report::{ - create::create_post_report, - list::list_post_reports, - resolve::resolve_post_report, - }, - private_message_report::{ - create::create_pm_report, - list::list_pm_reports, - resolve::resolve_pm_report, - }, + comment_report::{create::create_comment_report, resolve::resolve_comment_report}, + post_report::{create::create_post_report, resolve::resolve_post_report}, + private_message_report::{create::create_pm_report, resolve::resolve_pm_report}, report_combined::list::list_reports, }, site::{ @@ -255,7 +243,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_post)) .route("/report", post().to(create_post_report)) .route("/report/resolve", put().to(resolve_post_report)) - .route("/report/list", get().to(list_post_reports)) .route("/site_metadata", get().to(get_link_metadata)), ) // Comment @@ -280,8 +267,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_comment)) .route("/list", get().to(list_comments)) .route("/report", post().to(create_comment_report)) - .route("/report/resolve", put().to(resolve_comment_report)) - .route("/report/list", get().to(list_comment_reports)), + .route("/report/resolve", put().to(resolve_comment_report)), ) .service( scope("report") @@ -298,8 +284,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/delete", post().to(delete_private_message)) .route("/mark_as_read", post().to(mark_pm_as_read)) .route("/report", post().to(create_pm_report)) - .route("/report/resolve", put().to(resolve_pm_report)) - .route("/report/list", get().to(list_pm_reports)), + .route("/report/resolve", put().to(resolve_pm_report)), ) // User .service( From 612c2e916ff84211a51bb0b69bc6df97a63a6e8f Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 16:23:50 -0500 Subject: [PATCH 10/43] Fixing column XOR check. --- migrations/2024-12-02-181601_add_combined_tables/up.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/migrations/2024-12-02-181601_add_combined_tables/up.sql b/migrations/2024-12-02-181601_add_combined_tables/up.sql index ef7900ddd7..236d7ab14f 100644 --- a/migrations/2024-12-02-181601_add_combined_tables/up.sql +++ b/migrations/2024-12-02-181601_add_combined_tables/up.sql @@ -14,7 +14,8 @@ CREATE TABLE report_combined ( post_report_id int REFERENCES post_report ON UPDATE CASCADE ON DELETE CASCADE, comment_report_id int REFERENCES comment_report ON UPDATE CASCADE ON DELETE CASCADE, private_message_report_id int REFERENCES private_message_report ON UPDATE CASCADE ON DELETE CASCADE, - UNIQUE (post_report_id, comment_report_id, private_message_report_id) + -- Make sure only one of the columns is not null + CHECK ((post_report_id IS NOT NULL)::integer + (comment_report_id IS NOT NULL)::integer + (private_message_report_id IS NOT NULL)::integer = 1) ); CREATE INDEX idx_report_combined_published ON report_combined (published DESC, id DESC); From 0c854d2d3a48f40f1c961a29a204d95523015562 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 16:33:45 -0500 Subject: [PATCH 11/43] Forgot to remove list report actions. --- crates/api_common/src/reports/comment.rs | 28 ------------------- crates/api_common/src/reports/post.rs | 28 ------------------- .../api_common/src/reports/private_message.rs | 23 --------------- 3 files changed, 79 deletions(-) diff --git a/crates/api_common/src/reports/comment.rs b/crates/api_common/src/reports/comment.rs index 4324079e5f..6e0426752b 100644 --- a/crates/api_common/src/reports/comment.rs +++ b/crates/api_common/src/reports/comment.rs @@ -30,31 +30,3 @@ pub struct ResolveCommentReport { pub report_id: CommentReportId, pub resolved: bool, } - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List comment reports. -pub struct ListCommentReports { - #[cfg_attr(feature = "full", ts(optional))] - pub comment_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, - /// if no community is given, it returns reports for all communities moderated by the auth user - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The comment report list response. -pub struct ListCommentReportsResponse { - pub comment_reports: Vec, -} diff --git a/crates/api_common/src/reports/post.rs b/crates/api_common/src/reports/post.rs index 1ed62252c0..8a935babff 100644 --- a/crates/api_common/src/reports/post.rs +++ b/crates/api_common/src/reports/post.rs @@ -30,31 +30,3 @@ pub struct ResolvePostReport { pub report_id: PostReportId, pub resolved: bool, } - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List post reports. -pub struct ListPostReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, - /// if no community is given, it returns reports for all communities moderated by the auth user - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub post_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The post reports response. -pub struct ListPostReportsResponse { - pub post_reports: Vec, -} diff --git a/crates/api_common/src/reports/private_message.rs b/crates/api_common/src/reports/private_message.rs index 22cd4f9752..6d1467b63a 100644 --- a/crates/api_common/src/reports/private_message.rs +++ b/crates/api_common/src/reports/private_message.rs @@ -30,26 +30,3 @@ pub struct ResolvePrivateMessageReport { pub report_id: PrivateMessageReportId, pub resolved: bool, } - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List private message reports. -pub struct ListPrivateMessageReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The response for list private message reports. -pub struct ListPrivateMessageReportsResponse { - pub private_message_reports: Vec, -} From 802a8a64e77af91165a56d0b8ebe5959cf6e76d1 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 16:34:23 -0500 Subject: [PATCH 12/43] Cleanup. --- crates/api_common/src/reports/comment.rs | 3 +-- crates/api_common/src/reports/post.rs | 3 +-- crates/api_common/src/reports/private_message.rs | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/api_common/src/reports/comment.rs b/crates/api_common/src/reports/comment.rs index 6e0426752b..d1a51a6a85 100644 --- a/crates/api_common/src/reports/comment.rs +++ b/crates/api_common/src/reports/comment.rs @@ -1,7 +1,6 @@ -use lemmy_db_schema::newtypes::{CommentId, CommentReportId, CommunityId}; +use lemmy_db_schema::newtypes::{CommentId, CommentReportId}; use lemmy_db_views::structs::CommentReportView; use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; diff --git a/crates/api_common/src/reports/post.rs b/crates/api_common/src/reports/post.rs index 8a935babff..a4d20d575a 100644 --- a/crates/api_common/src/reports/post.rs +++ b/crates/api_common/src/reports/post.rs @@ -1,7 +1,6 @@ -use lemmy_db_schema::newtypes::{CommunityId, PostId, PostReportId}; +use lemmy_db_schema::newtypes::{PostId, PostReportId}; use lemmy_db_views::structs::PostReportView; use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; diff --git a/crates/api_common/src/reports/private_message.rs b/crates/api_common/src/reports/private_message.rs index 6d1467b63a..5fd401564c 100644 --- a/crates/api_common/src/reports/private_message.rs +++ b/crates/api_common/src/reports/private_message.rs @@ -1,7 +1,6 @@ use lemmy_db_schema::newtypes::{PrivateMessageId, PrivateMessageReportId}; use lemmy_db_views::structs::PrivateMessageReportView; use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; From 4cac67c09909f3c77a18d2723cdcc760aa522d4c Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 18:05:38 -0500 Subject: [PATCH 13/43] Use internal tagging. --- Cargo.lock | 1 + crates/db_views/src/structs.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b000f97411..475946d6e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2549,6 +2549,7 @@ dependencies = [ "lemmy_db_views", "lemmy_db_views_actor", "lemmy_utils", + "regex", "serde", "serde_json", "serde_with", diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index cb80c5a2d1..0e3bbe6f4c 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -282,6 +282,8 @@ pub struct ReportCombinedViewInternal { #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] pub enum ReportCombinedView { Post(PostReportView), Comment(CommentReportView), From 82a14fc5fa8c6fbc19450775075a61b101d920f4 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 18:23:12 -0500 Subject: [PATCH 14/43] Fixing api tests. --- api_tests/package.json | 2 +- api_tests/pnpm-lock.yaml | 10 +++--- api_tests/src/comment.spec.ts | 32 +++++++++++++------ api_tests/src/post.spec.ts | 58 +++++++++++++++++++++++------------ api_tests/src/shared.ts | 22 ++++--------- 5 files changed, 73 insertions(+), 51 deletions(-) diff --git a/api_tests/package.json b/api_tests/package.json index ef47bf1929..7eb7f05932 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -28,7 +28,7 @@ "eslint": "^9.14.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-instance-blocks.5", + "lemmy-js-client": "0.20.0-reports-combined.2", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index a95e807266..91efa2716f 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^29.5.0 version: 29.7.0(@types/node@22.9.0) lemmy-js-client: - specifier: 0.20.0-instance-blocks.5 - version: 0.20.0-instance-blocks.5 + specifier: 0.20.0-reports-combined.2 + version: 0.20.0-reports-combined.2 prettier: specifier: ^3.2.5 version: 3.3.3 @@ -1167,8 +1167,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-instance-blocks.5: - resolution: {integrity: sha512-wDuRFzg32lbbJr4cNmd+cbzjgw+okw2/d5AujYjAm4gv0OEFfsYhP3QQ2WscwUR5HJTdzsR7IIyiBnvmaEUzUw==} + lemmy-js-client@0.20.0-reports-combined.2: + resolution: {integrity: sha512-ZquK08ggeHhFVHMxz42vZvZ3/UgPHYcvMT3MaNevqCJFMFgiXg0+1HWUctVAaZrj8ooiRTiVqupelEhq6LKKTQ==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -3077,7 +3077,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-instance-blocks.5: {} + lemmy-js-client@0.20.0-reports-combined.2: {} leven@3.1.0: {} diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index edc588db78..94404f4494 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -22,7 +22,6 @@ import { createCommunity, registerUser, reportComment, - listCommentReports, randomString, unfollows, getComments, @@ -38,8 +37,14 @@ import { blockCommunity, delay, saveUserSettings, + listReports, } from "./shared"; -import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client"; +import { + CommentReportView, + CommentView, + CommunityView, + SaveUserSettings, +} from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; let postOnAlphaRes: PostResponse; @@ -796,13 +801,22 @@ test("Report a comment", async () => { let alphaReport = (await reportComment(alpha, alphaComment.id, reason)) .comment_report_view.comment_report; - let betaReport = (await waitUntil( - () => - listCommentReports(beta).then(r => - r.comment_reports.find(rep => rep.comment_report.reason === reason), - ), - e => !!e, - ))!.comment_report; + let betaReport = ( + (await waitUntil( + () => + listReports(beta).then(p => + p.reports.find(r => { + switch (r.type_) { + case "Comment": + return r.comment_report.reason === reason; + default: + return false; + } + }), + ), + e => !!e, + )!) as CommentReportView + ).comment_report; expect(betaReport).toBeDefined(); expect(betaReport.resolved).toBe(false); expect(betaReport.original_comment_text).toBe( diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index a6063e0a2e..7dd7dd483a 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -27,7 +27,6 @@ import { followCommunity, banPersonFromCommunity, reportPost, - listPostReports, randomString, registerUser, getSite, @@ -38,10 +37,11 @@ import { alphaUrl, loginUser, createCommunity, + listReports, } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; -import { EditSite, ResolveObject } from "lemmy-js-client"; +import { EditSite, PostReportView, ResolveObject } from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -690,16 +690,25 @@ test("Report a post", async () => { expect(gammaReport).toBeDefined(); // Report was federated to community instance - let betaReport = (await waitUntil( - () => - listPostReports(beta).then(p => - p.post_reports.find( - r => - r.post_report.original_post_name === gammaReport.original_post_name, + let betaReport = ( + (await waitUntil( + () => + listReports(beta).then(p => + p.reports.find(r => { + switch (r.type_) { + case "Post": + return ( + r.post_report.original_post_name === + gammaReport.original_post_name + ); + default: + return false; + } + }), ), - ), - res => !!res, - ))!.post_report; + res => !!res, + ))! as PostReportView + ).post_report; expect(betaReport).toBeDefined(); expect(betaReport.resolved).toBe(false); expect(betaReport.original_post_name).toBe(gammaReport.original_post_name); @@ -709,16 +718,25 @@ test("Report a post", async () => { await unfollowRemotes(alpha); // Report was federated to poster's instance - let alphaReport = (await waitUntil( - () => - listPostReports(alpha).then(p => - p.post_reports.find( - r => - r.post_report.original_post_name === gammaReport.original_post_name, + let alphaReport = ( + (await waitUntil( + () => + listReports(alpha).then(p => + p.reports.find(r => { + switch (r.type_) { + case "Post": + return ( + r.post_report.original_post_name === + gammaReport.original_post_name + ); + default: + return false; + } + }), ), - ), - res => !!res, - ))!.post_report; + res => !!res, + ))! as PostReportView + ).post_report; expect(alphaReport).toBeDefined(); expect(alphaReport.resolved).toBe(false); expect(alphaReport.original_post_name).toBe(gammaReport.original_post_name); diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 9b0662959c..a46fd104aa 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -1,5 +1,4 @@ import { - AdminBlockInstanceParams, ApproveCommunityPendingFollower, BlockCommunity, BlockCommunityResponse, @@ -16,6 +15,8 @@ import { LemmyHttp, ListCommunityPendingFollows, ListCommunityPendingFollowsResponse, + ListReports, + ListReportsResponse, PersonId, PostView, PrivateMessageReportResponse, @@ -74,12 +75,8 @@ import { PrivateMessagesResponse } from "lemmy-js-client/dist/types/PrivateMessa import { GetPrivateMessages } from "lemmy-js-client/dist/types/GetPrivateMessages"; import { PostReportResponse } from "lemmy-js-client/dist/types/PostReportResponse"; import { CreatePostReport } from "lemmy-js-client/dist/types/CreatePostReport"; -import { ListPostReportsResponse } from "lemmy-js-client/dist/types/ListPostReportsResponse"; -import { ListPostReports } from "lemmy-js-client/dist/types/ListPostReports"; import { CommentReportResponse } from "lemmy-js-client/dist/types/CommentReportResponse"; import { CreateCommentReport } from "lemmy-js-client/dist/types/CreateCommentReport"; -import { ListCommentReportsResponse } from "lemmy-js-client/dist/types/ListCommentReportsResponse"; -import { ListCommentReports } from "lemmy-js-client/dist/types/ListCommentReports"; import { GetPostsResponse } from "lemmy-js-client/dist/types/GetPostsResponse"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDetailsResponse"; @@ -807,11 +804,11 @@ export async function reportPost( return api.createPostReport(form); } -export async function listPostReports( +export async function listReports( api: LemmyHttp, -): Promise { - let form: ListPostReports = {}; - return api.listPostReports(form); +): Promise { + let form: ListReports = {}; + return api.listReports(form); } export async function reportComment( @@ -838,13 +835,6 @@ export async function reportPrivateMessage( return api.createPrivateMessageReport(form); } -export async function listCommentReports( - api: LemmyHttp, -): Promise { - let form: ListCommentReports = {}; - return api.listCommentReports(form); -} - export function getPosts( api: LemmyHttp, listingType?: ListingType, From 1f284076ce7e1141358c40966d6b8f23322fa56d Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 18:35:18 -0500 Subject: [PATCH 15/43] Adding a few indexes. --- crates/db_schema/src/schema.rs | 1 - migrations/2024-12-02-181601_add_combined_tables/up.sql | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 925401c953..c93dabf486 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -660,7 +660,6 @@ diesel::table! { enabled -> Bool, published -> Timestamptz, updated -> Nullable, - use_pkce -> Bool, } } diff --git a/migrations/2024-12-02-181601_add_combined_tables/up.sql b/migrations/2024-12-02-181601_add_combined_tables/up.sql index 236d7ab14f..0b93d26e42 100644 --- a/migrations/2024-12-02-181601_add_combined_tables/up.sql +++ b/migrations/2024-12-02-181601_add_combined_tables/up.sql @@ -22,6 +22,12 @@ CREATE INDEX idx_report_combined_published ON report_combined (published DESC, i CREATE INDEX idx_report_combined_published_asc ON report_combined (reverse_timestamp_sort (published) DESC, id DESC); +CREATE INDEX idx_report_combined_post_report ON report_combined (post_report_id); + +CREATE INDEX idx_report_combined_comment_report ON report_combined (comment_report_id); + +CREATE INDEX idx_report_combined_private_message_report ON report_combined (private_message_report_id); + -- Updating the history INSERT INTO report_combined (published, post_report_id) SELECT From 34a440a6d70112b704ad966aefbb547f0ffc5f54 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 2 Dec 2024 18:38:18 -0500 Subject: [PATCH 16/43] Fixing migration name. --- crates/db_schema/src/schema.rs | 1 + .../down.sql | 0 .../up.sql | 0 3 files changed, 1 insertion(+) rename migrations/{2024-12-02-181601_add_combined_tables => 2024-12-02-181601_add_report_combined_table}/down.sql (100%) rename migrations/{2024-12-02-181601_add_combined_tables => 2024-12-02-181601_add_report_combined_table}/up.sql (100%) diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index c93dabf486..925401c953 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -660,6 +660,7 @@ diesel::table! { enabled -> Bool, published -> Timestamptz, updated -> Nullable, + use_pkce -> Bool, } } diff --git a/migrations/2024-12-02-181601_add_combined_tables/down.sql b/migrations/2024-12-02-181601_add_report_combined_table/down.sql similarity index 100% rename from migrations/2024-12-02-181601_add_combined_tables/down.sql rename to migrations/2024-12-02-181601_add_report_combined_table/down.sql diff --git a/migrations/2024-12-02-181601_add_combined_tables/up.sql b/migrations/2024-12-02-181601_add_report_combined_table/up.sql similarity index 100% rename from migrations/2024-12-02-181601_add_combined_tables/up.sql rename to migrations/2024-12-02-181601_add_report_combined_table/up.sql From 1a739a12e8bd97b3230465e32ae3b740e996f312 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 3 Dec 2024 12:18:13 -0500 Subject: [PATCH 17/43] Fixing unique constraints. --- .../up.sql | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/migrations/2024-12-02-181601_add_report_combined_table/up.sql b/migrations/2024-12-02-181601_add_report_combined_table/up.sql index 0b93d26e42..3b92dab93e 100644 --- a/migrations/2024-12-02-181601_add_report_combined_table/up.sql +++ b/migrations/2024-12-02-181601_add_report_combined_table/up.sql @@ -11,9 +11,9 @@ CREATE TABLE report_combined ( id serial PRIMARY KEY, published timestamptz NOT NULL, - post_report_id int REFERENCES post_report ON UPDATE CASCADE ON DELETE CASCADE, - comment_report_id int REFERENCES comment_report ON UPDATE CASCADE ON DELETE CASCADE, - private_message_report_id int REFERENCES private_message_report ON UPDATE CASCADE ON DELETE CASCADE, + post_report_id int UNIQUE REFERENCES post_report ON UPDATE CASCADE ON DELETE CASCADE, + comment_report_id int UNIQUE REFERENCES comment_report ON UPDATE CASCADE ON DELETE CASCADE, + private_message_report_id int UNIQUE REFERENCES private_message_report ON UPDATE CASCADE ON DELETE CASCADE, -- Make sure only one of the columns is not null CHECK ((post_report_id IS NOT NULL)::integer + (comment_report_id IS NOT NULL)::integer + (private_message_report_id IS NOT NULL)::integer = 1) ); @@ -22,12 +22,6 @@ CREATE INDEX idx_report_combined_published ON report_combined (published DESC, i CREATE INDEX idx_report_combined_published_asc ON report_combined (reverse_timestamp_sort (published) DESC, id DESC); -CREATE INDEX idx_report_combined_post_report ON report_combined (post_report_id); - -CREATE INDEX idx_report_combined_comment_report ON report_combined (comment_report_id); - -CREATE INDEX idx_report_combined_private_message_report ON report_combined (private_message_report_id); - -- Updating the history INSERT INTO report_combined (published, post_report_id) SELECT From 921d53227c035047b82c2335207d991ec76af5c3 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 4 Dec 2024 12:48:41 -0500 Subject: [PATCH 18/43] Addressing PR comments. --- api_tests/src/comment.spec.ts | 17 +++++++----- api_tests/src/post.spec.ts | 27 ++++++++++++------- crates/db_views/src/report_combined_view.rs | 9 +++---- .../up.sql | 10 +------ 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index 94404f4494..adcfa8a0b5 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -43,6 +43,7 @@ import { CommentReportView, CommentView, CommunityView, + ReportCombinedView, SaveUserSettings, } from "lemmy-js-client"; @@ -806,12 +807,7 @@ test("Report a comment", async () => { () => listReports(beta).then(p => p.reports.find(r => { - switch (r.type_) { - case "Comment": - return r.comment_report.reason === reason; - default: - return false; - } + return checkCommentReportReason(r, reason); }), ), e => !!e, @@ -891,3 +887,12 @@ test.skip("Fetch a deeply nested comment", async () => { expect(betaComment!.comment!.comment).toBeDefined(); expect(betaComment?.comment?.post).toBeDefined(); }); + +function checkCommentReportReason(rcv: ReportCombinedView, reason: string) { + switch (rcv.type_) { + case "Comment": + return rcv.comment_report.reason === reason; + default: + return false; + } +} diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 7dd7dd483a..7fbd8d0bcc 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -41,7 +41,13 @@ import { } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; -import { EditSite, PostReportView, ResolveObject } from "lemmy-js-client"; +import { + EditSite, + PostReport, + PostReportView, + ReportCombinedView, + ResolveObject, +} from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -695,15 +701,7 @@ test("Report a post", async () => { () => listReports(beta).then(p => p.reports.find(r => { - switch (r.type_) { - case "Post": - return ( - r.post_report.original_post_name === - gammaReport.original_post_name - ); - default: - return false; - } + return checkReportName(r, gammaReport); }), ), res => !!res, @@ -837,3 +835,12 @@ test("Rewrite markdown links", async () => { `[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`, ); }); + +function checkReportName(rcv: ReportCombinedView, report: PostReport) { + switch (rcv.type_) { + case "Post": + return rcv.post_report.original_post_name === report.original_post_name; + default: + return false; + } +} diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index b1232940f4..5944568e09 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -148,7 +148,6 @@ impl ReportCombinedQuery { pool: &mut DbPool<'_>, user: &LocalUserView, ) -> LemmyResult> { - let options = self; let my_person_id = user.local_user.person_id; let item_creator = aliases::person1.field(person::id); @@ -283,7 +282,7 @@ impl ReportCombinedQuery { )) .into_boxed(); - if let Some(community_id) = options.community_id { + if let Some(community_id) = self.community_id { query = query.filter(community::id.eq(community_id)); } @@ -294,9 +293,9 @@ impl ReportCombinedQuery { let mut query = PaginatedQueryBuilder::new(query); - let page_after = options.page_after.map(|c| c.0); + let page_after = self.page_after.map(|c| c.0); - if options.page_back.unwrap_or_default() { + if self.page_back.unwrap_or_default() { query = query.before(page_after).limit_and_offset_from_end(); } else { query = query.after(page_after); @@ -304,7 +303,7 @@ impl ReportCombinedQuery { // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest // first (FIFO) - if options.unresolved_only.unwrap_or_default() { + if self.unresolved_only.unwrap_or_default() { query = query .filter( post_report::resolved diff --git a/migrations/2024-12-02-181601_add_report_combined_table/up.sql b/migrations/2024-12-02-181601_add_report_combined_table/up.sql index 3b92dab93e..40dd9b277a 100644 --- a/migrations/2024-12-02-181601_add_report_combined_table/up.sql +++ b/migrations/2024-12-02-181601_add_report_combined_table/up.sql @@ -1,13 +1,5 @@ --- Creates combined tables for the following: --- +-- Creates combined tables for -- Reports: (comment, post, and private_message) --- Inbox: (Comment replies, post replies, comment mentions, post mentions, private messages) --- Profile: (Posts and Comments) --- Modlog: (lots of types) --- Search: (community, post, comment, user, url) --- TODO not sure about these two: --- Home: (comment, post) --- Community: (comment, post) CREATE TABLE report_combined ( id serial PRIMARY KEY, published timestamptz NOT NULL, From 724856d6843990931ab9ed6a178c84b815ed08cf Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 6 Dec 2024 08:18:08 -0500 Subject: [PATCH 19/43] Start working on profile combined --- crates/db_schema/src/newtypes.rs | 6 + crates/db_schema/src/schema.rs | 12 + crates/db_schema/src/source/combined/mod.rs | 1 + .../db_schema/src/source/combined/profile.rs | 30 ++ crates/db_views/src/lib.rs | 2 + crates/db_views/src/profile_combined_view.rs | 304 ++++++++++++++++++ crates/db_views/src/structs.rs | 47 +++ .../down.sql | 1 + .../up.sql | 29 ++ 9 files changed, 432 insertions(+) create mode 100644 crates/db_schema/src/source/combined/profile.rs create mode 100644 crates/db_views/src/profile_combined_view.rs create mode 100644 migrations/2024-12-05-233704_add_profile_combined_table/down.sql create mode 100644 migrations/2024-12-05-233704_add_profile_combined_table/up.sql diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c417ea2e45..38bd4e5912 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -185,6 +185,12 @@ pub struct DbUrl(pub(crate) Box); /// The report combined id pub struct ReportCombinedId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The profile combined id +pub struct ProfileCombinedId(i32); + impl DbUrl { pub fn inner(&self) -> &Url { &self.0 diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 925401c953..468bd2b0b0 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -856,6 +856,15 @@ diesel::table! { } } +diesel::table! { + profile_combined (id) { + id -> Int4, + published -> Timestamptz, + post_id -> Nullable, + comment_id -> Nullable, + } +} + diesel::table! { received_activity (ap_id) { ap_id -> Text, @@ -1043,6 +1052,8 @@ diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); diesel::joinable!(post_report -> post (post_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); +diesel::joinable!(profile_combined -> comment (comment_id)); +diesel::joinable!(profile_combined -> post (post_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); diesel::joinable!(report_combined -> comment_report (comment_report_id)); @@ -1113,6 +1124,7 @@ diesel::allow_tables_to_appear_in_same_query!( post_report, private_message, private_message_report, + profile_combined, received_activity, registration_application, remote_image, diff --git a/crates/db_schema/src/source/combined/mod.rs b/crates/db_schema/src/source/combined/mod.rs index 7352eef8e0..1d8a026d2f 100644 --- a/crates/db_schema/src/source/combined/mod.rs +++ b/crates/db_schema/src/source/combined/mod.rs @@ -1 +1,2 @@ +pub mod profile; pub mod report; diff --git a/crates/db_schema/src/source/combined/profile.rs b/crates/db_schema/src/source/combined/profile.rs new file mode 100644 index 0000000000..ffb6560918 --- /dev/null +++ b/crates/db_schema/src/source/combined/profile.rs @@ -0,0 +1,30 @@ +use crate::newtypes::{CommentId, PostId, ProfileCombinedId}; +#[cfg(feature = "full")] +use crate::schema::profile_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = profile_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +#[cfg_attr(feature = "full", cursor_keys_module(name = profile_combined_keys))] +/// A combined profile table. +pub struct ProfileCombined { + pub id: ProfileCombinedId, + pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] + pub post_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub comment_id: Option, +} diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index e93741be8c..9a5d3cb7ce 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -20,6 +20,8 @@ pub mod private_message_report_view; #[cfg(feature = "full")] pub mod private_message_view; #[cfg(feature = "full")] +pub mod profile_combined_view; +#[cfg(feature = "full")] pub mod registration_application_view; #[cfg(feature = "full")] pub mod report_combined_view; diff --git a/crates/db_views/src/profile_combined_view.rs b/crates/db_views/src/profile_combined_view.rs new file mode 100644 index 0000000000..59d2180259 --- /dev/null +++ b/crates/db_views/src/profile_combined_view.rs @@ -0,0 +1,304 @@ +use crate::structs::{ + CommentView, + LocalUserView, + PostView, + ProfileCombinedPaginationCursor, + ProfileCombinedView, + ProfileCombinedViewInternal, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::CommunityId, + schema::{ + comment, + comment_actions, + comment_aggregates, + community, + community_actions, + local_user, + person, + person_actions, + post, + post_actions, + post_aggregates, + profile_combined, + }, + source::{ + combined::profile::{profile_combined_keys as key, ProfileCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, +}; +use lemmy_utils::error::LemmyResult; + +impl ProfileCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &ProfileCombinedView) -> ProfileCombinedPaginationCursor { + let (prefix, id) = match view { + ProfileCombinedView::Comment(v) => ('C', v.comment.id.0), + ProfileCombinedView::Post(v) => ('P', v.post.id.0), + }; + // hex encoding to prevent ossification + ProfileCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = profile_combined::table + .select(ProfileCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(profile_combined::comment_id.eq(id)), + "P" => query.filter(profile_combined::post_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(ProfileCombined); + +// TODO check these +#[derive(Default)] +pub struct ProfileCombinedQuery { + pub community_id: Option, + pub unresolved_only: Option, + pub page_after: Option, + pub page_back: Option, +} + +impl ProfileCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &LocalUserView, + ) -> LemmyResult> { + let my_person_id = user.local_user.person_id; + // let item_creator = aliases::person1.field(person::id); + let item_creator = person::id; + + let conn = &mut get_conn(pool).await?; + + // Notes: since the post_id and comment_id are optional columns, + // many joins must use an OR condition. + // For example, the creator must be the person table joined to either: + // - post.creator_id + // - comment.creator_id + let mut query = profile_combined::table + // The comment + .left_join(comment::table.on(profile_combined::comment_id.eq(comment::id.nullable()))) + // The post + .inner_join( + post::table.on( + profile_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.nullable().eq(profile_combined::post_id)), + ), + ) + // The item creator + .inner_join( + person::table.on( + post::creator_id + .eq(person::id) + .or(comment::creator_id.eq(person::id)), + ), + ) + // The item creator + // You can now use aliases::person1.field(person::id) / item_creator for all the item actions + // .inner_join( + // aliases::person1.on( + // post::creator_id + // .eq(item_creator) + // .or(comment::creator_id.eq(item_creator)), + // ), + // ) + // The community + .inner_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + .left_join( + post_aggregates::table + .on(profile_combined::post_id.eq(post_aggregates::post_id.nullable())), + ) + .left_join( + comment_aggregates::table + .on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), + ) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment::id, + )) + .select(( + // Post-specific + post::all_columns.nullable(), + // post_aggregates::all_columns.nullable(), + // coalesce( + // post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + // post_aggregates::comments, + // ) + // .nullable(), + // post_actions::saved.nullable().is_not_null(), + // post_actions::read.nullable().is_not_null(), + // post_actions::hidden.nullable().is_not_null(), + // post_actions::like_score.nullable(), + // // Comment-specific + // comment::all_columns.nullable(), + // comment_aggregates::all_columns.nullable(), + // comment_actions::saved.nullable().is_not_null(), + // comment_actions::like_score.nullable(), + // // Private-message-specific + // private_message_profile::all_columns.nullable(), + // private_message::all_columns.nullable(), + // // Shared + // person::all_columns, + // aliases::person1.fields(person::all_columns), + // community::all_columns.nullable(), + // CommunityFollower::select_subscribed_type(), + // aliases::person2.fields(person::all_columns.nullable()), + // local_user::admin.nullable().is_not_null(), + // creator_community_actions + // .field(community_actions::received_ban) + // .nullable() + // .is_not_null(), + // creator_community_actions + // .field(community_actions::became_moderator) + // .nullable() + // .is_not_null(), + // person_actions::blocked.nullable().is_not_null(), + )) + .into_boxed(); + + if let Some(community_id) = self.community_id { + query = query.filter(community::id.eq(community_id)); + } + + // If its not an admin, get only the ones you mod + if !user.local_user.admin { + query = query.filter(community_actions::became_moderator.is_not_null()); + } + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // If viewing all profiles, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) + if self.unresolved_only.unwrap_or_default() { + query = query + .filter( + post_profile::resolved + .eq(false) + .or(comment_profile::resolved.eq(false)) + .or(private_message_profile::resolved.eq(false)), + ) + // TODO: when a `then_asc` method is added, use it here, make the id sort direction match, + // and remove the separate index; unless additional columns are added to this sort + .then_desc(ReverseTimestampKey(key::published)); + } else { + query = query.then_desc(key::published); + } + + // Tie breaker + query = query.then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(map_to_enum).collect(); + + Ok(out) + } +} + +/// Maps the combined DB row to an enum +fn map_to_enum(view: ProfileCombinedViewInternal) -> Option { + // Use for a short alias + let v = view; + + if let (Some(post), Some(community), Some(unread_comments), Some(counts)) = + (v.post, v.community, v.post_unread_comments, v.post_counts) + { + Some(ProfileCombinedView::Post(PostView { + post, + community, + unread_comments, + counts, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + })) + } else if let (Some(comment), Some(counts), Some(post), Some(community)) = ( + v.comment, + v.comment_counts, + v.post.clone(), + v.community.clone(), + ) { + Some(ProfileCombinedView::Comment(CommentView { + comment, + counts, + post, + community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + })) + } else { + None + } +} diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 0e3bbe6f4c..5dd42684b4 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -132,6 +132,12 @@ pub struct PaginationCursor(pub String); #[cfg_attr(feature = "full", ts(export))] pub struct ReportCombinedPaginationCursor(pub String); +/// like PaginationCursor but for the profile_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(ts_rs::TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ProfileCombinedPaginationCursor(pub String); + #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] @@ -289,3 +295,44 @@ pub enum ReportCombinedView { Comment(CommentReportView), PrivateMessage(PrivateMessageReportView), } + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined profile view +pub struct ProfileCombinedViewInternal { + // Post-specific + pub post_counts: PostAggregates, + pub post_unread_comments: i64, + pub post_saved: bool, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + pub image_details: Option, + // Comment-specific + pub comment: Comment, + pub comment_counts: CommentAggregates, + pub comment_saved: bool, + pub my_comment_vote: Option, + // Shared + pub post: Post, + pub community: Community, + pub item_creator: Person, + pub subscribed: SubscribedType, + pub item_creator_is_admin: bool, + pub item_creator_is_moderator: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_blocked: bool, + pub item_saved: bool, + pub banned_from_community: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum ProfileCombinedView { + Post(PostView), + Comment(CommentView), +} diff --git a/migrations/2024-12-05-233704_add_profile_combined_table/down.sql b/migrations/2024-12-05-233704_add_profile_combined_table/down.sql new file mode 100644 index 0000000000..9d9a394110 --- /dev/null +++ b/migrations/2024-12-05-233704_add_profile_combined_table/down.sql @@ -0,0 +1 @@ +DROP TABLE profile_combined; diff --git a/migrations/2024-12-05-233704_add_profile_combined_table/up.sql b/migrations/2024-12-05-233704_add_profile_combined_table/up.sql new file mode 100644 index 0000000000..c0d4171f0c --- /dev/null +++ b/migrations/2024-12-05-233704_add_profile_combined_table/up.sql @@ -0,0 +1,29 @@ +-- Creates combined tables for +-- Profile: (comment, post) +CREATE TABLE profile_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES comment ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) +); + +CREATE INDEX idx_profile_combined_published ON profile_combined (published DESC, id DESC); + +CREATE INDEX idx_profile_combined_published_asc ON profile_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO profile_combined (published, post_id) +SELECT + published, + id +FROM + post; + +INSERT INTO profile_combined (published, comment_id) +SELECT + published, + id +FROM + comment; From 1053df1a4b5ca81625b4917fe6bcb198d633c7a9 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 6 Dec 2024 09:49:11 -0500 Subject: [PATCH 20/43] Adding views and replaceable schema. --- crates/api_common/src/person.rs | 19 +- crates/apub/src/api/read_person.rs | 67 +++---- .../db_schema/replaceable_schema/triggers.sql | 28 +++ crates/db_views/src/profile_combined_view.rs | 184 ++++++++---------- crates/db_views/src/structs.rs | 5 +- .../down.sql | 1 + .../up.sql | 3 +- 7 files changed, 150 insertions(+), 157 deletions(-) diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 797946d656..0b51c74534 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -7,7 +7,11 @@ use lemmy_db_schema::{ PostListingMode, PostSortType, }; -use lemmy_db_views::structs::{CommentView, LocalImageView, PostView}; +use lemmy_db_views::structs::{ + LocalImageView, + ProfileCombinedPaginationCursor, + ProfileCombinedView, +}; use lemmy_db_views_actor::structs::{ CommentReplyView, CommunityModeratorView, @@ -223,15 +227,13 @@ pub struct GetPersonDetails { #[cfg_attr(feature = "full", ts(optional))] pub username: Option, #[cfg_attr(feature = "full", ts(optional))] - pub sort: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, #[cfg_attr(feature = "full", ts(optional))] pub saved_only: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, } #[skip_serializing_none] @@ -243,8 +245,7 @@ pub struct GetPersonDetailsResponse { pub person_view: PersonView, #[cfg_attr(feature = "full", ts(optional))] pub site: Option, - pub comments: Vec, - pub posts: Vec, + pub content: Vec, pub moderates: Vec, } diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index fac68cd63b..ba3160386f 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -6,10 +6,9 @@ use lemmy_api_common::{ person::{GetPersonDetails, GetPersonDetailsResponse}, utils::{check_private_instance, read_site_for_actor}, }; -use lemmy_db_schema::{source::person::Person, utils::post_to_comment_sort_type}; +use lemmy_db_schema::source::person::Person; use lemmy_db_views::{ - comment_view::CommentQuery, - post_view::PostQuery, + profile_combined_view::ProfileCombinedQuery, structs::{LocalUserView, SiteView}, }; use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView}; @@ -47,46 +46,38 @@ pub async fn read_person( // `my_user` let person_view = PersonView::read(&mut context.pool(), person_details_id).await?; - let sort = data.sort; - let page = data.page; - let limit = data.limit; + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; let saved_only = data.saved_only; let community_id = data.community_id; - // If its saved only, you don't care what creator it was - // Or, if its not saved, then you only want it for that specific creator + + // If its saved only, then ignore the person details id, + // and use your local user's id let creator_id = if !saved_only.unwrap_or_default() { Some(person_details_id) } else { - None + local_user_view.as_ref().map(|u| u.local_user.person_id) }; - let local_user = local_user_view.as_ref().map(|l| &l.local_user); - - let posts = PostQuery { - sort, - saved_only, - local_user, - community_id, - page, - limit, - creator_id, - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await?; - - let comments = CommentQuery { - local_user, - sort: sort.map(post_to_comment_sort_type), - saved_only, - community_id, - page, - limit, - creator_id, - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await?; + let content = if let Some(creator_id) = creator_id { + ProfileCombinedQuery { + creator_id, + community_id, + saved_only, + page_after, + page_back, + } + .list(&mut context.pool(), &local_user_view) + .await? + } else { + // if the creator is missing (saved_only, and no local_user), then return empty content + Vec::new() + }; let moderates = CommunityModeratorView::for_person( &mut context.pool(), @@ -97,12 +88,10 @@ pub async fn read_person( let site = read_site_for_actor(person_view.person.actor_id.clone(), &context).await?; - // Return the jwt Ok(Json(GetPersonDetailsResponse { person_view, site, moderates, - comments, - posts, + content, })) } diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 2d9b0df6e5..596a86eeb4 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -685,3 +685,31 @@ CALL r.create_report_combined_trigger ('comment_report'); CALL r.create_report_combined_trigger ('private_message_report'); +-- Profile (comment, post) +CREATE PROCEDURE r.create_profile_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.profile_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO profile_combined (published, thing_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER profile_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.profile_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_profile_combined_trigger ('post'); + +CALL r.create_profile_combined_trigger ('comment'); + diff --git a/crates/db_views/src/profile_combined_view.rs b/crates/db_views/src/profile_combined_view.rs index 59d2180259..d62b9315c6 100644 --- a/crates/db_views/src/profile_combined_view.rs +++ b/crates/db_views/src/profile_combined_view.rs @@ -18,14 +18,15 @@ use diesel::{ use diesel_async::RunQueryDsl; use i_love_jesus::PaginatedQueryBuilder; use lemmy_db_schema::{ - aliases::{self, creator_community_actions}, - newtypes::CommunityId, + aliases::creator_community_actions, + newtypes::{CommunityId, PersonId}, schema::{ comment, comment_actions, comment_aggregates, community, community_actions, + image_details, local_user, person, person_actions, @@ -74,11 +75,11 @@ impl ProfileCombinedPaginationCursor { #[derive(Clone)] pub struct PaginationCursorData(ProfileCombined); -// TODO check these #[derive(Default)] pub struct ProfileCombinedQuery { + pub creator_id: PersonId, pub community_id: Option, - pub unresolved_only: Option, + pub saved_only: Option, pub page_after: Option, pub page_back: Option, } @@ -87,10 +88,12 @@ impl ProfileCombinedQuery { pub async fn list( self, pool: &mut DbPool<'_>, - user: &LocalUserView, + user: &Option, ) -> LemmyResult> { - let my_person_id = user.local_user.person_id; - // let item_creator = aliases::person1.field(person::id); + let my_person_id = user + .as_ref() + .map(|u| u.local_user.person_id) + .unwrap_or(PersonId(-1)); let item_creator = person::id; let conn = &mut get_conn(pool).await?; @@ -114,20 +117,11 @@ impl ProfileCombinedQuery { // The item creator .inner_join( person::table.on( - post::creator_id + comment::creator_id .eq(person::id) - .or(comment::creator_id.eq(person::id)), + .or(post::creator_id.eq(person::id)), ), ) - // The item creator - // You can now use aliases::person1.field(person::id) / item_creator for all the item actions - // .inner_join( - // aliases::person1.on( - // post::creator_id - // .eq(item_creator) - // .or(comment::creator_id.eq(item_creator)), - // ), - // ) // The community .inner_join(community::table.on(post::community_id.eq(community::id))) .left_join(actions_alias( @@ -153,10 +147,7 @@ impl ProfileCombinedQuery { Some(my_person_id), item_creator, )) - .left_join( - post_aggregates::table - .on(profile_combined::post_id.eq(post_aggregates::post_id.nullable())), - ) + .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) .left_join( comment_aggregates::table .on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), @@ -166,43 +157,42 @@ impl ProfileCombinedQuery { Some(my_person_id), comment::id, )) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + // The creator id filter + .filter(item_creator.eq(self.creator_id)) .select(( // Post-specific - post::all_columns.nullable(), - // post_aggregates::all_columns.nullable(), - // coalesce( - // post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), - // post_aggregates::comments, - // ) - // .nullable(), - // post_actions::saved.nullable().is_not_null(), - // post_actions::read.nullable().is_not_null(), - // post_actions::hidden.nullable().is_not_null(), - // post_actions::like_score.nullable(), - // // Comment-specific - // comment::all_columns.nullable(), - // comment_aggregates::all_columns.nullable(), - // comment_actions::saved.nullable().is_not_null(), - // comment_actions::like_score.nullable(), - // // Private-message-specific - // private_message_profile::all_columns.nullable(), - // private_message::all_columns.nullable(), - // // Shared - // person::all_columns, - // aliases::person1.fields(person::all_columns), - // community::all_columns.nullable(), - // CommunityFollower::select_subscribed_type(), - // aliases::person2.fields(person::all_columns.nullable()), - // local_user::admin.nullable().is_not_null(), - // creator_community_actions - // .field(community_actions::received_ban) - // .nullable() - // .is_not_null(), - // creator_community_actions - // .field(community_actions::became_moderator) - // .nullable() - // .is_not_null(), - // person_actions::blocked.nullable().is_not_null(), + post_aggregates::all_columns, + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + // Comment-specific + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Shared + post::all_columns, + community::all_columns, + person::all_columns, + CommunityFollower::select_subscribed_type(), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), )) .into_boxed(); @@ -210,9 +200,13 @@ impl ProfileCombinedQuery { query = query.filter(community::id.eq(community_id)); } - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); + // If its saved only, then filter + if self.saved_only.unwrap_or_default() { + query = query.filter( + comment_actions::saved + .is_not_null() + .or(post_actions::saved.is_not_null()), + ) } let mut query = PaginatedQueryBuilder::new(query); @@ -225,25 +219,11 @@ impl ProfileCombinedQuery { query = query.after(page_after); } - // If viewing all profiles, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if self.unresolved_only.unwrap_or_default() { - query = query - .filter( - post_profile::resolved - .eq(false) - .or(comment_profile::resolved.eq(false)) - .or(private_message_profile::resolved.eq(false)), - ) - // TODO: when a `then_asc` method is added, use it here, make the id sort direction match, - // and remove the separate index; unless additional columns are added to this sort - .then_desc(ReverseTimestampKey(key::published)); - } else { - query = query.then_desc(key::published); - } - - // Tie breaker - query = query.then_desc(key::id); + // Sorting by published + query = query + .then_desc(ReverseTimestampKey(key::published)) + // Tie breaker + .then_desc(key::id); let res = query.load::(conn).await?; @@ -259,46 +239,40 @@ fn map_to_enum(view: ProfileCombinedViewInternal) -> Option // Use for a short alias let v = view; - if let (Some(post), Some(community), Some(unread_comments), Some(counts)) = - (v.post, v.community, v.post_unread_comments, v.post_counts) - { - Some(ProfileCombinedView::Post(PostView { - post, - community, - unread_comments, + if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { + Some(ProfileCombinedView::Comment(CommentView { + comment, counts, + post: v.post, + community: v.community, creator: v.item_creator, creator_banned_from_community: v.item_creator_banned_from_community, creator_is_moderator: v.item_creator_is_moderator, creator_is_admin: v.item_creator_is_admin, creator_blocked: v.item_creator_blocked, subscribed: v.subscribed, - saved: v.post_saved, - read: v.post_read, - hidden: v.post_hidden, - my_vote: v.my_post_vote, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, })) - } else if let (Some(comment), Some(counts), Some(post), Some(community)) = ( - v.comment, - v.comment_counts, - v.post.clone(), - v.community.clone(), - ) { - Some(ProfileCombinedView::Comment(CommentView { - comment, - counts, - post, - community, + } else { + Some(ProfileCombinedView::Post(PostView { + post: v.post, + community: v.community, + unread_comments: v.post_unread_comments, + counts: v.post_counts, creator: v.item_creator, creator_banned_from_community: v.item_creator_banned_from_community, creator_is_moderator: v.item_creator_is_moderator, creator_is_admin: v.item_creator_is_admin, creator_blocked: v.item_creator_blocked, subscribed: v.subscribed, - saved: v.comment_saved, - my_vote: v.my_comment_vote, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + image_details: v.image_details, + banned_from_community: v.banned_from_community, })) - } else { - None } } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 5dd42684b4..3ee2fd25cc 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -310,8 +310,8 @@ pub struct ProfileCombinedViewInternal { pub my_post_vote: Option, pub image_details: Option, // Comment-specific - pub comment: Comment, - pub comment_counts: CommentAggregates, + pub comment: Option, + pub comment_counts: Option, pub comment_saved: bool, pub my_comment_vote: Option, // Shared @@ -323,7 +323,6 @@ pub struct ProfileCombinedViewInternal { pub item_creator_is_moderator: bool, pub item_creator_banned_from_community: bool, pub item_creator_blocked: bool, - pub item_saved: bool, pub banned_from_community: bool, } diff --git a/migrations/2024-12-05-233704_add_profile_combined_table/down.sql b/migrations/2024-12-05-233704_add_profile_combined_table/down.sql index 9d9a394110..9426ebe38b 100644 --- a/migrations/2024-12-05-233704_add_profile_combined_table/down.sql +++ b/migrations/2024-12-05-233704_add_profile_combined_table/down.sql @@ -1 +1,2 @@ DROP TABLE profile_combined; + diff --git a/migrations/2024-12-05-233704_add_profile_combined_table/up.sql b/migrations/2024-12-05-233704_add_profile_combined_table/up.sql index c0d4171f0c..183529ee43 100644 --- a/migrations/2024-12-05-233704_add_profile_combined_table/up.sql +++ b/migrations/2024-12-05-233704_add_profile_combined_table/up.sql @@ -4,7 +4,7 @@ CREATE TABLE profile_combined ( id serial PRIMARY KEY, published timestamptz NOT NULL, post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, - comment_id int UNIQUE REFERENCES comment ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, -- Make sure only one of the columns is not null CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) ); @@ -27,3 +27,4 @@ SELECT id FROM comment; + From 32b5411abde60f20234895de0fe53c38583a758b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 7 Dec 2024 12:38:20 -0500 Subject: [PATCH 21/43] A few changes to profile view. - Separating the profile fetch from its combined content fetch. - Starting to separate saved_only into its own combined view. --- Cargo.lock | 1 + crates/api/src/local_user/list_saved.rs | 0 crates/api/src/local_user/mod.rs | 1 + crates/api_common/src/comment.rs | 2 - crates/api_common/src/person.rs | 62 +- crates/api_common/src/post.rs | 2 - crates/api_common/src/site.rs | 2 - crates/apub/src/api/list_comments.rs | 2 - crates/apub/src/api/list_person_content.rs | 50 ++ crates/apub/src/api/list_posts.rs | 2 - crates/apub/src/api/mod.rs | 35 +- crates/apub/src/api/read_person.rs | 69 +-- crates/apub/src/api/search.rs | 3 - .../db_schema/replaceable_schema/triggers.sql | 47 +- crates/db_schema/src/newtypes.rs | 10 +- crates/db_schema/src/schema.rs | 36 +- crates/db_schema/src/source/combined/mod.rs | 3 +- .../src/source/combined/person_content.rs | 30 + .../combined/{profile.rs => person_saved.rs} | 14 +- crates/db_views/Cargo.toml | 1 + crates/db_views/src/comment_view.rs | 61 +- crates/db_views/src/lib.rs | 13 +- .../src/person_content_combined_view.rs | 430 ++++++++++++++ .../src/person_saved_combined_view.rs | 554 ++++++++++++++++++ crates/db_views/src/post_view.rs | 43 +- crates/db_views/src/profile_combined_view.rs | 278 --------- crates/db_views/src/report_combined_view.rs | 170 +++--- crates/db_views/src/structs.rs | 16 +- .../down.sql | 3 + .../up.sql | 65 ++ .../down.sql | 2 - .../up.sql | 30 - src/api_routes_http.rs | 4 + 33 files changed, 1428 insertions(+), 613 deletions(-) create mode 100644 crates/api/src/local_user/list_saved.rs create mode 100644 crates/apub/src/api/list_person_content.rs create mode 100644 crates/db_schema/src/source/combined/person_content.rs rename crates/db_schema/src/source/combined/{profile.rs => person_saved.rs} (66%) create mode 100644 crates/db_views/src/person_content_combined_view.rs create mode 100644 crates/db_views/src/person_saved_combined_view.rs delete mode 100644 crates/db_views/src/profile_combined_view.rs create mode 100644 migrations/2024-12-05-233704_add_person_content_combined_table/down.sql create mode 100644 migrations/2024-12-05-233704_add_person_content_combined_table/up.sql delete mode 100644 migrations/2024-12-05-233704_add_profile_combined_table/down.sql delete mode 100644 migrations/2024-12-05-233704_add_profile_combined_table/up.sql diff --git a/Cargo.lock b/Cargo.lock index eebb1ce1a7..5076cd3b57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2677,6 +2677,7 @@ version = "0.19.6-beta.7" dependencies = [ "actix-web", "chrono", + "derive-new", "diesel", "diesel-async", "diesel_ltree", diff --git a/crates/api/src/local_user/list_saved.rs b/crates/api/src/local_user/list_saved.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index b1ee7c0b6e..728cc03af4 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -8,6 +8,7 @@ pub mod get_captcha; pub mod list_banned; pub mod list_logins; pub mod list_media; +pub mod list_saved; pub mod login; pub mod logout; pub mod notifications; diff --git a/crates/api_common/src/comment.rs b/crates/api_common/src/comment.rs index bae9c4de4e..0d416e9f05 100644 --- a/crates/api_common/src/comment.rs +++ b/crates/api_common/src/comment.rs @@ -131,8 +131,6 @@ pub struct GetComments { #[cfg_attr(feature = "full", ts(optional))] pub parent_id: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 0b51c74534..37f03ecaf4 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -9,8 +9,8 @@ use lemmy_db_schema::{ }; use lemmy_db_views::structs::{ LocalImageView, - ProfileCombinedPaginationCursor, - ProfileCombinedView, + PersonContentCombinedPaginationCursor, + PersonContentCombinedView, }; use lemmy_db_views_actor::structs::{ CommentReplyView, @@ -226,14 +226,6 @@ pub struct GetPersonDetails { /// Example: dessalines , or dessalines@xyz.tld #[cfg_attr(feature = "full", ts(optional))] pub username: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page_back: Option, } #[skip_serializing_none] @@ -245,10 +237,58 @@ pub struct GetPersonDetailsResponse { pub person_view: PersonView, #[cfg_attr(feature = "full", ts(optional))] pub site: Option, - pub content: Vec, pub moderates: Vec, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Gets a person's content (posts and comments) +/// +/// Either person_id, or username are required. +pub struct ListPersonContent { + #[cfg_attr(feature = "full", ts(optional))] + pub person_id: Option, + /// Example: dessalines , or dessalines@xyz.tld + #[cfg_attr(feature = "full", ts(optional))] + pub username: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A person's content response. +pub struct ListPersonContentResponse { + pub content: Vec, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Gets your saved posts and comments +pub struct ListSaved { + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A person's saved content response. +pub struct ListSavedResponse { + pub saved: Vec, +} + #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 8d8d3e0802..81cd7363be 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -95,8 +95,6 @@ pub struct GetPosts { #[cfg_attr(feature = "full", ts(optional))] pub community_name: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 9babe423c1..2ec6f29799 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -94,8 +94,6 @@ pub struct Search { #[cfg_attr(feature = "full", ts(optional))] pub post_url_only: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 3e7a2f4ebe..2411b874a9 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -46,7 +46,6 @@ pub async fn list_comments( &site_view.local_site, )); let max_depth = data.max_depth; - let saved_only = data.saved_only; let liked_only = data.liked_only; let disliked_only = data.disliked_only; @@ -80,7 +79,6 @@ pub async fn list_comments( listing_type, sort, max_depth, - saved_only, liked_only, disliked_only, community_id, diff --git a/crates/apub/src/api/list_person_content.rs b/crates/apub/src/api/list_person_content.rs new file mode 100644 index 0000000000..477e62e851 --- /dev/null +++ b/crates/apub/src/api/list_person_content.rs @@ -0,0 +1,50 @@ +use super::resolve_person_id_from_id_or_username; +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + person::{ListPersonContent, ListPersonContentResponse}, + utils::check_private_instance, +}; +use lemmy_db_views::{ + person_content_combined_view::PersonContentCombinedQuery, + structs::{LocalUserView, SiteView}, +}; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn list_person_content( + data: Query, + context: Data, + local_user_view: Option, +) -> LemmyResult> { + let local_site = SiteView::read_local(&mut context.pool()).await?; + + check_private_instance(&local_user_view, &local_site.local_site)?; + + let person_details_id = resolve_person_id_from_id_or_username( + &data.person_id, + &data.username, + &context, + &local_user_view, + ) + .await?; + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let content = PersonContentCombinedQuery { + creator_id: person_details_id, + page_after, + page_back, + } + .list(&mut context.pool(), &local_user_view) + .await?; + + Ok(Json(ListPersonContentResponse { content })) +} diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 63e737fdd0..6d043ae4f3 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -41,7 +41,6 @@ pub async fn list_posts( } else { data.community_id }; - let saved_only = data.saved_only; let show_hidden = data.show_hidden; let show_read = data.show_read; let show_nsfw = data.show_nsfw; @@ -77,7 +76,6 @@ pub async fn list_posts( listing_type, sort, community_id, - saved_only, liked_only, disliked_only, page, diff --git a/crates/apub/src/api/mod.rs b/crates/apub/src/api/mod.rs index 580be32286..9359eabc4b 100644 --- a/crates/apub/src/api/mod.rs +++ b/crates/apub/src/api/mod.rs @@ -1,12 +1,18 @@ +use crate::{fetcher::resolve_actor_identifier, objects::person::ApubPerson}; +use activitypub_federation::config::Data; +use lemmy_api_common::{context::LemmyContext, LemmyErrorType}; use lemmy_db_schema::{ - newtypes::CommunityId, - source::{local_site::LocalSite, local_user::LocalUser}, + newtypes::{CommunityId, PersonId}, + source::{local_site::LocalSite, local_user::LocalUser, person::Person}, CommentSortType, ListingType, PostSortType, }; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; pub mod list_comments; +pub mod list_person_content; pub mod list_posts; pub mod read_community; pub mod read_person; @@ -61,3 +67,28 @@ fn comment_sort_type_with_default( .unwrap_or(local_site.default_comment_sort_type), ) } + +async fn resolve_person_id_from_id_or_username( + person_id: &Option, + username: &Option, + context: &Data, + local_user_view: &Option, +) -> LemmyResult { + // Check to make sure a person name or an id is given + if username.is_none() && person_id.is_none() { + Err(LemmyErrorType::NoIdGiven)? + } + + Ok(match person_id { + Some(id) => *id, + None => { + if let Some(username) = username { + resolve_actor_identifier::(username, context, local_user_view, true) + .await? + .id + } else { + Err(LemmyErrorType::NotFound)? + } + } + }) +} diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index ba3160386f..fdcb6ba585 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -1,4 +1,4 @@ -use crate::{fetcher::resolve_actor_identifier, objects::person::ApubPerson}; +use super::resolve_person_id_from_id_or_username; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; use lemmy_api_common::{ @@ -6,13 +6,9 @@ use lemmy_api_common::{ person::{GetPersonDetails, GetPersonDetailsResponse}, utils::{check_private_instance, read_site_for_actor}, }; -use lemmy_db_schema::source::person::Person; -use lemmy_db_views::{ - profile_combined_view::ProfileCombinedQuery, - structs::{LocalUserView, SiteView}, -}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView}; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn read_person( @@ -20,65 +16,21 @@ pub async fn read_person( context: Data, local_user_view: Option, ) -> LemmyResult> { - // Check to make sure a person name or an id is given - if data.username.is_none() && data.person_id.is_none() { - Err(LemmyErrorType::NoIdGiven)? - } - let local_site = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site.local_site)?; - let person_details_id = match data.person_id { - Some(id) => id, - None => { - if let Some(username) = &data.username { - resolve_actor_identifier::(username, &context, &local_user_view, true) - .await? - .id - } else { - Err(LemmyErrorType::NotFound)? - } - } - }; + let person_details_id = resolve_person_id_from_id_or_username( + &data.person_id, + &data.username, + &context, + &local_user_view, + ) + .await?; // You don't need to return settings for the user, since this comes back with GetSite // `my_user` let person_view = PersonView::read(&mut context.pool(), person_details_id).await?; - - // parse pagination token - let page_after = if let Some(pa) = &data.page_cursor { - Some(pa.read(&mut context.pool()).await?) - } else { - None - }; - let page_back = data.page_back; - let saved_only = data.saved_only; - let community_id = data.community_id; - - // If its saved only, then ignore the person details id, - // and use your local user's id - let creator_id = if !saved_only.unwrap_or_default() { - Some(person_details_id) - } else { - local_user_view.as_ref().map(|u| u.local_user.person_id) - }; - - let content = if let Some(creator_id) = creator_id { - ProfileCombinedQuery { - creator_id, - community_id, - saved_only, - page_after, - page_back, - } - .list(&mut context.pool(), &local_user_view) - .await? - } else { - // if the creator is missing (saved_only, and no local_user), then return empty content - Vec::new() - }; - let moderates = CommunityModeratorView::for_person( &mut context.pool(), person_details_id, @@ -92,6 +44,5 @@ pub async fn read_person( person_view, site, moderates, - content, })) } diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index cdc9bc55ed..0ae7053d39 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -53,7 +53,6 @@ pub async fn search( limit, title_only, post_url_only, - saved_only, liked_only, disliked_only, }) = data; @@ -86,7 +85,6 @@ pub async fn search( url_only: post_url_only, liked_only, disliked_only, - saved_only, ..Default::default() }; @@ -101,7 +99,6 @@ pub async fn search( limit, liked_only, disliked_only, - saved_only, ..Default::default() }; diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 596a86eeb4..9768bb1d2f 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -685,31 +685,62 @@ CALL r.create_report_combined_trigger ('comment_report'); CALL r.create_report_combined_trigger ('private_message_report'); --- Profile (comment, post) -CREATE PROCEDURE r.create_profile_combined_trigger (table_name text) +-- person_content (comment, post) +CREATE PROCEDURE r.create_person_content_combined_trigger (table_name text) LANGUAGE plpgsql AS $a$ BEGIN - EXECUTE replace($b$ CREATE FUNCTION r.profile_combined_thing_insert ( ) + EXECUTE replace($b$ CREATE FUNCTION r.person_content_combined_thing_insert ( ) RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN - INSERT INTO profile_combined (published, thing_id) + INSERT INTO person_content_combined (published, thing_id) VALUES (NEW.published, NEW.id); RETURN NEW; END $$; - CREATE TRIGGER profile_combined + CREATE TRIGGER person_content_combined AFTER INSERT ON thing FOR EACH ROW - EXECUTE FUNCTION r.profile_combined_thing_insert ( ); + EXECUTE FUNCTION r.person_content_combined_thing_insert ( ); $b$, 'thing', table_name); END; $a$; -CALL r.create_profile_combined_trigger ('post'); +CALL r.create_person_content_combined_trigger ('post'); + +CALL r.create_person_content_combined_trigger ('comment'); + +-- person_saved (comment, post) +-- TODO, not sure how to handle changes to post_actions and comment_actions.saved column. +-- False should delete this row, true should insert +-- CREATE PROCEDURE r.create_person_saved_combined_trigger (table_name text) +-- LANGUAGE plpgsql +-- AS $a$ +-- BEGIN +-- EXECUTE replace($b$ CREATE FUNCTION r.person_saved_combined_thing_insert ( ) +-- RETURNS TRIGGER +-- LANGUAGE plpgsql +-- AS $$ +-- BEGIN +-- INSERT INTO person_saved_combined (published, thing_id) +-- VALUES (NEW.saved, NEW.id); +-- RETURN NEW; +-- END $$; +-- CREATE TRIGGER person_saved_combined +-- AFTER INSERT ON thing +-- FOR EACH ROW +-- EXECUTE FUNCTION r.person_saved_combined_thing_insert ( ); +-- $b$, +-- 'thing', +-- table_name); +-- END; +-- $a$; + +-- CALL r.create_person_saved_combined_trigger ('post_actions'); + +-- CALL r.create_person_saved_combined_trigger ('comment_actions'); -CALL r.create_profile_combined_trigger ('comment'); diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 38bd4e5912..5ae0290713 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -188,8 +188,14 @@ pub struct ReportCombinedId(i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] -/// The profile combined id -pub struct ProfileCombinedId(i32); +/// The person content combined id +pub struct PersonContentCombinedId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The person saved combined id +pub struct PersonSavedCombinedId(i32); impl DbUrl { pub fn inner(&self) -> &Url { diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 468bd2b0b0..dd6690849c 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -729,6 +729,15 @@ diesel::table! { } } +diesel::table! { + person_content_combined (id) { + id -> Int4, + published -> Timestamptz, + post_id -> Nullable, + comment_id -> Nullable, + } +} + diesel::table! { person_mention (id) { id -> Int4, @@ -739,6 +748,15 @@ diesel::table! { } } +diesel::table! { + person_saved_combined (id) { + id -> Int4, + published -> Timestamptz, + post_id -> Nullable, + comment_id -> Nullable, + } +} + diesel::table! { post (id) { id -> Int4, @@ -856,15 +874,6 @@ diesel::table! { } } -diesel::table! { - profile_combined (id) { - id -> Int4, - published -> Timestamptz, - post_id -> Nullable, - comment_id -> Nullable, - } -} - diesel::table! { received_activity (ap_id) { ap_id -> Text, @@ -1039,8 +1048,12 @@ diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(person -> instance (instance_id)); diesel::joinable!(person_aggregates -> person (person_id)); diesel::joinable!(person_ban -> person (person_id)); +diesel::joinable!(person_content_combined -> comment (comment_id)); +diesel::joinable!(person_content_combined -> post (post_id)); diesel::joinable!(person_mention -> comment (comment_id)); diesel::joinable!(person_mention -> person (recipient_id)); +diesel::joinable!(person_saved_combined -> comment (comment_id)); +diesel::joinable!(person_saved_combined -> post (post_id)); diesel::joinable!(post -> community (community_id)); diesel::joinable!(post -> language (language_id)); diesel::joinable!(post -> person (creator_id)); @@ -1052,8 +1065,6 @@ diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); diesel::joinable!(post_report -> post (post_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); -diesel::joinable!(profile_combined -> comment (comment_id)); -diesel::joinable!(profile_combined -> post (post_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); diesel::joinable!(report_combined -> comment_report (comment_report_id)); @@ -1117,14 +1128,15 @@ diesel::allow_tables_to_appear_in_same_query!( person_actions, person_aggregates, person_ban, + person_content_combined, person_mention, + person_saved_combined, post, post_actions, post_aggregates, post_report, private_message, private_message_report, - profile_combined, received_activity, registration_application, remote_image, diff --git a/crates/db_schema/src/source/combined/mod.rs b/crates/db_schema/src/source/combined/mod.rs index 1d8a026d2f..b2b5e7d8e1 100644 --- a/crates/db_schema/src/source/combined/mod.rs +++ b/crates/db_schema/src/source/combined/mod.rs @@ -1,2 +1,3 @@ -pub mod profile; +pub mod person_content; +pub mod person_saved; pub mod report; diff --git a/crates/db_schema/src/source/combined/person_content.rs b/crates/db_schema/src/source/combined/person_content.rs new file mode 100644 index 0000000000..c85b8f3b3c --- /dev/null +++ b/crates/db_schema/src/source/combined/person_content.rs @@ -0,0 +1,30 @@ +use crate::newtypes::{CommentId, PersonContentCombinedId, PostId}; +#[cfg(feature = "full")] +use crate::schema::person_content_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = person_content_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +#[cfg_attr(feature = "full", cursor_keys_module(name = person_content_combined_keys))] +/// A combined table for a persons contents (posts and comments) +pub struct PersonContentCombined { + pub id: PersonContentCombinedId, + pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] + pub post_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub comment_id: Option, +} diff --git a/crates/db_schema/src/source/combined/profile.rs b/crates/db_schema/src/source/combined/person_saved.rs similarity index 66% rename from crates/db_schema/src/source/combined/profile.rs rename to crates/db_schema/src/source/combined/person_saved.rs index ffb6560918..08cdd04159 100644 --- a/crates/db_schema/src/source/combined/profile.rs +++ b/crates/db_schema/src/source/combined/person_saved.rs @@ -1,6 +1,6 @@ -use crate::newtypes::{CommentId, PostId, ProfileCombinedId}; +use crate::newtypes::{CommentId, PersonSavedCombinedId, PostId}; #[cfg(feature = "full")] -use crate::schema::profile_combined; +use crate::schema::person_saved_combined; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] use i_love_jesus::CursorKeysModule; @@ -15,13 +15,13 @@ use ts_rs::TS; feature = "full", derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) )] -#[cfg_attr(feature = "full", diesel(table_name = profile_combined))] +#[cfg_attr(feature = "full", diesel(table_name = person_saved_combined))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] -#[cfg_attr(feature = "full", cursor_keys_module(name = profile_combined_keys))] -/// A combined profile table. -pub struct ProfileCombined { - pub id: ProfileCombinedId, +#[cfg_attr(feature = "full", cursor_keys_module(name = person_saved_combined_keys))] +/// A combined person_saved table. +pub struct PersonSavedCombined { + pub id: PersonSavedCombinedId, pub published: DateTime, #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index df8124c8a9..20dca5139f 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -40,6 +40,7 @@ ts-rs = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } i-love-jesus = { workspace = true, optional = true } chrono = { workspace = true } +derive-new.workspace = true [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 2cf751f9f2..0067d0807b 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -189,13 +189,6 @@ fn queries<'a>() -> Queries< } } - // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only.unwrap_or_default() { - query = query - .filter(comment_actions::saved.is_not_null()) - .then_order_by(comment_actions::saved.desc()); - } - if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { @@ -337,7 +330,6 @@ pub struct CommentQuery<'a> { pub creator_id: Option, pub local_user: Option<&'a LocalUser>, pub search_term: Option, - pub saved_only: Option, pub liked_only: Option, pub disliked_only: Option, pub page: Option, @@ -381,15 +373,7 @@ mod tests { newtypes::LanguageId, source::{ actor_language::LocalUserLanguage, - comment::{ - Comment, - CommentInsertForm, - CommentLike, - CommentLikeForm, - CommentSaved, - CommentSavedForm, - CommentUpdateForm, - }, + comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, community::{ Community, CommunityFollower, @@ -411,7 +395,7 @@ mod tests { post::{Post, PostInsertForm, PostUpdateForm}, site::{Site, SiteInsertForm}, }, - traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable}, utils::{build_db_pool_for_tests, RANK_DEFAULT}, CommunityVisibility, SubscribedType, @@ -897,47 +881,6 @@ mod tests { cleanup(data, pool).await } - #[tokio::test] - #[serial] - async fn test_saved_order() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - // Save two comments - let save_comment_0_form = CommentSavedForm { - person_id: data.timmy_local_user_view.person.id, - comment_id: data.inserted_comment_0.id, - }; - CommentSaved::save(pool, &save_comment_0_form).await?; - - let save_comment_2_form = CommentSavedForm { - person_id: data.timmy_local_user_view.person.id, - comment_id: data.inserted_comment_2.id, - }; - CommentSaved::save(pool, &save_comment_2_form).await?; - - // Fetch the saved comments - let comments = CommentQuery { - local_user: Some(&data.timmy_local_user_view.local_user), - saved_only: Some(true), - ..Default::default() - } - .list(&data.site, pool) - .await?; - - // There should only be two comments - assert_eq!(2, comments.len()); - - // The first comment, should be the last one saved (descending order) - assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); - - // The second comment, should be the first one saved - assert_eq!(comments[1].comment.id, data.inserted_comment_0.id); - - cleanup(data, pool).await - } - async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { CommentLike::remove( pool, diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 9a5d3cb7ce..aa3b53e823 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -12,6 +12,10 @@ pub mod local_image_view; #[cfg(feature = "full")] pub mod local_user_view; #[cfg(feature = "full")] +pub mod person_content_combined_view; +#[cfg(feature = "full")] +pub mod person_saved_combined_view; +#[cfg(feature = "full")] pub mod post_report_view; #[cfg(feature = "full")] pub mod post_view; @@ -20,8 +24,6 @@ pub mod private_message_report_view; #[cfg(feature = "full")] pub mod private_message_view; #[cfg(feature = "full")] -pub mod profile_combined_view; -#[cfg(feature = "full")] pub mod registration_application_view; #[cfg(feature = "full")] pub mod report_combined_view; @@ -30,3 +32,10 @@ pub mod site_view; pub mod structs; #[cfg(feature = "full")] pub mod vote_view; + +pub trait InternalToCombinedView { + type CombinedView; + + /// Maps the combined DB row to an enum + fn map_to_enum(&self) -> Option; +} diff --git a/crates/db_views/src/person_content_combined_view.rs b/crates/db_views/src/person_content_combined_view.rs new file mode 100644 index 0000000000..69be392ded --- /dev/null +++ b/crates/db_views/src/person_content_combined_view.rs @@ -0,0 +1,430 @@ +use crate::{ + structs::{ + CommentView, + LocalUserView, + PersonContentCombinedPaginationCursor, + PersonContentCombinedView, + PersonContentViewInternal, + PostView, + }, + InternalToCombinedView, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::creator_community_actions, + newtypes::PersonId, + schema::{ + comment, + comment_actions, + comment_aggregates, + community, + community_actions, + image_details, + local_user, + person, + person_actions, + person_content_combined, + post, + post_actions, + post_aggregates, + }, + source::{ + combined::person_content::{person_content_combined_keys as key, PersonContentCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, +}; +use lemmy_utils::error::LemmyResult; + +impl PersonContentCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &PersonContentCombinedView) -> PersonContentCombinedPaginationCursor { + let (prefix, id) = match view { + PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), + PersonContentCombinedView::Post(v) => ('P', v.post.id.0), + }; + // hex encoding to prevent ossification + PersonContentCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = person_content_combined::table + .select(PersonContentCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(person_content_combined::comment_id.eq(id)), + "P" => query.filter(person_content_combined::post_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(PersonContentCombined); + +#[derive(derive_new::new)] +pub struct PersonContentCombinedQuery { + pub creator_id: PersonId, + #[new(default)] + pub page_after: Option, + #[new(default)] + pub page_back: Option, +} + +impl PersonContentCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &Option, + ) -> LemmyResult> { + let my_person_id = user.as_ref().map(|u| u.local_user.person_id); + let item_creator = person::id; + + let conn = &mut get_conn(pool).await?; + + // Notes: since the post_id and comment_id are optional columns, + // many joins must use an OR condition. + // For example, the creator must be the person table joined to either: + // - post.creator_id + // - comment.creator_id + let query = person_content_combined::table + // The comment + .left_join(comment::table.on(person_content_combined::comment_id.eq(comment::id.nullable()))) + // The post + // It gets a bit complicated here, because since both comments and post combined have a post + // attached, you can do an inner join. + .inner_join( + post::table.on( + person_content_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.eq(post::id)), + ), + ) + // The item creator + .inner_join( + person::table.on( + comment::creator_id + .eq(item_creator) + // Need to filter out the post rows where both the post and comment creator are the + // same. + .or( + post::creator_id + .eq(item_creator) + .and(person_content_combined::post_id.is_not_null()), + ), + ), + ) + // The community + .inner_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(post_actions::table, my_person_id, post::id)) + .left_join(actions(person_actions::table, my_person_id, item_creator)) + .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .left_join( + comment_aggregates::table + .on(person_content_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), + ) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + // The creator id filter + .filter(item_creator.eq(self.creator_id)) + .select(( + // Post-specific + post_aggregates::all_columns, + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + // Comment-specific + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Shared + post::all_columns, + community::all_columns, + person::all_columns, + CommunityFollower::select_subscribed_type(), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + )) + .into_boxed(); + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // Sorting by published + query = query + .then_desc(key::published) + // Tie breaker + .then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +impl InternalToCombinedView for PersonContentViewInternal { + type CombinedView = PersonContentCombinedView; + + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { + Some(PersonContentCombinedView::Comment(CommentView { + comment, + counts, + post: v.post, + community: v.community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + })) + } else { + Some(PersonContentCombinedView::Post(PostView { + post: v.post, + community: v.community, + unread_comments: v.post_unread_comments, + counts: v.post_counts, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + image_details: v.image_details, + banned_from_community: v.banned_from_community, + })) + } + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + person_content_combined_view::PersonContentCombinedQuery, + structs::PersonContentCombinedView, + }; + use lemmy_db_schema::{ + source::{ + comment::{Comment, CommentInsertForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, + }, + traits::Crud, + utils::{build_db_pool_for_tests, DbPool}, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + sara: Person, + timmy_post: Post, + timmy_post_2: Post, + sara_post: Post, + timmy_comment: Comment, + sara_comment: Comment, + sara_comment_2: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv"); + let timmy = Person::create(pool, &timmy_form).await?; + + let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); + let sara = Person::create(pool, &sara_form).await?; + + let community_form = CommunityInsertForm::new( + instance.id, + "test community pcv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + let timmy_post_form = PostInsertForm::new("timmy post prv".into(), timmy.id, community.id); + let timmy_post = Post::create(pool, &timmy_post_form).await?; + + let timmy_post_form_2 = PostInsertForm::new("timmy post prv 2".into(), timmy.id, community.id); + let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + + let sara_post_form = PostInsertForm::new("sara post prv".into(), sara.id, community.id); + let sara_post = Post::create(pool, &sara_post_form).await?; + + let timmy_comment_form = + CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv".into()); + let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + + let sara_comment_form = + CommentInsertForm::new(sara.id, timmy_post.id, "sara comment prv".into()); + let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + + let sara_comment_form_2 = + CommentInsertForm::new(sara.id, timmy_post_2.id, "sara comment prv 2".into()); + let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + + Ok(Data { + instance, + timmy, + sara, + timmy_post, + timmy_post_2, + sara_post, + timmy_comment, + sara_comment, + sara_comment_2, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Do a batch read of timmy + let timmy_content = PersonContentCombinedQuery::new(data.timmy.id) + .list(pool, &None) + .await?; + assert_eq!(3, timmy_content.len()); + + // Make sure the report types are correct + if let PersonContentCombinedView::Comment(v) = &timmy_content[0] { + assert_eq!(data.timmy_comment.id, v.comment.id); + assert_eq!(data.timmy.id, v.creator.id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &timmy_content[1] { + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &timmy_content[2] { + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + + // Do a batch read of sara + let sara_content = PersonContentCombinedQuery::new(data.sara.id) + .list(pool, &None) + .await?; + assert_eq!(3, sara_content.len()); + + // Make sure the report types are correct + if let PersonContentCombinedView::Comment(v) = &sara_content[0] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.sara.id, v.creator.id); + // This one was to timmy_post_2 + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Comment(v) = &sara_content[1] { + assert_eq!(data.sara_comment.id, v.comment.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &sara_content[2] { + assert_eq!(data.sara_post.id, v.post.id); + assert_eq!(data.sara.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views/src/person_saved_combined_view.rs b/crates/db_views/src/person_saved_combined_view.rs new file mode 100644 index 0000000000..2e0a9f5bf6 --- /dev/null +++ b/crates/db_views/src/person_saved_combined_view.rs @@ -0,0 +1,554 @@ +// use crate::{ +// structs::{ +// CommentView, +// LocalUserView, +// PostView, +// ProfileCombinedPaginationCursor, +// PersonContentCombinedView, +// PersonContentViewInternal, +// }, +// InternalToCombinedView, +// }; +// use diesel::{ +// result::Error, +// BoolExpressionMethods, +// ExpressionMethods, +// JoinOnDsl, +// NullableExpressionMethods, +// QueryDsl, +// SelectableHelper, +// }; +// use diesel_async::RunQueryDsl; +// use i_love_jesus::PaginatedQueryBuilder; +// use lemmy_db_schema::{ +// aliases::creator_community_actions, +// newtypes::{CommunityId, PersonId}, +// schema::{ +// comment, +// comment_actions, +// comment_aggregates, +// community, +// community_actions, +// image_details, +// local_user, +// person, +// person_actions, +// post, +// post_actions, +// post_aggregates, +// profile_combined, +// }, +// source::{ +// combined::profile::{profile_combined_keys as key, ProfileCombined}, +// community::CommunityFollower, +// }, +// utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, +// }; +// use lemmy_utils::error::LemmyResult; + +// impl ProfileCombinedPaginationCursor { +// // get cursor for page that starts immediately after the given post +// pub fn after_post(view: &PersonContentCombinedView) -> ProfileCombinedPaginationCursor { +// let (prefix, id) = match view { +// PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), +// PersonContentCombinedView::Post(v) => ('P', v.post.id.0), +// }; +// // hex encoding to prevent ossification +// ProfileCombinedPaginationCursor(format!("{prefix}{id:x}")) +// } + +// pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { +// let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); +// let mut query = profile_combined::table +// .select(ProfileCombined::as_select()) +// .into_boxed(); +// let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; +// let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; +// query = match prefix { +// "C" => query.filter(profile_combined::comment_id.eq(id)), +// "P" => query.filter(profile_combined::post_id.eq(id)), +// _ => return Err(err_msg()), +// }; +// let token = query.first(&mut get_conn(pool).await?).await?; + +// Ok(PaginationCursorData(token)) +// } +// } + +// #[derive(Clone)] +// pub struct PaginationCursorData(ProfileCombined); + +// #[derive(Default)] +// pub struct ProfileCombinedQuery { +// pub creator_id: PersonId, +// pub page_after: Option, +// pub page_back: Option, +// } + +// impl ProfileCombinedQuery { +// pub async fn list( +// self, +// pool: &mut DbPool<'_>, +// user: &Option, +// ) -> LemmyResult> { +// let my_person_id = user +// .as_ref() +// .map(|u| u.local_user.person_id) +// .unwrap_or(PersonId(-1)); +// let item_creator = person::id; + +// let conn = &mut get_conn(pool).await?; + +// // Notes: since the post_id and comment_id are optional columns, +// // many joins must use an OR condition. +// // For example, the creator must be the person table joined to either: +// // - post.creator_id +// // - comment.creator_id +// let mut query = profile_combined::table +// // The comment +// .left_join(comment::table.on(profile_combined::comment_id.eq(comment::id.nullable()))) +// // The post +// .inner_join( +// post::table.on( +// profile_combined::post_id +// .eq(post::id.nullable()) +// .or(comment::post_id.nullable().eq(profile_combined::post_id)), +// ), +// ) +// // The item creator +// .inner_join( +// person::table.on( +// comment::creator_id +// .eq(person::id) +// .or(post::creator_id.eq(person::id)), +// ), +// ) +// // The community +// .inner_join(community::table.on(post::community_id.eq(community::id))) +// .left_join(actions_alias( +// creator_community_actions, +// item_creator, +// post::community_id, +// )) +// .left_join( +// local_user::table.on( +// item_creator +// .eq(local_user::person_id) +// .and(local_user::admin.eq(true)), +// ), +// ) +// .left_join(actions( +// community_actions::table, +// Some(my_person_id), +// post::community_id, +// )) +// .left_join(actions(post_actions::table, Some(my_person_id), post::id)) +// .left_join(actions( +// person_actions::table, +// Some(my_person_id), +// item_creator, +// )) +// .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) +// .left_join( +// comment_aggregates::table +// .on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), +// ) +// .left_join(actions( +// comment_actions::table, +// Some(my_person_id), +// comment::id, +// )) +// .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) +// // The creator id filter +// .filter(item_creator.eq(self.creator_id)) +// .select(( +// // Post-specific +// post_aggregates::all_columns, +// coalesce( +// post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), +// post_aggregates::comments, +// ), +// post_actions::saved.nullable().is_not_null(), +// post_actions::read.nullable().is_not_null(), +// post_actions::hidden.nullable().is_not_null(), +// post_actions::like_score.nullable(), +// image_details::all_columns.nullable(), +// // Comment-specific +// comment::all_columns.nullable(), +// comment_aggregates::all_columns.nullable(), +// comment_actions::saved.nullable().is_not_null(), +// comment_actions::like_score.nullable(), +// // Shared +// post::all_columns, +// community::all_columns, +// person::all_columns, +// CommunityFollower::select_subscribed_type(), +// local_user::admin.nullable().is_not_null(), +// creator_community_actions +// .field(community_actions::became_moderator) +// .nullable() +// .is_not_null(), +// creator_community_actions +// .field(community_actions::received_ban) +// .nullable() +// .is_not_null(), +// person_actions::blocked.nullable().is_not_null(), +// community_actions::received_ban.nullable().is_not_null(), +// )) +// .into_boxed(); + +// let mut query = PaginatedQueryBuilder::new(query); + +// let page_after = self.page_after.map(|c| c.0); + +// if self.page_back.unwrap_or_default() { +// query = query.before(page_after).limit_and_offset_from_end(); +// } else { +// query = query.after(page_after); +// } + +// // Sorting by published +// query = query +// .then_desc(ReverseTimestampKey(key::published)) +// // Tie breaker +// .then_desc(key::id); + +// let res = query.load::(conn).await?; + +// // Map the query results to the enum +// let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + +// Ok(out) +// } +// } + +// impl InternalToCombinedView for PersonContentViewInternal { +// type CombinedView = PersonContentCombinedView; + +// fn map_to_enum(&self) -> Option { +// // Use for a short alias +// let v = self.clone(); + +// if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { +// Some(PersonContentCombinedView::Comment(CommentView { +// comment, +// counts, +// post: v.post, +// community: v.community, +// creator: v.item_creator, +// creator_banned_from_community: v.item_creator_banned_from_community, +// creator_is_moderator: v.item_creator_is_moderator, +// creator_is_admin: v.item_creator_is_admin, +// creator_blocked: v.item_creator_blocked, +// subscribed: v.subscribed, +// saved: v.comment_saved, +// my_vote: v.my_comment_vote, +// banned_from_community: v.banned_from_community, +// })) +// } else { +// Some(PersonContentCombinedView::Post(PostView { +// post: v.post, +// community: v.community, +// unread_comments: v.post_unread_comments, +// counts: v.post_counts, +// creator: v.item_creator, +// creator_banned_from_community: v.item_creator_banned_from_community, +// creator_is_moderator: v.item_creator_is_moderator, +// creator_is_admin: v.item_creator_is_admin, +// creator_blocked: v.item_creator_blocked, +// subscribed: v.subscribed, +// saved: v.post_saved, +// read: v.post_read, +// hidden: v.post_hidden, +// my_vote: v.my_post_vote, +// image_details: v.image_details, +// banned_from_community: v.banned_from_community, +// })) +// } +// } +// } + +// #[cfg(test)] +// #[expect(clippy::indexing_slicing)] +// mod tests { + +// use crate::{ +// profile_combined_view::ProfileCombinedQuery, +// report_combined_view::ReportCombinedQuery, +// structs::{ +// CommentReportView, +// LocalUserView, +// PostReportView, +// PersonContentCombinedView, +// ReportCombinedView, +// ReportCombinedViewInternal, +// }, +// }; +// use lemmy_db_schema::{ +// aggregates::structs::{CommentAggregates, PostAggregates}, +// assert_length, +// source::{ +// comment::{Comment, CommentInsertForm, CommentSaved, CommentSavedForm}, +// comment_report::{CommentReport, CommentReportForm}, +// community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, +// instance::Instance, +// local_user::{LocalUser, LocalUserInsertForm}, +// local_user_vote_display_mode::LocalUserVoteDisplayMode, +// person::{Person, PersonInsertForm}, +// post::{Post, PostInsertForm}, +// post_report::{PostReport, PostReportForm}, +// private_message::{PrivateMessage, PrivateMessageInsertForm}, +// private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, +// }, +// traits::{Crud, Joinable, Reportable, Saveable}, +// utils::{build_db_pool_for_tests, DbPool}, +// }; +// use lemmy_utils::error::LemmyResult; +// use pretty_assertions::assert_eq; +// use serial_test::serial; + +// struct Data { +// instance: Instance, +// timmy: Person, +// sara: Person, +// timmy_view: LocalUserView, +// community: Community, +// timmy_post: Post, +// timmy_post_2: Post, +// sara_post: Post, +// timmy_comment: Comment, +// sara_comment: Comment, +// sara_comment_2: Comment, +// } + +// async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { +// let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + +// let timmy_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_pcv"); +// let inserted_timmy = Person::create(pool, &timmy_form).await?; +// let timmy_local_user_form = LocalUserInsertForm::test_form(inserted_timmy.id); +// let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; +// let timmy_view = LocalUserView { +// local_user: timmy_local_user, +// local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), +// person: inserted_timmy.clone(), +// counts: Default::default(), +// }; + +// let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_pcv"); +// let inserted_sara = Person::create(pool, &sara_form).await?; + +// let community_form = CommunityInsertForm::new( +// inserted_instance.id, +// "test community pcv".to_string(), +// "nada".to_owned(), +// "pubkey".to_string(), +// ); +// let inserted_community = Community::create(pool, &community_form).await?; + +// let timmy_post_form = PostInsertForm::new( +// "timmy post prv".into(), +// inserted_timmy.id, +// inserted_community.id, +// ); +// let timmy_post = Post::create(pool, &timmy_post_form).await?; + +// let timmy_post_form_2 = PostInsertForm::new( +// "timmy post prv 2".into(), +// inserted_timmy.id, +// inserted_community.id, +// ); +// let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + +// let sara_post_form = PostInsertForm::new( +// "sara post prv".into(), +// inserted_sara.id, +// inserted_community.id, +// ); +// let sara_post = Post::create(pool, &sara_post_form).await?; + +// let timmy_comment_form = +// CommentInsertForm::new(inserted_timmy.id, timmy_post.id, "timmy comment prv".into()); +// let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + +// let sara_comment_form = +// CommentInsertForm::new(inserted_sara.id, timmy_post.id, "sara comment prv".into()); +// let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + +// let sara_comment_form_2 = CommentInsertForm::new( +// inserted_sara.id, +// timmy_post_2.id, +// "sara comment prv 2".into(), +// ); +// let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + +// Ok(Data { +// instance: inserted_instance, +// timmy: inserted_timmy, +// sara: inserted_sara, +// timmy_view, +// community: inserted_community, +// timmy_post, +// timmy_post_2, +// sara_post, +// timmy_comment, +// sara_comment, +// sara_comment_2, +// }) +// } + +// async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { +// Instance::delete(pool, data.instance.id).await?; + +// Ok(()) +// } + +// #[tokio::test] +// #[serial] +// async fn test_combined() -> LemmyResult<()> { +// let pool = &build_db_pool_for_tests(); +// let pool = &mut pool.into(); +// let data = init_data(pool).await?; + +// // Do a batch read of timmy +// let timmy_content = ProfileCombinedQuery::default().list(pool, &None).await?; +// assert_eq!(3, timmy_content.len()); + +// // Make sure the report types are correct +// if let PersonContentCombinedView::Comment(v) = &timmy_content[0] { +// assert_eq!(data.timmy_comment.id, v.comment.id); +// assert_eq!(data.timmy.id, v.creator.id); +// } else { +// panic!("wrong type"); +// } +// if let PersonContentCombinedView::Post(v) = &timmy_content[1] { +// assert_eq!(data.timmy_post_2.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let PersonContentCombinedView::Post(v) = &timmy_content[2] { +// assert_eq!(data.timmy_post.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } + +// // Do a batch read of sara +// let sara_content = ProfileCombinedQuery::default().list(pool, &None).await?; +// assert_eq!(3, sara_content.len()); + +// // Make sure the report types are correct +// if let PersonContentCombinedView::Comment(v) = &sara_content[0] { +// assert_eq!(data.sara_comment_2.id, v.comment.id); +// assert_eq!(data.sara.id, v.creator.id); +// // This one was to timmy_post_2 +// assert_eq!(data.timmy_post_2.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let PersonContentCombinedView::Comment(v) = &sara_content[1] { +// assert_eq!(data.sara_comment.id, v.comment.id); +// assert_eq!(data.sara.id, v.creator.id); +// assert_eq!(data.timmy_post.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } +// if let PersonContentCombinedView::Post(v) = &sara_content[2] { +// assert_eq!(data.timmy_post.id, v.post.id); +// assert_eq!(data.timmy.id, v.post.creator_id); +// } else { +// panic!("wrong type"); +// } + +// // Timmy saves sara's comment, and his 2nd post +// let save_comment_0_form = CommentSavedForm { +// person_id: data.timmy.id, +// comment_id: data.sara_comment.id, +// }; +// CommentSaved::save(pool, &save_comment_0_form).await?; + +// // Timmy saves sara's comment, and his 2nd post +// let save_comment_0_form = CommentSavedForm { +// person_id: data.timmy.id, +// comment_id: data.sara_comment.id, +// }; +// CommentSaved::save(pool, &save_comment_0_form).await?; + +// // Do a saved_only query +// let timmy_content_saved_only = ProfileCombinedQuery {}.list(pool, &None).await?; + +// cleanup(data, pool).await?; + +// Ok(()) +// } +// } +// #[tokio::test] +// #[serial] +// async fn test_saved_order() -> LemmyResult<()> { +// let pool = &build_db_pool_for_tests(); +// let pool = &mut pool.into(); +// let data = init_data(pool).await?; + +// // Save two comments +// let save_comment_0_form = CommentSavedForm { +// person_id: data.timmy_local_user_view.person.id, +// comment_id: data.inserted_comment_0.id, +// }; +// CommentSaved::save(pool, &save_comment_0_form).await?; + +// let save_comment_2_form = CommentSavedForm { +// person_id: data.timmy_local_user_view.person.id, +// comment_id: data.inserted_comment_2.id, +// }; +// CommentSaved::save(pool, &save_comment_2_form).await?; + +// // Fetch the saved comments +// let comments = CommentQuery { +// local_user: Some(&data.timmy_local_user_view.local_user), +// saved_only: Some(true), +// ..Default::default() +// } +// .list(&data.site, pool) +// .await?; + +// // There should only be two comments +// assert_eq!(2, comments.len()); + +// // The first comment, should be the last one saved (descending order) +// assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); + +// // The second comment, should be the first one saved +// assert_eq!(comments[1].comment.id, data.inserted_comment_0.id); + +// cleanup(data, pool).await +// } +// #[tokio::test] +// #[serial] +// async fn post_listing_saved_only() -> LemmyResult<()> { +// let pool = &build_db_pool()?; +// let pool = &mut pool.into(); +// let data = init_data(pool).await?; + +// // Save only the bot post +// // The saved_only should only show the bot post +// let post_save_form = +// PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); +// PostSaved::save(pool, &post_save_form).await?; + +// // Read the saved only +// let read_saved_post_listing = PostQuery { +// community_id: Some(data.inserted_community.id), +// saved_only: Some(true), +// ..data.default_post_query() +// } +// .list(&data.site, pool) +// .await?; + +// // This should only include the bot post, not the one you created +// assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); + +// cleanup(data, pool).await +// } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index c6d1b036f1..e4a65721ec 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -287,15 +287,7 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::comments.eq(0)); }; - // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only.unwrap_or_default() { - query = query - .filter(post_actions::saved.is_not_null()) - .then_order_by(post_actions::saved.desc()); - } - // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read - // setting wont be able to see saved posts. - else if !options + if !options .show_read .unwrap_or(options.local_user.show_read_posts()) { @@ -488,7 +480,6 @@ pub struct PostQuery<'a> { pub local_user: Option<&'a LocalUser>, pub search_term: Option, pub url_only: Option, - pub saved_only: Option, pub liked_only: Option, pub disliked_only: Option, pub title_only: Option, @@ -646,13 +637,11 @@ mod tests { PostLikeForm, PostRead, PostReadForm, - PostSaved, - PostSavedForm, PostUpdateForm, }, site::Site, }, - traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable}, utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, @@ -1090,34 +1079,6 @@ mod tests { cleanup(data, pool).await } - #[tokio::test] - #[serial] - async fn post_listing_saved_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - // Save only the bot post - // The saved_only should only show the bot post - let post_save_form = - PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); - PostSaved::save(pool, &post_save_form).await?; - - // Read the saved only - let read_saved_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), - saved_only: Some(true), - ..data.default_post_query() - } - .list(&data.site, pool) - .await?; - - // This should only include the bot post, not the one you created - assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); - - cleanup(data, pool).await - } - #[tokio::test] #[serial] async fn creator_info() -> LemmyResult<()> { diff --git a/crates/db_views/src/profile_combined_view.rs b/crates/db_views/src/profile_combined_view.rs deleted file mode 100644 index d62b9315c6..0000000000 --- a/crates/db_views/src/profile_combined_view.rs +++ /dev/null @@ -1,278 +0,0 @@ -use crate::structs::{ - CommentView, - LocalUserView, - PostView, - ProfileCombinedPaginationCursor, - ProfileCombinedView, - ProfileCombinedViewInternal, -}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, - SelectableHelper, -}; -use diesel_async::RunQueryDsl; -use i_love_jesus::PaginatedQueryBuilder; -use lemmy_db_schema::{ - aliases::creator_community_actions, - newtypes::{CommunityId, PersonId}, - schema::{ - comment, - comment_actions, - comment_aggregates, - community, - community_actions, - image_details, - local_user, - person, - person_actions, - post, - post_actions, - post_aggregates, - profile_combined, - }, - source::{ - combined::profile::{profile_combined_keys as key, ProfileCombined}, - community::CommunityFollower, - }, - utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, -}; -use lemmy_utils::error::LemmyResult; - -impl ProfileCombinedPaginationCursor { - // get cursor for page that starts immediately after the given post - pub fn after_post(view: &ProfileCombinedView) -> ProfileCombinedPaginationCursor { - let (prefix, id) = match view { - ProfileCombinedView::Comment(v) => ('C', v.comment.id.0), - ProfileCombinedView::Post(v) => ('P', v.post.id.0), - }; - // hex encoding to prevent ossification - ProfileCombinedPaginationCursor(format!("{prefix}{id:x}")) - } - - pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { - let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); - let mut query = profile_combined::table - .select(ProfileCombined::as_select()) - .into_boxed(); - let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; - let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; - query = match prefix { - "C" => query.filter(profile_combined::comment_id.eq(id)), - "P" => query.filter(profile_combined::post_id.eq(id)), - _ => return Err(err_msg()), - }; - let token = query.first(&mut get_conn(pool).await?).await?; - - Ok(PaginationCursorData(token)) - } -} - -#[derive(Clone)] -pub struct PaginationCursorData(ProfileCombined); - -#[derive(Default)] -pub struct ProfileCombinedQuery { - pub creator_id: PersonId, - pub community_id: Option, - pub saved_only: Option, - pub page_after: Option, - pub page_back: Option, -} - -impl ProfileCombinedQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - user: &Option, - ) -> LemmyResult> { - let my_person_id = user - .as_ref() - .map(|u| u.local_user.person_id) - .unwrap_or(PersonId(-1)); - let item_creator = person::id; - - let conn = &mut get_conn(pool).await?; - - // Notes: since the post_id and comment_id are optional columns, - // many joins must use an OR condition. - // For example, the creator must be the person table joined to either: - // - post.creator_id - // - comment.creator_id - let mut query = profile_combined::table - // The comment - .left_join(comment::table.on(profile_combined::comment_id.eq(comment::id.nullable()))) - // The post - .inner_join( - post::table.on( - profile_combined::post_id - .eq(post::id.nullable()) - .or(comment::post_id.nullable().eq(profile_combined::post_id)), - ), - ) - // The item creator - .inner_join( - person::table.on( - comment::creator_id - .eq(person::id) - .or(post::creator_id.eq(person::id)), - ), - ) - // The community - .inner_join(community::table.on(post::community_id.eq(community::id))) - .left_join(actions_alias( - creator_community_actions, - item_creator, - post::community_id, - )) - .left_join( - local_user::table.on( - item_creator - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ) - .left_join(actions( - community_actions::table, - Some(my_person_id), - post::community_id, - )) - .left_join(actions(post_actions::table, Some(my_person_id), post::id)) - .left_join(actions( - person_actions::table, - Some(my_person_id), - item_creator, - )) - .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) - .left_join( - comment_aggregates::table - .on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), - ) - .left_join(actions( - comment_actions::table, - Some(my_person_id), - comment::id, - )) - .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) - // The creator id filter - .filter(item_creator.eq(self.creator_id)) - .select(( - // Post-specific - post_aggregates::all_columns, - coalesce( - post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), - post_aggregates::comments, - ), - post_actions::saved.nullable().is_not_null(), - post_actions::read.nullable().is_not_null(), - post_actions::hidden.nullable().is_not_null(), - post_actions::like_score.nullable(), - image_details::all_columns.nullable(), - // Comment-specific - comment::all_columns.nullable(), - comment_aggregates::all_columns.nullable(), - comment_actions::saved.nullable().is_not_null(), - comment_actions::like_score.nullable(), - // Shared - post::all_columns, - community::all_columns, - person::all_columns, - CommunityFollower::select_subscribed_type(), - local_user::admin.nullable().is_not_null(), - creator_community_actions - .field(community_actions::became_moderator) - .nullable() - .is_not_null(), - creator_community_actions - .field(community_actions::received_ban) - .nullable() - .is_not_null(), - person_actions::blocked.nullable().is_not_null(), - community_actions::received_ban.nullable().is_not_null(), - )) - .into_boxed(); - - if let Some(community_id) = self.community_id { - query = query.filter(community::id.eq(community_id)); - } - - // If its saved only, then filter - if self.saved_only.unwrap_or_default() { - query = query.filter( - comment_actions::saved - .is_not_null() - .or(post_actions::saved.is_not_null()), - ) - } - - let mut query = PaginatedQueryBuilder::new(query); - - let page_after = self.page_after.map(|c| c.0); - - if self.page_back.unwrap_or_default() { - query = query.before(page_after).limit_and_offset_from_end(); - } else { - query = query.after(page_after); - } - - // Sorting by published - query = query - .then_desc(ReverseTimestampKey(key::published)) - // Tie breaker - .then_desc(key::id); - - let res = query.load::(conn).await?; - - // Map the query results to the enum - let out = res.into_iter().filter_map(map_to_enum).collect(); - - Ok(out) - } -} - -/// Maps the combined DB row to an enum -fn map_to_enum(view: ProfileCombinedViewInternal) -> Option { - // Use for a short alias - let v = view; - - if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { - Some(ProfileCombinedView::Comment(CommentView { - comment, - counts, - post: v.post, - community: v.community, - creator: v.item_creator, - creator_banned_from_community: v.item_creator_banned_from_community, - creator_is_moderator: v.item_creator_is_moderator, - creator_is_admin: v.item_creator_is_admin, - creator_blocked: v.item_creator_blocked, - subscribed: v.subscribed, - saved: v.comment_saved, - my_vote: v.my_comment_vote, - banned_from_community: v.banned_from_community, - })) - } else { - Some(ProfileCombinedView::Post(PostView { - post: v.post, - community: v.community, - unread_comments: v.post_unread_comments, - counts: v.post_counts, - creator: v.item_creator, - creator_banned_from_community: v.item_creator_banned_from_community, - creator_is_moderator: v.item_creator_is_moderator, - creator_is_admin: v.item_creator_is_admin, - creator_blocked: v.item_creator_blocked, - subscribed: v.subscribed, - saved: v.post_saved, - read: v.post_read, - hidden: v.post_hidden, - my_vote: v.my_post_vote, - image_details: v.image_details, - banned_from_community: v.banned_from_community, - })) - } -} diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 5944568e09..b38c7841fe 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -1,11 +1,14 @@ -use crate::structs::{ - CommentReportView, - LocalUserView, - PostReportView, - PrivateMessageReportView, - ReportCombinedPaginationCursor, - ReportCombinedView, - ReportCombinedViewInternal, +use crate::{ + structs::{ + CommentReportView, + LocalUserView, + PostReportView, + PrivateMessageReportView, + ReportCombinedPaginationCursor, + ReportCombinedView, + ReportCombinedViewInternal, + }, + InternalToCombinedView, }; use diesel::{ result::Error, @@ -149,9 +152,10 @@ impl ReportCombinedQuery { user: &LocalUserView, ) -> LemmyResult> { let my_person_id = user.local_user.person_id; + let report_creator = person::id; let item_creator = aliases::person1.field(person::id); - let resolver = aliases::person2.field(person::id).nullable(); + let conn = &mut get_conn(pool).await?; // Notes: since the post_report_id and comment_report_id are optional columns, @@ -167,9 +171,9 @@ impl ReportCombinedQuery { .inner_join( person::table.on( post_report::creator_id - .eq(person::id) - .or(comment_report::creator_id.eq(person::id)) - .or(private_message_report::creator_id.eq(person::id)), + .eq(report_creator) + .or(comment_report::creator_id.eq(report_creator)) + .or(private_message_report::creator_id.eq(report_creator)), ), ) // The comment @@ -188,7 +192,8 @@ impl ReportCombinedQuery { ), ) // The item creator - // You can now use aliases::person1.field(person::id) / item_creator for all the item actions + // You can now use aliases::person1.field(person::id) / item_creator + // for all the item actions .inner_join( aliases::person1.on( post::creator_id @@ -324,81 +329,84 @@ impl ReportCombinedQuery { let res = query.load::(conn).await?; // Map the query results to the enum - let out = res.into_iter().filter_map(map_to_enum).collect(); + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); Ok(out) } } -/// Maps the combined DB row to an enum -fn map_to_enum(view: ReportCombinedViewInternal) -> Option { - // Use for a short alias - let v = view; - - if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = ( - v.post_report, - v.post.clone(), - v.community.clone(), - v.post_unread_comments, - v.post_counts, - ) { - Some(ReportCombinedView::Post(PostReportView { - post_report, - post, - community, - unread_comments, - counts, - creator: v.report_creator, - post_creator: v.item_creator, - creator_banned_from_community: v.item_creator_banned_from_community, - creator_is_moderator: v.item_creator_is_moderator, - creator_is_admin: v.item_creator_is_admin, - creator_blocked: v.item_creator_blocked, - subscribed: v.subscribed, - saved: v.post_saved, - read: v.post_read, - hidden: v.post_hidden, - my_vote: v.my_post_vote, - resolver: v.resolver, - })) - } else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = ( - v.comment_report, - v.comment, - v.comment_counts, - v.post.clone(), - v.community.clone(), - ) { - Some(ReportCombinedView::Comment(CommentReportView { - comment_report, - comment, - counts, - post, - community, - creator: v.report_creator, - comment_creator: v.item_creator, - creator_banned_from_community: v.item_creator_banned_from_community, - creator_is_moderator: v.item_creator_is_moderator, - creator_is_admin: v.item_creator_is_admin, - creator_blocked: v.item_creator_blocked, - subscribed: v.subscribed, - saved: v.comment_saved, - my_vote: v.my_comment_vote, - resolver: v.resolver, - })) - } else if let (Some(private_message_report), Some(private_message)) = - (v.private_message_report, v.private_message) - { - Some(ReportCombinedView::PrivateMessage( - PrivateMessageReportView { - private_message_report, - private_message, +impl InternalToCombinedView for ReportCombinedViewInternal { + type CombinedView = ReportCombinedView; + + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = ( + v.post_report, + v.post.clone(), + v.community.clone(), + v.post_unread_comments, + v.post_counts, + ) { + Some(ReportCombinedView::Post(PostReportView { + post_report, + post, + community, + unread_comments, + counts, + creator: v.report_creator, + post_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + resolver: v.resolver, + })) + } else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = ( + v.comment_report, + v.comment, + v.comment_counts, + v.post, + v.community, + ) { + Some(ReportCombinedView::Comment(CommentReportView { + comment_report, + comment, + counts, + post, + community, creator: v.report_creator, - private_message_creator: v.item_creator, + comment_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, resolver: v.resolver, - }, - )) - } else { - None + })) + } else if let (Some(private_message_report), Some(private_message)) = + (v.private_message_report, v.private_message) + { + Some(ReportCombinedView::PrivateMessage( + PrivateMessageReportView { + private_message_report, + private_message, + creator: v.report_creator, + private_message_creator: v.item_creator, + resolver: v.resolver, + }, + )) + } else { + None + } } } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 3ee2fd25cc..8cbbba6744 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -132,11 +132,17 @@ pub struct PaginationCursor(pub String); #[cfg_attr(feature = "full", ts(export))] pub struct ReportCombinedPaginationCursor(pub String); -/// like PaginationCursor but for the profile_combined table +/// like PaginationCursor but for the person_content_combined table #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(ts_rs::TS))] #[cfg_attr(feature = "full", ts(export))] -pub struct ProfileCombinedPaginationCursor(pub String); +pub struct PersonContentCombinedPaginationCursor(pub String); + +/// like PaginationCursor but for the person_saved_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(ts_rs::TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct PersonSavedCombinedPaginationCursor(pub String); #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -299,8 +305,8 @@ pub enum ReportCombinedView { #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -/// A combined profile view -pub struct ProfileCombinedViewInternal { +/// A combined person_content view +pub struct PersonContentViewInternal { // Post-specific pub post_counts: PostAggregates, pub post_unread_comments: i64, @@ -331,7 +337,7 @@ pub struct ProfileCombinedViewInternal { #[cfg_attr(feature = "full", ts(export))] // Use serde's internal tagging, to work easier with javascript libraries #[serde(tag = "type_")] -pub enum ProfileCombinedView { +pub enum PersonContentCombinedView { Post(PostView), Comment(CommentView), } diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql new file mode 100644 index 0000000000..a8db9ec61d --- /dev/null +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql @@ -0,0 +1,3 @@ +DROP TABLE person_content_combined; +DROP TABLE person_saved_combined; + diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql new file mode 100644 index 0000000000..40ced8238f --- /dev/null +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql @@ -0,0 +1,65 @@ +-- Creates combined tables for +-- person_content: (comment, post) +-- person_saved: (comment, post) +CREATE TABLE person_content_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) +); + +CREATE INDEX idx_person_content_combined_published ON person_content_combined (published DESC, id DESC); + +CREATE INDEX idx_person_content_combined_published_asc ON person_content_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO person_content_combined (published, post_id) +SELECT + published, + id +FROM + post; + +INSERT INTO person_content_combined (published, comment_id) +SELECT + published, + id +FROM + comment; + +-- This one is special, because you use the saved date, not the ordinary published +CREATE TABLE person_saved_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) +); + +CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (published DESC, id DESC); + +CREATE INDEX idx_person_saved_combined_published_asc ON person_saved_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO person_saved_combined (published, post_id) +SELECT + saved, + post_id +FROM + post_actions +WHERE + saved IS NOT NULL; + +INSERT INTO person_saved_combined (published, comment_id) +SELECT + saved, + comment_id +FROM + comment_actions +WHERE + saved IS NOT NULL; + + diff --git a/migrations/2024-12-05-233704_add_profile_combined_table/down.sql b/migrations/2024-12-05-233704_add_profile_combined_table/down.sql deleted file mode 100644 index 9426ebe38b..0000000000 --- a/migrations/2024-12-05-233704_add_profile_combined_table/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE profile_combined; - diff --git a/migrations/2024-12-05-233704_add_profile_combined_table/up.sql b/migrations/2024-12-05-233704_add_profile_combined_table/up.sql deleted file mode 100644 index 183529ee43..0000000000 --- a/migrations/2024-12-05-233704_add_profile_combined_table/up.sql +++ /dev/null @@ -1,30 +0,0 @@ --- Creates combined tables for --- Profile: (comment, post) -CREATE TABLE profile_combined ( - id serial PRIMARY KEY, - published timestamptz NOT NULL, - post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, - comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, - -- Make sure only one of the columns is not null - CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) -); - -CREATE INDEX idx_profile_combined_published ON profile_combined (published DESC, id DESC); - -CREATE INDEX idx_profile_combined_published_asc ON profile_combined (reverse_timestamp_sort (published) DESC, id DESC); - --- Updating the history -INSERT INTO profile_combined (published, post_id) -SELECT - published, - id -FROM - post; - -INSERT INTO profile_combined (published, comment_id) -SELECT - published, - id -FROM - comment; - diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 3f61534359..472728ded6 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -142,6 +142,7 @@ use lemmy_api_crud::{ }; use lemmy_apub::api::{ list_comments::list_comments, + list_person_content::list_person_content, list_posts::list_posts, read_community::get_community, read_person::read_person, @@ -338,6 +339,9 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { scope("/user") .wrap(rate_limit.message()) .route("", get().to(read_person)) + .route("/content", get().to(list_person_content)) + // TODO move this to /account/saved after http routes + // .route("/saved", get().to(read_person_saved)) .route("/mention", get().to(list_mentions)) .route( "/mention/mark_as_read", From 3abc46fad9861a7fd5582efc777a09238fb39b33 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 7 Dec 2024 15:46:46 -0500 Subject: [PATCH 22/43] Finishing up combined person_saved and person_content. --- crates/api/src/comment/save.rs | 5 +- crates/api/src/local_user/list_saved.rs | 40 + crates/api_common/src/person.rs | 7 +- crates/apub/src/api/user_settings_backup.rs | 5 +- .../db_schema/replaceable_schema/triggers.sql | 97 +- crates/db_schema/src/impls/comment.rs | 10 +- crates/db_schema/src/schema.rs | 2 + .../src/source/combined/person_saved.rs | 3 +- crates/db_schema/src/source/comment.rs | 3 + .../src/person_content_combined_view.rs | 6 +- .../src/person_saved_combined_view.rs | 947 ++++++++---------- .../down.sql | 1 + .../up.sql | 14 +- src/api_routes_http.rs | 3 +- 14 files changed, 531 insertions(+), 612 deletions(-) diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs index 6efa6296da..cca6d06bc6 100644 --- a/crates/api/src/comment/save.rs +++ b/crates/api/src/comment/save.rs @@ -16,10 +16,7 @@ pub async fn save_comment( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let comment_saved_form = CommentSavedForm { - comment_id: data.comment_id, - person_id: local_user_view.person.id, - }; + let comment_saved_form = CommentSavedForm::new(data.comment_id, local_user_view.person.id); if data.save { CommentSaved::save(&mut context.pool(), &comment_saved_form) diff --git a/crates/api/src/local_user/list_saved.rs b/crates/api/src/local_user/list_saved.rs index e69de29bb2..5f0deff39e 100644 --- a/crates/api/src/local_user/list_saved.rs +++ b/crates/api/src/local_user/list_saved.rs @@ -0,0 +1,40 @@ +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + person::{ListPersonSaved, ListPersonSavedResponse}, + utils::check_private_instance, +}; +use lemmy_db_views::{ + person_saved_combined_view::PersonSavedCombinedQuery, + structs::{LocalUserView, SiteView}, +}; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn list_person_saved( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let local_site = SiteView::read_local(&mut context.pool()).await?; + + check_private_instance(&Some(local_user_view.clone()), &local_site.local_site)?; + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let saved = PersonSavedCombinedQuery { + page_after, + page_back, + } + .list(&mut context.pool(), &local_user_view) + .await?; + + Ok(Json(ListPersonSavedResponse { saved })) +} diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 37f03ecaf4..92a5245435 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -11,6 +11,7 @@ use lemmy_db_views::structs::{ LocalImageView, PersonContentCombinedPaginationCursor, PersonContentCombinedView, + PersonSavedCombinedPaginationCursor, }; use lemmy_db_views_actor::structs::{ CommentReplyView, @@ -273,9 +274,9 @@ pub struct ListPersonContentResponse { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Gets your saved posts and comments -pub struct ListSaved { +pub struct ListPersonSaved { #[cfg_attr(feature = "full", ts(optional))] - pub page_cursor: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_back: Option, } @@ -285,7 +286,7 @@ pub struct ListSaved { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// A person's saved content response. -pub struct ListSavedResponse { +pub struct ListPersonSavedResponse { pub saved: Vec, } diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 6184df7d34..d98df25adf 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -212,10 +212,7 @@ pub async fn import_settings( &context, |(saved, context)| async move { let comment = saved.dereference(&context).await?; - let form = CommentSavedForm { - person_id, - comment_id: comment.id, - }; + let form = CommentSavedForm::new(comment.id, person_id); CommentSaved::save(&mut context.pool(), &form).await?; LemmyResult::Ok(()) }, diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 9768bb1d2f..91c4fb841f 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -714,33 +714,74 @@ CALL r.create_person_content_combined_trigger ('post'); CALL r.create_person_content_combined_trigger ('comment'); -- person_saved (comment, post) --- TODO, not sure how to handle changes to post_actions and comment_actions.saved column. --- False should delete this row, true should insert --- CREATE PROCEDURE r.create_person_saved_combined_trigger (table_name text) --- LANGUAGE plpgsql --- AS $a$ --- BEGIN --- EXECUTE replace($b$ CREATE FUNCTION r.person_saved_combined_thing_insert ( ) --- RETURNS TRIGGER --- LANGUAGE plpgsql --- AS $$ --- BEGIN --- INSERT INTO person_saved_combined (published, thing_id) --- VALUES (NEW.saved, NEW.id); --- RETURN NEW; --- END $$; --- CREATE TRIGGER person_saved_combined --- AFTER INSERT ON thing --- FOR EACH ROW --- EXECUTE FUNCTION r.person_saved_combined_thing_insert ( ); --- $b$, --- 'thing', --- table_name); --- END; --- $a$; - --- CALL r.create_person_saved_combined_trigger ('post_actions'); - --- CALL r.create_person_saved_combined_trigger ('comment_actions'); +-- TODO, not sure how to handle changes to post_actions and comment_actions.saved column using @dullbanana's trigger method. +-- Post +CREATE FUNCTION r.person_saved_combined_change_values_post () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'DELETE') THEN + DELETE FROM person_saved_combined AS p + WHERE p.person_id = OLD.person_id + AND p.post_id = OLD.post_id; + ELSIF (TG_OP = 'INSERT') THEN + IF NEW.saved IS NOT NULL THEN + INSERT INTO person_saved_combined (published, person_id, post_id) + VALUES (NEW.saved, NEW.person_id, NEW.post_id); + END IF; + ELSIF (TG_OP = 'UPDATE') THEN + IF NEW.saved IS NOT NULL THEN + INSERT INTO person_saved_combined (published, person_id, post_id) + VALUES (NEW.saved, NEW.person_id, NEW.post_id); + -- If saved gets set as null, delete the row + ELSE + DELETE FROM person_saved_combined AS p + WHERE p.person_id = NEW.person_id + AND p.post_id = NEW.post_id; + END IF; + END IF; + RETURN NULL; +END +$$; + +CREATE TRIGGER person_saved_combined_post + AFTER INSERT OR DELETE OR UPDATE OF saved ON post_actions + FOR EACH ROW + EXECUTE FUNCTION r.person_saved_combined_change_values_post (); +-- Comment +CREATE FUNCTION r.person_saved_combined_change_values_comment () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + IF (TG_OP = 'DELETE') THEN + DELETE FROM person_saved_combined AS p + WHERE p.person_id = OLD.person_id + AND p.comment_id = OLD.comment_id; + ELSIF (TG_OP = 'INSERT') THEN + IF NEW.saved IS NOT NULL THEN + INSERT INTO person_saved_combined (published, person_id, comment_id) + VALUES (NEW.saved, NEW.person_id, NEW.comment_id); + END IF; + ELSIF (TG_OP = 'UPDATE') THEN + IF NEW.saved IS NOT NULL THEN + INSERT INTO person_saved_combined (published, person_id, comment_id) + VALUES (NEW.saved, NEW.person_id, NEW.comment_id); + -- If saved gets set as null, delete the row + ELSE + DELETE FROM person_saved_combined AS p + WHERE p.person_id = NEW.person_id + AND p.comment_id = NEW.comment_id; + END IF; + END IF; + RETURN NULL; +END +$$; + +CREATE TRIGGER person_saved_combined_comment + AFTER INSERT OR DELETE OR UPDATE OF saved ON comment_actions + FOR EACH ROW + EXECUTE FUNCTION r.person_saved_combined_change_values_comment (); diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 7dcc033a17..17cd6ce5cd 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -184,10 +184,6 @@ impl Saveable for CommentSaved { comment_saved_form: &CommentSavedForm, ) -> Result { let conn = &mut get_conn(pool).await?; - let comment_saved_form = ( - comment_saved_form, - comment_actions::saved.eq(now().nullable()), - ); insert_into(comment_actions::table) .values(comment_saved_form) .on_conflict((comment_actions::comment_id, comment_actions::person_id)) @@ -319,11 +315,7 @@ mod tests { }; // Comment Saved - let comment_saved_form = CommentSavedForm { - comment_id: inserted_comment.id, - person_id: inserted_person.id, - }; - + let comment_saved_form = CommentSavedForm::new(inserted_comment.id, inserted_person.id); let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await?; let expected_comment_saved = CommentSaved { diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index dd6690849c..51e4304e7e 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -752,6 +752,7 @@ diesel::table! { person_saved_combined (id) { id -> Int4, published -> Timestamptz, + person_id -> Int4, post_id -> Nullable, comment_id -> Nullable, } @@ -1053,6 +1054,7 @@ diesel::joinable!(person_content_combined -> post (post_id)); diesel::joinable!(person_mention -> comment (comment_id)); diesel::joinable!(person_mention -> person (recipient_id)); diesel::joinable!(person_saved_combined -> comment (comment_id)); +diesel::joinable!(person_saved_combined -> person (person_id)); diesel::joinable!(person_saved_combined -> post (post_id)); diesel::joinable!(post -> community (community_id)); diesel::joinable!(post -> language (language_id)); diff --git a/crates/db_schema/src/source/combined/person_saved.rs b/crates/db_schema/src/source/combined/person_saved.rs index 08cdd04159..298360a6d5 100644 --- a/crates/db_schema/src/source/combined/person_saved.rs +++ b/crates/db_schema/src/source/combined/person_saved.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{CommentId, PersonSavedCombinedId, PostId}; +use crate::newtypes::{CommentId, PersonId, PersonSavedCombinedId, PostId}; #[cfg(feature = "full")] use crate::schema::person_saved_combined; use chrono::{DateTime, Utc}; @@ -23,6 +23,7 @@ use ts_rs::TS; pub struct PersonSavedCombined { pub id: PersonSavedCombinedId, pub published: DateTime, + pub person_id: PersonId, #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, #[cfg_attr(feature = "full", ts(optional))] diff --git a/crates/db_schema/src/source/comment.rs b/crates/db_schema/src/source/comment.rs index d4001807f7..cc5d8c20c1 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -142,7 +142,10 @@ pub struct CommentSaved { #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = comment_actions))] +#[derive(derive_new::new)] pub struct CommentSavedForm { pub comment_id: CommentId, pub person_id: PersonId, + #[new(value = "Utc::now()")] + pub saved: DateTime, } diff --git a/crates/db_views/src/person_content_combined_view.rs b/crates/db_views/src/person_content_combined_view.rs index 69be392ded..a9af32f401 100644 --- a/crates/db_views/src/person_content_combined_view.rs +++ b/crates/db_views/src/person_content_combined_view.rs @@ -121,8 +121,8 @@ impl PersonContentCombinedQuery { person::table.on( comment::creator_id .eq(item_creator) - // Need to filter out the post rows where both the post and comment creator are the - // same. + // Need to filter out the post rows where the post_id given is null + // Otherwise you'll get duped post rows .or( post::creator_id .eq(item_creator) @@ -372,7 +372,7 @@ mod tests { .await?; assert_eq!(3, timmy_content.len()); - // Make sure the report types are correct + // Make sure the types are correct if let PersonContentCombinedView::Comment(v) = &timmy_content[0] { assert_eq!(data.timmy_comment.id, v.comment.id); assert_eq!(data.timmy.id, v.creator.id); diff --git a/crates/db_views/src/person_saved_combined_view.rs b/crates/db_views/src/person_saved_combined_view.rs index 2e0a9f5bf6..d252dcca79 100644 --- a/crates/db_views/src/person_saved_combined_view.rs +++ b/crates/db_views/src/person_saved_combined_view.rs @@ -1,554 +1,393 @@ -// use crate::{ -// structs::{ -// CommentView, -// LocalUserView, -// PostView, -// ProfileCombinedPaginationCursor, -// PersonContentCombinedView, -// PersonContentViewInternal, -// }, -// InternalToCombinedView, -// }; -// use diesel::{ -// result::Error, -// BoolExpressionMethods, -// ExpressionMethods, -// JoinOnDsl, -// NullableExpressionMethods, -// QueryDsl, -// SelectableHelper, -// }; -// use diesel_async::RunQueryDsl; -// use i_love_jesus::PaginatedQueryBuilder; -// use lemmy_db_schema::{ -// aliases::creator_community_actions, -// newtypes::{CommunityId, PersonId}, -// schema::{ -// comment, -// comment_actions, -// comment_aggregates, -// community, -// community_actions, -// image_details, -// local_user, -// person, -// person_actions, -// post, -// post_actions, -// post_aggregates, -// profile_combined, -// }, -// source::{ -// combined::profile::{profile_combined_keys as key, ProfileCombined}, -// community::CommunityFollower, -// }, -// utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, -// }; -// use lemmy_utils::error::LemmyResult; - -// impl ProfileCombinedPaginationCursor { -// // get cursor for page that starts immediately after the given post -// pub fn after_post(view: &PersonContentCombinedView) -> ProfileCombinedPaginationCursor { -// let (prefix, id) = match view { -// PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), -// PersonContentCombinedView::Post(v) => ('P', v.post.id.0), -// }; -// // hex encoding to prevent ossification -// ProfileCombinedPaginationCursor(format!("{prefix}{id:x}")) -// } - -// pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { -// let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); -// let mut query = profile_combined::table -// .select(ProfileCombined::as_select()) -// .into_boxed(); -// let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; -// let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; -// query = match prefix { -// "C" => query.filter(profile_combined::comment_id.eq(id)), -// "P" => query.filter(profile_combined::post_id.eq(id)), -// _ => return Err(err_msg()), -// }; -// let token = query.first(&mut get_conn(pool).await?).await?; - -// Ok(PaginationCursorData(token)) -// } -// } - -// #[derive(Clone)] -// pub struct PaginationCursorData(ProfileCombined); - -// #[derive(Default)] -// pub struct ProfileCombinedQuery { -// pub creator_id: PersonId, -// pub page_after: Option, -// pub page_back: Option, -// } - -// impl ProfileCombinedQuery { -// pub async fn list( -// self, -// pool: &mut DbPool<'_>, -// user: &Option, -// ) -> LemmyResult> { -// let my_person_id = user -// .as_ref() -// .map(|u| u.local_user.person_id) -// .unwrap_or(PersonId(-1)); -// let item_creator = person::id; - -// let conn = &mut get_conn(pool).await?; - -// // Notes: since the post_id and comment_id are optional columns, -// // many joins must use an OR condition. -// // For example, the creator must be the person table joined to either: -// // - post.creator_id -// // - comment.creator_id -// let mut query = profile_combined::table -// // The comment -// .left_join(comment::table.on(profile_combined::comment_id.eq(comment::id.nullable()))) -// // The post -// .inner_join( -// post::table.on( -// profile_combined::post_id -// .eq(post::id.nullable()) -// .or(comment::post_id.nullable().eq(profile_combined::post_id)), -// ), -// ) -// // The item creator -// .inner_join( -// person::table.on( -// comment::creator_id -// .eq(person::id) -// .or(post::creator_id.eq(person::id)), -// ), -// ) -// // The community -// .inner_join(community::table.on(post::community_id.eq(community::id))) -// .left_join(actions_alias( -// creator_community_actions, -// item_creator, -// post::community_id, -// )) -// .left_join( -// local_user::table.on( -// item_creator -// .eq(local_user::person_id) -// .and(local_user::admin.eq(true)), -// ), -// ) -// .left_join(actions( -// community_actions::table, -// Some(my_person_id), -// post::community_id, -// )) -// .left_join(actions(post_actions::table, Some(my_person_id), post::id)) -// .left_join(actions( -// person_actions::table, -// Some(my_person_id), -// item_creator, -// )) -// .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) -// .left_join( -// comment_aggregates::table -// .on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), -// ) -// .left_join(actions( -// comment_actions::table, -// Some(my_person_id), -// comment::id, -// )) -// .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) -// // The creator id filter -// .filter(item_creator.eq(self.creator_id)) -// .select(( -// // Post-specific -// post_aggregates::all_columns, -// coalesce( -// post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), -// post_aggregates::comments, -// ), -// post_actions::saved.nullable().is_not_null(), -// post_actions::read.nullable().is_not_null(), -// post_actions::hidden.nullable().is_not_null(), -// post_actions::like_score.nullable(), -// image_details::all_columns.nullable(), -// // Comment-specific -// comment::all_columns.nullable(), -// comment_aggregates::all_columns.nullable(), -// comment_actions::saved.nullable().is_not_null(), -// comment_actions::like_score.nullable(), -// // Shared -// post::all_columns, -// community::all_columns, -// person::all_columns, -// CommunityFollower::select_subscribed_type(), -// local_user::admin.nullable().is_not_null(), -// creator_community_actions -// .field(community_actions::became_moderator) -// .nullable() -// .is_not_null(), -// creator_community_actions -// .field(community_actions::received_ban) -// .nullable() -// .is_not_null(), -// person_actions::blocked.nullable().is_not_null(), -// community_actions::received_ban.nullable().is_not_null(), -// )) -// .into_boxed(); - -// let mut query = PaginatedQueryBuilder::new(query); - -// let page_after = self.page_after.map(|c| c.0); - -// if self.page_back.unwrap_or_default() { -// query = query.before(page_after).limit_and_offset_from_end(); -// } else { -// query = query.after(page_after); -// } - -// // Sorting by published -// query = query -// .then_desc(ReverseTimestampKey(key::published)) -// // Tie breaker -// .then_desc(key::id); - -// let res = query.load::(conn).await?; - -// // Map the query results to the enum -// let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); - -// Ok(out) -// } -// } - -// impl InternalToCombinedView for PersonContentViewInternal { -// type CombinedView = PersonContentCombinedView; - -// fn map_to_enum(&self) -> Option { -// // Use for a short alias -// let v = self.clone(); - -// if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { -// Some(PersonContentCombinedView::Comment(CommentView { -// comment, -// counts, -// post: v.post, -// community: v.community, -// creator: v.item_creator, -// creator_banned_from_community: v.item_creator_banned_from_community, -// creator_is_moderator: v.item_creator_is_moderator, -// creator_is_admin: v.item_creator_is_admin, -// creator_blocked: v.item_creator_blocked, -// subscribed: v.subscribed, -// saved: v.comment_saved, -// my_vote: v.my_comment_vote, -// banned_from_community: v.banned_from_community, -// })) -// } else { -// Some(PersonContentCombinedView::Post(PostView { -// post: v.post, -// community: v.community, -// unread_comments: v.post_unread_comments, -// counts: v.post_counts, -// creator: v.item_creator, -// creator_banned_from_community: v.item_creator_banned_from_community, -// creator_is_moderator: v.item_creator_is_moderator, -// creator_is_admin: v.item_creator_is_admin, -// creator_blocked: v.item_creator_blocked, -// subscribed: v.subscribed, -// saved: v.post_saved, -// read: v.post_read, -// hidden: v.post_hidden, -// my_vote: v.my_post_vote, -// image_details: v.image_details, -// banned_from_community: v.banned_from_community, -// })) -// } -// } -// } - -// #[cfg(test)] -// #[expect(clippy::indexing_slicing)] -// mod tests { - -// use crate::{ -// profile_combined_view::ProfileCombinedQuery, -// report_combined_view::ReportCombinedQuery, -// structs::{ -// CommentReportView, -// LocalUserView, -// PostReportView, -// PersonContentCombinedView, -// ReportCombinedView, -// ReportCombinedViewInternal, -// }, -// }; -// use lemmy_db_schema::{ -// aggregates::structs::{CommentAggregates, PostAggregates}, -// assert_length, -// source::{ -// comment::{Comment, CommentInsertForm, CommentSaved, CommentSavedForm}, -// comment_report::{CommentReport, CommentReportForm}, -// community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, -// instance::Instance, -// local_user::{LocalUser, LocalUserInsertForm}, -// local_user_vote_display_mode::LocalUserVoteDisplayMode, -// person::{Person, PersonInsertForm}, -// post::{Post, PostInsertForm}, -// post_report::{PostReport, PostReportForm}, -// private_message::{PrivateMessage, PrivateMessageInsertForm}, -// private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, -// }, -// traits::{Crud, Joinable, Reportable, Saveable}, -// utils::{build_db_pool_for_tests, DbPool}, -// }; -// use lemmy_utils::error::LemmyResult; -// use pretty_assertions::assert_eq; -// use serial_test::serial; - -// struct Data { -// instance: Instance, -// timmy: Person, -// sara: Person, -// timmy_view: LocalUserView, -// community: Community, -// timmy_post: Post, -// timmy_post_2: Post, -// sara_post: Post, -// timmy_comment: Comment, -// sara_comment: Comment, -// sara_comment_2: Comment, -// } - -// async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { -// let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - -// let timmy_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_pcv"); -// let inserted_timmy = Person::create(pool, &timmy_form).await?; -// let timmy_local_user_form = LocalUserInsertForm::test_form(inserted_timmy.id); -// let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; -// let timmy_view = LocalUserView { -// local_user: timmy_local_user, -// local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), -// person: inserted_timmy.clone(), -// counts: Default::default(), -// }; - -// let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_pcv"); -// let inserted_sara = Person::create(pool, &sara_form).await?; - -// let community_form = CommunityInsertForm::new( -// inserted_instance.id, -// "test community pcv".to_string(), -// "nada".to_owned(), -// "pubkey".to_string(), -// ); -// let inserted_community = Community::create(pool, &community_form).await?; - -// let timmy_post_form = PostInsertForm::new( -// "timmy post prv".into(), -// inserted_timmy.id, -// inserted_community.id, -// ); -// let timmy_post = Post::create(pool, &timmy_post_form).await?; - -// let timmy_post_form_2 = PostInsertForm::new( -// "timmy post prv 2".into(), -// inserted_timmy.id, -// inserted_community.id, -// ); -// let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; - -// let sara_post_form = PostInsertForm::new( -// "sara post prv".into(), -// inserted_sara.id, -// inserted_community.id, -// ); -// let sara_post = Post::create(pool, &sara_post_form).await?; - -// let timmy_comment_form = -// CommentInsertForm::new(inserted_timmy.id, timmy_post.id, "timmy comment prv".into()); -// let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; - -// let sara_comment_form = -// CommentInsertForm::new(inserted_sara.id, timmy_post.id, "sara comment prv".into()); -// let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; - -// let sara_comment_form_2 = CommentInsertForm::new( -// inserted_sara.id, -// timmy_post_2.id, -// "sara comment prv 2".into(), -// ); -// let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; - -// Ok(Data { -// instance: inserted_instance, -// timmy: inserted_timmy, -// sara: inserted_sara, -// timmy_view, -// community: inserted_community, -// timmy_post, -// timmy_post_2, -// sara_post, -// timmy_comment, -// sara_comment, -// sara_comment_2, -// }) -// } - -// async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { -// Instance::delete(pool, data.instance.id).await?; - -// Ok(()) -// } - -// #[tokio::test] -// #[serial] -// async fn test_combined() -> LemmyResult<()> { -// let pool = &build_db_pool_for_tests(); -// let pool = &mut pool.into(); -// let data = init_data(pool).await?; - -// // Do a batch read of timmy -// let timmy_content = ProfileCombinedQuery::default().list(pool, &None).await?; -// assert_eq!(3, timmy_content.len()); - -// // Make sure the report types are correct -// if let PersonContentCombinedView::Comment(v) = &timmy_content[0] { -// assert_eq!(data.timmy_comment.id, v.comment.id); -// assert_eq!(data.timmy.id, v.creator.id); -// } else { -// panic!("wrong type"); -// } -// if let PersonContentCombinedView::Post(v) = &timmy_content[1] { -// assert_eq!(data.timmy_post_2.id, v.post.id); -// assert_eq!(data.timmy.id, v.post.creator_id); -// } else { -// panic!("wrong type"); -// } -// if let PersonContentCombinedView::Post(v) = &timmy_content[2] { -// assert_eq!(data.timmy_post.id, v.post.id); -// assert_eq!(data.timmy.id, v.post.creator_id); -// } else { -// panic!("wrong type"); -// } - -// // Do a batch read of sara -// let sara_content = ProfileCombinedQuery::default().list(pool, &None).await?; -// assert_eq!(3, sara_content.len()); - -// // Make sure the report types are correct -// if let PersonContentCombinedView::Comment(v) = &sara_content[0] { -// assert_eq!(data.sara_comment_2.id, v.comment.id); -// assert_eq!(data.sara.id, v.creator.id); -// // This one was to timmy_post_2 -// assert_eq!(data.timmy_post_2.id, v.post.id); -// assert_eq!(data.timmy.id, v.post.creator_id); -// } else { -// panic!("wrong type"); -// } -// if let PersonContentCombinedView::Comment(v) = &sara_content[1] { -// assert_eq!(data.sara_comment.id, v.comment.id); -// assert_eq!(data.sara.id, v.creator.id); -// assert_eq!(data.timmy_post.id, v.post.id); -// assert_eq!(data.timmy.id, v.post.creator_id); -// } else { -// panic!("wrong type"); -// } -// if let PersonContentCombinedView::Post(v) = &sara_content[2] { -// assert_eq!(data.timmy_post.id, v.post.id); -// assert_eq!(data.timmy.id, v.post.creator_id); -// } else { -// panic!("wrong type"); -// } - -// // Timmy saves sara's comment, and his 2nd post -// let save_comment_0_form = CommentSavedForm { -// person_id: data.timmy.id, -// comment_id: data.sara_comment.id, -// }; -// CommentSaved::save(pool, &save_comment_0_form).await?; - -// // Timmy saves sara's comment, and his 2nd post -// let save_comment_0_form = CommentSavedForm { -// person_id: data.timmy.id, -// comment_id: data.sara_comment.id, -// }; -// CommentSaved::save(pool, &save_comment_0_form).await?; - -// // Do a saved_only query -// let timmy_content_saved_only = ProfileCombinedQuery {}.list(pool, &None).await?; - -// cleanup(data, pool).await?; - -// Ok(()) -// } -// } -// #[tokio::test] -// #[serial] -// async fn test_saved_order() -> LemmyResult<()> { -// let pool = &build_db_pool_for_tests(); -// let pool = &mut pool.into(); -// let data = init_data(pool).await?; - -// // Save two comments -// let save_comment_0_form = CommentSavedForm { -// person_id: data.timmy_local_user_view.person.id, -// comment_id: data.inserted_comment_0.id, -// }; -// CommentSaved::save(pool, &save_comment_0_form).await?; - -// let save_comment_2_form = CommentSavedForm { -// person_id: data.timmy_local_user_view.person.id, -// comment_id: data.inserted_comment_2.id, -// }; -// CommentSaved::save(pool, &save_comment_2_form).await?; - -// // Fetch the saved comments -// let comments = CommentQuery { -// local_user: Some(&data.timmy_local_user_view.local_user), -// saved_only: Some(true), -// ..Default::default() -// } -// .list(&data.site, pool) -// .await?; - -// // There should only be two comments -// assert_eq!(2, comments.len()); - -// // The first comment, should be the last one saved (descending order) -// assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); - -// // The second comment, should be the first one saved -// assert_eq!(comments[1].comment.id, data.inserted_comment_0.id); - -// cleanup(data, pool).await -// } -// #[tokio::test] -// #[serial] -// async fn post_listing_saved_only() -> LemmyResult<()> { -// let pool = &build_db_pool()?; -// let pool = &mut pool.into(); -// let data = init_data(pool).await?; - -// // Save only the bot post -// // The saved_only should only show the bot post -// let post_save_form = -// PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); -// PostSaved::save(pool, &post_save_form).await?; - -// // Read the saved only -// let read_saved_post_listing = PostQuery { -// community_id: Some(data.inserted_community.id), -// saved_only: Some(true), -// ..data.default_post_query() -// } -// .list(&data.site, pool) -// .await?; - -// // This should only include the bot post, not the one you created -// assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); - -// cleanup(data, pool).await -// } +use crate::{ + structs::{ + LocalUserView, + PersonContentCombinedView, + PersonContentViewInternal, + PersonSavedCombinedPaginationCursor, + }, + InternalToCombinedView, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::creator_community_actions, + schema::{ + comment, + comment_actions, + comment_aggregates, + community, + community_actions, + image_details, + local_user, + person, + person_actions, + person_saved_combined, + post, + post_actions, + post_aggregates, + }, + source::{ + combined::person_saved::{person_saved_combined_keys as key, PersonSavedCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, +}; +use lemmy_utils::error::LemmyResult; + +impl PersonSavedCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &PersonContentCombinedView) -> PersonSavedCombinedPaginationCursor { + let (prefix, id) = match view { + PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), + PersonContentCombinedView::Post(v) => ('P', v.post.id.0), + }; + // hex encoding to prevent ossification + PersonSavedCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = person_saved_combined::table + .select(PersonSavedCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(person_saved_combined::comment_id.eq(id)), + "P" => query.filter(person_saved_combined::post_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(PersonSavedCombined); + +#[derive(Default)] +pub struct PersonSavedCombinedQuery { + pub page_after: Option, + pub page_back: Option, +} + +impl PersonSavedCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &LocalUserView, + ) -> LemmyResult> { + let my_person_id = user.local_user.person_id; + let item_creator = person::id; + + let conn = &mut get_conn(pool).await?; + + // Notes: since the post_id and comment_id are optional columns, + // many joins must use an OR condition. + // For example, the creator must be the person table joined to either: + // - post.creator_id + // - comment.creator_id + let query = person_saved_combined::table + // The comment + .left_join(comment::table.on(person_saved_combined::comment_id.eq(comment::id.nullable()))) + // The post + // It gets a bit complicated here, because since both comments and post combined have a post + // attached, you can do an inner join. + .inner_join( + post::table.on( + person_saved_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.eq(post::id)), + ), + ) + // The item creator + .inner_join( + person::table.on( + comment::creator_id + .eq(item_creator) + // Need to filter out the post rows where the post_id given is null + // Otherwise you'll get duped post rows + .or( + post::creator_id + .eq(item_creator) + .and(person_saved_combined::post_id.is_not_null()), + ), + ), + ) + // The community + .inner_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .left_join( + comment_aggregates::table + .on(person_saved_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), + ) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment::id, + )) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + // The person id filter + .filter(person_saved_combined::person_id.eq(my_person_id)) + .select(( + // Post-specific + post_aggregates::all_columns, + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + // Comment-specific + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Shared + post::all_columns, + community::all_columns, + person::all_columns, + CommunityFollower::select_subscribed_type(), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + )) + .into_boxed(); + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // Sorting by published + query = query + .then_desc(key::published) + // Tie breaker + .then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + person_saved_combined_view::PersonSavedCombinedQuery, + structs::{LocalUserView, PersonContentCombinedView}, + }; + use lemmy_db_schema::{ + source::{ + comment::{Comment, CommentInsertForm, CommentSaved, CommentSavedForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + local_user_vote_display_mode::LocalUserVoteDisplayMode, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm, PostSaved, PostSavedForm}, + }, + traits::{Crud, Saveable}, + utils::{build_db_pool_for_tests, DbPool}, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + timmy_view: LocalUserView, + sara: Person, + timmy_post: Post, + sara_comment: Comment, + sara_comment_2: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv"); + let timmy = Person::create(pool, &timmy_form).await?; + let timmy_local_user_form = LocalUserInsertForm::test_form(timmy.id); + let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; + let timmy_view = LocalUserView { + local_user: timmy_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: timmy.clone(), + counts: Default::default(), + }; + + let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); + let sara = Person::create(pool, &sara_form).await?; + + let community_form = CommunityInsertForm::new( + instance.id, + "test community pcv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + let timmy_post_form = PostInsertForm::new("timmy post prv".into(), timmy.id, community.id); + let timmy_post = Post::create(pool, &timmy_post_form).await?; + + let timmy_post_form_2 = PostInsertForm::new("timmy post prv 2".into(), timmy.id, community.id); + let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + + let sara_post_form = PostInsertForm::new("sara post prv".into(), sara.id, community.id); + let _sara_post = Post::create(pool, &sara_post_form).await?; + + let timmy_comment_form = + CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv".into()); + let _timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + + let sara_comment_form = + CommentInsertForm::new(sara.id, timmy_post.id, "sara comment prv".into()); + let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + + let sara_comment_form_2 = + CommentInsertForm::new(sara.id, timmy_post_2.id, "sara comment prv 2".into()); + let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + + Ok(Data { + instance, + timmy, + timmy_view, + sara, + timmy_post, + sara_comment, + sara_comment_2, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Do a batch read of timmy saved + let timmy_saved = PersonSavedCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_eq!(0, timmy_saved.len()); + + // Save a few things + let save_sara_comment_2 = + CommentSavedForm::new(data.sara_comment_2.id, data.timmy_view.person.id); + CommentSaved::save(pool, &save_sara_comment_2).await?; + + let save_sara_comment = CommentSavedForm::new(data.sara_comment.id, data.timmy_view.person.id); + CommentSaved::save(pool, &save_sara_comment).await?; + + let post_save_form = PostSavedForm::new(data.timmy_post.id, data.timmy.id); + PostSaved::save(pool, &post_save_form).await?; + + let timmy_saved = PersonSavedCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_eq!(3, timmy_saved.len()); + + // Make sure the types and order are correct + if let PersonContentCombinedView::Post(v) = &timmy_saved[0] { + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Comment(v) = &timmy_saved[1] { + assert_eq!(data.sara_comment.id, v.comment.id); + assert_eq!(data.sara.id, v.comment.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Comment(v) = &timmy_saved[2] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.sara.id, v.comment.creator_id); + } else { + panic!("wrong type"); + } + + // Try unsaving 2 things + CommentSaved::unsave(pool, &save_sara_comment).await?; + PostSaved::unsave(pool, &post_save_form).await?; + + let timmy_saved = PersonSavedCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_eq!(1, timmy_saved.len()); + + if let PersonContentCombinedView::Comment(v) = &timmy_saved[0] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.sara.id, v.comment.creator_id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql index a8db9ec61d..0733315a71 100644 --- a/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql @@ -1,3 +1,4 @@ DROP TABLE person_content_combined; + DROP TABLE person_saved_combined; diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql index 40ced8238f..a53f52925f 100644 --- a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql @@ -33,6 +33,7 @@ FROM CREATE TABLE person_saved_combined ( id serial PRIMARY KEY, published timestamptz NOT NULL, + person_id int NOT NULL REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE, post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, -- Make sure only one of the columns is not null @@ -43,23 +44,26 @@ CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (publi CREATE INDEX idx_person_saved_combined_published_asc ON person_saved_combined (reverse_timestamp_sort (published) DESC, id DESC); +CREATE INDEX idx_person_saved_combined ON person_saved_combined (person_id); + -- Updating the history -INSERT INTO person_saved_combined (published, post_id) +INSERT INTO person_saved_combined (published, person_id, post_id) SELECT saved, + person_id, post_id FROM post_actions -WHERE +WHERE saved IS NOT NULL; -INSERT INTO person_saved_combined (published, comment_id) +INSERT INTO person_saved_combined (published, person_id, comment_id) SELECT saved, + person_id, comment_id FROM comment_actions -WHERE +WHERE saved IS NOT NULL; - diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 472728ded6..74764ac7d2 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -31,6 +31,7 @@ use lemmy_api::{ list_banned::list_banned_users, list_logins::list_logins, list_media::list_media, + list_saved::list_person_saved, login::login, logout::logout, notifications::{ @@ -341,7 +342,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("", get().to(read_person)) .route("/content", get().to(list_person_content)) // TODO move this to /account/saved after http routes - // .route("/saved", get().to(read_person_saved)) + .route("/saved", get().to(list_person_saved)) .route("/mention", get().to(list_mentions)) .route( "/mention/mark_as_read", From 67d72e36ac2c52932067fa9f3d447cf1c4c459db Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 7 Dec 2024 21:19:49 -0500 Subject: [PATCH 23/43] Fixing api tests. --- api_tests/package.json | 2 +- api_tests/pnpm-lock.yaml | 10 +++++----- api_tests/src/community.spec.ts | 5 ++--- api_tests/src/follow.spec.ts | 1 - api_tests/src/image.spec.ts | 1 - api_tests/src/post.spec.ts | 1 - api_tests/src/shared.ts | 4 +++- src/api_routes_v3.rs | 6 ------ src/api_routes_v4.rs | 32 +++++++++++++------------------- 9 files changed, 24 insertions(+), 38 deletions(-) diff --git a/api_tests/package.json b/api_tests/package.json index 7ea21d0ba0..9653886256 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -28,7 +28,7 @@ "eslint": "^9.14.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-api-v4.16", + "lemmy-js-client": "0.20.0-reports-combined.3", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 496606e6c2..198062652b 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^29.5.0 version: 29.7.0(@types/node@22.9.0) lemmy-js-client: - specifier: 0.20.0-api-v4.16 - version: 0.20.0-api-v4.16 + specifier: 0.20.0-reports-combined.3 + version: 0.20.0-reports-combined.3 prettier: specifier: ^3.2.5 version: 3.3.3 @@ -1167,8 +1167,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-api-v4.16: - resolution: {integrity: sha512-9Wn7b8YT2KnEA286+RV1B3mLmecAynvAERoC0ZZiccfSgkEvd3rG9A5X9ejiPqp+JzDZJeisO57+Ut4QHr5oTw==} + lemmy-js-client@0.20.0-reports-combined.3: + resolution: {integrity: sha512-0Z/9S41r6NM8f09Gkxerq9zYBE6UcywXfeWNxsYknkyh0ZnKbtNxjTkSxE6JpRbz7wokKFRSH9NpwgNloQY5uw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -3077,7 +3077,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-api-v4.16: {} + lemmy-js-client@0.20.0-reports-combined.3: {} leven@3.1.0: {} diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index 2bb0920881..2d1570ea6f 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -16,7 +16,6 @@ import { followCommunity, banPersonFromCommunity, resolvePerson, - getSite, createPost, getPost, resolvePost, @@ -36,7 +35,7 @@ import { userBlockInstance, } from "./shared"; import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; -import { EditCommunity, EditSite, GetPosts } from "lemmy-js-client"; +import { EditCommunity, GetPosts } from "lemmy-js-client"; beforeAll(setupLogins); afterAll(unfollows); @@ -573,7 +572,7 @@ test("Remote mods can edit communities", async () => { communityRes.community_view.community.id, ); - await expect(alphaCommunity.community_view.community.description).toBe( + expect(alphaCommunity.community_view.community.description).toBe( "Example description", ); }); diff --git a/api_tests/src/follow.spec.ts b/api_tests/src/follow.spec.ts index 936ce26065..c447e14cd7 100644 --- a/api_tests/src/follow.spec.ts +++ b/api_tests/src/follow.spec.ts @@ -5,7 +5,6 @@ import { setupLogins, resolveBetaCommunity, followCommunity, - getSite, waitUntil, beta, betaUrl, diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index a3478081ad..4d1abbdfd9 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -18,7 +18,6 @@ import { epsilon, followCommunity, gamma, - getSite, imageFetchLimit, registerUser, resolveBetaCommunity, diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 13c436e68d..37381d3023 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -29,7 +29,6 @@ import { reportPost, randomString, registerUser, - getSite, unfollows, resolveCommunity, waitUntil, diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index a3c560d8f7..b10d57002c 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -207,7 +207,9 @@ async function allowInstance(api: LemmyHttp, instance: string) { // Ignore errors from duplicate allows (because setup gets called for each test file) try { await api.adminAllowInstance(params); - } catch {} + } catch (error) { + console.error(error); + } } export async function createPost( diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index be1ae14ef0..5e8fb741dd 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.rs @@ -58,7 +58,6 @@ use lemmy_api::{ comment_report::{create::create_comment_report, resolve::resolve_comment_report}, post_report::{create::create_post_report, resolve::resolve_post_report}, private_message_report::{create::create_pm_report, resolve::resolve_pm_report}, - report_combined::list::list_reports, }, site::{ federated_instances::get_federated_instances, @@ -239,11 +238,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/report", post().to(create_comment_report)) .route("/report/resolve", put().to(resolve_comment_report)), ) - .service( - scope("report") - .wrap(rate_limit.message()) - .route("/list", get().to(list_reports)), - ) // Private Message .service( scope("/private_message") diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index a9f71c9da9..b03be60f2e 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -6,11 +6,6 @@ use lemmy_api::{ list_comment_likes::list_comment_likes, save::save_comment, }, - comment_report::{ - create::create_comment_report, - list::list_comment_reports, - resolve::resolve_comment_report, - }, community::{ add_mod::add_mod_to_community, ban::ban_from_community, @@ -65,16 +60,12 @@ use lemmy_api::{ mark_read::mark_post_as_read, save::save_post, }, - post_report::{ - create::create_post_report, - list::list_post_reports, - resolve::resolve_post_report, - }, private_message::mark_read::mark_pm_as_read, - private_message_report::{ - create::create_pm_report, - list::list_pm_reports, - resolve::resolve_pm_report, + reports::{ + comment_report::{create::create_comment_report, resolve::resolve_comment_report}, + post_report::{create::create_post_report, resolve::resolve_post_report}, + private_message_report::{create::create_pm_report, resolve::resolve_pm_report}, + report_combined::list::list_reports, }, site::{ admin_allow_instance::admin_allow_instance, @@ -235,7 +226,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_post)) .route("/report", post().to(create_post_report)) .route("/report/resolve", put().to(resolve_post_report)) - .route("/report/list", get().to(list_post_reports)) .route("/site_metadata", get().to(get_link_metadata)), ) // Comment @@ -259,8 +249,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_comment)) .route("/list", get().to(list_comments)) .route("/report", post().to(create_comment_report)) - .route("/report/resolve", put().to(resolve_comment_report)) - .route("/report/list", get().to(list_comment_reports)), + .route("/report/resolve", put().to(resolve_comment_report)), ) // Private Message .service( @@ -271,8 +260,13 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/delete", post().to(delete_private_message)) .route("/mark_as_read", post().to(mark_pm_as_read)) .route("/report", post().to(create_pm_report)) - .route("/report/resolve", put().to(resolve_pm_report)) - .route("/report/list", get().to(list_pm_reports)), + .route("/report/resolve", put().to(resolve_pm_report)), + ) + // Reports + .service( + scope("/report") + .wrap(rate_limit.message()) + .route("/list", get().to(list_reports)), ) // User .service( From 5d2b6115f51ffe54b1a97fb8f46e1c762900d2c5 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 7 Dec 2024 21:25:18 -0500 Subject: [PATCH 24/43] Moving to api-v4 routes. --- src/api_routes_v3.rs | 3 --- src/api_routes_v4.rs | 9 +++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index e0f51dcc4d..1c6dac0471 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.rs @@ -304,9 +304,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { scope("/user") .wrap(rate_limit.message()) .route("", get().to(read_person)) - .route("/content", get().to(list_person_content)) - // TODO move this to /account/saved after http routes - .route("/saved", get().to(list_person_saved)) .route("/mention", get().to(list_mentions)) .route( "/mention/mark_as_read", diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index b03be60f2e..2a1082d2b0 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -282,7 +282,8 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/change_password", put().to(change_password)) .route("/totp/generate", post().to(generate_totp_secret)) .route("/totp/update", post().to(update_totp)) - .route("/verify_email", post().to(verify_email)), + .route("/verify_email", post().to(verify_email)) + .route("/saved", get().to(list_person_saved)), ) .route("/account/settings/save", put().to(save_user_settings)) .service( @@ -318,7 +319,11 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { ), ) // User actions - .route("/person", get().to(read_person)) + .service( + scope("/person") + .route("", get().to(read_person)) + .route("/content", get().to(list_person_content)), + ) // Admin Actions .service( scope("/admin") From a0f2966e43b94c8012baf66199fb1a66d8f084ec Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 7 Dec 2024 21:27:32 -0500 Subject: [PATCH 25/43] Fixing imports. --- src/api_routes_v3.rs | 2 -- src/api_routes_v4.rs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index 1c6dac0471..5e8fb741dd 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.rs @@ -25,7 +25,6 @@ use lemmy_api::{ list_banned::list_banned_users, list_logins::list_logins, list_media::list_media, - list_saved::list_person_saved, login::login, logout::logout, notifications::{ @@ -118,7 +117,6 @@ use lemmy_api_crud::{ }; use lemmy_apub::api::{ list_comments::list_comments, - list_person_content::list_person_content, list_posts::list_posts, read_community::get_community, read_person::read_person, diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index 2a1082d2b0..9f2b8d2894 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -31,6 +31,7 @@ use lemmy_api::{ list_banned::list_banned_users, list_logins::list_logins, list_media::list_media, + list_saved::list_person_saved, login::login, logout::logout, notifications::{ @@ -143,6 +144,7 @@ use lemmy_api_crud::{ }; use lemmy_apub::api::{ list_comments::list_comments, + list_person_content::list_person_content, list_posts::list_posts, read_community::get_community, read_person::read_person, From 165c7f47628441c93794f8623f332517b7306295 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 08:40:58 -0500 Subject: [PATCH 26/43] Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas --- crates/db_views/src/report_combined_view.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 5944568e09..a61663b718 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -79,9 +79,9 @@ impl ReportCombinedViewInternal { Some(my_person_id), post::community_id, )) - .filter(post_report::resolved.eq(false)) - .or_filter(comment_report::resolved.eq(false)) - .or_filter(private_message_report::resolved.eq(false)) + .filter(post_report::resolved + .or(comment_report::resolved) + .or(private_message_report::resolved).is_distinct_from(true)) .into_boxed(); if let Some(community_id) = community_id { From dc0ec159dfc9dd88dfb2cb95ea1018e15861bf00 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 08:42:05 -0500 Subject: [PATCH 27/43] Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas --- crates/db_views/src/report_combined_view.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index a61663b718..69f4e825db 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -187,8 +187,7 @@ impl ReportCombinedQuery { .or(comment::post_id.eq(post::id)), ), ) - // The item creator - // You can now use aliases::person1.field(person::id) / item_creator for all the item actions + // The item creator (`item_creator` is the id of this person) .inner_join( aliases::person1.on( post::creator_id From 22d869730da64737587b9430fd733215d22315e4 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 08:42:27 -0500 Subject: [PATCH 28/43] Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas --- crates/db_views/src/report_combined_view.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 69f4e825db..f09b041f1b 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -306,9 +306,9 @@ impl ReportCombinedQuery { query = query .filter( post_report::resolved - .eq(false) - .or(comment_report::resolved.eq(false)) - .or(private_message_report::resolved.eq(false)), + + .or(comment_report::resolved) + .or(private_message_report::resolved).is_distinct_from(true), ) // TODO: when a `then_asc` method is added, use it here, make the id sort direction match, // and remove the separate index; unless additional columns are added to this sort From fa31fc37d0fb93157d6976f0b5d6e4ed9b8b0769 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 08:44:37 -0500 Subject: [PATCH 29/43] Update migrations/2024-12-02-181601_add_report_combined_table/up.sql Co-authored-by: dullbananas --- migrations/2024-12-02-181601_add_report_combined_table/up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/2024-12-02-181601_add_report_combined_table/up.sql b/migrations/2024-12-02-181601_add_report_combined_table/up.sql index 40dd9b277a..4d450269c7 100644 --- a/migrations/2024-12-02-181601_add_report_combined_table/up.sql +++ b/migrations/2024-12-02-181601_add_report_combined_table/up.sql @@ -7,7 +7,7 @@ CREATE TABLE report_combined ( comment_report_id int UNIQUE REFERENCES comment_report ON UPDATE CASCADE ON DELETE CASCADE, private_message_report_id int UNIQUE REFERENCES private_message_report ON UPDATE CASCADE ON DELETE CASCADE, -- Make sure only one of the columns is not null - CHECK ((post_report_id IS NOT NULL)::integer + (comment_report_id IS NOT NULL)::integer + (private_message_report_id IS NOT NULL)::integer = 1) + CHECK (num_nonnulls (post_report_id, comment_report_id, private_message_report_id) = 1) ); CREATE INDEX idx_report_combined_published ON report_combined (published DESC, id DESC); From 10f5e373cf0e282c113a86f41bc0753fa3db7ca2 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 08:45:49 -0500 Subject: [PATCH 30/43] Update migrations/2024-12-02-181601_add_report_combined_table/up.sql Co-authored-by: dullbananas --- .../up.sql | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/migrations/2024-12-02-181601_add_report_combined_table/up.sql b/migrations/2024-12-02-181601_add_report_combined_table/up.sql index 4d450269c7..8425f5d0d1 100644 --- a/migrations/2024-12-02-181601_add_report_combined_table/up.sql +++ b/migrations/2024-12-02-181601_add_report_combined_table/up.sql @@ -15,23 +15,27 @@ CREATE INDEX idx_report_combined_published ON report_combined (published DESC, i CREATE INDEX idx_report_combined_published_asc ON report_combined (reverse_timestamp_sort (published) DESC, id DESC); -- Updating the history -INSERT INTO report_combined (published, post_report_id) +INSERT INTO report_combined (published, post_report_id, comment_report_id, private_message_report_id) SELECT published, - id + id, + NULL, + NULL FROM - post_report; - -INSERT INTO report_combined (published, comment_report_id) + post_report +UNION ALL SELECT published, - id + NULL, + id, + NULL FROM - comment_report; - -INSERT INTO report_combined (published, private_message_report_id) + comment_report +UNION ALL SELECT published, + NULL, + NULL, id FROM private_message_report; From 02bd2f6764edc1c0f858ec4fb76f4d8df5fd98f9 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 08:49:26 -0500 Subject: [PATCH 31/43] Fixing import and fmt. --- crates/db_schema/src/newtypes.rs | 3 +-- crates/db_schema/src/source/combined/report.rs | 8 +------- crates/db_views/src/report_combined_view.rs | 14 +++++++++----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c417ea2e45..605687e473 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -180,8 +180,7 @@ pub struct LtreeDef(pub String); pub struct DbUrl(pub(crate) Box); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] -#[cfg_attr(feature = "full", derive(DieselNewType, TS))] -#[cfg_attr(feature = "full", ts(export))] +#[cfg_attr(feature = "full", derive(DieselNewType))] /// The report combined id pub struct ReportCombinedId(i32); diff --git a/crates/db_schema/src/source/combined/report.rs b/crates/db_schema/src/source/combined/report.rs index 4085bddd69..5ea825b832 100644 --- a/crates/db_schema/src/source/combined/report.rs +++ b/crates/db_schema/src/source/combined/report.rs @@ -6,27 +6,21 @@ use chrono::{DateTime, Utc}; use i_love_jesus::CursorKeysModule; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -#[cfg(feature = "full")] -use ts_rs::TS; #[skip_serializing_none] #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] #[cfg_attr( feature = "full", - derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) + derive(Identifiable, Queryable, Selectable, CursorKeysModule) )] #[cfg_attr(feature = "full", diesel(table_name = report_combined))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", cursor_keys_module(name = report_combined_keys))] /// A combined reports table. pub struct ReportCombined { pub id: ReportCombinedId, pub published: DateTime, - #[cfg_attr(feature = "full", ts(optional))] pub post_report_id: Option, - #[cfg_attr(feature = "full", ts(optional))] pub comment_report_id: Option, - #[cfg_attr(feature = "full", ts(optional))] pub private_message_report_id: Option, } diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index f09b041f1b..879634cf00 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -13,6 +13,7 @@ use diesel::{ ExpressionMethods, JoinOnDsl, NullableExpressionMethods, + PgExpressionMethods, QueryDsl, SelectableHelper, }; @@ -79,9 +80,12 @@ impl ReportCombinedViewInternal { Some(my_person_id), post::community_id, )) - .filter(post_report::resolved - .or(comment_report::resolved) - .or(private_message_report::resolved).is_distinct_from(true)) + .filter( + post_report::resolved + .or(comment_report::resolved) + .or(private_message_report::resolved) + .is_distinct_from(true), + ) .into_boxed(); if let Some(community_id) = community_id { @@ -306,9 +310,9 @@ impl ReportCombinedQuery { query = query .filter( post_report::resolved - .or(comment_report::resolved) - .or(private_message_report::resolved).is_distinct_from(true), + .or(private_message_report::resolved) + .is_distinct_from(true), ) // TODO: when a `then_asc` method is added, use it here, make the id sort direction match, // and remove the separate index; unless additional columns are added to this sort From 2d482b355685f949ffe32620b6f43e4e0eb4180e Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 08:56:12 -0500 Subject: [PATCH 32/43] Fixing null types in postgres. --- .../up.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/migrations/2024-12-02-181601_add_report_combined_table/up.sql b/migrations/2024-12-02-181601_add_report_combined_table/up.sql index 8425f5d0d1..8efb2a0742 100644 --- a/migrations/2024-12-02-181601_add_report_combined_table/up.sql +++ b/migrations/2024-12-02-181601_add_report_combined_table/up.sql @@ -19,23 +19,23 @@ INSERT INTO report_combined (published, post_report_id, comment_report_id, priva SELECT published, id, - NULL, - NULL + NULL::int, + NULL::int FROM post_report UNION ALL SELECT published, - NULL, + NULL::int, id, - NULL + NULL::int FROM comment_report UNION ALL SELECT published, - NULL, - NULL, + NULL::int, + NULL::int, id FROM private_message_report; From 3e31e1c512ff6f8e7deaf5c546765537c42dca44 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 09:16:43 -0500 Subject: [PATCH 33/43] Comment out err. --- api_tests/src/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index b10d57002c..4cad739f4c 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -208,7 +208,7 @@ async function allowInstance(api: LemmyHttp, instance: string) { try { await api.adminAllowInstance(params); } catch (error) { - console.error(error); + // console.error(error); } } From 58e62d55d9074cf20e02fec3332d56151b687053 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sun, 8 Dec 2024 09:27:29 -0500 Subject: [PATCH 34/43] Fixing TS issues. --- .../src/source/combined/person_content.rs | 7 +---- .../src/source/combined/person_saved.rs | 7 +---- .../up.sql | 26 ++++++++++--------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/crates/db_schema/src/source/combined/person_content.rs b/crates/db_schema/src/source/combined/person_content.rs index c85b8f3b3c..ed83401c04 100644 --- a/crates/db_schema/src/source/combined/person_content.rs +++ b/crates/db_schema/src/source/combined/person_content.rs @@ -6,25 +6,20 @@ use chrono::{DateTime, Utc}; use i_love_jesus::CursorKeysModule; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -#[cfg(feature = "full")] -use ts_rs::TS; #[skip_serializing_none] #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] #[cfg_attr( feature = "full", - derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) + derive(Identifiable, Queryable, Selectable, CursorKeysModule) )] #[cfg_attr(feature = "full", diesel(table_name = person_content_combined))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", cursor_keys_module(name = person_content_combined_keys))] /// A combined table for a persons contents (posts and comments) pub struct PersonContentCombined { pub id: PersonContentCombinedId, pub published: DateTime, - #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, - #[cfg_attr(feature = "full", ts(optional))] pub comment_id: Option, } diff --git a/crates/db_schema/src/source/combined/person_saved.rs b/crates/db_schema/src/source/combined/person_saved.rs index 298360a6d5..afd91594dd 100644 --- a/crates/db_schema/src/source/combined/person_saved.rs +++ b/crates/db_schema/src/source/combined/person_saved.rs @@ -6,26 +6,21 @@ use chrono::{DateTime, Utc}; use i_love_jesus::CursorKeysModule; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -#[cfg(feature = "full")] -use ts_rs::TS; #[skip_serializing_none] #[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] #[cfg_attr( feature = "full", - derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule) + derive(Identifiable, Queryable, Selectable, CursorKeysModule) )] #[cfg_attr(feature = "full", diesel(table_name = person_saved_combined))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", cursor_keys_module(name = person_saved_combined_keys))] /// A combined person_saved table. pub struct PersonSavedCombined { pub id: PersonSavedCombinedId, pub published: DateTime, pub person_id: PersonId, - #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, - #[cfg_attr(feature = "full", ts(optional))] pub comment_id: Option, } diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql index a53f52925f..cbef85ecca 100644 --- a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql @@ -7,7 +7,7 @@ CREATE TABLE person_content_combined ( post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, -- Make sure only one of the columns is not null - CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) + CHECK (num_nonnulls (post_id, comment_id) = 1) ); CREATE INDEX idx_person_content_combined_published ON person_content_combined (published DESC, id DESC); @@ -15,16 +15,17 @@ CREATE INDEX idx_person_content_combined_published ON person_content_combined (p CREATE INDEX idx_person_content_combined_published_asc ON person_content_combined (reverse_timestamp_sort (published) DESC, id DESC); -- Updating the history -INSERT INTO person_content_combined (published, post_id) +INSERT INTO person_content_combined (published, post_id, comment_id) SELECT published, - id + id, + NULL::int FROM - post; - -INSERT INTO person_content_combined (published, comment_id) + post +UNION ALL SELECT published, + NULL::int, id FROM comment; @@ -37,7 +38,7 @@ CREATE TABLE person_saved_combined ( post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, -- Make sure only one of the columns is not null - CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1) + CHECK (num_nonnulls (post_id, comment_id) = 1) ); CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (published DESC, id DESC); @@ -47,20 +48,21 @@ CREATE INDEX idx_person_saved_combined_published_asc ON person_saved_combined (r CREATE INDEX idx_person_saved_combined ON person_saved_combined (person_id); -- Updating the history -INSERT INTO person_saved_combined (published, person_id, post_id) +INSERT INTO person_saved_combined (published, person_id, post_id, comment_id) SELECT saved, person_id, - post_id + post_id, + NULL::int FROM post_actions WHERE - saved IS NOT NULL; - -INSERT INTO person_saved_combined (published, person_id, comment_id) + saved IS NOT NULL +UNION ALL SELECT saved, person_id, + NULL::int, comment_id FROM comment_actions From 63b7f726cbf35092973a12bc777b2b9976fc3ebf Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 9 Dec 2024 08:22:35 -0500 Subject: [PATCH 35/43] Using dullbananas trigger procedure --- .../db_schema/replaceable_schema/triggers.sql | 110 +++++++----------- 1 file changed, 43 insertions(+), 67 deletions(-) diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 91c4fb841f..df86f11e51 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -714,74 +714,50 @@ CALL r.create_person_content_combined_trigger ('post'); CALL r.create_person_content_combined_trigger ('comment'); -- person_saved (comment, post) --- TODO, not sure how to handle changes to post_actions and comment_actions.saved column using @dullbanana's trigger method. --- Post -CREATE FUNCTION r.person_saved_combined_change_values_post () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ +-- This one is a little different, because it triggers using x_actions.saved, +-- Rather than any row insert +CREATE PROCEDURE r.create_person_saved_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ BEGIN - IF (TG_OP = 'DELETE') THEN - DELETE FROM person_saved_combined AS p - WHERE p.person_id = OLD.person_id - AND p.post_id = OLD.post_id; - ELSIF (TG_OP = 'INSERT') THEN - IF NEW.saved IS NOT NULL THEN - INSERT INTO person_saved_combined (published, person_id, post_id) - VALUES (NEW.saved, NEW.person_id, NEW.post_id); - END IF; - ELSIF (TG_OP = 'UPDATE') THEN - IF NEW.saved IS NOT NULL THEN - INSERT INTO person_saved_combined (published, person_id, post_id) - VALUES (NEW.saved, NEW.person_id, NEW.post_id); - -- If saved gets set as null, delete the row - ELSE - DELETE FROM person_saved_combined AS p - WHERE p.person_id = NEW.person_id - AND p.post_id = NEW.post_id; - END IF; - END IF; - RETURN NULL; -END -$$; - -CREATE TRIGGER person_saved_combined_post - AFTER INSERT OR DELETE OR UPDATE OF saved ON post_actions - FOR EACH ROW - EXECUTE FUNCTION r.person_saved_combined_change_values_post (); + EXECUTE replace($b$ CREATE FUNCTION r.person_saved_combined_change_values_thing ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + IF (TG_OP = 'DELETE') THEN + DELETE FROM person_saved_combined AS p + WHERE p.person_id = OLD.person_id + AND p.thing_id = OLD.thing_id; + ELSIF (TG_OP = 'INSERT') THEN + IF NEW.saved IS NOT NULL THEN + INSERT INTO person_saved_combined (published, person_id, thing_id) + VALUES (NEW.saved, NEW.person_id, NEW.thing_id); + END IF; + ELSIF (TG_OP = 'UPDATE') THEN + IF NEW.saved IS NOT NULL THEN + INSERT INTO person_saved_combined (published, person_id, thing_id) + VALUES (NEW.saved, NEW.person_id, NEW.thing_id); + -- If saved gets set as null, delete the row + ELSE + DELETE FROM person_saved_combined AS p + WHERE p.person_id = NEW.person_id + AND p.thing_id = NEW.thing_id; + END IF; + END IF; + RETURN NULL; + END $$; + CREATE TRIGGER person_saved_combined + AFTER INSERT OR DELETE OR UPDATE OF saved ON thing_actions + FOR EACH ROW + EXECUTE FUNCTION r.person_saved_combined_change_values_thing ( ); + $b$, + 'thing', + table_name); +END; +$a$; --- Comment -CREATE FUNCTION r.person_saved_combined_change_values_comment () - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ -BEGIN - IF (TG_OP = 'DELETE') THEN - DELETE FROM person_saved_combined AS p - WHERE p.person_id = OLD.person_id - AND p.comment_id = OLD.comment_id; - ELSIF (TG_OP = 'INSERT') THEN - IF NEW.saved IS NOT NULL THEN - INSERT INTO person_saved_combined (published, person_id, comment_id) - VALUES (NEW.saved, NEW.person_id, NEW.comment_id); - END IF; - ELSIF (TG_OP = 'UPDATE') THEN - IF NEW.saved IS NOT NULL THEN - INSERT INTO person_saved_combined (published, person_id, comment_id) - VALUES (NEW.saved, NEW.person_id, NEW.comment_id); - -- If saved gets set as null, delete the row - ELSE - DELETE FROM person_saved_combined AS p - WHERE p.person_id = NEW.person_id - AND p.comment_id = NEW.comment_id; - END IF; - END IF; - RETURN NULL; -END -$$; +CALL r.create_person_saved_combined_trigger ('post'); -CREATE TRIGGER person_saved_combined_comment - AFTER INSERT OR DELETE OR UPDATE OF saved ON comment_actions - FOR EACH ROW - EXECUTE FUNCTION r.person_saved_combined_change_values_comment (); +CALL r.create_person_saved_combined_trigger ('comment'); From c72d5e88d519e7fac72bfc5a6dc498479a79a137 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 12 Dec 2024 09:46:16 -0500 Subject: [PATCH 36/43] Addressing PR comments. --- api_tests/src/post.spec.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 37381d3023..52f86e8ef5 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -698,7 +698,7 @@ test("Report a post", async () => { () => listReports(beta).then(p => p.reports.find(r => { - return checkReportName(r, gammaReport); + return checkPostReportName(r, gammaReport); }), ), res => !!res, @@ -718,15 +718,7 @@ test("Report a post", async () => { () => listReports(alpha).then(p => p.reports.find(r => { - switch (r.type_) { - case "Post": - return ( - r.post_report.original_post_name === - gammaReport.original_post_name - ); - default: - return false; - } + return checkPostReportName(r, gammaReport); }), ), res => !!res, @@ -833,7 +825,7 @@ test("Rewrite markdown links", async () => { ); }); -function checkReportName(rcv: ReportCombinedView, report: PostReport) { +function checkPostReportName(rcv: ReportCombinedView, report: PostReport) { switch (rcv.type_) { case "Post": return rcv.post_report.original_post_name === report.original_post_name; From 7c962d63fb22c47d161d6ea31593a9346776d67f Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 12 Dec 2024 19:20:31 -0500 Subject: [PATCH 37/43] Removing serialization --- crates/db_schema/src/newtypes.rs | 2 +- crates/db_schema/src/source/combined/report.rs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 605687e473..18f8ef923b 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -179,7 +179,7 @@ pub struct LtreeDef(pub String); #[cfg_attr(feature = "full", ts(export))] pub struct DbUrl(pub(crate) Box); -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default)] #[cfg_attr(feature = "full", derive(DieselNewType))] /// The report combined id pub struct ReportCombinedId(i32); diff --git a/crates/db_schema/src/source/combined/report.rs b/crates/db_schema/src/source/combined/report.rs index 5ea825b832..2902c55484 100644 --- a/crates/db_schema/src/source/combined/report.rs +++ b/crates/db_schema/src/source/combined/report.rs @@ -4,11 +4,8 @@ use crate::schema::report_combined; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] use i_love_jesus::CursorKeysModule; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -#[skip_serializing_none] -#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone)] #[cfg_attr( feature = "full", derive(Identifiable, Queryable, Selectable, CursorKeysModule) From b4ce1d91211ee24e20b9f1a390c3187566a1732f Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 13 Dec 2024 08:50:14 -0500 Subject: [PATCH 38/43] Removing serialization --- crates/db_schema/src/newtypes.rs | 10 ++++------ crates/db_schema/src/source/combined/person_content.rs | 5 +---- crates/db_schema/src/source/combined/person_saved.rs | 5 +---- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 08d853dfaf..8fe128c2ae 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -184,15 +184,13 @@ pub struct DbUrl(pub(crate) Box); /// The report combined id pub struct ReportCombinedId(i32); -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] -#[cfg_attr(feature = "full", derive(DieselNewType, TS))] -#[cfg_attr(feature = "full", ts(export))] +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] /// The person content combined id pub struct PersonContentCombinedId(i32); -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] -#[cfg_attr(feature = "full", derive(DieselNewType, TS))] -#[cfg_attr(feature = "full", ts(export))] +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] /// The person saved combined id pub struct PersonSavedCombinedId(i32); diff --git a/crates/db_schema/src/source/combined/person_content.rs b/crates/db_schema/src/source/combined/person_content.rs index ed83401c04..05f8c1a467 100644 --- a/crates/db_schema/src/source/combined/person_content.rs +++ b/crates/db_schema/src/source/combined/person_content.rs @@ -4,11 +4,8 @@ use crate::schema::person_content_combined; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] use i_love_jesus::CursorKeysModule; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -#[skip_serializing_none] -#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone)] #[cfg_attr( feature = "full", derive(Identifiable, Queryable, Selectable, CursorKeysModule) diff --git a/crates/db_schema/src/source/combined/person_saved.rs b/crates/db_schema/src/source/combined/person_saved.rs index afd91594dd..4b0e80ea9b 100644 --- a/crates/db_schema/src/source/combined/person_saved.rs +++ b/crates/db_schema/src/source/combined/person_saved.rs @@ -4,11 +4,8 @@ use crate::schema::person_saved_combined; use chrono::{DateTime, Utc}; #[cfg(feature = "full")] use i_love_jesus::CursorKeysModule; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; -#[skip_serializing_none] -#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone)] #[cfg_attr( feature = "full", derive(Identifiable, Queryable, Selectable, CursorKeysModule) From cfd302dc1dd1b151a0a1aeacf094bff5c4e2bffb Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 19 Dec 2024 17:58:18 -0500 Subject: [PATCH 39/43] Fixing duped trigger. --- .../db_schema/replaceable_schema/triggers.sql | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index a6e7dcce7b..df86f11e51 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -761,35 +761,3 @@ CALL r.create_person_saved_combined_trigger ('post'); CALL r.create_person_saved_combined_trigger ('comment'); --- Combined tables triggers --- These insert (published, item_id) into X_combined tables --- Reports (comment_report, post_report, private_message_report) -CREATE PROCEDURE r.create_report_combined_trigger (table_name text) -LANGUAGE plpgsql -AS $a$ -BEGIN - EXECUTE replace($b$ CREATE FUNCTION r.report_combined_thing_insert ( ) - RETURNS TRIGGER - LANGUAGE plpgsql - AS $$ - BEGIN - INSERT INTO report_combined (published, thing_id) - VALUES (NEW.published, NEW.id); - RETURN NEW; - END $$; - CREATE TRIGGER report_combined - AFTER INSERT ON thing - FOR EACH ROW - EXECUTE FUNCTION r.report_combined_thing_insert ( ); - $b$, - 'thing', - table_name); -END; -$a$; - -CALL r.create_report_combined_trigger ('post_report'); - -CALL r.create_report_combined_trigger ('comment_report'); - -CALL r.create_report_combined_trigger ('private_message_report'); - From 9a0e050ee6d4981508cb73a097ac7cc570e45a47 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 19 Dec 2024 18:03:57 -0500 Subject: [PATCH 40/43] Remove saved_only test. --- crates/db_views/src/post_view.rs | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index b59d61817e..fc2afe844a 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -674,7 +674,7 @@ mod tests { site::Site, tag::{PostTagInsertForm, Tag, TagInsertForm}, }, - traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable}, utils::{build_db_pool, get_conn, uplete, ActualDbPool, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, @@ -1206,34 +1206,6 @@ mod tests { Ok(()) } - #[test_context(Data)] - #[tokio::test] - #[serial] - async fn post_listing_saved_only(data: &mut Data) -> LemmyResult<()> { - let pool = &data.pool(); - let pool = &mut pool.into(); - - // Save only the bot post - // The saved_only should only show the bot post - let post_save_form = - PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); - PostSaved::save(pool, &post_save_form).await?; - - // Read the saved only - let read_saved_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), - saved_only: Some(true), - ..data.default_post_query() - } - .list(&data.site, pool) - .await?; - - // This should only include the bot post, not the one you created - assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); - - Ok(()) - } - #[test_context(Data)] #[tokio::test] #[serial] From 776c5007a1baa50b320bd6186768126ce2bc4780 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 19 Dec 2024 18:07:59 -0500 Subject: [PATCH 41/43] Remove pointless post_tags types. --- .../src/person_content_combined_view.rs | 23 +++++++------------ .../src/person_saved_combined_view.rs | 23 +++++++------------ crates/db_views/src/post_view.rs | 23 ++++++++----------- 3 files changed, 25 insertions(+), 44 deletions(-) diff --git a/crates/db_views/src/person_content_combined_view.rs b/crates/db_views/src/person_content_combined_view.rs index 0811fbf223..0da4ca3599 100644 --- a/crates/db_views/src/person_content_combined_view.rs +++ b/crates/db_views/src/person_content_combined_view.rs @@ -10,11 +10,8 @@ use crate::{ InternalToCombinedView, }; use diesel::{ - pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, @@ -103,18 +100,14 @@ impl PersonContentCombinedQuery { let conn = &mut get_conn(pool).await?; - let post_tags: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = Box::new( - post_tag::table - .inner_join(tag::table) - .select(diesel::dsl::sql::( - "json_agg(tag.*)", - )) - .filter(post_tag::post_id.eq(post::id)) - .filter(tag::deleted.eq(false)) - .single_value(), - ); + let post_tags = post_tag::table + .inner_join(tag::table) + .select(diesel::dsl::sql::( + "json_agg(tag.*)", + )) + .filter(post_tag::post_id.eq(post::id)) + .filter(tag::deleted.eq(false)) + .single_value(); // Notes: since the post_id and comment_id are optional columns, // many joins must use an OR condition. diff --git a/crates/db_views/src/person_saved_combined_view.rs b/crates/db_views/src/person_saved_combined_view.rs index 3c61d9705a..94b93e9d81 100644 --- a/crates/db_views/src/person_saved_combined_view.rs +++ b/crates/db_views/src/person_saved_combined_view.rs @@ -8,11 +8,8 @@ use crate::{ InternalToCombinedView, }; use diesel::{ - pg::Pg, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, @@ -97,18 +94,14 @@ impl PersonSavedCombinedQuery { let conn = &mut get_conn(pool).await?; - let post_tags: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = Box::new( - post_tag::table - .inner_join(tag::table) - .select(diesel::dsl::sql::( - "json_agg(tag.*)", - )) - .filter(post_tag::post_id.eq(post::id)) - .filter(tag::deleted.eq(false)) - .single_value(), - ); + let post_tags = post_tag::table + .inner_join(tag::table) + .select(diesel::dsl::sql::( + "json_agg(tag.*)", + )) + .filter(post_tag::post_id.eq(post::id)) + .filter(tag::deleted.eq(false)) + .single_value(); // Notes: since the post_id and comment_id are optional columns, // many joins must use an OR condition. diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index fc2afe844a..c40e62f1ac 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -5,9 +5,7 @@ use diesel::{ pg::Pg, query_builder::AsQuery, result::Error, - sql_types, BoolExpressionMethods, - BoxableExpression, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, @@ -97,18 +95,15 @@ fn queries<'a>() -> Queries< // If we want to filter by post tag we will have to add // separate logic below since this subquery can't affect filtering, but it is simple (`WHERE // exists (select 1 from post_community_post_tags where community_post_tag_id in (1,2,3,4)`). - let post_tags: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = Box::new( - post_tag::table - .inner_join(tag::table) - .select(diesel::dsl::sql::( - "json_agg(tag.*)", - )) - .filter(post_tag::post_id.eq(post_aggregates::post_id)) - .filter(tag::deleted.eq(false)) - .single_value(), - ); + let post_tags = post_tag::table + .inner_join(tag::table) + .select(diesel::dsl::sql::( + "json_agg(tag.*)", + )) + .filter(post_tag::post_id.eq(post_aggregates::post_id)) + .filter(tag::deleted.eq(false)) + .single_value(); + query .inner_join(person::table) .inner_join(community::table) From 4bc4876f3144d5f3394f11abfc4443f7372d6a9a Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 24 Dec 2024 14:30:34 -0500 Subject: [PATCH 42/43] Remove pointless index. --- .../up.sql | 4 ---- 1 file changed, 4 deletions(-) diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql index cbef85ecca..973535b89f 100644 --- a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql @@ -12,8 +12,6 @@ CREATE TABLE person_content_combined ( CREATE INDEX idx_person_content_combined_published ON person_content_combined (published DESC, id DESC); -CREATE INDEX idx_person_content_combined_published_asc ON person_content_combined (reverse_timestamp_sort (published) DESC, id DESC); - -- Updating the history INSERT INTO person_content_combined (published, post_id, comment_id) SELECT @@ -43,8 +41,6 @@ CREATE TABLE person_saved_combined ( CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (published DESC, id DESC); -CREATE INDEX idx_person_saved_combined_published_asc ON person_saved_combined (reverse_timestamp_sort (published) DESC, id DESC); - CREATE INDEX idx_person_saved_combined ON person_saved_combined (person_id); -- Updating the history From 2244fd005d0598c95ea313dc7093bed85893e204 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 2 Jan 2025 17:08:05 -0500 Subject: [PATCH 43/43] Changing published to saved for person_saved_combined. --- crates/db_schema/replaceable_schema/triggers.sql | 4 ++-- crates/db_schema/src/schema.rs | 2 +- crates/db_schema/src/source/combined/person_saved.rs | 2 +- crates/db_views/src/person_saved_combined_view.rs | 4 ++-- .../up.sql | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index df86f11e51..77d368287c 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -731,12 +731,12 @@ BEGIN AND p.thing_id = OLD.thing_id; ELSIF (TG_OP = 'INSERT') THEN IF NEW.saved IS NOT NULL THEN - INSERT INTO person_saved_combined (published, person_id, thing_id) + INSERT INTO person_saved_combined (saved, person_id, thing_id) VALUES (NEW.saved, NEW.person_id, NEW.thing_id); END IF; ELSIF (TG_OP = 'UPDATE') THEN IF NEW.saved IS NOT NULL THEN - INSERT INTO person_saved_combined (published, person_id, thing_id) + INSERT INTO person_saved_combined (saved, person_id, thing_id) VALUES (NEW.saved, NEW.person_id, NEW.thing_id); -- If saved gets set as null, delete the row ELSE diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 44f2ab03c3..7bdeeb6f26 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -751,7 +751,7 @@ diesel::table! { diesel::table! { person_saved_combined (id) { id -> Int4, - published -> Timestamptz, + saved -> Timestamptz, person_id -> Int4, post_id -> Nullable, comment_id -> Nullable, diff --git a/crates/db_schema/src/source/combined/person_saved.rs b/crates/db_schema/src/source/combined/person_saved.rs index 4b0e80ea9b..bee11e8b8a 100644 --- a/crates/db_schema/src/source/combined/person_saved.rs +++ b/crates/db_schema/src/source/combined/person_saved.rs @@ -16,7 +16,7 @@ use i_love_jesus::CursorKeysModule; /// A combined person_saved table. pub struct PersonSavedCombined { pub id: PersonSavedCombinedId, - pub published: DateTime, + pub saved: DateTime, pub person_id: PersonId, pub post_id: Option, pub comment_id: Option, diff --git a/crates/db_views/src/person_saved_combined_view.rs b/crates/db_views/src/person_saved_combined_view.rs index 94b93e9d81..cb6eef3e77 100644 --- a/crates/db_views/src/person_saved_combined_view.rs +++ b/crates/db_views/src/person_saved_combined_view.rs @@ -220,9 +220,9 @@ impl PersonSavedCombinedQuery { query = query.after(page_after); } - // Sorting by published + // Sorting by saved desc query = query - .then_desc(key::published) + .then_desc(key::saved) // Tie breaker .then_desc(key::id); diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql index 973535b89f..805d2ca94c 100644 --- a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql @@ -31,7 +31,7 @@ FROM -- This one is special, because you use the saved date, not the ordinary published CREATE TABLE person_saved_combined ( id serial PRIMARY KEY, - published timestamptz NOT NULL, + saved timestamptz NOT NULL, person_id int NOT NULL REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE, post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, @@ -39,12 +39,12 @@ CREATE TABLE person_saved_combined ( CHECK (num_nonnulls (post_id, comment_id) = 1) ); -CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (published DESC, id DESC); +CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (saved DESC, id DESC); CREATE INDEX idx_person_saved_combined ON person_saved_combined (person_id); -- Updating the history -INSERT INTO person_saved_combined (published, person_id, post_id, comment_id) +INSERT INTO person_saved_combined (saved, person_id, post_id, comment_id) SELECT saved, person_id,