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