Federate reports (#1830)
authorNutomic <me@nutomic.com>
Fri, 15 Oct 2021 14:37:33 +0000 (14:37 +0000)
committerGitHub <noreply@github.com>
Fri, 15 Oct 2021 14:37:33 +0000 (10:37 -0400)
* Federate reports

* add federation test cases for reports

api_tests/package.json
api_tests/src/comment.spec.ts
api_tests/src/post.spec.ts
api_tests/src/shared.ts
api_tests/yarn.lock
crates/api/src/comment_report.rs
crates/api/src/post_report.rs
crates/apub/src/activities/mod.rs
crates/apub/src/activities/report.rs [new file with mode: 0644]
crates/apub/src/http/community.rs

index 68cb125e494f6928ed430da45e1b065e815c06cb..e8f62b23ff517b64c4d76c970e6b5417c181a446 100644 (file)
@@ -16,7 +16,7 @@
     "eslint": "^7.30.0",
     "eslint-plugin-jane": "^9.0.3",
     "jest": "^27.0.6",
-    "lemmy-js-client": "0.12.0",
+    "lemmy-js-client": "0.13.1-rc.1",
     "node-fetch": "^2.6.1",
     "prettier": "^2.3.2",
     "ts-jest": "^27.0.3",
index b1739d4b439bb85357ed65d2cd5158f0a1986208..7ded279933ed4475620972c0e77bdb58ed7f259e 100644 (file)
@@ -19,6 +19,9 @@ import {
   unfollowRemotes,
   createCommunity,
   registerUser,
+  reportComment,
+  listCommentReports,
+  randomString,
   API,
 } from './shared';
 import { CommentView } from 'lemmy-js-client';
@@ -382,3 +385,23 @@ test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedde
 
   await unfollowRemotes(alpha);
 });
+
+
+test('Report a comment', async () => {
+  let betaCommunity = (await resolveBetaCommunity(beta)).community;
+  console.log(betaCommunity);
+  let postRes = (await createPost(beta, betaCommunity.community.id)).post_view.post;
+  expect(postRes).toBeDefined();
+  let commentRes = (await createComment(beta, postRes.id)).comment_view.comment;
+  expect(commentRes).toBeDefined();
+
+  let alphaComment = (await resolveComment(alpha, commentRes)).comment.comment;
+  let alphaReport = (await reportComment(alpha, alphaComment.id, randomString(10)))
+        .comment_report_view.comment_report;
+
+  let betaReport = (await listCommentReports(beta)).comment_reports[0].comment_report;
+  expect(betaReport).toBeDefined();
+  expect(betaReport.resolved).toBe(false);
+  expect(betaReport.original_comment_text).toBe(alphaReport.original_comment_text);
+  expect(betaReport.reason).toBe(alphaReport.reason);
+});
\ No newline at end of file
index 280396b003eb86cadc06d09a83643e44a3db92b9..5e78dc5187d1ba9f54bd420cfd4e4dce8dd1917d 100644 (file)
@@ -24,6 +24,9 @@ import {
   searchPostLocal,
   followCommunity,
   banPersonFromCommunity,
+  reportPost,
+  listPostReports,
+  randomString,
 } from './shared';
 import { PostView, CommunityView } from 'lemmy-js-client';
 
@@ -352,3 +355,22 @@ test('Enforce community ban for federated user', async () => {
   let betaPost = searchBeta.posts[0];
   expect(betaPost).toBeDefined();
 });
+
+test('Report a post', async () => {
+  let betaCommunity = (await resolveBetaCommunity(beta)).community;
+  console.log(betaCommunity);
+  let postRes = await createPost(beta, betaCommunity.community.id);
+  expect(postRes.post_view.post).toBeDefined();
+
+  let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
+  let alphaReport = (await reportPost(alpha, alphaPost.post.id, randomString(10)))
+        .post_report_view.post_report;
+
+  let betaReport = (await listPostReports(beta)).post_reports[0].post_report;
+  expect(betaReport).toBeDefined();
+  expect(betaReport.resolved).toBe(false);
+  expect(betaReport.original_post_name).toBe(alphaReport.original_post_name);
+  expect(betaReport.original_post_url).toBe(alphaReport.original_post_url);
+  expect(betaReport.original_post_body).toBe(alphaReport.original_post_body);
+  expect(betaReport.reason).toBe(alphaReport.reason);
+});
\ No newline at end of file
index 8c7a793313d92d50b4c0c905b495734b1dc2f951..87b2991b8633672d1f4015fdc4724a8bd3478981 100644 (file)
@@ -49,6 +49,15 @@ import {
   CreatePrivateMessage,
   ResolveObjectResponse,
   ResolveObject,
+  CreatePostReport,
+  PostReport,
+  ListPostReports,
+  PostReportResponse,
+  ListPostReportsResponse,
+  CreateCommentReport,
+  CommentReportResponse,
+  ListCommentReports,
+  ListCommentReportsResponse,
 } from 'lemmy-js-client';
 
 export interface API {
@@ -586,6 +595,46 @@ export async function followBeta(api: API): Promise<CommunityResponse> {
   }
 }
 
+export async function reportPost(
+  api: API,
+  post_id: number,
+  reason: string,
+): Promise<PostReportResponse> {
+  let form: CreatePostReport = {
+    post_id,
+    reason,
+    auth: api.auth,
+  };
+  return api.client.createPostReport(form);
+}
+
+export async function listPostReports(api: API): Promise<ListPostReportsResponse> {
+  let form: ListPostReports = {
+    auth: api.auth,
+  };
+  return api.client.listPostReports(form);
+}
+
+export async function reportComment(
+  api: API,
+  comment_id: number,
+  reason: string,
+): Promise<CommentReportResponse> {
+  let form: CreateCommentReport = {
+    comment_id,
+    reason,
+    auth: api.auth,
+  };
+  return api.client.createCommentReport(form);
+}
+
+export async function listCommentReports(api: API): Promise<ListCommentReportsResponse> {
+  let form: ListCommentReports = {
+    auth: api.auth,
+  };
+  return api.client.listCommentReports(form);
+}
+
 export function delay(millis: number = 500) {
   return new Promise(resolve => setTimeout(resolve, millis));
 }
@@ -598,7 +647,7 @@ export function wrapper(form: any): string {
   return JSON.stringify(form);
 }
 
-function randomString(length: number): string {
+export function randomString(length: number): string {
   var result = '';
   var characters = 'abcdefghijklmnopqrstuvwxyz0123456789_';
   var charactersLength = characters.length;
index 65ea5d3962e37e6a6828c2dbd83da882244b2abb..2d9d43b478159783560b0292571e43dc01c06739 100644 (file)
@@ -3076,10 +3076,10 @@ language-tags@^1.0.5:
   dependencies:
     language-subtag-registry "~0.3.2"
 
-lemmy-js-client@0.12.0:
-  version "0.12.0"
-  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.12.0.tgz#2337aca9d8b38d92908d7f7a9479f0066a9eaeae"
-  integrity sha512-PSebUBkojM7OUlfSXKQhL4IcYKaKF+Xj2G0+pybaCvP9sJvviy32qHUi9BQeIhRHXgw8ILRH0Y+xZGKu0a3wvQ==
+lemmy-js-client@0.13.1-rc.1:
+  version "0.13.1-rc.1"
+  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.13.1-rc.1.tgz#e1af4749a5493954a17f87b7b20dcccb8c585f22"
+  integrity sha512-fncCq6Zu8s6GpeCrkmJS8/rqXcyrJ8p8EyWfXiiuZlWkgzOIi+qZjTRnO63wI6DomYwVOjwk7sry4RbOJSdK5Q==
 
 leven@^3.1.0:
   version "3.1.0"
index d12b8d6f2762ec5387ce9096998f9e5052794f0e..5d2561dd9bb0976346a121788fba8eb536ae3239 100644 (file)
@@ -7,6 +7,7 @@ use lemmy_api_common::{
   get_local_user_view_from_jwt,
   is_mod_or_admin,
 };
+use lemmy_apub::{activities::report::Report, fetcher::object_id::ObjectId};
 use lemmy_db_queries::Reportable;
 use lemmy_db_schema::source::comment_report::*;
 use lemmy_db_views::{
@@ -77,6 +78,15 @@ impl Perform for CreateCommentReport {
       websocket_id,
     });
 
+    Report::send(
+      ObjectId::new(comment_view.comment.ap_id),
+      &local_user_view.person,
+      comment_view.community.id,
+      reason.to_string(),
+      context,
+    )
+    .await?;
+
     Ok(res)
   }
 }
index 82f3c44a4e190dc2f9d0043484db7bc2d4b16113..5b8b0b656f0d8f47dbcad286864724915d07701a 100644 (file)
@@ -13,6 +13,7 @@ use lemmy_api_common::{
     ResolvePostReport,
   },
 };
+use lemmy_apub::{activities::report::Report, fetcher::object_id::ObjectId};
 use lemmy_db_queries::Reportable;
 use lemmy_db_schema::source::post_report::{PostReport, PostReportForm};
 use lemmy_db_views::{
@@ -83,6 +84,15 @@ impl Perform for CreatePostReport {
       websocket_id,
     });
 
+    Report::send(
+      ObjectId::new(post_view.post.ap_id),
+      &local_user_view.person,
+      post_view.community.id,
+      reason.to_string(),
+      context,
+    )
+    .await?;
+
     Ok(res)
   }
 }
index 43f86572fa2a117367a0a601c09c7a7bad2576b1..618c14f50ddca2a88158d62df5d8dffba540df15 100644 (file)
@@ -22,6 +22,7 @@ pub mod deletion;
 pub mod following;
 pub mod post;
 pub mod private_message;
+pub mod report;
 pub mod undo_remove;
 pub mod voting;
 
diff --git a/crates/apub/src/activities/report.rs b/crates/apub/src/activities/report.rs
new file mode 100644 (file)
index 0000000..c08210b
--- /dev/null
@@ -0,0 +1,168 @@
+use crate::{
+  activities::{generate_activity_id, verify_activity, verify_person_in_community},
+  context::lemmy_context,
+  fetcher::object_id::ObjectId,
+  send_lemmy_activity,
+  PostOrComment,
+};
+use activitystreams::{
+  activity::kind::FlagType,
+  base::AnyBase,
+  primitives::OneOrMany,
+  unparsed::Unparsed,
+};
+use lemmy_api_common::{blocking, comment::CommentReportResponse, post::PostReportResponse};
+use lemmy_apub_lib::{
+  data::Data,
+  traits::{ActivityFields, ActivityHandler, ActorType},
+};
+use lemmy_db_queries::{Crud, Reportable};
+use lemmy_db_schema::{
+  source::{
+    comment_report::{CommentReport, CommentReportForm},
+    community::Community,
+    person::Person,
+    post_report::{PostReport, PostReportForm},
+  },
+  CommunityId,
+};
+use lemmy_db_views::{comment_report_view::CommentReportView, post_report_view::PostReportView};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation};
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+#[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)]
+#[serde(rename_all = "camelCase")]
+pub struct Report {
+  actor: ObjectId<Person>,
+  to: [ObjectId<Community>; 1],
+  object: ObjectId<PostOrComment>,
+  summary: String,
+  #[serde(rename = "type")]
+  kind: FlagType,
+  id: Url,
+  #[serde(rename = "@context")]
+  context: OneOrMany<AnyBase>,
+  #[serde(flatten)]
+  unparsed: Unparsed,
+}
+
+impl Report {
+  pub async fn send(
+    object_id: ObjectId<PostOrComment>,
+    actor: &Person,
+    community_id: CommunityId,
+    reason: String,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError> {
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
+    let kind = FlagType::Flag;
+    let id = generate_activity_id(
+      kind.clone(),
+      &context.settings().get_protocol_and_hostname(),
+    )?;
+    let report = Report {
+      actor: ObjectId::new(actor.actor_id()),
+      to: [ObjectId::new(community.actor_id())],
+      object: object_id,
+      summary: reason,
+      kind,
+      id: id.clone(),
+      context: lemmy_context(),
+      unparsed: Default::default(),
+    };
+    send_lemmy_activity(
+      context,
+      &report,
+      &id,
+      actor,
+      vec![community.shared_inbox_or_inbox_url()],
+      false,
+    )
+    .await
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for Report {
+  type DataType = LemmyContext;
+  async fn verify(
+    &self,
+    context: &Data<LemmyContext>,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self, &context.settings())?;
+    verify_person_in_community(&self.actor, &self.to[0], context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    self,
+    context: &Data<LemmyContext>,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let actor = self.actor.dereference(context, request_counter).await?;
+    match self.object.dereference(context, request_counter).await? {
+      PostOrComment::Post(post) => {
+        let report_form = PostReportForm {
+          creator_id: actor.id,
+          post_id: post.id,
+          original_post_name: post.name,
+          original_post_url: post.url,
+          reason: self.summary,
+          original_post_body: post.body,
+        };
+
+        let report = blocking(context.pool(), move |conn| {
+          PostReport::report(conn, &report_form)
+        })
+        .await??;
+
+        let post_report_view = blocking(context.pool(), move |conn| {
+          PostReportView::read(conn, report.id, actor.id)
+        })
+        .await??;
+
+        context.chat_server().do_send(SendModRoomMessage {
+          op: UserOperation::CreateCommentReport,
+          response: PostReportResponse { post_report_view },
+          community_id: post.community_id,
+          websocket_id: None,
+        });
+      }
+      PostOrComment::Comment(comment) => {
+        let report_form = CommentReportForm {
+          creator_id: actor.id,
+          comment_id: comment.id,
+          original_comment_text: comment.content,
+          reason: self.summary,
+        };
+
+        let report = blocking(context.pool(), move |conn| {
+          CommentReport::report(conn, &report_form)
+        })
+        .await??;
+
+        let comment_report_view = blocking(context.pool(), move |conn| {
+          CommentReportView::read(conn, report.id, actor.id)
+        })
+        .await??;
+        let community_id = comment_report_view.community.id;
+
+        context.chat_server().do_send(SendModRoomMessage {
+          op: UserOperation::CreateCommentReport,
+          response: CommentReportResponse {
+            comment_report_view,
+          },
+          community_id,
+          websocket_id: None,
+        });
+      }
+    };
+    Ok(())
+  }
+}
index 119dfe26329fdc8476eabe3db1a09aa910c029c5..3f74b622aade62f324d90de5e65eb63b7efa5631 100644 (file)
@@ -3,6 +3,7 @@ use crate::{
     community::announce::{AnnouncableActivities, AnnounceActivity},
     extract_community,
     following::{follow::FollowCommunity, undo::UndoFollowCommunity},
+    report::Report,
   },
   context::lemmy_context,
   generate_moderators_url,
@@ -65,6 +66,7 @@ pub enum GroupInboxActivities {
   FollowCommunity(FollowCommunity),
   UndoFollowCommunity(UndoFollowCommunity),
   AnnouncableActivities(AnnouncableActivities),
+  Report(Report),
 }
 
 /// Handler for all incoming receive to community inboxes.