From 8b78ddeb68daf7cde07d3204414b7bcdc5cdbef5 Mon Sep 17 00:00:00 2001 From: Integral Date: Fri, 20 Dec 2024 04:42:01 +0800 Subject: [PATCH 1/2] refactor: avoid using format! when String creation is unnecessary (#5268) --- crates/apub/src/fetcher/markdown_links.rs | 3 ++- crates/routes/src/feeds.rs | 4 ++-- crates/utils/src/utils/markdown/image_links.rs | 3 ++- src/scheduled_tasks.rs | 6 ++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/apub/src/fetcher/markdown_links.rs b/crates/apub/src/fetcher/markdown_links.rs index d83aae515..a5e51caa7 100644 --- a/crates/apub/src/fetcher/markdown_links.rs +++ b/crates/apub/src/fetcher/markdown_links.rs @@ -42,7 +42,8 @@ pub async fn markdown_rewrite_remote_links( let mut local_url = local_url.to_string(); // restore title if let Some(extra) = extra { - local_url = format!("{local_url} {extra}"); + local_url.push(' '); + local_url.push_str(extra); } src.replace_range(start..end, local_url.as_str()); } diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 55e9cc7f3..cd1ca3e98 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -454,7 +454,6 @@ fn build_item( protocol_and_hostname: &str, ) -> LemmyResult { // TODO add images - let author_url = format!("{protocol_and_hostname}/u/{creator_name}"); let guid = Some(Guid { permalink: true, value: url.to_owned(), @@ -464,7 +463,8 @@ fn build_item( Ok(Item { title: Some(format!("Reply from {creator_name}")), author: Some(format!( - "/u/{creator_name} (link)" + "/u/{creator_name} (link)", + format_args!("{protocol_and_hostname}/u/{creator_name}") )), pub_date: Some(published.to_rfc2822()), comments: Some(url.to_owned()), diff --git a/crates/utils/src/utils/markdown/image_links.rs b/crates/utils/src/utils/markdown/image_links.rs index 0990b1bc7..7914452ff 100644 --- a/crates/utils/src/utils/markdown/image_links.rs +++ b/crates/utils/src/utils/markdown/image_links.rs @@ -24,7 +24,8 @@ pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec) { ); // restore custom emoji format if let Some(extra) = extra { - proxied = format!("{proxied} {extra}"); + proxied.push(' '); + proxied.push_str(extra); } src.replace_range(start..end, &proxied); } diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 3406bf694..53c0b888b 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -190,10 +190,8 @@ async fn process_ranks_in_batches( UPDATE {aggregates_table} a {set_clause} FROM batch WHERE a.{id_column} = batch.{id_column} RETURNING a.published; "#, - id_column = format!("{table_name}_id"), - aggregates_table = format!("{table_name}_aggregates"), - set_clause = set_clause, - where_clause = where_clause + id_column = format_args!("{table_name}_id"), + aggregates_table = format_args!("{table_name}_aggregates"), )) .bind::(previous_batch_last_published) .bind::(update_batch_size) From 9d3a0cef56ba0ab2412d05c2bda2bd2edea5a944 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 19 Dec 2024 17:22:31 -0500 Subject: [PATCH 2/2] Adding `report_combined` table. (#5231) * Combined tables try 2 * Finishing up combined report table. * Fix ts optionals. * Adding tests, triggers, and history updates for report_combined. * Adding profile. * 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 * Fixing migration and paged API. * Using dullbananas trigger procedure * Removing pointless list routes, reorganizing tests. * Fixing column XOR check. * Forgot to remove list report actions. * Cleanup. * Use internal tagging. * Fixing api tests. * Adding a few indexes. * Fixing migration name. * Fixing unique constraints. * Addressing PR comments. * Fixing api tests. * Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas * Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas * Update crates/db_views/src/report_combined_view.rs Co-authored-by: dullbananas * Update migrations/2024-12-02-181601_add_report_combined_table/up.sql Co-authored-by: dullbananas * Update migrations/2024-12-02-181601_add_report_combined_table/up.sql Co-authored-by: dullbananas * Fixing import and fmt. * Fixing null types in postgres. * Comment out err. * Addressing PR comments. * Removing serialization --------- Co-authored-by: dullbananas --- api_tests/package.json | 2 +- api_tests/pnpm-lock.yaml | 10 +- api_tests/src/comment.spec.ts | 37 +- 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 | 58 +- api_tests/src/shared.ts | 26 +- crates/api/src/comment_report/list.rs | 37 - crates/api/src/lib.rs | 4 +- crates/api/src/local_user/report_count.rs | 36 +- crates/api/src/post_report/list.rs | 37 - crates/api/src/private_message_report/list.rs | 35 - .../{ => reports}/comment_report/create.rs | 2 +- .../comment_report}/mod.rs | 1 - .../{ => reports}/comment_report/resolve.rs | 2 +- crates/api/src/reports/mod.rs | 4 + .../src/{ => reports}/post_report/create.rs | 2 +- .../post_report}/mod.rs | 1 - .../src/{ => reports}/post_report/resolve.rs | 2 +- .../private_message_report/create.rs | 2 +- .../private_message_report}/mod.rs | 1 - .../private_message_report/resolve.rs | 2 +- .../api/src/reports/report_combined/list.rs | 41 + 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/person.rs | 7 +- crates/api_common/src/post.rs | 61 +- crates/api_common/src/private_message.rs | 54 +- crates/api_common/src/reports/combined.rs | 32 + crates/api_common/src/reports/comment.rs | 31 + crates/api_common/src/reports/mod.rs | 4 + crates/api_common/src/reports/post.rs | 31 + .../api_common/src/reports/private_message.rs | 31 + .../db_schema/replaceable_schema/triggers.sql | 32 + crates/db_schema/src/newtypes.rs | 11 +- crates/db_schema/src/schema.rs | 14 + crates/db_schema/src/source/combined/mod.rs | 1 + .../db_schema/src/source/combined/report.rs | 23 + crates/db_schema/src/source/mod.rs | 1 + crates/db_views/src/comment_report_view.rs | 489 +-------- crates/db_views/src/lib.rs | 2 + crates/db_views/src/post_report_view.rs | 356 +------ .../src/private_message_report_view.rs | 195 +--- crates/db_views/src/report_combined_view.rs | 967 ++++++++++++++++++ crates/db_views/src/structs.rs | 52 + .../down.sql | 2 + .../up.sql | 42 + src/api_routes_v3.rs | 25 +- src/api_routes_v4.rs | 32 +- 51 files changed, 1501 insertions(+), 1403 deletions(-) delete mode 100644 crates/api/src/comment_report/list.rs delete mode 100644 crates/api/src/post_report/list.rs delete mode 100644 crates/api/src/private_message_report/list.rs rename crates/api/src/{ => reports}/comment_report/create.rs (97%) rename crates/api/src/{post_report => reports/comment_report}/mod.rs (70%) 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/{private_message_report => reports/post_report}/mod.rs (70%) 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/{comment_report => reports/private_message_report}/mod.rs (70%) 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 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-12-02-181601_add_report_combined_table/down.sql create mode 100644 migrations/2024-12-02-181601_add_report_combined_table/up.sql diff --git a/api_tests/package.json b/api_tests/package.json index 7ea21d0ba..965388625 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 496606e6c..198062652 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/comment.spec.ts b/api_tests/src/comment.spec.ts index 5cf94aa03..419e58769 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,15 @@ import { blockCommunity, delay, saveUserSettings, + listReports, } from "./shared"; -import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client"; +import { + CommentReportView, + CommentView, + CommunityView, + ReportCombinedView, + SaveUserSettings, +} from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; let postOnAlphaRes: PostResponse; @@ -796,13 +802,17 @@ 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 => { + return checkCommentReportReason(r, reason); + }), + ), + e => !!e, + )!) as CommentReportView + ).comment_report; expect(betaReport).toBeDefined(); expect(betaReport.resolved).toBe(false); expect(betaReport.original_comment_text).toBe( @@ -877,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/community.spec.ts b/api_tests/src/community.spec.ts index 2bb092088..2d1570ea6 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 936ce2606..c447e14cd 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 a3478081a..4d1abbdfd 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 4158bbdc7..52f86e8ef 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -27,10 +27,8 @@ import { followCommunity, banPersonFromCommunity, reportPost, - listPostReports, randomString, registerUser, - getSite, unfollows, resolveCommunity, waitUntil, @@ -38,11 +36,18 @@ import { alphaUrl, loginUser, createCommunity, + listReports, getMyUser, } 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, + PostReport, + PostReportView, + ReportCombinedView, + ResolveObject, +} from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -688,16 +693,17 @@ 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 => { + return checkPostReportName(r, gammaReport); + }), ), - ), - 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); @@ -707,16 +713,17 @@ 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 => { + return checkPostReportName(r, gammaReport); + }), ), - ), - 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); @@ -817,3 +824,12 @@ test("Rewrite markdown links", async () => { `[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`, ); }); + +function checkPostReportName(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/api_tests/src/shared.ts b/api_tests/src/shared.ts index 1ed13d9cf..4cad739f4 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, MyUserInfo, PersonId, PostView, @@ -75,12 +76,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"; @@ -210,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( @@ -809,11 +808,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( @@ -840,13 +839,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, diff --git a/crates/api/src/comment_report/list.rs b/crates/api/src/comment_report/list.rs deleted file mode 100644 index d2f723819..000000000 --- a/crates/api/src/comment_report/list.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - comment::{ListCommentReports, ListCommentReportsResponse}, - context::LemmyContext, - 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/lib.rs b/crates/api/src/lib.rs index 6a2c94332..aa6e37000 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/local_user/report_count.rs b/crates/api/src/local_user/report_count.rs index 32448dcaa..0d24a4de9 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 count = ReportCombinedViewInternal::get_report_count( + &mut context.pool(), + &local_user_view, + data.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 - }; - - Ok(Json(GetReportCountResponse { - community_id, - comment_reports, - post_reports, - private_message_reports, - })) + Ok(Json(GetReportCountResponse { count })) } diff --git a/crates/api/src/post_report/list.rs b/crates/api/src/post_report/list.rs deleted file mode 100644 index 7d1d50b0b..000000000 --- a/crates/api/src/post_report/list.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - 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/private_message_report/list.rs b/crates/api/src/private_message_report/list.rs deleted file mode 100644 index 79ef53e1c..000000000 --- a/crates/api/src/private_message_report/list.rs +++ /dev/null @@ -1,35 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - 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/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 48066cfe6..a456ded36 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/post_report/mod.rs b/crates/api/src/reports/comment_report/mod.rs similarity index 70% rename from crates/api/src/post_report/mod.rs rename to crates/api/src/reports/comment_report/mod.rs index 3bb1a9b46..c85613aa6 100644 --- a/crates/api/src/post_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/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 58d5041dc..5ab36054f 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 000000000..f23d1d71f --- /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 b9edf35c5..bc85bdbe7 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/private_message_report/mod.rs b/crates/api/src/reports/post_report/mod.rs similarity index 70% rename from crates/api/src/private_message_report/mod.rs rename to crates/api/src/reports/post_report/mod.rs index 3bb1a9b46..c85613aa6 100644 --- a/crates/api/src/private_message_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/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 652327513..26b182a45 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 de8ca390f..17b5dceeb 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/comment_report/mod.rs b/crates/api/src/reports/private_message_report/mod.rs similarity index 70% rename from crates/api/src/comment_report/mod.rs rename to crates/api/src/reports/private_message_report/mod.rs index 3bb1a9b46..c85613aa6 100644 --- a/crates/api/src/comment_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/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 7d821a60c..3f812e4fe 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 000000000..12548d189 --- /dev/null +++ b/crates/api/src/reports/report_combined/list.rs @@ -0,0 +1,41 @@ +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; + + check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).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 reports = ReportCombinedQuery { + community_id, + unresolved_only, + page_after, + page_back, + } + .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 000000000..d17e233fb --- /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 e08365789..bae9c4de4 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 6e09d904d..8af1dec25 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/person.rs b/crates/api_common/src/person.rs index b95cf5e77..797946d65 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/api_common/src/post.rs b/crates/api_common/src/post.rs index fb16c8aa8..db987d63c 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, TagId}, + newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, TagId}, 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; @@ -118,6 +118,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] @@ -251,61 +253,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 666fe3865..8bd417a8e 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 000000000..69d928830 --- /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::{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, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// List reports. +pub struct ListReports { + /// 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)] +#[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 000000000..d1a51a6a8 --- /dev/null +++ b/crates/api_common/src/reports/comment.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{CommentId, CommentReportId}; +use lemmy_db_views::structs::CommentReportView; +use serde::{Deserialize, Serialize}; +#[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, +} diff --git a/crates/api_common/src/reports/mod.rs b/crates/api_common/src/reports/mod.rs new file mode 100644 index 000000000..6584de1bc --- /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 000000000..a4d20d575 --- /dev/null +++ b/crates/api_common/src/reports/post.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{PostId, PostReportId}; +use lemmy_db_views::structs::PostReportView; +use serde::{Deserialize, Serialize}; +#[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, +} 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 000000000..5fd401564 --- /dev/null +++ b/crates/api_common/src/reports/private_message.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{PrivateMessageId, PrivateMessageReportId}; +use lemmy_db_views::structs::PrivateMessageReportView; +use serde::{Deserialize, Serialize}; +#[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, +} diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index e5b3e22d0..2d9b0df6e 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -653,3 +653,35 @@ 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_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'); + diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 963f847a5..4c4a9b66c 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))] @@ -179,6 +179,11 @@ pub struct LtreeDef(pub String); #[cfg_attr(feature = "full", ts(export))] pub struct DbUrl(pub(crate) Box); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +/// 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 77122f7cb..64aff118b 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -889,6 +889,16 @@ diesel::table! { } } +diesel::table! { + report_combined (id) { + id -> Int4, + published -> Timestamptz, + post_report_id -> Nullable, + comment_report_id -> Nullable, + private_message_report_id -> Nullable, + } +} + diesel::table! { secret (id) { id -> Int4, @@ -1057,6 +1067,9 @@ diesel::joinable!(post_tag -> tag (tag_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!(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)); @@ -1127,6 +1140,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..2902c5548 --- /dev/null +++ b/crates/db_schema/src/source/combined/report.rs @@ -0,0 +1,23 @@ +use crate::newtypes::{CommentReportId, PostReportId, PrivateMessageReportId, ReportCombinedId}; +#[cfg(feature = "full")] +use crate::schema::report_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; + +#[derive(PartialEq, Eq, Debug, Clone)] +#[cfg_attr( + feature = "full", + 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", cursor_keys_module(name = report_combined_keys))] +/// A combined reports table. +pub struct ReportCombined { + pub id: ReportCombinedId, + pub published: DateTime, + pub post_report_id: Option, + pub comment_report_id: Option, + pub private_message_report_id: Option, +} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 6230d004d..2ac2692b4 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/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index b4a23a0da..6154b9b56 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/lib.rs b/crates/db_views/src/lib.rs index 3c1fcd84a..6c9b21023 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -24,6 +24,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..4c7fd676c 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()); - } - - 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 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 e59d99608..956ccf0e1 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 new file mode 100644 index 000000000..879634cf0 --- /dev/null +++ b/crates/db_views/src/report_combined_view.rs @@ -0,0 +1,967 @@ +use crate::structs::{ + CommentReportView, + LocalUserView, + PostReportView, + PrivateMessageReportView, + ReportCombinedPaginationCursor, + ReportCombinedView, + ReportCombinedViewInternal, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + PgExpressionMethods, + 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, + comment_report, + community, + community_actions, + local_user, + person, + person_actions, + post, + post_actions, + post_aggregates, + post_report, + private_message, + private_message_report, + report_combined, + }, + source::{ + combined::report::{report_combined_keys as key, ReportCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, +}; +use lemmy_utils::error::LemmyResult; + +impl ReportCombinedViewInternal { + /// returns the current unresolved report count for the communities you mod + pub async fn get_report_count( + pool: &mut DbPool<'_>, + user: &LocalUserView, + community_id: Option, + ) -> Result { + use diesel::dsl::count; + + let conn = &mut get_conn(pool).await?; + 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 + .or(comment_report::resolved) + .or(private_message_report::resolved) + .is_distinct_from(true), + ) + .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 !user.local_user.admin { + query = query.filter(community_actions::became_moderator.is_not_null()); + } + + query + .select(count(report_combined::id)) + .first::(conn) + .await + } +} + +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 unresolved_only: Option, + pub page_after: Option, + pub page_back: Option, +} + +impl ReportCombinedQuery { + 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 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, + // 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) + .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(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)), + ), + ) + // The item creator (`item_creator` is the id of this person) + .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)), + ) + // 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), + 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(), + 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) = 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 reports, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) + if self.unresolved_only.unwrap_or_default() { + query = query + .filter( + post_report::resolved + .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 + .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: 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, + creator: v.report_creator, + private_message_creator: v.item_creator, + resolver: v.resolver, + }, + )) + } else { + None + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + report_combined_view::ReportCombinedQuery, + structs::{ + CommentReportView, + LocalUserView, + PostReportView, + ReportCombinedView, + ReportCombinedViewInternal, + }, + }; + use lemmy_db_schema::{ + aggregates::structs::{CommentAggregates, PostAggregates}, + 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, DbPool}, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + 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"); + 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 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(), + "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?; + + 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( + inserted_timmy.id, + inserted_post.id, + "A test comment rv".into(), + ); + 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: data.sara.id, + comment_id: data.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( + 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: data.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, &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!(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!(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"); + } + 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, &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, &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!(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!(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, &data.timmy_view, None).await?; + assert_eq!(2, report_count_timmy); + + // Resolve the post report + 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 + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .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, &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); + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index a95376a1a..7fe529eb6 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -129,6 +129,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))] @@ -242,6 +248,52 @@ pub struct LocalImageView { pub person: Person, } +#[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 report view +pub struct ReportCombinedViewInternal { + // Post-specific + pub post_report: 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: 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, +} + +#[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), + PrivateMessage(PrivateMessageReportView), +} + #[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)] #[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))] #[serde(transparent)] diff --git a/migrations/2024-12-02-181601_add_report_combined_table/down.sql b/migrations/2024-12-02-181601_add_report_combined_table/down.sql new file mode 100644 index 000000000..b27ba9bc4 --- /dev/null +++ b/migrations/2024-12-02-181601_add_report_combined_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE report_combined; + 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 new file mode 100644 index 000000000..8efb2a074 --- /dev/null +++ b/migrations/2024-12-02-181601_add_report_combined_table/up.sql @@ -0,0 +1,42 @@ +-- Creates combined tables for +-- Reports: (comment, post, and private_message) +CREATE TABLE report_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + 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 (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); + +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, comment_report_id, private_message_report_id) +SELECT + published, + id, + NULL::int, + NULL::int +FROM + post_report +UNION ALL +SELECT + published, + NULL::int, + id, + NULL::int +FROM + comment_report +UNION ALL +SELECT + published, + NULL::int, + NULL::int, + id +FROM + private_message_report; + diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index eefaf5b87..5e8fb741d 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.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, @@ -58,16 +53,11 @@ 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}, }, site::{ federated_instances::get_federated_instances, @@ -222,7 +212,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 @@ -247,8 +236,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( @@ -260,8 +248,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( diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index a9f71c9da..b03be60f2 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(