]> Untitled Git - lemmy.git/commitdiff
Split apart api files (#2216)
authorNutomic <me@nutomic.com>
Wed, 13 Apr 2022 18:12:25 +0000 (18:12 +0000)
committerGitHub <noreply@github.com>
Wed, 13 Apr 2022 18:12:25 +0000 (18:12 +0000)
76 files changed:
crates/api/src/comment/like.rs [moved from crates/api/src/comment.rs with 53% similarity]
crates/api/src/comment/mark_as_read.rs [new file with mode: 0644]
crates/api/src/comment/mod.rs [new file with mode: 0644]
crates/api/src/comment/save.rs [new file with mode: 0644]
crates/api/src/comment_report.rs [deleted file]
crates/api/src/comment_report/create.rs [new file with mode: 0644]
crates/api/src/comment_report/list.rs [new file with mode: 0644]
crates/api/src/comment_report/mod.rs [new file with mode: 0644]
crates/api/src/comment_report/resolve.rs [new file with mode: 0644]
crates/api/src/community.rs [deleted file]
crates/api/src/community/add_mod.rs [new file with mode: 0644]
crates/api/src/community/ban.rs [new file with mode: 0644]
crates/api/src/community/block.rs [new file with mode: 0644]
crates/api/src/community/follow.rs [new file with mode: 0644]
crates/api/src/community/mod.rs [new file with mode: 0644]
crates/api/src/community/transfer.rs [new file with mode: 0644]
crates/api/src/lib.rs
crates/api/src/local_user.rs [deleted file]
crates/api/src/local_user/add_admin.rs [new file with mode: 0644]
crates/api/src/local_user/ban_person.rs [new file with mode: 0644]
crates/api/src/local_user/block.rs [new file with mode: 0644]
crates/api/src/local_user/change_password.rs [new file with mode: 0644]
crates/api/src/local_user/change_password_after_reset.rs [new file with mode: 0644]
crates/api/src/local_user/get_captcha.rs [new file with mode: 0644]
crates/api/src/local_user/list_banned.rs [new file with mode: 0644]
crates/api/src/local_user/login.rs [new file with mode: 0644]
crates/api/src/local_user/mod.rs [new file with mode: 0644]
crates/api/src/local_user/notifications/list_mentions.rs [new file with mode: 0644]
crates/api/src/local_user/notifications/list_replies.rs [new file with mode: 0644]
crates/api/src/local_user/notifications/mark_all_read.rs [new file with mode: 0644]
crates/api/src/local_user/notifications/mark_mention_read.rs [new file with mode: 0644]
crates/api/src/local_user/notifications/mod.rs [new file with mode: 0644]
crates/api/src/local_user/notifications/unread_count.rs [new file with mode: 0644]
crates/api/src/local_user/report_count.rs [new file with mode: 0644]
crates/api/src/local_user/reset_password.rs [new file with mode: 0644]
crates/api/src/local_user/save_settings.rs [new file with mode: 0644]
crates/api/src/local_user/verify_email.rs [new file with mode: 0644]
crates/api/src/post.rs [deleted file]
crates/api/src/post/get_link_metadata.rs [new file with mode: 0644]
crates/api/src/post/like.rs [new file with mode: 0644]
crates/api/src/post/lock.rs [new file with mode: 0644]
crates/api/src/post/mark_read.rs [new file with mode: 0644]
crates/api/src/post/mod.rs [new file with mode: 0644]
crates/api/src/post/save.rs [new file with mode: 0644]
crates/api/src/post/sticky.rs [new file with mode: 0644]
crates/api/src/post_report.rs [deleted file]
crates/api/src/post_report/create.rs [new file with mode: 0644]
crates/api/src/post_report/list.rs [new file with mode: 0644]
crates/api/src/post_report/mod.rs [new file with mode: 0644]
crates/api/src/post_report/resolve.rs [new file with mode: 0644]
crates/api/src/private_message/mark_read.rs [moved from crates/api/src/private_message.rs with 100% similarity]
crates/api/src/private_message/mod.rs [new file with mode: 0644]
crates/api/src/site.rs [deleted file]
crates/api/src/site/config/mod.rs [new file with mode: 0644]
crates/api/src/site/config/read.rs [new file with mode: 0644]
crates/api/src/site/config/update.rs [new file with mode: 0644]
crates/api/src/site/leave_admin.rs [new file with mode: 0644]
crates/api/src/site/mod.rs [new file with mode: 0644]
crates/api/src/site/mod_log.rs [new file with mode: 0644]
crates/api/src/site/registration_applications/approve.rs [new file with mode: 0644]
crates/api/src/site/registration_applications/list.rs [new file with mode: 0644]
crates/api/src/site/registration_applications/mod.rs [new file with mode: 0644]
crates/api/src/site/registration_applications/unread_count.rs [new file with mode: 0644]
crates/api/src/site/resolve_object.rs [new file with mode: 0644]
crates/api/src/site/search.rs [new file with mode: 0644]
crates/api_common/src/person.rs
crates/api_crud/src/comment/list.rs [new file with mode: 0644]
crates/api_crud/src/comment/mod.rs
crates/api_crud/src/comment/read.rs
crates/api_crud/src/community/list.rs [new file with mode: 0644]
crates/api_crud/src/community/mod.rs
crates/api_crud/src/community/read.rs
crates/api_crud/src/post/list.rs [new file with mode: 0644]
crates/api_crud/src/post/mod.rs
crates/api_crud/src/post/read.rs
src/api_routes.rs

similarity index 53%
rename from crates/api/src/comment.rs
rename to crates/api/src/comment/like.rs
index d505669201a3d938ee658c1d980423293b7597a1..a5bb368a91bf75b4d3411231750c0c57bd92b5ad 100644 (file)
@@ -1,12 +1,10 @@
-use std::convert::TryInto;
-
+use crate::Perform;
 use actix_web::web::Data;
-
 use lemmy_api_common::{
   blocking,
   check_community_ban,
   check_downvotes_enabled,
-  comment::*,
+  comment::{CommentResponse, CreateCommentLike},
   get_local_user_view_from_jwt,
 };
 use lemmy_apub::{
@@ -18,111 +16,13 @@ use lemmy_apub::{
 };
 use lemmy_db_schema::{
   newtypes::LocalUserId,
-  source::comment::*,
-  traits::{Likeable, Saveable},
+  source::comment::{CommentLike, CommentLikeForm},
+  traits::Likeable,
 };
 use lemmy_db_views::{comment_view::CommentView, local_user_view::LocalUserView};
 use lemmy_utils::{ConnectionId, LemmyError};
 use lemmy_websocket::{send::send_comment_ws_message, LemmyContext, UserOperation};
-
-use crate::Perform;
-
-#[async_trait::async_trait(?Send)]
-impl Perform for MarkCommentAsRead {
-  type Response = CommentResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<CommentResponse, LemmyError> {
-    let data: &MarkCommentAsRead = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let comment_id = data.comment_id;
-    let orig_comment = blocking(context.pool(), move |conn| {
-      CommentView::read(conn, comment_id, None)
-    })
-    .await??;
-
-    // Verify that only the recipient can mark as read
-    if local_user_view.person.id != orig_comment.get_recipient_id() {
-      return Err(LemmyError::from_message("no_comment_edit_allowed"));
-    }
-
-    // Do the mark as read
-    let read = data.read;
-    blocking(context.pool(), move |conn| {
-      Comment::update_read(conn, comment_id, read)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
-
-    // Refetch it
-    let comment_id = data.comment_id;
-    let person_id = local_user_view.person.id;
-    let comment_view = blocking(context.pool(), move |conn| {
-      CommentView::read(conn, comment_id, Some(person_id))
-    })
-    .await??;
-
-    let res = CommentResponse {
-      comment_view,
-      recipient_ids: Vec::new(),
-      form_id: None,
-    };
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for SaveComment {
-  type Response = CommentResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<CommentResponse, LemmyError> {
-    let data: &SaveComment = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let comment_saved_form = CommentSavedForm {
-      comment_id: data.comment_id,
-      person_id: local_user_view.person.id,
-    };
-
-    if data.save {
-      let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
-      blocking(context.pool(), save_comment)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_comment"))?;
-    } else {
-      let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
-      blocking(context.pool(), unsave_comment)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_comment"))?;
-    }
-
-    let comment_id = data.comment_id;
-    let person_id = local_user_view.person.id;
-    let comment_view = blocking(context.pool(), move |conn| {
-      CommentView::read(conn, comment_id, Some(person_id))
-    })
-    .await??;
-
-    Ok(CommentResponse {
-      comment_view,
-      recipient_ids: Vec::new(),
-      form_id: None,
-    })
-  }
-}
+use std::convert::TryInto;
 
 #[async_trait::async_trait(?Send)]
 impl Perform for CreateCommentLike {
diff --git a/crates/api/src/comment/mark_as_read.rs b/crates/api/src/comment/mark_as_read.rs
new file mode 100644 (file)
index 0000000..25c4fb4
--- /dev/null
@@ -0,0 +1,62 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  comment::{CommentResponse, MarkCommentAsRead},
+  get_local_user_view_from_jwt,
+};
+use lemmy_db_schema::source::comment::Comment;
+use lemmy_db_views::comment_view::CommentView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for MarkCommentAsRead {
+  type Response = CommentResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &MarkCommentAsRead = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let comment_id = data.comment_id;
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, comment_id, None)
+    })
+    .await??;
+
+    // Verify that only the recipient can mark as read
+    if local_user_view.person.id != orig_comment.get_recipient_id() {
+      return Err(LemmyError::from_message("no_comment_edit_allowed"));
+    }
+
+    // Do the mark as read
+    let read = data.read;
+    blocking(context.pool(), move |conn| {
+      Comment::update_read(conn, comment_id, read)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
+
+    // Refetch it
+    let comment_id = data.comment_id;
+    let person_id = local_user_view.person.id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, comment_id, Some(person_id))
+    })
+    .await??;
+
+    let res = CommentResponse {
+      comment_view,
+      recipient_ids: Vec::new(),
+      form_id: None,
+    };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/comment/mod.rs b/crates/api/src/comment/mod.rs
new file mode 100644 (file)
index 0000000..93a71b4
--- /dev/null
@@ -0,0 +1,3 @@
+mod like;
+mod mark_as_read;
+mod save;
diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs
new file mode 100644 (file)
index 0000000..6fc0f49
--- /dev/null
@@ -0,0 +1,60 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  comment::{CommentResponse, SaveComment},
+  get_local_user_view_from_jwt,
+};
+use lemmy_db_schema::{
+  source::comment::{CommentSaved, CommentSavedForm},
+  traits::Saveable,
+};
+use lemmy_db_views::comment_view::CommentView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for SaveComment {
+  type Response = CommentResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &SaveComment = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let comment_saved_form = CommentSavedForm {
+      comment_id: data.comment_id,
+      person_id: local_user_view.person.id,
+    };
+
+    if data.save {
+      let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
+      blocking(context.pool(), save_comment)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_comment"))?;
+    } else {
+      let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
+      blocking(context.pool(), unsave_comment)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_comment"))?;
+    }
+
+    let comment_id = data.comment_id;
+    let person_id = local_user_view.person.id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, comment_id, Some(person_id))
+    })
+    .await??;
+
+    Ok(CommentResponse {
+      comment_view,
+      recipient_ids: Vec::new(),
+      form_id: None,
+    })
+  }
+}
diff --git a/crates/api/src/comment_report.rs b/crates/api/src/comment_report.rs
deleted file mode 100644 (file)
index 515c710..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-use crate::Perform;
-use actix_web::web::Data;
-use lemmy_api_common::{
-  blocking,
-  check_community_ban,
-  comment::*,
-  get_local_user_view_from_jwt,
-  is_mod_or_admin,
-};
-use lemmy_apub::protocol::activities::community::report::Report;
-use lemmy_apub_lib::object_id::ObjectId;
-use lemmy_db_schema::{source::comment_report::*, traits::Reportable};
-use lemmy_db_views::{
-  comment_report_view::{CommentReportQueryBuilder, CommentReportView},
-  comment_view::CommentView,
-};
-use lemmy_utils::{ConnectionId, LemmyError};
-use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation};
-
-/// Creates a comment report and notifies the moderators of the community
-#[async_trait::async_trait(?Send)]
-impl Perform for CreateCommentReport {
-  type Response = CommentReportResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommentReportResponse, LemmyError> {
-    let data: &CreateCommentReport = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // check size of report and check for whitespace
-    let reason = data.reason.trim();
-    if reason.is_empty() {
-      return Err(LemmyError::from_message("report_reason_required"));
-    }
-    if reason.chars().count() > 1000 {
-      return Err(LemmyError::from_message("report_too_long"));
-    }
-
-    let person_id = local_user_view.person.id;
-    let comment_id = data.comment_id;
-    let comment_view = blocking(context.pool(), move |conn| {
-      CommentView::read(conn, comment_id, None)
-    })
-    .await??;
-
-    check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
-
-    let report_form = CommentReportForm {
-      creator_id: person_id,
-      comment_id,
-      original_comment_text: comment_view.comment.content,
-      reason: data.reason.to_owned(),
-    };
-
-    let report = blocking(context.pool(), move |conn| {
-      CommentReport::report(conn, &report_form)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?;
-
-    let comment_report_view = blocking(context.pool(), move |conn| {
-      CommentReportView::read(conn, report.id, person_id)
-    })
-    .await??;
-
-    let res = CommentReportResponse {
-      comment_report_view,
-    };
-
-    context.chat_server().do_send(SendModRoomMessage {
-      op: UserOperation::CreateCommentReport,
-      response: res.clone(),
-      community_id: comment_view.community.id,
-      websocket_id,
-    });
-
-    Report::send(
-      ObjectId::new(comment_view.comment.ap_id),
-      &local_user_view.person.into(),
-      ObjectId::new(comment_view.community.actor_id),
-      reason.to_string(),
-      context,
-    )
-    .await?;
-
-    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 = CommentReportResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommentReportResponse, LemmyError> {
-    let data: &ResolveCommentReport = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let report_id = data.report_id;
-    let person_id = local_user_view.person.id;
-    let report = blocking(context.pool(), move |conn| {
-      CommentReportView::read(conn, report_id, person_id)
-    })
-    .await??;
-
-    let person_id = local_user_view.person.id;
-    is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
-
-    let resolved = data.resolved;
-    let resolve_fun = move |conn: &'_ _| {
-      if resolved {
-        CommentReport::resolve(conn, report_id, person_id)
-      } else {
-        CommentReport::unresolve(conn, report_id, person_id)
-      }
-    };
-
-    blocking(context.pool(), resolve_fun)
-      .await?
-      .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?;
-
-    let report_id = data.report_id;
-    let comment_report_view = blocking(context.pool(), move |conn| {
-      CommentReportView::read(conn, report_id, person_id)
-    })
-    .await??;
-
-    let res = CommentReportResponse {
-      comment_report_view,
-    };
-
-    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;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<ListCommentReportsResponse, LemmyError> {
-    let data: &ListCommentReports = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let person_id = local_user_view.person.id;
-    let admin = local_user_view.person.admin;
-    let community_id = data.community_id;
-    let unresolved_only = data.unresolved_only;
-
-    let page = data.page;
-    let limit = data.limit;
-    let comment_reports = blocking(context.pool(), move |conn| {
-      CommentReportQueryBuilder::create(conn, person_id, admin)
-        .community_id(community_id)
-        .unresolved_only(unresolved_only)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    let res = ListCommentReportsResponse { comment_reports };
-
-    Ok(res)
-  }
-}
diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs
new file mode 100644 (file)
index 0000000..4f4ef3b
--- /dev/null
@@ -0,0 +1,92 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  comment::{CommentReportResponse, CreateCommentReport},
+  get_local_user_view_from_jwt,
+};
+use lemmy_apub::protocol::activities::community::report::Report;
+use lemmy_apub_lib::object_id::ObjectId;
+use lemmy_db_schema::{
+  source::comment_report::{CommentReport, CommentReportForm},
+  traits::Reportable,
+};
+use lemmy_db_views::{comment_report_view::CommentReportView, comment_view::CommentView};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation};
+
+/// Creates a comment report and notifies the moderators of the community
+#[async_trait::async_trait(?Send)]
+impl Perform for CreateCommentReport {
+  type Response = CommentReportResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentReportResponse, LemmyError> {
+    let data: &CreateCommentReport = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // check size of report and check for whitespace
+    let reason = data.reason.trim();
+    if reason.is_empty() {
+      return Err(LemmyError::from_message("report_reason_required"));
+    }
+    if reason.chars().count() > 1000 {
+      return Err(LemmyError::from_message("report_too_long"));
+    }
+
+    let person_id = local_user_view.person.id;
+    let comment_id = data.comment_id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, comment_id, None)
+    })
+    .await??;
+
+    check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
+
+    let report_form = CommentReportForm {
+      creator_id: person_id,
+      comment_id,
+      original_comment_text: comment_view.comment.content,
+      reason: data.reason.to_owned(),
+    };
+
+    let report = blocking(context.pool(), move |conn| {
+      CommentReport::report(conn, &report_form)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?;
+
+    let comment_report_view = blocking(context.pool(), move |conn| {
+      CommentReportView::read(conn, report.id, person_id)
+    })
+    .await??;
+
+    let res = CommentReportResponse {
+      comment_report_view,
+    };
+
+    context.chat_server().do_send(SendModRoomMessage {
+      op: UserOperation::CreateCommentReport,
+      response: res.clone(),
+      community_id: comment_view.community.id,
+      websocket_id,
+    });
+
+    Report::send(
+      ObjectId::new(comment_view.comment.ap_id),
+      &local_user_view.person.into(),
+      ObjectId::new(comment_view.community.actor_id),
+      reason.to_string(),
+      context,
+    )
+    .await?;
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/comment_report/list.rs b/crates/api/src/comment_report/list.rs
new file mode 100644 (file)
index 0000000..b88aced
--- /dev/null
@@ -0,0 +1,49 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  comment::{ListCommentReports, ListCommentReportsResponse},
+  get_local_user_view_from_jwt,
+};
+use lemmy_db_views::comment_report_view::CommentReportQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+/// 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;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<ListCommentReportsResponse, LemmyError> {
+    let data: &ListCommentReports = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let person_id = local_user_view.person.id;
+    let admin = local_user_view.person.admin;
+    let community_id = data.community_id;
+    let unresolved_only = data.unresolved_only;
+
+    let page = data.page;
+    let limit = data.limit;
+    let comment_reports = blocking(context.pool(), move |conn| {
+      CommentReportQueryBuilder::create(conn, person_id, admin)
+        .community_id(community_id)
+        .unresolved_only(unresolved_only)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    let res = ListCommentReportsResponse { comment_reports };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/comment_report/mod.rs b/crates/api/src/comment_report/mod.rs
new file mode 100644 (file)
index 0000000..375fde4
--- /dev/null
@@ -0,0 +1,3 @@
+mod create;
+mod list;
+mod resolve;
diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/comment_report/resolve.rs
new file mode 100644 (file)
index 0000000..9446fb3
--- /dev/null
@@ -0,0 +1,71 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  comment::{CommentReportResponse, ResolveCommentReport},
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+};
+use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable};
+use lemmy_db_views::comment_report_view::CommentReportView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation};
+
+/// Resolves or unresolves a comment report and notifies the moderators of the community
+#[async_trait::async_trait(?Send)]
+impl Perform for ResolveCommentReport {
+  type Response = CommentReportResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentReportResponse, LemmyError> {
+    let data: &ResolveCommentReport = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let report_id = data.report_id;
+    let person_id = local_user_view.person.id;
+    let report = blocking(context.pool(), move |conn| {
+      CommentReportView::read(conn, report_id, person_id)
+    })
+    .await??;
+
+    let person_id = local_user_view.person.id;
+    is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
+
+    let resolved = data.resolved;
+    let resolve_fun = move |conn: &'_ _| {
+      if resolved {
+        CommentReport::resolve(conn, report_id, person_id)
+      } else {
+        CommentReport::unresolve(conn, report_id, person_id)
+      }
+    };
+
+    blocking(context.pool(), resolve_fun)
+      .await?
+      .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?;
+
+    let report_id = data.report_id;
+    let comment_report_view = blocking(context.pool(), move |conn| {
+      CommentReportView::read(conn, report_id, person_id)
+    })
+    .await??;
+
+    let res = CommentReportResponse {
+      comment_report_view,
+    };
+
+    context.chat_server().do_send(SendModRoomMessage {
+      op: UserOperation::ResolveCommentReport,
+      response: res.clone(),
+      community_id: report.community.id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs
deleted file mode 100644 (file)
index 82ee807..0000000
+++ /dev/null
@@ -1,513 +0,0 @@
-use crate::Perform;
-use actix_web::web::Data;
-use anyhow::Context;
-use lemmy_api_common::{
-  blocking,
-  check_community_ban,
-  check_community_deleted_or_removed,
-  community::*,
-  get_local_user_view_from_jwt,
-  is_mod_or_admin,
-  remove_user_data_in_community,
-};
-use lemmy_apub::{
-  activities::block::SiteOrCommunity,
-  objects::{community::ApubCommunity, person::ApubPerson},
-  protocol::activities::{
-    block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
-    community::{add_mod::AddMod, remove_mod::RemoveMod},
-    following::{follow::FollowCommunity as FollowCommunityApub, undo_follow::UndoFollowCommunity},
-  },
-};
-use lemmy_db_schema::{
-  source::{
-    community::{
-      Community,
-      CommunityFollower,
-      CommunityFollowerForm,
-      CommunityModerator,
-      CommunityModeratorForm,
-      CommunityPersonBan,
-      CommunityPersonBanForm,
-    },
-    community_block::{CommunityBlock, CommunityBlockForm},
-    moderator::{
-      ModAddCommunity,
-      ModAddCommunityForm,
-      ModBanFromCommunity,
-      ModBanFromCommunityForm,
-      ModTransferCommunity,
-      ModTransferCommunityForm,
-    },
-    person::Person,
-  },
-  traits::{Bannable, Blockable, Crud, Followable, Joinable},
-};
-use lemmy_db_views_actor::{
-  community_moderator_view::CommunityModeratorView,
-  community_view::CommunityView,
-  person_view::PersonViewSafe,
-};
-use lemmy_utils::{location_info, utils::naive_from_unix, ConnectionId, LemmyError};
-use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
-
-#[async_trait::async_trait(?Send)]
-impl Perform for FollowCommunity {
-  type Response = CommunityResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<CommunityResponse, LemmyError> {
-    let data: &FollowCommunity = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let community_id = data.community_id;
-    let community: ApubCommunity = blocking(context.pool(), move |conn| {
-      Community::read(conn, community_id)
-    })
-    .await??
-    .into();
-    let community_follower_form = CommunityFollowerForm {
-      community_id: data.community_id,
-      person_id: local_user_view.person.id,
-      pending: false,
-    };
-
-    if community.local {
-      if data.follow {
-        check_community_ban(local_user_view.person.id, community_id, context.pool()).await?;
-        check_community_deleted_or_removed(community_id, context.pool()).await?;
-
-        let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
-        blocking(context.pool(), follow)
-          .await?
-          .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?;
-      } else {
-        let unfollow =
-          move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
-        blocking(context.pool(), unfollow)
-          .await?
-          .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?;
-      }
-    } else if data.follow {
-      // Dont actually add to the community followers here, because you need
-      // to wait for the accept
-      FollowCommunityApub::send(&local_user_view.person.clone().into(), &community, context)
-        .await?;
-    } else {
-      UndoFollowCommunity::send(&local_user_view.person.clone().into(), &community, context)
-        .await?;
-      let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
-      blocking(context.pool(), unfollow)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?;
-    }
-
-    let community_id = data.community_id;
-    let person_id = local_user_view.person.id;
-    let mut community_view = blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, Some(person_id))
-    })
-    .await??;
-
-    // TODO: this needs to return a "pending" state, until Accept is received from the remote server
-    // For now, just assume that remote follows are accepted.
-    // Otherwise, the subscribed will be null
-    if !community.local {
-      community_view.subscribed = data.follow;
-    }
-
-    Ok(CommunityResponse { community_view })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for BlockCommunity {
-  type Response = BlockCommunityResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<BlockCommunityResponse, LemmyError> {
-    let data: &BlockCommunity = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let community_id = data.community_id;
-    let person_id = local_user_view.person.id;
-    let community_block_form = CommunityBlockForm {
-      person_id,
-      community_id,
-    };
-
-    if data.block {
-      let block = move |conn: &'_ _| CommunityBlock::block(conn, &community_block_form);
-      blocking(context.pool(), block)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "community_block_already_exists"))?;
-
-      // Also, unfollow the community, and send a federated unfollow
-      let community_follower_form = CommunityFollowerForm {
-        community_id: data.community_id,
-        person_id,
-        pending: false,
-      };
-      blocking(context.pool(), move |conn: &'_ _| {
-        CommunityFollower::unfollow(conn, &community_follower_form)
-      })
-      .await?
-      .ok();
-      let community = blocking(context.pool(), move |conn| {
-        Community::read(conn, community_id)
-      })
-      .await??;
-      UndoFollowCommunity::send(&local_user_view.person.into(), &community.into(), context).await?;
-    } else {
-      let unblock = move |conn: &'_ _| CommunityBlock::unblock(conn, &community_block_form);
-      blocking(context.pool(), unblock)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "community_block_already_exists"))?;
-    }
-
-    let community_view = blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, Some(person_id))
-    })
-    .await??;
-
-    Ok(BlockCommunityResponse {
-      blocked: data.block,
-      community_view,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for BanFromCommunity {
-  type Response = BanFromCommunityResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<BanFromCommunityResponse, LemmyError> {
-    let data: &BanFromCommunity = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let community_id = data.community_id;
-    let banned_person_id = data.person_id;
-    let remove_data = data.remove_data.unwrap_or(false);
-    let expires = data.expires.map(naive_from_unix);
-
-    // Verify that only mods or admins can ban
-    is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
-
-    let community_user_ban_form = CommunityPersonBanForm {
-      community_id: data.community_id,
-      person_id: data.person_id,
-      expires: Some(expires),
-    };
-
-    let community: ApubCommunity = blocking(context.pool(), move |conn: &'_ _| {
-      Community::read(conn, community_id)
-    })
-    .await??
-    .into();
-    let banned_person: ApubPerson = blocking(context.pool(), move |conn: &'_ _| {
-      Person::read(conn, banned_person_id)
-    })
-    .await??
-    .into();
-
-    if data.ban {
-      let ban = move |conn: &'_ _| CommunityPersonBan::ban(conn, &community_user_ban_form);
-      blocking(context.pool(), ban)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "community_user_already_banned"))?;
-
-      // Also unsubscribe them from the community, if they are subscribed
-      let community_follower_form = CommunityFollowerForm {
-        community_id: data.community_id,
-        person_id: banned_person_id,
-        pending: false,
-      };
-      blocking(context.pool(), move |conn: &'_ _| {
-        CommunityFollower::unfollow(conn, &community_follower_form)
-      })
-      .await?
-      .ok();
-
-      BlockUser::send(
-        &SiteOrCommunity::Community(community),
-        &banned_person,
-        &local_user_view.person.clone().into(),
-        remove_data,
-        data.reason.clone(),
-        expires,
-        context,
-      )
-      .await?;
-    } else {
-      let unban = move |conn: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form);
-      blocking(context.pool(), unban)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "community_user_already_banned"))?;
-      UndoBlockUser::send(
-        &SiteOrCommunity::Community(community),
-        &banned_person,
-        &local_user_view.person.clone().into(),
-        data.reason.clone(),
-        context,
-      )
-      .await?;
-    }
-
-    // Remove/Restore their data if that's desired
-    if remove_data {
-      remove_user_data_in_community(community_id, banned_person_id, context.pool()).await?;
-    }
-
-    // Mod tables
-    let form = ModBanFromCommunityForm {
-      mod_person_id: local_user_view.person.id,
-      other_person_id: data.person_id,
-      community_id: data.community_id,
-      reason: data.reason.to_owned(),
-      banned: Some(data.ban),
-      expires,
-    };
-    blocking(context.pool(), move |conn| {
-      ModBanFromCommunity::create(conn, &form)
-    })
-    .await??;
-
-    let person_id = data.person_id;
-    let person_view = blocking(context.pool(), move |conn| {
-      PersonViewSafe::read(conn, person_id)
-    })
-    .await??;
-
-    let res = BanFromCommunityResponse {
-      person_view,
-      banned: data.ban,
-    };
-
-    context.chat_server().do_send(SendCommunityRoomMessage {
-      op: UserOperation::BanFromCommunity,
-      response: res.clone(),
-      community_id,
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for AddModToCommunity {
-  type Response = AddModToCommunityResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<AddModToCommunityResponse, LemmyError> {
-    let data: &AddModToCommunity = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let community_id = data.community_id;
-
-    // Verify that only mods or admins can add mod
-    is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
-    let community = blocking(context.pool(), move |conn| {
-      Community::read(conn, community_id)
-    })
-    .await??;
-    if local_user_view.person.admin && !community.local {
-      return Err(LemmyError::from_message("not_a_moderator"));
-    }
-
-    // Update in local database
-    let community_moderator_form = CommunityModeratorForm {
-      community_id: data.community_id,
-      person_id: data.person_id,
-    };
-    if data.added {
-      let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
-      blocking(context.pool(), join)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?;
-    } else {
-      let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
-      blocking(context.pool(), leave)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?;
-    }
-
-    // Mod tables
-    let form = ModAddCommunityForm {
-      mod_person_id: local_user_view.person.id,
-      other_person_id: data.person_id,
-      community_id: data.community_id,
-      removed: Some(!data.added),
-    };
-    blocking(context.pool(), move |conn| {
-      ModAddCommunity::create(conn, &form)
-    })
-    .await??;
-
-    // Send to federated instances
-    let updated_mod_id = data.person_id;
-    let updated_mod: ApubPerson = blocking(context.pool(), move |conn| {
-      Person::read(conn, updated_mod_id)
-    })
-    .await??
-    .into();
-    let community: ApubCommunity = community.into();
-    if data.added {
-      AddMod::send(
-        &community,
-        &updated_mod,
-        &local_user_view.person.into(),
-        context,
-      )
-      .await?;
-    } else {
-      RemoveMod::send(
-        &community,
-        &updated_mod,
-        &local_user_view.person.into(),
-        context,
-      )
-      .await?;
-    }
-
-    // Note: in case a remote mod is added, this returns the old moderators list, it will only get
-    //       updated once we receive an activity from the community (like `Announce/Add/Moderator`)
-    let community_id = data.community_id;
-    let moderators = blocking(context.pool(), move |conn| {
-      CommunityModeratorView::for_community(conn, community_id)
-    })
-    .await??;
-
-    let res = AddModToCommunityResponse { moderators };
-    context.chat_server().do_send(SendCommunityRoomMessage {
-      op: UserOperation::AddModToCommunity,
-      response: res.clone(),
-      community_id,
-      websocket_id,
-    });
-    Ok(res)
-  }
-}
-
-// TODO: we dont do anything for federation here, it should be updated the next time the community
-//       gets fetched. i hope we can get rid of the community creator role soon.
-#[async_trait::async_trait(?Send)]
-impl Perform for TransferCommunity {
-  type Response = GetCommunityResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetCommunityResponse, LemmyError> {
-    let data: &TransferCommunity = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
-
-    // Fetch the community mods
-    let community_id = data.community_id;
-    let mut community_mods = blocking(context.pool(), move |conn| {
-      CommunityModeratorView::for_community(conn, community_id)
-    })
-    .await??;
-
-    // Make sure transferrer is either the top community mod, or an admin
-    if local_user_view.person.id != community_mods[0].moderator.id
-      && !admins
-        .iter()
-        .map(|a| a.person.id)
-        .any(|x| x == local_user_view.person.id)
-    {
-      return Err(LemmyError::from_message("not_an_admin"));
-    }
-
-    // You have to re-do the community_moderator table, reordering it.
-    // Add the transferee to the top
-    let creator_index = community_mods
-      .iter()
-      .position(|r| r.moderator.id == data.person_id)
-      .context(location_info!())?;
-    let creator_person = community_mods.remove(creator_index);
-    community_mods.insert(0, creator_person);
-
-    // Delete all the mods
-    let community_id = data.community_id;
-    blocking(context.pool(), move |conn| {
-      CommunityModerator::delete_for_community(conn, community_id)
-    })
-    .await??;
-
-    // TODO: this should probably be a bulk operation
-    // Re-add the mods, in the new order
-    for cmod in &community_mods {
-      let community_moderator_form = CommunityModeratorForm {
-        community_id: cmod.community.id,
-        person_id: cmod.moderator.id,
-      };
-
-      let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
-      blocking(context.pool(), join)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?;
-    }
-
-    // Mod tables
-    let form = ModTransferCommunityForm {
-      mod_person_id: local_user_view.person.id,
-      other_person_id: data.person_id,
-      community_id: data.community_id,
-      removed: Some(false),
-    };
-    blocking(context.pool(), move |conn| {
-      ModTransferCommunity::create(conn, &form)
-    })
-    .await??;
-
-    let community_id = data.community_id;
-    let person_id = local_user_view.person.id;
-    let community_view = blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, Some(person_id))
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?;
-
-    let community_id = data.community_id;
-    let moderators = blocking(context.pool(), move |conn| {
-      CommunityModeratorView::for_community(conn, community_id)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?;
-
-    // Return the jwt
-    Ok(GetCommunityResponse {
-      community_view,
-      site: None,
-      moderators,
-      online: 0,
-    })
-  }
-}
diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs
new file mode 100644 (file)
index 0000000..6e790a0
--- /dev/null
@@ -0,0 +1,123 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  community::{AddModToCommunity, AddModToCommunityResponse},
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+};
+use lemmy_apub::{
+  objects::{community::ApubCommunity, person::ApubPerson},
+  protocol::activities::community::{add_mod::AddMod, remove_mod::RemoveMod},
+};
+use lemmy_db_schema::{
+  source::{
+    community::{Community, CommunityModerator, CommunityModeratorForm},
+    moderator::{ModAddCommunity, ModAddCommunityForm},
+    person::Person,
+  },
+  traits::{Crud, Joinable},
+};
+use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for AddModToCommunity {
+  type Response = AddModToCommunityResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<AddModToCommunityResponse, LemmyError> {
+    let data: &AddModToCommunity = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let community_id = data.community_id;
+
+    // Verify that only mods or admins can add mod
+    is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
+    if local_user_view.person.admin && !community.local {
+      return Err(LemmyError::from_message("not_a_moderator"));
+    }
+
+    // Update in local database
+    let community_moderator_form = CommunityModeratorForm {
+      community_id: data.community_id,
+      person_id: data.person_id,
+    };
+    if data.added {
+      let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
+      blocking(context.pool(), join)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?;
+    } else {
+      let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
+      blocking(context.pool(), leave)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?;
+    }
+
+    // Mod tables
+    let form = ModAddCommunityForm {
+      mod_person_id: local_user_view.person.id,
+      other_person_id: data.person_id,
+      community_id: data.community_id,
+      removed: Some(!data.added),
+    };
+    blocking(context.pool(), move |conn| {
+      ModAddCommunity::create(conn, &form)
+    })
+    .await??;
+
+    // Send to federated instances
+    let updated_mod_id = data.person_id;
+    let updated_mod: ApubPerson = blocking(context.pool(), move |conn| {
+      Person::read(conn, updated_mod_id)
+    })
+    .await??
+    .into();
+    let community: ApubCommunity = community.into();
+    if data.added {
+      AddMod::send(
+        &community,
+        &updated_mod,
+        &local_user_view.person.into(),
+        context,
+      )
+      .await?;
+    } else {
+      RemoveMod::send(
+        &community,
+        &updated_mod,
+        &local_user_view.person.into(),
+        context,
+      )
+      .await?;
+    }
+
+    // Note: in case a remote mod is added, this returns the old moderators list, it will only get
+    //       updated once we receive an activity from the community (like `Announce/Add/Moderator`)
+    let community_id = data.community_id;
+    let moderators = blocking(context.pool(), move |conn| {
+      CommunityModeratorView::for_community(conn, community_id)
+    })
+    .await??;
+
+    let res = AddModToCommunityResponse { moderators };
+    context.chat_server().do_send(SendCommunityRoomMessage {
+      op: UserOperation::AddModToCommunity,
+      response: res.clone(),
+      community_id,
+      websocket_id,
+    });
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs
new file mode 100644 (file)
index 0000000..ccdc1b9
--- /dev/null
@@ -0,0 +1,154 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  community::{BanFromCommunity, BanFromCommunityResponse},
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+  remove_user_data_in_community,
+};
+use lemmy_apub::{
+  activities::block::SiteOrCommunity,
+  objects::{community::ApubCommunity, person::ApubPerson},
+  protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
+};
+use lemmy_db_schema::{
+  source::{
+    community::{
+      Community,
+      CommunityFollower,
+      CommunityFollowerForm,
+      CommunityPersonBan,
+      CommunityPersonBanForm,
+    },
+    moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
+    person::Person,
+  },
+  traits::{Bannable, Crud, Followable},
+};
+use lemmy_db_views_actor::person_view::PersonViewSafe;
+use lemmy_utils::{utils::naive_from_unix, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for BanFromCommunity {
+  type Response = BanFromCommunityResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<BanFromCommunityResponse, LemmyError> {
+    let data: &BanFromCommunity = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let community_id = data.community_id;
+    let banned_person_id = data.person_id;
+    let remove_data = data.remove_data.unwrap_or(false);
+    let expires = data.expires.map(naive_from_unix);
+
+    // Verify that only mods or admins can ban
+    is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
+
+    let community_user_ban_form = CommunityPersonBanForm {
+      community_id: data.community_id,
+      person_id: data.person_id,
+      expires: Some(expires),
+    };
+
+    let community: ApubCommunity = blocking(context.pool(), move |conn: &'_ _| {
+      Community::read(conn, community_id)
+    })
+    .await??
+    .into();
+    let banned_person: ApubPerson = blocking(context.pool(), move |conn: &'_ _| {
+      Person::read(conn, banned_person_id)
+    })
+    .await??
+    .into();
+
+    if data.ban {
+      let ban = move |conn: &'_ _| CommunityPersonBan::ban(conn, &community_user_ban_form);
+      blocking(context.pool(), ban)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "community_user_already_banned"))?;
+
+      // Also unsubscribe them from the community, if they are subscribed
+      let community_follower_form = CommunityFollowerForm {
+        community_id: data.community_id,
+        person_id: banned_person_id,
+        pending: false,
+      };
+      blocking(context.pool(), move |conn: &'_ _| {
+        CommunityFollower::unfollow(conn, &community_follower_form)
+      })
+      .await?
+      .ok();
+
+      BlockUser::send(
+        &SiteOrCommunity::Community(community),
+        &banned_person,
+        &local_user_view.person.clone().into(),
+        remove_data,
+        data.reason.clone(),
+        expires,
+        context,
+      )
+      .await?;
+    } else {
+      let unban = move |conn: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form);
+      blocking(context.pool(), unban)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "community_user_already_banned"))?;
+      UndoBlockUser::send(
+        &SiteOrCommunity::Community(community),
+        &banned_person,
+        &local_user_view.person.clone().into(),
+        data.reason.clone(),
+        context,
+      )
+      .await?;
+    }
+
+    // Remove/Restore their data if that's desired
+    if remove_data {
+      remove_user_data_in_community(community_id, banned_person_id, context.pool()).await?;
+    }
+
+    // Mod tables
+    let form = ModBanFromCommunityForm {
+      mod_person_id: local_user_view.person.id,
+      other_person_id: data.person_id,
+      community_id: data.community_id,
+      reason: data.reason.to_owned(),
+      banned: Some(data.ban),
+      expires,
+    };
+    blocking(context.pool(), move |conn| {
+      ModBanFromCommunity::create(conn, &form)
+    })
+    .await??;
+
+    let person_id = data.person_id;
+    let person_view = blocking(context.pool(), move |conn| {
+      PersonViewSafe::read(conn, person_id)
+    })
+    .await??;
+
+    let res = BanFromCommunityResponse {
+      person_view,
+      banned: data.ban,
+    };
+
+    context.chat_server().do_send(SendCommunityRoomMessage {
+      op: UserOperation::BanFromCommunity,
+      response: res.clone(),
+      community_id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs
new file mode 100644 (file)
index 0000000..02ece47
--- /dev/null
@@ -0,0 +1,80 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  community::{BlockCommunity, BlockCommunityResponse},
+  get_local_user_view_from_jwt,
+};
+use lemmy_apub::protocol::activities::following::undo_follow::UndoFollowCommunity;
+use lemmy_db_schema::{
+  source::{
+    community::{Community, CommunityFollower, CommunityFollowerForm},
+    community_block::{CommunityBlock, CommunityBlockForm},
+  },
+  traits::{Blockable, Crud, Followable},
+};
+use lemmy_db_views_actor::community_view::CommunityView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for BlockCommunity {
+  type Response = BlockCommunityResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<BlockCommunityResponse, LemmyError> {
+    let data: &BlockCommunity = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let community_id = data.community_id;
+    let person_id = local_user_view.person.id;
+    let community_block_form = CommunityBlockForm {
+      person_id,
+      community_id,
+    };
+
+    if data.block {
+      let block = move |conn: &'_ _| CommunityBlock::block(conn, &community_block_form);
+      blocking(context.pool(), block)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "community_block_already_exists"))?;
+
+      // Also, unfollow the community, and send a federated unfollow
+      let community_follower_form = CommunityFollowerForm {
+        community_id: data.community_id,
+        person_id,
+        pending: false,
+      };
+      blocking(context.pool(), move |conn: &'_ _| {
+        CommunityFollower::unfollow(conn, &community_follower_form)
+      })
+      .await?
+      .ok();
+      let community = blocking(context.pool(), move |conn| {
+        Community::read(conn, community_id)
+      })
+      .await??;
+      UndoFollowCommunity::send(&local_user_view.person.into(), &community.into(), context).await?;
+    } else {
+      let unblock = move |conn: &'_ _| CommunityBlock::unblock(conn, &community_block_form);
+      blocking(context.pool(), unblock)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "community_block_already_exists"))?;
+    }
+
+    let community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, Some(person_id))
+    })
+    .await??;
+
+    Ok(BlockCommunityResponse {
+      blocked: data.block,
+      community_view,
+    })
+  }
+}
diff --git a/crates/api/src/community/follow.rs b/crates/api/src/community/follow.rs
new file mode 100644 (file)
index 0000000..62eb34b
--- /dev/null
@@ -0,0 +1,97 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  check_community_deleted_or_removed,
+  community::{CommunityResponse, FollowCommunity},
+  get_local_user_view_from_jwt,
+};
+use lemmy_apub::{
+  objects::community::ApubCommunity,
+  protocol::activities::following::{
+    follow::FollowCommunity as FollowCommunityApub,
+    undo_follow::UndoFollowCommunity,
+  },
+};
+use lemmy_db_schema::{
+  source::community::{Community, CommunityFollower, CommunityFollowerForm},
+  traits::{Crud, Followable},
+};
+use lemmy_db_views_actor::community_view::CommunityView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for FollowCommunity {
+  type Response = CommunityResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<CommunityResponse, LemmyError> {
+    let data: &FollowCommunity = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let community_id = data.community_id;
+    let community: ApubCommunity = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??
+    .into();
+    let community_follower_form = CommunityFollowerForm {
+      community_id: data.community_id,
+      person_id: local_user_view.person.id,
+      pending: false,
+    };
+
+    if community.local {
+      if data.follow {
+        check_community_ban(local_user_view.person.id, community_id, context.pool()).await?;
+        check_community_deleted_or_removed(community_id, context.pool()).await?;
+
+        let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
+        blocking(context.pool(), follow)
+          .await?
+          .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?;
+      } else {
+        let unfollow =
+          move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
+        blocking(context.pool(), unfollow)
+          .await?
+          .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?;
+      }
+    } else if data.follow {
+      // Dont actually add to the community followers here, because you need
+      // to wait for the accept
+      FollowCommunityApub::send(&local_user_view.person.clone().into(), &community, context)
+        .await?;
+    } else {
+      UndoFollowCommunity::send(&local_user_view.person.clone().into(), &community, context)
+        .await?;
+      let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
+      blocking(context.pool(), unfollow)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?;
+    }
+
+    let community_id = data.community_id;
+    let person_id = local_user_view.person.id;
+    let mut community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, Some(person_id))
+    })
+    .await??;
+
+    // TODO: this needs to return a "pending" state, until Accept is received from the remote server
+    // For now, just assume that remote follows are accepted.
+    // Otherwise, the subscribed will be null
+    if !community.local {
+      community_view.subscribed = data.follow;
+    }
+
+    Ok(CommunityResponse { community_view })
+  }
+}
diff --git a/crates/api/src/community/mod.rs b/crates/api/src/community/mod.rs
new file mode 100644 (file)
index 0000000..8bf2ed5
--- /dev/null
@@ -0,0 +1,5 @@
+mod add_mod;
+mod ban;
+mod block;
+mod follow;
+mod transfer;
diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs
new file mode 100644 (file)
index 0000000..05282f4
--- /dev/null
@@ -0,0 +1,124 @@
+use crate::Perform;
+use actix_web::web::Data;
+use anyhow::Context;
+use lemmy_api_common::{
+  blocking,
+  community::{GetCommunityResponse, TransferCommunity},
+  get_local_user_view_from_jwt,
+};
+use lemmy_db_schema::{
+  source::{
+    community::{CommunityModerator, CommunityModeratorForm},
+    moderator::{ModTransferCommunity, ModTransferCommunityForm},
+  },
+  traits::{Crud, Joinable},
+};
+use lemmy_db_views_actor::{
+  community_moderator_view::CommunityModeratorView,
+  community_view::CommunityView,
+  person_view::PersonViewSafe,
+};
+use lemmy_utils::{location_info, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+// TODO: we dont do anything for federation here, it should be updated the next time the community
+//       gets fetched. i hope we can get rid of the community creator role soon.
+#[async_trait::async_trait(?Send)]
+impl Perform for TransferCommunity {
+  type Response = GetCommunityResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetCommunityResponse, LemmyError> {
+    let data: &TransferCommunity = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
+
+    // Fetch the community mods
+    let community_id = data.community_id;
+    let mut community_mods = blocking(context.pool(), move |conn| {
+      CommunityModeratorView::for_community(conn, community_id)
+    })
+    .await??;
+
+    // Make sure transferrer is either the top community mod, or an admin
+    if local_user_view.person.id != community_mods[0].moderator.id
+      && !admins
+        .iter()
+        .map(|a| a.person.id)
+        .any(|x| x == local_user_view.person.id)
+    {
+      return Err(LemmyError::from_message("not_an_admin"));
+    }
+
+    // You have to re-do the community_moderator table, reordering it.
+    // Add the transferee to the top
+    let creator_index = community_mods
+      .iter()
+      .position(|r| r.moderator.id == data.person_id)
+      .context(location_info!())?;
+    let creator_person = community_mods.remove(creator_index);
+    community_mods.insert(0, creator_person);
+
+    // Delete all the mods
+    let community_id = data.community_id;
+    blocking(context.pool(), move |conn| {
+      CommunityModerator::delete_for_community(conn, community_id)
+    })
+    .await??;
+
+    // TODO: this should probably be a bulk operation
+    // Re-add the mods, in the new order
+    for cmod in &community_mods {
+      let community_moderator_form = CommunityModeratorForm {
+        community_id: cmod.community.id,
+        person_id: cmod.moderator.id,
+      };
+
+      let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
+      blocking(context.pool(), join)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?;
+    }
+
+    // Mod tables
+    let form = ModTransferCommunityForm {
+      mod_person_id: local_user_view.person.id,
+      other_person_id: data.person_id,
+      community_id: data.community_id,
+      removed: Some(false),
+    };
+    blocking(context.pool(), move |conn| {
+      ModTransferCommunity::create(conn, &form)
+    })
+    .await??;
+
+    let community_id = data.community_id;
+    let person_id = local_user_view.person.id;
+    let community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, Some(person_id))
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?;
+
+    let community_id = data.community_id;
+    let moderators = blocking(context.pool(), move |conn| {
+      CommunityModeratorView::for_community(conn, community_id)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?;
+
+    // Return the jwt
+    Ok(GetCommunityResponse {
+      community_view,
+      site: None,
+      moderators,
+      online: 0,
+    })
+  }
+}
index c7c24062db547a0c85e0d660d8baa269f210df50..d6de25ccb22c4b49786f0750ea3df32b50e1bf86 100644 (file)
@@ -67,7 +67,7 @@ pub async fn match_websocket_operation(
       do_websocket_operation::<PasswordReset>(context, id, op, data).await
     }
     UserOperation::PasswordChange => {
-      do_websocket_operation::<PasswordChange>(context, id, op, data).await
+      do_websocket_operation::<PasswordChangeAfterReset>(context, id, op, data).await
     }
     UserOperation::UserJoin => do_websocket_operation::<UserJoin>(context, id, op, data).await,
     UserOperation::PostJoin => do_websocket_operation::<PostJoin>(context, id, op, data).await,
diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs
deleted file mode 100644 (file)
index 731f743..0000000
+++ /dev/null
@@ -1,966 +0,0 @@
-use crate::{captcha_as_wav_base64, Perform};
-use actix_web::web::Data;
-use bcrypt::verify;
-use captcha::{gen, Difficulty};
-use chrono::Duration;
-use lemmy_api_common::{
-  blocking,
-  check_image_has_local_domain,
-  check_registration_application,
-  get_local_user_view_from_jwt,
-  is_admin,
-  password_length_check,
-  person::*,
-  remove_user_data,
-  send_email_verification_success,
-  send_password_reset_email,
-  send_verification_email,
-};
-use lemmy_apub::{
-  activities::block::SiteOrCommunity,
-  protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
-};
-use lemmy_db_schema::{
-  diesel_option_overwrite,
-  diesel_option_overwrite_to_url,
-  from_opt_str_to_opt_enum,
-  naive_now,
-  source::{
-    comment::Comment,
-    email_verification::EmailVerification,
-    local_user::{LocalUser, LocalUserForm},
-    moderator::*,
-    password_reset_request::*,
-    person::*,
-    person_block::{PersonBlock, PersonBlockForm},
-    person_mention::*,
-    private_message::PrivateMessage,
-    site::*,
-  },
-  traits::{Blockable, Crud},
-  SortType,
-};
-use lemmy_db_views::{
-  comment_report_view::CommentReportView,
-  comment_view::{CommentQueryBuilder, CommentView},
-  local_user_view::LocalUserView,
-  post_report_view::PostReportView,
-  private_message_view::PrivateMessageView,
-};
-use lemmy_db_views_actor::{
-  person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
-  person_view::PersonViewSafe,
-};
-use lemmy_utils::{
-  claims::Claims,
-  utils::{is_valid_display_name, is_valid_matrix_id, naive_from_unix},
-  ConnectionId,
-  LemmyError,
-};
-use lemmy_websocket::{
-  messages::{CaptchaItem, SendAllMessage},
-  LemmyContext,
-  UserOperation,
-};
-
-#[async_trait::async_trait(?Send)]
-impl Perform for Login {
-  type Response = LoginResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<LoginResponse, LemmyError> {
-    let data: &Login = self;
-
-    // Fetch that username / email
-    let username_or_email = data.username_or_email.clone();
-    let local_user_view = blocking(context.pool(), move |conn| {
-      LocalUserView::find_by_email_or_name(conn, &username_or_email)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_that_username_or_email"))?;
-
-    // Verify the password
-    let valid: bool = verify(
-      &data.password,
-      &local_user_view.local_user.password_encrypted,
-    )
-    .unwrap_or(false);
-    if !valid {
-      return Err(LemmyError::from_message("password_incorrect"));
-    }
-
-    let site = blocking(context.pool(), Site::read_local_site).await??;
-    if site.require_email_verification && !local_user_view.local_user.email_verified {
-      return Err(LemmyError::from_message("email_not_verified"));
-    }
-
-    check_registration_application(&site, &local_user_view, context.pool()).await?;
-
-    // Return the jwt
-    Ok(LoginResponse {
-      jwt: Some(
-        Claims::jwt(
-          local_user_view.local_user.id.0,
-          &context.secret().jwt_secret,
-          &context.settings().hostname,
-        )?
-        .into(),
-      ),
-      verify_email_sent: false,
-      registration_created: false,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetCaptcha {
-  type Response = GetCaptchaResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<Self::Response, LemmyError> {
-    let captcha_settings = context.settings().captcha;
-
-    if !captcha_settings.enabled {
-      return Ok(GetCaptchaResponse { ok: None });
-    }
-
-    let captcha = match captcha_settings.difficulty.as_str() {
-      "easy" => gen(Difficulty::Easy),
-      "medium" => gen(Difficulty::Medium),
-      "hard" => gen(Difficulty::Hard),
-      _ => gen(Difficulty::Medium),
-    };
-
-    let answer = captcha.chars_as_string();
-
-    let png = captcha.as_base64().expect("failed to generate captcha");
-
-    let uuid = uuid::Uuid::new_v4().to_string();
-
-    let wav = captcha_as_wav_base64(&captcha);
-
-    let captcha_item = CaptchaItem {
-      answer,
-      uuid: uuid.to_owned(),
-      expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
-    };
-
-    // Stores the captcha item on the queue
-    context.chat_server().do_send(captcha_item);
-
-    Ok(GetCaptchaResponse {
-      ok: Some(CaptchaResponse { png, wav, uuid }),
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for SaveUserSettings {
-  type Response = LoginResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<LoginResponse, LemmyError> {
-    let data: &SaveUserSettings = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
-    let banner = diesel_option_overwrite_to_url(&data.banner)?;
-    let bio = diesel_option_overwrite(&data.bio);
-    let display_name = diesel_option_overwrite(&data.display_name);
-    let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
-    let bot_account = data.bot_account;
-    let email_deref = data.email.as_deref().map(|e| e.to_owned());
-    let email = diesel_option_overwrite(&email_deref);
-
-    check_image_has_local_domain(avatar.as_ref().unwrap_or(&None))?;
-    check_image_has_local_domain(banner.as_ref().unwrap_or(&None))?;
-
-    if let Some(Some(email)) = &email {
-      let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
-      // Only send the verification email if there was an email change
-      if previous_email.ne(email) {
-        send_verification_email(&local_user_view, email, context.pool(), &context.settings())
-          .await?;
-      }
-    }
-
-    // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
-    if let Some(email) = &email {
-      let site_fut = blocking(context.pool(), Site::read_local_site);
-      if email.is_none() && site_fut.await??.require_email_verification {
-        return Err(LemmyError::from_message("email_required"));
-      }
-    }
-
-    if let Some(Some(bio)) = &bio {
-      if bio.chars().count() > 300 {
-        return Err(LemmyError::from_message("bio_length_overflow"));
-      }
-    }
-
-    if let Some(Some(display_name)) = &display_name {
-      if !is_valid_display_name(
-        display_name.trim(),
-        context.settings().actor_name_max_length,
-      ) {
-        return Err(LemmyError::from_message("invalid_username"));
-      }
-    }
-
-    if let Some(Some(matrix_user_id)) = &matrix_user_id {
-      if !is_valid_matrix_id(matrix_user_id) {
-        return Err(LemmyError::from_message("invalid_matrix_id"));
-      }
-    }
-
-    let local_user_id = local_user_view.local_user.id;
-    let person_id = local_user_view.person.id;
-    let default_listing_type = data.default_listing_type;
-    let default_sort_type = data.default_sort_type;
-    let password_encrypted = local_user_view.local_user.password_encrypted;
-    let public_key = local_user_view.person.public_key;
-
-    let person_form = PersonForm {
-      name: local_user_view.person.name,
-      avatar,
-      banner,
-      inbox_url: None,
-      display_name,
-      published: None,
-      updated: Some(naive_now()),
-      banned: None,
-      deleted: None,
-      actor_id: None,
-      bio,
-      local: None,
-      admin: None,
-      private_key: None,
-      public_key,
-      last_refreshed_at: None,
-      shared_inbox_url: None,
-      matrix_user_id,
-      bot_account,
-      ban_expires: None,
-    };
-
-    blocking(context.pool(), move |conn| {
-      Person::update(conn, person_id, &person_form)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "user_already_exists"))?;
-
-    let local_user_form = LocalUserForm {
-      person_id: Some(person_id),
-      email,
-      password_encrypted: Some(password_encrypted),
-      show_nsfw: data.show_nsfw,
-      show_bot_accounts: data.show_bot_accounts,
-      show_scores: data.show_scores,
-      theme: data.theme.to_owned(),
-      default_sort_type,
-      default_listing_type,
-      lang: data.lang.to_owned(),
-      show_avatars: data.show_avatars,
-      show_read_posts: data.show_read_posts,
-      show_new_post_notifs: data.show_new_post_notifs,
-      send_notifications_to_email: data.send_notifications_to_email,
-      email_verified: None,
-      accepted_application: None,
-    };
-
-    let local_user_res = blocking(context.pool(), move |conn| {
-      LocalUser::update(conn, local_user_id, &local_user_form)
-    })
-    .await?;
-    let updated_local_user = match local_user_res {
-      Ok(u) => u,
-      Err(e) => {
-        let err_type = if e.to_string()
-          == "duplicate key value violates unique constraint \"local_user_email_key\""
-        {
-          "email_already_exists"
-        } else {
-          "user_already_exists"
-        };
-
-        return Err(LemmyError::from_error_message(e, err_type));
-      }
-    };
-
-    // Return the jwt
-    Ok(LoginResponse {
-      jwt: Some(
-        Claims::jwt(
-          updated_local_user.id.0,
-          &context.secret().jwt_secret,
-          &context.settings().hostname,
-        )?
-        .into(),
-      ),
-      verify_email_sent: false,
-      registration_created: false,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for ChangePassword {
-  type Response = LoginResponse;
-
-  #[tracing::instrument(skip(self, context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<LoginResponse, LemmyError> {
-    let data: &ChangePassword = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?;
-
-    password_length_check(&data.new_password)?;
-
-    // Make sure passwords match
-    if data.new_password != data.new_password_verify {
-      return Err(LemmyError::from_message("passwords_dont_match"));
-    }
-
-    // Check the old password
-    let valid: bool = verify(
-      &data.old_password,
-      &local_user_view.local_user.password_encrypted,
-    )
-    .unwrap_or(false);
-    if !valid {
-      return Err(LemmyError::from_message("password_incorrect"));
-    }
-
-    let local_user_id = local_user_view.local_user.id;
-    let new_password = data.new_password.to_owned();
-    let updated_local_user = blocking(context.pool(), move |conn| {
-      LocalUser::update_password(conn, local_user_id, &new_password)
-    })
-    .await??;
-
-    // Return the jwt
-    Ok(LoginResponse {
-      jwt: Some(
-        Claims::jwt(
-          updated_local_user.id.0,
-          &context.secret().jwt_secret,
-          &context.settings().hostname,
-        )?
-        .into(),
-      ),
-      verify_email_sent: false,
-      registration_created: false,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for AddAdmin {
-  type Response = AddAdminResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<AddAdminResponse, LemmyError> {
-    let data: &AddAdmin = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
-
-    let added = data.added;
-    let added_person_id = data.person_id;
-    let added_admin = blocking(context.pool(), move |conn| {
-      Person::add_admin(conn, added_person_id, added)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?;
-
-    // Mod tables
-    let form = ModAddForm {
-      mod_person_id: local_user_view.person.id,
-      other_person_id: added_admin.id,
-      removed: Some(!data.added),
-    };
-
-    blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
-
-    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
-
-    let res = AddAdminResponse { admins };
-
-    context.chat_server().do_send(SendAllMessage {
-      op: UserOperation::AddAdmin,
-      response: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for BanPerson {
-  type Response = BanPersonResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<BanPersonResponse, LemmyError> {
-    let data: &BanPerson = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
-
-    let ban = data.ban;
-    let banned_person_id = data.person_id;
-    let expires = data.expires.map(naive_from_unix);
-
-    let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires);
-    let person = blocking(context.pool(), ban_person)
-      .await?
-      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?;
-
-    // Remove their data if that's desired
-    let remove_data = data.remove_data.unwrap_or(false);
-    if remove_data {
-      remove_user_data(person.id, context.pool()).await?;
-    }
-
-    // Mod tables
-    let form = ModBanForm {
-      mod_person_id: local_user_view.person.id,
-      other_person_id: data.person_id,
-      reason: data.reason.to_owned(),
-      banned: Some(data.ban),
-      expires,
-    };
-
-    blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
-
-    let person_id = data.person_id;
-    let person_view = blocking(context.pool(), move |conn| {
-      PersonViewSafe::read(conn, person_id)
-    })
-    .await??;
-
-    let site = SiteOrCommunity::Site(
-      blocking(context.pool(), Site::read_local_site)
-        .await??
-        .into(),
-    );
-    // if the action affects a local user, federate to other instances
-    if person.local {
-      if ban {
-        BlockUser::send(
-          &site,
-          &person.into(),
-          &local_user_view.person.into(),
-          remove_data,
-          data.reason.clone(),
-          expires,
-          context,
-        )
-        .await?;
-      } else {
-        UndoBlockUser::send(
-          &site,
-          &person.into(),
-          &local_user_view.person.into(),
-          data.reason.clone(),
-          context,
-        )
-        .await?;
-      }
-    }
-
-    let res = BanPersonResponse {
-      person_view,
-      banned: data.ban,
-    };
-
-    context.chat_server().do_send(SendAllMessage {
-      op: UserOperation::BanPerson,
-      response: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetBannedPersons {
-  type Response = BannedPersonsResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<Self::Response, LemmyError> {
-    let data: &GetBannedPersons = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
-
-    let banned = blocking(context.pool(), PersonViewSafe::banned).await??;
-
-    let res = Self::Response { banned };
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for BlockPerson {
-  type Response = BlockPersonResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<BlockPersonResponse, LemmyError> {
-    let data: &BlockPerson = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let target_id = data.person_id;
-    let person_id = local_user_view.person.id;
-
-    // Don't let a person block themselves
-    if target_id == person_id {
-      return Err(LemmyError::from_message("cant_block_yourself"));
-    }
-
-    let person_block_form = PersonBlockForm {
-      person_id,
-      target_id,
-    };
-
-    if data.block {
-      let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form);
-      blocking(context.pool(), block)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "person_block_already_exists"))?;
-    } else {
-      let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form);
-      blocking(context.pool(), unblock)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "person_block_already_exists"))?;
-    }
-
-    // TODO does any federated stuff need to be done here?
-
-    let person_view = blocking(context.pool(), move |conn| {
-      PersonViewSafe::read(conn, target_id)
-    })
-    .await??;
-
-    let res = BlockPersonResponse {
-      person_view,
-      blocked: data.block,
-    };
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetReplies {
-  type Response = GetRepliesResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetRepliesResponse, LemmyError> {
-    let data: &GetReplies = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
-
-    let page = data.page;
-    let limit = data.limit;
-    let unread_only = data.unread_only;
-    let person_id = local_user_view.person.id;
-    let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
-
-    let replies = blocking(context.pool(), move |conn| {
-      CommentQueryBuilder::create(conn)
-        .sort(sort)
-        .unread_only(unread_only)
-        .recipient_id(person_id)
-        .show_bot_accounts(show_bot_accounts)
-        .my_person_id(person_id)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    Ok(GetRepliesResponse { replies })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetPersonMentions {
-  type Response = GetPersonMentionsResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetPersonMentionsResponse, LemmyError> {
-    let data: &GetPersonMentions = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
-
-    let page = data.page;
-    let limit = data.limit;
-    let unread_only = data.unread_only;
-    let person_id = local_user_view.person.id;
-    let mentions = blocking(context.pool(), move |conn| {
-      PersonMentionQueryBuilder::create(conn)
-        .recipient_id(person_id)
-        .my_person_id(person_id)
-        .sort(sort)
-        .unread_only(unread_only)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    Ok(GetPersonMentionsResponse { mentions })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for MarkPersonMentionAsRead {
-  type Response = PersonMentionResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<PersonMentionResponse, LemmyError> {
-    let data: &MarkPersonMentionAsRead = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let person_mention_id = data.person_mention_id;
-    let read_person_mention = blocking(context.pool(), move |conn| {
-      PersonMention::read(conn, person_mention_id)
-    })
-    .await??;
-
-    if local_user_view.person.id != read_person_mention.recipient_id {
-      return Err(LemmyError::from_message("couldnt_update_comment"));
-    }
-
-    let person_mention_id = read_person_mention.id;
-    let read = data.read;
-    let update_mention =
-      move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read);
-    blocking(context.pool(), update_mention)
-      .await?
-      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
-
-    let person_mention_id = read_person_mention.id;
-    let person_id = local_user_view.person.id;
-    let person_mention_view = blocking(context.pool(), move |conn| {
-      PersonMentionView::read(conn, person_mention_id, Some(person_id))
-    })
-    .await??;
-
-    Ok(PersonMentionResponse {
-      person_mention_view,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for MarkAllAsRead {
-  type Response = GetRepliesResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetRepliesResponse, LemmyError> {
-    let data: &MarkAllAsRead = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let person_id = local_user_view.person.id;
-    let replies = blocking(context.pool(), move |conn| {
-      CommentQueryBuilder::create(conn)
-        .my_person_id(person_id)
-        .recipient_id(person_id)
-        .unread_only(true)
-        .page(1)
-        .limit(999)
-        .list()
-    })
-    .await??;
-
-    // TODO: this should probably be a bulk operation
-    // Not easy to do as a bulk operation,
-    // because recipient_id isn't in the comment table
-    for comment_view in &replies {
-      let reply_id = comment_view.comment.id;
-      let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
-      blocking(context.pool(), mark_as_read)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
-    }
-
-    // Mark all user mentions as read
-    let update_person_mentions =
-      move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id);
-    blocking(context.pool(), update_person_mentions)
-      .await?
-      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
-
-    // Mark all private_messages as read
-    let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id);
-    blocking(context.pool(), update_pm)
-      .await?
-      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_private_message"))?;
-
-    Ok(GetRepliesResponse { replies: vec![] })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for PasswordReset {
-  type Response = PasswordResetResponse;
-
-  #[tracing::instrument(skip(self, context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<PasswordResetResponse, LemmyError> {
-    let data: &PasswordReset = self;
-
-    // Fetch that email
-    let email = data.email.clone();
-    let local_user_view = blocking(context.pool(), move |conn| {
-      LocalUserView::find_by_email(conn, &email)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_that_username_or_email"))?;
-
-    // Email the pure token to the user.
-    send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?;
-    Ok(PasswordResetResponse {})
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for PasswordChange {
-  type Response = LoginResponse;
-
-  #[tracing::instrument(skip(self, context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<LoginResponse, LemmyError> {
-    let data: &PasswordChange = self;
-
-    // Fetch the user_id from the token
-    let token = data.token.clone();
-    let local_user_id = blocking(context.pool(), move |conn| {
-      PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id)
-    })
-    .await??;
-
-    password_length_check(&data.password)?;
-
-    // Make sure passwords match
-    if data.password != data.password_verify {
-      return Err(LemmyError::from_message("passwords_dont_match"));
-    }
-
-    // Update the user with the new password
-    let password = data.password.clone();
-    let updated_local_user = blocking(context.pool(), move |conn| {
-      LocalUser::update_password(conn, local_user_id, &password)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?;
-
-    // Return the jwt
-    Ok(LoginResponse {
-      jwt: Some(
-        Claims::jwt(
-          updated_local_user.id.0,
-          &context.secret().jwt_secret,
-          &context.settings().hostname,
-        )?
-        .into(),
-      ),
-      verify_email_sent: false,
-      registration_created: false,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetReportCount {
-  type Response = GetReportCountResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetReportCountResponse, LemmyError> {
-    let data: &GetReportCount = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let person_id = local_user_view.person.id;
-    let admin = local_user_view.person.admin;
-    let community_id = data.community_id;
-
-    let comment_reports = blocking(context.pool(), move |conn| {
-      CommentReportView::get_report_count(conn, person_id, admin, community_id)
-    })
-    .await??;
-
-    let post_reports = blocking(context.pool(), move |conn| {
-      PostReportView::get_report_count(conn, person_id, admin, community_id)
-    })
-    .await??;
-
-    let res = GetReportCountResponse {
-      community_id,
-      comment_reports,
-      post_reports,
-    };
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetUnreadCount {
-  type Response = GetUnreadCountResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<Self::Response, LemmyError> {
-    let data = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let person_id = local_user_view.person.id;
-
-    let replies = blocking(context.pool(), move |conn| {
-      CommentView::get_unread_replies(conn, person_id)
-    })
-    .await??;
-
-    let mentions = blocking(context.pool(), move |conn| {
-      PersonMentionView::get_unread_mentions(conn, person_id)
-    })
-    .await??;
-
-    let private_messages = blocking(context.pool(), move |conn| {
-      PrivateMessageView::get_unread_messages(conn, person_id)
-    })
-    .await??;
-
-    let res = Self::Response {
-      replies,
-      mentions,
-      private_messages,
-    };
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for VerifyEmail {
-  type Response = VerifyEmailResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<usize>,
-  ) -> Result<Self::Response, LemmyError> {
-    let token = self.token.clone();
-    let verification = blocking(context.pool(), move |conn| {
-      EmailVerification::read_for_token(conn, &token)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "token_not_found"))?;
-
-    let form = LocalUserForm {
-      // necessary in case this is a new signup
-      email_verified: Some(true),
-      // necessary in case email of an existing user was changed
-      email: Some(Some(verification.email)),
-      ..LocalUserForm::default()
-    };
-    let local_user_id = verification.local_user_id;
-    blocking(context.pool(), move |conn| {
-      LocalUser::update(conn, local_user_id, &form)
-    })
-    .await??;
-
-    let local_user_view = blocking(context.pool(), move |conn| {
-      LocalUserView::read(conn, local_user_id)
-    })
-    .await??;
-
-    send_email_verification_success(&local_user_view, &context.settings())?;
-
-    blocking(context.pool(), move |conn| {
-      EmailVerification::delete_old_tokens_for_local_user(conn, local_user_id)
-    })
-    .await??;
-
-    Ok(VerifyEmailResponse {})
-  }
-}
diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs
new file mode 100644 (file)
index 0000000..0630e24
--- /dev/null
@@ -0,0 +1,66 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  is_admin,
+  person::{AddAdmin, AddAdminResponse},
+};
+use lemmy_db_schema::{
+  source::{
+    moderator::{ModAdd, ModAddForm},
+    person::Person,
+  },
+  traits::Crud,
+};
+use lemmy_db_views_actor::person_view::PersonViewSafe;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for AddAdmin {
+  type Response = AddAdminResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<AddAdminResponse, LemmyError> {
+    let data: &AddAdmin = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Make sure user is an admin
+    is_admin(&local_user_view)?;
+
+    let added = data.added;
+    let added_person_id = data.person_id;
+    let added_admin = blocking(context.pool(), move |conn| {
+      Person::add_admin(conn, added_person_id, added)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?;
+
+    // Mod tables
+    let form = ModAddForm {
+      mod_person_id: local_user_view.person.id,
+      other_person_id: added_admin.id,
+      removed: Some(!data.added),
+    };
+
+    blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
+
+    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
+
+    let res = AddAdminResponse { admins };
+
+    context.chat_server().do_send(SendAllMessage {
+      op: UserOperation::AddAdmin,
+      response: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs
new file mode 100644 (file)
index 0000000..0397d09
--- /dev/null
@@ -0,0 +1,118 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  is_admin,
+  person::{BanPerson, BanPersonResponse},
+  remove_user_data,
+};
+use lemmy_apub::{
+  activities::block::SiteOrCommunity,
+  protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
+};
+use lemmy_db_schema::{
+  source::{
+    moderator::{ModBan, ModBanForm},
+    person::Person,
+    site::Site,
+  },
+  traits::Crud,
+};
+use lemmy_db_views_actor::person_view::PersonViewSafe;
+use lemmy_utils::{utils::naive_from_unix, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for BanPerson {
+  type Response = BanPersonResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<BanPersonResponse, LemmyError> {
+    let data: &BanPerson = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Make sure user is an admin
+    is_admin(&local_user_view)?;
+
+    let ban = data.ban;
+    let banned_person_id = data.person_id;
+    let expires = data.expires.map(naive_from_unix);
+
+    let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires);
+    let person = blocking(context.pool(), ban_person)
+      .await?
+      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?;
+
+    // Remove their data if that's desired
+    let remove_data = data.remove_data.unwrap_or(false);
+    if remove_data {
+      remove_user_data(person.id, context.pool()).await?;
+    }
+
+    // Mod tables
+    let form = ModBanForm {
+      mod_person_id: local_user_view.person.id,
+      other_person_id: data.person_id,
+      reason: data.reason.to_owned(),
+      banned: Some(data.ban),
+      expires,
+    };
+
+    blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
+
+    let person_id = data.person_id;
+    let person_view = blocking(context.pool(), move |conn| {
+      PersonViewSafe::read(conn, person_id)
+    })
+    .await??;
+
+    let site = SiteOrCommunity::Site(
+      blocking(context.pool(), Site::read_local_site)
+        .await??
+        .into(),
+    );
+    // if the action affects a local user, federate to other instances
+    if person.local {
+      if ban {
+        BlockUser::send(
+          &site,
+          &person.into(),
+          &local_user_view.person.into(),
+          remove_data,
+          data.reason.clone(),
+          expires,
+          context,
+        )
+        .await?;
+      } else {
+        UndoBlockUser::send(
+          &site,
+          &person.into(),
+          &local_user_view.person.into(),
+          data.reason.clone(),
+          context,
+        )
+        .await?;
+      }
+    }
+
+    let res = BanPersonResponse {
+      person_view,
+      banned: data.ban,
+    };
+
+    context.chat_server().do_send(SendAllMessage {
+      op: UserOperation::BanPerson,
+      response: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/local_user/block.rs b/crates/api/src/local_user/block.rs
new file mode 100644 (file)
index 0000000..5dc68dd
--- /dev/null
@@ -0,0 +1,67 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{BlockPerson, BlockPersonResponse},
+};
+use lemmy_db_schema::{
+  source::person_block::{PersonBlock, PersonBlockForm},
+  traits::Blockable,
+};
+use lemmy_db_views_actor::person_view::PersonViewSafe;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for BlockPerson {
+  type Response = BlockPersonResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<BlockPersonResponse, LemmyError> {
+    let data: &BlockPerson = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let target_id = data.person_id;
+    let person_id = local_user_view.person.id;
+
+    // Don't let a person block themselves
+    if target_id == person_id {
+      return Err(LemmyError::from_message("cant_block_yourself"));
+    }
+
+    let person_block_form = PersonBlockForm {
+      person_id,
+      target_id,
+    };
+
+    if data.block {
+      let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form);
+      blocking(context.pool(), block)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "person_block_already_exists"))?;
+    } else {
+      let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form);
+      blocking(context.pool(), unblock)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "person_block_already_exists"))?;
+    }
+
+    let person_view = blocking(context.pool(), move |conn| {
+      PersonViewSafe::read(conn, target_id)
+    })
+    .await??;
+
+    let res = BlockPersonResponse {
+      person_view,
+      blocked: data.block,
+    };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs
new file mode 100644 (file)
index 0000000..2b66f2e
--- /dev/null
@@ -0,0 +1,66 @@
+use crate::Perform;
+use actix_web::web::Data;
+use bcrypt::verify;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  password_length_check,
+  person::{ChangePassword, LoginResponse},
+};
+use lemmy_db_schema::source::local_user::LocalUser;
+use lemmy_utils::{claims::Claims, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for ChangePassword {
+  type Response = LoginResponse;
+
+  #[tracing::instrument(skip(self, context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<LoginResponse, LemmyError> {
+    let data: &ChangePassword = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?;
+
+    password_length_check(&data.new_password)?;
+
+    // Make sure passwords match
+    if data.new_password != data.new_password_verify {
+      return Err(LemmyError::from_message("passwords_dont_match"));
+    }
+
+    // Check the old password
+    let valid: bool = verify(
+      &data.old_password,
+      &local_user_view.local_user.password_encrypted,
+    )
+    .unwrap_or(false);
+    if !valid {
+      return Err(LemmyError::from_message("password_incorrect"));
+    }
+
+    let local_user_id = local_user_view.local_user.id;
+    let new_password = data.new_password.to_owned();
+    let updated_local_user = blocking(context.pool(), move |conn| {
+      LocalUser::update_password(conn, local_user_id, &new_password)
+    })
+    .await??;
+
+    // Return the jwt
+    Ok(LoginResponse {
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
+    })
+  }
+}
diff --git a/crates/api/src/local_user/change_password_after_reset.rs b/crates/api/src/local_user/change_password_after_reset.rs
new file mode 100644 (file)
index 0000000..533a0d1
--- /dev/null
@@ -0,0 +1,63 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  password_length_check,
+  person::{LoginResponse, PasswordChangeAfterReset},
+};
+use lemmy_db_schema::source::{
+  local_user::LocalUser,
+  password_reset_request::PasswordResetRequest,
+};
+use lemmy_utils::{claims::Claims, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for PasswordChangeAfterReset {
+  type Response = LoginResponse;
+
+  #[tracing::instrument(skip(self, context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<LoginResponse, LemmyError> {
+    let data: &PasswordChangeAfterReset = self;
+
+    // Fetch the user_id from the token
+    let token = data.token.clone();
+    let local_user_id = blocking(context.pool(), move |conn| {
+      PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id)
+    })
+    .await??;
+
+    password_length_check(&data.password)?;
+
+    // Make sure passwords match
+    if data.password != data.password_verify {
+      return Err(LemmyError::from_message("passwords_dont_match"));
+    }
+
+    // Update the user with the new password
+    let password = data.password.clone();
+    let updated_local_user = blocking(context.pool(), move |conn| {
+      LocalUser::update_password(conn, local_user_id, &password)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?;
+
+    // Return the jwt
+    Ok(LoginResponse {
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
+    })
+  }
+}
diff --git a/crates/api/src/local_user/get_captcha.rs b/crates/api/src/local_user/get_captcha.rs
new file mode 100644 (file)
index 0000000..efcb668
--- /dev/null
@@ -0,0 +1,53 @@
+use crate::{captcha_as_wav_base64, Perform};
+use actix_web::web::Data;
+use captcha::{gen, Difficulty};
+use chrono::Duration;
+use lemmy_api_common::person::{CaptchaResponse, GetCaptcha, GetCaptchaResponse};
+use lemmy_db_schema::naive_now;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{messages::CaptchaItem, LemmyContext};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetCaptcha {
+  type Response = GetCaptchaResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let captcha_settings = context.settings().captcha;
+
+    if !captcha_settings.enabled {
+      return Ok(GetCaptchaResponse { ok: None });
+    }
+
+    let captcha = gen(match captcha_settings.difficulty.as_str() {
+      "easy" => Difficulty::Easy,
+      "hard" => Difficulty::Hard,
+      _ => Difficulty::Medium,
+    });
+
+    let answer = captcha.chars_as_string();
+
+    let png = captcha.as_base64().expect("failed to generate captcha");
+
+    let uuid = uuid::Uuid::new_v4().to_string();
+
+    let wav = captcha_as_wav_base64(&captcha);
+
+    let captcha_item = CaptchaItem {
+      answer,
+      uuid: uuid.to_owned(),
+      expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
+    };
+
+    // Stores the captcha item on the queue
+    context.chat_server().do_send(captcha_item);
+
+    Ok(GetCaptchaResponse {
+      ok: Some(CaptchaResponse { png, wav, uuid }),
+    })
+  }
+}
diff --git a/crates/api/src/local_user/list_banned.rs b/crates/api/src/local_user/list_banned.rs
new file mode 100644 (file)
index 0000000..1fef559
--- /dev/null
@@ -0,0 +1,35 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  is_admin,
+  person::{BannedPersonsResponse, GetBannedPersons},
+};
+use lemmy_db_views_actor::person_view::PersonViewSafe;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetBannedPersons {
+  type Response = BannedPersonsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data: &GetBannedPersons = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Make sure user is an admin
+    is_admin(&local_user_view)?;
+
+    let banned = blocking(context.pool(), PersonViewSafe::banned).await??;
+
+    let res = Self::Response { banned };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs
new file mode 100644 (file)
index 0000000..5455b86
--- /dev/null
@@ -0,0 +1,65 @@
+use crate::Perform;
+use actix_web::web::Data;
+use bcrypt::verify;
+use lemmy_api_common::{
+  blocking,
+  check_registration_application,
+  person::{Login, LoginResponse},
+};
+use lemmy_db_schema::source::site::Site;
+use lemmy_db_views::local_user_view::LocalUserView;
+use lemmy_utils::{claims::Claims, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for Login {
+  type Response = LoginResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<LoginResponse, LemmyError> {
+    let data: &Login = self;
+
+    // Fetch that username / email
+    let username_or_email = data.username_or_email.clone();
+    let local_user_view = blocking(context.pool(), move |conn| {
+      LocalUserView::find_by_email_or_name(conn, &username_or_email)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_that_username_or_email"))?;
+
+    // Verify the password
+    let valid: bool = verify(
+      &data.password,
+      &local_user_view.local_user.password_encrypted,
+    )
+    .unwrap_or(false);
+    if !valid {
+      return Err(LemmyError::from_message("password_incorrect"));
+    }
+
+    let site = blocking(context.pool(), Site::read_local_site).await??;
+    if site.require_email_verification && !local_user_view.local_user.email_verified {
+      return Err(LemmyError::from_message("email_not_verified"));
+    }
+
+    check_registration_application(&site, &local_user_view, context.pool()).await?;
+
+    // Return the jwt
+    Ok(LoginResponse {
+      jwt: Some(
+        Claims::jwt(
+          local_user_view.local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
+    })
+  }
+}
diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs
new file mode 100644 (file)
index 0000000..3a92bed
--- /dev/null
@@ -0,0 +1,13 @@
+mod add_admin;
+mod ban_person;
+mod block;
+mod change_password;
+mod change_password_after_reset;
+mod get_captcha;
+mod list_banned;
+mod login;
+mod notifications;
+mod report_count;
+mod reset_password;
+mod save_settings;
+mod verify_email;
diff --git a/crates/api/src/local_user/notifications/list_mentions.rs b/crates/api/src/local_user/notifications/list_mentions.rs
new file mode 100644 (file)
index 0000000..f47d3cc
--- /dev/null
@@ -0,0 +1,47 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{GetPersonMentions, GetPersonMentionsResponse},
+};
+use lemmy_db_schema::{from_opt_str_to_opt_enum, SortType};
+use lemmy_db_views_actor::person_mention_view::PersonMentionQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetPersonMentions {
+  type Response = GetPersonMentionsResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetPersonMentionsResponse, LemmyError> {
+    let data: &GetPersonMentions = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
+
+    let page = data.page;
+    let limit = data.limit;
+    let unread_only = data.unread_only;
+    let person_id = local_user_view.person.id;
+    let mentions = blocking(context.pool(), move |conn| {
+      PersonMentionQueryBuilder::create(conn)
+        .recipient_id(person_id)
+        .my_person_id(person_id)
+        .sort(sort)
+        .unread_only(unread_only)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    Ok(GetPersonMentionsResponse { mentions })
+  }
+}
diff --git a/crates/api/src/local_user/notifications/list_replies.rs b/crates/api/src/local_user/notifications/list_replies.rs
new file mode 100644 (file)
index 0000000..9199421
--- /dev/null
@@ -0,0 +1,50 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{GetReplies, GetRepliesResponse},
+};
+use lemmy_db_schema::{from_opt_str_to_opt_enum, SortType};
+use lemmy_db_views::comment_view::CommentQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetReplies {
+  type Response = GetRepliesResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetRepliesResponse, LemmyError> {
+    let data: &GetReplies = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
+
+    let page = data.page;
+    let limit = data.limit;
+    let unread_only = data.unread_only;
+    let person_id = local_user_view.person.id;
+    let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
+
+    let replies = blocking(context.pool(), move |conn| {
+      CommentQueryBuilder::create(conn)
+        .sort(sort)
+        .unread_only(unread_only)
+        .recipient_id(person_id)
+        .show_bot_accounts(show_bot_accounts)
+        .my_person_id(person_id)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    Ok(GetRepliesResponse { replies })
+  }
+}
diff --git a/crates/api/src/local_user/notifications/mark_all_read.rs b/crates/api/src/local_user/notifications/mark_all_read.rs
new file mode 100644 (file)
index 0000000..dc3b00d
--- /dev/null
@@ -0,0 +1,69 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{GetRepliesResponse, MarkAllAsRead},
+};
+use lemmy_db_schema::source::{
+  comment::Comment,
+  person_mention::PersonMention,
+  private_message::PrivateMessage,
+};
+use lemmy_db_views::comment_view::CommentQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for MarkAllAsRead {
+  type Response = GetRepliesResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetRepliesResponse, LemmyError> {
+    let data: &MarkAllAsRead = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let person_id = local_user_view.person.id;
+    let replies = blocking(context.pool(), move |conn| {
+      CommentQueryBuilder::create(conn)
+        .my_person_id(person_id)
+        .recipient_id(person_id)
+        .unread_only(true)
+        .page(1)
+        .limit(999)
+        .list()
+    })
+    .await??;
+
+    // TODO: this should probably be a bulk operation
+    // Not easy to do as a bulk operation,
+    // because recipient_id isn't in the comment table
+    for comment_view in &replies {
+      let reply_id = comment_view.comment.id;
+      let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
+      blocking(context.pool(), mark_as_read)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
+    }
+
+    // Mark all user mentions as read
+    let update_person_mentions =
+      move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id);
+    blocking(context.pool(), update_person_mentions)
+      .await?
+      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
+
+    // Mark all private_messages as read
+    let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id);
+    blocking(context.pool(), update_pm)
+      .await?
+      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_private_message"))?;
+
+    Ok(GetRepliesResponse { replies: vec![] })
+  }
+}
diff --git a/crates/api/src/local_user/notifications/mark_mention_read.rs b/crates/api/src/local_user/notifications/mark_mention_read.rs
new file mode 100644 (file)
index 0000000..33e427a
--- /dev/null
@@ -0,0 +1,56 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{MarkPersonMentionAsRead, PersonMentionResponse},
+};
+use lemmy_db_schema::{source::person_mention::PersonMention, traits::Crud};
+use lemmy_db_views_actor::person_mention_view::PersonMentionView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for MarkPersonMentionAsRead {
+  type Response = PersonMentionResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<PersonMentionResponse, LemmyError> {
+    let data: &MarkPersonMentionAsRead = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let person_mention_id = data.person_mention_id;
+    let read_person_mention = blocking(context.pool(), move |conn| {
+      PersonMention::read(conn, person_mention_id)
+    })
+    .await??;
+
+    if local_user_view.person.id != read_person_mention.recipient_id {
+      return Err(LemmyError::from_message("couldnt_update_comment"));
+    }
+
+    let person_mention_id = read_person_mention.id;
+    let read = data.read;
+    let update_mention =
+      move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read);
+    blocking(context.pool(), update_mention)
+      .await?
+      .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
+
+    let person_mention_id = read_person_mention.id;
+    let person_id = local_user_view.person.id;
+    let person_mention_view = blocking(context.pool(), move |conn| {
+      PersonMentionView::read(conn, person_mention_id, Some(person_id))
+    })
+    .await??;
+
+    Ok(PersonMentionResponse {
+      person_mention_view,
+    })
+  }
+}
diff --git a/crates/api/src/local_user/notifications/mod.rs b/crates/api/src/local_user/notifications/mod.rs
new file mode 100644 (file)
index 0000000..ba90293
--- /dev/null
@@ -0,0 +1,5 @@
+mod list_mentions;
+mod list_replies;
+mod mark_all_read;
+mod mark_mention_read;
+mod unread_count;
diff --git a/crates/api/src/local_user/notifications/unread_count.rs b/crates/api/src/local_user/notifications/unread_count.rs
new file mode 100644 (file)
index 0000000..be80556
--- /dev/null
@@ -0,0 +1,52 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{GetUnreadCount, GetUnreadCountResponse},
+};
+use lemmy_db_views::{comment_view::CommentView, private_message_view::PrivateMessageView};
+use lemmy_db_views_actor::person_mention_view::PersonMentionView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetUnreadCount {
+  type Response = GetUnreadCountResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let person_id = local_user_view.person.id;
+
+    let replies = blocking(context.pool(), move |conn| {
+      CommentView::get_unread_replies(conn, person_id)
+    })
+    .await??;
+
+    let mentions = blocking(context.pool(), move |conn| {
+      PersonMentionView::get_unread_mentions(conn, person_id)
+    })
+    .await??;
+
+    let private_messages = blocking(context.pool(), move |conn| {
+      PrivateMessageView::get_unread_messages(conn, person_id)
+    })
+    .await??;
+
+    let res = Self::Response {
+      replies,
+      mentions,
+      private_messages,
+    };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/local_user/report_count.rs b/crates/api/src/local_user/report_count.rs
new file mode 100644 (file)
index 0000000..5aa2f46
--- /dev/null
@@ -0,0 +1,48 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{GetReportCount, GetReportCountResponse},
+};
+use lemmy_db_views::{comment_report_view::CommentReportView, post_report_view::PostReportView};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetReportCount {
+  type Response = GetReportCountResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetReportCountResponse, LemmyError> {
+    let data: &GetReportCount = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let person_id = local_user_view.person.id;
+    let admin = local_user_view.person.admin;
+    let community_id = data.community_id;
+
+    let comment_reports = blocking(context.pool(), move |conn| {
+      CommentReportView::get_report_count(conn, person_id, admin, community_id)
+    })
+    .await??;
+
+    let post_reports = blocking(context.pool(), move |conn| {
+      PostReportView::get_report_count(conn, person_id, admin, community_id)
+    })
+    .await??;
+
+    let res = GetReportCountResponse {
+      community_id,
+      comment_reports,
+      post_reports,
+    };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs
new file mode 100644 (file)
index 0000000..78b0364
--- /dev/null
@@ -0,0 +1,36 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  person::{PasswordReset, PasswordResetResponse},
+  send_password_reset_email,
+};
+use lemmy_db_views::local_user_view::LocalUserView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for PasswordReset {
+  type Response = PasswordResetResponse;
+
+  #[tracing::instrument(skip(self, context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<PasswordResetResponse, LemmyError> {
+    let data: &PasswordReset = self;
+
+    // Fetch that email
+    let email = data.email.clone();
+    let local_user_view = blocking(context.pool(), move |conn| {
+      LocalUserView::find_by_email(conn, &email)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_that_username_or_email"))?;
+
+    // Email the pure token to the user.
+    send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?;
+    Ok(PasswordResetResponse {})
+  }
+}
diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs
new file mode 100644 (file)
index 0000000..15a65b0
--- /dev/null
@@ -0,0 +1,181 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_image_has_local_domain,
+  get_local_user_view_from_jwt,
+  person::{LoginResponse, SaveUserSettings},
+  send_verification_email,
+};
+use lemmy_db_schema::{
+  diesel_option_overwrite,
+  diesel_option_overwrite_to_url,
+  naive_now,
+  source::{
+    local_user::{LocalUser, LocalUserForm},
+    person::{Person, PersonForm},
+    site::Site,
+  },
+  traits::Crud,
+};
+use lemmy_utils::{
+  claims::Claims,
+  utils::{is_valid_display_name, is_valid_matrix_id},
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for SaveUserSettings {
+  type Response = LoginResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<LoginResponse, LemmyError> {
+    let data: &SaveUserSettings = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
+    let banner = diesel_option_overwrite_to_url(&data.banner)?;
+    let bio = diesel_option_overwrite(&data.bio);
+    let display_name = diesel_option_overwrite(&data.display_name);
+    let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
+    let bot_account = data.bot_account;
+    let email_deref = data.email.as_deref().map(|e| e.to_owned());
+    let email = diesel_option_overwrite(&email_deref);
+
+    check_image_has_local_domain(avatar.as_ref().unwrap_or(&None))?;
+    check_image_has_local_domain(banner.as_ref().unwrap_or(&None))?;
+
+    if let Some(Some(email)) = &email {
+      let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
+      // Only send the verification email if there was an email change
+      if previous_email.ne(email) {
+        send_verification_email(&local_user_view, email, context.pool(), &context.settings())
+          .await?;
+      }
+    }
+
+    // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
+    if let Some(email) = &email {
+      let site_fut = blocking(context.pool(), Site::read_local_site);
+      if email.is_none() && site_fut.await??.require_email_verification {
+        return Err(LemmyError::from_message("email_required"));
+      }
+    }
+
+    if let Some(Some(bio)) = &bio {
+      if bio.chars().count() > 300 {
+        return Err(LemmyError::from_message("bio_length_overflow"));
+      }
+    }
+
+    if let Some(Some(display_name)) = &display_name {
+      if !is_valid_display_name(
+        display_name.trim(),
+        context.settings().actor_name_max_length,
+      ) {
+        return Err(LemmyError::from_message("invalid_username"));
+      }
+    }
+
+    if let Some(Some(matrix_user_id)) = &matrix_user_id {
+      if !is_valid_matrix_id(matrix_user_id) {
+        return Err(LemmyError::from_message("invalid_matrix_id"));
+      }
+    }
+
+    let local_user_id = local_user_view.local_user.id;
+    let person_id = local_user_view.person.id;
+    let default_listing_type = data.default_listing_type;
+    let default_sort_type = data.default_sort_type;
+    let password_encrypted = local_user_view.local_user.password_encrypted;
+    let public_key = local_user_view.person.public_key;
+
+    let person_form = PersonForm {
+      name: local_user_view.person.name,
+      avatar,
+      banner,
+      inbox_url: None,
+      display_name,
+      published: None,
+      updated: Some(naive_now()),
+      banned: None,
+      deleted: None,
+      actor_id: None,
+      bio,
+      local: None,
+      admin: None,
+      private_key: None,
+      public_key,
+      last_refreshed_at: None,
+      shared_inbox_url: None,
+      matrix_user_id,
+      bot_account,
+      ban_expires: None,
+    };
+
+    blocking(context.pool(), move |conn| {
+      Person::update(conn, person_id, &person_form)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "user_already_exists"))?;
+
+    let local_user_form = LocalUserForm {
+      person_id: Some(person_id),
+      email,
+      password_encrypted: Some(password_encrypted),
+      show_nsfw: data.show_nsfw,
+      show_bot_accounts: data.show_bot_accounts,
+      show_scores: data.show_scores,
+      theme: data.theme.to_owned(),
+      default_sort_type,
+      default_listing_type,
+      lang: data.lang.to_owned(),
+      show_avatars: data.show_avatars,
+      show_read_posts: data.show_read_posts,
+      show_new_post_notifs: data.show_new_post_notifs,
+      send_notifications_to_email: data.send_notifications_to_email,
+      email_verified: None,
+      accepted_application: None,
+    };
+
+    let local_user_res = blocking(context.pool(), move |conn| {
+      LocalUser::update(conn, local_user_id, &local_user_form)
+    })
+    .await?;
+    let updated_local_user = match local_user_res {
+      Ok(u) => u,
+      Err(e) => {
+        let err_type = if e.to_string()
+          == "duplicate key value violates unique constraint \"local_user_email_key\""
+        {
+          "email_already_exists"
+        } else {
+          "user_already_exists"
+        };
+
+        return Err(LemmyError::from_error_message(e, err_type));
+      }
+    };
+
+    // Return the jwt
+    Ok(LoginResponse {
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
+    })
+  }
+}
diff --git a/crates/api/src/local_user/verify_email.rs b/crates/api/src/local_user/verify_email.rs
new file mode 100644 (file)
index 0000000..852e1cf
--- /dev/null
@@ -0,0 +1,62 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  person::{VerifyEmail, VerifyEmailResponse},
+  send_email_verification_success,
+};
+use lemmy_db_schema::{
+  source::{
+    email_verification::EmailVerification,
+    local_user::{LocalUser, LocalUserForm},
+  },
+  traits::Crud,
+};
+use lemmy_db_views::local_user_view::LocalUserView;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for VerifyEmail {
+  type Response = VerifyEmailResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<usize>,
+  ) -> Result<Self::Response, LemmyError> {
+    let token = self.token.clone();
+    let verification = blocking(context.pool(), move |conn| {
+      EmailVerification::read_for_token(conn, &token)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "token_not_found"))?;
+
+    let form = LocalUserForm {
+      // necessary in case this is a new signup
+      email_verified: Some(true),
+      // necessary in case email of an existing user was changed
+      email: Some(Some(verification.email)),
+      ..LocalUserForm::default()
+    };
+    let local_user_id = verification.local_user_id;
+    blocking(context.pool(), move |conn| {
+      LocalUser::update(conn, local_user_id, &form)
+    })
+    .await??;
+
+    let local_user_view = blocking(context.pool(), move |conn| {
+      LocalUserView::read(conn, local_user_id)
+    })
+    .await??;
+
+    send_email_verification_success(&local_user_view, &context.settings())?;
+
+    blocking(context.pool(), move |conn| {
+      EmailVerification::delete_old_tokens_for_local_user(conn, local_user_id)
+    })
+    .await??;
+
+    Ok(VerifyEmailResponse {})
+  }
+}
diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs
deleted file mode 100644 (file)
index 6f0ab00..0000000
+++ /dev/null
@@ -1,361 +0,0 @@
-use crate::Perform;
-use actix_web::web::Data;
-use lemmy_api_common::{
-  blocking,
-  check_community_ban,
-  check_community_deleted_or_removed,
-  check_downvotes_enabled,
-  get_local_user_view_from_jwt,
-  is_mod_or_admin,
-  mark_post_as_read,
-  mark_post_as_unread,
-  post::*,
-};
-use lemmy_apub::{
-  fetcher::post_or_comment::PostOrComment,
-  objects::post::ApubPost,
-  protocol::activities::{
-    create_or_update::post::CreateOrUpdatePost,
-    voting::{
-      undo_vote::UndoVote,
-      vote::{Vote, VoteType},
-    },
-    CreateOrUpdateType,
-  },
-};
-use lemmy_db_schema::{
-  source::{moderator::*, post::*},
-  traits::{Crud, Likeable, Saveable},
-};
-use lemmy_db_views::post_view::PostView;
-use lemmy_utils::{request::fetch_site_metadata, ConnectionId, LemmyError};
-use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation};
-use std::convert::TryInto;
-
-#[async_trait::async_trait(?Send)]
-impl Perform for CreatePostLike {
-  type Response = PostResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostResponse, LemmyError> {
-    let data: &CreatePostLike = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // Don't do a downvote if site has downvotes disabled
-    check_downvotes_enabled(data.score, context.pool()).await?;
-
-    // Check for a community ban
-    let post_id = data.post_id;
-    let post: ApubPost = blocking(context.pool(), move |conn| Post::read(conn, post_id))
-      .await??
-      .into();
-
-    check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
-    check_community_deleted_or_removed(post.community_id, context.pool()).await?;
-
-    let like_form = PostLikeForm {
-      post_id: data.post_id,
-      person_id: local_user_view.person.id,
-      score: data.score,
-    };
-
-    // Remove any likes first
-    let person_id = local_user_view.person.id;
-    blocking(context.pool(), move |conn| {
-      PostLike::remove(conn, person_id, post_id)
-    })
-    .await??;
-
-    let community_id = post.community_id;
-    let object = PostOrComment::Post(Box::new(post));
-
-    // Only add the like if the score isnt 0
-    let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
-    if do_add {
-      let like_form2 = like_form.clone();
-      let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
-      blocking(context.pool(), like)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "couldnt_like_post"))?;
-
-      Vote::send(
-        &object,
-        &local_user_view.person.clone().into(),
-        community_id,
-        like_form.score.try_into()?,
-        context,
-      )
-      .await?;
-    } else {
-      // API doesn't distinguish between Undo/Like and Undo/Dislike
-      UndoVote::send(
-        &object,
-        &local_user_view.person.clone().into(),
-        community_id,
-        VoteType::Like,
-        context,
-      )
-      .await?;
-    }
-
-    // Mark the post as read
-    mark_post_as_read(person_id, post_id, context.pool()).await?;
-
-    send_post_ws_message(
-      data.post_id,
-      UserOperation::CreatePostLike,
-      websocket_id,
-      Some(local_user_view.person.id),
-      context,
-    )
-    .await
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for MarkPostAsRead {
-  type Response = PostResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<Self::Response, LemmyError> {
-    let data = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let post_id = data.post_id;
-    let person_id = local_user_view.person.id;
-
-    // Mark the post as read / unread
-    if data.read {
-      mark_post_as_read(person_id, post_id, context.pool()).await?;
-    } else {
-      mark_post_as_unread(person_id, post_id, context.pool()).await?;
-    }
-
-    // Fetch it
-    let post_view = blocking(context.pool(), move |conn| {
-      PostView::read(conn, post_id, Some(person_id))
-    })
-    .await??;
-
-    let res = Self::Response { post_view };
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for LockPost {
-  type Response = PostResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostResponse, LemmyError> {
-    let data: &LockPost = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let post_id = data.post_id;
-    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_post.community_id,
-      context.pool(),
-    )
-    .await?;
-    check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
-
-    // Verify that only the mods can lock
-    is_mod_or_admin(
-      context.pool(),
-      local_user_view.person.id,
-      orig_post.community_id,
-    )
-    .await?;
-
-    // Update the post
-    let post_id = data.post_id;
-    let locked = data.locked;
-    let updated_post: ApubPost = blocking(context.pool(), move |conn| {
-      Post::update_locked(conn, post_id, locked)
-    })
-    .await??
-    .into();
-
-    // Mod tables
-    let form = ModLockPostForm {
-      mod_person_id: local_user_view.person.id,
-      post_id: data.post_id,
-      locked: Some(locked),
-    };
-    blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
-
-    // apub updates
-    CreateOrUpdatePost::send(
-      updated_post,
-      &local_user_view.person.clone().into(),
-      CreateOrUpdateType::Update,
-      context,
-    )
-    .await?;
-
-    send_post_ws_message(
-      data.post_id,
-      UserOperation::LockPost,
-      websocket_id,
-      Some(local_user_view.person.id),
-      context,
-    )
-    .await
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for StickyPost {
-  type Response = PostResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostResponse, LemmyError> {
-    let data: &StickyPost = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let post_id = data.post_id;
-    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_post.community_id,
-      context.pool(),
-    )
-    .await?;
-    check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
-
-    // Verify that only the mods can sticky
-    is_mod_or_admin(
-      context.pool(),
-      local_user_view.person.id,
-      orig_post.community_id,
-    )
-    .await?;
-
-    // Update the post
-    let post_id = data.post_id;
-    let stickied = data.stickied;
-    let updated_post: ApubPost = blocking(context.pool(), move |conn| {
-      Post::update_stickied(conn, post_id, stickied)
-    })
-    .await??
-    .into();
-
-    // Mod tables
-    let form = ModStickyPostForm {
-      mod_person_id: local_user_view.person.id,
-      post_id: data.post_id,
-      stickied: Some(stickied),
-    };
-    blocking(context.pool(), move |conn| {
-      ModStickyPost::create(conn, &form)
-    })
-    .await??;
-
-    // Apub updates
-    // TODO stickied should pry work like locked for ease of use
-    CreateOrUpdatePost::send(
-      updated_post,
-      &local_user_view.person.clone().into(),
-      CreateOrUpdateType::Update,
-      context,
-    )
-    .await?;
-
-    send_post_ws_message(
-      data.post_id,
-      UserOperation::StickyPost,
-      websocket_id,
-      Some(local_user_view.person.id),
-      context,
-    )
-    .await
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for SavePost {
-  type Response = PostResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<PostResponse, LemmyError> {
-    let data: &SavePost = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let post_saved_form = PostSavedForm {
-      post_id: data.post_id,
-      person_id: local_user_view.person.id,
-    };
-
-    if data.save {
-      let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
-      blocking(context.pool(), save)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_post"))?;
-    } else {
-      let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
-      blocking(context.pool(), unsave)
-        .await?
-        .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_post"))?;
-    }
-
-    let post_id = data.post_id;
-    let person_id = local_user_view.person.id;
-    let post_view = blocking(context.pool(), move |conn| {
-      PostView::read(conn, post_id, Some(person_id))
-    })
-    .await??;
-
-    // Mark the post as read
-    mark_post_as_read(person_id, post_id, context.pool()).await?;
-
-    Ok(PostResponse { post_view })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetSiteMetadata {
-  type Response = GetSiteMetadataResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetSiteMetadataResponse, LemmyError> {
-    let data: &Self = self;
-
-    let metadata = fetch_site_metadata(context.client(), &data.url).await?;
-
-    Ok(GetSiteMetadataResponse { metadata })
-  }
-}
diff --git a/crates/api/src/post/get_link_metadata.rs b/crates/api/src/post/get_link_metadata.rs
new file mode 100644 (file)
index 0000000..a016bd2
--- /dev/null
@@ -0,0 +1,23 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::post::{GetSiteMetadata, GetSiteMetadataResponse};
+use lemmy_utils::{request::fetch_site_metadata, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetSiteMetadata {
+  type Response = GetSiteMetadataResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetSiteMetadataResponse, LemmyError> {
+    let data: &Self = self;
+
+    let metadata = fetch_site_metadata(context.client(), &data.url).await?;
+
+    Ok(GetSiteMetadataResponse { metadata })
+  }
+}
diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs
new file mode 100644 (file)
index 0000000..22eb477
--- /dev/null
@@ -0,0 +1,110 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  check_community_deleted_or_removed,
+  check_downvotes_enabled,
+  get_local_user_view_from_jwt,
+  mark_post_as_read,
+  post::{CreatePostLike, PostResponse},
+};
+use lemmy_apub::{
+  fetcher::post_or_comment::PostOrComment,
+  objects::post::ApubPost,
+  protocol::activities::voting::{
+    undo_vote::UndoVote,
+    vote::{Vote, VoteType},
+  },
+};
+use lemmy_db_schema::{
+  source::post::{Post, PostLike, PostLikeForm},
+  traits::{Crud, Likeable},
+};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for CreatePostLike {
+  type Response = PostResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &CreatePostLike = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Don't do a downvote if site has downvotes disabled
+    check_downvotes_enabled(data.score, context.pool()).await?;
+
+    // Check for a community ban
+    let post_id = data.post_id;
+    let post: ApubPost = blocking(context.pool(), move |conn| Post::read(conn, post_id))
+      .await??
+      .into();
+
+    check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
+    check_community_deleted_or_removed(post.community_id, context.pool()).await?;
+
+    let like_form = PostLikeForm {
+      post_id: data.post_id,
+      person_id: local_user_view.person.id,
+      score: data.score,
+    };
+
+    // Remove any likes first
+    let person_id = local_user_view.person.id;
+    blocking(context.pool(), move |conn| {
+      PostLike::remove(conn, person_id, post_id)
+    })
+    .await??;
+
+    let community_id = post.community_id;
+    let object = PostOrComment::Post(Box::new(post));
+
+    // Only add the like if the score isnt 0
+    let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
+    if do_add {
+      let like_form2 = like_form.clone();
+      let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
+      blocking(context.pool(), like)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "couldnt_like_post"))?;
+
+      Vote::send(
+        &object,
+        &local_user_view.person.clone().into(),
+        community_id,
+        like_form.score.try_into()?,
+        context,
+      )
+      .await?;
+    } else {
+      // API doesn't distinguish between Undo/Like and Undo/Dislike
+      UndoVote::send(
+        &object,
+        &local_user_view.person.clone().into(),
+        community_id,
+        VoteType::Like,
+        context,
+      )
+      .await?;
+    }
+
+    // Mark the post as read
+    mark_post_as_read(person_id, post_id, context.pool()).await?;
+
+    send_post_ws_message(
+      data.post_id,
+      UserOperation::CreatePostLike,
+      websocket_id,
+      Some(local_user_view.person.id),
+      context,
+    )
+    .await
+  }
+}
diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs
new file mode 100644 (file)
index 0000000..7210d44
--- /dev/null
@@ -0,0 +1,93 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  check_community_deleted_or_removed,
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+  post::{LockPost, PostResponse},
+};
+use lemmy_apub::{
+  objects::post::ApubPost,
+  protocol::activities::{create_or_update::post::CreateOrUpdatePost, CreateOrUpdateType},
+};
+use lemmy_db_schema::{
+  source::{
+    moderator::{ModLockPost, ModLockPostForm},
+    post::Post,
+  },
+  traits::Crud,
+};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for LockPost {
+  type Response = PostResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &LockPost = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let post_id = data.post_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+
+    check_community_ban(
+      local_user_view.person.id,
+      orig_post.community_id,
+      context.pool(),
+    )
+    .await?;
+    check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
+
+    // Verify that only the mods can lock
+    is_mod_or_admin(
+      context.pool(),
+      local_user_view.person.id,
+      orig_post.community_id,
+    )
+    .await?;
+
+    // Update the post
+    let post_id = data.post_id;
+    let locked = data.locked;
+    let updated_post: ApubPost = blocking(context.pool(), move |conn| {
+      Post::update_locked(conn, post_id, locked)
+    })
+    .await??
+    .into();
+
+    // Mod tables
+    let form = ModLockPostForm {
+      mod_person_id: local_user_view.person.id,
+      post_id: data.post_id,
+      locked: Some(locked),
+    };
+    blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
+
+    // apub updates
+    CreateOrUpdatePost::send(
+      updated_post,
+      &local_user_view.person.clone().into(),
+      CreateOrUpdateType::Update,
+      context,
+    )
+    .await?;
+
+    send_post_ws_message(
+      data.post_id,
+      UserOperation::LockPost,
+      websocket_id,
+      Some(local_user_view.person.id),
+      context,
+    )
+    .await
+  }
+}
diff --git a/crates/api/src/post/mark_read.rs b/crates/api/src/post/mark_read.rs
new file mode 100644 (file)
index 0000000..927fc65
--- /dev/null
@@ -0,0 +1,48 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  mark_post_as_read,
+  mark_post_as_unread,
+  post::{MarkPostAsRead, PostResponse},
+};
+use lemmy_db_views::post_view::PostView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for MarkPostAsRead {
+  type Response = PostResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let post_id = data.post_id;
+    let person_id = local_user_view.person.id;
+
+    // Mark the post as read / unread
+    if data.read {
+      mark_post_as_read(person_id, post_id, context.pool()).await?;
+    } else {
+      mark_post_as_unread(person_id, post_id, context.pool()).await?;
+    }
+
+    // Fetch it
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, post_id, Some(person_id))
+    })
+    .await??;
+
+    let res = Self::Response { post_view };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/post/mod.rs b/crates/api/src/post/mod.rs
new file mode 100644 (file)
index 0000000..40cea54
--- /dev/null
@@ -0,0 +1,6 @@
+mod get_link_metadata;
+mod like;
+mod lock;
+mod mark_read;
+mod save;
+mod sticky;
diff --git a/crates/api/src/post/save.rs b/crates/api/src/post/save.rs
new file mode 100644 (file)
index 0000000..973b99d
--- /dev/null
@@ -0,0 +1,60 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  mark_post_as_read,
+  post::{PostResponse, SavePost},
+};
+use lemmy_db_schema::{
+  source::post::{PostSaved, PostSavedForm},
+  traits::Saveable,
+};
+use lemmy_db_views::post_view::PostView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for SavePost {
+  type Response = PostResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &SavePost = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let post_saved_form = PostSavedForm {
+      post_id: data.post_id,
+      person_id: local_user_view.person.id,
+    };
+
+    if data.save {
+      let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
+      blocking(context.pool(), save)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_post"))?;
+    } else {
+      let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
+      blocking(context.pool(), unsave)
+        .await?
+        .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_post"))?;
+    }
+
+    let post_id = data.post_id;
+    let person_id = local_user_view.person.id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, post_id, Some(person_id))
+    })
+    .await??;
+
+    // Mark the post as read
+    mark_post_as_read(person_id, post_id, context.pool()).await?;
+
+    Ok(PostResponse { post_view })
+  }
+}
diff --git a/crates/api/src/post/sticky.rs b/crates/api/src/post/sticky.rs
new file mode 100644 (file)
index 0000000..db66925
--- /dev/null
@@ -0,0 +1,97 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  check_community_deleted_or_removed,
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+  post::{PostResponse, StickyPost},
+};
+use lemmy_apub::{
+  objects::post::ApubPost,
+  protocol::activities::{create_or_update::post::CreateOrUpdatePost, CreateOrUpdateType},
+};
+use lemmy_db_schema::{
+  source::{
+    moderator::{ModStickyPost, ModStickyPostForm},
+    post::Post,
+  },
+  traits::Crud,
+};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for StickyPost {
+  type Response = PostResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &StickyPost = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let post_id = data.post_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+
+    check_community_ban(
+      local_user_view.person.id,
+      orig_post.community_id,
+      context.pool(),
+    )
+    .await?;
+    check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
+
+    // Verify that only the mods can sticky
+    is_mod_or_admin(
+      context.pool(),
+      local_user_view.person.id,
+      orig_post.community_id,
+    )
+    .await?;
+
+    // Update the post
+    let post_id = data.post_id;
+    let stickied = data.stickied;
+    let updated_post: ApubPost = blocking(context.pool(), move |conn| {
+      Post::update_stickied(conn, post_id, stickied)
+    })
+    .await??
+    .into();
+
+    // Mod tables
+    let form = ModStickyPostForm {
+      mod_person_id: local_user_view.person.id,
+      post_id: data.post_id,
+      stickied: Some(stickied),
+    };
+    blocking(context.pool(), move |conn| {
+      ModStickyPost::create(conn, &form)
+    })
+    .await??;
+
+    // Apub updates
+    // TODO stickied should pry work like locked for ease of use
+    CreateOrUpdatePost::send(
+      updated_post,
+      &local_user_view.person.clone().into(),
+      CreateOrUpdateType::Update,
+      context,
+    )
+    .await?;
+
+    send_post_ws_message(
+      data.post_id,
+      UserOperation::StickyPost,
+      websocket_id,
+      Some(local_user_view.person.id),
+      context,
+    )
+    .await
+  }
+}
diff --git a/crates/api/src/post_report.rs b/crates/api/src/post_report.rs
deleted file mode 100644 (file)
index 26c4d7a..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-use crate::Perform;
-use actix_web::web::Data;
-use lemmy_api_common::{
-  blocking,
-  check_community_ban,
-  get_local_user_view_from_jwt,
-  is_mod_or_admin,
-  post::{
-    CreatePostReport,
-    ListPostReports,
-    ListPostReportsResponse,
-    PostReportResponse,
-    ResolvePostReport,
-  },
-};
-use lemmy_apub::protocol::activities::community::report::Report;
-use lemmy_apub_lib::object_id::ObjectId;
-use lemmy_db_schema::{
-  source::post_report::{PostReport, PostReportForm},
-  traits::Reportable,
-};
-use lemmy_db_views::{
-  post_report_view::{PostReportQueryBuilder, PostReportView},
-  post_view::PostView,
-};
-use lemmy_utils::{ConnectionId, LemmyError};
-use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation};
-
-/// Creates a post report and notifies the moderators of the community
-#[async_trait::async_trait(?Send)]
-impl Perform for CreatePostReport {
-  type Response = PostReportResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostReportResponse, LemmyError> {
-    let data: &CreatePostReport = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // check size of report and check for whitespace
-    let reason = data.reason.trim();
-    if reason.is_empty() {
-      return Err(LemmyError::from_message("report_reason_required"));
-    }
-    if reason.chars().count() > 1000 {
-      return Err(LemmyError::from_message("report_too_long"));
-    }
-
-    let person_id = local_user_view.person.id;
-    let post_id = data.post_id;
-    let post_view = blocking(context.pool(), move |conn| {
-      PostView::read(conn, post_id, None)
-    })
-    .await??;
-
-    check_community_ban(person_id, post_view.community.id, context.pool()).await?;
-
-    let report_form = PostReportForm {
-      creator_id: person_id,
-      post_id,
-      original_post_name: post_view.post.name,
-      original_post_url: post_view.post.url,
-      original_post_body: post_view.post.body,
-      reason: data.reason.to_owned(),
-    };
-
-    let report = blocking(context.pool(), move |conn| {
-      PostReport::report(conn, &report_form)
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?;
-
-    let post_report_view = blocking(context.pool(), move |conn| {
-      PostReportView::read(conn, report.id, person_id)
-    })
-    .await??;
-
-    let res = PostReportResponse { post_report_view };
-
-    context.chat_server().do_send(SendModRoomMessage {
-      op: UserOperation::CreatePostReport,
-      response: res.clone(),
-      community_id: post_view.community.id,
-      websocket_id,
-    });
-
-    Report::send(
-      ObjectId::new(post_view.post.ap_id),
-      &local_user_view.person.into(),
-      ObjectId::new(post_view.community.actor_id),
-      reason.to_string(),
-      context,
-    )
-    .await?;
-
-    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 = PostReportResponse;
-
-  #[tracing::instrument(skip(context, websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostReportResponse, LemmyError> {
-    let data: &ResolvePostReport = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let report_id = data.report_id;
-    let person_id = local_user_view.person.id;
-    let report = blocking(context.pool(), move |conn| {
-      PostReportView::read(conn, report_id, person_id)
-    })
-    .await??;
-
-    let person_id = local_user_view.person.id;
-    is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
-
-    let resolved = data.resolved;
-    let resolve_fun = move |conn: &'_ _| {
-      if resolved {
-        PostReport::resolve(conn, report_id, person_id)
-      } else {
-        PostReport::unresolve(conn, report_id, person_id)
-      }
-    };
-
-    blocking(context.pool(), resolve_fun)
-      .await?
-      .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?;
-
-    let post_report_view = blocking(context.pool(), move |conn| {
-      PostReportView::read(conn, report_id, person_id)
-    })
-    .await??;
-
-    let res = PostReportResponse { post_report_view };
-
-    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;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<ListPostReportsResponse, LemmyError> {
-    let data: &ListPostReports = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let person_id = local_user_view.person.id;
-    let admin = local_user_view.person.admin;
-    let community_id = data.community_id;
-    let unresolved_only = data.unresolved_only;
-
-    let page = data.page;
-    let limit = data.limit;
-    let post_reports = blocking(context.pool(), move |conn| {
-      PostReportQueryBuilder::create(conn, person_id, admin)
-        .community_id(community_id)
-        .unresolved_only(unresolved_only)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    let res = ListPostReportsResponse { post_reports };
-
-    Ok(res)
-  }
-}
diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs
new file mode 100644 (file)
index 0000000..a85aa9a
--- /dev/null
@@ -0,0 +1,92 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  get_local_user_view_from_jwt,
+  post::{CreatePostReport, PostReportResponse},
+};
+use lemmy_apub::protocol::activities::community::report::Report;
+use lemmy_apub_lib::object_id::ObjectId;
+use lemmy_db_schema::{
+  source::post_report::{PostReport, PostReportForm},
+  traits::Reportable,
+};
+use lemmy_db_views::{post_report_view::PostReportView, post_view::PostView};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation};
+
+/// Creates a post report and notifies the moderators of the community
+#[async_trait::async_trait(?Send)]
+impl Perform for CreatePostReport {
+  type Response = PostReportResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostReportResponse, LemmyError> {
+    let data: &CreatePostReport = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // check size of report and check for whitespace
+    let reason = data.reason.trim();
+    if reason.is_empty() {
+      return Err(LemmyError::from_message("report_reason_required"));
+    }
+    if reason.chars().count() > 1000 {
+      return Err(LemmyError::from_message("report_too_long"));
+    }
+
+    let person_id = local_user_view.person.id;
+    let post_id = data.post_id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, post_id, None)
+    })
+    .await??;
+
+    check_community_ban(person_id, post_view.community.id, context.pool()).await?;
+
+    let report_form = PostReportForm {
+      creator_id: person_id,
+      post_id,
+      original_post_name: post_view.post.name,
+      original_post_url: post_view.post.url,
+      original_post_body: post_view.post.body,
+      reason: data.reason.to_owned(),
+    };
+
+    let report = blocking(context.pool(), move |conn| {
+      PostReport::report(conn, &report_form)
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?;
+
+    let post_report_view = blocking(context.pool(), move |conn| {
+      PostReportView::read(conn, report.id, person_id)
+    })
+    .await??;
+
+    let res = PostReportResponse { post_report_view };
+
+    context.chat_server().do_send(SendModRoomMessage {
+      op: UserOperation::CreatePostReport,
+      response: res.clone(),
+      community_id: post_view.community.id,
+      websocket_id,
+    });
+
+    Report::send(
+      ObjectId::new(post_view.post.ap_id),
+      &local_user_view.person.into(),
+      ObjectId::new(post_view.community.actor_id),
+      reason.to_string(),
+      context,
+    )
+    .await?;
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/post_report/list.rs b/crates/api/src/post_report/list.rs
new file mode 100644 (file)
index 0000000..7dfbb10
--- /dev/null
@@ -0,0 +1,49 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  post::{ListPostReports, ListPostReportsResponse},
+};
+use lemmy_db_views::post_report_view::PostReportQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+/// 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;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<ListPostReportsResponse, LemmyError> {
+    let data: &ListPostReports = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let person_id = local_user_view.person.id;
+    let admin = local_user_view.person.admin;
+    let community_id = data.community_id;
+    let unresolved_only = data.unresolved_only;
+
+    let page = data.page;
+    let limit = data.limit;
+    let post_reports = blocking(context.pool(), move |conn| {
+      PostReportQueryBuilder::create(conn, person_id, admin)
+        .community_id(community_id)
+        .unresolved_only(unresolved_only)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    let res = ListPostReportsResponse { post_reports };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/post_report/mod.rs b/crates/api/src/post_report/mod.rs
new file mode 100644 (file)
index 0000000..375fde4
--- /dev/null
@@ -0,0 +1,3 @@
+mod create;
+mod list;
+mod resolve;
diff --git a/crates/api/src/post_report/resolve.rs b/crates/api/src/post_report/resolve.rs
new file mode 100644 (file)
index 0000000..f0eb3fd
--- /dev/null
@@ -0,0 +1,68 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+  post::{PostReportResponse, ResolvePostReport},
+};
+use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable};
+use lemmy_db_views::post_report_view::PostReportView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation};
+
+/// Resolves or unresolves a post report and notifies the moderators of the community
+#[async_trait::async_trait(?Send)]
+impl Perform for ResolvePostReport {
+  type Response = PostReportResponse;
+
+  #[tracing::instrument(skip(context, websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostReportResponse, LemmyError> {
+    let data: &ResolvePostReport = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let report_id = data.report_id;
+    let person_id = local_user_view.person.id;
+    let report = blocking(context.pool(), move |conn| {
+      PostReportView::read(conn, report_id, person_id)
+    })
+    .await??;
+
+    let person_id = local_user_view.person.id;
+    is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
+
+    let resolved = data.resolved;
+    let resolve_fun = move |conn: &'_ _| {
+      if resolved {
+        PostReport::resolve(conn, report_id, person_id)
+      } else {
+        PostReport::unresolve(conn, report_id, person_id)
+      }
+    };
+
+    blocking(context.pool(), resolve_fun)
+      .await?
+      .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?;
+
+    let post_report_view = blocking(context.pool(), move |conn| {
+      PostReportView::read(conn, report_id, person_id)
+    })
+    .await??;
+
+    let res = PostReportResponse { post_report_view };
+
+    context.chat_server().do_send(SendModRoomMessage {
+      op: UserOperation::ResolvePostReport,
+      response: res.clone(),
+      community_id: report.community.id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/private_message/mod.rs b/crates/api/src/private_message/mod.rs
new file mode 100644 (file)
index 0000000..ce8c624
--- /dev/null
@@ -0,0 +1 @@
+mod mark_read;
diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs
deleted file mode 100644 (file)
index 54b7e0c..0000000
+++ /dev/null
@@ -1,710 +0,0 @@
-use crate::Perform;
-use actix_web::web::Data;
-use diesel::NotFound;
-use lemmy_api_common::{
-  blocking,
-  build_federated_instances,
-  check_private_instance,
-  get_local_user_view_from_jwt,
-  get_local_user_view_from_jwt_opt,
-  is_admin,
-  send_application_approved_email,
-  site::*,
-};
-use lemmy_apub::{
-  fetcher::{
-    resolve_actor_identifier,
-    search::{search_by_apub_id, SearchableObjects},
-  },
-  objects::community::ApubCommunity,
-};
-use lemmy_db_schema::{
-  diesel_option_overwrite,
-  from_opt_str_to_opt_enum,
-  newtypes::PersonId,
-  source::{
-    community::Community,
-    local_user::{LocalUser, LocalUserForm},
-    moderator::*,
-    person::Person,
-    registration_application::{RegistrationApplication, RegistrationApplicationForm},
-    site::Site,
-  },
-  traits::{Crud, DeleteableOrRemoveable},
-  DbPool,
-  ListingType,
-  SearchType,
-  SortType,
-};
-use lemmy_db_views::{
-  comment_view::{CommentQueryBuilder, CommentView},
-  local_user_view::LocalUserView,
-  post_view::{PostQueryBuilder, PostView},
-  registration_application_view::{
-    RegistrationApplicationQueryBuilder,
-    RegistrationApplicationView,
-  },
-  site_view::SiteView,
-};
-use lemmy_db_views_actor::{
-  community_view::{CommunityQueryBuilder, CommunityView},
-  person_view::{PersonQueryBuilder, PersonViewSafe},
-};
-use lemmy_db_views_moderator::{
-  mod_add_community_view::ModAddCommunityView,
-  mod_add_view::ModAddView,
-  mod_ban_from_community_view::ModBanFromCommunityView,
-  mod_ban_view::ModBanView,
-  mod_hide_community_view::ModHideCommunityView,
-  mod_lock_post_view::ModLockPostView,
-  mod_remove_comment_view::ModRemoveCommentView,
-  mod_remove_community_view::ModRemoveCommunityView,
-  mod_remove_post_view::ModRemovePostView,
-  mod_sticky_post_view::ModStickyPostView,
-  mod_transfer_community_view::ModTransferCommunityView,
-};
-use lemmy_utils::{settings::structs::Settings, version, ConnectionId, LemmyError};
-use lemmy_websocket::LemmyContext;
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetModlog {
-  type Response = GetModlogResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetModlogResponse, LemmyError> {
-    let data: &GetModlog = self;
-
-    let local_user_view =
-      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
-        .await?;
-
-    check_private_instance(&local_user_view, context.pool()).await?;
-
-    let community_id = data.community_id;
-    let mod_person_id = data.mod_person_id;
-    let page = data.page;
-    let limit = data.limit;
-    let removed_posts = blocking(context.pool(), move |conn| {
-      ModRemovePostView::list(conn, community_id, mod_person_id, page, limit)
-    })
-    .await??;
-
-    let locked_posts = blocking(context.pool(), move |conn| {
-      ModLockPostView::list(conn, community_id, mod_person_id, page, limit)
-    })
-    .await??;
-
-    let stickied_posts = blocking(context.pool(), move |conn| {
-      ModStickyPostView::list(conn, community_id, mod_person_id, page, limit)
-    })
-    .await??;
-
-    let removed_comments = blocking(context.pool(), move |conn| {
-      ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit)
-    })
-    .await??;
-
-    let banned_from_community = blocking(context.pool(), move |conn| {
-      ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit)
-    })
-    .await??;
-
-    let added_to_community = blocking(context.pool(), move |conn| {
-      ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
-    })
-    .await??;
-
-    let transferred_to_community = blocking(context.pool(), move |conn| {
-      ModTransferCommunityView::list(conn, community_id, mod_person_id, page, limit)
-    })
-    .await??;
-
-    let hidden_communities = blocking(context.pool(), move |conn| {
-      ModHideCommunityView::list(conn, community_id, mod_person_id, page, limit)
-    })
-    .await??;
-
-    // These arrays are only for the full modlog, when a community isn't given
-    let (removed_communities, banned, added) = if data.community_id.is_none() {
-      blocking(context.pool(), move |conn| {
-        Ok((
-          ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
-          ModBanView::list(conn, mod_person_id, page, limit)?,
-          ModAddView::list(conn, mod_person_id, page, limit)?,
-        )) as Result<_, LemmyError>
-      })
-      .await??
-    } else {
-      (Vec::new(), Vec::new(), Vec::new())
-    };
-
-    // Return the jwt
-    Ok(GetModlogResponse {
-      removed_posts,
-      locked_posts,
-      stickied_posts,
-      removed_comments,
-      removed_communities,
-      banned_from_community,
-      banned,
-      added_to_community,
-      added,
-      transferred_to_community,
-      hidden_communities,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for Search {
-  type Response = SearchResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<SearchResponse, LemmyError> {
-    let data: &Search = self;
-
-    let local_user_view =
-      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
-        .await?;
-
-    check_private_instance(&local_user_view, context.pool()).await?;
-
-    let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
-    let show_bot_accounts = local_user_view
-      .as_ref()
-      .map(|t| t.local_user.show_bot_accounts);
-    let show_read_posts = local_user_view
-      .as_ref()
-      .map(|t| t.local_user.show_read_posts);
-
-    let person_id = local_user_view.map(|u| u.person.id);
-
-    let mut posts = Vec::new();
-    let mut comments = Vec::new();
-    let mut communities = Vec::new();
-    let mut users = Vec::new();
-
-    // TODO no clean / non-nsfw searching rn
-
-    let q = data.q.to_owned();
-    let page = data.page;
-    let limit = data.limit;
-    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
-    let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.listing_type);
-    let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All);
-    let community_id = data.community_id;
-    let community_actor_id = if let Some(name) = &data.community_name {
-      resolve_actor_identifier::<ApubCommunity, Community>(name, context)
-        .await
-        .ok()
-        .map(|c| c.actor_id)
-    } else {
-      None
-    };
-    let creator_id = data.creator_id;
-    match search_type {
-      SearchType::Posts => {
-        posts = blocking(context.pool(), move |conn| {
-          PostQueryBuilder::create(conn)
-            .sort(sort)
-            .show_nsfw(show_nsfw)
-            .show_bot_accounts(show_bot_accounts)
-            .show_read_posts(show_read_posts)
-            .listing_type(listing_type)
-            .community_id(community_id)
-            .community_actor_id(community_actor_id)
-            .creator_id(creator_id)
-            .my_person_id(person_id)
-            .search_term(q)
-            .page(page)
-            .limit(limit)
-            .list()
-        })
-        .await??;
-      }
-      SearchType::Comments => {
-        comments = blocking(context.pool(), move |conn| {
-          CommentQueryBuilder::create(conn)
-            .sort(sort)
-            .listing_type(listing_type)
-            .search_term(q)
-            .show_bot_accounts(show_bot_accounts)
-            .community_id(community_id)
-            .community_actor_id(community_actor_id)
-            .creator_id(creator_id)
-            .my_person_id(person_id)
-            .page(page)
-            .limit(limit)
-            .list()
-        })
-        .await??;
-      }
-      SearchType::Communities => {
-        communities = blocking(context.pool(), move |conn| {
-          CommunityQueryBuilder::create(conn)
-            .sort(sort)
-            .listing_type(listing_type)
-            .search_term(q)
-            .my_person_id(person_id)
-            .page(page)
-            .limit(limit)
-            .list()
-        })
-        .await??;
-      }
-      SearchType::Users => {
-        users = blocking(context.pool(), move |conn| {
-          PersonQueryBuilder::create(conn)
-            .sort(sort)
-            .search_term(q)
-            .page(page)
-            .limit(limit)
-            .list()
-        })
-        .await??;
-      }
-      SearchType::All => {
-        // If the community or creator is included, dont search communities or users
-        let community_or_creator_included =
-          data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some();
-        let community_actor_id_2 = community_actor_id.to_owned();
-
-        posts = blocking(context.pool(), move |conn| {
-          PostQueryBuilder::create(conn)
-            .sort(sort)
-            .show_nsfw(show_nsfw)
-            .show_bot_accounts(show_bot_accounts)
-            .show_read_posts(show_read_posts)
-            .listing_type(listing_type)
-            .community_id(community_id)
-            .community_actor_id(community_actor_id_2)
-            .creator_id(creator_id)
-            .my_person_id(person_id)
-            .search_term(q)
-            .page(page)
-            .limit(limit)
-            .list()
-        })
-        .await??;
-
-        let q = data.q.to_owned();
-        let community_actor_id = community_actor_id.to_owned();
-
-        comments = blocking(context.pool(), move |conn| {
-          CommentQueryBuilder::create(conn)
-            .sort(sort)
-            .listing_type(listing_type)
-            .search_term(q)
-            .show_bot_accounts(show_bot_accounts)
-            .community_id(community_id)
-            .community_actor_id(community_actor_id)
-            .creator_id(creator_id)
-            .my_person_id(person_id)
-            .page(page)
-            .limit(limit)
-            .list()
-        })
-        .await??;
-
-        let q = data.q.to_owned();
-
-        communities = if community_or_creator_included {
-          vec![]
-        } else {
-          blocking(context.pool(), move |conn| {
-            CommunityQueryBuilder::create(conn)
-              .sort(sort)
-              .listing_type(listing_type)
-              .search_term(q)
-              .my_person_id(person_id)
-              .page(page)
-              .limit(limit)
-              .list()
-          })
-          .await??
-        };
-
-        let q = data.q.to_owned();
-
-        users = if community_or_creator_included {
-          vec![]
-        } else {
-          blocking(context.pool(), move |conn| {
-            PersonQueryBuilder::create(conn)
-              .sort(sort)
-              .search_term(q)
-              .page(page)
-              .limit(limit)
-              .list()
-          })
-          .await??
-        };
-      }
-      SearchType::Url => {
-        posts = blocking(context.pool(), move |conn| {
-          PostQueryBuilder::create(conn)
-            .sort(sort)
-            .show_nsfw(show_nsfw)
-            .show_bot_accounts(show_bot_accounts)
-            .show_read_posts(show_read_posts)
-            .listing_type(listing_type)
-            .my_person_id(person_id)
-            .community_id(community_id)
-            .community_actor_id(community_actor_id)
-            .creator_id(creator_id)
-            .url_search(q)
-            .page(page)
-            .limit(limit)
-            .list()
-        })
-        .await??;
-      }
-    };
-
-    // Blank out deleted or removed info for non logged in users
-    if person_id.is_none() {
-      for cv in communities
-        .iter_mut()
-        .filter(|cv| cv.community.deleted || cv.community.removed)
-      {
-        cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
-      }
-
-      for pv in posts
-        .iter_mut()
-        .filter(|p| p.post.deleted || p.post.removed)
-      {
-        pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
-      }
-
-      for cv in comments
-        .iter_mut()
-        .filter(|cv| cv.comment.deleted || cv.comment.removed)
-      {
-        cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
-      }
-    }
-
-    // Return the jwt
-    Ok(SearchResponse {
-      type_: search_type.to_string(),
-      comments,
-      posts,
-      communities,
-      users,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for ResolveObject {
-  type Response = ResolveObjectResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<ResolveObjectResponse, LemmyError> {
-    let local_user_view =
-      get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret())
-        .await?;
-    check_private_instance(&local_user_view, context.pool()).await?;
-
-    let res = search_by_apub_id(&self.q, context)
-      .await
-      .map_err(|e| e.with_message("couldnt_find_object"))?;
-    convert_response(res, local_user_view.map(|l| l.person.id), context.pool())
-      .await
-      .map_err(|e| e.with_message("couldnt_find_object"))
-  }
-}
-
-async fn convert_response(
-  object: SearchableObjects,
-  user_id: Option<PersonId>,
-  pool: &DbPool,
-) -> Result<ResolveObjectResponse, LemmyError> {
-  let removed_or_deleted;
-  let mut res = ResolveObjectResponse {
-    comment: None,
-    post: None,
-    community: None,
-    person: None,
-  };
-  use SearchableObjects::*;
-  match object {
-    Person(p) => {
-      removed_or_deleted = p.deleted;
-      res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??)
-    }
-    Community(c) => {
-      removed_or_deleted = c.deleted || c.removed;
-      res.community =
-        Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??)
-    }
-    Post(p) => {
-      removed_or_deleted = p.deleted || p.removed;
-      res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??)
-    }
-    Comment(c) => {
-      removed_or_deleted = c.deleted || c.removed;
-      res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??)
-    }
-  };
-  // if the object was deleted from database, dont return it
-  if removed_or_deleted {
-    return Err(NotFound {}.into());
-  }
-  Ok(res)
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for LeaveAdmin {
-  type Response = GetSiteResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetSiteResponse, LemmyError> {
-    let data: &LeaveAdmin = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    is_admin(&local_user_view)?;
-
-    // Make sure there isn't just one admin (so if one leaves, there will still be one left)
-    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
-    if admins.len() == 1 {
-      return Err(LemmyError::from_message("cannot_leave_admin"));
-    }
-
-    let person_id = local_user_view.person.id;
-    blocking(context.pool(), move |conn| {
-      Person::leave_admin(conn, person_id)
-    })
-    .await??;
-
-    // Mod tables
-    let form = ModAddForm {
-      mod_person_id: person_id,
-      other_person_id: person_id,
-      removed: Some(true),
-    };
-
-    blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
-
-    // Reread site and admins
-    let site_view = blocking(context.pool(), SiteView::read_local).await??;
-    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
-
-    let federated_instances =
-      build_federated_instances(context.pool(), &context.settings()).await?;
-
-    Ok(GetSiteResponse {
-      site_view: Some(site_view),
-      admins,
-      online: 0,
-      version: version::VERSION.to_string(),
-      my_user: None,
-      federated_instances,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetSiteConfig {
-  type Response = GetSiteConfigResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetSiteConfigResponse, LemmyError> {
-    let data: &GetSiteConfig = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // Only let admins read this
-    is_admin(&local_user_view)?;
-
-    let config_hjson = Settings::read_config_file()?;
-
-    Ok(GetSiteConfigResponse { config_hjson })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for SaveSiteConfig {
-  type Response = GetSiteConfigResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetSiteConfigResponse, LemmyError> {
-    let data: &SaveSiteConfig = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // Only let admins read this
-    is_admin(&local_user_view)?;
-
-    // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
-    let config_hjson = Settings::save_config_file(&data.config_hjson)
-      .map_err(|e| e.with_message("couldnt_update_site"))?;
-
-    Ok(GetSiteConfigResponse { config_hjson })
-  }
-}
-
-/// Lists registration applications, filterable by undenied only.
-#[async_trait::async_trait(?Send)]
-impl Perform for ListRegistrationApplications {
-  type Response = ListRegistrationApplicationsResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<Self::Response, LemmyError> {
-    let data = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
-
-    let unread_only = data.unread_only;
-    let verified_email_only = blocking(context.pool(), Site::read_local_site)
-      .await??
-      .require_email_verification;
-
-    let page = data.page;
-    let limit = data.limit;
-    let registration_applications = blocking(context.pool(), move |conn| {
-      RegistrationApplicationQueryBuilder::create(conn)
-        .unread_only(unread_only)
-        .verified_email_only(verified_email_only)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    let res = Self::Response {
-      registration_applications,
-    };
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for ApproveRegistrationApplication {
-  type Response = RegistrationApplicationResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<Self::Response, LemmyError> {
-    let data = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    let app_id = data.id;
-
-    // Only let admins do this
-    is_admin(&local_user_view)?;
-
-    // Update the registration with reason, admin_id
-    let deny_reason = diesel_option_overwrite(&data.deny_reason);
-    let app_form = RegistrationApplicationForm {
-      admin_id: Some(local_user_view.person.id),
-      deny_reason,
-      ..RegistrationApplicationForm::default()
-    };
-
-    let registration_application = blocking(context.pool(), move |conn| {
-      RegistrationApplication::update(conn, app_id, &app_form)
-    })
-    .await??;
-
-    // Update the local_user row
-    let local_user_form = LocalUserForm {
-      accepted_application: Some(data.approve),
-      ..LocalUserForm::default()
-    };
-
-    let approved_user_id = registration_application.local_user_id;
-    blocking(context.pool(), move |conn| {
-      LocalUser::update(conn, approved_user_id, &local_user_form)
-    })
-    .await??;
-
-    if data.approve {
-      let approved_local_user_view = blocking(context.pool(), move |conn| {
-        LocalUserView::read(conn, approved_user_id)
-      })
-      .await??;
-
-      if approved_local_user_view.local_user.email.is_some() {
-        send_application_approved_email(&approved_local_user_view, &context.settings())?;
-      }
-    }
-
-    // Read the view
-    let registration_application = blocking(context.pool(), move |conn| {
-      RegistrationApplicationView::read(conn, app_id)
-    })
-    .await??;
-
-    Ok(Self::Response {
-      registration_application,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetUnreadRegistrationApplicationCount {
-  type Response = GetUnreadRegistrationApplicationCountResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<Self::Response, LemmyError> {
-    let data = self;
-    let local_user_view =
-      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
-
-    // Only let admins do this
-    is_admin(&local_user_view)?;
-
-    let verified_email_only = blocking(context.pool(), Site::read_local_site)
-      .await??
-      .require_email_verification;
-
-    let registration_applications = blocking(context.pool(), move |conn| {
-      RegistrationApplicationView::get_unread_count(conn, verified_email_only)
-    })
-    .await??;
-
-    Ok(Self::Response {
-      registration_applications,
-    })
-  }
-}
diff --git a/crates/api/src/site/config/mod.rs b/crates/api/src/site/config/mod.rs
new file mode 100644 (file)
index 0000000..d538ff2
--- /dev/null
@@ -0,0 +1,2 @@
+mod read;
+mod update;
diff --git a/crates/api/src/site/config/read.rs b/crates/api/src/site/config/read.rs
new file mode 100644 (file)
index 0000000..76117d4
--- /dev/null
@@ -0,0 +1,32 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  get_local_user_view_from_jwt,
+  is_admin,
+  site::{GetSiteConfig, GetSiteConfigResponse},
+};
+use lemmy_utils::{settings::structs::Settings, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetSiteConfig {
+  type Response = GetSiteConfigResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetSiteConfigResponse, LemmyError> {
+    let data: &GetSiteConfig = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Only let admins read this
+    is_admin(&local_user_view)?;
+
+    let config_hjson = Settings::read_config_file()?;
+
+    Ok(GetSiteConfigResponse { config_hjson })
+  }
+}
diff --git a/crates/api/src/site/config/update.rs b/crates/api/src/site/config/update.rs
new file mode 100644 (file)
index 0000000..b36c69c
--- /dev/null
@@ -0,0 +1,34 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  get_local_user_view_from_jwt,
+  is_admin,
+  site::{GetSiteConfigResponse, SaveSiteConfig},
+};
+use lemmy_utils::{settings::structs::Settings, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for SaveSiteConfig {
+  type Response = GetSiteConfigResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetSiteConfigResponse, LemmyError> {
+    let data: &SaveSiteConfig = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Only let admins read this
+    is_admin(&local_user_view)?;
+
+    // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
+    let config_hjson = Settings::save_config_file(&data.config_hjson)
+      .map_err(|e| e.with_message("couldnt_update_site"))?;
+
+    Ok(GetSiteConfigResponse { config_hjson })
+  }
+}
diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs
new file mode 100644 (file)
index 0000000..ee714e8
--- /dev/null
@@ -0,0 +1,75 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  build_federated_instances,
+  get_local_user_view_from_jwt,
+  is_admin,
+  site::{GetSiteResponse, LeaveAdmin},
+};
+use lemmy_db_schema::{
+  source::{
+    moderator::{ModAdd, ModAddForm},
+    person::Person,
+  },
+  traits::Crud,
+};
+use lemmy_db_views::site_view::SiteView;
+use lemmy_db_views_actor::person_view::PersonViewSafe;
+use lemmy_utils::{version, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for LeaveAdmin {
+  type Response = GetSiteResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetSiteResponse, LemmyError> {
+    let data: &LeaveAdmin = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    is_admin(&local_user_view)?;
+
+    // Make sure there isn't just one admin (so if one leaves, there will still be one left)
+    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
+    if admins.len() == 1 {
+      return Err(LemmyError::from_message("cannot_leave_admin"));
+    }
+
+    let person_id = local_user_view.person.id;
+    blocking(context.pool(), move |conn| {
+      Person::leave_admin(conn, person_id)
+    })
+    .await??;
+
+    // Mod tables
+    let form = ModAddForm {
+      mod_person_id: person_id,
+      other_person_id: person_id,
+      removed: Some(true),
+    };
+
+    blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
+
+    // Reread site and admins
+    let site_view = blocking(context.pool(), SiteView::read_local).await??;
+    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
+
+    let federated_instances =
+      build_federated_instances(context.pool(), &context.settings()).await?;
+
+    Ok(GetSiteResponse {
+      site_view: Some(site_view),
+      admins,
+      online: 0,
+      version: version::VERSION.to_string(),
+      my_user: None,
+      federated_instances,
+    })
+  }
+}
diff --git a/crates/api/src/site/mod.rs b/crates/api/src/site/mod.rs
new file mode 100644 (file)
index 0000000..b8b9dd7
--- /dev/null
@@ -0,0 +1,6 @@
+mod config;
+mod leave_admin;
+mod mod_log;
+mod registration_applications;
+mod resolve_object;
+mod search;
diff --git a/crates/api/src/site/mod_log.rs b/crates/api/src/site/mod_log.rs
new file mode 100644 (file)
index 0000000..6dd06c1
--- /dev/null
@@ -0,0 +1,116 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  get_local_user_view_from_jwt_opt,
+  site::{GetModlog, GetModlogResponse},
+};
+use lemmy_db_views_moderator::{
+  mod_add_community_view::ModAddCommunityView,
+  mod_add_view::ModAddView,
+  mod_ban_from_community_view::ModBanFromCommunityView,
+  mod_ban_view::ModBanView,
+  mod_hide_community_view::ModHideCommunityView,
+  mod_lock_post_view::ModLockPostView,
+  mod_remove_comment_view::ModRemoveCommentView,
+  mod_remove_community_view::ModRemoveCommunityView,
+  mod_remove_post_view::ModRemovePostView,
+  mod_sticky_post_view::ModStickyPostView,
+  mod_transfer_community_view::ModTransferCommunityView,
+};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetModlog {
+  type Response = GetModlogResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetModlogResponse, LemmyError> {
+    let data: &GetModlog = self;
+
+    let local_user_view =
+      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
+        .await?;
+
+    check_private_instance(&local_user_view, context.pool()).await?;
+
+    let community_id = data.community_id;
+    let mod_person_id = data.mod_person_id;
+    let page = data.page;
+    let limit = data.limit;
+    let removed_posts = blocking(context.pool(), move |conn| {
+      ModRemovePostView::list(conn, community_id, mod_person_id, page, limit)
+    })
+    .await??;
+
+    let locked_posts = blocking(context.pool(), move |conn| {
+      ModLockPostView::list(conn, community_id, mod_person_id, page, limit)
+    })
+    .await??;
+
+    let stickied_posts = blocking(context.pool(), move |conn| {
+      ModStickyPostView::list(conn, community_id, mod_person_id, page, limit)
+    })
+    .await??;
+
+    let removed_comments = blocking(context.pool(), move |conn| {
+      ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit)
+    })
+    .await??;
+
+    let banned_from_community = blocking(context.pool(), move |conn| {
+      ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit)
+    })
+    .await??;
+
+    let added_to_community = blocking(context.pool(), move |conn| {
+      ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
+    })
+    .await??;
+
+    let transferred_to_community = blocking(context.pool(), move |conn| {
+      ModTransferCommunityView::list(conn, community_id, mod_person_id, page, limit)
+    })
+    .await??;
+
+    let hidden_communities = blocking(context.pool(), move |conn| {
+      ModHideCommunityView::list(conn, community_id, mod_person_id, page, limit)
+    })
+    .await??;
+
+    // These arrays are only for the full modlog, when a community isn't given
+    let (removed_communities, banned, added) = if data.community_id.is_none() {
+      blocking(context.pool(), move |conn| {
+        Ok((
+          ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
+          ModBanView::list(conn, mod_person_id, page, limit)?,
+          ModAddView::list(conn, mod_person_id, page, limit)?,
+        )) as Result<_, LemmyError>
+      })
+      .await??
+    } else {
+      (Vec::new(), Vec::new(), Vec::new())
+    };
+
+    // Return the jwt
+    Ok(GetModlogResponse {
+      removed_posts,
+      locked_posts,
+      stickied_posts,
+      removed_comments,
+      removed_communities,
+      banned_from_community,
+      banned,
+      added_to_community,
+      added,
+      transferred_to_community,
+      hidden_communities,
+    })
+  }
+}
diff --git a/crates/api/src/site/registration_applications/approve.rs b/crates/api/src/site/registration_applications/approve.rs
new file mode 100644 (file)
index 0000000..238da7e
--- /dev/null
@@ -0,0 +1,89 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  is_admin,
+  send_application_approved_email,
+  site::*,
+};
+use lemmy_db_schema::{
+  diesel_option_overwrite,
+  source::{
+    local_user::{LocalUser, LocalUserForm},
+    registration_application::{RegistrationApplication, RegistrationApplicationForm},
+  },
+  traits::Crud,
+};
+use lemmy_db_views::{
+  local_user_view::LocalUserView,
+  registration_application_view::RegistrationApplicationView,
+};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for ApproveRegistrationApplication {
+  type Response = RegistrationApplicationResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let app_id = data.id;
+
+    // Only let admins do this
+    is_admin(&local_user_view)?;
+
+    // Update the registration with reason, admin_id
+    let deny_reason = diesel_option_overwrite(&data.deny_reason);
+    let app_form = RegistrationApplicationForm {
+      admin_id: Some(local_user_view.person.id),
+      deny_reason,
+      ..RegistrationApplicationForm::default()
+    };
+
+    let registration_application = blocking(context.pool(), move |conn| {
+      RegistrationApplication::update(conn, app_id, &app_form)
+    })
+    .await??;
+
+    // Update the local_user row
+    let local_user_form = LocalUserForm {
+      accepted_application: Some(data.approve),
+      ..LocalUserForm::default()
+    };
+
+    let approved_user_id = registration_application.local_user_id;
+    blocking(context.pool(), move |conn| {
+      LocalUser::update(conn, approved_user_id, &local_user_form)
+    })
+    .await??;
+
+    if data.approve {
+      let approved_local_user_view = blocking(context.pool(), move |conn| {
+        LocalUserView::read(conn, approved_user_id)
+      })
+      .await??;
+
+      if approved_local_user_view.local_user.email.is_some() {
+        send_application_approved_email(&approved_local_user_view, &context.settings())?;
+      }
+    }
+
+    // Read the view
+    let registration_application = blocking(context.pool(), move |conn| {
+      RegistrationApplicationView::read(conn, app_id)
+    })
+    .await??;
+
+    Ok(Self::Response {
+      registration_application,
+    })
+  }
+}
diff --git a/crates/api/src/site/registration_applications/list.rs b/crates/api/src/site/registration_applications/list.rs
new file mode 100644 (file)
index 0000000..bd5170d
--- /dev/null
@@ -0,0 +1,54 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  is_admin,
+  site::{ListRegistrationApplications, ListRegistrationApplicationsResponse},
+};
+use lemmy_db_schema::source::site::Site;
+use lemmy_db_views::registration_application_view::RegistrationApplicationQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+/// Lists registration applications, filterable by undenied only.
+#[async_trait::async_trait(?Send)]
+impl Perform for ListRegistrationApplications {
+  type Response = ListRegistrationApplicationsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Make sure user is an admin
+    is_admin(&local_user_view)?;
+
+    let unread_only = data.unread_only;
+    let verified_email_only = blocking(context.pool(), Site::read_local_site)
+      .await??
+      .require_email_verification;
+
+    let page = data.page;
+    let limit = data.limit;
+    let registration_applications = blocking(context.pool(), move |conn| {
+      RegistrationApplicationQueryBuilder::create(conn)
+        .unread_only(unread_only)
+        .verified_email_only(verified_email_only)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    let res = Self::Response {
+      registration_applications,
+    };
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/site/registration_applications/mod.rs b/crates/api/src/site/registration_applications/mod.rs
new file mode 100644 (file)
index 0000000..0d330ba
--- /dev/null
@@ -0,0 +1,3 @@
+mod approve;
+mod list;
+mod unread_count;
diff --git a/crates/api/src/site/registration_applications/unread_count.rs b/crates/api/src/site/registration_applications/unread_count.rs
new file mode 100644 (file)
index 0000000..03584ba
--- /dev/null
@@ -0,0 +1,43 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  is_admin,
+  site::{GetUnreadRegistrationApplicationCount, GetUnreadRegistrationApplicationCountResponse},
+};
+use lemmy_db_schema::source::site::Site;
+use lemmy_db_views::registration_application_view::RegistrationApplicationView;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetUnreadRegistrationApplicationCount {
+  type Response = GetUnreadRegistrationApplicationCountResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Only let admins do this
+    is_admin(&local_user_view)?;
+
+    let verified_email_only = blocking(context.pool(), Site::read_local_site)
+      .await??
+      .require_email_verification;
+
+    let registration_applications = blocking(context.pool(), move |conn| {
+      RegistrationApplicationView::get_unread_count(conn, verified_email_only)
+    })
+    .await??;
+
+    Ok(Self::Response {
+      registration_applications,
+    })
+  }
+}
diff --git a/crates/api/src/site/resolve_object.rs b/crates/api/src/site/resolve_object.rs
new file mode 100644 (file)
index 0000000..87f3c85
--- /dev/null
@@ -0,0 +1,78 @@
+use crate::Perform;
+use actix_web::web::Data;
+use diesel::NotFound;
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  get_local_user_view_from_jwt_opt,
+  site::{ResolveObject, ResolveObjectResponse},
+};
+use lemmy_apub::fetcher::search::{search_by_apub_id, SearchableObjects};
+use lemmy_db_schema::{newtypes::PersonId, DbPool};
+use lemmy_db_views::{comment_view::CommentView, post_view::PostView};
+use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for ResolveObject {
+  type Response = ResolveObjectResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<ResolveObjectResponse, LemmyError> {
+    let local_user_view =
+      get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret())
+        .await?;
+    check_private_instance(&local_user_view, context.pool()).await?;
+
+    let res = search_by_apub_id(&self.q, context)
+      .await
+      .map_err(|e| e.with_message("couldnt_find_object"))?;
+    convert_response(res, local_user_view.map(|l| l.person.id), context.pool())
+      .await
+      .map_err(|e| e.with_message("couldnt_find_object"))
+  }
+}
+
+async fn convert_response(
+  object: SearchableObjects,
+  user_id: Option<PersonId>,
+  pool: &DbPool,
+) -> Result<ResolveObjectResponse, LemmyError> {
+  let removed_or_deleted;
+  let mut res = ResolveObjectResponse {
+    comment: None,
+    post: None,
+    community: None,
+    person: None,
+  };
+  use SearchableObjects::*;
+  match object {
+    Person(p) => {
+      removed_or_deleted = p.deleted;
+      res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??)
+    }
+    Community(c) => {
+      removed_or_deleted = c.deleted || c.removed;
+      res.community =
+        Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??)
+    }
+    Post(p) => {
+      removed_or_deleted = p.deleted || p.removed;
+      res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??)
+    }
+    Comment(c) => {
+      removed_or_deleted = c.deleted || c.removed;
+      res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??)
+    }
+  };
+  // if the object was deleted from database, dont return it
+  if removed_or_deleted {
+    return Err(NotFound {}.into());
+  }
+  Ok(res)
+}
diff --git a/crates/api/src/site/search.rs b/crates/api/src/site/search.rs
new file mode 100644 (file)
index 0000000..f4f998d
--- /dev/null
@@ -0,0 +1,269 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  get_local_user_view_from_jwt_opt,
+  site::{Search, SearchResponse},
+};
+use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
+use lemmy_db_schema::{
+  from_opt_str_to_opt_enum,
+  source::community::Community,
+  traits::DeleteableOrRemoveable,
+  ListingType,
+  SearchType,
+  SortType,
+};
+use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder};
+use lemmy_db_views_actor::{
+  community_view::CommunityQueryBuilder,
+  person_view::PersonQueryBuilder,
+};
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl Perform for Search {
+  type Response = SearchResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<SearchResponse, LemmyError> {
+    let data: &Search = self;
+
+    let local_user_view =
+      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
+        .await?;
+
+    check_private_instance(&local_user_view, context.pool()).await?;
+
+    let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
+    let show_bot_accounts = local_user_view
+      .as_ref()
+      .map(|t| t.local_user.show_bot_accounts);
+    let show_read_posts = local_user_view
+      .as_ref()
+      .map(|t| t.local_user.show_read_posts);
+
+    let person_id = local_user_view.map(|u| u.person.id);
+
+    let mut posts = Vec::new();
+    let mut comments = Vec::new();
+    let mut communities = Vec::new();
+    let mut users = Vec::new();
+
+    // TODO no clean / non-nsfw searching rn
+
+    let q = data.q.to_owned();
+    let page = data.page;
+    let limit = data.limit;
+    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
+    let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.listing_type);
+    let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All);
+    let community_id = data.community_id;
+    let community_actor_id = if let Some(name) = &data.community_name {
+      resolve_actor_identifier::<ApubCommunity, Community>(name, context)
+        .await
+        .ok()
+        .map(|c| c.actor_id)
+    } else {
+      None
+    };
+    let creator_id = data.creator_id;
+    match search_type {
+      SearchType::Posts => {
+        posts = blocking(context.pool(), move |conn| {
+          PostQueryBuilder::create(conn)
+            .sort(sort)
+            .show_nsfw(show_nsfw)
+            .show_bot_accounts(show_bot_accounts)
+            .show_read_posts(show_read_posts)
+            .listing_type(listing_type)
+            .community_id(community_id)
+            .community_actor_id(community_actor_id)
+            .creator_id(creator_id)
+            .my_person_id(person_id)
+            .search_term(q)
+            .page(page)
+            .limit(limit)
+            .list()
+        })
+        .await??;
+      }
+      SearchType::Comments => {
+        comments = blocking(context.pool(), move |conn| {
+          CommentQueryBuilder::create(conn)
+            .sort(sort)
+            .listing_type(listing_type)
+            .search_term(q)
+            .show_bot_accounts(show_bot_accounts)
+            .community_id(community_id)
+            .community_actor_id(community_actor_id)
+            .creator_id(creator_id)
+            .my_person_id(person_id)
+            .page(page)
+            .limit(limit)
+            .list()
+        })
+        .await??;
+      }
+      SearchType::Communities => {
+        communities = blocking(context.pool(), move |conn| {
+          CommunityQueryBuilder::create(conn)
+            .sort(sort)
+            .listing_type(listing_type)
+            .search_term(q)
+            .my_person_id(person_id)
+            .page(page)
+            .limit(limit)
+            .list()
+        })
+        .await??;
+      }
+      SearchType::Users => {
+        users = blocking(context.pool(), move |conn| {
+          PersonQueryBuilder::create(conn)
+            .sort(sort)
+            .search_term(q)
+            .page(page)
+            .limit(limit)
+            .list()
+        })
+        .await??;
+      }
+      SearchType::All => {
+        // If the community or creator is included, dont search communities or users
+        let community_or_creator_included =
+          data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some();
+        let community_actor_id_2 = community_actor_id.to_owned();
+
+        posts = blocking(context.pool(), move |conn| {
+          PostQueryBuilder::create(conn)
+            .sort(sort)
+            .show_nsfw(show_nsfw)
+            .show_bot_accounts(show_bot_accounts)
+            .show_read_posts(show_read_posts)
+            .listing_type(listing_type)
+            .community_id(community_id)
+            .community_actor_id(community_actor_id_2)
+            .creator_id(creator_id)
+            .my_person_id(person_id)
+            .search_term(q)
+            .page(page)
+            .limit(limit)
+            .list()
+        })
+        .await??;
+
+        let q = data.q.to_owned();
+        let community_actor_id = community_actor_id.to_owned();
+
+        comments = blocking(context.pool(), move |conn| {
+          CommentQueryBuilder::create(conn)
+            .sort(sort)
+            .listing_type(listing_type)
+            .search_term(q)
+            .show_bot_accounts(show_bot_accounts)
+            .community_id(community_id)
+            .community_actor_id(community_actor_id)
+            .creator_id(creator_id)
+            .my_person_id(person_id)
+            .page(page)
+            .limit(limit)
+            .list()
+        })
+        .await??;
+
+        let q = data.q.to_owned();
+
+        communities = if community_or_creator_included {
+          vec![]
+        } else {
+          blocking(context.pool(), move |conn| {
+            CommunityQueryBuilder::create(conn)
+              .sort(sort)
+              .listing_type(listing_type)
+              .search_term(q)
+              .my_person_id(person_id)
+              .page(page)
+              .limit(limit)
+              .list()
+          })
+          .await??
+        };
+
+        let q = data.q.to_owned();
+
+        users = if community_or_creator_included {
+          vec![]
+        } else {
+          blocking(context.pool(), move |conn| {
+            PersonQueryBuilder::create(conn)
+              .sort(sort)
+              .search_term(q)
+              .page(page)
+              .limit(limit)
+              .list()
+          })
+          .await??
+        };
+      }
+      SearchType::Url => {
+        posts = blocking(context.pool(), move |conn| {
+          PostQueryBuilder::create(conn)
+            .sort(sort)
+            .show_nsfw(show_nsfw)
+            .show_bot_accounts(show_bot_accounts)
+            .show_read_posts(show_read_posts)
+            .listing_type(listing_type)
+            .my_person_id(person_id)
+            .community_id(community_id)
+            .community_actor_id(community_actor_id)
+            .creator_id(creator_id)
+            .url_search(q)
+            .page(page)
+            .limit(limit)
+            .list()
+        })
+        .await??;
+      }
+    };
+
+    // Blank out deleted or removed info for non logged in users
+    if person_id.is_none() {
+      for cv in communities
+        .iter_mut()
+        .filter(|cv| cv.community.deleted || cv.community.removed)
+      {
+        cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
+      }
+
+      for pv in posts
+        .iter_mut()
+        .filter(|p| p.post.deleted || p.post.removed)
+      {
+        pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
+      }
+
+      for cv in comments
+        .iter_mut()
+        .filter(|cv| cv.comment.deleted || cv.comment.removed)
+      {
+        cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
+      }
+    }
+
+    // Return the jwt
+    Ok(SearchResponse {
+      type_: search_type.to_string(),
+      comments,
+      posts,
+      communities,
+      users,
+    })
+  }
+}
index 0b5da49fea2493f57beb6935a4db845aa2ff9daf..898efbf5d3dbf18b8eb223864a75538c7e36aa69 100644 (file)
@@ -222,7 +222,7 @@ pub struct PasswordReset {
 pub struct PasswordResetResponse {}
 
 #[derive(Debug, Serialize, Deserialize)]
-pub struct PasswordChange {
+pub struct PasswordChangeAfterReset {
   pub token: Sensitive<String>,
   pub password: Sensitive<String>,
   pub password_verify: Sensitive<String>,
diff --git a/crates/api_crud/src/comment/list.rs b/crates/api_crud/src/comment/list.rs
new file mode 100644 (file)
index 0000000..cd91a3b
--- /dev/null
@@ -0,0 +1,84 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  comment::*,
+  get_local_user_view_from_jwt_opt,
+};
+use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
+use lemmy_db_schema::{
+  from_opt_str_to_opt_enum,
+  source::community::Community,
+  traits::DeleteableOrRemoveable,
+  ListingType,
+  SortType,
+};
+use lemmy_db_views::comment_view::CommentQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetComments {
+  type Response = GetCommentsResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetCommentsResponse, LemmyError> {
+    let data: &GetComments = self;
+    let local_user_view =
+      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
+        .await?;
+
+    check_private_instance(&local_user_view, context.pool()).await?;
+
+    let show_bot_accounts = local_user_view
+      .as_ref()
+      .map(|t| t.local_user.show_bot_accounts);
+    let person_id = local_user_view.map(|u| u.person.id);
+
+    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
+    let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.type_);
+
+    let community_id = data.community_id;
+    let community_actor_id = if let Some(name) = &data.community_name {
+      resolve_actor_identifier::<ApubCommunity, Community>(name, context)
+        .await
+        .ok()
+        .map(|c| c.actor_id)
+    } else {
+      None
+    };
+    let saved_only = data.saved_only;
+    let page = data.page;
+    let limit = data.limit;
+    let mut comments = blocking(context.pool(), move |conn| {
+      CommentQueryBuilder::create(conn)
+        .listing_type(listing_type)
+        .sort(sort)
+        .saved_only(saved_only)
+        .community_id(community_id)
+        .community_actor_id(community_actor_id)
+        .my_person_id(person_id)
+        .show_bot_accounts(show_bot_accounts)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_get_comments"))?;
+
+    // Blank out deleted or removed info
+    for cv in comments
+      .iter_mut()
+      .filter(|cv| cv.comment.deleted || cv.comment.removed)
+    {
+      cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
+    }
+
+    Ok(GetCommentsResponse { comments })
+  }
+}
index 7168323761acf1865cb9f84c3cfba68be3ace373..7003bdd860e421f5daa5769ae538bbc5da3e74b2 100644 (file)
@@ -1,4 +1,5 @@
 mod create;
 mod delete;
+mod list;
 mod read;
 mod update;
index d04a0b45a82fbde06b1493c2e88392df35351f4a..5b2155da01880c84b6ea90a793ef9407efe9718f 100644 (file)
@@ -6,15 +6,7 @@ use lemmy_api_common::{
   comment::*,
   get_local_user_view_from_jwt_opt,
 };
-use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
-use lemmy_db_schema::{
-  from_opt_str_to_opt_enum,
-  source::community::Community,
-  traits::DeleteableOrRemoveable,
-  ListingType,
-  SortType,
-};
-use lemmy_db_views::comment_view::{CommentQueryBuilder, CommentView};
+use lemmy_db_views::comment_view::CommentView;
 use lemmy_utils::{ConnectionId, LemmyError};
 use lemmy_websocket::LemmyContext;
 
@@ -50,68 +42,3 @@ impl PerformCrud for GetComment {
     })
   }
 }
-
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for GetComments {
-  type Response = GetCommentsResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetCommentsResponse, LemmyError> {
-    let data: &GetComments = self;
-    let local_user_view =
-      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
-        .await?;
-
-    check_private_instance(&local_user_view, context.pool()).await?;
-
-    let show_bot_accounts = local_user_view
-      .as_ref()
-      .map(|t| t.local_user.show_bot_accounts);
-    let person_id = local_user_view.map(|u| u.person.id);
-
-    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
-    let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.type_);
-
-    let community_id = data.community_id;
-    let community_actor_id = if let Some(name) = &data.community_name {
-      resolve_actor_identifier::<ApubCommunity, Community>(name, context)
-        .await
-        .ok()
-        .map(|c| c.actor_id)
-    } else {
-      None
-    };
-    let saved_only = data.saved_only;
-    let page = data.page;
-    let limit = data.limit;
-    let mut comments = blocking(context.pool(), move |conn| {
-      CommentQueryBuilder::create(conn)
-        .listing_type(listing_type)
-        .sort(sort)
-        .saved_only(saved_only)
-        .community_id(community_id)
-        .community_actor_id(community_actor_id)
-        .my_person_id(person_id)
-        .show_bot_accounts(show_bot_accounts)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_get_comments"))?;
-
-    // Blank out deleted or removed info
-    for cv in comments
-      .iter_mut()
-      .filter(|cv| cv.comment.deleted || cv.comment.removed)
-    {
-      cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
-    }
-
-    Ok(GetCommentsResponse { comments })
-  }
-}
diff --git a/crates/api_crud/src/community/list.rs b/crates/api_crud/src/community/list.rs
new file mode 100644 (file)
index 0000000..24c154a
--- /dev/null
@@ -0,0 +1,74 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  community::*,
+  get_local_user_view_from_jwt_opt,
+};
+use lemmy_db_schema::{
+  from_opt_str_to_opt_enum,
+  traits::DeleteableOrRemoveable,
+  ListingType,
+  SortType,
+};
+use lemmy_db_views_actor::community_view::CommunityQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for ListCommunities {
+  type Response = ListCommunitiesResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<ListCommunitiesResponse, LemmyError> {
+    let data: &ListCommunities = self;
+    let local_user_view =
+      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
+        .await?;
+
+    check_private_instance(&local_user_view, context.pool()).await?;
+
+    let person_id = local_user_view.to_owned().map(|l| l.person.id);
+
+    // Don't show NSFW by default
+    let show_nsfw = match &local_user_view {
+      Some(uv) => uv.local_user.show_nsfw,
+      None => false,
+    };
+
+    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
+    let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.type_);
+
+    let page = data.page;
+    let limit = data.limit;
+    let mut communities = blocking(context.pool(), move |conn| {
+      CommunityQueryBuilder::create(conn)
+        .listing_type(listing_type)
+        .sort(sort)
+        .show_nsfw(show_nsfw)
+        .my_person_id(person_id)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    // Blank out deleted or removed info for non-logged in users
+    if person_id.is_none() {
+      for cv in communities
+        .iter_mut()
+        .filter(|cv| cv.community.deleted || cv.community.removed)
+      {
+        cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
+      }
+    }
+
+    // Return the jwt
+    Ok(ListCommunitiesResponse { communities })
+  }
+}
index 7168323761acf1865cb9f84c3cfba68be3ace373..7003bdd860e421f5daa5769ae538bbc5da3e74b2 100644 (file)
@@ -1,4 +1,5 @@
 mod create;
 mod delete;
+mod list;
 mod read;
 mod update;
index d4a28b7819231326450d9528ec7bfda8ed7b6cbe..9c4ba333cce55439e59d43c1eadd0b9234925d42 100644 (file)
@@ -11,15 +11,12 @@ use lemmy_apub::{
   objects::{community::ApubCommunity, instance::instance_actor_id_from_url},
 };
 use lemmy_db_schema::{
-  from_opt_str_to_opt_enum,
   source::{community::Community, site::Site},
   traits::DeleteableOrRemoveable,
-  ListingType,
-  SortType,
 };
 use lemmy_db_views_actor::{
   community_moderator_view::CommunityModeratorView,
-  community_view::{CommunityQueryBuilder, CommunityView},
+  community_view::CommunityView,
 };
 use lemmy_utils::{ConnectionId, LemmyError};
 use lemmy_websocket::{messages::GetCommunityUsersOnline, LemmyContext};
@@ -102,60 +99,3 @@ impl PerformCrud for GetCommunity {
     Ok(res)
   }
 }
-
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for ListCommunities {
-  type Response = ListCommunitiesResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<ListCommunitiesResponse, LemmyError> {
-    let data: &ListCommunities = self;
-    let local_user_view =
-      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
-        .await?;
-
-    check_private_instance(&local_user_view, context.pool()).await?;
-
-    let person_id = local_user_view.to_owned().map(|l| l.person.id);
-
-    // Don't show NSFW by default
-    let show_nsfw = match &local_user_view {
-      Some(uv) => uv.local_user.show_nsfw,
-      None => false,
-    };
-
-    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
-    let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.type_);
-
-    let page = data.page;
-    let limit = data.limit;
-    let mut communities = blocking(context.pool(), move |conn| {
-      CommunityQueryBuilder::create(conn)
-        .listing_type(listing_type)
-        .sort(sort)
-        .show_nsfw(show_nsfw)
-        .my_person_id(person_id)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    // Blank out deleted or removed info for non-logged in users
-    if person_id.is_none() {
-      for cv in communities
-        .iter_mut()
-        .filter(|cv| cv.community.deleted || cv.community.removed)
-      {
-        cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
-      }
-    }
-
-    // Return the jwt
-    Ok(ListCommunitiesResponse { communities })
-  }
-}
diff --git a/crates/api_crud/src/post/list.rs b/crates/api_crud/src/post/list.rs
new file mode 100644 (file)
index 0000000..8a714e9
--- /dev/null
@@ -0,0 +1,101 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  get_local_user_view_from_jwt_opt,
+  post::*,
+};
+use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
+use lemmy_db_schema::{
+  from_opt_str_to_opt_enum,
+  source::community::Community,
+  traits::DeleteableOrRemoveable,
+  ListingType,
+  SortType,
+};
+use lemmy_db_views::post_view::PostQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetPosts {
+  type Response = GetPostsResponse;
+
+  #[tracing::instrument(skip(context, _websocket_id))]
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetPostsResponse, LemmyError> {
+    let data: &GetPosts = self;
+    let local_user_view =
+      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
+        .await?;
+
+    check_private_instance(&local_user_view, context.pool()).await?;
+
+    let person_id = local_user_view.to_owned().map(|l| l.person.id);
+
+    let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
+    let show_bot_accounts = local_user_view
+      .as_ref()
+      .map(|t| t.local_user.show_bot_accounts);
+    let show_read_posts = local_user_view
+      .as_ref()
+      .map(|t| t.local_user.show_read_posts);
+
+    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
+    let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.type_);
+
+    let page = data.page;
+    let limit = data.limit;
+    let community_id = data.community_id;
+    let community_actor_id = if let Some(name) = &data.community_name {
+      resolve_actor_identifier::<ApubCommunity, Community>(name, context)
+        .await
+        .ok()
+        .map(|c| c.actor_id)
+    } else {
+      None
+    };
+    let saved_only = data.saved_only;
+
+    let mut posts = blocking(context.pool(), move |conn| {
+      PostQueryBuilder::create(conn)
+        .listing_type(listing_type)
+        .sort(sort)
+        .show_nsfw(show_nsfw)
+        .show_bot_accounts(show_bot_accounts)
+        .show_read_posts(show_read_posts)
+        .community_id(community_id)
+        .community_actor_id(community_actor_id)
+        .saved_only(saved_only)
+        .my_person_id(person_id)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await?
+    .map_err(|e| LemmyError::from_error_message(e, "couldnt_get_posts"))?;
+
+    // Blank out deleted or removed info for non-logged in users
+    if person_id.is_none() {
+      for pv in posts
+        .iter_mut()
+        .filter(|p| p.post.deleted || p.post.removed)
+      {
+        pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
+      }
+
+      for pv in posts
+        .iter_mut()
+        .filter(|p| p.community.deleted || p.community.removed)
+      {
+        pv.community = pv.to_owned().community.blank_out_deleted_or_removed_info();
+      }
+    }
+
+    Ok(GetPostsResponse { posts })
+  }
+}
index 7168323761acf1865cb9f84c3cfba68be3ace373..7003bdd860e421f5daa5769ae538bbc5da3e74b2 100644 (file)
@@ -1,4 +1,5 @@
 mod create;
 mod delete;
+mod list;
 mod read;
 mod update;
index 724442245507b1545b0306f315768951d1c97e1d..0d4f94ff2a0a4b00f485441d19e92e77459a5a49 100644 (file)
@@ -7,18 +7,8 @@ use lemmy_api_common::{
   mark_post_as_read,
   post::*,
 };
-use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
-use lemmy_db_schema::{
-  from_opt_str_to_opt_enum,
-  source::community::Community,
-  traits::DeleteableOrRemoveable,
-  ListingType,
-  SortType,
-};
-use lemmy_db_views::{
-  comment_view::CommentQueryBuilder,
-  post_view::{PostQueryBuilder, PostView},
-};
+use lemmy_db_schema::traits::DeleteableOrRemoveable;
+use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostView};
 use lemmy_db_views_actor::{
   community_moderator_view::CommunityModeratorView,
   community_view::CommunityView,
@@ -117,85 +107,3 @@ impl PerformCrud for GetPost {
     })
   }
 }
-
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for GetPosts {
-  type Response = GetPostsResponse;
-
-  #[tracing::instrument(skip(context, _websocket_id))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetPostsResponse, LemmyError> {
-    let data: &GetPosts = self;
-    let local_user_view =
-      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
-        .await?;
-
-    check_private_instance(&local_user_view, context.pool()).await?;
-
-    let person_id = local_user_view.to_owned().map(|l| l.person.id);
-
-    let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
-    let show_bot_accounts = local_user_view
-      .as_ref()
-      .map(|t| t.local_user.show_bot_accounts);
-    let show_read_posts = local_user_view
-      .as_ref()
-      .map(|t| t.local_user.show_read_posts);
-
-    let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
-    let listing_type: Option<ListingType> = from_opt_str_to_opt_enum(&data.type_);
-
-    let page = data.page;
-    let limit = data.limit;
-    let community_id = data.community_id;
-    let community_actor_id = if let Some(name) = &data.community_name {
-      resolve_actor_identifier::<ApubCommunity, Community>(name, context)
-        .await
-        .ok()
-        .map(|c| c.actor_id)
-    } else {
-      None
-    };
-    let saved_only = data.saved_only;
-
-    let mut posts = blocking(context.pool(), move |conn| {
-      PostQueryBuilder::create(conn)
-        .listing_type(listing_type)
-        .sort(sort)
-        .show_nsfw(show_nsfw)
-        .show_bot_accounts(show_bot_accounts)
-        .show_read_posts(show_read_posts)
-        .community_id(community_id)
-        .community_actor_id(community_actor_id)
-        .saved_only(saved_only)
-        .my_person_id(person_id)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await?
-    .map_err(|e| LemmyError::from_error_message(e, "couldnt_get_posts"))?;
-
-    // Blank out deleted or removed info for non-logged in users
-    if person_id.is_none() {
-      for pv in posts
-        .iter_mut()
-        .filter(|p| p.post.deleted || p.post.removed)
-      {
-        pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
-      }
-
-      for pv in posts
-        .iter_mut()
-        .filter(|p| p.community.deleted || p.community.removed)
-      {
-        pv.community = pv.to_owned().community.blank_out_deleted_or_removed_info();
-      }
-    }
-
-    Ok(GetPostsResponse { posts })
-  }
-}
index 7a3feb5cb6d424e4bb157127a3f51cbe158263fd..757e40a1102e8088d84bb19808f0601bd9e8c236 100644 (file)
@@ -195,7 +195,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           )
           .route(
             "/password_change",
-            web::post().to(route_post::<PasswordChange>),
+            web::post().to(route_post::<PasswordChangeAfterReset>),
           )
           // mark_all_as_read feels off being in this section as well
           .route(