diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c28be8222..fe84802d7 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 f2b186d35..1d3177b15 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 000000000..7352eef8e --- /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 000000000..7c55329e7 --- /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 5082ddbd1..17252c603 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 e93c7409d..e93741be8 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 9429c258f..c530c9739 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 000000000..d34838c83 --- /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 4586fbcac..e51aec67c 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 000000000..02c747794 --- /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 000000000..a7a0a7414 --- /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 +