diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 73ed5c9cc..bb9b063a5 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -306,11 +306,12 @@ Connect to ws://***host***/api/v1/ws to get started. If the ***`host`*** supports secure connections, you can use wss://***host***/api/v1/ws. -To receive websocket messages, you must join a room / context. The three available are: +To receive websocket messages, you must join a room / context. The four available are: - [UserJoin](#user-join). Receives replies, private messages, etc. - [PostJoin](#post-join). Receives new comments on a post. - [CommunityJoin](#community-join). Receives front page / community posts. +- [ModJoin](#mod-join). Receives community moderator updates like reports. #### Testing with Websocat @@ -916,6 +917,35 @@ Marks all user replies and mentions as read. `POST /user/join` +#### Get Report Count + +If a community is supplied, returns the report count for only that community, otherwise returns the report count for all communities the user moderates. + +##### Request +```rust +{ + op: "GetReportCount", + data: { + community: Option, + auth: String + } +} +``` +##### Response +```rust +{ + op: "GetReportCount", + data: { + community: Option, + comment_reports: i64, + post_reports: i64, + } +} +``` +##### HTTP + +`GET /user/report_count` + ### Site #### List Categories ##### Request @@ -1492,6 +1522,29 @@ The main / frontpage community is `community_id: 0`. `POST /community/join` +#### Mod Join +##### Request +```rust +{ + op: "ModJoin", + data: { + community_id: i32 + } +} +``` +##### Response +```rust +{ + op: "ModJoin", + data: { + joined: bool, + } +} +``` +##### HTTP + +`POST /community/mod/join` + ### Post #### Create Post ##### Request @@ -1801,6 +1854,86 @@ Only admins and mods can sticky a post. `POST /post/join` +#### Create Post Report +##### Request +```rust +{ + op: "CreatePostReport", + data: { + post_id: i32, + reason: String, + auth: String + } +} +``` +##### Response +```rust +{ + op: "CreatePostReport", + data: { + success: bool + } +} +``` +##### HTTP + +`POST /post/report` + +#### Resolve Post Report +##### Request +```rust +{ + op: "ResolvePostReport", + data: { + report_id: i32, + resolved: bool, + auth: String + } +} +``` +##### Response +```rust +{ + op: "ResolvePostReport", + data: { + report_id: i32, + resolved: bool + } +} +``` +##### HTTP + +`PUT /post/report/resolve` + +#### List Post Reports + +If a community is supplied, returns reports for only that community, otherwise returns the reports for all communities the user moderates + +##### Request +```rust +{ + op: "ListPostReports", + data: { + page: Option, + limit: Option, + community: Option, + auth: String + } +} +``` +##### Response +```rust +{ + op: "ListPostReports", + data: { + posts: Vec + } +} +``` +##### HTTP + +`GET /post/report/list` + ### Comment #### Create Comment ##### Request @@ -2032,6 +2165,86 @@ Only the recipient can do this. `POST /comment/like` +#### Create Comment Report +##### Request +```rust +{ + op: "CreateCommentReport", + data: { + comment_id: i32, + reason: String, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "CreateCommentReport", + data: { + success: bool, + } +} +``` +##### HTTP + +`POST /comment/report` + +#### Resolve Comment Report +##### Request +```rust +{ + op: "ResolveCommentReport", + data: { + report_id: i32, + resolved: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "ResolveCommentReport", + data: { + report_id: i32, + resolved: bool, + } +} +``` +##### HTTP + +`PUT /comment/report/resolve` + +#### List Comment Reports + +If a community is supplied, returns reports for only that community, otherwise returns the reports for all communities the user moderates + +##### Request +```rust +{ + op: "ListCommentReports", + data: { + page: Option, + limit: Option, + community: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "ListCommentReports", + data: { + comments: Vec + } +} +``` +##### HTTP + +`GET /comment/report/list` + ### RSS / Atom feeds #### All diff --git a/lemmy_api/src/comment.rs b/lemmy_api/src/comment.rs index 5a78ba914..b1107d0dc 100644 --- a/lemmy_api/src/comment.rs +++ b/lemmy_api/src/comment.rs @@ -1,5 +1,6 @@ use crate::{ check_community_ban, + collect_moderated_communities, get_post, get_user_from_jwt, get_user_from_jwt_opt, @@ -10,6 +11,7 @@ use actix_web::web::Data; use lemmy_apub::{ApubLikeableType, ApubObjectType}; use lemmy_db::{ comment::*, + comment_report::*, comment_view::*, moderator::*, post::*, @@ -18,6 +20,7 @@ use lemmy_db::{ Crud, Likeable, ListingType, + Reportable, Saveable, SortType, }; @@ -29,7 +32,11 @@ use lemmy_utils::{ ConnectionId, LemmyError, }; -use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation}; +use lemmy_websocket::{ + messages::{SendComment, SendModRoomMessage, SendUserRoomMessage}, + LemmyContext, + UserOperation, +}; use std::str::FromStr; #[async_trait::async_trait(?Send)] @@ -682,3 +689,165 @@ impl Perform for GetComments { Ok(GetCommentsResponse { comments }) } } + +/// Creates a comment report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for CreateCommentReport { + type Response = CreateCommentReportResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + 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 = data.reason.trim(); + if reason.is_empty() { + return Err(APIError::err("report_reason_required").into()); + } + if reason.len() > 1000 { + return Err(APIError::err("report_too_long").into()); + } + + let user_id = user.id; + let comment_id = data.comment_id; + let comment = blocking(context.pool(), move |conn| { + CommentView::read(&conn, comment_id, None) + }) + .await??; + + check_community_ban(user_id, comment.community_id, context.pool()).await?; + + let report_form = CommentReportForm { + creator_id: user_id, + comment_id, + original_comment_text: comment.content, + reason: data.reason.to_owned(), + }; + + let report = match blocking(context.pool(), move |conn| { + CommentReport::report(conn, &report_form) + }) + .await? + { + Ok(report) => report, + Err(_e) => return Err(APIError::err("couldnt_create_report").into()), + }; + + let res = CreateCommentReportResponse { success: true }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::CreateCommentReport, + response: res.clone(), + recipient_id: user.id, + websocket_id, + }); + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::CreateCommentReport, + response: report, + community_id: comment.community_id, + websocket_id, + }); + + Ok(res) + } +} + +/// Resolves or unresolves a comment report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for ResolveCommentReport { + type Response = ResolveCommentReportResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &ResolveCommentReport = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + let report_id = data.report_id; + let report = blocking(context.pool(), move |conn| { + CommentReportView::read(&conn, report_id) + }) + .await??; + + let user_id = user.id; + is_mod_or_admin(context.pool(), user_id, report.community_id).await?; + + let resolved = data.resolved; + let resolve_fun = move |conn: &'_ _| { + if resolved { + CommentReport::resolve(conn, report_id.clone(), user_id) + } else { + CommentReport::unresolve(conn, report_id.clone(), user_id) + } + }; + + if blocking(context.pool(), resolve_fun).await?.is_err() { + return Err(APIError::err("couldnt_resolve_report").into()); + }; + + let report_id = data.report_id; + let res = ResolveCommentReportResponse { + report_id, + resolved, + }; + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::ResolveCommentReport, + response: res.clone(), + community_id: report.community_id, + websocket_id, + }); + + Ok(res) + } +} + +/// Lists comment reports for a community if an id is supplied +/// or returns all comment reports for communities a user moderates +#[async_trait::async_trait(?Send)] +impl Perform for ListCommentReports { + type Response = ListCommentReportsResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &ListCommentReports = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + let user_id = user.id; + let community_id = data.community; + let community_ids = + collect_moderated_communities(user_id, community_id, context.pool()).await?; + + let page = data.page; + let limit = data.limit; + let comments = blocking(context.pool(), move |conn| { + CommentReportQueryBuilder::create(conn) + .community_ids(community_ids) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let res = ListCommentReportsResponse { comments }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::ListCommentReports, + response: res.clone(), + recipient_id: user.id, + websocket_id, + }); + + Ok(res) + } +} diff --git a/lemmy_api/src/community.rs b/lemmy_api/src/community.rs index a69f2ce97..762420202 100644 --- a/lemmy_api/src/community.rs +++ b/lemmy_api/src/community.rs @@ -36,7 +36,7 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::{ - messages::{GetCommunityUsersOnline, JoinCommunityRoom, SendCommunityRoomMessage}, + messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage}, LemmyContext, UserOperation, }; @@ -883,3 +883,25 @@ impl Perform for CommunityJoin { Ok(CommunityJoinResponse { joined: true }) } } + +#[async_trait::async_trait(?Send)] +impl Perform for ModJoin { + type Response = ModJoinResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &ModJoin = &self; + + if let Some(ws_id) = websocket_id { + context.chat_server().do_send(JoinModRoom { + community_id: data.community_id, + id: ws_id, + }); + } + + Ok(ModJoinResponse { joined: true }) + } +} diff --git a/lemmy_api/src/lib.rs b/lemmy_api/src/lib.rs index dd9377822..06b629c77 100644 --- a/lemmy_api/src/lib.rs +++ b/lemmy_api/src/lib.rs @@ -1,7 +1,7 @@ use crate::claims::Claims; use actix_web::{web, web::Data}; use lemmy_db::{ - community::Community, + community::{Community, CommunityModerator}, community_view::CommunityUserBanView, post::Post, user::User_, @@ -100,6 +100,31 @@ pub(crate) async fn check_community_ban( } } +/// Returns a list of communities that the user moderates +/// or if a community_id is supplied validates the user is a moderator +/// of that community and returns the community id in a vec +/// +/// * `user_id` - the user id of the moderator +/// * `community_id` - optional community id to check for moderator privileges +/// * `pool` - the diesel db pool +pub(crate) async fn collect_moderated_communities( + user_id: i32, + community_id: Option, + pool: &DbPool, +) -> Result, LemmyError> { + if let Some(community_id) = community_id { + // if the user provides a community_id, just check for mod/admin privileges + is_mod_or_admin(pool, user_id, community_id).await?; + Ok(vec![community_id]) + } else { + let ids = blocking(pool, move |conn: &'_ _| { + CommunityModerator::get_user_moderated_communities(conn, user_id) + }) + .await??; + Ok(ids) + } +} + pub(crate) fn check_optional_url(item: &Option>) -> Result<(), LemmyError> { if let Some(Some(item)) = &item { if Url::parse(item).is_err() { @@ -178,9 +203,13 @@ pub async fn match_websocket_operation( UserOperation::CommunityJoin => { do_websocket_operation::(context, id, op, data).await } + UserOperation::ModJoin => do_websocket_operation::(context, id, op, data).await, UserOperation::SaveUserSettings => { do_websocket_operation::(context, id, op, data).await } + UserOperation::GetReportCount => { + do_websocket_operation::(context, id, op, data).await + } // Private Message ops UserOperation::CreatePrivateMessage => { @@ -266,6 +295,15 @@ pub async fn match_websocket_operation( do_websocket_operation::(context, id, op, data).await } UserOperation::SavePost => do_websocket_operation::(context, id, op, data).await, + UserOperation::CreatePostReport => { + do_websocket_operation::(context, id, op, data).await + } + UserOperation::ListPostReports => { + do_websocket_operation::(context, id, op, data).await + } + UserOperation::ResolvePostReport => { + do_websocket_operation::(context, id, op, data).await + } // Comment ops UserOperation::CreateComment => { @@ -292,6 +330,15 @@ pub async fn match_websocket_operation( UserOperation::CreateCommentLike => { do_websocket_operation::(context, id, op, data).await } + UserOperation::CreateCommentReport => { + do_websocket_operation::(context, id, op, data).await + } + UserOperation::ListCommentReports => { + do_websocket_operation::(context, id, op, data).await + } + UserOperation::ResolveCommentReport => { + do_websocket_operation::(context, id, op, data).await + } } } diff --git a/lemmy_api/src/post.rs b/lemmy_api/src/post.rs index 755b98af2..707c83359 100644 --- a/lemmy_api/src/post.rs +++ b/lemmy_api/src/post.rs @@ -1,6 +1,7 @@ use crate::{ check_community_ban, check_optional_url, + collect_moderated_communities, get_user_from_jwt, get_user_from_jwt_opt, is_mod_or_admin, @@ -14,11 +15,13 @@ use lemmy_db::{ moderator::*, naive_now, post::*, + post_report::*, post_view::*, site_view::*, Crud, Likeable, ListingType, + Reportable, Saveable, SortType, }; @@ -32,7 +35,7 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::{ - messages::{GetPostUsersOnline, JoinPostRoom, SendPost}, + messages::{GetPostUsersOnline, JoinPostRoom, SendModRoomMessage, SendPost, SendUserRoomMessage}, LemmyContext, UserOperation, }; @@ -741,3 +744,166 @@ impl Perform for PostJoin { Ok(PostJoinResponse { joined: true }) } } + +/// Creates a post report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for CreatePostReport { + type Response = CreatePostReportResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + 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 = data.reason.trim(); + if reason.is_empty() { + return Err(APIError::err("report_reason_required").into()); + } + if reason.len() > 1000 { + return Err(APIError::err("report_too_long").into()); + } + + let user_id = user.id; + let post_id = data.post_id; + let post = blocking(context.pool(), move |conn| { + PostView::read(&conn, post_id, None) + }) + .await??; + + check_community_ban(user_id, post.community_id, context.pool()).await?; + + let report_form = PostReportForm { + creator_id: user_id, + post_id, + original_post_name: post.name, + original_post_url: post.url, + original_post_body: post.body, + reason: data.reason.to_owned(), + }; + + let report = match blocking(context.pool(), move |conn| { + PostReport::report(conn, &report_form) + }) + .await? + { + Ok(report) => report, + Err(_e) => return Err(APIError::err("couldnt_create_report").into()), + }; + + let res = CreatePostReportResponse { success: true }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::CreatePostReport, + response: res.clone(), + recipient_id: user.id, + websocket_id, + }); + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::CreatePostReport, + response: report, + community_id: post.community_id, + websocket_id, + }); + + Ok(res) + } +} + +/// Resolves or unresolves a post report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for ResolvePostReport { + type Response = ResolvePostReportResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &ResolvePostReport = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + let report_id = data.report_id; + let report = blocking(context.pool(), move |conn| { + PostReportView::read(&conn, report_id) + }) + .await??; + + let user_id = user.id; + is_mod_or_admin(context.pool(), user_id, report.community_id).await?; + + let resolved = data.resolved; + let resolve_fun = move |conn: &'_ _| { + if resolved { + PostReport::resolve(conn, report_id.clone(), user_id) + } else { + PostReport::unresolve(conn, report_id.clone(), user_id) + } + }; + + let res = ResolvePostReportResponse { + report_id, + resolved: true, + }; + + if blocking(context.pool(), resolve_fun).await?.is_err() { + return Err(APIError::err("couldnt_resolve_report").into()); + }; + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::ResolvePostReport, + response: res.clone(), + community_id: report.community_id, + websocket_id, + }); + + Ok(res) + } +} + +/// Lists post reports for a community if an id is supplied +/// or returns all post reports for communities a user moderates +#[async_trait::async_trait(?Send)] +impl Perform for ListPostReports { + type Response = ListPostReportsResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &ListPostReports = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + let user_id = user.id; + let community_id = data.community; + let community_ids = + collect_moderated_communities(user_id, community_id, context.pool()).await?; + + let page = data.page; + let limit = data.limit; + let posts = blocking(context.pool(), move |conn| { + PostReportQueryBuilder::create(conn) + .community_ids(community_ids) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let res = ListPostReportsResponse { posts }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::ListPostReports, + response: res.clone(), + recipient_id: user.id, + websocket_id, + }); + + Ok(res) + } +} diff --git a/lemmy_api/src/user.rs b/lemmy_api/src/user.rs index 4828888ff..3b0b6d3fe 100644 --- a/lemmy_api/src/user.rs +++ b/lemmy_api/src/user.rs @@ -2,6 +2,7 @@ use crate::{ captcha_espeak_wav_base64, check_optional_url, claims::Claims, + collect_moderated_communities, get_user_from_jwt, get_user_from_jwt_opt, is_admin, @@ -15,6 +16,7 @@ use chrono::Duration; use lemmy_apub::ApubObjectType; use lemmy_db::{ comment::*, + comment_report::CommentReportView, comment_view::*, community::*, community_view::*, @@ -23,6 +25,7 @@ use lemmy_db::{ naive_now, password_reset_request::*, post::*, + post_report::PostReportView, post_view::*, private_message::*, private_message_view::*, @@ -1294,3 +1297,59 @@ impl Perform for UserJoin { Ok(UserJoinResponse { joined: true }) } } + +#[async_trait::async_trait(?Send)] +impl Perform for GetReportCount { + type Response = GetReportCountResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &GetReportCount = &self; + let user = get_user_from_jwt(&data.auth, context.pool()).await?; + + let user_id = user.id; + let community_id = data.community; + let community_ids = + collect_moderated_communities(user_id, community_id, context.pool()).await?; + + let res = { + if community_ids.is_empty() { + GetReportCountResponse { + community: None, + comment_reports: 0, + post_reports: 0, + } + } else { + let ids = community_ids.clone(); + let comment_reports = blocking(context.pool(), move |conn| { + CommentReportView::get_report_count(conn, &ids) + }) + .await??; + + let ids = community_ids.clone(); + let post_reports = blocking(context.pool(), move |conn| { + PostReportView::get_report_count(conn, &ids) + }) + .await??; + + GetReportCountResponse { + community: data.community, + comment_reports, + post_reports, + } + } + }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::GetReportCount, + response: res.clone(), + recipient_id: user.id, + websocket_id, + }); + + Ok(res) + } +} diff --git a/lemmy_db/src/comment_report.rs b/lemmy_db/src/comment_report.rs new file mode 100644 index 000000000..a243891eb --- /dev/null +++ b/lemmy_db/src/comment_report.rs @@ -0,0 +1,235 @@ +use diesel::{dsl::*, pg::Pg, result::Error, *}; +use serde::{Deserialize, Serialize}; + +use crate::{ + comment::Comment, + limit_and_offset, + naive_now, + schema::comment_report, + MaybeOptional, + Reportable, +}; + +table! { + comment_report_view (id) { + id -> Int4, + creator_id -> Int4, + comment_id -> Int4, + original_comment_text -> Text, + reason -> Text, + resolved -> Bool, + resolver_id -> Nullable, + published -> Timestamp, + updated -> Nullable, + post_id -> Int4, + current_comment_text -> Text, + community_id -> Int4, + creator_actor_id -> Text, + creator_name -> Varchar, + creator_preferred_username -> Nullable, + creator_avatar -> Nullable, + creator_local -> Bool, + comment_creator_id -> Int4, + comment_creator_actor_id -> Text, + comment_creator_name -> Varchar, + comment_creator_preferred_username -> Nullable, + comment_creator_avatar -> Nullable, + comment_creator_local -> Bool, + resolver_actor_id -> Nullable, + resolver_name -> Nullable, + resolver_preferred_username -> Nullable, + resolver_avatar -> Nullable, + resolver_local -> Nullable, + } +} + +#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Serialize)] +#[belongs_to(Comment)] +#[table_name = "comment_report"] +pub struct CommentReport { + pub id: i32, + pub creator_id: i32, + pub comment_id: i32, + pub original_comment_text: String, + pub reason: String, + pub resolved: bool, + pub resolver_id: Option, + pub published: chrono::NaiveDateTime, + pub updated: Option, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name = "comment_report"] +pub struct CommentReportForm { + pub creator_id: i32, + pub comment_id: i32, + pub original_comment_text: String, + pub reason: String, +} + +impl Reportable for CommentReport { + /// creates a comment report and returns it + /// + /// * `conn` - the postgres connection + /// * `comment_report_form` - the filled CommentReportForm to insert + fn report(conn: &PgConnection, comment_report_form: &CommentReportForm) -> Result { + use crate::schema::comment_report::dsl::*; + insert_into(comment_report) + .values(comment_report_form) + .get_result::(conn) + } + + /// resolve a comment 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: i32, by_resolver_id: i32) -> Result { + use crate::schema::comment_report::dsl::*; + update(comment_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: i32, by_resolver_id: i32) -> Result { + use crate::schema::comment_report::dsl::*; + update(comment_report.find(report_id)) + .set(( + resolved.eq(false), + resolver_id.eq(by_resolver_id), + updated.eq(naive_now()), + )) + .execute(conn) + } +} + +#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone)] +#[table_name = "comment_report_view"] +pub struct CommentReportView { + pub id: i32, + pub creator_id: i32, + pub comment_id: i32, + pub original_comment_text: String, + pub reason: String, + pub resolved: bool, + pub resolver_id: Option, + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub post_id: i32, + pub current_comment_text: String, + pub community_id: i32, + pub creator_actor_id: String, + pub creator_name: String, + pub creator_preferred_username: Option, + pub creator_avatar: Option, + pub creator_local: bool, + pub comment_creator_id: i32, + pub comment_creator_actor_id: String, + pub comment_creator_name: String, + pub comment_creator_preferred_username: Option, + pub comment_creator_avatar: Option, + pub comment_creator_local: bool, + pub resolver_actor_id: Option, + pub resolver_name: Option, + pub resolver_preferred_username: Option, + pub resolver_avatar: Option, + pub resolver_local: Option, +} + +pub struct CommentReportQueryBuilder<'a> { + conn: &'a PgConnection, + query: comment_report_view::BoxedQuery<'a, Pg>, + for_community_ids: Option>, + page: Option, + limit: Option, + resolved: Option, +} + +impl CommentReportView { + /// returns the CommentReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub fn read(conn: &PgConnection, report_id: i32) -> Result { + use super::comment_report::comment_report_view::dsl::*; + comment_report_view.find(report_id).first::(conn) + } + + /// returns the current unresolved comment report count for the supplied community ids + /// + /// * `community_ids` - a Vec of community_ids to get a count for + pub fn get_report_count(conn: &PgConnection, community_ids: &[i32]) -> Result { + use super::comment_report::comment_report_view::dsl::*; + comment_report_view + .filter(resolved.eq(false).and(community_id.eq_any(community_ids))) + .select(count(id)) + .first::(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_ids: None, + page: None, + limit: None, + resolved: Some(false), + } + } + + pub fn community_ids>>(mut self, community_ids: T) -> Self { + self.for_community_ids = community_ids.get_optional(); + self + } + + pub fn page>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn resolved>(mut self, resolved: T) -> Self { + self.resolved = resolved.get_optional(); + self + } + + pub fn list(self) -> Result, Error> { + use super::comment_report::comment_report_view::dsl::*; + + let mut query = self.query; + + if let Some(comm_ids) = self.for_community_ids { + query = query.filter(community_id.eq_any(comm_ids)); + } + + 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(published.asc()) + .limit(limit) + .offset(offset) + .load::(self.conn) + } +} diff --git a/lemmy_db/src/community.rs b/lemmy_db/src/community.rs index 768babe97..5f76d5143 100644 --- a/lemmy_db/src/community.rs +++ b/lemmy_db/src/community.rs @@ -224,6 +224,17 @@ impl CommunityModerator { use crate::schema::community_moderator::dsl::*; diesel::delete(community_moderator.filter(community_id.eq(for_community_id))).execute(conn) } + + pub fn get_user_moderated_communities( + conn: &PgConnection, + for_user_id: i32, + ) -> Result, Error> { + use crate::schema::community_moderator::dsl::*; + community_moderator + .filter(user_id.eq(for_user_id)) + .select(community_id) + .load::(conn) + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] diff --git a/lemmy_db/src/lib.rs b/lemmy_db/src/lib.rs index dd11008b7..608632061 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; @@ -110,6 +112,18 @@ pub trait Readable { Self: Sized; } +pub trait Reportable { + fn report(conn: &PgConnection, form: &T) -> Result + where + Self: Sized; + fn resolve(conn: &PgConnection, report_id: i32, resolver_id: i32) -> Result + where + Self: Sized; + fn unresolve(conn: &PgConnection, report_id: i32, resolver_id: i32) -> Result + where + Self: Sized; +} + pub trait MaybeOptional { fn get_optional(self) -> Option; } diff --git a/lemmy_db/src/post_report.rs b/lemmy_db/src/post_report.rs new file mode 100644 index 000000000..5f8aa5ea5 --- /dev/null +++ b/lemmy_db/src/post_report.rs @@ -0,0 +1,245 @@ +use diesel::{dsl::*, pg::Pg, result::Error, *}; +use serde::{Deserialize, Serialize}; + +use crate::{ + limit_and_offset, + naive_now, + post::Post, + schema::post_report, + MaybeOptional, + Reportable, +}; + +table! { + post_report_view (id) { + id -> Int4, + creator_id -> Int4, + post_id -> Int4, + original_post_name -> Varchar, + original_post_url -> Nullable, + original_post_body -> Nullable, + reason -> Text, + resolved -> Bool, + resolver_id -> Nullable, + published -> Timestamp, + updated -> Nullable, + current_post_name -> Varchar, + current_post_url -> Nullable, + current_post_body -> Nullable, + community_id -> Int4, + creator_actor_id -> Text, + creator_name -> Varchar, + creator_preferred_username -> Nullable, + creator_avatar -> Nullable, + creator_local -> Bool, + post_creator_id -> Int4, + post_creator_actor_id -> Text, + post_creator_name -> Varchar, + post_creator_preferred_username -> Nullable, + post_creator_avatar -> Nullable, + post_creator_local -> Bool, + resolver_actor_id -> Nullable, + resolver_name -> Nullable, + resolver_preferred_username -> Nullable, + resolver_avatar -> Nullable, + resolver_local -> Nullable, + } +} + +#[derive(Identifiable, Queryable, Associations, PartialEq, Serialize, Deserialize, Debug)] +#[belongs_to(Post)] +#[table_name = "post_report"] +pub struct PostReport { + pub id: i32, + pub creator_id: i32, + pub post_id: i32, + pub original_post_name: String, + pub original_post_url: Option, + pub original_post_body: Option, + pub reason: String, + pub resolved: bool, + pub resolver_id: Option, + pub published: chrono::NaiveDateTime, + pub updated: Option, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name = "post_report"] +pub struct PostReportForm { + pub creator_id: i32, + pub post_id: i32, + pub original_post_name: String, + pub original_post_url: Option, + pub original_post_body: Option, + pub reason: String, +} + +impl Reportable for PostReport { + /// creates a post report and returns it + /// + /// * `conn` - the postgres connection + /// * `post_report_form` - the filled CommentReportForm to insert + fn report(conn: &PgConnection, post_report_form: &PostReportForm) -> Result { + use crate::schema::post_report::dsl::*; + insert_into(post_report) + .values(post_report_form) + .get_result::(conn) + } + + /// resolve a post 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: i32, by_resolver_id: i32) -> Result { + use crate::schema::post_report::dsl::*; + update(post_report.find(report_id)) + .set(( + resolved.eq(true), + resolver_id.eq(by_resolver_id), + updated.eq(naive_now()), + )) + .execute(conn) + } + + /// resolve a post 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: i32, by_resolver_id: i32) -> Result { + use crate::schema::post_report::dsl::*; + update(post_report.find(report_id)) + .set(( + resolved.eq(false), + resolver_id.eq(by_resolver_id), + updated.eq(naive_now()), + )) + .execute(conn) + } +} + +#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone)] +#[table_name = "post_report_view"] +pub struct PostReportView { + pub id: i32, + pub creator_id: i32, + pub post_id: i32, + pub original_post_name: String, + pub original_post_url: Option, + pub original_post_body: Option, + pub reason: String, + pub resolved: bool, + pub resolver_id: Option, + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub current_post_name: String, + pub current_post_url: Option, + pub current_post_body: Option, + pub community_id: i32, + pub creator_actor_id: String, + pub creator_name: String, + pub creator_preferred_username: Option, + pub creator_avatar: Option, + pub creator_local: bool, + pub post_creator_id: i32, + pub post_creator_actor_id: String, + pub post_creator_name: String, + pub post_creator_preferred_username: Option, + pub post_creator_avatar: Option, + pub post_creator_local: bool, + pub resolver_actor_id: Option, + pub resolver_name: Option, + pub resolver_preferred_username: Option, + pub resolver_avatar: Option, + pub resolver_local: Option, +} + +impl PostReportView { + /// returns the PostReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub fn read(conn: &PgConnection, report_id: i32) -> Result { + use super::post_report::post_report_view::dsl::*; + post_report_view.find(report_id).first::(conn) + } + + /// returns the current unresolved post report count for the supplied community ids + /// + /// * `community_ids` - a Vec of community_ids to get a count for + pub fn get_report_count(conn: &PgConnection, community_ids: &[i32]) -> Result { + use super::post_report::post_report_view::dsl::*; + post_report_view + .filter(resolved.eq(false).and(community_id.eq_any(community_ids))) + .select(count(id)) + .first::(conn) + } +} + +pub struct PostReportQueryBuilder<'a> { + conn: &'a PgConnection, + query: post_report_view::BoxedQuery<'a, Pg>, + for_community_ids: Option>, + page: Option, + limit: Option, + resolved: Option, +} + +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_ids: None, + page: None, + limit: None, + resolved: Some(false), + } + } + + pub fn community_ids>>(mut self, community_ids: T) -> Self { + self.for_community_ids = community_ids.get_optional(); + self + } + + pub fn page>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn resolved>(mut self, resolved: T) -> Self { + self.resolved = resolved.get_optional(); + self + } + + pub fn list(self) -> Result, Error> { + use super::post_report::post_report_view::dsl::*; + + let mut query = self.query; + + if let Some(comm_ids) = self.for_community_ids { + query = query.filter(community_id.eq_any(comm_ids)); + } + + 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(published.asc()) + .limit(limit) + .offset(offset) + .load::(self.conn) + } +} diff --git a/lemmy_db/src/schema.rs b/lemmy_db/src/schema.rs index 65838b1a8..400c87d42 100644 --- a/lemmy_db/src/schema.rs +++ b/lemmy_db/src/schema.rs @@ -81,6 +81,20 @@ table! { } } +table! { + comment_report (id) { + id -> Int4, + creator_id -> Int4, + comment_id -> Int4, + original_comment_text -> Text, + reason -> Text, + resolved -> Bool, + resolver_id -> Nullable, + published -> Timestamp, + updated -> Nullable, + } +} + table! { comment_saved (id) { id -> Int4, @@ -370,6 +384,22 @@ table! { } } +table! { + post_report (id) { + id -> Int4, + creator_id -> Int4, + post_id -> Int4, + original_post_name -> Varchar, + original_post_url -> Nullable, + original_post_body -> Nullable, + reason -> Text, + resolved -> Bool, + resolver_id -> Nullable, + published -> Timestamp, + updated -> Nullable, + } +} + table! { post_saved (id) { id -> Int4, @@ -487,6 +517,7 @@ 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_saved -> comment (comment_id)); joinable!(comment_saved -> user_ (user_id)); joinable!(community -> category (category_id)); @@ -516,6 +547,7 @@ 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_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/src/comment.rs b/lemmy_structs/src/comment.rs index 4c18a3dec..6479124f8 100644 --- a/lemmy_structs/src/comment.rs +++ b/lemmy_structs/src/comment.rs @@ -1,4 +1,4 @@ -use lemmy_db::comment_view::CommentView; +use lemmy_db::{comment_report::CommentReportView, comment_view::CommentView}; use serde::{Deserialize, Serialize}; #[derive(Deserialize)] @@ -76,3 +76,42 @@ pub struct GetComments { pub struct GetCommentsResponse { pub comments: Vec, } + +#[derive(Serialize, Deserialize)] +pub struct CreateCommentReport { + pub comment_id: i32, + pub reason: String, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct CreateCommentReportResponse { + pub success: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ResolveCommentReport { + pub report_id: i32, + pub resolved: bool, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ResolveCommentReportResponse { + pub report_id: i32, + pub resolved: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ListCommentReports { + pub page: Option, + pub limit: Option, + /// if no community is given, it returns reports for all communities moderated by the auth user + pub community: Option, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ListCommentReportsResponse { + pub comments: Vec, +} diff --git a/lemmy_structs/src/community.rs b/lemmy_structs/src/community.rs index 6c543eac2..3535c05a9 100644 --- a/lemmy_structs/src/community.rs +++ b/lemmy_structs/src/community.rs @@ -139,3 +139,13 @@ pub struct CommunityJoin { pub struct CommunityJoinResponse { pub joined: bool, } + +#[derive(Deserialize, Debug)] +pub struct ModJoin { + pub community_id: i32, +} + +#[derive(Serialize, Clone)] +pub struct ModJoinResponse { + pub joined: bool, +} diff --git a/lemmy_structs/src/post.rs b/lemmy_structs/src/post.rs index 1ccbe7e32..331c2dca4 100644 --- a/lemmy_structs/src/post.rs +++ b/lemmy_structs/src/post.rs @@ -1,6 +1,7 @@ use lemmy_db::{ comment_view::CommentView, community_view::{CommunityModeratorView, CommunityView}, + post_report::PostReportView, post_view::PostView, }; use serde::{Deserialize, Serialize}; @@ -113,3 +114,41 @@ pub struct PostJoin { pub struct PostJoinResponse { pub joined: bool, } + +#[derive(Serialize, Deserialize)] +pub struct CreatePostReport { + pub post_id: i32, + pub reason: String, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct CreatePostReportResponse { + pub success: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ResolvePostReport { + pub report_id: i32, + pub resolved: bool, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ResolvePostReportResponse { + pub report_id: i32, + pub resolved: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ListPostReports { + pub page: Option, + pub limit: Option, + pub community: Option, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ListPostReportsResponse { + pub posts: Vec, +} diff --git a/lemmy_structs/src/user.rs b/lemmy_structs/src/user.rs index 8e4ca5bd0..bf4a36286 100644 --- a/lemmy_structs/src/user.rs +++ b/lemmy_structs/src/user.rs @@ -237,3 +237,16 @@ pub struct UserJoin { pub struct UserJoinResponse { pub joined: bool, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetReportCount { + pub community: Option, + pub auth: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct GetReportCountResponse { + pub community: Option, + pub comment_reports: i64, + pub post_reports: i64, +} diff --git a/lemmy_websocket/src/chat_server.rs b/lemmy_websocket/src/chat_server.rs index 8346a32f6..0be54c33f 100644 --- a/lemmy_websocket/src/chat_server.rs +++ b/lemmy_websocket/src/chat_server.rs @@ -47,6 +47,8 @@ pub struct ChatServer { /// A map from community to set of connectionIDs pub community_rooms: HashMap>, + pub mod_rooms: HashMap>, + /// A map from user id to its connection ID for joined users. Remember a user can have multiple /// sessions (IE clients) pub(super) user_rooms: HashMap>, @@ -90,6 +92,7 @@ impl ChatServer { sessions: HashMap::new(), post_rooms: HashMap::new(), community_rooms: HashMap::new(), + mod_rooms: HashMap::new(), user_rooms: HashMap::new(), rng: rand::thread_rng(), pool, @@ -130,6 +133,29 @@ impl ChatServer { Ok(()) } + pub fn join_mod_room( + &mut self, + community_id: CommunityId, + id: ConnectionId, + ) -> Result<(), LemmyError> { + // remove session from all rooms + for sessions in self.mod_rooms.values_mut() { + sessions.remove(&id); + } + + // If the room doesn't exist yet + if self.mod_rooms.get_mut(&community_id).is_none() { + self.mod_rooms.insert(community_id, HashSet::new()); + } + + self + .mod_rooms + .get_mut(&community_id) + .context(location_info!())? + .insert(id); + Ok(()) + } + pub fn join_post_room(&mut self, post_id: PostId, id: ConnectionId) -> Result<(), LemmyError> { // remove session from all rooms for sessions in self.post_rooms.values_mut() { @@ -227,6 +253,30 @@ impl ChatServer { Ok(()) } + pub fn send_mod_room_message( + &self, + op: &UserOperation, + response: &Response, + community_id: CommunityId, + websocket_id: Option, + ) -> Result<(), LemmyError> + where + Response: Serialize, + { + let res_str = &serialize_websocket_message(op, response)?; + if let Some(sessions) = self.mod_rooms.get(&community_id) { + for id in sessions { + if let Some(my_id) = websocket_id { + if *id == my_id { + continue; + } + } + self.sendit(res_str, *id); + } + } + Ok(()) + } + pub fn send_all_message( &self, op: &UserOperation, diff --git a/lemmy_websocket/src/handlers.rs b/lemmy_websocket/src/handlers.rs index 258098d62..d95dfd57f 100644 --- a/lemmy_websocket/src/handlers.rs +++ b/lemmy_websocket/src/handlers.rs @@ -120,6 +120,19 @@ where } } +impl Handler> for ChatServer +where + Response: Serialize, +{ + type Result = (); + + fn handle(&mut self, msg: SendModRoomMessage, _: &mut Context) { + self + .send_mod_room_message(&msg.op, &msg.response, msg.community_id, msg.websocket_id) + .ok(); + } +} + impl Handler for ChatServer { type Result = (); @@ -154,6 +167,14 @@ impl Handler for ChatServer { } } +impl Handler for ChatServer { + type Result = (); + + fn handle(&mut self, msg: JoinModRoom, _: &mut Context) { + self.join_mod_room(msg.community_id, msg.id).ok(); + } +} + impl Handler for ChatServer { type Result = (); diff --git a/lemmy_websocket/src/lib.rs b/lemmy_websocket/src/lib.rs index 26b00a061..d789efdd1 100644 --- a/lemmy_websocket/src/lib.rs +++ b/lemmy_websocket/src/lib.rs @@ -97,6 +97,9 @@ pub enum UserOperation { MarkCommentAsRead, SaveComment, CreateCommentLike, + CreateCommentReport, + ResolveCommentReport, + ListCommentReports, GetPosts, CreatePostLike, EditPost, @@ -105,6 +108,10 @@ pub enum UserOperation { LockPost, StickyPost, SavePost, + CreatePostReport, + ResolvePostReport, + ListPostReports, + GetReportCount, EditCommunity, DeleteCommunity, RemoveCommunity, @@ -141,4 +148,5 @@ pub enum UserOperation { SaveSiteConfig, PostJoin, CommunityJoin, + ModJoin, } diff --git a/lemmy_websocket/src/messages.rs b/lemmy_websocket/src/messages.rs index d9f8320a8..c678a96ef 100644 --- a/lemmy_websocket/src/messages.rs +++ b/lemmy_websocket/src/messages.rs @@ -63,6 +63,15 @@ pub struct SendCommunityRoomMessage { pub websocket_id: Option, } +#[derive(Message)] +#[rtype(result = "()")] +pub struct SendModRoomMessage { + pub op: UserOperation, + pub response: Response, + pub community_id: CommunityId, + pub websocket_id: Option, +} + #[derive(Message)] #[rtype(result = "()")] pub struct SendPost { @@ -93,6 +102,13 @@ pub struct JoinCommunityRoom { pub id: ConnectionId, } +#[derive(Message)] +#[rtype(result = "()")] +pub struct JoinModRoom { + pub community_id: CommunityId, + pub id: ConnectionId, +} + #[derive(Message)] #[rtype(result = "()")] pub struct JoinPostRoom { 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 000000000..e1c39faaf --- /dev/null +++ b/migrations/2020-10-13-212240_create_report_tables/down.sql @@ -0,0 +1,4 @@ +drop view comment_report_view; +drop view post_report_view; +drop table comment_report; +drop table post_report; 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 000000000..e9dce1adb --- /dev/null +++ b/migrations/2020-10-13-212240_create_report_tables/up.sql @@ -0,0 +1,89 @@ +create table comment_report ( + id serial primary key, + creator_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 + original_comment_text text not null, + reason text not null, + resolved bool not null default false, + resolver_id int references user_ on update cascade on delete cascade, -- user resolving report + published timestamp not null default now(), + updated timestamp null, + unique(comment_id, creator_id) -- users should only be able to report a comment once +); + +create table post_report ( + id serial primary key, + creator_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 + original_post_name varchar(100) not null, + original_post_url text, + original_post_body text, + reason text not null, + resolved bool not null default false, + resolver_id int references user_ on update cascade on delete cascade, -- user resolving report + published timestamp not null default now(), + updated timestamp null, + unique(post_id, creator_id) -- users should only be able to report a post once +); + +create or replace view comment_report_view as +select cr.*, +c.post_id, +c.content as current_comment_text, +p.community_id, +-- report creator details +f.actor_id as creator_actor_id, +f.name as creator_name, +f.preferred_username as creator_preferred_username, +f.avatar as creator_avatar, +f.local as creator_local, +-- comment creator details +u.id as comment_creator_id, +u.actor_id as comment_creator_actor_id, +u.name as comment_creator_name, +u.preferred_username as comment_creator_preferred_username, +u.avatar as comment_creator_avatar, +u.local as comment_creator_local, +-- resolver details +r.actor_id as resolver_actor_id, +r.name as resolver_name, +r.preferred_username as resolver_preferred_username, +r.avatar as resolver_avatar, +r.local as resolver_local +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.creator_id +left join user_ r on r.id = cr.resolver_id; + +create or replace view post_report_view as +select pr.*, +p.name as current_post_name, +p.url as current_post_url, +p.body as current_post_body, +p.community_id, +-- report creator details +f.actor_id as creator_actor_id, +f.name as creator_name, +f.preferred_username as creator_preferred_username, +f.avatar as creator_avatar, +f.local as creator_local, +-- post creator details +u.id as post_creator_id, +u.actor_id as post_creator_actor_id, +u.name as post_creator_name, +u.preferred_username as post_creator_preferred_username, +u.avatar as post_creator_avatar, +u.local as post_creator_local, +-- resolver details +r.actor_id as resolver_actor_id, +r.name as resolver_name, +r.preferred_username as resolver_preferred_username, +r.avatar as resolver_avatar, +r.local as resolver_local +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.creator_id +left join user_ r on r.id = pr.resolver_id; diff --git a/src/routes/api.rs b/src/routes/api.rs index 7a8ddbf1e..167797d7d 100644 --- a/src/routes/api.rs +++ b/src/routes/api.rs @@ -57,7 +57,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route("/transfer", web::post().to(route_post::)) .route("/ban_user", web::post().to(route_post::)) .route("/mod", web::post().to(route_post::)) - .route("/join", web::post().to(route_post::)), + .route("/join", web::post().to(route_post::)) + .route("/mod/join", web::post().to(route_post::)), ) // Post .service( @@ -79,7 +80,13 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route("/list", web::get().to(route_get::)) .route("/like", web::post().to(route_post::)) .route("/save", web::put().to(route_post::)) - .route("/join", web::post().to(route_post::)), + .route("/join", web::post().to(route_post::)) + .route("/report", web::post().to(route_post::)) + .route( + "/report/resolve", + web::put().to(route_post::), + ) + .route("/report/list", web::get().to(route_get::)), ) // Comment .service( @@ -95,7 +102,16 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { ) .route("/like", web::post().to(route_post::)) .route("/save", web::put().to(route_post::)) - .route("/list", web::get().to(route_get::)), + .route("/list", web::get().to(route_get::)) + .route("/report", web::post().to(route_post::)) + .route( + "/report/resolve", + web::put().to(route_post::), + ) + .route( + "/report/list", + web::get().to(route_get::), + ), ) // Private Message .service( @@ -163,7 +179,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route( "/save_user_settings", web::put().to(route_post::), - ), + ) + .route("/report_count", web::get().to(route_get::)), ) // Admin Actions .service(