diff --git a/api_tests/package.json b/api_tests/package.json index 68cb125e4..e8f62b23f 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -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", diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index b1739d4b4..7ded27993 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -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 diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 280396b00..5e78dc518 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -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 diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 8c7a79331..87b2991b8 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -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 { } } +export async function reportPost( + api: API, + post_id: number, + reason: string, +): Promise { + let form: CreatePostReport = { + post_id, + reason, + auth: api.auth, + }; + return api.client.createPostReport(form); +} + +export async function listPostReports(api: API): Promise { + let form: ListPostReports = { + auth: api.auth, + }; + return api.client.listPostReports(form); +} + +export async function reportComment( + api: API, + comment_id: number, + reason: string, +): Promise { + let form: CreateCommentReport = { + comment_id, + reason, + auth: api.auth, + }; + return api.client.createCommentReport(form); +} + +export async function listCommentReports(api: API): Promise { + 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; diff --git a/api_tests/yarn.lock b/api_tests/yarn.lock index 65ea5d396..2d9d43b47 100644 --- a/api_tests/yarn.lock +++ b/api_tests/yarn.lock @@ -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" diff --git a/crates/api/src/comment_report.rs b/crates/api/src/comment_report.rs index d12b8d6f2..5d2561dd9 100644 --- a/crates/api/src/comment_report.rs +++ b/crates/api/src/comment_report.rs @@ -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) } } diff --git a/crates/api/src/post_report.rs b/crates/api/src/post_report.rs index 82f3c44a4..5b8b0b656 100644 --- a/crates/api/src/post_report.rs +++ b/crates/api/src/post_report.rs @@ -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) } } diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index 43f86572f..618c14f50 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -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 index 000000000..c08210b3d --- /dev/null +++ b/crates/apub/src/activities/report.rs @@ -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, + to: [ObjectId; 1], + object: ObjectId, + summary: String, + #[serde(rename = "type")] + kind: FlagType, + id: Url, + #[serde(rename = "@context")] + context: OneOrMany, + #[serde(flatten)] + unparsed: Unparsed, +} + +impl Report { + pub async fn send( + object_id: ObjectId, + 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, + 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, + 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(()) + } +} diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 119dfe263..3f74b622a 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -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.