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
This commit is contained in:
Nutomic 2022-09-19 22:58:42 +00:00 committed by GitHub
parent 09246a20fb
commit 004efd5d94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 734 additions and 90 deletions

View file

@ -1,4 +1,4 @@
use crate::Perform; use crate::{check_report_reason, Perform};
use activitypub_federation::core::object_id::ObjectId; use activitypub_federation::core::object_id::ObjectId;
use actix_web::web::Data; use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
@ -29,14 +29,8 @@ impl Perform for CreateCommentReport {
let local_user_view = let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
// check size of report and check for whitespace let reason = self.reason.trim();
let reason = data.reason.trim(); check_report_reason(reason, context)?;
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 person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_id = data.comment_id; let comment_id = data.comment_id;
@ -51,7 +45,7 @@ impl Perform for CreateCommentReport {
creator_id: person_id, creator_id: person_id,
comment_id, comment_id,
original_comment_text: comment_view.comment.content, original_comment_text: comment_view.comment.content,
reason: data.reason.to_owned(), reason: reason.to_owned(),
}; };
let report = blocking(context.pool(), move |conn| { let report = blocking(context.pool(), move |conn| {

View file

@ -1,7 +1,15 @@
use actix_web::{web, web::Data}; use actix_web::{web, web::Data};
use captcha::Captcha; use captcha::Captcha;
use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*, websocket::*}; use lemmy_api_common::{
use lemmy_utils::{error::LemmyError, ConnectionId}; 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 lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
use serde::Deserialize; use serde::Deserialize;
@ -12,6 +20,7 @@ mod local_user;
mod post; mod post;
mod post_report; mod post_report;
mod private_message; mod private_message;
mod private_message_report;
mod site; mod site;
mod websocket; mod websocket;
@ -98,6 +107,15 @@ pub async fn match_websocket_operation(
UserOperation::MarkPrivateMessageAsRead => { UserOperation::MarkPrivateMessageAsRead => {
do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await 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 // Site ops
UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await, 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) 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)] #[cfg(test)]
mod tests { mod tests {
use lemmy_api_common::utils::check_validator_time; use lemmy_api_common::utils::check_validator_time;

View file

@ -4,7 +4,7 @@ use lemmy_api_common::{
person::{GetReportCount, GetReportCountResponse}, person::{GetReportCount, GetReportCountResponse},
utils::{blocking, get_local_user_view_from_jwt}, 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_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -36,10 +36,22 @@ impl Perform for GetReportCount {
}) })
.await??; .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 { let res = GetReportCountResponse {
community_id, community_id,
comment_reports, comment_reports,
post_reports, post_reports,
private_message_reports,
}; };
Ok(res) Ok(res)

View file

@ -1,4 +1,4 @@
use crate::Perform; use crate::{check_report_reason, Perform};
use activitypub_federation::core::object_id::ObjectId; use activitypub_federation::core::object_id::ObjectId;
use actix_web::web::Data; use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
@ -29,14 +29,8 @@ impl Perform for CreatePostReport {
let local_user_view = let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
// check size of report and check for whitespace let reason = self.reason.trim();
let reason = data.reason.trim(); check_report_reason(reason, context)?;
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 person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_id = data.post_id; let post_id = data.post_id;
@ -53,7 +47,7 @@ impl Perform for CreatePostReport {
original_post_name: post_view.post.name, original_post_name: post_view.post.name,
original_post_url: post_view.post.url, original_post_url: post_view.post.url,
original_post_body: post_view.post.body, original_post_body: post_view.post.body,
reason: data.reason.to_owned(), reason: reason.to_owned(),
}; };
let report = blocking(context.pool(), move |conn| { let report = blocking(context.pool(), move |conn| {

View file

@ -1,7 +1,7 @@
use crate::Perform; use crate::Perform;
use actix_web::web::Data; use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
person::{MarkPrivateMessageAsRead, PrivateMessageResponse}, private_message::{MarkPrivateMessageAsRead, PrivateMessageResponse},
utils::{blocking, get_local_user_view_from_jwt}, utils::{blocking, get_local_user_view_from_jwt},
}; };
use lemmy_db_schema::{source::private_message::PrivateMessage, traits::Crud}; use lemmy_db_schema::{source::private_message::PrivateMessage, traits::Crud};

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,3 @@
mod create;
mod list;
mod resolve;

View file

@ -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)
}
}

View file

@ -2,6 +2,7 @@ pub mod comment;
pub mod community; pub mod community;
pub mod person; pub mod person;
pub mod post; pub mod post;
pub mod private_message;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod request; pub mod request;
pub mod sensitive; pub mod sensitive;

View file

@ -1,5 +1,10 @@
use crate::sensitive::Sensitive; 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::{ use lemmy_db_views_actor::structs::{
CommentReplyView, CommentReplyView,
CommunityModeratorView, CommunityModeratorView,
@ -13,18 +18,6 @@ pub struct Login {
pub username_or_email: Sensitive<String>, pub username_or_email: Sensitive<String>,
pub password: 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)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Register { pub struct Register {
@ -249,52 +242,6 @@ pub struct PasswordChangeAfterReset {
pub password_verify: Sensitive<String>, 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)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct GetReportCount { pub struct GetReportCount {
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
@ -306,6 +253,7 @@ pub struct GetReportCountResponse {
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
pub comment_reports: i64, pub comment_reports: i64,
pub post_reports: i64, pub post_reports: i64,
pub private_message_reports: Option<i64>,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]

View file

@ -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>,
}

View file

@ -1,5 +1,5 @@
use actix_web::{web, web::Data}; 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_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperationCrud}; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperationCrud};
use serde::Deserialize; use serde::Deserialize;

View file

@ -1,7 +1,7 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
person::{CreatePrivateMessage, PrivateMessageResponse}, private_message::{CreatePrivateMessage, PrivateMessageResponse},
utils::{ utils::{
blocking, blocking,
check_person_block, check_person_block,

View file

@ -1,7 +1,7 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
person::{DeletePrivateMessage, PrivateMessageResponse}, private_message::{DeletePrivateMessage, PrivateMessageResponse},
utils::{blocking, get_local_user_view_from_jwt}, utils::{blocking, get_local_user_view_from_jwt},
}; };
use lemmy_apub::activities::deletion::send_apub_delete_private_message; use lemmy_apub::activities::deletion::send_apub_delete_private_message;

View file

@ -1,7 +1,7 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
person::{GetPrivateMessages, PrivateMessagesResponse}, private_message::{GetPrivateMessages, PrivateMessagesResponse},
utils::{blocking, get_local_user_view_from_jwt}, utils::{blocking, get_local_user_view_from_jwt},
}; };
use lemmy_db_schema::traits::DeleteableOrRemoveable; use lemmy_db_schema::traits::DeleteableOrRemoveable;

View file

@ -1,7 +1,7 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; use actix_web::web::Data;
use lemmy_api_common::{ use lemmy_api_common::{
person::{EditPrivateMessage, PrivateMessageResponse}, private_message::{EditPrivateMessage, PrivateMessageResponse},
utils::{blocking, get_local_user_view_from_jwt}, utils::{blocking, get_local_user_view_from_jwt},
}; };
use lemmy_apub::protocol::activities::{ use lemmy_apub::protocol::activities::{

View file

@ -16,6 +16,7 @@ pub mod person_mention;
pub mod post; pub mod post;
pub mod post_report; pub mod post_report;
pub mod private_message; pub mod private_message;
pub mod private_message_report;
pub mod registration_application; pub mod registration_application;
pub mod secret; pub mod secret;
pub mod site; pub mod site;

View file

@ -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)
}
}

View file

@ -69,6 +69,10 @@ pub struct CommentReportId(i32);
#[cfg_attr(feature = "full", derive(DieselNewType))] #[cfg_attr(feature = "full", derive(DieselNewType))]
pub struct PostReportId(i32); 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)] #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType))] #[cfg_attr(feature = "full", derive(DieselNewType))]
pub struct LanguageId(pub i32); pub struct LanguageId(pub i32);

View 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! { table! {
site (id) { site (id) {
id -> Int4, id -> Int4,
@ -667,9 +681,11 @@ joinable!(person_mention -> person_alias_1 (recipient_id));
joinable!(comment_reply -> person_alias_1 (recipient_id)); joinable!(comment_reply -> person_alias_1 (recipient_id));
joinable!(post -> person_alias_1 (creator_id)); joinable!(post -> person_alias_1 (creator_id));
joinable!(comment -> 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!(post_report -> person_alias_2 (resolver_id));
joinable!(comment_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 (person_id));
joinable!(person_block -> person_alias_1 (target_id)); joinable!(person_block -> person_alias_1 (target_id));
@ -733,6 +749,7 @@ joinable!(post -> language (language_id));
joinable!(comment -> language (language_id)); joinable!(comment -> language (language_id));
joinable!(local_user_language -> language (language_id)); joinable!(local_user_language -> language (language_id));
joinable!(local_user_language -> local_user (local_user_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 -> person (admin_person_id));
joinable!(admin_purge_comment -> post (post_id)); joinable!(admin_purge_comment -> post (post_id));
@ -780,6 +797,7 @@ allow_tables_to_appear_in_same_query!(
post_report, post_report,
post_saved, post_saved,
private_message, private_message,
private_message_report,
site, site,
site_aggregates, site_aggregates,
person_alias_1, person_alias_1,

View file

@ -17,6 +17,7 @@ pub mod person_mention;
pub mod post; pub mod post;
pub mod post_report; pub mod post_report;
pub mod private_message; pub mod private_message;
pub mod private_message_report;
pub mod registration_application; pub mod registration_application;
pub mod secret; pub mod secret;
pub mod site; pub mod site;

View file

@ -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,
}

View file

@ -14,6 +14,9 @@ pub mod post_report_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod post_view; pub mod post_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
#[cfg(feature = "full")]
pub mod private_message_report_view;
#[cfg(feature = "full")]
pub mod private_message_view; pub mod private_message_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod registration_application_view; pub mod registration_application_view;

View file

@ -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
);
}
}

View file

@ -10,6 +10,7 @@ use lemmy_db_schema::{
post::Post, post::Post,
post_report::PostReport, post_report::PostReport,
private_message::PrivateMessage, private_message::PrivateMessage,
private_message_report::PrivateMessageReport,
registration_application::RegistrationApplication, registration_application::RegistrationApplication,
site::Site, site::Site,
}, },
@ -94,6 +95,15 @@ pub struct PrivateMessageView {
pub recipient: PersonSafeAlias1, 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)] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct RegistrationApplicationView { pub struct RegistrationApplicationView {
pub registration_application: RegistrationApplication, pub registration_application: RegistrationApplication,

View file

@ -134,6 +134,9 @@ pub enum UserOperation {
PasswordReset, PasswordReset,
PasswordChange, PasswordChange,
MarkPrivateMessageAsRead, MarkPrivateMessageAsRead,
CreatePrivateMessageReport,
ResolvePrivateMessageReport,
ListPrivateMessageReports,
UserJoin, UserJoin,
PostJoin, PostJoin,
CommunityJoin, CommunityJoin,

View file

@ -55,6 +55,7 @@ pub struct SendUserRoomMessage<OP: ToString, Response> {
pub websocket_id: Option<ConnectionId>, pub websocket_id: Option<ConnectionId>,
} }
/// Send message to all users viewing the given community.
#[derive(Message)] #[derive(Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct SendCommunityRoomMessage<OP: ToString, Response> { pub struct SendCommunityRoomMessage<OP: ToString, Response> {
@ -64,6 +65,7 @@ pub struct SendCommunityRoomMessage<OP: ToString, Response> {
pub websocket_id: Option<ConnectionId>, pub websocket_id: Option<ConnectionId>,
} }
/// Send message to mods of a given community. Set community_id = 0 to send to site admins.
#[derive(Message)] #[derive(Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct SendModRoomMessage<Response> { pub struct SendModRoomMessage<Response> {

View file

@ -6,8 +6,8 @@ use crate::{
use lemmy_api_common::{ use lemmy_api_common::{
comment::CommentResponse, comment::CommentResponse,
community::CommunityResponse, community::CommunityResponse,
person::PrivateMessageResponse,
post::PostResponse, post::PostResponse,
private_message::PrivateMessageResponse,
utils::{blocking, check_person_block, get_interface_language, send_email_to_user}, utils::{blocking, check_person_block, get_interface_language, send_email_to_user},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{

View file

@ -0,0 +1 @@
drop table private_message_report;

View file

@ -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
);

View file

@ -1,6 +1,14 @@
use actix_web::*; use actix_web::*;
use lemmy_api::Perform; 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_api_crud::PerformCrud;
use lemmy_utils::rate_limit::RateLimit; use lemmy_utils::rate_limit::RateLimit;
use lemmy_websocket::{routes::chat_route, LemmyContext}; use lemmy_websocket::{routes::chat_route, LemmyContext};
@ -148,6 +156,18 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route( .route(
"/mark_as_read", "/mark_as_read",
web::post().to(route_post::<MarkPrivateMessageAsRead>), 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 // User