]> Untitled Git - lemmy.git/commitdiff
Implement reports for private messages (#2433)
authorNutomic <me@nutomic.com>
Mon, 19 Sep 2022 22:58:42 +0000 (22:58 +0000)
committerGitHub <noreply@github.com>
Mon, 19 Sep 2022 22:58:42 +0000 (22:58 +0000)
* 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

32 files changed:
crates/api/src/comment_report/create.rs
crates/api/src/lib.rs
crates/api/src/local_user/report_count.rs
crates/api/src/post_report/create.rs
crates/api/src/private_message/mark_read.rs
crates/api/src/private_message_report/create.rs [new file with mode: 0644]
crates/api/src/private_message_report/list.rs [new file with mode: 0644]
crates/api/src/private_message_report/mod.rs [new file with mode: 0644]
crates/api/src/private_message_report/resolve.rs [new file with mode: 0644]
crates/api_common/src/lib.rs
crates/api_common/src/person.rs
crates/api_common/src/private_message.rs [new file with mode: 0644]
crates/api_crud/src/lib.rs
crates/api_crud/src/private_message/create.rs
crates/api_crud/src/private_message/delete.rs
crates/api_crud/src/private_message/read.rs
crates/api_crud/src/private_message/update.rs
crates/db_schema/src/impls/mod.rs
crates/db_schema/src/impls/private_message_report.rs [new file with mode: 0644]
crates/db_schema/src/newtypes.rs
crates/db_schema/src/schema.rs
crates/db_schema/src/source/mod.rs
crates/db_schema/src/source/private_message_report.rs [new file with mode: 0644]
crates/db_views/src/lib.rs
crates/db_views/src/private_message_report_view.rs [new file with mode: 0644]
crates/db_views/src/structs.rs
crates/websocket/src/lib.rs
crates/websocket/src/messages.rs
crates/websocket/src/send.rs
migrations/2022-09-07-114618_pm-reports/down.sql [new file with mode: 0644]
migrations/2022-09-07-114618_pm-reports/up.sql [new file with mode: 0644]
src/api_routes.rs

index 6a1c2287e154ebf8b40bc1270e48b5f4cf175d34..6d21f066b4e29012cfe93659f8d8e828ea00c727 100644 (file)
@@ -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| {
index 026045dd2a0c7f368f8fcb8f4491f958fda18391..2659535a0ceafa745a990519af379b5d78e8cf42 100644 (file)
@@ -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::<MarkPrivateMessageAsRead>(context, id, op, data).await
     }
+    UserOperation::CreatePrivateMessageReport => {
+      do_websocket_operation::<CreatePrivateMessageReport>(context, id, op, data).await
+    }
+    UserOperation::ResolvePrivateMessageReport => {
+      do_websocket_operation::<ResolvePrivateMessageReport>(context, id, op, data).await
+    }
+    UserOperation::ListPrivateMessageReports => {
+      do_websocket_operation::<ListPrivateMessageReports>(context, id, op, data).await
+    }
 
     // Site ops
     UserOperation::GetModlog => do_websocket_operation::<GetModlog>(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;
index 774b2fbbdaf8f8de09dfa26658a4360e6eb0e1bf..a6556f6c4215cdf8858c898285199fe53df8c1b3 100644 (file)
@@ -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)
index 28915f01ab03315ef25a902b4ca09ae6d2c749e1..6843bcd3b0c053ce1a9cd2dc6cca9c8e79d5e547 100644 (file)
@@ -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| {
index fa25060100da441b6631858fe277f3817f18c021..cdf38b80ea314a9e0eefab8e587824fd5998095e 100644 (file)
@@ -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 (file)
index 0000000..ee78af8
--- /dev/null
@@ -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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    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 (file)
index 0000000..6e53030
--- /dev/null
@@ -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<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    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 (file)
index 0000000..375fde4
--- /dev/null
@@ -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 (file)
index 0000000..7b3500b
--- /dev/null
@@ -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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    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)
+  }
+}
index 30e38c922db01f014e9a8a141a72f04d8ff4be97..ef907f88a1565f1900fd978eb51660dcf6923e9b 100644 (file)
@@ -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;
index 79e3041d8e8c8ab8ca0147b2bb52b6ae0eab442e..8c4131dd64fcfd085c2d10afaaa70203135c5823 100644 (file)
@@ -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<String>,
   pub password: Sensitive<String>,
 }
-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<String>,
 }
 
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct CreatePrivateMessage {
-  pub content: String,
-  pub recipient_id: PersonId,
-  pub auth: Sensitive<String>,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct EditPrivateMessage {
-  pub private_message_id: PrivateMessageId,
-  pub content: String,
-  pub auth: Sensitive<String>,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct DeletePrivateMessage {
-  pub private_message_id: PrivateMessageId,
-  pub deleted: bool,
-  pub auth: Sensitive<String>,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct MarkPrivateMessageAsRead {
-  pub private_message_id: PrivateMessageId,
-  pub read: bool,
-  pub auth: Sensitive<String>,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct GetPrivateMessages {
-  pub unread_only: Option<bool>,
-  pub page: Option<i64>,
-  pub limit: Option<i64>,
-  pub auth: Sensitive<String>,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct PrivateMessagesResponse {
-  pub private_messages: Vec<PrivateMessageView>,
-}
-
-#[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<CommunityId>,
@@ -306,6 +253,7 @@ pub struct GetReportCountResponse {
   pub community_id: Option<CommunityId>,
   pub comment_reports: i64,
   pub post_reports: i64,
+  pub private_message_reports: Option<i64>,
 }
 
 #[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 (file)
index 0000000..8cf2cb6
--- /dev/null
@@ -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<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+pub struct EditPrivateMessage {
+  pub private_message_id: PrivateMessageId,
+  pub content: String,
+  pub auth: Sensitive<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+pub struct DeletePrivateMessage {
+  pub private_message_id: PrivateMessageId,
+  pub deleted: bool,
+  pub auth: Sensitive<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+pub struct MarkPrivateMessageAsRead {
+  pub private_message_id: PrivateMessageId,
+  pub read: bool,
+  pub auth: Sensitive<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+pub struct GetPrivateMessages {
+  pub unread_only: Option<bool>,
+  pub page: Option<i64>,
+  pub limit: Option<i64>,
+  pub auth: Sensitive<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct PrivateMessagesResponse {
+  pub private_messages: Vec<PrivateMessageView>,
+}
+
+#[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<String>,
+}
+
+#[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<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+pub struct ListPrivateMessageReports {
+  pub page: Option<i64>,
+  pub limit: Option<i64>,
+  /// Only shows the unresolved reports
+  pub unresolved_only: Option<bool>,
+  pub auth: Sensitive<String>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct ListPrivateMessageReportsResponse {
+  pub private_message_reports: Vec<PrivateMessageReportView>,
+}
index e29c8fa4f94d8ded43d4d2dbe7dfff06bba830b7..28ad22b8a252d862bb6a74d597f2906fba131e6b 100644 (file)
@@ -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;
index 56d68c6e57d62d8df861c5d2e1f8dfc3d8aa50f2..278031311ed938504c0d1a82731bb687e8e71735 100644 (file)
@@ -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,
index ef200f485ad3cd8b6bc810e0dc71c36ed91f211c..e7d6702022b826201a54e1e5171fb14f65f4b4e7 100644 (file)
@@ -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;
index ebd9dddaa490d53b33728a7ec9188141e46e1b5f..fbf7621c71266900f1fafc4d6e9a9e3cb1c11f1e 100644 (file)
@@ -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;
index 2c4cba5e1672eb41dfcab3efbb86b295df096352..9de33a69ac6ff65d6b89c37937648d4af0f2af72 100644 (file)
@@ -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::{
index bd5df90420eb619c7329276ac97d7ed6df2782c4..43f341824ce060ed3d6409730b5d1d9bc430db78 100644 (file)
@@ -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 (file)
index 0000000..45ced6c
--- /dev/null
@@ -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<Self, Error> {
+    use crate::schema::private_message_report::dsl::*;
+    insert_into(private_message_report)
+      .values(pm_report_form)
+      .get_result::<Self>(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<usize, Error> {
+    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<usize, Error> {
+    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)
+  }
+}
index e91bfea656280abea48e905152cd5afb54d49e17..4431103cced6a6775912b73d6363052553ec294f 100644 (file)
@@ -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);
index 4870ff7c53cc1d221784509b8d18aa220bb770b3..2d6ff6124e1a95d232d261664c733c9a11cfbc57 100644 (file)
@@ -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<Int4>,
+        published -> Timestamp,
+        updated -> Nullable<Timestamp>,
+    }
+}
+
 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,
index f142bbaecc30c2edb6872fcd63577d100af877d9..e7667010455d1a36b7f6d59160c8d85f4e50ff23 100644 (file)
@@ -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 (file)
index 0000000..ba563aa
--- /dev/null
@@ -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<PersonId>,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+}
+
+#[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,
+}
index 507d37f7542e6aabafea52df03d3b40b8cfdfb61..8eeca692e087cfc1db8fb07105110205a543a01c 100644 (file)
@@ -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 (file)
index 0000000..c7b2f59
--- /dev/null
@@ -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<PersonSafeAlias2>,
+);
+
+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<Self, Error> {
+    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::<PrivateMessageReportViewTuple>(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<i64, Error> {
+    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::<i64>(conn)
+  }
+}
+
+#[derive(TypedBuilder)]
+#[builder(field_defaults(default))]
+pub struct PrivateMessageReportQuery<'a> {
+  #[builder(!default)]
+  conn: &'a PgConnection,
+  page: Option<i64>,
+  limit: Option<i64>,
+  unresolved_only: Option<bool>,
+}
+
+impl<'a> PrivateMessageReportQuery<'a> {
+  pub fn list(self) -> Result<Vec<PrivateMessageReportView>, 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::<PrivateMessageReportViewTuple>(self.conn)?;
+
+    Ok(PrivateMessageReportView::from_tuple_to_vec(res))
+  }
+}
+
+impl ViewToVec for PrivateMessageReportView {
+  type DbTuple = PrivateMessageReportViewTuple;
+  fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
+    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::<Vec<Self>>()
+  }
+}
+
+#[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
+    );
+  }
+}
index a3c11171228ef258a3b36bd8cac377964fd529b3..1d509a1399fb7c6bb042fa92c25055e4f84758a6 100644 (file)
@@ -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<PersonSafeAlias2>,
+}
+
 #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
 pub struct RegistrationApplicationView {
   pub registration_application: RegistrationApplication,
index 634e7b4b4ae1789747ac6771f32d07716bd6d3d1..7f363b0b5a4d3fd1c0f8ed27d24fd8b5b6f20969 100644 (file)
@@ -134,6 +134,9 @@ pub enum UserOperation {
   PasswordReset,
   PasswordChange,
   MarkPrivateMessageAsRead,
+  CreatePrivateMessageReport,
+  ResolvePrivateMessageReport,
+  ListPrivateMessageReports,
   UserJoin,
   PostJoin,
   CommunityJoin,
index 4192b540220ec271f19d387037585502be077b6d..21b6ac4b7e256494ca15b94b9762aedfc9fbad52 100644 (file)
@@ -55,6 +55,7 @@ pub struct SendUserRoomMessage<OP: ToString, Response> {
   pub websocket_id: Option<ConnectionId>,
 }
 
+/// Send message to all users viewing the given community.
 #[derive(Message)]
 #[rtype(result = "()")]
 pub struct SendCommunityRoomMessage<OP: ToString, Response> {
@@ -64,6 +65,7 @@ pub struct SendCommunityRoomMessage<OP: ToString, Response> {
   pub websocket_id: Option<ConnectionId>,
 }
 
+/// Send message to mods of a given community. Set community_id = 0 to send to site admins.
 #[derive(Message)]
 #[rtype(result = "()")]
 pub struct SendModRoomMessage<Response> {
index 83e638193f8eaa62be13049480f6b1552b88fabc..ce0f73995545612f59206a922191ba9cd2784ca7 100644 (file)
@@ -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 (file)
index 0000000..1db179a
--- /dev/null
@@ -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 (file)
index 0000000..7574f7c
--- /dev/null
@@ -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
+);
index 2e2d30019ce50e391bf6e3700a9cba7a3af5ab32..006140262f73fc34e197eeae8ae30aa7f31b99e1 100644 (file)
@@ -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::<MarkPrivateMessageAsRead>),
+          )
+          .route(
+            "/report",
+            web::post().to(route_post::<CreatePrivateMessageReport>),
+          )
+          .route(
+            "/report/resolve",
+            web::put().to(route_post::<ResolvePrivateMessageReport>),
+          )
+          .route(
+            "/report/list",
+            web::get().to(route_get::<ListPrivateMessageReports>),
           ),
       )
       // User