From 6d43202efb3101f162cc2bf8663f8a6dca160bdf Mon Sep 17 00:00:00 2001
From: eiknat <eiknat@protonmail.com>
Date: Tue, 13 Oct 2020 19:32:35 -0400
Subject: [PATCH] reports: initial reports api commit

---
 Cargo.lock                                    |   3 +
 lemmy_api/src/lib.rs                          |  24 +-
 lemmy_api/src/report.rs                       | 394 ++++++++++++++++++
 lemmy_db/Cargo.toml                           |   3 +-
 lemmy_db/src/comment_report.rs                | 186 +++++++++
 lemmy_db/src/lib.rs                           |  11 +
 lemmy_db/src/post_report.rs                   | 190 +++++++++
 lemmy_db/src/schema.rs                        |  34 ++
 lemmy_structs/Cargo.toml                      |   1 +
 lemmy_structs/src/lib.rs                      |   1 +
 lemmy_structs/src/report.rs                   |  92 ++++
 lemmy_websocket/src/lib.rs                    |   7 +
 .../down.sql                                  |   5 +
 .../up.sql                                    |  51 +++
 src/routes/api.rs                             |  15 +-
 15 files changed, 1011 insertions(+), 6 deletions(-)
 create mode 100644 lemmy_api/src/report.rs
 create mode 100644 lemmy_db/src/comment_report.rs
 create mode 100644 lemmy_db/src/post_report.rs
 create mode 100644 lemmy_structs/src/report.rs
 create mode 100644 migrations/2020-10-13-212240_create_report_tables/down.sql
 create mode 100644 migrations/2020-10-13-212240_create_report_tables/up.sql

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