]> Untitled Git - lemmy.git/commitdiff
Merge pull request 'Set valid context for our extra fields (ref #1220)' (#142) from...
authordessalines <dessalines@noreply.yerbamate.ml>
Fri, 27 Nov 2020 20:28:52 +0000 (20:28 +0000)
committerdessalines <dessalines@noreply.yerbamate.ml>
Fri, 27 Nov 2020 20:28:52 +0000 (20:28 +0000)
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/142

24 files changed:
Cargo.lock
docs/src/contributing_websocket_http_api.md
lemmy_api/src/comment.rs
lemmy_api/src/community.rs
lemmy_api/src/lib.rs
lemmy_api/src/post.rs
lemmy_api/src/user.rs
lemmy_db/src/comment_report.rs [new file with mode: 0644]
lemmy_db/src/community.rs
lemmy_db/src/lib.rs
lemmy_db/src/post_report.rs [new file with mode: 0644]
lemmy_db/src/schema.rs
lemmy_structs/src/comment.rs
lemmy_structs/src/community.rs
lemmy_structs/src/post.rs
lemmy_structs/src/user.rs
lemmy_utils/Cargo.toml
lemmy_websocket/src/chat_server.rs
lemmy_websocket/src/handlers.rs
lemmy_websocket/src/lib.rs
lemmy_websocket/src/messages.rs
migrations/2020-10-13-212240_create_report_tables/down.sql [new file with mode: 0644]
migrations/2020-10-13-212240_create_report_tables/up.sql [new file with mode: 0644]
src/routes/api.rs

index 6b7469287b787b48be210a2b3442873fc9280e7a..7823e9fc21a3f057d155549df6a229ff7ec61e3a 100644 (file)
@@ -397,15 +397,6 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "ansi_term"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
-dependencies = [
- "winapi 0.3.9",
-]
-
 [[package]]
 name = "anyhow"
 version = "1.0.33"
@@ -777,21 +768,6 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
-[[package]]
-name = "clap"
-version = "2.33.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
-dependencies = [
- "ansi_term",
- "atty",
- "bitflags 1.2.1",
- "strsim 0.8.0",
- "textwrap",
- "unicode-width",
- "vec_map",
-]
-
 [[package]]
 name = "cloudabi"
 version = "0.1.0"
@@ -813,7 +789,6 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d325e4f2ffff52ca77d995bb675494d5364aa332499d5f7c7fbb28c25e671f6"
 dependencies = [
- "clap",
  "entities",
  "lazy_static",
  "pest",
@@ -980,7 +955,7 @@ dependencies = [
  "ident_case",
  "proc-macro2",
  "quote",
- "strsim 0.9.3",
+ "strsim",
  "syn",
 ]
 
@@ -3166,12 +3141,6 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
 
-[[package]]
-name = "strsim"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-
 [[package]]
 name = "strsim"
 version = "0.9.3"
@@ -3230,15 +3199,6 @@ dependencies = [
  "winapi-util",
 ]
 
-[[package]]
-name = "textwrap"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-dependencies = [
- "unicode-width",
-]
-
 [[package]]
 name = "thiserror"
 version = "1.0.21"
@@ -3565,12 +3525,6 @@ version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
 
-[[package]]
-name = "unicode-width"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
-
 [[package]]
 name = "unicode-xid"
 version = "0.2.1"
@@ -3649,12 +3603,6 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
 
-[[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-
 [[package]]
 name = "version_check"
 version = "0.1.5"
index 73ed5c9cc665c0a09773e1c1536da97c8d45a700..bb9b063a559241ea07ca76ab1d7702aa4b122edf 100644 (file)
@@ -306,11 +306,12 @@ Connect to <code>ws://***host***/api/v1/ws</code> to get started.
 
 If the ***`host`*** supports secure connections, you can use <code>wss://***host***/api/v1/ws</code>.
 
-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<i32>,
+    auth: String
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "GetReportCount",
+  data: {
+    community: Option<i32>,
+    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<i64>,
+    limit: Option<i64>,
+    community: Option<i32>,
+    auth: String
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "ListPostReports",
+  data: {
+    posts: Vec<PostReportView>
+  }
+}
+```
+##### 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<i64>,
+    limit: Option<i64>,
+    community: Option<i32>,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "ListCommentReports",
+  data: {
+    comments: Vec<CommentReportView>
+  }
+}
+```
+##### HTTP
+
+`GET /comment/report/list`
+
 ### RSS / Atom feeds
 
 #### All
index 5a78ba91404b16f8c7c06ee4805be0cff4cde1f9..b1107d0dc00c907422f1f3e178595436bcc0336a 100644 (file)
@@ -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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CreateCommentReportResponse, 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 = 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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ResolveCommentReportResponse, LemmyError> {
+    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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ListCommentReportsResponse, LemmyError> {
+    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)
+  }
+}
index a69f2ce9797ceeabc15a708b2068b2c9a1483b95..76242020256e286d211ed180b70ee95fcf2ae4fb 100644 (file)
@@ -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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ModJoinResponse, LemmyError> {
+    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 })
+  }
+}
index dd9377822cc17f3065c00915365dd532d706290d..06b629c772cd78e592c91ba802e67f603d93f46e 100644 (file)
@@ -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<i32>,
+  pool: &DbPool,
+) -> Result<Vec<i32>, 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<Option<String>>) -> 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::<CommunityJoin>(context, id, op, data).await
     }
+    UserOperation::ModJoin => do_websocket_operation::<ModJoin>(context, id, op, data).await,
     UserOperation::SaveUserSettings => {
       do_websocket_operation::<SaveUserSettings>(context, id, op, data).await
     }
+    UserOperation::GetReportCount => {
+      do_websocket_operation::<GetReportCount>(context, id, op, data).await
+    }
 
     // Private Message ops
     UserOperation::CreatePrivateMessage => {
@@ -266,6 +295,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 +330,15 @@ 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
+    }
   }
 }
 
index 755b98af2eaca583218d7d05577a1a178e2e766e..707c833593d937c1a0d7ff7702ed44d8400b96be 100644 (file)
@@ -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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CreatePostReportResponse, 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 = 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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ResolvePostReportResponse, LemmyError> {
+    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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ListPostReportsResponse, LemmyError> {
+    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)
+  }
+}
index 4828888ff377dd80b9da0558dfcdcf713c63e1b6..3b0b6d3fe47f4b9f925642478ab2b371e4ef8da3 100644 (file)
@@ -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<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<GetReportCountResponse, LemmyError> {
+    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 (file)
index 0000000..a243891
--- /dev/null
@@ -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<Int4>,
+      published -> Timestamp,
+      updated -> Nullable<Timestamp>,
+      post_id -> Int4,
+      current_comment_text -> Text,
+      community_id -> Int4,
+      creator_actor_id -> Text,
+      creator_name -> Varchar,
+      creator_preferred_username -> Nullable<Varchar>,
+      creator_avatar -> Nullable<Text>,
+      creator_local -> Bool,
+      comment_creator_id -> Int4,
+      comment_creator_actor_id -> Text,
+      comment_creator_name -> Varchar,
+      comment_creator_preferred_username -> Nullable<Varchar>,
+      comment_creator_avatar -> Nullable<Text>,
+      comment_creator_local -> Bool,
+      resolver_actor_id -> Nullable<Text>,
+      resolver_name -> Nullable<Varchar>,
+      resolver_preferred_username -> Nullable<Varchar>,
+      resolver_avatar -> Nullable<Text>,
+      resolver_local -> Nullable<Bool>,
+    }
+}
+
+#[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<i32>,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+}
+
+#[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<CommentReportForm> 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<Self, Error> {
+    use crate::schema::comment_report::dsl::*;
+    insert_into(comment_report)
+      .values(comment_report_form)
+      .get_result::<Self>(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<usize, Error> {
+    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<usize, Error> {
+    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<i32>,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+  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<String>,
+  pub creator_avatar: Option<String>,
+  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<String>,
+  pub comment_creator_avatar: Option<String>,
+  pub comment_creator_local: bool,
+  pub resolver_actor_id: Option<String>,
+  pub resolver_name: Option<String>,
+  pub resolver_preferred_username: Option<String>,
+  pub resolver_avatar: Option<String>,
+  pub resolver_local: Option<bool>,
+}
+
+pub struct CommentReportQueryBuilder<'a> {
+  conn: &'a PgConnection,
+  query: comment_report_view::BoxedQuery<'a, Pg>,
+  for_community_ids: Option<Vec<i32>>,
+  page: Option<i64>,
+  limit: Option<i64>,
+  resolved: Option<bool>,
+}
+
+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<Self, Error> {
+    use super::comment_report::comment_report_view::dsl::*;
+    comment_report_view.find(report_id).first::<Self>(conn)
+  }
+
+  /// returns the current unresolved comment report count for the supplied community ids
+  ///
+  /// * `community_ids` - a Vec<i32> of community_ids to get a count for
+  pub fn get_report_count(conn: &PgConnection, community_ids: &[i32]) -> Result<i64, Error> {
+    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::<i64>(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<T: MaybeOptional<Vec<i32>>>(mut self, community_ids: T) -> Self {
+    self.for_community_ids = community_ids.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_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::<CommentReportView>(self.conn)
+  }
+}
index 768babe97a4019d5ee85d4ca482c567fdd095474..5f76d5143c01019a015cb51cd109ddca28b8fd9a 100644 (file)
@@ -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<Vec<i32>, Error> {
+    use crate::schema::community_moderator::dsl::*;
+    community_moderator
+      .filter(user_id.eq(for_user_id))
+      .select(community_id)
+      .load::<i32>(conn)
+  }
 }
 
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
index dd11008b7933a530c8049b84ebb63b94536c4a15..608632061340e834e2984a43ffd5630a9771abe0 100644 (file)
@@ -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<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: i32, resolver_id: i32) -> Result<usize, Error>
+  where
+    Self: Sized;
+  fn unresolve(conn: &PgConnection, report_id: i32, resolver_id: i32) -> 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 (file)
index 0000000..5f8aa5e
--- /dev/null
@@ -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<Text>,
+        original_post_body -> Nullable<Text>,
+        reason -> Text,
+        resolved -> Bool,
+        resolver_id -> Nullable<Int4>,
+        published -> Timestamp,
+        updated -> Nullable<Timestamp>,
+        current_post_name -> Varchar,
+        current_post_url -> Nullable<Text>,
+        current_post_body -> Nullable<Text>,
+        community_id -> Int4,
+        creator_actor_id -> Text,
+        creator_name -> Varchar,
+        creator_preferred_username -> Nullable<Varchar>,
+        creator_avatar -> Nullable<Text>,
+        creator_local -> Bool,
+        post_creator_id -> Int4,
+        post_creator_actor_id -> Text,
+        post_creator_name -> Varchar,
+        post_creator_preferred_username -> Nullable<Varchar>,
+        post_creator_avatar -> Nullable<Text>,
+        post_creator_local -> Bool,
+        resolver_actor_id -> Nullable<Text>,
+        resolver_name -> Nullable<Varchar>,
+        resolver_preferred_username -> Nullable<Varchar>,
+        resolver_avatar -> Nullable<Text>,
+        resolver_local -> Nullable<Bool>,
+    }
+}
+
+#[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<String>,
+  pub original_post_body: Option<String>,
+  pub reason: String,
+  pub resolved: bool,
+  pub resolver_id: Option<i32>,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+}
+
+#[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<String>,
+  pub original_post_body: Option<String>,
+  pub reason: String,
+}
+
+impl Reportable<PostReportForm> 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<Self, Error> {
+    use crate::schema::post_report::dsl::*;
+    insert_into(post_report)
+      .values(post_report_form)
+      .get_result::<Self>(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<usize, Error> {
+    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<usize, Error> {
+    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<String>,
+  pub original_post_body: Option<String>,
+  pub reason: String,
+  pub resolved: bool,
+  pub resolver_id: Option<i32>,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+  pub current_post_name: String,
+  pub current_post_url: Option<String>,
+  pub current_post_body: Option<String>,
+  pub community_id: i32,
+  pub creator_actor_id: String,
+  pub creator_name: String,
+  pub creator_preferred_username: Option<String>,
+  pub creator_avatar: Option<String>,
+  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<String>,
+  pub post_creator_avatar: Option<String>,
+  pub post_creator_local: bool,
+  pub resolver_actor_id: Option<String>,
+  pub resolver_name: Option<String>,
+  pub resolver_preferred_username: Option<String>,
+  pub resolver_avatar: Option<String>,
+  pub resolver_local: Option<bool>,
+}
+
+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<Self, Error> {
+    use super::post_report::post_report_view::dsl::*;
+    post_report_view.find(report_id).first::<Self>(conn)
+  }
+
+  /// returns the current unresolved post report count for the supplied community ids
+  ///
+  /// * `community_ids` - a Vec<i32> of community_ids to get a count for
+  pub fn get_report_count(conn: &PgConnection, community_ids: &[i32]) -> Result<i64, Error> {
+    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::<i64>(conn)
+  }
+}
+
+pub struct PostReportQueryBuilder<'a> {
+  conn: &'a PgConnection,
+  query: post_report_view::BoxedQuery<'a, Pg>,
+  for_community_ids: Option<Vec<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_ids: None,
+      page: None,
+      limit: None,
+      resolved: Some(false),
+    }
+  }
+
+  pub fn community_ids<T: MaybeOptional<Vec<i32>>>(mut self, community_ids: T) -> Self {
+    self.for_community_ids = community_ids.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_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::<PostReportView>(self.conn)
+  }
+}
index 65838b1a8c449064b99c1f13e77e1e22d2bb95ff..400c87d420ab3e97ac0f9f8e3f8e54029cb9998f 100644 (file)
@@ -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<Int4>,
+        published -> Timestamp,
+        updated -> Nullable<Timestamp>,
+    }
+}
+
 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<Text>,
+        original_post_body -> Nullable<Text>,
+        reason -> Text,
+        resolved -> Bool,
+        resolver_id -> Nullable<Int4>,
+        published -> Timestamp,
+        updated -> Nullable<Timestamp>,
+    }
+}
+
 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,
index 4c18a3decabb03ee59dd3ff34987f4fc598b2df1..6479124f8eca606ba290e1b6fcd53f0e08c4cd44 100644 (file)
@@ -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<CommentView>,
 }
+
+#[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<i64>,
+  pub limit: Option<i64>,
+  /// if no community is given, it returns reports for all communities moderated by the auth user
+  pub community: Option<i32>,
+  pub auth: String,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct ListCommentReportsResponse {
+  pub comments: Vec<CommentReportView>,
+}
index 6c543eac252f28c017c3fe6342286481d4d19d14..3535c05a9fb029a54126dba188b064ee5dc1c884 100644 (file)
@@ -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,
+}
index 1ccbe7e321603e75afd39425bda4b13277728dc5..331c2dca45e944636d101b65697db5920af8176c 100644 (file)
@@ -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<i64>,
+  pub limit: Option<i64>,
+  pub community: Option<i32>,
+  pub auth: String,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct ListPostReportsResponse {
+  pub posts: Vec<PostReportView>,
+}
index 8e4ca5bd0847a189f83298e87e1bbaf389e5f2bc..bf4a362860557365dedfe8c8f1ec2967ab1371c7 100644 (file)
@@ -237,3 +237,16 @@ pub struct UserJoin {
 pub struct UserJoinResponse {
   pub joined: bool,
 }
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct GetReportCount {
+  pub community: Option<i32>,
+  pub auth: String,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct GetReportCountResponse {
+  pub community: Option<i32>,
+  pub comment_reports: i64,
+  pub post_reports: i64,
+}
index 1d5a6b0d9d46ebb8802d0d5e0744fd7dc6175d52..b0a0f3e6b5430b1c93dd101fd9e93ea49e4b9019 100644 (file)
@@ -19,7 +19,7 @@ percent-encoding = "2.1"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = { version = "1.0", features = ["preserve_order"]}
 thiserror = "1.0"
-comrak = "0.8"
+comrak = { version = "0.8", default-features = false }
 lazy_static = "1.3"
 openssl = "0.10"
 url = { version = "2.1", features = ["serde"] }
index 8346a32f60450ef8f727fe9e8bb9e65972dd2555..0be54c33f8fb2241bf3ac41811aad652195d8699 100644 (file)
@@ -47,6 +47,8 @@ pub struct ChatServer {
   /// A map from community to set of connectionIDs
   pub community_rooms: HashMap<CommunityId, HashSet<ConnectionId>>,
 
+  pub mod_rooms: HashMap<CommunityId, HashSet<ConnectionId>>,
+
   /// 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<UserId, HashSet<ConnectionId>>,
@@ -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<Response>(
+    &self,
+    op: &UserOperation,
+    response: &Response,
+    community_id: CommunityId,
+    websocket_id: Option<ConnectionId>,
+  ) -> 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<Response>(
     &self,
     op: &UserOperation,
index 258098d6276a298b919cb63b8d6139a265067692..d95dfd57fab8d82b5d882badccfbe2970b81c7a2 100644 (file)
@@ -120,6 +120,19 @@ where
   }
 }
 
+impl<Response> Handler<SendModRoomMessage<Response>> for ChatServer
+where
+  Response: Serialize,
+{
+  type Result = ();
+
+  fn handle(&mut self, msg: SendModRoomMessage<Response>, _: &mut Context<Self>) {
+    self
+      .send_mod_room_message(&msg.op, &msg.response, msg.community_id, msg.websocket_id)
+      .ok();
+  }
+}
+
 impl Handler<SendPost> for ChatServer {
   type Result = ();
 
@@ -154,6 +167,14 @@ impl Handler<JoinCommunityRoom> for ChatServer {
   }
 }
 
+impl Handler<JoinModRoom> for ChatServer {
+  type Result = ();
+
+  fn handle(&mut self, msg: JoinModRoom, _: &mut Context<Self>) {
+    self.join_mod_room(msg.community_id, msg.id).ok();
+  }
+}
+
 impl Handler<JoinPostRoom> for ChatServer {
   type Result = ();
 
index 26b00a06158ba6185b5e8f7f63cda21293fd56b2..d789efdd1bfe6c98a9b17b1cb27cfd0d542a7cda 100644 (file)
@@ -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,
 }
index d9f8320a8dcdc028bc4cfd1d57f841f702275ee7..c678a96ef202d0899497e64ea4104d4c57bf63bd 100644 (file)
@@ -63,6 +63,15 @@ pub struct SendCommunityRoomMessage<Response> {
   pub websocket_id: Option<ConnectionId>,
 }
 
+#[derive(Message)]
+#[rtype(result = "()")]
+pub struct SendModRoomMessage<Response> {
+  pub op: UserOperation,
+  pub response: Response,
+  pub community_id: CommunityId,
+  pub websocket_id: Option<ConnectionId>,
+}
+
 #[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 (file)
index 0000000..e1c39fa
--- /dev/null
@@ -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 (file)
index 0000000..e9dce1a
--- /dev/null
@@ -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;
index 7a8ddbf1ec8042516c4abea2195bfef27bcb03e6..167797d7da90e088e6924e5d87bee8cdbcb8d47b 100644 (file)
@@ -57,7 +57,8 @@ 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("/mod/join", web::post().to(route_post::<ModJoin>)),
       )
       // Post
       .service(
@@ -79,7 +80,13 @@ 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::post().to(route_post::<CreatePostReport>))
+          .route(
+            "/report/resolve",
+            web::put().to(route_post::<ResolvePostReport>),
+          )
+          .route("/report/list", web::get().to(route_get::<ListPostReports>)),
       )
       // Comment
       .service(
@@ -95,7 +102,16 @@ 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::post().to(route_post::<CreateCommentReport>))
+          .route(
+            "/report/resolve",
+            web::put().to(route_post::<ResolveCommentReport>),
+          )
+          .route(
+            "/report/list",
+            web::get().to(route_get::<ListCommentReports>),
+          ),
       )
       // 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::<SaveUserSettings>),
-          ),
+          )
+          .route("/report_count", web::get().to(route_get::<GetReportCount>)),
       )
       // Admin Actions
       .service(