From 5fcf166a89975a74dc5a8b6a10e21c96f21c7761 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Mon, 3 Feb 2025 23:52:22 +0000 Subject: [PATCH] Federate "resolve report" action (#5367) * Federete resolve report action (fixes #4744) * working * clippy and fixes * fix test * verify mod action * add workaround for test --------- Co-authored-by: Dessalines --- api_tests/prepare-drone-federation-test.sh | 2 - api_tests/src/post.spec.ts | 73 ++++++++++-- api_tests/src/shared.ts | 2 +- .../api/src/reports/comment_report/resolve.rs | 14 ++- crates/api/src/reports/post_report/resolve.rs | 15 ++- crates/api_common/src/send_activity.rs | 6 + .../community/resolve_report_page.json | 14 +++ crates/apub/src/activities/community/mod.rs | 48 +++++++- .../apub/src/activities/community/report.rs | 86 +++++++------- .../activities/community/resolve_report.rs | 110 ++++++++++++++++++ crates/apub/src/activities/mod.rs | 28 ++++- crates/apub/src/activity_lists.rs | 6 + crates/apub/src/lib.rs | 2 + .../src/protocol/activities/community/mod.rs | 6 + .../protocol/activities/community/report.rs | 28 ++++- .../activities/community/resolve_report.rs | 39 +++++++ crates/db_schema/src/impls/comment_report.rs | 58 ++++++--- .../db_schema/src/impls/community_report.rs | 58 ++++++--- crates/db_schema/src/impls/post_report.rs | 58 ++++++--- .../src/impls/private_message_report.rs | 9 ++ crates/db_schema/src/traits.rs | 9 ++ 21 files changed, 554 insertions(+), 117 deletions(-) create mode 100644 crates/apub/assets/lemmy/activities/community/resolve_report_page.json create mode 100644 crates/apub/src/activities/community/resolve_report.rs create mode 100644 crates/apub/src/protocol/activities/community/resolve_report.rs diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh index cb4fcf880..64a921f52 100755 --- a/api_tests/prepare-drone-federation-test.sh +++ b/api_tests/prepare-drone-federation-test.sh @@ -84,13 +84,11 @@ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \ target/lemmy_server >$LOG_DIR/lemmy_gamma.out 2>&1 & echo "start delta" -# An instance with only an allowlist for beta LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \ target/lemmy_server >$LOG_DIR/lemmy_delta.out 2>&1 & echo "start epsilon" -# An instance who has a blocklist, with lemmy-alpha blocked LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \ target/lemmy_server >$LOG_DIR/lemmy_epsilon.out 2>&1 & diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 8ac46de22..a1a089d03 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -39,16 +39,20 @@ import { listReports, getMyUser, listInbox, + allowInstance, } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; import { + AddModToCommunity, EditSite, + LemmyHttp, PersonPostMentionView, PostReport, PostReportView, ReportCombinedView, ResolveObject, + ResolvePostReport, } from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -57,6 +61,11 @@ beforeAll(async () => { await setupLogins(); betaCommunity = (await resolveBetaCommunity(alpha)).community; expect(betaCommunity).toBeDefined(); + + // Hack: Force outgoing federation queue for beta to be created on epsilon, + // otherwise report test fails + let person = await resolvePerson(epsilon, "@lemmy_beta@lemmy-beta:8551"); + expect(person.person).toBeDefined(); }); afterAll(unfollows); @@ -679,16 +688,26 @@ test("Report a post", async () => { // Create post from alpha let alphaCommunity = (await resolveBetaCommunity(alpha)).community!; await followBeta(alpha); - let postRes = await createPost(alpha, alphaCommunity.community.id); - expect(postRes.post_view.post).toBeDefined(); + let alphaPost = await createPost(alpha, alphaCommunity.community.id); + expect(alphaPost.post_view.post).toBeDefined(); - let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post; - if (!alphaPost) { - throw "Missing alpha post"; - } + // add remote mod on epsilon + await followBeta(epsilon); + + let betaCommunity = (await resolveBetaCommunity(beta)).community!; + let epsilonUser = ( + await resolvePerson(beta, "@lemmy_epsilon@lemmy-epsilon:8581") + ).person!; + let mod_params: AddModToCommunity = { + community_id: betaCommunity.community.id, + person_id: epsilonUser.person.id, + added: true, + }; + let res = await beta.addModToCommunity(mod_params); + expect(res.moderators.length).toBe(2); // Send report from gamma - let gammaPost = (await resolvePost(gamma, alphaPost.post)).post!; + let gammaPost = (await resolvePost(gamma, alphaPost.post_view.post)).post!; let gammaReport = ( await reportPost(gamma, gammaPost.post.id, randomString(10)) ).post_report_view.post_report; @@ -732,6 +751,45 @@ test("Report a post", async () => { //expect(alphaReport.original_post_url).toBe(gammaReport.original_post_url); expect(alphaReport.original_post_body).toBe(gammaReport.original_post_body); expect(alphaReport.reason).toBe(gammaReport.reason); + + // Report was federated to remote mod instance + let epsilonReport = ( + (await waitUntil( + () => + listReports(epsilon).then(p => + p.reports.find(r => { + return checkPostReportName(r, gammaReport); + }), + ), + res => !!res, + ))! as PostReportView + ).post_report; + expect(epsilonReport).toBeDefined(); + expect(epsilonReport.resolved).toBe(false); + expect(epsilonReport.original_post_name).toBe(gammaReport.original_post_name); + + // Resolve report as remote mod + let resolve_params: ResolvePostReport = { + report_id: epsilonReport.id, + resolved: true, + }; + let resolve = await epsilon.resolvePostReport(resolve_params); + expect(resolve.post_report_view.post_report.resolved).toBeTruthy(); + + // Report should be marked resolved on community instance + let resolvedReport = ( + (await waitUntil( + () => + listReports(beta).then(p => + p.reports.find(r => { + return checkPostReportName(r, gammaReport) && r.resolver != null; + }), + ), + res => !!res, + ))! as PostReportView + ).post_report; + expect(resolvedReport).toBeDefined(); + expect(resolvedReport.resolved).toBe(true); }); test("Fetch post via redirect", async () => { @@ -852,7 +910,6 @@ test("Rewrite markdown links", async () => { "https://example.com/", `[link](${postRes1.post_view.post.ap_id})`, ); - console.log(postRes2.post_view.post.body); expect(postRes2.post_view.post).toBeDefined(); // fetch both posts from another instance diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 88dca842e..6ea944c20 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -199,7 +199,7 @@ export async function setupLogins() { } } -async function allowInstance(api: LemmyHttp, instance: string) { +export async function allowInstance(api: LemmyHttp, instance: string) { const params: AdminAllowInstanceParams = { instance, allow: true, diff --git a/crates/api/src/reports/comment_report/resolve.rs b/crates/api/src/reports/comment_report/resolve.rs index 0d8e625c9..bed110483 100644 --- a/crates/api/src/reports/comment_report/resolve.rs +++ b/crates/api/src/reports/comment_report/resolve.rs @@ -1,7 +1,9 @@ -use actix_web::web::{Data, Json}; +use activitypub_federation::config::Data; +use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, reports::comment::{CommentReportResponse, ResolveCommentReport}, + send_activity::{ActivityChannel, SendActivityData}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable}; @@ -41,6 +43,16 @@ pub async fn resolve_comment_report( let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id).await?; + ActivityChannel::submit_activity( + SendActivityData::SendResolveReport { + object_id: comment_report_view.comment.ap_id.inner().clone(), + actor: local_user_view.person, + report_creator: report.creator, + community: comment_report_view.community.clone(), + }, + &context, + )?; + Ok(Json(CommentReportResponse { comment_report_view, })) diff --git a/crates/api/src/reports/post_report/resolve.rs b/crates/api/src/reports/post_report/resolve.rs index 37ceb4bea..fff6187b0 100644 --- a/crates/api/src/reports/post_report/resolve.rs +++ b/crates/api/src/reports/post_report/resolve.rs @@ -1,7 +1,9 @@ -use actix_web::web::{Data, Json}; +use activitypub_federation::config::Data; +use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, reports::post::{PostReportResponse, ResolvePostReport}, + send_activity::{ActivityChannel, SendActivityData}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable}; @@ -32,6 +34,7 @@ pub async fn resolve_post_report( .await .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; } else { + // TODO: not federated PostReport::unresolve(&mut context.pool(), report_id, person_id) .await .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; @@ -39,5 +42,15 @@ pub async fn resolve_post_report( let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id).await?; + ActivityChannel::submit_activity( + SendActivityData::SendResolveReport { + object_id: post_report_view.post.ap_id.inner().clone(), + actor: local_user_view.person, + report_creator: report.creator, + community: post_report_view.community.clone(), + }, + &context, + )?; + Ok(Json(PostReportResponse { post_report_view })) } diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index b606c9a90..fdefbaac0 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -99,6 +99,12 @@ pub enum SendActivityData { community: Community, reason: String, }, + SendResolveReport { + object_id: Url, + actor: Person, + report_creator: Person, + community: Community, + }, } // TODO: instead of static, move this into LemmyContext. make sure that stopping the process with diff --git a/crates/apub/assets/lemmy/activities/community/resolve_report_page.json b/crates/apub/assets/lemmy/activities/community/resolve_report_page.json new file mode 100644 index 000000000..6fc218c7d --- /dev/null +++ b/crates/apub/assets/lemmy/activities/community/resolve_report_page.json @@ -0,0 +1,14 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_user", + "to": ["http://enterprise.lemmy.ml/c/main"], + "type": "Resolve", + "id": "http://ds9.lemmy.ml/activities/flag/4323412-5e45-4a95-a15f-e0dc86361ba4", + "object": { + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": ["http://enterprise.lemmy.ml/c/main"], + "object": "http://enterprise.lemmy.ml/post/7", + "summary": "report this post", + "type": "Flag", + "id": "http://ds9.lemmy.ml/activities/flag/98b0933f-5e45-4a95-a15f-e0dc86361ba4" + } +} diff --git a/crates/apub/src/activities/community/mod.rs b/crates/apub/src/activities/community/mod.rs index 93c6e5c77..1efabe8e2 100644 --- a/crates/apub/src/activities/community/mod.rs +++ b/crates/apub/src/activities/community/mod.rs @@ -1,15 +1,22 @@ use crate::{ activities::send_lemmy_activity, activity_lists::AnnouncableActivities, - objects::{community::ApubCommunity, person::ApubPerson}, + fetcher::post_or_comment::PostOrComment, + objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson}, protocol::activities::community::announce::AnnounceActivity, }; -use activitypub_federation::{config::Data, traits::Actor}; +use activitypub_federation::{config::Data, fetch::object_id::ObjectId, traits::Actor}; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ - source::{activity::ActivitySendTargets, person::PersonFollower}, + source::{ + activity::ActivitySendTargets, + person::{Person, PersonFollower}, + site::Site, + }, + traits::Crud, CommunityVisibility, }; +use lemmy_db_views::structs::CommunityModeratorView; use lemmy_utils::error::LemmyResult; pub mod announce; @@ -17,6 +24,7 @@ pub mod collection_add; pub mod collection_remove; pub mod lock_page; pub mod report; +pub mod resolve_report; pub mod update; /// This function sends all activities which are happening in a community to the right inboxes. @@ -70,3 +78,37 @@ pub(crate) async fn send_activity_in_community( send_lemmy_activity(context, activity.clone(), actor, inboxes, false).await?; Ok(()) } + +async fn report_inboxes( + object_id: ObjectId, + community: &ApubCommunity, + context: &Data, +) -> LemmyResult { + // send report to the community where object was posted + let mut inboxes = ActivitySendTargets::to_inbox(community.shared_inbox_or_inbox()); + + if community.local { + // send to all moderators + let moderators = + CommunityModeratorView::for_community(&mut context.pool(), community.id).await?; + for m in moderators { + inboxes.add_inbox(m.moderator.inbox_url.into()); + } + + // also send report to user's home instance if possible + let object_creator_id = match object_id.dereference_local(context).await? { + PostOrComment::Post(p) => p.creator_id, + PostOrComment::Comment(c) => c.creator_id, + }; + let object_creator = Person::read(&mut context.pool(), object_creator_id).await?; + let object_creator_site: Option = + Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id) + .await + .ok() + .map(Into::into); + if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) { + inboxes.add_inbox(inbox); + } + } + Ok(inboxes) +} diff --git a/crates/apub/src/activities/community/report.rs b/crates/apub/src/activities/community/report.rs index ef57bb3e2..15597e4a2 100644 --- a/crates/apub/src/activities/community/report.rs +++ b/crates/apub/src/activities/community/report.rs @@ -1,9 +1,14 @@ +use super::report_inboxes; use crate::{ activities::{generate_activity_id, send_lemmy_activity, verify_person_in_community}, + activity_lists::AnnouncableActivities, insert_received_activity, - objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson}, + objects::{community::ApubCommunity, person::ApubPerson}, protocol::{ - activities::community::report::{Report, ReportObject}, + activities::community::{ + announce::AnnounceActivity, + report::{Report, ReportObject}, + }, InCommunity, }, PostOrComment, @@ -20,71 +25,49 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - activity::ActivitySendTargets, comment_report::{CommentReport, CommentReportForm}, - community::Community, - person::Person, post_report::{PostReport, PostReportForm}, - site::Site, }, - traits::{Crud, Reportable}, + traits::Reportable, }; -use lemmy_db_views::structs::CommunityModeratorView; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; impl Report { - #[tracing::instrument(skip_all)] - pub(crate) async fn send( - object_id: ObjectId, - actor: Person, - community: Community, - reason: String, - context: Data, - ) -> LemmyResult<()> { - let actor: ApubPerson = actor.into(); - let community: ApubCommunity = community.into(); + pub(crate) fn new( + object_id: &ObjectId, + actor: &ApubPerson, + community: &ApubCommunity, + reason: Option, + context: &Data, + ) -> LemmyResult { let kind = FlagType::Flag; let id = generate_activity_id( kind.clone(), &context.settings().get_protocol_and_hostname(), )?; - let report = Report { + Ok(Report { actor: actor.id().into(), to: [community.id().into()], object: ReportObject::Lemmy(object_id.clone()), - summary: Some(reason), + summary: reason, content: None, kind, id: id.clone(), - }; + }) + } - // send report to the community where object was posted - let mut inboxes = ActivitySendTargets::to_inbox(community.shared_inbox_or_inbox()); + pub(crate) async fn send( + object_id: ObjectId, + actor: &ApubPerson, + community: &ApubCommunity, + reason: String, + context: Data, + ) -> LemmyResult<()> { + let report = Self::new(&object_id, actor, community, Some(reason), &context)?; + let inboxes = report_inboxes(object_id, community, &context).await?; - // send to all moderators - let moderators = - CommunityModeratorView::for_community(&mut context.pool(), community.id).await?; - for m in moderators { - inboxes.add_inbox(m.moderator.actor_id.into()); - } - - // also send report to user's home instance if possible - let object_creator_id = match object_id.dereference_local(&context).await? { - PostOrComment::Post(p) => p.creator_id, - PostOrComment::Comment(c) => c.creator_id, - }; - let object_creator = Person::read(&mut context.pool(), object_creator_id).await?; - let object_creator_site: Option = - Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id) - .await - .ok() - .map(Into::into); - if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) { - inboxes.add_inbox(inbox); - } - - send_lemmy_activity(&context, report, &actor, inboxes, false).await + send_lemmy_activity(&context, report, actor, inboxes, false).await } } @@ -137,6 +120,17 @@ impl ActivityHandler for Report { CommentReport::report(&mut context.pool(), &report_form).await?; } }; + + let community = self.community(context).await?; + if community.local { + // forward to remote mods + let object_id = self.object.object_id(context).await?; + let announce = AnnouncableActivities::Report(self); + let announce = AnnounceActivity::new(announce.try_into()?, &community, context)?; + let inboxes = report_inboxes(object_id, &community, context).await?; + send_lemmy_activity(context, announce, &community, inboxes.clone(), false).await?; + } + Ok(()) } } diff --git a/crates/apub/src/activities/community/resolve_report.rs b/crates/apub/src/activities/community/resolve_report.rs new file mode 100644 index 000000000..e59e1fea6 --- /dev/null +++ b/crates/apub/src/activities/community/resolve_report.rs @@ -0,0 +1,110 @@ +use super::report_inboxes; +use crate::{ + activities::{ + generate_activity_id, + send_lemmy_activity, + verify_mod_action, + verify_person_in_community, + }, + activity_lists::AnnouncableActivities, + insert_received_activity, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::{ + activities::community::{ + announce::AnnounceActivity, + report::Report, + resolve_report::{ResolveReport, ResolveType}, + }, + InCommunity, + }, + PostOrComment, +}; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + protocol::verification::verify_urls_match, + traits::{ActivityHandler, Actor}, +}; +use lemmy_api_common::context::LemmyContext; +use lemmy_db_schema::{ + source::{comment_report::CommentReport, post_report::PostReport}, + traits::Reportable, +}; +use lemmy_utils::error::{LemmyError, LemmyResult}; +use url::Url; + +impl ResolveReport { + pub(crate) async fn send( + object_id: ObjectId, + actor: &ApubPerson, + report_creator: &ApubPerson, + community: &ApubCommunity, + context: Data, + ) -> LemmyResult<()> { + let kind = ResolveType::Resolve; + let id = generate_activity_id( + kind.clone(), + &context.settings().get_protocol_and_hostname(), + )?; + let object = Report::new(&object_id, report_creator, community, None, &context)?; + let resolve = ResolveReport { + actor: actor.id().into(), + to: [community.id().into()], + object, + kind, + id: id.clone(), + }; + let inboxes = report_inboxes(object_id, community, &context).await?; + + send_lemmy_activity(&context, resolve, actor, inboxes, false).await + } +} + +#[async_trait::async_trait] +impl ActivityHandler for ResolveReport { + type DataType = LemmyContext; + type Error = LemmyError; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + async fn verify(&self, context: &Data) -> LemmyResult<()> { + self.object.verify(context).await?; + let community = self.community(context).await?; + verify_person_in_community(&self.actor, &community, context).await?; + verify_urls_match(self.to[0].inner(), self.object.to[0].inner())?; + verify_mod_action(&self.actor, &community, context).await?; + Ok(()) + } + + async fn receive(self, context: &Data) -> LemmyResult<()> { + insert_received_activity(&self.id, context).await?; + let reporter = self.object.actor.dereference(context).await?; + let actor = self.actor.dereference(context).await?; + match self.object.object.dereference(context).await? { + PostOrComment::Post(post) => { + PostReport::resolve_apub(&mut context.pool(), post.id, reporter.id, actor.id).await?; + } + PostOrComment::Comment(comment) => { + CommentReport::resolve_apub(&mut context.pool(), comment.id, reporter.id, actor.id).await?; + } + }; + + let community = self.community(context).await?; + if community.local { + // forward to remote mods + let object_id = self.object.object.object_id(context).await?; + let announce = AnnouncableActivities::ResolveReport(self); + let announce = AnnounceActivity::new(announce.try_into()?, &community, context)?; + let inboxes = report_inboxes(object_id, &community, context).await?; + send_lemmy_activity(context, announce, &community, inboxes.clone(), false).await?; + } + + Ok(()) + } +} diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index 9ab4023c0..4a284aa7b 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -18,7 +18,7 @@ use crate::{ }, objects::{community::ApubCommunity, person::ApubPerson}, protocol::activities::{ - community::report::Report, + community::{report::Report, resolve_report::ResolveReport}, create_or_update::{note::CreateOrUpdateNote, page::CreateOrUpdatePage}, CreateOrUpdateType, }, @@ -378,7 +378,31 @@ pub async fn match_outgoing_activities( actor, community, reason, - } => Report::send(ObjectId::from(object_id), actor, community, reason, context).await, + } => { + Report::send( + ObjectId::from(object_id), + &actor.into(), + &community.into(), + reason, + context, + ) + .await + } + SendResolveReport { + object_id, + actor, + report_creator, + community, + } => { + ResolveReport::send( + ObjectId::from(object_id), + &actor.into(), + &report_creator.into(), + &community.into(), + context, + ) + .await + } AcceptFollower(community_id, person_id) => { send_accept_or_reject_follow(community_id, person_id, true, &context).await } diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index 0113d15f8..68cfeb377 100644 --- a/crates/apub/src/activity_lists.rs +++ b/crates/apub/src/activity_lists.rs @@ -9,6 +9,7 @@ use crate::{ collection_remove::CollectionRemove, lock_page::{LockPage, UndoLockPage}, report::Report, + resolve_report::ResolveReport, update::UpdateCommunity, }, create_or_update::{note_wrapper::CreateOrUpdateNoteWrapper, page::CreateOrUpdatePage}, @@ -45,6 +46,7 @@ pub enum SharedInboxActivities { RejectFollow(RejectFollow), UndoFollow(UndoFollow), Report(Report), + ResolveReport(ResolveReport), AnnounceActivity(AnnounceActivity), /// This is a catch-all and needs to be last RawAnnouncableActivities(RawAnnouncableActivities), @@ -67,6 +69,8 @@ pub enum AnnouncableActivities { CollectionRemove(CollectionRemove), LockPost(LockPage), UndoLockPost(UndoLockPage), + Report(Report), + ResolveReport(ResolveReport), // For compatibility with Pleroma/Mastodon (send only) Page(Page), } @@ -89,6 +93,8 @@ impl InCommunity for AnnouncableActivities { CollectionRemove(a) => a.community(context).await, LockPost(a) => a.community(context).await, UndoLockPost(a) => a.community(context).await, + Report(a) => a.community(context).await, + ResolveReport(a) => a.community(context).await, Page(_) => Err(LemmyErrorType::NotFound.into()), } } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 99f781c31..aeb5a8d05 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -17,6 +17,7 @@ use lemmy_utils::{ use moka::future::Cache; use serde_json::Value; use std::sync::{Arc, LazyLock}; +use tracing::debug; use url::Url; pub mod activities; @@ -213,6 +214,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness( /// This ensures that the same activity doesn't get received and processed more than once, which /// would be a waste of resources. async fn insert_received_activity(ap_id: &Url, data: &Data) -> LemmyResult<()> { + debug!("Received activity {}", ap_id.to_string()); ReceivedActivity::create(&mut data.pool(), &ap_id.clone().into()).await?; Ok(()) } diff --git a/crates/apub/src/protocol/activities/community/mod.rs b/crates/apub/src/protocol/activities/community/mod.rs index 0c52e6e77..ab734fc4f 100644 --- a/crates/apub/src/protocol/activities/community/mod.rs +++ b/crates/apub/src/protocol/activities/community/mod.rs @@ -3,10 +3,12 @@ pub mod collection_add; pub mod collection_remove; pub mod lock_page; pub mod report; +pub mod resolve_report; pub mod update; #[cfg(test)] mod tests { + use super::resolve_report::ResolveReport; use crate::protocol::{ activities::community::{ announce::AnnounceActivity, @@ -44,6 +46,10 @@ mod tests { )?; test_parse_lemmy_item::("assets/lemmy/activities/community/report_page.json")?; + test_parse_lemmy_item::( + "assets/lemmy/activities/community/resolve_report_page.json", + )?; + Ok(()) } } diff --git a/crates/apub/src/protocol/activities/community/report.rs b/crates/apub/src/protocol/activities/community/report.rs index 71ecf4c5c..5fd3415b1 100644 --- a/crates/apub/src/protocol/activities/community/report.rs +++ b/crates/apub/src/protocol/activities/community/report.rs @@ -49,14 +49,17 @@ pub(crate) enum ReportObject { } impl ReportObject { - pub async fn dereference(self, context: &Data) -> LemmyResult { + pub(crate) async fn dereference( + &self, + context: &Data, + ) -> LemmyResult { match self { ReportObject::Lemmy(l) => l.dereference(context).await, ReportObject::Mastodon(objects) => { for o in objects { // Find the first reported item which can be dereferenced as post or comment (Lemmy can // only handle one item per report). - let deref = ObjectId::from(o).dereference(context).await; + let deref = ObjectId::from(o.clone()).dereference(context).await; if deref.is_ok() { return deref; } @@ -65,6 +68,27 @@ impl ReportObject { } } } + + pub(crate) async fn object_id( + &self, + context: &Data, + ) -> LemmyResult> { + match self { + ReportObject::Lemmy(l) => Ok(l.clone()), + ReportObject::Mastodon(objects) => { + for o in objects { + // Same logic as above, but return the ID and not the object itself. + let deref = ObjectId::::from(o.clone()) + .dereference(context) + .await; + if deref.is_ok() { + return Ok(o.clone().into()); + } + } + Err(LemmyErrorType::NotFound.into()) + } + } + } } #[async_trait::async_trait] diff --git a/crates/apub/src/protocol/activities/community/resolve_report.rs b/crates/apub/src/protocol/activities/community/resolve_report.rs new file mode 100644 index 000000000..6f9c36234 --- /dev/null +++ b/crates/apub/src/protocol/activities/community/resolve_report.rs @@ -0,0 +1,39 @@ +use super::report::Report; +use crate::{ + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::InCommunity, +}; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + protocol::helpers::deserialize_one, +}; +use lemmy_api_common::context::LemmyContext; +use lemmy_utils::error::LemmyResult; +use serde::{Deserialize, Serialize}; +use strum::Display; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display)] +pub enum ResolveType { + Resolve, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolveReport { + pub(crate) actor: ObjectId, + #[serde(deserialize_with = "deserialize_one")] + pub(crate) to: [ObjectId; 1], + pub(crate) object: Report, + #[serde(rename = "type")] + pub(crate) kind: ResolveType, + pub(crate) id: Url, +} + +#[async_trait::async_trait] +impl InCommunity for ResolveReport { + async fn community(&self, context: &Data) -> LemmyResult { + self.object.community(context).await + } +} diff --git a/crates/db_schema/src/impls/comment_report.rs b/crates/db_schema/src/impls/comment_report.rs index 4c6a1e0d0..0d21c5a8e 100644 --- a/crates/db_schema/src/impls/comment_report.rs +++ b/crates/db_schema/src/impls/comment_report.rs @@ -1,9 +1,6 @@ use crate::{ newtypes::{CommentId, CommentReportId, PersonId}, - schema::comment_report::{ - comment_id, - dsl::{comment_report, resolved, resolver_id, updated}, - }, + schema::comment_report, source::comment_report::{CommentReport, CommentReportForm}, traits::Reportable, utils::{get_conn, DbPool}, @@ -12,10 +9,12 @@ use chrono::Utc; use diesel::{ dsl::{insert_into, update}, result::Error, + BoolExpressionMethods, ExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; #[async_trait] impl Reportable for CommentReport { @@ -31,7 +30,7 @@ impl Reportable for CommentReport { comment_report_form: &CommentReportForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(comment_report) + insert_into(comment_report::table) .values(comment_report_form) .get_result::(conn) .await @@ -48,27 +47,52 @@ impl Reportable for CommentReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(comment_report.find(report_id_)) + update(comment_report::table.find(report_id_)) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + comment_report::resolved.eq(true), + comment_report::resolver_id.eq(by_resolver_id), + comment_report::updated.eq(Utc::now()), )) .execute(conn) .await } + async fn resolve_apub( + pool: &mut DbPool<'_>, + object_id: Self::ObjectIdType, + report_creator_id: PersonId, + resolver_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + Ok( + update( + comment_report::table.filter( + comment_report::comment_id + .eq(object_id) + .and(comment_report::creator_id.eq(report_creator_id)), + ), + ) + .set(( + comment_report::resolved.eq(true), + comment_report::resolver_id.eq(resolver_id), + comment_report::updated.eq(Utc::now()), + )) + .execute(conn) + .await?, + ) + } + async fn resolve_all_for_object( pool: &mut DbPool<'_>, comment_id_: CommentId, by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(comment_report.filter(comment_id.eq(comment_id_))) + update(comment_report::table.filter(comment_report::comment_id.eq(comment_id_))) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + comment_report::resolved.eq(true), + comment_report::resolver_id.eq(by_resolver_id), + comment_report::updated.eq(Utc::now()), )) .execute(conn) .await @@ -85,11 +109,11 @@ impl Reportable for CommentReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(comment_report.find(report_id_)) + update(comment_report::table.find(report_id_)) .set(( - resolved.eq(false), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + comment_report::resolved.eq(false), + comment_report::resolver_id.eq(by_resolver_id), + comment_report::updated.eq(Utc::now()), )) .execute(conn) .await diff --git a/crates/db_schema/src/impls/community_report.rs b/crates/db_schema/src/impls/community_report.rs index 85c3cc5c0..f62545072 100644 --- a/crates/db_schema/src/impls/community_report.rs +++ b/crates/db_schema/src/impls/community_report.rs @@ -1,9 +1,6 @@ use crate::{ newtypes::{CommunityId, CommunityReportId, PersonId}, - schema::community_report::{ - community_id, - dsl::{community_report, resolved, resolver_id, updated}, - }, + schema::community_report, source::community_report::{CommunityReport, CommunityReportForm}, traits::Reportable, utils::{get_conn, DbPool}, @@ -12,10 +9,12 @@ use chrono::Utc; use diesel::{ dsl::{insert_into, update}, result::Error, + BoolExpressionMethods, ExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; #[async_trait] impl Reportable for CommunityReport { @@ -31,7 +30,7 @@ impl Reportable for CommunityReport { community_report_form: &CommunityReportForm, ) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(community_report) + insert_into(community_report::table) .values(community_report_form) .get_result::(conn) .await @@ -48,27 +47,52 @@ impl Reportable for CommunityReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(community_report.find(report_id_)) + update(community_report::table.find(report_id_)) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + community_report::resolved.eq(true), + community_report::resolver_id.eq(by_resolver_id), + community_report::updated.eq(Utc::now()), )) .execute(conn) .await } + async fn resolve_apub( + pool: &mut DbPool<'_>, + object_id: Self::ObjectIdType, + report_creator_id: PersonId, + resolver_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + Ok( + update( + community_report::table.filter( + community_report::community_id + .eq(object_id) + .and(community_report::creator_id.eq(report_creator_id)), + ), + ) + .set(( + community_report::resolved.eq(true), + community_report::resolver_id.eq(resolver_id), + community_report::updated.eq(Utc::now()), + )) + .execute(conn) + .await?, + ) + } + async fn resolve_all_for_object( pool: &mut DbPool<'_>, community_id_: CommunityId, by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(community_report.filter(community_id.eq(community_id_))) + update(community_report::table.filter(community_report::community_id.eq(community_id_))) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + community_report::resolved.eq(true), + community_report::resolver_id.eq(by_resolver_id), + community_report::updated.eq(Utc::now()), )) .execute(conn) .await @@ -85,11 +109,11 @@ impl Reportable for CommunityReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(community_report.find(report_id_)) + update(community_report::table.find(report_id_)) .set(( - resolved.eq(false), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + community_report::resolved.eq(false), + community_report::resolver_id.eq(by_resolver_id), + community_report::updated.eq(Utc::now()), )) .execute(conn) .await diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index 90ac030c1..70dcf16ad 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -1,9 +1,7 @@ use crate::{ + diesel::BoolExpressionMethods, newtypes::{PersonId, PostId, PostReportId}, - schema::post_report::{ - dsl::{post_report, resolved, resolver_id, updated}, - post_id, - }, + schema::post_report, source::post_report::{PostReport, PostReportForm}, traits::Reportable, utils::{get_conn, DbPool}, @@ -16,6 +14,7 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; #[async_trait] impl Reportable for PostReport { @@ -25,7 +24,7 @@ impl Reportable for PostReport { async fn report(pool: &mut DbPool<'_>, post_report_form: &PostReportForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(post_report) + insert_into(post_report::table) .values(post_report_form) .get_result::(conn) .await @@ -37,27 +36,52 @@ impl Reportable for PostReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(post_report.find(report_id)) + update(post_report::table.find(report_id)) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + post_report::resolved.eq(true), + post_report::resolver_id.eq(by_resolver_id), + post_report::updated.eq(Utc::now()), )) .execute(conn) .await } + async fn resolve_apub( + pool: &mut DbPool<'_>, + object_id: Self::ObjectIdType, + report_creator_id: PersonId, + resolver_id: PersonId, + ) -> LemmyResult { + let conn = &mut get_conn(pool).await?; + Ok( + update( + post_report::table.filter( + post_report::post_id + .eq(object_id) + .and(post_report::creator_id.eq(report_creator_id)), + ), + ) + .set(( + post_report::resolved.eq(true), + post_report::resolver_id.eq(resolver_id), + post_report::updated.eq(Utc::now()), + )) + .execute(conn) + .await?, + ) + } + async fn resolve_all_for_object( pool: &mut DbPool<'_>, post_id_: PostId, by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(post_report.filter(post_id.eq(post_id_))) + update(post_report::table.filter(post_report::post_id.eq(post_id_))) .set(( - resolved.eq(true), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + post_report::resolved.eq(true), + post_report::resolver_id.eq(by_resolver_id), + post_report::updated.eq(Utc::now()), )) .execute(conn) .await @@ -69,11 +93,11 @@ impl Reportable for PostReport { by_resolver_id: PersonId, ) -> Result { let conn = &mut get_conn(pool).await?; - update(post_report.find(report_id)) + update(post_report::table.find(report_id)) .set(( - resolved.eq(false), - resolver_id.eq(by_resolver_id), - updated.eq(Utc::now()), + post_report::resolved.eq(false), + post_report::resolver_id.eq(by_resolver_id), + post_report::updated.eq(Utc::now()), )) .execute(conn) .await diff --git a/crates/db_schema/src/impls/private_message_report.rs b/crates/db_schema/src/impls/private_message_report.rs index 0a83bf637..cc1f41769 100644 --- a/crates/db_schema/src/impls/private_message_report.rs +++ b/crates/db_schema/src/impls/private_message_report.rs @@ -13,6 +13,7 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; +use lemmy_utils::error::{FederationError, LemmyResult}; #[async_trait] impl Reportable for PrivateMessageReport { @@ -46,6 +47,14 @@ impl Reportable for PrivateMessageReport { .execute(conn) .await } + async fn resolve_apub( + _pool: &mut DbPool<'_>, + _object_id: Self::ObjectIdType, + _report_creator_id: PersonId, + _resolver_id: PersonId, + ) -> LemmyResult { + Err(FederationError::Unreachable.into()) + } // TODO: this is unused because private message doesn't have remove handler async fn resolve_all_for_object( diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index f010d3d1b..56bbe1eb8 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -15,6 +15,7 @@ use diesel_async::{ AsyncPgConnection, RunQueryDsl, }; +use lemmy_utils::error::LemmyResult; /// Returned by `diesel::delete` pub type Delete = DeleteStatement<::Table, ::WhereClause>; @@ -154,6 +155,14 @@ pub trait Reportable { report_id: Self::IdType, resolver_id: PersonId, ) -> Result + where + Self: Sized; + async fn resolve_apub( + pool: &mut DbPool<'_>, + object_id: Self::ObjectIdType, + report_creator_id: PersonId, + resolver_id: PersonId, + ) -> LemmyResult where Self: Sized; async fn resolve_all_for_object(