From: Nutomic Date: Mon, 19 Sep 2022 22:58:42 +0000 (+0000) Subject: Implement reports for private messages (#2433) X-Git-Url: http://these/git/%22https:/image.com/%7B%7D/static/%22%7Burl%7D/%7BjoinLemmyUrl%7D?a=commitdiff_plain;h=004efd5d94da0eee2a51f8c88a441c0c4afe3871;p=lemmy.git Implement reports for private messages (#2433) * Implement reports for private messages * finish private message report view + test * implement api for pm reports * merge list report api calls into one, move report count to site * fix compile error * Revert "merge list report api calls into one, move report count to site" This reverts commit 3bf3b06a705c6bcf2bf20d07e2819b81298790f3. * add websocket messages for pm report created/resolved * remove private_message_report_view * add joinable private_message_report -> person_alias_1 * Address review comments --- diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs index 6a1c2287..6d21f066 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/comment_report/create.rs @@ -1,4 +1,4 @@ -use crate::Perform; +use crate::{check_report_reason, Perform}; use activitypub_federation::core::object_id::ObjectId; use actix_web::web::Data; use lemmy_api_common::{ @@ -29,14 +29,8 @@ impl Perform for CreateCommentReport { let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - // check size of report and check for whitespace - let reason = data.reason.trim(); - if reason.is_empty() { - return Err(LemmyError::from_message("report_reason_required")); - } - if reason.chars().count() > 1000 { - return Err(LemmyError::from_message("report_too_long")); - } + let reason = self.reason.trim(); + check_report_reason(reason, context)?; let person_id = local_user_view.person.id; let comment_id = data.comment_id; @@ -51,7 +45,7 @@ impl Perform for CreateCommentReport { creator_id: person_id, comment_id, original_comment_text: comment_view.comment.content, - reason: data.reason.to_owned(), + reason: reason.to_owned(), }; let report = blocking(context.pool(), move |conn| { diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 026045dd..2659535a 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,7 +1,15 @@ use actix_web::{web, web::Data}; use captcha::Captcha; -use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*, websocket::*}; -use lemmy_utils::{error::LemmyError, ConnectionId}; +use lemmy_api_common::{ + comment::*, + community::*, + person::*, + post::*, + private_message::*, + site::*, + websocket::*, +}; +use lemmy_utils::{error::LemmyError, utils::check_slurs, ConnectionId}; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; use serde::Deserialize; @@ -12,6 +20,7 @@ mod local_user; mod post; mod post_report; mod private_message; +mod private_message_report; mod site; mod websocket; @@ -98,6 +107,15 @@ pub async fn match_websocket_operation( UserOperation::MarkPrivateMessageAsRead => { do_websocket_operation::(context, id, op, data).await } + UserOperation::CreatePrivateMessageReport => { + do_websocket_operation::(context, id, op, data).await + } + UserOperation::ResolvePrivateMessageReport => { + do_websocket_operation::(context, id, op, data).await + } + UserOperation::ListPrivateMessageReports => { + do_websocket_operation::(context, id, op, data).await + } // Site ops UserOperation::GetModlog => do_websocket_operation::(context, id, op, data).await, @@ -208,6 +226,18 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> String { base64::encode(concat_letters) } +/// Check size of report and remove whitespace +pub(crate) fn check_report_reason(reason: &str, context: &LemmyContext) -> Result<(), LemmyError> { + check_slurs(reason, &context.settings().slur_regex())?; + if reason.is_empty() { + return Err(LemmyError::from_message("report_reason_required")); + } + if reason.chars().count() > 1000 { + return Err(LemmyError::from_message("report_too_long")); + } + Ok(()) +} + #[cfg(test)] mod tests { use lemmy_api_common::utils::check_validator_time; diff --git a/crates/api/src/local_user/report_count.rs b/crates/api/src/local_user/report_count.rs index 774b2fbb..a6556f6c 100644 --- a/crates/api/src/local_user/report_count.rs +++ b/crates/api/src/local_user/report_count.rs @@ -4,7 +4,7 @@ use lemmy_api_common::{ person::{GetReportCount, GetReportCountResponse}, utils::{blocking, get_local_user_view_from_jwt}, }; -use lemmy_db_views::structs::{CommentReportView, PostReportView}; +use lemmy_db_views::structs::{CommentReportView, PostReportView, PrivateMessageReportView}; use lemmy_utils::{error::LemmyError, ConnectionId}; use lemmy_websocket::LemmyContext; @@ -36,10 +36,22 @@ impl Perform for GetReportCount { }) .await??; + let private_message_reports = if admin && community_id.is_none() { + Some( + blocking(context.pool(), move |conn| { + PrivateMessageReportView::get_report_count(conn) + }) + .await??, + ) + } else { + None + }; + let res = GetReportCountResponse { community_id, comment_reports, post_reports, + private_message_reports, }; Ok(res) diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs index 28915f01..6843bcd3 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/post_report/create.rs @@ -1,4 +1,4 @@ -use crate::Perform; +use crate::{check_report_reason, Perform}; use activitypub_federation::core::object_id::ObjectId; use actix_web::web::Data; use lemmy_api_common::{ @@ -29,14 +29,8 @@ impl Perform for CreatePostReport { let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - // check size of report and check for whitespace - let reason = data.reason.trim(); - if reason.is_empty() { - return Err(LemmyError::from_message("report_reason_required")); - } - if reason.chars().count() > 1000 { - return Err(LemmyError::from_message("report_too_long")); - } + let reason = self.reason.trim(); + check_report_reason(reason, context)?; let person_id = local_user_view.person.id; let post_id = data.post_id; @@ -53,7 +47,7 @@ impl Perform for CreatePostReport { original_post_name: post_view.post.name, original_post_url: post_view.post.url, original_post_body: post_view.post.body, - reason: data.reason.to_owned(), + reason: reason.to_owned(), }; let report = blocking(context.pool(), move |conn| { diff --git a/crates/api/src/private_message/mark_read.rs b/crates/api/src/private_message/mark_read.rs index fa250601..cdf38b80 100644 --- a/crates/api/src/private_message/mark_read.rs +++ b/crates/api/src/private_message/mark_read.rs @@ -1,7 +1,7 @@ use crate::Perform; use actix_web::web::Data; use lemmy_api_common::{ - person::{MarkPrivateMessageAsRead, PrivateMessageResponse}, + private_message::{MarkPrivateMessageAsRead, PrivateMessageResponse}, utils::{blocking, get_local_user_view_from_jwt}, }; use lemmy_db_schema::{source::private_message::PrivateMessage, traits::Crud}; diff --git a/crates/api/src/private_message_report/create.rs b/crates/api/src/private_message_report/create.rs new file mode 100644 index 00000000..ee78af80 --- /dev/null +++ b/crates/api/src/private_message_report/create.rs @@ -0,0 +1,75 @@ +use crate::{check_report_reason, Perform}; +use actix_web::web::Data; +use lemmy_api_common::{ + private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse}, + utils::{blocking, get_local_user_view_from_jwt}, +}; +use lemmy_db_schema::{ + newtypes::CommunityId, + source::{ + private_message::PrivateMessage, + private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, + }, + traits::{Crud, Reportable}, +}; +use lemmy_db_views::structs::PrivateMessageReportView; +use lemmy_utils::{error::LemmyError, ConnectionId}; +use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for CreatePrivateMessageReport { + type Response = PrivateMessageReportResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let local_user_view = + get_local_user_view_from_jwt(&self.auth, context.pool(), context.secret()).await?; + + let reason = self.reason.trim(); + check_report_reason(reason, context)?; + + let person_id = local_user_view.person.id; + let private_message_id = self.private_message_id; + let private_message = blocking(context.pool(), move |conn| { + PrivateMessage::read(conn, private_message_id) + }) + .await??; + + let report_form = PrivateMessageReportForm { + creator_id: person_id, + private_message_id, + original_pm_text: private_message.content, + reason: reason.to_owned(), + }; + + let report = blocking(context.pool(), move |conn| { + PrivateMessageReport::report(conn, &report_form) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?; + + let private_message_report_view = blocking(context.pool(), move |conn| { + PrivateMessageReportView::read(conn, report.id) + }) + .await??; + + let res = PrivateMessageReportResponse { + private_message_report_view, + }; + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::CreatePrivateMessageReport, + response: res.clone(), + community_id: CommunityId(0), + websocket_id, + }); + + // TODO: consider federating this + + Ok(res) + } +} diff --git a/crates/api/src/private_message_report/list.rs b/crates/api/src/private_message_report/list.rs new file mode 100644 index 00000000..6e530309 --- /dev/null +++ b/crates/api/src/private_message_report/list.rs @@ -0,0 +1,46 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + private_message::{ListPrivateMessageReports, ListPrivateMessageReportsResponse}, + utils::{blocking, get_local_user_view_from_jwt, is_admin}, +}; +use lemmy_db_views::private_message_report_view::PrivateMessageReportQuery; +use lemmy_utils::{error::LemmyError, ConnectionId}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for ListPrivateMessageReports { + type Response = ListPrivateMessageReportsResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let local_user_view = + get_local_user_view_from_jwt(&self.auth, context.pool(), context.secret()).await?; + + is_admin(&local_user_view)?; + + let unresolved_only = self.unresolved_only; + let page = self.page; + let limit = self.limit; + let private_message_reports = blocking(context.pool(), move |conn| { + PrivateMessageReportQuery::builder() + .conn(conn) + .unresolved_only(unresolved_only) + .page(page) + .limit(limit) + .build() + .list() + }) + .await??; + + let res = ListPrivateMessageReportsResponse { + private_message_reports, + }; + + Ok(res) + } +} diff --git a/crates/api/src/private_message_report/mod.rs b/crates/api/src/private_message_report/mod.rs new file mode 100644 index 00000000..375fde4c --- /dev/null +++ b/crates/api/src/private_message_report/mod.rs @@ -0,0 +1,3 @@ +mod create; +mod list; +mod resolve; diff --git a/crates/api/src/private_message_report/resolve.rs b/crates/api/src/private_message_report/resolve.rs new file mode 100644 index 00000000..7b3500b4 --- /dev/null +++ b/crates/api/src/private_message_report/resolve.rs @@ -0,0 +1,64 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + private_message::{PrivateMessageReportResponse, ResolvePrivateMessageReport}, + utils::{blocking, get_local_user_view_from_jwt, is_admin}, +}; +use lemmy_db_schema::{ + newtypes::CommunityId, + source::private_message_report::PrivateMessageReport, + traits::Reportable, +}; +use lemmy_db_views::structs::PrivateMessageReportView; +use lemmy_utils::{error::LemmyError, ConnectionId}; +use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for ResolvePrivateMessageReport { + type Response = PrivateMessageReportResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let local_user_view = + get_local_user_view_from_jwt(&self.auth, context.pool(), context.secret()).await?; + + is_admin(&local_user_view)?; + + let resolved = self.resolved; + let report_id = self.report_id; + let person_id = local_user_view.person.id; + let resolve_fn = move |conn: &'_ _| { + if resolved { + PrivateMessageReport::resolve(conn, report_id, person_id) + } else { + PrivateMessageReport::unresolve(conn, report_id, person_id) + } + }; + + blocking(context.pool(), resolve_fn) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?; + + let private_message_report_view = blocking(context.pool(), move |conn| { + PrivateMessageReportView::read(conn, report_id) + }) + .await??; + + let res = PrivateMessageReportResponse { + private_message_report_view, + }; + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::ResolvePrivateMessageReport, + response: res.clone(), + community_id: CommunityId(0), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 30e38c92..ef907f88 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -2,6 +2,7 @@ pub mod comment; pub mod community; pub mod person; pub mod post; +pub mod private_message; #[cfg(feature = "full")] pub mod request; pub mod sensitive; diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 79e3041d..8c4131dd 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -1,5 +1,10 @@ use crate::sensitive::Sensitive; -use lemmy_db_views::structs::{CommentView, PostView, PrivateMessageView}; +use lemmy_db_schema::{ + newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, + CommentSortType, + SortType, +}; +use lemmy_db_views::structs::{CommentView, PostView}; use lemmy_db_views_actor::structs::{ CommentReplyView, CommunityModeratorView, @@ -13,18 +18,6 @@ pub struct Login { pub username_or_email: Sensitive, pub password: Sensitive, } -use lemmy_db_schema::{ - newtypes::{ - CommentReplyId, - CommunityId, - LanguageId, - PersonId, - PersonMentionId, - PrivateMessageId, - }, - CommentSortType, - SortType, -}; #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct Register { @@ -249,52 +242,6 @@ pub struct PasswordChangeAfterReset { pub password_verify: Sensitive, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct CreatePrivateMessage { - pub content: String, - pub recipient_id: PersonId, - pub auth: Sensitive, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct EditPrivateMessage { - pub private_message_id: PrivateMessageId, - pub content: String, - pub auth: Sensitive, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct DeletePrivateMessage { - pub private_message_id: PrivateMessageId, - pub deleted: bool, - pub auth: Sensitive, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct MarkPrivateMessageAsRead { - pub private_message_id: PrivateMessageId, - pub read: bool, - pub auth: Sensitive, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct GetPrivateMessages { - pub unread_only: Option, - pub page: Option, - pub limit: Option, - pub auth: Sensitive, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct PrivateMessagesResponse { - pub private_messages: Vec, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct PrivateMessageResponse { - pub private_message_view: PrivateMessageView, -} - #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct GetReportCount { pub community_id: Option, @@ -306,6 +253,7 @@ pub struct GetReportCountResponse { pub community_id: Option, pub comment_reports: i64, pub post_reports: i64, + pub private_message_reports: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Default)] diff --git a/crates/api_common/src/private_message.rs b/crates/api_common/src/private_message.rs new file mode 100644 index 00000000..8cf2cb67 --- /dev/null +++ b/crates/api_common/src/private_message.rs @@ -0,0 +1,83 @@ +use crate::sensitive::Sensitive; +use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId, PrivateMessageReportId}; +use lemmy_db_views::structs::{PrivateMessageReportView, PrivateMessageView}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct CreatePrivateMessage { + pub content: String, + pub recipient_id: PersonId, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct EditPrivateMessage { + pub private_message_id: PrivateMessageId, + pub content: String, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct DeletePrivateMessage { + pub private_message_id: PrivateMessageId, + pub deleted: bool, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct MarkPrivateMessageAsRead { + pub private_message_id: PrivateMessageId, + pub read: bool, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct GetPrivateMessages { + pub unread_only: Option, + pub page: Option, + pub limit: Option, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PrivateMessagesResponse { + pub private_messages: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PrivateMessageResponse { + pub private_message_view: PrivateMessageView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct CreatePrivateMessageReport { + pub private_message_id: PrivateMessageId, + pub reason: String, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PrivateMessageReportResponse { + pub private_message_report_view: PrivateMessageReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct ResolvePrivateMessageReport { + pub report_id: PrivateMessageReportId, + pub resolved: bool, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct ListPrivateMessageReports { + pub page: Option, + pub limit: Option, + /// Only shows the unresolved reports + pub unresolved_only: Option, + pub auth: Sensitive, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ListPrivateMessageReportsResponse { + pub private_message_reports: Vec, +} diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs index e29c8fa4..28ad22b8 100644 --- a/crates/api_crud/src/lib.rs +++ b/crates/api_crud/src/lib.rs @@ -1,5 +1,5 @@ use actix_web::{web, web::Data}; -use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*}; +use lemmy_api_common::{comment::*, community::*, person::*, post::*, private_message::*, site::*}; use lemmy_utils::{error::LemmyError, ConnectionId}; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperationCrud}; use serde::Deserialize; diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 56d68c6e..27803131 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -1,7 +1,7 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{ - person::{CreatePrivateMessage, PrivateMessageResponse}, + private_message::{CreatePrivateMessage, PrivateMessageResponse}, utils::{ blocking, check_person_block, diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index ef200f48..e7d67020 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -1,7 +1,7 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{ - person::{DeletePrivateMessage, PrivateMessageResponse}, + private_message::{DeletePrivateMessage, PrivateMessageResponse}, utils::{blocking, get_local_user_view_from_jwt}, }; use lemmy_apub::activities::deletion::send_apub_delete_private_message; diff --git a/crates/api_crud/src/private_message/read.rs b/crates/api_crud/src/private_message/read.rs index ebd9ddda..fbf7621c 100644 --- a/crates/api_crud/src/private_message/read.rs +++ b/crates/api_crud/src/private_message/read.rs @@ -1,7 +1,7 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{ - person::{GetPrivateMessages, PrivateMessagesResponse}, + private_message::{GetPrivateMessages, PrivateMessagesResponse}, utils::{blocking, get_local_user_view_from_jwt}, }; use lemmy_db_schema::traits::DeleteableOrRemoveable; diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 2c4cba5e..9de33a69 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -1,7 +1,7 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{ - person::{EditPrivateMessage, PrivateMessageResponse}, + private_message::{EditPrivateMessage, PrivateMessageResponse}, utils::{blocking, get_local_user_view_from_jwt}, }; use lemmy_apub::protocol::activities::{ diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index bd5df904..43f34182 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -16,6 +16,7 @@ pub mod person_mention; pub mod post; pub mod post_report; pub mod private_message; +pub mod private_message_report; pub mod registration_application; pub mod secret; pub mod site; diff --git a/crates/db_schema/src/impls/private_message_report.rs b/crates/db_schema/src/impls/private_message_report.rs new file mode 100644 index 00000000..45ced6ce --- /dev/null +++ b/crates/db_schema/src/impls/private_message_report.rs @@ -0,0 +1,62 @@ +use crate::{ + newtypes::{PersonId, PrivateMessageReportId}, + source::private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, + traits::Reportable, + utils::naive_now, +}; +use diesel::{dsl::*, result::Error, *}; + +impl Reportable for PrivateMessageReport { + type Form = PrivateMessageReportForm; + type IdType = PrivateMessageReportId; + /// creates a comment report and returns it + /// + /// * `conn` - the postgres connection + /// * `comment_report_form` - the filled CommentReportForm to insert + fn report(conn: &PgConnection, pm_report_form: &PrivateMessageReportForm) -> Result { + use crate::schema::private_message_report::dsl::*; + insert_into(private_message_report) + .values(pm_report_form) + .get_result::(conn) + } + + /// resolve a pm report + /// + /// * `conn` - the postgres connection + /// * `report_id` - the id of the report to resolve + /// * `by_resolver_id` - the id of the user resolving the report + fn resolve( + conn: &PgConnection, + report_id: Self::IdType, + by_resolver_id: PersonId, + ) -> Result { + use crate::schema::private_message_report::dsl::*; + update(private_message_report.find(report_id)) + .set(( + resolved.eq(true), + resolver_id.eq(by_resolver_id), + updated.eq(naive_now()), + )) + .execute(conn) + } + + /// unresolve a comment report + /// + /// * `conn` - the postgres connection + /// * `report_id` - the id of the report to unresolve + /// * `by_resolver_id` - the id of the user unresolving the report + fn unresolve( + conn: &PgConnection, + report_id: Self::IdType, + by_resolver_id: PersonId, + ) -> Result { + use crate::schema::private_message_report::dsl::*; + update(private_message_report.find(report_id)) + .set(( + resolved.eq(false), + resolver_id.eq(by_resolver_id), + updated.eq(naive_now()), + )) + .execute(conn) + } +} diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index e91bfea6..4431103c 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -69,6 +69,10 @@ pub struct CommentReportId(i32); #[cfg_attr(feature = "full", derive(DieselNewType))] pub struct PostReportId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +pub struct PrivateMessageReportId(i32); + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType))] pub struct LanguageId(pub i32); diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 4870ff7c..2d6ff612 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -454,6 +454,20 @@ table! { } } +table! { + private_message_report (id) { + id -> Int4, + creator_id -> Int4, + private_message_id -> Int4, + original_pm_text -> Text, + reason -> Text, + resolved -> Bool, + resolver_id -> Nullable, + published -> Timestamp, + updated -> Nullable, + } +} + table! { site (id) { id -> Int4, @@ -667,9 +681,11 @@ joinable!(person_mention -> person_alias_1 (recipient_id)); joinable!(comment_reply -> person_alias_1 (recipient_id)); joinable!(post -> person_alias_1 (creator_id)); joinable!(comment -> person_alias_1 (creator_id)); +joinable!(private_message_report -> person_alias_1 (resolver_id)); joinable!(post_report -> person_alias_2 (resolver_id)); joinable!(comment_report -> person_alias_2 (resolver_id)); +joinable!(private_message_report -> person_alias_2 (resolver_id)); joinable!(person_block -> person (person_id)); joinable!(person_block -> person_alias_1 (target_id)); @@ -733,6 +749,7 @@ joinable!(post -> language (language_id)); joinable!(comment -> language (language_id)); joinable!(local_user_language -> language (language_id)); joinable!(local_user_language -> local_user (local_user_id)); +joinable!(private_message_report -> private_message (private_message_id)); joinable!(admin_purge_comment -> person (admin_person_id)); joinable!(admin_purge_comment -> post (post_id)); @@ -780,6 +797,7 @@ allow_tables_to_appear_in_same_query!( post_report, post_saved, private_message, + private_message_report, site, site_aggregates, person_alias_1, diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index f142bbae..e7667010 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -17,6 +17,7 @@ pub mod person_mention; pub mod post; pub mod post_report; pub mod private_message; +pub mod private_message_report; pub mod registration_application; pub mod secret; pub mod site; diff --git a/crates/db_schema/src/source/private_message_report.rs b/crates/db_schema/src/source/private_message_report.rs new file mode 100644 index 00000000..ba563aa7 --- /dev/null +++ b/crates/db_schema/src/source/private_message_report.rs @@ -0,0 +1,34 @@ +use crate::newtypes::{PersonId, PrivateMessageId, PrivateMessageReportId}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "full")] +use crate::schema::private_message_report; + +#[derive(PartialEq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))] +#[cfg_attr( + feature = "full", + belongs_to(crate::source::private_message::PrivateMessage) +)] +#[cfg_attr(feature = "full", table_name = "private_message_report")] +pub struct PrivateMessageReport { + pub id: PrivateMessageReportId, + pub creator_id: PersonId, + pub private_message_id: PrivateMessageId, + pub original_pm_text: String, + pub reason: String, + pub resolved: bool, + pub resolver_id: Option, + pub published: chrono::NaiveDateTime, + pub updated: Option, +} + +#[derive(Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", table_name = "private_message_report")] +pub struct PrivateMessageReportForm { + pub creator_id: PersonId, + pub private_message_id: PrivateMessageId, + pub original_pm_text: String, + pub reason: String, +} diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 507d37f7..8eeca692 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -14,6 +14,9 @@ pub mod post_report_view; #[cfg(feature = "full")] pub mod post_view; #[cfg(feature = "full")] +#[cfg(feature = "full")] +pub mod private_message_report_view; +#[cfg(feature = "full")] pub mod private_message_view; #[cfg(feature = "full")] pub mod registration_application_view; diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs new file mode 100644 index 00000000..c7b2f590 --- /dev/null +++ b/crates/db_views/src/private_message_report_view.rs @@ -0,0 +1,223 @@ +use crate::structs::PrivateMessageReportView; +use diesel::{result::Error, *}; +use lemmy_db_schema::{ + newtypes::PrivateMessageReportId, + schema::{person, person_alias_1, person_alias_2, private_message, private_message_report}, + source::{ + person::{Person, PersonAlias1, PersonAlias2, PersonSafe, PersonSafeAlias1, PersonSafeAlias2}, + private_message::PrivateMessage, + private_message_report::PrivateMessageReport, + }, + traits::{ToSafe, ViewToVec}, + utils::limit_and_offset, +}; +use typed_builder::TypedBuilder; + +type PrivateMessageReportViewTuple = ( + PrivateMessageReport, + PrivateMessage, + PersonSafe, + PersonSafeAlias1, + Option, +); + +impl PrivateMessageReportView { + /// returns the PrivateMessageReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub fn read(conn: &PgConnection, report_id: PrivateMessageReportId) -> Result { + let (private_message_report, private_message, private_message_creator, creator, resolver) = + private_message_report::table + .find(report_id) + .inner_join(private_message::table) + .inner_join(person::table.on(private_message::creator_id.eq(person::id))) + .inner_join( + person_alias_1::table.on(private_message_report::creator_id.eq(person_alias_1::id)), + ) + .left_join( + person_alias_2::table + .on(private_message_report::resolver_id.eq(person_alias_2::id.nullable())), + ) + .select(( + private_message_report::all_columns, + private_message::all_columns, + Person::safe_columns_tuple(), + PersonAlias1::safe_columns_tuple(), + PersonAlias2::safe_columns_tuple().nullable(), + )) + .first::(conn)?; + + Ok(Self { + private_message_report, + private_message, + private_message_creator, + creator, + resolver, + }) + } + + /// Returns the current unresolved post report count for the communities you mod + pub fn get_report_count(conn: &PgConnection) -> Result { + use diesel::dsl::*; + + private_message_report::table + .inner_join(private_message::table) + .filter(private_message_report::resolved.eq(false)) + .into_boxed() + .select(count(private_message_report::id)) + .first::(conn) + } +} + +#[derive(TypedBuilder)] +#[builder(field_defaults(default))] +pub struct PrivateMessageReportQuery<'a> { + #[builder(!default)] + conn: &'a PgConnection, + page: Option, + limit: Option, + unresolved_only: Option, +} + +impl<'a> PrivateMessageReportQuery<'a> { + pub fn list(self) -> Result, Error> { + let mut query = private_message_report::table + .inner_join(private_message::table) + .inner_join(person::table.on(private_message::creator_id.eq(person::id))) + .inner_join( + person_alias_1::table.on(private_message_report::creator_id.eq(person_alias_1::id)), + ) + .left_join( + person_alias_2::table + .on(private_message_report::resolver_id.eq(person_alias_2::id.nullable())), + ) + .select(( + private_message_report::all_columns, + private_message::all_columns, + Person::safe_columns_tuple(), + PersonAlias1::safe_columns_tuple(), + PersonAlias2::safe_columns_tuple().nullable(), + )) + .into_boxed(); + + if self.unresolved_only.unwrap_or(true) { + query = query.filter(private_message_report::resolved.eq(false)); + } + + let (limit, offset) = limit_and_offset(self.page, self.limit)?; + + query = query + .order_by(private_message::published.desc()) + .limit(limit) + .offset(offset); + + let res = query.load::(self.conn)?; + + Ok(PrivateMessageReportView::from_tuple_to_vec(res)) + } +} + +impl ViewToVec for PrivateMessageReportView { + type DbTuple = PrivateMessageReportViewTuple; + fn from_tuple_to_vec(items: Vec) -> Vec { + items + .into_iter() + .map(|a| Self { + private_message_report: a.0, + private_message: a.1, + private_message_creator: a.2, + creator: a.3, + resolver: a.4, + }) + .collect::>() + } +} + +#[cfg(test)] +mod tests { + use crate::private_message_report_view::PrivateMessageReportQuery; + use lemmy_db_schema::{ + source::{ + person::{Person, PersonForm}, + private_message::{PrivateMessage, PrivateMessageForm}, + private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, + }, + traits::{Crud, Reportable}, + utils::establish_unpooled_connection, + }; + use serial_test::serial; + + #[test] + #[serial] + fn test_crud() { + let conn = establish_unpooled_connection(); + + let new_person_1 = PersonForm { + name: "timmy_mrv".into(), + public_key: Some("pubkey".to_string()), + ..PersonForm::default() + }; + let inserted_timmy = Person::create(&conn, &new_person_1).unwrap(); + + let new_person_2 = PersonForm { + name: "jessica_mrv".into(), + public_key: Some("pubkey".to_string()), + ..PersonForm::default() + }; + let inserted_jessica = Person::create(&conn, &new_person_2).unwrap(); + + // timmy sends private message to jessica + let pm_form = PrivateMessageForm { + creator_id: inserted_timmy.id, + recipient_id: inserted_jessica.id, + content: "something offensive".to_string(), + ..Default::default() + }; + let pm = PrivateMessage::create(&conn, &pm_form).unwrap(); + + // 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(&conn, &pm_report_form).unwrap(); + + let reports = PrivateMessageReportQuery::builder() + .conn(&conn) + .build() + .list() + .unwrap(); + assert_eq!(1, reports.len()); + 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 = PersonForm { + name: "admin_mrv".into(), + public_key: Some("pubkey".to_string()), + ..PersonForm::default() + }; + let inserted_admin = Person::create(&conn, &new_person_3).unwrap(); + + // admin resolves the report (after taking appropriate action) + PrivateMessageReport::resolve(&conn, pm_report.id, inserted_admin.id).unwrap(); + + let reports = PrivateMessageReportQuery::builder() + .conn(&conn) + .unresolved_only(Some(false)) + .build() + .list() + .unwrap(); + assert_eq!(1, reports.len()); + assert!(reports[0].private_message_report.resolved); + assert!(reports[0].resolver.is_some()); + assert_eq!( + inserted_admin.name, + reports[0].resolver.as_ref().unwrap().name + ); + } +} diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index a3c11171..1d509a13 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -10,6 +10,7 @@ use lemmy_db_schema::{ post::Post, post_report::PostReport, private_message::PrivateMessage, + private_message_report::PrivateMessageReport, registration_application::RegistrationApplication, site::Site, }, @@ -94,6 +95,15 @@ pub struct PrivateMessageView { pub recipient: PersonSafeAlias1, } +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +pub struct PrivateMessageReportView { + pub private_message_report: PrivateMessageReport, + pub private_message: PrivateMessage, + pub private_message_creator: PersonSafe, + pub creator: PersonSafeAlias1, + pub resolver: Option, +} + #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct RegistrationApplicationView { pub registration_application: RegistrationApplication, diff --git a/crates/websocket/src/lib.rs b/crates/websocket/src/lib.rs index 634e7b4b..7f363b0b 100644 --- a/crates/websocket/src/lib.rs +++ b/crates/websocket/src/lib.rs @@ -134,6 +134,9 @@ pub enum UserOperation { PasswordReset, PasswordChange, MarkPrivateMessageAsRead, + CreatePrivateMessageReport, + ResolvePrivateMessageReport, + ListPrivateMessageReports, UserJoin, PostJoin, CommunityJoin, diff --git a/crates/websocket/src/messages.rs b/crates/websocket/src/messages.rs index 4192b540..21b6ac4b 100644 --- a/crates/websocket/src/messages.rs +++ b/crates/websocket/src/messages.rs @@ -55,6 +55,7 @@ pub struct SendUserRoomMessage { pub websocket_id: Option, } +/// Send message to all users viewing the given community. #[derive(Message)] #[rtype(result = "()")] pub struct SendCommunityRoomMessage { @@ -64,6 +65,7 @@ pub struct SendCommunityRoomMessage { pub websocket_id: Option, } +/// Send message to mods of a given community. Set community_id = 0 to send to site admins. #[derive(Message)] #[rtype(result = "()")] pub struct SendModRoomMessage { diff --git a/crates/websocket/src/send.rs b/crates/websocket/src/send.rs index 83e63819..ce0f7399 100644 --- a/crates/websocket/src/send.rs +++ b/crates/websocket/src/send.rs @@ -6,8 +6,8 @@ use crate::{ use lemmy_api_common::{ comment::CommentResponse, community::CommunityResponse, - person::PrivateMessageResponse, post::PostResponse, + private_message::PrivateMessageResponse, utils::{blocking, check_person_block, get_interface_language, send_email_to_user}, }; use lemmy_db_schema::{ diff --git a/migrations/2022-09-07-114618_pm-reports/down.sql b/migrations/2022-09-07-114618_pm-reports/down.sql new file mode 100644 index 00000000..1db179af --- /dev/null +++ b/migrations/2022-09-07-114618_pm-reports/down.sql @@ -0,0 +1 @@ +drop table private_message_report; diff --git a/migrations/2022-09-07-114618_pm-reports/up.sql b/migrations/2022-09-07-114618_pm-reports/up.sql new file mode 100644 index 00000000..7574f7cf --- /dev/null +++ b/migrations/2022-09-07-114618_pm-reports/up.sql @@ -0,0 +1,12 @@ +create table private_message_report ( + id serial primary key, + creator_id int references person on update cascade on delete cascade not null, -- user reporting comment + private_message_id int references private_message on update cascade on delete cascade not null, -- comment being reported + original_pm_text text not null, + reason text not null, + resolved bool not null default false, + resolver_id int references person on update cascade on delete cascade, -- user resolving report + published timestamp not null default now(), + updated timestamp null, + unique(private_message_id, creator_id) -- users should only be able to report a pm once +); diff --git a/src/api_routes.rs b/src/api_routes.rs index 2e2d3001..00614026 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -1,6 +1,14 @@ use actix_web::*; use lemmy_api::Perform; -use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*, websocket::*}; +use lemmy_api_common::{ + comment::*, + community::*, + person::*, + post::*, + private_message::*, + site::*, + websocket::*, +}; use lemmy_api_crud::PerformCrud; use lemmy_utils::rate_limit::RateLimit; use lemmy_websocket::{routes::chat_route, LemmyContext}; @@ -148,6 +156,18 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route( "/mark_as_read", web::post().to(route_post::), + ) + .route( + "/report", + web::post().to(route_post::), + ) + .route( + "/report/resolve", + web::put().to(route_post::), + ) + .route( + "/report/list", + web::get().to(route_get::), ), ) // User