From: Nutomic <me@nutomic.com>
Date: Fri, 28 Jul 2023 14:39:38 +0000 (+0200)
Subject: Rewrite some API handlers to remove Perform trait (#3735)
X-Git-Url: http://these/git/%22%7Burl%7D/%7B%60https:/%24%7Bargs.thread.url%7D?a=commitdiff_plain;h=37998b3398ed925a7640a9b67d1dc6ef871893a9;p=lemmy.git

Rewrite some API handlers to remove Perform trait (#3735)

* Rewrite some API handlers to remove Perform trait

* Convert CreateComment

* ci
---

diff --git a/Cargo.lock b/Cargo.lock
index 6ac601c0..bac96d5e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2573,6 +2573,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 name = "lemmy_api"
 version = "0.18.1"
 dependencies = [
+ "activitypub_federation",
  "actix-web",
  "anyhow",
  "async-trait",
diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml
index be3065e4..17f40d57 100644
--- a/crates/api/Cargo.toml
+++ b/crates/api/Cargo.toml
@@ -20,6 +20,7 @@ lemmy_db_views = { workspace = true, features = ["full"] }
 lemmy_db_views_moderator = { workspace = true, features = ["full"] }
 lemmy_db_views_actor = { workspace = true, features = ["full"] }
 lemmy_api_common = { workspace = true, features = ["full"] }
+activitypub_federation = { workspace = true }
 bcrypt = { workspace = true }
 serde = { workspace = true }
 actix-web = { workspace = true }
diff --git a/crates/api/src/comment/distinguish.rs b/crates/api/src/comment/distinguish.rs
index 47c23d3d..540c19a3 100644
--- a/crates/api/src/comment/distinguish.rs
+++ b/crates/api/src/comment/distinguish.rs
@@ -1,5 +1,4 @@
-use crate::Perform;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json};
 use lemmy_api_common::{
   comment::{CommentResponse, DistinguishComment},
   context::LemmyContext,
@@ -12,50 +11,47 @@ use lemmy_db_schema::{
 use lemmy_db_views::structs::CommentView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl Perform for DistinguishComment {
-  type Response = CommentResponse;
-
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommentResponse, LemmyError> {
-    let data: &DistinguishComment = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-
-    let comment_id = data.comment_id;
-    let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_comment.community.id,
-      &mut context.pool(),
-    )
-    .await?;
-
-    // Verify that only a mod or admin can distinguish a comment
-    is_mod_or_admin(
-      &mut context.pool(),
-      local_user_view.person.id,
-      orig_comment.community.id,
-    )
-    .await?;
-
-    // Update the Comment
-    let comment_id = data.comment_id;
-    let form = CommentUpdateForm::builder()
-      .distinguished(Some(data.distinguished))
-      .build();
-    Comment::update(&mut context.pool(), comment_id, &form)
-      .await
-      .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
-
-    let comment_id = data.comment_id;
-    let person_id = local_user_view.person.id;
-    let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
-
-    Ok(CommentResponse {
-      comment_view,
-      recipient_ids: Vec::new(),
-      form_id: None,
-    })
-  }
+#[tracing::instrument(skip(context))]
+pub async fn distinguish_comment(
+  data: Json<DistinguishComment>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommentResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+
+  let comment_id = data.comment_id;
+  let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
+
+  check_community_ban(
+    local_user_view.person.id,
+    orig_comment.community.id,
+    &mut context.pool(),
+  )
+  .await?;
+
+  // Verify that only a mod or admin can distinguish a comment
+  is_mod_or_admin(
+    &mut context.pool(),
+    local_user_view.person.id,
+    orig_comment.community.id,
+  )
+  .await?;
+
+  // Update the Comment
+  let comment_id = data.comment_id;
+  let form = CommentUpdateForm::builder()
+    .distinguished(Some(data.distinguished))
+    .build();
+  Comment::update(&mut context.pool(), comment_id, &form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
+
+  let comment_id = data.comment_id;
+  let person_id = local_user_view.person.id;
+  let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
+
+  Ok(Json(CommentResponse {
+    comment_view,
+    recipient_ids: Vec::new(),
+    form_id: None,
+  }))
 }
diff --git a/crates/api/src/comment/mod.rs b/crates/api/src/comment/mod.rs
index 27584c36..8caeaf8b 100644
--- a/crates/api/src/comment/mod.rs
+++ b/crates/api/src/comment/mod.rs
@@ -1,3 +1,3 @@
-mod distinguish;
-mod like;
-mod save;
+pub mod distinguish;
+pub mod like;
+pub mod save;
diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs
index 7161c8e9..8c9d9055 100644
--- a/crates/api/src/comment/save.rs
+++ b/crates/api/src/comment/save.rs
@@ -1,5 +1,4 @@
-use crate::Perform;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json};
 use lemmy_api_common::{
   comment::{CommentResponse, SaveComment},
   context::LemmyContext,
@@ -12,38 +11,35 @@ use lemmy_db_schema::{
 use lemmy_db_views::structs::CommentView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl Perform for SaveComment {
-  type Response = CommentResponse;
+#[tracing::instrument(skip(context))]
+pub async fn save_comment(
+  data: Json<SaveComment>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommentResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommentResponse, LemmyError> {
-    let data: &SaveComment = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let comment_saved_form = CommentSavedForm {
+    comment_id: data.comment_id,
+    person_id: local_user_view.person.id,
+  };
 
-    let comment_saved_form = CommentSavedForm {
-      comment_id: data.comment_id,
-      person_id: local_user_view.person.id,
-    };
-
-    if data.save {
-      CommentSaved::save(&mut context.pool(), &comment_saved_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
-    } else {
-      CommentSaved::unsave(&mut context.pool(), &comment_saved_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
-    }
+  if data.save {
+    CommentSaved::save(&mut context.pool(), &comment_saved_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
+  } else {
+    CommentSaved::unsave(&mut context.pool(), &comment_saved_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
+  }
 
-    let comment_id = data.comment_id;
-    let person_id = local_user_view.person.id;
-    let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
+  let comment_id = data.comment_id;
+  let person_id = local_user_view.person.id;
+  let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
 
-    Ok(CommentResponse {
-      comment_view,
-      recipient_ids: Vec::new(),
-      form_id: None,
-    })
-  }
+  Ok(Json(CommentResponse {
+    comment_view,
+    recipient_ids: Vec::new(),
+    form_id: None,
+  }))
 }
diff --git a/crates/api/src/comment_report/list.rs b/crates/api/src/comment_report/list.rs
index b67ec333..0ca093c7 100644
--- a/crates/api/src/comment_report/list.rs
+++ b/crates/api/src/comment_report/list.rs
@@ -1,5 +1,4 @@
-use crate::Perform;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json, Query};
 use lemmy_api_common::{
   comment::{ListCommentReports, ListCommentReportsResponse},
   context::LemmyContext,
@@ -10,32 +9,26 @@ use lemmy_utils::error::LemmyError;
 
 /// 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))]
+pub async fn list_comment_reports(
+  data: Query<ListCommentReports>,
+  context: Data<LemmyContext>,
+) -> Result<Json<ListCommentReportsResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<ListCommentReportsResponse, LemmyError> {
-    let data: &ListCommentReports = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let community_id = data.community_id;
+  let unresolved_only = data.unresolved_only;
 
-    let community_id = data.community_id;
-    let unresolved_only = data.unresolved_only;
-
-    let page = data.page;
-    let limit = data.limit;
-    let comment_reports = CommentReportQuery {
-      community_id,
-      unresolved_only,
-      page,
-      limit,
-    }
-    .list(&mut context.pool(), &local_user_view.person)
-    .await?;
-
-    Ok(ListCommentReportsResponse { comment_reports })
+  let page = data.page;
+  let limit = data.limit;
+  let comment_reports = CommentReportQuery {
+    community_id,
+    unresolved_only,
+    page,
+    limit,
   }
+  .list(&mut context.pool(), &local_user_view.person)
+  .await?;
+
+  Ok(Json(ListCommentReportsResponse { comment_reports }))
 }
diff --git a/crates/api/src/comment_report/mod.rs b/crates/api/src/comment_report/mod.rs
index 375fde4c..3bb1a9b4 100644
--- a/crates/api/src/comment_report/mod.rs
+++ b/crates/api/src/comment_report/mod.rs
@@ -1,3 +1,3 @@
-mod create;
-mod list;
-mod resolve;
+pub mod create;
+pub mod list;
+pub mod resolve;
diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/comment_report/resolve.rs
index 11149527..8e03484e 100644
--- a/crates/api/src/comment_report/resolve.rs
+++ b/crates/api/src/comment_report/resolve.rs
@@ -1,5 +1,4 @@
-use crate::Perform;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json};
 use lemmy_api_common::{
   comment::{CommentReportResponse, ResolveCommentReport},
   context::LemmyContext,
@@ -10,41 +9,35 @@ use lemmy_db_views::structs::CommentReportView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
 /// 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))]
+pub async fn resolve_comment_report(
+  data: Json<ResolveCommentReport>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommentReportResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<CommentReportResponse, LemmyError> {
-    let data: &ResolveCommentReport = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let report_id = data.report_id;
+  let person_id = local_user_view.person.id;
+  let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
 
-    let report_id = data.report_id;
-    let person_id = local_user_view.person.id;
-    let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
+  let person_id = local_user_view.person.id;
+  is_mod_or_admin(&mut context.pool(), person_id, report.community.id).await?;
 
-    let person_id = local_user_view.person.id;
-    is_mod_or_admin(&mut context.pool(), person_id, report.community.id).await?;
-
-    if data.resolved {
-      CommentReport::resolve(&mut context.pool(), report_id, person_id)
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
-    } else {
-      CommentReport::unresolve(&mut context.pool(), report_id, person_id)
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
-    }
+  if data.resolved {
+    CommentReport::resolve(&mut context.pool(), report_id, person_id)
+      .await
+      .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
+  } else {
+    CommentReport::unresolve(&mut context.pool(), report_id, person_id)
+      .await
+      .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
+  }
 
-    let report_id = data.report_id;
-    let comment_report_view =
-      CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
+  let report_id = data.report_id;
+  let comment_report_view =
+    CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
 
-    Ok(CommentReportResponse {
-      comment_report_view,
-    })
-  }
+  Ok(Json(CommentReportResponse {
+    comment_report_view,
+  }))
 }
diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs
index b297f503..cc5fb8e8 100644
--- a/crates/api/src/lib.rs
+++ b/crates/api/src/lib.rs
@@ -9,15 +9,15 @@ use lemmy_utils::{
 };
 use std::io::Cursor;
 
-mod comment;
-mod comment_report;
-mod community;
-mod local_user;
-mod post;
-mod post_report;
-mod private_message;
-mod private_message_report;
-mod site;
+pub mod comment;
+pub mod comment_report;
+pub mod community;
+pub mod local_user;
+pub mod post;
+pub mod post_report;
+pub mod private_message;
+pub mod private_message_report;
+pub mod site;
 
 #[async_trait::async_trait(?Send)]
 pub trait Perform {
diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs
index 3a92beda..806fa66a 100644
--- a/crates/api/src/local_user/mod.rs
+++ b/crates/api/src/local_user/mod.rs
@@ -1,13 +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;
+pub mod add_admin;
+pub mod ban_person;
+pub mod block;
+pub mod change_password;
+pub mod change_password_after_reset;
+pub mod get_captcha;
+pub mod list_banned;
+pub mod login;
+pub mod notifications;
+pub mod report_count;
+pub mod reset_password;
+pub mod save_settings;
+pub mod verify_email;
diff --git a/crates/api/src/local_user/notifications/mark_reply_read.rs b/crates/api/src/local_user/notifications/mark_reply_read.rs
index 4071a466..9ae9f525 100644
--- a/crates/api/src/local_user/notifications/mark_reply_read.rs
+++ b/crates/api/src/local_user/notifications/mark_reply_read.rs
@@ -1,5 +1,4 @@
-use crate::Perform;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json};
 use lemmy_api_common::{
   context::LemmyContext,
   person::{CommentReplyResponse, MarkCommentReplyAsRead},
@@ -12,41 +11,35 @@ use lemmy_db_schema::{
 use lemmy_db_views_actor::structs::CommentReplyView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl Perform for MarkCommentReplyAsRead {
-  type Response = CommentReplyResponse;
+#[tracing::instrument(skip(context))]
+pub async fn mark_reply_as_read(
+  data: Json<MarkCommentReplyAsRead>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommentReplyResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<CommentReplyResponse, LemmyError> {
-    let data = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let comment_reply_id = data.comment_reply_id;
+  let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
 
-    let comment_reply_id = data.comment_reply_id;
-    let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
-
-    if local_user_view.person.id != read_comment_reply.recipient_id {
-      return Err(LemmyErrorType::CouldntUpdateComment)?;
-    }
+  if local_user_view.person.id != read_comment_reply.recipient_id {
+    return Err(LemmyErrorType::CouldntUpdateComment)?;
+  }
 
-    let comment_reply_id = read_comment_reply.id;
-    let read = Some(data.read);
+  let comment_reply_id = read_comment_reply.id;
+  let read = Some(data.read);
 
-    CommentReply::update(
-      &mut context.pool(),
-      comment_reply_id,
-      &CommentReplyUpdateForm { read },
-    )
-    .await
-    .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
+  CommentReply::update(
+    &mut context.pool(),
+    comment_reply_id,
+    &CommentReplyUpdateForm { read },
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
 
-    let comment_reply_id = read_comment_reply.id;
-    let person_id = local_user_view.person.id;
-    let comment_reply_view =
-      CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?;
+  let comment_reply_id = read_comment_reply.id;
+  let person_id = local_user_view.person.id;
+  let comment_reply_view =
+    CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?;
 
-    Ok(CommentReplyResponse { comment_reply_view })
-  }
+  Ok(Json(CommentReplyResponse { comment_reply_view }))
 }
diff --git a/crates/api/src/local_user/notifications/mod.rs b/crates/api/src/local_user/notifications/mod.rs
index ab98053f..35567afd 100644
--- a/crates/api/src/local_user/notifications/mod.rs
+++ b/crates/api/src/local_user/notifications/mod.rs
@@ -1,6 +1,6 @@
-mod list_mentions;
-mod list_replies;
-mod mark_all_read;
-mod mark_mention_read;
-mod mark_reply_read;
-mod unread_count;
+pub mod list_mentions;
+pub mod list_replies;
+pub mod mark_all_read;
+pub mod mark_mention_read;
+pub mod mark_reply_read;
+pub mod unread_count;
diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs
index 8a63f7ad..b8c02457 100644
--- a/crates/api_common/src/build_response.rs
+++ b/crates/api_common/src/build_response.rs
@@ -23,7 +23,7 @@ use lemmy_db_views_actor::structs::CommunityView;
 use lemmy_utils::{error::LemmyError, utils::mention::MentionData};
 
 pub async fn build_comment_response(
-  context: &Data<LemmyContext>,
+  context: &LemmyContext,
   comment_id: CommentId,
   local_user_view: Option<LocalUserView>,
   form_id: Option<String>,
diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs
index 6c91258e..994aea2a 100644
--- a/crates/api_common/src/send_activity.rs
+++ b/crates/api_common/src/send_activity.rs
@@ -1,7 +1,7 @@
 use crate::context::LemmyContext;
 use activitypub_federation::config::Data;
 use futures::future::BoxFuture;
-use lemmy_db_schema::source::post::Post;
+use lemmy_db_schema::source::{comment::Comment, post::Post};
 use lemmy_utils::{error::LemmyResult, SYNCHRONOUS_FEDERATION};
 use once_cell::sync::{Lazy, OnceCell};
 use tokio::{
@@ -22,6 +22,7 @@ pub static MATCH_OUTGOING_ACTIVITIES: OnceCell<MatchOutgoingActivitiesBoxed> = O
 #[derive(Debug)]
 pub enum SendActivityData {
   CreatePost(Post),
+  CreateComment(Comment),
 }
 
 // TODO: instead of static, move this into LemmyContext. make sure that stopping the process with
diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs
index 4a7513a4..f334efe5 100644
--- a/crates/api_crud/src/comment/create.rs
+++ b/crates/api_crud/src/comment/create.rs
@@ -1,9 +1,10 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   build_response::{build_comment_response, send_local_notifs},
   comment::{CommentResponse, CreateComment},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{
     check_community_ban,
     check_community_deleted_or_removed,
@@ -35,169 +36,174 @@ use lemmy_utils::{
     validation::is_valid_body_field,
   },
 };
+use std::ops::Deref;
 
 const MAX_COMMENT_DEPTH_LIMIT: usize = 100;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for CreateComment {
-  type Response = CommentResponse;
-
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommentResponse, LemmyError> {
-    let data: &CreateComment = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
-
-    let content = remove_slurs(
-      &data.content.clone(),
-      &local_site_to_slur_regex(&local_site),
-    );
-    is_valid_body_field(&Some(content.clone()), false)?;
-    let content = sanitize_html(&content);
-
-    // Check for a community ban
-    let post_id = data.post_id;
-    let post = get_post(post_id, &mut context.pool()).await?;
-    let community_id = post.community_id;
-
-    check_community_ban(local_user_view.person.id, community_id, &mut context.pool()).await?;
-    check_community_deleted_or_removed(community_id, &mut context.pool()).await?;
-    check_post_deleted_or_removed(&post)?;
-
-    // Check if post is locked, no new comments
-    if post.locked {
-      return Err(LemmyErrorType::Locked)?;
-    }
+#[tracing::instrument(skip(context))]
+pub async fn create_comment(
+  data: Json<CreateComment>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommentResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
+
+  let content = remove_slurs(
+    &data.content.clone(),
+    &local_site_to_slur_regex(&local_site),
+  );
+  is_valid_body_field(&Some(content.clone()), false)?;
+  let content = sanitize_html(&content);
+
+  // Check for a community ban
+  let post_id = data.post_id;
+  let post = get_post(post_id, &mut context.pool()).await?;
+  let community_id = post.community_id;
+
+  check_community_ban(local_user_view.person.id, community_id, &mut context.pool()).await?;
+  check_community_deleted_or_removed(community_id, &mut context.pool()).await?;
+  check_post_deleted_or_removed(&post)?;
+
+  // Check if post is locked, no new comments
+  if post.locked {
+    return Err(LemmyErrorType::Locked)?;
+  }
 
-    // Fetch the parent, if it exists
-    let parent_opt = if let Some(parent_id) = data.parent_id {
-      Comment::read(&mut context.pool(), parent_id).await.ok()
-    } else {
-      None
-    };
-
-    // If there's a parent_id, check to make sure that comment is in that post
-    // Strange issue where sometimes the post ID of the parent comment is incorrect
-    if let Some(parent) = parent_opt.as_ref() {
-      if parent.post_id != post_id {
-        return Err(LemmyErrorType::CouldntCreateComment)?;
-      }
-      check_comment_depth(parent)?;
+  // Fetch the parent, if it exists
+  let parent_opt = if let Some(parent_id) = data.parent_id {
+    Comment::read(&mut context.pool(), parent_id).await.ok()
+  } else {
+    None
+  };
+
+  // If there's a parent_id, check to make sure that comment is in that post
+  // Strange issue where sometimes the post ID of the parent comment is incorrect
+  if let Some(parent) = parent_opt.as_ref() {
+    if parent.post_id != post_id {
+      return Err(LemmyErrorType::CouldntCreateComment)?;
     }
+    check_comment_depth(parent)?;
+  }
 
-    CommunityLanguage::is_allowed_community_language(
-      &mut context.pool(),
-      data.language_id,
-      community_id,
-    )
-    .await?;
-
-    // attempt to set default language if none was provided
-    let language_id = match data.language_id {
-      Some(lid) => Some(lid),
-      None => {
-        default_post_language(
-          &mut context.pool(),
-          community_id,
-          local_user_view.local_user.id,
-        )
-        .await?
-      }
-    };
-
-    let comment_form = CommentInsertForm::builder()
-      .content(content.clone())
-      .post_id(data.post_id)
-      .creator_id(local_user_view.person.id)
-      .language_id(language_id)
-      .build();
-
-    // Create the comment
-    let parent_path = parent_opt.clone().map(|t| t.path);
-    let inserted_comment =
-      Comment::create(&mut context.pool(), &comment_form, parent_path.as_ref())
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
-
-    // Necessary to update the ap_id
-    let inserted_comment_id = inserted_comment.id;
-    let protocol_and_hostname = context.settings().get_protocol_and_hostname();
-
-    let apub_id = generate_local_apub_endpoint(
-      EndpointType::Comment,
-      &inserted_comment_id.to_string(),
-      &protocol_and_hostname,
-    )?;
-    let updated_comment = Comment::update(
-      &mut context.pool(),
-      inserted_comment_id,
-      &CommentUpdateForm::builder().ap_id(Some(apub_id)).build(),
-    )
+  CommunityLanguage::is_allowed_community_language(
+    &mut context.pool(),
+    data.language_id,
+    community_id,
+  )
+  .await?;
+
+  // attempt to set default language if none was provided
+  let language_id = match data.language_id {
+    Some(lid) => Some(lid),
+    None => {
+      default_post_language(
+        &mut context.pool(),
+        community_id,
+        local_user_view.local_user.id,
+      )
+      .await?
+    }
+  };
+
+  let comment_form = CommentInsertForm::builder()
+    .content(content.clone())
+    .post_id(data.post_id)
+    .creator_id(local_user_view.person.id)
+    .language_id(language_id)
+    .build();
+
+  // Create the comment
+  let parent_path = parent_opt.clone().map(|t| t.path);
+  let inserted_comment = Comment::create(&mut context.pool(), &comment_form, parent_path.as_ref())
     .await
     .with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
 
-    // Scan the comment for user mentions, add those rows
-    let mentions = scrape_text_for_mentions(&content);
-    let recipient_ids = send_local_notifs(
-      mentions,
-      &updated_comment,
-      &local_user_view.person,
-      &post,
-      true,
-      context,
-    )
-    .await?;
-
-    // You like your own comment by default
-    let like_form = CommentLikeForm {
-      comment_id: inserted_comment.id,
-      post_id: post.id,
-      person_id: local_user_view.person.id,
-      score: 1,
-    };
+  // Necessary to update the ap_id
+  let inserted_comment_id = inserted_comment.id;
+  let protocol_and_hostname = context.settings().get_protocol_and_hostname();
+
+  let apub_id = generate_local_apub_endpoint(
+    EndpointType::Comment,
+    &inserted_comment_id.to_string(),
+    &protocol_and_hostname,
+  )?;
+  let updated_comment = Comment::update(
+    &mut context.pool(),
+    inserted_comment_id,
+    &CommentUpdateForm::builder().ap_id(Some(apub_id)).build(),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
+
+  // Scan the comment for user mentions, add those rows
+  let mentions = scrape_text_for_mentions(&content);
+  let recipient_ids = send_local_notifs(
+    mentions,
+    &updated_comment,
+    &local_user_view.person,
+    &post,
+    true,
+    &context,
+  )
+  .await?;
+
+  // You like your own comment by default
+  let like_form = CommentLikeForm {
+    comment_id: inserted_comment.id,
+    post_id: post.id,
+    person_id: local_user_view.person.id,
+    score: 1,
+  };
+
+  CommentLike::like(&mut context.pool(), &like_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
+
+  ActivityChannel::submit_activity(
+    SendActivityData::CreateComment(updated_comment.clone()),
+    &context,
+  )
+  .await?;
+
+  // If its a reply, mark the parent as read
+  if let Some(parent) = parent_opt {
+    let parent_id = parent.id;
+    let comment_reply = CommentReply::read_by_comment(&mut context.pool(), parent_id).await;
+    if let Ok(reply) = comment_reply {
+      CommentReply::update(
+        &mut context.pool(),
+        reply.id,
+        &CommentReplyUpdateForm { read: Some(true) },
+      )
+      .await
+      .with_lemmy_type(LemmyErrorType::CouldntUpdateReplies)?;
+    }
 
-    CommentLike::like(&mut context.pool(), &like_form)
+    // If the parent has PersonMentions mark them as read too
+    let person_id = local_user_view.person.id;
+    let person_mention =
+      PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
+    if let Ok(mention) = person_mention {
+      PersonMention::update(
+        &mut context.pool(),
+        mention.id,
+        &PersonMentionUpdateForm { read: Some(true) },
+      )
       .await
-      .with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
-
-    // If its a reply, mark the parent as read
-    if let Some(parent) = parent_opt {
-      let parent_id = parent.id;
-      let comment_reply = CommentReply::read_by_comment(&mut context.pool(), parent_id).await;
-      if let Ok(reply) = comment_reply {
-        CommentReply::update(
-          &mut context.pool(),
-          reply.id,
-          &CommentReplyUpdateForm { read: Some(true) },
-        )
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntUpdateReplies)?;
-      }
-
-      // If the parent has PersonMentions mark them as read too
-      let person_id = local_user_view.person.id;
-      let person_mention =
-        PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
-      if let Ok(mention) = person_mention {
-        PersonMention::update(
-          &mut context.pool(),
-          mention.id,
-          &PersonMentionUpdateForm { read: Some(true) },
-        )
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntUpdatePersonMentions)?;
-      }
+      .with_lemmy_type(LemmyErrorType::CouldntUpdatePersonMentions)?;
     }
+  }
 
+  Ok(Json(
     build_comment_response(
-      context,
+      context.deref(),
       inserted_comment.id,
       Some(local_user_view),
-      self.form_id.clone(),
+      data.form_id.clone(),
       recipient_ids,
     )
-    .await
-  }
+    .await?,
+  ))
 }
 
 pub fn check_comment_depth(comment: &Comment) -> Result<(), LemmyError> {
diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs
index c42924de..eba7d1de 100644
--- a/crates/api_crud/src/comment/delete.rs
+++ b/crates/api_crud/src/comment/delete.rs
@@ -15,6 +15,7 @@ use lemmy_db_schema::{
 };
 use lemmy_db_views::structs::CommentView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
+use std::ops::Deref;
 
 #[async_trait::async_trait(?Send)]
 impl PerformCrud for DeleteComment {
@@ -68,7 +69,7 @@ impl PerformCrud for DeleteComment {
     .await?;
 
     build_comment_response(
-      context,
+      context.deref(),
       updated_comment.id,
       Some(local_user_view),
       None,
diff --git a/crates/api_crud/src/comment/mod.rs b/crates/api_crud/src/comment/mod.rs
index d3d789a0..8bb842b7 100644
--- a/crates/api_crud/src/comment/mod.rs
+++ b/crates/api_crud/src/comment/mod.rs
@@ -1,5 +1,5 @@
-mod create;
-mod delete;
-mod read;
-mod remove;
-mod update;
+pub mod create;
+pub mod delete;
+pub mod read;
+pub mod remove;
+pub mod update;
diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs
index e6899fdc..1a794dc5 100644
--- a/crates/api_crud/src/comment/read.rs
+++ b/crates/api_crud/src/comment/read.rs
@@ -1,5 +1,4 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json, Query};
 use lemmy_api_common::{
   build_response::build_comment_response,
   comment::{CommentResponse, GetComment},
@@ -8,19 +7,19 @@ use lemmy_api_common::{
 };
 use lemmy_db_schema::source::local_site::LocalSite;
 use lemmy_utils::error::LemmyError;
+use std::ops::Deref;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for GetComment {
-  type Response = CommentResponse;
+#[tracing::instrument(skip(context))]
+pub async fn get_comment(
+  data: Query<GetComment>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommentResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError> {
-    let data = self;
-    let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), context).await;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
+  check_private_instance(&local_user_view, &local_site)?;
 
-    check_private_instance(&local_user_view, &local_site)?;
-
-    build_comment_response(context, data.id, local_user_view, None, vec![]).await
-  }
+  Ok(Json(
+    build_comment_response(context.deref(), data.id, local_user_view, None, vec![]).await?,
+  ))
 }
diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs
index e87eb425..cfc3ccff 100644
--- a/crates/api_crud/src/comment/remove.rs
+++ b/crates/api_crud/src/comment/remove.rs
@@ -16,6 +16,7 @@ use lemmy_db_schema::{
 };
 use lemmy_db_views::structs::CommentView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
+use std::ops::Deref;
 
 #[async_trait::async_trait(?Send)]
 impl PerformCrud for RemoveComment {
@@ -76,7 +77,7 @@ impl PerformCrud for RemoveComment {
     .await?;
 
     build_comment_response(
-      context,
+      context.deref(),
       updated_comment.id,
       Some(local_user_view),
       None,
diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs
index 558965f6..5d4d75a3 100644
--- a/crates/api_crud/src/comment/update.rs
+++ b/crates/api_crud/src/comment/update.rs
@@ -29,6 +29,7 @@ use lemmy_utils::{
     validation::is_valid_body_field,
   },
 };
+use std::ops::Deref;
 
 #[async_trait::async_trait(?Send)]
 impl PerformCrud for EditComment {
@@ -95,7 +96,7 @@ impl PerformCrud for EditComment {
     .await?;
 
     build_comment_response(
-      context,
+      context.deref(),
       updated_comment.id,
       Some(local_user_view),
       self.form_id.clone(),
diff --git a/crates/api_crud/src/community/list.rs b/crates/api_crud/src/community/list.rs
index bd818995..c8ce9e58 100644
--- a/crates/api_crud/src/community/list.rs
+++ b/crates/api_crud/src/community/list.rs
@@ -1,5 +1,4 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json, Query};
 use lemmy_api_common::{
   community::{ListCommunities, ListCommunitiesResponse},
   context::LemmyContext,
@@ -9,42 +8,36 @@ use lemmy_db_schema::source::local_site::LocalSite;
 use lemmy_db_views_actor::community_view::CommunityQuery;
 use lemmy_utils::error::LemmyError;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for ListCommunities {
-  type Response = ListCommunitiesResponse;
+#[tracing::instrument(skip(context))]
+pub async fn list_communities(
+  data: Query<ListCommunities>,
+  context: Data<LemmyContext>,
+) -> Result<Json<ListCommunitiesResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
+  let is_admin = local_user_view.as_ref().map(|luv| is_admin(luv).is_ok());
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<ListCommunitiesResponse, LemmyError> {
-    let data: &ListCommunities = self;
-    let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), context).await;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
-    let is_admin = local_user_view.as_ref().map(|luv| is_admin(luv).is_ok());
+  check_private_instance(&local_user_view, &local_site)?;
 
-    check_private_instance(&local_user_view, &local_site)?;
-
-    let sort = data.sort;
-    let listing_type = data.type_;
-    let show_nsfw = data.show_nsfw;
-    let page = data.page;
-    let limit = data.limit;
-    let local_user = local_user_view.map(|l| l.local_user);
-    let communities = CommunityQuery {
-      listing_type,
-      show_nsfw,
-      sort,
-      local_user: local_user.as_ref(),
-      page,
-      limit,
-      is_mod_or_admin: is_admin,
-      ..Default::default()
-    }
-    .list(&mut context.pool())
-    .await?;
-
-    // Return the jwt
-    Ok(ListCommunitiesResponse { communities })
+  let sort = data.sort;
+  let listing_type = data.type_;
+  let show_nsfw = data.show_nsfw;
+  let page = data.page;
+  let limit = data.limit;
+  let local_user = local_user_view.map(|l| l.local_user);
+  let communities = CommunityQuery {
+    listing_type,
+    show_nsfw,
+    sort,
+    local_user: local_user.as_ref(),
+    page,
+    limit,
+    is_mod_or_admin: is_admin,
+    ..Default::default()
   }
+  .list(&mut context.pool())
+  .await?;
+
+  // Return the jwt
+  Ok(Json(ListCommunitiesResponse { communities }))
 }
diff --git a/crates/api_crud/src/community/mod.rs b/crates/api_crud/src/community/mod.rs
index 3fc74165..4bd02848 100644
--- a/crates/api_crud/src/community/mod.rs
+++ b/crates/api_crud/src/community/mod.rs
@@ -1,5 +1,5 @@
-mod create;
-mod delete;
-mod list;
-mod remove;
-mod update;
+pub mod create;
+pub mod delete;
+pub mod list;
+pub mod remove;
+pub mod update;
diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs
index e7934286..edd5c46f 100644
--- a/crates/api_crud/src/lib.rs
+++ b/crates/api_crud/src/lib.rs
@@ -2,13 +2,13 @@ use actix_web::web::Data;
 use lemmy_api_common::context::LemmyContext;
 use lemmy_utils::error::LemmyError;
 
-mod comment;
-mod community;
-mod custom_emoji;
+pub mod comment;
+pub mod community;
+pub mod custom_emoji;
 pub mod post;
-mod private_message;
-mod site;
-mod user;
+pub mod private_message;
+pub mod site;
+pub mod user;
 
 #[async_trait::async_trait(?Send)]
 pub trait PerformCrud {
diff --git a/crates/api_crud/src/post/mod.rs b/crates/api_crud/src/post/mod.rs
index 43795556..8bb842b7 100644
--- a/crates/api_crud/src/post/mod.rs
+++ b/crates/api_crud/src/post/mod.rs
@@ -1,5 +1,5 @@
 pub mod create;
-mod delete;
-mod read;
-mod remove;
-mod update;
+pub mod delete;
+pub mod read;
+pub mod remove;
+pub mod update;
diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs
index e668517d..efa0c87b 100644
--- a/crates/api_crud/src/post/read.rs
+++ b/crates/api_crud/src/post/read.rs
@@ -1,5 +1,4 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json, Query};
 use lemmy_api_common::{
   context::LemmyContext,
   post::{GetPost, GetPostResponse},
@@ -19,107 +18,103 @@ use lemmy_db_views::{post_view::PostQuery, structs::PostView};
 use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for GetPost {
-  type Response = GetPostResponse;
+#[tracing::instrument(skip(context))]
+pub async fn get_post(
+  data: Query<GetPost>,
+  context: Data<LemmyContext>,
+) -> Result<Json<GetPostResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), &context).await;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<GetPostResponse, LemmyError> {
-    let data: &GetPost = self;
-    let local_user_view = local_user_view_from_jwt_opt(data.auth.as_ref(), context).await;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
+  check_private_instance(&local_user_view, &local_site)?;
 
-    check_private_instance(&local_user_view, &local_site)?;
+  let person_id = local_user_view.as_ref().map(|u| u.person.id);
 
-    let person_id = local_user_view.as_ref().map(|u| u.person.id);
+  // I'd prefer fetching the post_view by a comment join, but it adds a lot of boilerplate
+  let post_id = if let Some(id) = data.id {
+    id
+  } else if let Some(comment_id) = data.comment_id {
+    Comment::read(&mut context.pool(), comment_id)
+      .await
+      .with_lemmy_type(LemmyErrorType::CouldntFindPost)?
+      .post_id
+  } else {
+    Err(LemmyErrorType::CouldntFindPost)?
+  };
 
-    // I'd prefer fetching the post_view by a comment join, but it adds a lot of boilerplate
-    let post_id = if let Some(id) = data.id {
-      id
-    } else if let Some(comment_id) = data.comment_id {
-      Comment::read(&mut context.pool(), comment_id)
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntFindPost)?
-        .post_id
-    } else {
-      Err(LemmyErrorType::CouldntFindPost)?
-    };
+  // Check to see if the person is a mod or admin, to show deleted / removed
+  let community_id = Post::read(&mut context.pool(), post_id).await?.community_id;
+  let is_mod_or_admin = is_mod_or_admin_opt(
+    &mut context.pool(),
+    local_user_view.as_ref(),
+    Some(community_id),
+  )
+  .await
+  .is_ok();
 
-    // Check to see if the person is a mod or admin, to show deleted / removed
-    let community_id = Post::read(&mut context.pool(), post_id).await?.community_id;
-    let is_mod_or_admin = is_mod_or_admin_opt(
-      &mut context.pool(),
-      local_user_view.as_ref(),
-      Some(community_id),
-    )
-    .await
-    .is_ok();
+  let post_view = PostView::read(
+    &mut context.pool(),
+    post_id,
+    person_id,
+    Some(is_mod_or_admin),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
 
-    let post_view = PostView::read(
-      &mut context.pool(),
-      post_id,
-      person_id,
-      Some(is_mod_or_admin),
-    )
-    .await
-    .with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
+  // Mark the post as read
+  let post_id = post_view.post.id;
+  if let Some(person_id) = person_id {
+    mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
+  }
 
-    // Mark the post as read
-    let post_id = post_view.post.id;
-    if let Some(person_id) = person_id {
-      mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
-    }
+  // Necessary for the sidebar subscribed
+  let community_view = CommunityView::read(
+    &mut context.pool(),
+    community_id,
+    person_id,
+    Some(is_mod_or_admin),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
 
-    // Necessary for the sidebar subscribed
-    let community_view = CommunityView::read(
-      &mut context.pool(),
-      community_id,
+  // Insert into PersonPostAggregates
+  // to update the read_comments count
+  if let Some(person_id) = person_id {
+    let read_comments = post_view.counts.comments;
+    let person_post_agg_form = PersonPostAggregatesForm {
       person_id,
-      Some(is_mod_or_admin),
-    )
-    .await
-    .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
-
-    // Insert into PersonPostAggregates
-    // to update the read_comments count
-    if let Some(person_id) = person_id {
-      let read_comments = post_view.counts.comments;
-      let person_post_agg_form = PersonPostAggregatesForm {
-        person_id,
-        post_id,
-        read_comments,
-        ..PersonPostAggregatesForm::default()
-      };
-      PersonPostAggregates::upsert(&mut context.pool(), &person_post_agg_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
-    }
+      post_id,
+      read_comments,
+      ..PersonPostAggregatesForm::default()
+    };
+    PersonPostAggregates::upsert(&mut context.pool(), &person_post_agg_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
+  }
 
-    let moderators =
-      CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
+  let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
 
-    // Fetch the cross_posts
-    let cross_posts = if let Some(url) = &post_view.post.url {
-      let mut x_posts = PostQuery {
-        url_search: Some(url.inner().as_str().into()),
-        ..Default::default()
-      }
-      .list(&mut context.pool())
-      .await?;
+  // Fetch the cross_posts
+  let cross_posts = if let Some(url) = &post_view.post.url {
+    let mut x_posts = PostQuery {
+      url_search: Some(url.inner().as_str().into()),
+      ..Default::default()
+    }
+    .list(&mut context.pool())
+    .await?;
 
-      // Don't return this post as one of the cross_posts
-      x_posts.retain(|x| x.post.id != post_id);
-      x_posts
-    } else {
-      Vec::new()
-    };
+    // Don't return this post as one of the cross_posts
+    x_posts.retain(|x| x.post.id != post_id);
+    x_posts
+  } else {
+    Vec::new()
+  };
 
-    // Return the jwt
-    Ok(GetPostResponse {
-      post_view,
-      community_view,
-      moderators,
-      cross_posts,
-    })
-  }
+  // Return the jwt
+  Ok(Json(GetPostResponse {
+    post_view,
+    community_view,
+    moderators,
+    cross_posts,
+  }))
 }
diff --git a/crates/api_crud/src/private_message/mod.rs b/crates/api_crud/src/private_message/mod.rs
index 71683237..ab7fa439 100644
--- a/crates/api_crud/src/private_message/mod.rs
+++ b/crates/api_crud/src/private_message/mod.rs
@@ -1,4 +1,4 @@
-mod create;
-mod delete;
-mod read;
-mod update;
+pub mod create;
+pub mod delete;
+pub mod read;
+pub mod update;
diff --git a/crates/api_crud/src/private_message/read.rs b/crates/api_crud/src/private_message/read.rs
index 87d8ee66..ec4f5c10 100644
--- a/crates/api_crud/src/private_message/read.rs
+++ b/crates/api_crud/src/private_message/read.rs
@@ -1,5 +1,4 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json, Query};
 use lemmy_api_common::{
   context::LemmyContext,
   private_message::{GetPrivateMessages, PrivateMessagesResponse},
@@ -8,40 +7,34 @@ use lemmy_api_common::{
 use lemmy_db_views::private_message_view::PrivateMessageQuery;
 use lemmy_utils::error::LemmyError;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for GetPrivateMessages {
-  type Response = PrivateMessagesResponse;
+#[tracing::instrument(skip(context))]
+pub async fn get_private_message(
+  data: Query<GetPrivateMessages>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PrivateMessagesResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(data.auth.as_ref(), &context).await?;
+  let person_id = local_user_view.person.id;
 
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<PrivateMessagesResponse, LemmyError> {
-    let data: &GetPrivateMessages = self;
-    let local_user_view = local_user_view_from_jwt(data.auth.as_ref(), context).await?;
-    let person_id = local_user_view.person.id;
+  let page = data.page;
+  let limit = data.limit;
+  let unread_only = data.unread_only;
+  let mut messages = PrivateMessageQuery {
+    page,
+    limit,
+    unread_only,
+  }
+  .list(&mut context.pool(), person_id)
+  .await?;
 
-    let page = data.page;
-    let limit = data.limit;
-    let unread_only = data.unread_only;
-    let mut messages = PrivateMessageQuery {
-      page,
-      limit,
-      unread_only,
+  // Messages sent by ourselves should be marked as read. The `read` column in database is only
+  // for the recipient, and shouldnt be exposed to sender.
+  messages.iter_mut().for_each(|pmv| {
+    if pmv.creator.id == person_id {
+      pmv.private_message.read = true
     }
-    .list(&mut context.pool(), person_id)
-    .await?;
-
-    // Messages sent by ourselves should be marked as read. The `read` column in database is only
-    // for the recipient, and shouldnt be exposed to sender.
-    messages.iter_mut().for_each(|pmv| {
-      if pmv.creator.id == person_id {
-        pmv.private_message.read = true
-      }
-    });
+  });
 
-    Ok(PrivateMessagesResponse {
-      private_messages: messages,
-    })
-  }
+  Ok(Json(PrivateMessagesResponse {
+    private_messages: messages,
+  }))
 }
diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs
index 98d111a1..59a57ff8 100644
--- a/crates/api_crud/src/site/create.rs
+++ b/crates/api_crud/src/site/create.rs
@@ -1,9 +1,6 @@
-use crate::{
-  site::{application_question_check, site_default_post_listing_type_check},
-  PerformCrud,
-};
+use crate::site::{application_question_check, site_default_post_listing_type_check};
 use activitypub_federation::http_signatures::generate_actor_keypair;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json};
 use lemmy_api_common::{
   context::LemmyContext,
   site::{CreateSite, SiteResponse},
@@ -43,108 +40,105 @@ use lemmy_utils::{
 };
 use url::Url;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for CreateSite {
-  type Response = SiteResponse;
-
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> {
-    let data: &CreateSite = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
-
-    // Make sure user is an admin; other types of users should not create site data...
-    is_admin(&local_user_view)?;
-
-    validate_create_payload(&local_site, data)?;
-
-    let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
-    let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
-    let keypair = generate_actor_keypair()?;
-    let name = sanitize_html(&data.name);
-    let sidebar = sanitize_html_opt(&data.sidebar);
-    let description = sanitize_html_opt(&data.description);
-
-    let site_form = SiteUpdateForm::builder()
-      .name(Some(name))
-      .sidebar(diesel_option_overwrite(sidebar))
-      .description(diesel_option_overwrite(description))
-      .icon(diesel_option_overwrite_to_url(&data.icon)?)
-      .banner(diesel_option_overwrite_to_url(&data.banner)?)
-      .actor_id(Some(actor_id))
-      .last_refreshed_at(Some(naive_now()))
-      .inbox_url(inbox_url)
-      .private_key(Some(Some(keypair.private_key)))
-      .public_key(Some(keypair.public_key))
-      .build();
-
-    let site_id = local_site.site_id;
-
-    Site::update(&mut context.pool(), site_id, &site_form).await?;
-
-    let application_question = sanitize_html_opt(&data.application_question);
-    let default_theme = sanitize_html_opt(&data.default_theme);
-    let legal_information = sanitize_html_opt(&data.legal_information);
-
-    let local_site_form = LocalSiteUpdateForm::builder()
-      // Set the site setup to true
-      .site_setup(Some(true))
-      .enable_downvotes(data.enable_downvotes)
-      .registration_mode(data.registration_mode)
-      .enable_nsfw(data.enable_nsfw)
-      .community_creation_admin_only(data.community_creation_admin_only)
-      .require_email_verification(data.require_email_verification)
-      .application_question(diesel_option_overwrite(application_question))
-      .private_instance(data.private_instance)
-      .default_theme(default_theme)
-      .default_post_listing_type(data.default_post_listing_type)
-      .legal_information(diesel_option_overwrite(legal_information))
-      .application_email_admins(data.application_email_admins)
-      .hide_modlog_mod_names(data.hide_modlog_mod_names)
-      .updated(Some(Some(naive_now())))
-      .slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
-      .actor_name_max_length(data.actor_name_max_length)
-      .federation_enabled(data.federation_enabled)
-      .captcha_enabled(data.captcha_enabled)
-      .captcha_difficulty(data.captcha_difficulty.clone())
-      .build();
-
-    LocalSite::update(&mut context.pool(), &local_site_form).await?;
-
-    let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
-      .message(data.rate_limit_message)
-      .message_per_second(data.rate_limit_message_per_second)
-      .post(data.rate_limit_post)
-      .post_per_second(data.rate_limit_post_per_second)
-      .register(data.rate_limit_register)
-      .register_per_second(data.rate_limit_register_per_second)
-      .image(data.rate_limit_image)
-      .image_per_second(data.rate_limit_image_per_second)
-      .comment(data.rate_limit_comment)
-      .comment_per_second(data.rate_limit_comment_per_second)
-      .search(data.rate_limit_search)
-      .search_per_second(data.rate_limit_search_per_second)
-      .build();
-
-    LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
-
-    let site_view = SiteView::read_local(&mut context.pool()).await?;
-
-    let new_taglines = data.taglines.clone();
-    let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
-
-    let rate_limit_config =
-      local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
-    context
-      .settings_updated_channel()
-      .send(rate_limit_config)
-      .await?;
-
-    Ok(SiteResponse {
-      site_view,
-      taglines,
-    })
-  }
+#[tracing::instrument(skip(context))]
+pub async fn create_site(
+  data: Json<CreateSite>,
+  context: Data<LemmyContext>,
+) -> Result<Json<SiteResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
+
+  // Make sure user is an admin; other types of users should not create site data...
+  is_admin(&local_user_view)?;
+
+  validate_create_payload(&local_site, &data)?;
+
+  let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
+  let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
+  let keypair = generate_actor_keypair()?;
+  let name = sanitize_html(&data.name);
+  let sidebar = sanitize_html_opt(&data.sidebar);
+  let description = sanitize_html_opt(&data.description);
+
+  let site_form = SiteUpdateForm::builder()
+    .name(Some(name))
+    .sidebar(diesel_option_overwrite(sidebar))
+    .description(diesel_option_overwrite(description))
+    .icon(diesel_option_overwrite_to_url(&data.icon)?)
+    .banner(diesel_option_overwrite_to_url(&data.banner)?)
+    .actor_id(Some(actor_id))
+    .last_refreshed_at(Some(naive_now()))
+    .inbox_url(inbox_url)
+    .private_key(Some(Some(keypair.private_key)))
+    .public_key(Some(keypair.public_key))
+    .build();
+
+  let site_id = local_site.site_id;
+
+  Site::update(&mut context.pool(), site_id, &site_form).await?;
+
+  let application_question = sanitize_html_opt(&data.application_question);
+  let default_theme = sanitize_html_opt(&data.default_theme);
+  let legal_information = sanitize_html_opt(&data.legal_information);
+
+  let local_site_form = LocalSiteUpdateForm::builder()
+    // Set the site setup to true
+    .site_setup(Some(true))
+    .enable_downvotes(data.enable_downvotes)
+    .registration_mode(data.registration_mode)
+    .enable_nsfw(data.enable_nsfw)
+    .community_creation_admin_only(data.community_creation_admin_only)
+    .require_email_verification(data.require_email_verification)
+    .application_question(diesel_option_overwrite(application_question))
+    .private_instance(data.private_instance)
+    .default_theme(default_theme)
+    .default_post_listing_type(data.default_post_listing_type)
+    .legal_information(diesel_option_overwrite(legal_information))
+    .application_email_admins(data.application_email_admins)
+    .hide_modlog_mod_names(data.hide_modlog_mod_names)
+    .updated(Some(Some(naive_now())))
+    .slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
+    .actor_name_max_length(data.actor_name_max_length)
+    .federation_enabled(data.federation_enabled)
+    .captcha_enabled(data.captcha_enabled)
+    .captcha_difficulty(data.captcha_difficulty.clone())
+    .build();
+
+  LocalSite::update(&mut context.pool(), &local_site_form).await?;
+
+  let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
+    .message(data.rate_limit_message)
+    .message_per_second(data.rate_limit_message_per_second)
+    .post(data.rate_limit_post)
+    .post_per_second(data.rate_limit_post_per_second)
+    .register(data.rate_limit_register)
+    .register_per_second(data.rate_limit_register_per_second)
+    .image(data.rate_limit_image)
+    .image_per_second(data.rate_limit_image_per_second)
+    .comment(data.rate_limit_comment)
+    .comment_per_second(data.rate_limit_comment_per_second)
+    .search(data.rate_limit_search)
+    .search_per_second(data.rate_limit_search_per_second)
+    .build();
+
+  LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
+
+  let site_view = SiteView::read_local(&mut context.pool()).await?;
+
+  let new_taglines = data.taglines.clone();
+  let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
+
+  let rate_limit_config =
+    local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
+  context
+    .settings_updated_channel()
+    .send(rate_limit_config)
+    .await?;
+
+  Ok(Json(SiteResponse {
+    site_view,
+    taglines,
+  }))
 }
 
 fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> {
diff --git a/crates/api_crud/src/site/mod.rs b/crates/api_crud/src/site/mod.rs
index 652b9e65..e4911ba4 100644
--- a/crates/api_crud/src/site/mod.rs
+++ b/crates/api_crud/src/site/mod.rs
@@ -1,9 +1,9 @@
 use lemmy_db_schema::{ListingType, RegistrationMode};
 use lemmy_utils::error::{LemmyErrorType, LemmyResult};
 
-mod create;
-mod read;
-mod update;
+pub mod create;
+pub mod read;
+pub mod update;
 
 /// Checks whether the default post listing type is valid for a site.
 pub fn site_default_post_listing_type_check(
diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs
index e74eeefb..62d96492 100644
--- a/crates/api_crud/src/site/read.rs
+++ b/crates/api_crud/src/site/read.rs
@@ -1,5 +1,4 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use actix_web::web::{Data, Json, Query};
 use lemmy_api_common::{
   context::LemmyContext,
   sensitive::Sensitive,
@@ -28,76 +27,72 @@ use lemmy_utils::{
   version,
 };
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for GetSite {
-  type Response = GetSiteResponse;
-
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<GetSiteResponse, LemmyError> {
-    let data: &GetSite = self;
-
-    let site_view = SiteView::read_local(&mut context.pool()).await?;
-
-    let admins = PersonView::admins(&mut context.pool()).await?;
-
-    // Build the local user
-    let my_user = if let Some(local_user_view) =
-      local_user_settings_view_from_jwt_opt(data.auth.as_ref(), context).await
-    {
-      let person_id = local_user_view.person.id;
-      let local_user_id = local_user_view.local_user.id;
-
-      let follows = CommunityFollowerView::for_person(&mut context.pool(), person_id)
-        .await
-        .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
-
-      let person_id = local_user_view.person.id;
-      let community_blocks = CommunityBlockView::for_person(&mut context.pool(), person_id)
-        .await
-        .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
-
-      let person_id = local_user_view.person.id;
-      let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id)
-        .await
-        .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
-
-      let moderates = CommunityModeratorView::for_person(&mut context.pool(), person_id)
-        .await
-        .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
-
-      let discussion_languages = LocalUserLanguage::read(&mut context.pool(), local_user_id)
-        .await
-        .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
-
-      Some(MyUserInfo {
-        local_user_view,
-        follows,
-        moderates,
-        community_blocks,
-        person_blocks,
-        discussion_languages,
-      })
-    } else {
-      None
-    };
-
-    let all_languages = Language::read_all(&mut context.pool()).await?;
-    let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
-    let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
-    let custom_emojis =
-      CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
-
-    Ok(GetSiteResponse {
-      site_view,
-      admins,
-      version: version::VERSION.to_string(),
-      my_user,
-      all_languages,
+#[tracing::instrument(skip(context))]
+pub async fn get_site(
+  data: Query<GetSite>,
+  context: Data<LemmyContext>,
+) -> Result<Json<GetSiteResponse>, LemmyError> {
+  let site_view = SiteView::read_local(&mut context.pool()).await?;
+
+  let admins = PersonView::admins(&mut context.pool()).await?;
+
+  // Build the local user
+  let my_user = if let Some(local_user_view) =
+    local_user_settings_view_from_jwt_opt(data.auth.as_ref(), &context).await
+  {
+    let person_id = local_user_view.person.id;
+    let local_user_id = local_user_view.local_user.id;
+
+    let follows = CommunityFollowerView::for_person(&mut context.pool(), person_id)
+      .await
+      .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
+
+    let person_id = local_user_view.person.id;
+    let community_blocks = CommunityBlockView::for_person(&mut context.pool(), person_id)
+      .await
+      .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
+
+    let person_id = local_user_view.person.id;
+    let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id)
+      .await
+      .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
+
+    let moderates = CommunityModeratorView::for_person(&mut context.pool(), person_id)
+      .await
+      .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
+
+    let discussion_languages = LocalUserLanguage::read(&mut context.pool(), local_user_id)
+      .await
+      .with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
+
+    Some(MyUserInfo {
+      local_user_view,
+      follows,
+      moderates,
+      community_blocks,
+      person_blocks,
       discussion_languages,
-      taglines,
-      custom_emojis,
     })
-  }
+  } else {
+    None
+  };
+
+  let all_languages = Language::read_all(&mut context.pool()).await?;
+  let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
+  let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
+  let custom_emojis =
+    CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
+
+  Ok(Json(GetSiteResponse {
+    site_view,
+    admins,
+    version: version::VERSION.to_string(),
+    my_user,
+    all_languages,
+    discussion_languages,
+    taglines,
+    custom_emojis,
+  }))
 }
 
 #[tracing::instrument(skip_all)]
diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs
index 2b8ce4c0..f560c729 100644
--- a/crates/api_crud/src/site/update.rs
+++ b/crates/api_crud/src/site/update.rs
@@ -1,8 +1,5 @@
-use crate::{
-  site::{application_question_check, site_default_post_listing_type_check},
-  PerformCrud,
-};
-use actix_web::web::Data;
+use crate::site::{application_question_check, site_default_post_listing_type_check};
+use actix_web::web::{Data, Json};
 use lemmy_api_common::{
   context::LemmyContext,
   site::{EditSite, SiteResponse},
@@ -43,147 +40,142 @@ use lemmy_utils::{
   },
 };
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for EditSite {
-  type Response = SiteResponse;
-
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> {
-    let data: &EditSite = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-    let site_view = SiteView::read_local(&mut context.pool()).await?;
-    let local_site = site_view.local_site;
-    let site = site_view.site;
-
-    // Make sure user is an admin; other types of users should not update site data...
-    is_admin(&local_user_view)?;
-
-    validate_update_payload(&local_site, data)?;
+#[tracing::instrument(skip(context))]
+pub async fn update_site(
+  data: Json<EditSite>,
+  context: Data<LemmyContext>,
+) -> Result<Json<SiteResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  let site_view = SiteView::read_local(&mut context.pool()).await?;
+  let local_site = site_view.local_site;
+  let site = site_view.site;
 
-    if let Some(discussion_languages) = data.discussion_languages.clone() {
-      SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
-    }
+  // Make sure user is an admin; other types of users should not update site data...
+  is_admin(&local_user_view)?;
 
-    let name = sanitize_html_opt(&data.name);
-    let sidebar = sanitize_html_opt(&data.sidebar);
-    let description = sanitize_html_opt(&data.description);
+  validate_update_payload(&local_site, &data)?;
 
-    let site_form = SiteUpdateForm::builder()
-      .name(name)
-      .sidebar(diesel_option_overwrite(sidebar))
-      .description(diesel_option_overwrite(description))
-      .icon(diesel_option_overwrite_to_url(&data.icon)?)
-      .banner(diesel_option_overwrite_to_url(&data.banner)?)
-      .updated(Some(Some(naive_now())))
-      .build();
+  if let Some(discussion_languages) = data.discussion_languages.clone() {
+    SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
+  }
 
-    Site::update(&mut context.pool(), site.id, &site_form)
-      .await
-      // Ignore errors for all these, so as to not throw errors if no update occurs
-      // Diesel will throw an error for empty update forms
-      .ok();
-
-    let application_question = sanitize_html_opt(&data.application_question);
-    let default_theme = sanitize_html_opt(&data.default_theme);
-    let legal_information = sanitize_html_opt(&data.legal_information);
-
-    let local_site_form = LocalSiteUpdateForm::builder()
-      .enable_downvotes(data.enable_downvotes)
-      .registration_mode(data.registration_mode)
-      .enable_nsfw(data.enable_nsfw)
-      .community_creation_admin_only(data.community_creation_admin_only)
-      .require_email_verification(data.require_email_verification)
-      .application_question(diesel_option_overwrite(application_question))
-      .private_instance(data.private_instance)
-      .default_theme(default_theme)
-      .default_post_listing_type(data.default_post_listing_type)
-      .legal_information(diesel_option_overwrite(legal_information))
-      .application_email_admins(data.application_email_admins)
-      .hide_modlog_mod_names(data.hide_modlog_mod_names)
-      .updated(Some(Some(naive_now())))
-      .slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
-      .actor_name_max_length(data.actor_name_max_length)
-      .federation_enabled(data.federation_enabled)
-      .captcha_enabled(data.captcha_enabled)
-      .captcha_difficulty(data.captcha_difficulty.clone())
-      .reports_email_admins(data.reports_email_admins)
-      .build();
-
-    let update_local_site = LocalSite::update(&mut context.pool(), &local_site_form)
-      .await
-      .ok();
-
-    let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
-      .message(data.rate_limit_message)
-      .message_per_second(data.rate_limit_message_per_second)
-      .post(data.rate_limit_post)
-      .post_per_second(data.rate_limit_post_per_second)
-      .register(data.rate_limit_register)
-      .register_per_second(data.rate_limit_register_per_second)
-      .image(data.rate_limit_image)
-      .image_per_second(data.rate_limit_image_per_second)
-      .comment(data.rate_limit_comment)
-      .comment_per_second(data.rate_limit_comment_per_second)
-      .search(data.rate_limit_search)
-      .search_per_second(data.rate_limit_search_per_second)
-      .build();
-
-    LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form)
+  let name = sanitize_html_opt(&data.name);
+  let sidebar = sanitize_html_opt(&data.sidebar);
+  let description = sanitize_html_opt(&data.description);
+
+  let site_form = SiteUpdateForm::builder()
+    .name(name)
+    .sidebar(diesel_option_overwrite(sidebar))
+    .description(diesel_option_overwrite(description))
+    .icon(diesel_option_overwrite_to_url(&data.icon)?)
+    .banner(diesel_option_overwrite_to_url(&data.banner)?)
+    .updated(Some(Some(naive_now())))
+    .build();
+
+  Site::update(&mut context.pool(), site.id, &site_form)
+    .await
+    // Ignore errors for all these, so as to not throw errors if no update occurs
+    // Diesel will throw an error for empty update forms
+    .ok();
+
+  let application_question = sanitize_html_opt(&data.application_question);
+  let default_theme = sanitize_html_opt(&data.default_theme);
+  let legal_information = sanitize_html_opt(&data.legal_information);
+
+  let local_site_form = LocalSiteUpdateForm::builder()
+    .enable_downvotes(data.enable_downvotes)
+    .registration_mode(data.registration_mode)
+    .enable_nsfw(data.enable_nsfw)
+    .community_creation_admin_only(data.community_creation_admin_only)
+    .require_email_verification(data.require_email_verification)
+    .application_question(diesel_option_overwrite(application_question))
+    .private_instance(data.private_instance)
+    .default_theme(default_theme)
+    .default_post_listing_type(data.default_post_listing_type)
+    .legal_information(diesel_option_overwrite(legal_information))
+    .application_email_admins(data.application_email_admins)
+    .hide_modlog_mod_names(data.hide_modlog_mod_names)
+    .updated(Some(Some(naive_now())))
+    .slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
+    .actor_name_max_length(data.actor_name_max_length)
+    .federation_enabled(data.federation_enabled)
+    .captcha_enabled(data.captcha_enabled)
+    .captcha_difficulty(data.captcha_difficulty.clone())
+    .reports_email_admins(data.reports_email_admins)
+    .build();
+
+  let update_local_site = LocalSite::update(&mut context.pool(), &local_site_form)
+    .await
+    .ok();
+
+  let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
+    .message(data.rate_limit_message)
+    .message_per_second(data.rate_limit_message_per_second)
+    .post(data.rate_limit_post)
+    .post_per_second(data.rate_limit_post_per_second)
+    .register(data.rate_limit_register)
+    .register_per_second(data.rate_limit_register_per_second)
+    .image(data.rate_limit_image)
+    .image_per_second(data.rate_limit_image_per_second)
+    .comment(data.rate_limit_comment)
+    .comment_per_second(data.rate_limit_comment_per_second)
+    .search(data.rate_limit_search)
+    .search_per_second(data.rate_limit_search_per_second)
+    .build();
+
+  LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form)
+    .await
+    .ok();
+
+  // Replace the blocked and allowed instances
+  let allowed = data.allowed_instances.clone();
+  FederationAllowList::replace(&mut context.pool(), allowed).await?;
+  let blocked = data.blocked_instances.clone();
+  FederationBlockList::replace(&mut context.pool(), blocked).await?;
+
+  // TODO can't think of a better way to do this.
+  // If the server suddenly requires email verification, or required applications, no old users
+  // will be able to log in. It really only wants this to be a requirement for NEW signups.
+  // So if it was set from false, to true, you need to update all current users columns to be verified.
+
+  let old_require_application =
+    local_site.registration_mode == RegistrationMode::RequireApplication;
+  let new_require_application = update_local_site
+    .as_ref()
+    .map(|ols| ols.registration_mode == RegistrationMode::RequireApplication)
+    .unwrap_or(false);
+  if !old_require_application && new_require_application {
+    LocalUser::set_all_users_registration_applications_accepted(&mut context.pool())
       .await
-      .ok();
-
-    // Replace the blocked and allowed instances
-    let allowed = data.allowed_instances.clone();
-    FederationAllowList::replace(&mut context.pool(), allowed).await?;
-    let blocked = data.blocked_instances.clone();
-    FederationBlockList::replace(&mut context.pool(), blocked).await?;
-
-    // TODO can't think of a better way to do this.
-    // If the server suddenly requires email verification, or required applications, no old users
-    // will be able to log in. It really only wants this to be a requirement for NEW signups.
-    // So if it was set from false, to true, you need to update all current users columns to be verified.
-
-    let old_require_application =
-      local_site.registration_mode == RegistrationMode::RequireApplication;
-    let new_require_application = update_local_site
-      .as_ref()
-      .map(|ols| ols.registration_mode == RegistrationMode::RequireApplication)
-      .unwrap_or(false);
-    if !old_require_application && new_require_application {
-      LocalUser::set_all_users_registration_applications_accepted(&mut context.pool())
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntSetAllRegistrationsAccepted)?;
-    }
-
-    let new_require_email_verification = update_local_site
-      .as_ref()
-      .map(|ols| ols.require_email_verification)
-      .unwrap_or(false);
-    if !local_site.require_email_verification && new_require_email_verification {
-      LocalUser::set_all_users_email_verified(&mut context.pool())
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?;
-    }
+      .with_lemmy_type(LemmyErrorType::CouldntSetAllRegistrationsAccepted)?;
+  }
 
-    let new_taglines = data.taglines.clone();
-    let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
+  let new_require_email_verification = update_local_site
+    .as_ref()
+    .map(|ols| ols.require_email_verification)
+    .unwrap_or(false);
+  if !local_site.require_email_verification && new_require_email_verification {
+    LocalUser::set_all_users_email_verified(&mut context.pool())
+      .await
+      .with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?;
+  }
 
-    let site_view = SiteView::read_local(&mut context.pool()).await?;
+  let new_taglines = data.taglines.clone();
+  let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
 
-    let rate_limit_config =
-      local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
-    context
-      .settings_updated_channel()
-      .send(rate_limit_config)
-      .await?;
+  let site_view = SiteView::read_local(&mut context.pool()).await?;
 
-    let res = SiteResponse {
-      site_view,
-      taglines,
-    };
+  let rate_limit_config =
+    local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
+  context
+    .settings_updated_channel()
+    .send(rate_limit_config)
+    .await?;
 
-    Ok(res)
-  }
+  Ok(Json(SiteResponse {
+    site_view,
+    taglines,
+  }))
 }
 
 fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> LemmyResult<()> {
diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs
index 51b87ed2..fa235a7f 100644
--- a/crates/apub/src/activities/create_or_update/comment.rs
+++ b/crates/apub/src/activities/create_or_update/comment.rs
@@ -25,7 +25,7 @@ use activitypub_federation::{
 };
 use lemmy_api_common::{
   build_response::send_local_notifs,
-  comment::{CommentResponse, CreateComment, EditComment},
+  comment::{CommentResponse, EditComment},
   context::LemmyContext,
   utils::{check_post_deleted_or_removed, is_mod_or_admin},
 };
@@ -43,25 +43,6 @@ use lemmy_db_schema::{
 use lemmy_utils::{error::LemmyError, utils::mention::scrape_text_for_mentions};
 use url::Url;
 
-#[async_trait::async_trait]
-impl SendActivity for CreateComment {
-  type Response = CommentResponse;
-
-  async fn send_activity(
-    _request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    CreateOrUpdateNote::send(
-      &response.comment_view.comment,
-      response.comment_view.creator.id,
-      CreateOrUpdateType::Create,
-      context,
-    )
-    .await
-  }
-}
-
 #[async_trait::async_trait]
 impl SendActivity for EditComment {
   type Response = CommentResponse;
@@ -72,10 +53,10 @@ impl SendActivity for EditComment {
     context: &Data<LemmyContext>,
   ) -> Result<(), LemmyError> {
     CreateOrUpdateNote::send(
-      &response.comment_view.comment,
+      response.comment_view.comment.clone(),
       response.comment_view.creator.id,
       CreateOrUpdateType::Update,
-      context,
+      context.reset_request_count(),
     )
     .await
   }
@@ -83,11 +64,11 @@ impl SendActivity for EditComment {
 
 impl CreateOrUpdateNote {
   #[tracing::instrument(skip(comment, person_id, kind, context))]
-  async fn send(
-    comment: &Comment,
+  pub(crate) async fn send(
+    comment: Comment,
     person_id: PersonId,
     kind: CreateOrUpdateType,
-    context: &Data<LemmyContext>,
+    context: Data<LemmyContext>,
   ) -> Result<(), LemmyError> {
     // TODO: might be helpful to add a comment method to retrieve community directly
     let post_id = comment.post_id;
@@ -102,7 +83,7 @@ impl CreateOrUpdateNote {
       kind.clone(),
       &context.settings().get_protocol_and_hostname(),
     )?;
-    let note = ApubComment(comment.clone()).into_json(context).await?;
+    let note = ApubComment(comment).into_json(&context).await?;
 
     let create_or_update = CreateOrUpdateNote {
       actor: person.id().into(),
@@ -130,12 +111,12 @@ impl CreateOrUpdateNote {
       .collect();
     let mut inboxes = vec![];
     for t in tagged_users {
-      let person = t.dereference(context).await?;
+      let person = t.dereference(&context).await?;
       inboxes.push(person.shared_inbox_or_inbox());
     }
 
     let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update);
-    send_activity_in_community(activity, &person, &community, inboxes, false, context).await
+    send_activity_in_community(activity, &person, &community, inboxes, false, &context).await
   }
 }
 
diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs
index 02ad0b6b..c7d19e37 100644
--- a/crates/apub/src/activities/mod.rs
+++ b/crates/apub/src/activities/mod.rs
@@ -1,6 +1,9 @@
 use crate::{
   objects::{community::ApubCommunity, person::ApubPerson},
-  protocol::activities::{create_or_update::page::CreateOrUpdatePage, CreateOrUpdateType},
+  protocol::activities::{
+    create_or_update::{note::CreateOrUpdateNote, page::CreateOrUpdatePage},
+    CreateOrUpdateType,
+  },
   CONTEXT,
 };
 use activitypub_federation::{
@@ -217,15 +220,17 @@ pub async fn match_outgoing_activities(
   data: SendActivityData,
   context: &Data<LemmyContext>,
 ) -> LemmyResult<()> {
-  let fed_task = match data {
-    SendActivityData::CreatePost(post) => {
-      let creator_id = post.creator_id;
-      CreateOrUpdatePage::send(
-        post,
-        creator_id,
-        CreateOrUpdateType::Create,
-        context.reset_request_count(),
-      )
+  let context = context.reset_request_count();
+  let fed_task = async {
+    match data {
+      SendActivityData::CreatePost(post) => {
+        let creator_id = post.creator_id;
+        CreateOrUpdatePage::send(post, creator_id, CreateOrUpdateType::Create, context).await
+      }
+      SendActivityData::CreateComment(comment) => {
+        let creator_id = comment.creator_id;
+        CreateOrUpdateNote::send(comment, creator_id, CreateOrUpdateType::Create, context).await
+      }
     }
   };
   if *SYNCHRONOUS_FEDERATION {
diff --git a/crates/apub/src/api/read_community.rs b/crates/apub/src/api/read_community.rs
index 12e17dac..1bdfb88a 100644
--- a/crates/apub/src/api/read_community.rs
+++ b/crates/apub/src/api/read_community.rs
@@ -16,7 +16,7 @@ use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType};
 
 #[tracing::instrument(skip(context))]
-pub async fn read_community(
+pub async fn get_community(
   data: Query<GetCommunity>,
   context: Data<LemmyContext>,
 ) -> Result<Json<GetCommunityResponse>, LemmyError> {
diff --git a/crates/utils/translations b/crates/utils/translations
index 713ceed9..1c42c579 160000
--- a/crates/utils/translations
+++ b/crates/utils/translations
@@ -1 +1 @@
-Subproject commit 713ceed9c7ef84deaa222e68361e670e0763cd83
+Subproject commit 1c42c579460871de7b4ea18e58dc25543b80d289
diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs
index bc4340e3..e372e340 100644
--- a/src/api_routes_http.rs
+++ b/src/api_routes_http.rs
@@ -1,19 +1,12 @@
 use actix_web::{guard, web, Error, HttpResponse, Result};
-use lemmy_api::Perform;
+use lemmy_api::{
+  comment::{distinguish::distinguish_comment, save::save_comment},
+  comment_report::{list::list_comment_reports, resolve::resolve_comment_report},
+  local_user::notifications::mark_reply_read::mark_reply_as_read,
+  Perform,
+};
 use lemmy_api_common::{
-  comment::{
-    CreateComment,
-    CreateCommentLike,
-    CreateCommentReport,
-    DeleteComment,
-    DistinguishComment,
-    EditComment,
-    GetComment,
-    ListCommentReports,
-    RemoveComment,
-    ResolveCommentReport,
-    SaveComment,
-  },
+  comment::{CreateCommentLike, CreateCommentReport, DeleteComment, EditComment, RemoveComment},
   community::{
     AddModToCommunity,
     BanFromCommunity,
@@ -23,7 +16,6 @@ use lemmy_api_common::{
     EditCommunity,
     FollowCommunity,
     HideCommunity,
-    ListCommunities,
     RemoveCommunity,
     TransferCommunity,
   },
@@ -43,7 +35,6 @@ use lemmy_api_common::{
     GetUnreadCount,
     Login,
     MarkAllAsRead,
-    MarkCommentReplyAsRead,
     MarkPersonMentionAsRead,
     PasswordChangeAfterReset,
     PasswordReset,
@@ -57,7 +48,6 @@ use lemmy_api_common::{
     DeletePost,
     EditPost,
     FeaturePost,
-    GetPost,
     GetSiteMetadata,
     ListPostReports,
     LockPost,
@@ -71,18 +61,14 @@ use lemmy_api_common::{
     CreatePrivateMessageReport,
     DeletePrivateMessage,
     EditPrivateMessage,
-    GetPrivateMessages,
     ListPrivateMessageReports,
     MarkPrivateMessageAsRead,
     ResolvePrivateMessageReport,
   },
   site::{
     ApproveRegistrationApplication,
-    CreateSite,
-    EditSite,
     GetFederatedInstances,
     GetModlog,
-    GetSite,
     GetUnreadRegistrationApplicationCount,
     LeaveAdmin,
     ListRegistrationApplications,
@@ -92,12 +78,19 @@ use lemmy_api_common::{
     PurgePost,
   },
 };
-use lemmy_api_crud::{post::create::create_post, PerformCrud};
+use lemmy_api_crud::{
+  comment::{create::create_comment, read::get_comment},
+  community::list::list_communities,
+  post::{create::create_post, read::get_post},
+  private_message::read::get_private_message,
+  site::{create::create_site, read::get_site, update::update_site},
+  PerformCrud,
+};
 use lemmy_apub::{
   api::{
     list_comments::list_comments,
     list_posts::list_posts,
-    read_community::read_community,
+    read_community::get_community,
     read_person::read_person,
     resolve_object::resolve_object,
     search::search,
@@ -114,10 +107,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
       .service(
         web::scope("/site")
           .wrap(rate_limit.message())
-          .route("", web::get().to(route_get_crud::<GetSite>))
+          .route("", web::get().to(get_site))
           // Admin Actions
-          .route("", web::post().to(route_post_crud::<CreateSite>))
-          .route("", web::put().to(route_post_crud::<EditSite>)),
+          .route("", web::post().to(create_site))
+          .route("", web::put().to(update_site)),
       )
       .service(
         web::resource("/modlog")
@@ -144,10 +137,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
       .service(
         web::scope("/community")
           .wrap(rate_limit.message())
-          .route("", web::get().to(read_community))
+          .route("", web::get().to(get_community))
           .route("", web::put().to(route_post_crud::<EditCommunity>))
           .route("/hide", web::put().to(route_post::<HideCommunity>))
-          .route("/list", web::get().to(route_get_crud::<ListCommunities>))
+          .route("/list", web::get().to(list_communities))
           .route("/follow", web::post().to(route_post::<FollowCommunity>))
           .route("/block", web::post().to(route_post::<BlockCommunity>))
           .route(
@@ -179,7 +172,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
       .service(
         web::scope("/post")
           .wrap(rate_limit.message())
-          .route("", web::get().to(route_get_crud::<GetPost>))
+          .route("", web::get().to(get_post))
           .route("", web::put().to(route_post_crud::<EditPost>))
           .route("/delete", web::post().to(route_post_crud::<DeletePost>))
           .route("/remove", web::post().to(route_post_crud::<RemovePost>))
@@ -209,41 +202,29 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
         web::resource("/comment")
           .guard(guard::Post())
           .wrap(rate_limit.comment())
-          .route(web::post().to(route_post_crud::<CreateComment>)),
+          .route(web::post().to(create_comment)),
       )
       .service(
         web::scope("/comment")
           .wrap(rate_limit.message())
-          .route("", web::get().to(route_get_crud::<GetComment>))
+          .route("", web::get().to(get_comment))
           .route("", web::put().to(route_post_crud::<EditComment>))
           .route("/delete", web::post().to(route_post_crud::<DeleteComment>))
           .route("/remove", web::post().to(route_post_crud::<RemoveComment>))
-          .route(
-            "/mark_as_read",
-            web::post().to(route_post::<MarkCommentReplyAsRead>),
-          )
-          .route(
-            "/distinguish",
-            web::post().to(route_post::<DistinguishComment>),
-          )
+          .route("/mark_as_read", web::post().to(mark_reply_as_read))
+          .route("/distinguish", web::post().to(distinguish_comment))
           .route("/like", web::post().to(route_post::<CreateCommentLike>))
-          .route("/save", web::put().to(route_post::<SaveComment>))
+          .route("/save", web::put().to(save_comment))
           .route("/list", web::get().to(list_comments))
           .route("/report", web::post().to(route_post::<CreateCommentReport>))
-          .route(
-            "/report/resolve",
-            web::put().to(route_post::<ResolveCommentReport>),
-          )
-          .route(
-            "/report/list",
-            web::get().to(route_get::<ListCommentReports>),
-          ),
+          .route("/report/resolve", web::put().to(resolve_comment_report))
+          .route("/report/list", web::get().to(list_comment_reports)),
       )
       // Private Message
       .service(
         web::scope("/private_message")
           .wrap(rate_limit.message())
-          .route("/list", web::get().to(route_get_crud::<GetPrivateMessages>))
+          .route("/list", web::get().to(get_private_message))
           .route("", web::post().to(route_post_crud::<CreatePrivateMessage>))
           .route("", web::put().to(route_post_crud::<EditPrivateMessage>))
           .route(
@@ -447,22 +428,6 @@ where
   Ok(HttpResponse::Ok().json(&res))
 }
 
-async fn route_get_crud<'a, Data>(
-  data: web::Query<Data>,
-  context: web::Data<LemmyContext>,
-  apub_data: activitypub_federation::config::Data<LemmyContext>,
-) -> Result<HttpResponse, Error>
-where
-  Data: PerformCrud
-    + SendActivity<Response = <Data as PerformCrud>::Response>
-    + Clone
-    + Deserialize<'a>
-    + Send
-    + 'static,
-{
-  perform_crud::<Data>(data.0, context, apub_data).await
-}
-
 async fn route_post_crud<'a, Data>(
   data: web::Json<Data>,
   context: web::Data<LemmyContext>,