From 6d43202efb3101f162cc2bf8663f8a6dca160bdf Mon Sep 17 00:00:00 2001 From: eiknat <eiknat@protonmail.com> Date: Tue, 13 Oct 2020 19:32:35 -0400 Subject: [PATCH] reports: initial reports api commit --- Cargo.lock | 3 + lemmy_api/src/lib.rs | 24 +- lemmy_api/src/report.rs | 394 ++++++++++++++++++ lemmy_db/Cargo.toml | 3 +- lemmy_db/src/comment_report.rs | 186 +++++++++ lemmy_db/src/lib.rs | 11 + lemmy_db/src/post_report.rs | 190 +++++++++ lemmy_db/src/schema.rs | 34 ++ lemmy_structs/Cargo.toml | 1 + lemmy_structs/src/lib.rs | 1 + lemmy_structs/src/report.rs | 92 ++++ lemmy_websocket/src/lib.rs | 7 + .../down.sql | 5 + .../up.sql | 51 +++ src/routes/api.rs | 15 +- 15 files changed, 1011 insertions(+), 6 deletions(-) create mode 100644 lemmy_api/src/report.rs create mode 100644 lemmy_db/src/comment_report.rs create mode 100644 lemmy_db/src/post_report.rs create mode 100644 lemmy_structs/src/report.rs create mode 100644 migrations/2020-10-13-212240_create_report_tables/down.sql create mode 100644 migrations/2020-10-13-212240_create_report_tables/up.sql diff --git a/Cargo.lock b/Cargo.lock index c5f98478..8abe539b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1054,6 +1054,7 @@ dependencies = [ "pq-sys", "r2d2", "serde_json", + "uuid 0.6.5", ] [[package]] @@ -1833,6 +1834,7 @@ dependencies = [ "strum", "strum_macros", "url", + "uuid 0.6.5", ] [[package]] @@ -1898,6 +1900,7 @@ dependencies = [ "log", "serde 1.0.117", "serde_json", + "uuid 0.6.5", ] [[package]] diff --git a/lemmy_api/src/lib.rs b/lemmy_api/src/lib.rs index 0f0367cf..6b35f675 100644 --- a/lemmy_api/src/lib.rs +++ b/lemmy_api/src/lib.rs @@ -8,7 +8,7 @@ use lemmy_db::{ Crud, DbPool, }; -use lemmy_structs::{blocking, comment::*, community::*, post::*, site::*, user::*}; +use lemmy_structs::{blocking, comment::*, community::*, post::*, report::*, site::*, user::*}; use lemmy_utils::{settings::Settings, APIError, ConnectionId, LemmyError}; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; use serde::Deserialize; @@ -19,6 +19,7 @@ pub mod claims; pub mod comment; pub mod community; pub mod post; +pub mod report; pub mod site; pub mod user; pub mod version; @@ -266,6 +267,15 @@ pub async fn match_websocket_operation( do_websocket_operation::<CreatePostLike>(context, id, op, data).await } UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await, + UserOperation::CreatePostReport => { + do_websocket_operation::<CreatePostReport>(context, id, op, data).await + } + UserOperation::ListPostReports => { + do_websocket_operation::<ListPostReports>(context, id, op, data).await + } + UserOperation::ResolvePostReport => { + do_websocket_operation::<ResolvePostReport>(context, id, op, data).await + } // Comment ops UserOperation::CreateComment => { @@ -292,6 +302,18 @@ pub async fn match_websocket_operation( UserOperation::CreateCommentLike => { do_websocket_operation::<CreateCommentLike>(context, id, op, data).await } + UserOperation::CreateCommentReport => { + do_websocket_operation::<CreateCommentReport>(context, id, op, data).await + }, + UserOperation::ListCommentReports => { + do_websocket_operation::<ListCommentReports>(context, id, op, data).await + }, + UserOperation::ResolveCommentReport => { + do_websocket_operation::<ResolveCommentReport>(context, id, op, data).await + } + UserOperation::GetReportCount => { + do_websocket_operation::<GetReportCount>(context, id, op, data).await + } } } diff --git a/lemmy_api/src/report.rs b/lemmy_api/src/report.rs new file mode 100644 index 00000000..f5557500 --- /dev/null +++ b/lemmy_api/src/report.rs @@ -0,0 +1,394 @@ +use actix_web::web::Data; + +use lemmy_db::{ + comment_report::*, + comment_view::*, + community_view::*, + post_report::*, + post_view::*, + Reportable, + user_view::UserView, +}; +use lemmy_structs::{blocking, report::*}; +use lemmy_utils::{APIError, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +use crate::{check_community_ban, get_user_from_jwt, Perform}; + +const MAX_REPORT_LEN: usize = 1000; + +#[async_trait::async_trait(?Send)] +impl Perform for CreateCommentReport { + type Response = CommentReportResponse; + + async fn perform( + &self, + context: &Data<LemmyContext>, + _websocket_id: Option<ConnectionId>, + ) -> Result<CommentReportResponse, LemmyError> { + let data: &CreateCommentReport = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + // Check size of report and check for whitespace + let reason: Option<String> = match data.reason.clone() { + Some(s) if s.trim().is_empty() => None, + Some(s) if s.len() > MAX_REPORT_LEN => { + return Err(APIError::err("report_too_long").into()); + } + Some(s) => Some(s), + None => None, + }; + + // Fetch comment information + let comment_id = data.comment; + let comment = blocking(context.pool(), move |conn| CommentView::read(&conn, comment_id, None)).await??; + + // Check for community ban + check_community_ban(user.id, comment.community_id, context.pool()).await?; + + // Insert the report + let comment_time = match comment.updated { + Some(s) => s, + None => comment.published, + }; + let report_form = CommentReportForm { + time: None, // column defaults to now() in table + reason, + resolved: None, // column defaults to false + user_id: user.id, + comment_id, + comment_text: comment.content, + comment_time, + }; + blocking(context.pool(), move |conn| CommentReport::report(conn, &report_form)).await??; + + Ok(CommentReportResponse { success: true }) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for CreatePostReport { + type Response = PostReportResponse; + + async fn perform( + &self, + context: &Data<LemmyContext>, + _websocket_id: Option<ConnectionId>, + ) -> Result<PostReportResponse, LemmyError> { + let data: &CreatePostReport = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + // Check size of report and check for whitespace + let reason: Option<String> = match data.reason.clone() { + Some(s) if s.trim().is_empty() => None, + Some(s) if s.len() > MAX_REPORT_LEN => { + return Err(APIError::err("report_too_long").into()); + } + Some(s) => Some(s), + None => None, + }; + + // Fetch post information from the database + let post_id = data.post; + let post = blocking(context.pool(), move |conn| PostView::read(&conn, post_id, None)).await??; + + // Check for community ban + check_community_ban(user.id, post.community_id, context.pool()).await?; + + // Insert the report + let post_time = match post.updated { + Some(s) => s, + None => post.published, + }; + let report_form = PostReportForm { + time: None, // column defaults to now() in table + reason, + resolved: None, // column defaults to false + user_id: user.id, + post_id, + post_name: post.name, + post_url: post.url, + post_body: post.body, + post_time, + }; + blocking(context.pool(), move |conn| PostReport::report(conn, &report_form)).await??; + + Ok(PostReportResponse { success: true }) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for GetReportCount { + type Response = GetReportCountResponse; + + async fn perform( + &self, + context: &Data<LemmyContext>, + _websocket_id: Option<ConnectionId>, + ) -> Result<GetReportCountResponse, LemmyError> { + let data: &GetReportCount = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + let community_id = data.community; + //Check community exists. + let community_id = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, None) + }) + .await?? + .id; + + // Check community ban + check_community_ban(user.id, data.community, context.pool()).await?; + + let mut mod_ids: Vec<i32> = Vec::new(); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect()) + }) + .await??, + ); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) + }) + .await??, + ); + if !mod_ids.contains(&user.id) { + return Err(APIError::err("report_view_not_allowed").into()); + } + + let comment_reports = blocking(context.pool(), move |conn| { + CommentReportQueryBuilder::create(conn) + .community_id(community_id) + .resolved(false) + .count() + }) + .await??; + let post_reports = blocking(context.pool(), move |conn| { + PostReportQueryBuilder::create(conn) + .community_id(community_id) + .resolved(false) + .count() + }) + .await??; + + let response = GetReportCountResponse { + community: community_id, + comment_reports, + post_reports, + }; + + Ok(response) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for ListCommentReports { + type Response = ListCommentReportResponse; + + async fn perform( + &self, + context: &Data<LemmyContext>, + _websocket_id: Option<ConnectionId>, + ) -> Result<ListCommentReportResponse, LemmyError> { + let data: &ListCommentReports = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + let community_id = data.community; + //Check community exists. + let community_id = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, None) + }) + .await?? + .id; + + check_community_ban(user.id, data.community, context.pool()).await?; + + let mut mod_ids: Vec<i32> = Vec::new(); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect()) + }) + .await??, + ); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) + }) + .await??, + ); + if !mod_ids.contains(&user.id) { + return Err(APIError::err("report_view_not_allowed").into()); + } + + let page = data.page; + let limit = data.limit; + let reports = blocking(context.pool(), move |conn| { + CommentReportQueryBuilder::create(conn) + .community_id(community_id) + .page(page) + .limit(limit) + .list() + }) + .await??; + + Ok(ListCommentReportResponse { reports }) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for ListPostReports { + type Response = ListPostReportResponse; + + async fn perform( + &self, + context: &Data<LemmyContext>, + _websocket_id: Option<ConnectionId>, + ) -> Result<ListPostReportResponse, LemmyError> { + let data: &ListPostReports = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + let community_id = data.community; + //Check community exists. + let community_id = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, None) + }) + .await?? + .id; + // Check for community ban + check_community_ban(user.id, data.community, context.pool()).await?; + + let mut mod_ids: Vec<i32> = Vec::new(); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect()) + }) + .await??, + ); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) + }) + .await??, + ); + if !mod_ids.contains(&user.id) { + return Err(APIError::err("report_view_not_allowed").into()); + } + + let page = data.page; + let limit = data.limit; + let reports = blocking(context.pool(), move |conn| { + PostReportQueryBuilder::create(conn) + .community_id(community_id) + .page(page) + .limit(limit) + .list() + }) + .await??; + + Ok(ListPostReportResponse { reports }) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for ResolveCommentReport { + type Response = ResolveCommentReportResponse; + + async fn perform( + &self, + context: &Data<LemmyContext>, + _websocket_id: Option<ConnectionId>, + ) -> Result<ResolveCommentReportResponse, LemmyError> { + let data: &ResolveCommentReport = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + // Fetch the report view + let report_id = data.report; + let report = blocking(context.pool(), move |conn| CommentReportView::read(&conn, &report_id)).await??; + + // Check for community ban + check_community_ban(user.id, report.community_id, context.pool()).await?; + + // Check for mod/admin privileges + let mut mod_ids: Vec<i32> = Vec::new(); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, report.community_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect()) + }) + .await??, + ); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) + }) + .await??, + ); + if !mod_ids.contains(&user.id) { + return Err(APIError::err("resolve_report_not_allowed").into()); + } + + blocking(context.pool(), move |conn| { + CommentReport::resolve(conn, &report_id.clone()) + }) + .await??; + + Ok(ResolveCommentReportResponse { + report: report_id, + resolved: true, + }) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for ResolvePostReport { + type Response = ResolvePostReportResponse; + + async fn perform( + &self, + context: &Data<LemmyContext>, + _websocket_id: Option<ConnectionId>, + ) -> Result<ResolvePostReportResponse, LemmyError> { + let data: &ResolvePostReport = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + // Fetch the report view + let report_id = data.report; + let report = blocking(context.pool(), move |conn| PostReportView::read(&conn, &report_id)).await??; + + // Check for community ban + check_community_ban(user.id, report.community_id, context.pool()).await?; + + // Check for mod/admin privileges + let mut mod_ids: Vec<i32> = Vec::new(); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, report.community_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect()) + }) + .await??, + ); + mod_ids.append( + &mut blocking(context.pool(), move |conn| { + UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) + }) + .await??, + ); + if !mod_ids.contains(&user.id) { + return Err(APIError::err("resolve_report_not_allowed").into()); + } + + blocking(context.pool(), move |conn| { + PostReport::resolve(conn, &report_id.clone()) + }) + .await??; + + Ok(ResolvePostReportResponse { + report: report_id, + resolved: true, + }) + } +} diff --git a/lemmy_db/Cargo.toml b/lemmy_db/Cargo.toml index 904b1693..2a358a3e 100644 --- a/lemmy_db/Cargo.toml +++ b/lemmy_db/Cargo.toml @@ -9,7 +9,7 @@ path = "src/lib.rs" [dependencies] lemmy_utils = { path = "../lemmy_utils" } -diesel = { version = "1.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] } +diesel = { version = "1.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json", "uuid"] } chrono = { version = "0.4", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"]} @@ -21,3 +21,4 @@ bcrypt = "0.8" url = { version = "2.1", features = ["serde"] } lazy_static = "1.3" regex = "1.3" +uuid = { version = "0.6.5", features = ["serde", "v4"] } diff --git a/lemmy_db/src/comment_report.rs b/lemmy_db/src/comment_report.rs new file mode 100644 index 00000000..74d05e8c --- /dev/null +++ b/lemmy_db/src/comment_report.rs @@ -0,0 +1,186 @@ +use diesel::{PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods, insert_into, update}; +use diesel::pg::Pg; +use diesel::result::*; +use serde::{Deserialize, Serialize}; + +use crate::{ + limit_and_offset, + MaybeOptional, + schema::comment_report, + comment::Comment, + Reportable, +}; + +table! { + comment_report_view (id) { + id -> Uuid, + time -> Timestamp, + reason -> Nullable<Text>, + resolved -> Bool, + user_id -> Int4, + comment_id -> Int4, + comment_text -> Text, + comment_time -> Timestamp, + post_id -> Int4, + community_id -> Int4, + user_name -> Varchar, + creator_id -> Int4, + creator_name -> Varchar, + } +} + +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] +#[belongs_to(Comment)] +#[table_name = "comment_report"] +pub struct CommentReport { + pub id: uuid::Uuid, + pub time: chrono::NaiveDateTime, + pub reason: Option<String>, + pub resolved: bool, + pub user_id: i32, + pub comment_id: i32, + pub comment_text: String, + pub comment_time: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name = "comment_report"] +pub struct CommentReportForm { + pub time: Option<chrono::NaiveDateTime>, + pub reason: Option<String>, + pub resolved: Option<bool>, + pub user_id: i32, + pub comment_id: i32, + pub comment_text: String, + pub comment_time: chrono::NaiveDateTime, +} + +impl Reportable<CommentReportForm> for CommentReport { + fn report(conn: &PgConnection, comment_report_form: &CommentReportForm) -> Result<Self, Error> { + use crate::schema::comment_report::dsl::*; + insert_into(comment_report) + .values(comment_report_form) + .get_result::<Self>(conn) + } + + fn resolve(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<usize, Error> { + use crate::schema::comment_report::dsl::*; + update(comment_report.find(report_id)) + .set(resolved.eq(true)) + .execute(conn) + } +} + +#[derive( + Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, +)] +#[table_name = "comment_report_view"] +pub struct CommentReportView { + pub id: uuid::Uuid, + pub time: chrono::NaiveDateTime, + pub reason: Option<String>, + pub resolved: bool, + pub user_id: i32, + pub comment_id: i32, + pub comment_text: String, + pub comment_time: chrono::NaiveDateTime, + pub post_id: i32, + pub community_id: i32, + pub user_name: String, + pub creator_id: i32, + pub creator_name: String, +} + +pub struct CommentReportQueryBuilder<'a> { + conn: &'a PgConnection, + query: comment_report_view::BoxedQuery<'a, Pg>, + for_community_id: Option<i32>, + page: Option<i64>, + limit: Option<i64>, + resolved: Option<bool>, +} + +impl CommentReportView { + pub fn read(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<Self, Error> { + use super::comment_report::comment_report_view::dsl::*; + comment_report_view + .filter(id.eq(report_id)) + .first::<Self>(conn) + } +} + +impl<'a> CommentReportQueryBuilder<'a> { + pub fn create(conn: &'a PgConnection) -> Self { + use super::comment_report::comment_report_view::dsl::*; + + let query = comment_report_view.into_boxed(); + + CommentReportQueryBuilder { + conn, + query, + for_community_id: None, + page: None, + limit: None, + resolved: Some(false), + } + } + + pub fn community_id<T: MaybeOptional<i32>>(mut self, community_id: T) -> Self { + self.for_community_id = community_id.get_optional(); + self + } + + pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn resolved<T: MaybeOptional<bool>>(mut self, resolved: T) -> Self { + self.resolved = resolved.get_optional(); + self + } + + pub fn list(self) -> Result<Vec<CommentReportView>, Error> { + use super::comment_report::comment_report_view::dsl::*; + + let mut query = self.query; + + if let Some(comm_id) = self.for_community_id { + query = query.filter(community_id.eq(comm_id)); + } + + if let Some(resolved_flag) = self.resolved { + query = query.filter(resolved.eq(resolved_flag)); + } + + let (limit, offset) = limit_and_offset(self.page, self.limit); + + query + .order_by(time.desc()) + .limit(limit) + .offset(offset) + .load::<CommentReportView>(self.conn) + } + + pub fn count(self) -> Result<usize, Error> { + use super::comment_report::comment_report_view::dsl::*; + let mut query = self.query; + + if let Some(comm_id) = self.for_community_id { + query = query.filter(community_id.eq(comm_id)); + } + + if let Some(resolved_flag) = self.resolved { + query = query.filter(resolved.eq(resolved_flag)); + } + + query.execute(self.conn) + } +} + + diff --git a/lemmy_db/src/lib.rs b/lemmy_db/src/lib.rs index c17bc025..cf0e68ad 100644 --- a/lemmy_db/src/lib.rs +++ b/lemmy_db/src/lib.rs @@ -14,6 +14,7 @@ use std::{env, env::VarError}; pub mod activity; pub mod category; pub mod comment; +pub mod comment_report; pub mod comment_view; pub mod community; pub mod community_view; @@ -21,6 +22,7 @@ pub mod moderator; pub mod moderator_views; pub mod password_reset_request; pub mod post; +pub mod post_report; pub mod post_view; pub mod private_message; pub mod private_message_view; @@ -109,6 +111,15 @@ pub trait Readable<T> { Self: Sized; } +pub trait Reportable<T> { + fn report(conn: &PgConnection, form: &T) -> Result<Self, Error> + where + Self: Sized; + fn resolve(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<usize, Error> + where + Self: Sized; +} + pub trait MaybeOptional<T> { fn get_optional(self) -> Option<T>; } diff --git a/lemmy_db/src/post_report.rs b/lemmy_db/src/post_report.rs new file mode 100644 index 00000000..4cb5e570 --- /dev/null +++ b/lemmy_db/src/post_report.rs @@ -0,0 +1,190 @@ +use diesel::{PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods, insert_into, update}; +use diesel::pg::Pg; +use diesel::result::*; +use serde::{Deserialize, Serialize}; + +use crate::{ + limit_and_offset, + MaybeOptional, + schema::post_report, + post::Post, + Reportable, +}; + +table! { + post_report_view (id) { + id -> Uuid, + time -> Timestamp, + reason -> Nullable<Text>, + resolved -> Bool, + user_id -> Int4, + post_id -> Int4, + post_name -> Varchar, + post_url -> Nullable<Text>, + post_body -> Nullable<Text>, + post_time -> Timestamp, + community_id -> Int4, + user_name -> Varchar, + creator_id -> Int4, + creator_name -> Varchar, + } +} + +#[derive(Identifiable, Queryable, Associations, PartialEq, Serialize, Deserialize, Debug)] +#[belongs_to(Post)] +#[table_name = "post_report"] +pub struct PostReport { + pub id: uuid::Uuid, + pub time: chrono::NaiveDateTime, + pub reason: Option<String>, + pub resolved: bool, + pub user_id: i32, + pub post_id: i32, + pub post_name: String, + pub post_url: Option<String>, + pub post_body: Option<String>, + pub post_time: chrono::NaiveDateTime, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name = "post_report"] +pub struct PostReportForm { + pub time: Option<chrono::NaiveDateTime>, + pub reason: Option<String>, + pub resolved: Option<bool>, + pub user_id: i32, + pub post_id: i32, + pub post_name: String, + pub post_url: Option<String>, + pub post_body: Option<String>, + pub post_time: chrono::NaiveDateTime, +} + +impl Reportable<PostReportForm> for PostReport { + fn report(conn: &PgConnection, post_report_form: &PostReportForm) -> Result<Self, Error> { + use crate::schema::post_report::dsl::*; + insert_into(post_report) + .values(post_report_form) + .get_result::<Self>(conn) + } + + fn resolve(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<usize, Error> { + use crate::schema::post_report::dsl::*; + update(post_report.find(report_id)) + .set(resolved.eq(true)) + .execute(conn) + } +} + +#[derive( +Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, +)] +#[table_name = "post_report_view"] +pub struct PostReportView { + pub id: uuid::Uuid, + pub time: chrono::NaiveDateTime, + pub reason: Option<String>, + pub resolved: bool, + pub user_id: i32, + pub post_id: i32, + pub post_name: String, + pub post_url: Option<String>, + pub post_body: Option<String>, + pub post_time: chrono::NaiveDateTime, + pub community_id: i32, + pub user_name: String, + pub creator_id: i32, + pub creator_name: String, +} + +impl PostReportView { + pub fn read(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<Self, Error> { + use super::post_report::post_report_view::dsl::*; + post_report_view + .filter(id.eq(report_id)) + .first::<Self>(conn) + } +} + +pub struct PostReportQueryBuilder<'a> { + conn: &'a PgConnection, + query: post_report_view::BoxedQuery<'a, Pg>, + for_community_id: Option<i32>, + page: Option<i64>, + limit: Option<i64>, + resolved: Option<bool>, +} + +impl<'a> PostReportQueryBuilder<'a> { + pub fn create(conn: &'a PgConnection) -> Self { + use super::post_report::post_report_view::dsl::*; + + let query = post_report_view.into_boxed(); + + PostReportQueryBuilder { + conn, + query, + for_community_id: None, + page: None, + limit: None, + resolved: Some(false), + } + } + + pub fn community_id<T: MaybeOptional<i32>>(mut self, community_id: T) -> Self { + self.for_community_id = community_id.get_optional(); + self + } + + pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn resolved<T: MaybeOptional<bool>>(mut self, resolved: T) -> Self { + self.resolved = resolved.get_optional(); + self + } + + pub fn list(self) -> Result<Vec<PostReportView>, Error> { + use super::post_report::post_report_view::dsl::*; + + let mut query = self.query; + + if let Some(comm_id) = self.for_community_id { + query = query.filter(community_id.eq(comm_id)); + } + + if let Some(resolved_flag) = self.resolved { + query = query.filter(resolved.eq(resolved_flag)); + } + + let (limit, offset) = limit_and_offset(self.page, self.limit); + + query + .order_by(time.desc()) + .limit(limit) + .offset(offset) + .load::<PostReportView>(self.conn) + } + + pub fn count(self) -> Result<usize, Error> { + use super::post_report::post_report_view::dsl::*; + let mut query = self.query; + + if let Some(comm_id) = self.for_community_id { + query = query.filter(community_id.eq(comm_id)); + } + + if let Some(resolved_flag) = self.resolved { + query = query.filter(resolved.eq(resolved_flag)); + } + + query.execute(self.conn) + } +} diff --git a/lemmy_db/src/schema.rs b/lemmy_db/src/schema.rs index 65838b1a..71034b7a 100644 --- a/lemmy_db/src/schema.rs +++ b/lemmy_db/src/schema.rs @@ -81,6 +81,19 @@ table! { } } +table! { + comment_report (id) { + id -> Uuid, + time -> Timestamp, + reason -> Nullable<Text>, + resolved -> Bool, + user_id -> Int4, + comment_id -> Int4, + comment_text -> Text, + comment_time -> Timestamp, + } +} + table! { comment_saved (id) { id -> Int4, @@ -370,6 +383,21 @@ table! { } } +table! { + post_report (id) { + id -> Uuid, + time -> Timestamp, + reason -> Nullable<Text>, + resolved -> Bool, + user_id -> Int4, + post_id -> Int4, + post_name -> Varchar, + post_url -> Nullable<Text>, + post_body -> Nullable<Text>, + post_time -> Timestamp, + } +} + table! { post_saved (id) { id -> Int4, @@ -487,6 +515,8 @@ joinable!(comment -> user_ (creator_id)); joinable!(comment_like -> comment (comment_id)); joinable!(comment_like -> post (post_id)); joinable!(comment_like -> user_ (user_id)); +joinable!(comment_report -> comment (comment_id)); +joinable!(comment_report -> user_ (user_id)); joinable!(comment_saved -> comment (comment_id)); joinable!(comment_saved -> user_ (user_id)); joinable!(community -> category (category_id)); @@ -516,6 +546,8 @@ joinable!(post_like -> post (post_id)); joinable!(post_like -> user_ (user_id)); joinable!(post_read -> post (post_id)); joinable!(post_read -> user_ (user_id)); +joinable!(post_report -> post (post_id)); +joinable!(post_report -> user_ (user_id)); joinable!(post_saved -> post (post_id)); joinable!(post_saved -> user_ (user_id)); joinable!(site -> user_ (creator_id)); @@ -529,6 +561,7 @@ allow_tables_to_appear_in_same_query!( comment, comment_aggregates_fast, comment_like, + comment_report, comment_saved, community, community_aggregates_fast, @@ -549,6 +582,7 @@ allow_tables_to_appear_in_same_query!( post_aggregates_fast, post_like, post_read, + post_report, post_saved, private_message, site, diff --git a/lemmy_structs/Cargo.toml b/lemmy_structs/Cargo.toml index 8cf522c3..22fad79e 100644 --- a/lemmy_structs/Cargo.toml +++ b/lemmy_structs/Cargo.toml @@ -17,3 +17,4 @@ diesel = "1.4" actix-web = { version = "3.0" } chrono = { version = "0.4", features = ["serde"] } serde_json = { version = "1.0", features = ["preserve_order"]} +uuid = { version = "0.6.5", features = ["serde", "v4"] } \ No newline at end of file diff --git a/lemmy_structs/src/lib.rs b/lemmy_structs/src/lib.rs index 5d2e4273..1b7ccd21 100644 --- a/lemmy_structs/src/lib.rs +++ b/lemmy_structs/src/lib.rs @@ -1,6 +1,7 @@ pub mod comment; pub mod community; pub mod post; +pub mod report; pub mod site; pub mod user; pub mod websocket; diff --git a/lemmy_structs/src/report.rs b/lemmy_structs/src/report.rs new file mode 100644 index 00000000..ed12e261 --- /dev/null +++ b/lemmy_structs/src/report.rs @@ -0,0 +1,92 @@ +use lemmy_db::{ + comment_report::CommentReportView, + post_report::PostReportView, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct CreateCommentReport { + pub comment: i32, + pub reason: Option<String>, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct CommentReportResponse { + pub success: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct CreatePostReport { + pub post: i32, + pub reason: Option<String>, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct PostReportResponse { + pub success: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ListCommentReports { + pub page: Option<i64>, + pub limit: Option<i64>, + pub community: i32, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ListCommentReportResponse { + pub reports: Vec<CommentReportView>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ListPostReports { + pub page: Option<i64>, + pub limit: Option<i64>, + pub community: i32, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ListPostReportResponse { + pub reports: Vec<PostReportView>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetReportCount { + pub community: i32, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetReportCountResponse { + pub community: i32, + pub comment_reports: usize, + pub post_reports: usize, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ResolveCommentReport { + pub report: uuid::Uuid, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ResolveCommentReportResponse { + pub report: uuid::Uuid, + pub resolved: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ResolvePostReport { + pub report: uuid::Uuid, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ResolvePostReportResponse { + pub report: uuid::Uuid, + pub resolved: bool, +} diff --git a/lemmy_websocket/src/lib.rs b/lemmy_websocket/src/lib.rs index 26b00a06..d5e056dc 100644 --- a/lemmy_websocket/src/lib.rs +++ b/lemmy_websocket/src/lib.rs @@ -97,6 +97,9 @@ pub enum UserOperation { MarkCommentAsRead, SaveComment, CreateCommentLike, + CreateCommentReport, + ListCommentReports, + ResolveCommentReport, GetPosts, CreatePostLike, EditPost, @@ -105,6 +108,9 @@ pub enum UserOperation { LockPost, StickyPost, SavePost, + CreatePostReport, + ListPostReports, + ResolvePostReport, EditCommunity, DeleteCommunity, RemoveCommunity, @@ -115,6 +121,7 @@ pub enum UserOperation { GetUserMentions, MarkUserMentionAsRead, GetModlog, + GetReportCount, BanFromCommunity, AddModToCommunity, CreateSite, diff --git a/migrations/2020-10-13-212240_create_report_tables/down.sql b/migrations/2020-10-13-212240_create_report_tables/down.sql new file mode 100644 index 00000000..436f6dd4 --- /dev/null +++ b/migrations/2020-10-13-212240_create_report_tables/down.sql @@ -0,0 +1,5 @@ +drop view comment_report_view; +drop view post_report_view; +drop table comment_report; +drop table post_report; +drop extension "uuid-ossp"; diff --git a/migrations/2020-10-13-212240_create_report_tables/up.sql b/migrations/2020-10-13-212240_create_report_tables/up.sql new file mode 100644 index 00000000..7f6f5542 --- /dev/null +++ b/migrations/2020-10-13-212240_create_report_tables/up.sql @@ -0,0 +1,51 @@ +create extension "uuid-ossp"; + +create table comment_report ( + id uuid primary key default uuid_generate_v4(), + time timestamp not null default now(), + reason text, + resolved bool not null default false, + user_id int references user_ on update cascade on delete cascade not null, -- user reporting comment + comment_id int references comment on update cascade on delete cascade not null, -- comment being reported + comment_text text not null, + comment_time timestamp not null, + unique(comment_id, user_id) -- users should only be able to report a comment once +); + +create table post_report ( + id uuid primary key default uuid_generate_v4(), + time timestamp not null default now(), + reason text, + resolved bool not null default false, + user_id int references user_ on update cascade on delete cascade not null, -- user reporting post + post_id int references post on update cascade on delete cascade not null, -- post being reported + post_name varchar(100) not null, + post_url text, + post_body text, + post_time timestamp not null, + unique(post_id, user_id) -- users should only be able to report a post once +); + +create or replace view comment_report_view as +select cr.*, +c.post_id, +p.community_id, +f.name as user_name, +u.id as creator_id, +u.name as creator_name +from comment_report cr +left join comment c on c.id = cr.comment_id +left join post p on p.id = c.post_id +left join user_ u on u.id = c.creator_id +left join user_ f on f.id = cr.user_id; + +create or replace view post_report_view as +select pr.*, +p.community_id, +f.name as user_name, +u.id as creator_id, +u.name as creator_name +from post_report pr +left join post p on p.id = pr.post_id +left join user_ u on u.id = p.creator_id +left join user_ f on f.id = pr.user_id; diff --git a/src/routes/api.rs b/src/routes/api.rs index 7a8ddbf1..9cbca652 100644 --- a/src/routes/api.rs +++ b/src/routes/api.rs @@ -1,7 +1,7 @@ use actix_web::{error::ErrorBadRequest, *}; use lemmy_api::Perform; use lemmy_rate_limit::RateLimit; -use lemmy_structs::{comment::*, community::*, post::*, site::*, user::*}; +use lemmy_structs::{comment::*, community::*, post::*, report::*, site::*, user::*}; use lemmy_websocket::LemmyContext; use serde::Deserialize; @@ -57,7 +57,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route("/transfer", web::post().to(route_post::<TransferCommunity>)) .route("/ban_user", web::post().to(route_post::<BanFromCommunity>)) .route("/mod", web::post().to(route_post::<AddModToCommunity>)) - .route("/join", web::post().to(route_post::<CommunityJoin>)), + .route("/join", web::post().to(route_post::<CommunityJoin>)) + .route("/comment_reports",web::get().to(route_get::<ListCommentReports>)) + .route("/post_reports", web::get().to(route_get::<ListPostReports>)) + .route("/reports", web::get().to(route_get::<GetReportCount>)), ) // Post .service( @@ -79,7 +82,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route("/list", web::get().to(route_get::<GetPosts>)) .route("/like", web::post().to(route_post::<CreatePostLike>)) .route("/save", web::put().to(route_post::<SavePost>)) - .route("/join", web::post().to(route_post::<PostJoin>)), + .route("/join", web::post().to(route_post::<PostJoin>)) + .route("/report", web::put().to(route_post::<CreatePostReport>)) + .route("/resolve_report",web::post().to(route_post::<ResolvePostReport>)), ) // Comment .service( @@ -95,7 +100,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { ) .route("/like", web::post().to(route_post::<CreateCommentLike>)) .route("/save", web::put().to(route_post::<SaveComment>)) - .route("/list", web::get().to(route_get::<GetComments>)), + .route("/list", web::get().to(route_get::<GetComments>)) + .route("/report", web::put().to(route_post::<CreateCommentReport>)) + .route("/resolve_report",web::post().to(route_post::<ResolveCommentReport>)), ) // Private Message .service( -- 2.44.1