From: Nutomic <me@nutomic.com>
Date: Wed, 2 Aug 2023 16:52:41 +0000 (+0200)
Subject: Rewrite remaining federation actions, get rid of PerformCrud trait (#3794)
X-Git-Url: http://these/git/%22%7Bauthor_url%7D/static/%7Bthis.captchaPngSrc%28%29%7D?a=commitdiff_plain;h=27be1efb74d6761046999980561a343d06235674;p=lemmy.git

Rewrite remaining federation actions, get rid of PerformCrud trait (#3794)

* Rewrite ban actions

* Rewrite delete/remove actions

* Rewrite remove/delete community

* Rewrite report actions

* Rewrite feature/lock post

* Rewrite update community actions

* Rewrite remaining federation actions

* Get rid of PerformCrud trait

* clippy
---

diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh
index 3aae16bd..ef113328 100755
--- a/api_tests/prepare-drone-federation-test.sh
+++ b/api_tests/prepare-drone-federation-test.sh
@@ -30,9 +30,6 @@ else
   done
 fi
 
-echo "killall existing lemmy_server processes"
-killall -s1 lemmy_server || true
-
 echo "$PWD"
 
 echo "start alpha"
diff --git a/api_tests/run-federation-test.sh b/api_tests/run-federation-test.sh
index f611cce6..ff74744a 100755
--- a/api_tests/run-federation-test.sh
+++ b/api_tests/run-federation-test.sh
@@ -7,12 +7,14 @@ pushd ..
 cargo build
 rm target/lemmy_server || true
 cp target/debug/lemmy_server target/lemmy_server
+killall -s1 lemmy_server || true
 ./api_tests/prepare-drone-federation-test.sh
 popd
 
 yarn
 yarn api-test || true
 
+killall -s1 lemmy_server || true
 for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
   psql "$LEMMY_DATABASE_URL" -c "DROP DATABASE $INSTANCE"
 done
diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs
index 190e47a1..2ea973d3 100644
--- a/crates/api/src/comment_report/create.rs
+++ b/crates/api/src/comment_report/create.rs
@@ -1,8 +1,10 @@
-use crate::{check_report_reason, Perform};
-use actix_web::web::Data;
+use crate::check_report_reason;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   comment::{CommentReportResponse, CreateCommentReport},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{
     check_community_ban,
     local_user_view_from_jwt,
@@ -21,55 +23,60 @@ use lemmy_db_views::structs::{CommentReportView, CommentView};
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
 /// 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))]
+pub async fn create_comment_report(
+  data: Json<CreateCommentReport>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommentReportResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<CommentReportResponse, LemmyError> {
-    let data: &CreateCommentReport = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
+  let reason = sanitize_html(data.reason.trim());
+  check_report_reason(&reason, &local_site)?;
 
-    let reason = sanitize_html(self.reason.trim());
-    check_report_reason(&reason, &local_site)?;
+  let person_id = local_user_view.person.id;
+  let comment_id = data.comment_id;
+  let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?;
 
-    let person_id = local_user_view.person.id;
-    let comment_id = data.comment_id;
-    let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?;
+  check_community_ban(person_id, comment_view.community.id, &mut context.pool()).await?;
 
-    check_community_ban(person_id, comment_view.community.id, &mut context.pool()).await?;
+  let report_form = CommentReportForm {
+    creator_id: person_id,
+    comment_id,
+    original_comment_text: comment_view.comment.content,
+    reason,
+  };
 
-    let report_form = CommentReportForm {
-      creator_id: person_id,
-      comment_id,
-      original_comment_text: comment_view.comment.content,
-      reason,
-    };
+  let report = CommentReport::report(&mut context.pool(), &report_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
 
-    let report = CommentReport::report(&mut context.pool(), &report_form)
-      .await
-      .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
+  let comment_report_view =
+    CommentReportView::read(&mut context.pool(), report.id, person_id).await?;
 
-    let comment_report_view =
-      CommentReportView::read(&mut context.pool(), report.id, person_id).await?;
+  // Email the admins
+  if local_site.reports_email_admins {
+    send_new_report_email_to_admins(
+      &comment_report_view.creator.name,
+      &comment_report_view.comment_creator.name,
+      &mut context.pool(),
+      context.settings(),
+    )
+    .await?;
+  }
 
-    // Email the admins
-    if local_site.reports_email_admins {
-      send_new_report_email_to_admins(
-        &comment_report_view.creator.name,
-        &comment_report_view.comment_creator.name,
-        &mut context.pool(),
-        context.settings(),
-      )
-      .await?;
-    }
+  ActivityChannel::submit_activity(
+    SendActivityData::CreateReport(
+      comment_view.comment.ap_id.inner().clone(),
+      local_user_view.person,
+      comment_view.community,
+      data.reason.clone(),
+    ),
+    &context,
+  )
+  .await?;
 
-    Ok(CommentReportResponse {
-      comment_report_view,
-    })
-  }
+  Ok(Json(CommentReportResponse {
+    comment_report_view,
+  }))
 }
diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs
index 08620777..2d7b8875 100644
--- a/crates/api/src/community/add_mod.rs
+++ b/crates/api/src/community/add_mod.rs
@@ -1,8 +1,9 @@
-use crate::Perform;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   community::{AddModToCommunity, AddModToCommunityResponse},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{is_mod_or_admin, local_user_view_from_jwt},
 };
 use lemmy_db_schema::{
@@ -15,58 +16,62 @@ use lemmy_db_schema::{
 use lemmy_db_views_actor::structs::CommunityModeratorView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl Perform for AddModToCommunity {
-  type Response = AddModToCommunityResponse;
+#[tracing::instrument(skip(context))]
+pub async fn add_mod_to_community(
+  data: Json<AddModToCommunity>,
+  context: Data<LemmyContext>,
+) -> Result<Json<AddModToCommunityResponse>, 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<AddModToCommunityResponse, LemmyError> {
-    let data: &AddModToCommunity = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let community_id = data.community_id;
 
-    let community_id = data.community_id;
+  // Verify that only mods or admins can add mod
+  is_mod_or_admin(&mut context.pool(), local_user_view.person.id, community_id).await?;
+  let community = Community::read(&mut context.pool(), community_id).await?;
+  if local_user_view.person.admin && !community.local {
+    return Err(LemmyErrorType::NotAModerator)?;
+  }
 
-    // Verify that only mods or admins can add mod
-    is_mod_or_admin(&mut context.pool(), local_user_view.person.id, community_id).await?;
-    let community = Community::read(&mut context.pool(), community_id).await?;
-    if local_user_view.person.admin && !community.local {
-      return Err(LemmyErrorType::NotAModerator)?;
-    }
+  // Update in local database
+  let community_moderator_form = CommunityModeratorForm {
+    community_id: data.community_id,
+    person_id: data.person_id,
+  };
+  if data.added {
+    CommunityModerator::join(&mut context.pool(), &community_moderator_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
+  } else {
+    CommunityModerator::leave(&mut context.pool(), &community_moderator_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
+  }
 
-    // Update in local database
-    let community_moderator_form = CommunityModeratorForm {
-      community_id: data.community_id,
-      person_id: data.person_id,
-    };
-    if data.added {
-      CommunityModerator::join(&mut context.pool(), &community_moderator_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
-    } else {
-      CommunityModerator::leave(&mut context.pool(), &community_moderator_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
-    }
+  // 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),
+  };
 
-    // 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),
-    };
+  ModAddCommunity::create(&mut context.pool(), &form).await?;
 
-    ModAddCommunity::create(&mut context.pool(), &form).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 = CommunityModeratorView::for_community(&mut context.pool(), community_id).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 =
-      CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
+  ActivityChannel::submit_activity(
+    SendActivityData::AddModToCommunity(
+      local_user_view.person,
+      data.community_id,
+      data.person_id,
+      data.added,
+    ),
+    &context,
+  )
+  .await?;
 
-    Ok(AddModToCommunityResponse { moderators })
-  }
+  Ok(Json(AddModToCommunityResponse { moderators }))
 }
diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs
index 95c2bbc0..d04a8a0a 100644
--- a/crates/api/src/community/ban.rs
+++ b/crates/api/src/community/ban.rs
@@ -1,8 +1,9 @@
-use crate::Perform;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   community::{BanFromCommunity, BanFromCommunityResponse},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{
     is_mod_or_admin,
     local_user_view_from_jwt,
@@ -28,77 +29,85 @@ use lemmy_utils::{
   utils::{time::naive_from_unix, validation::is_valid_body_field},
 };
 
-#[async_trait::async_trait(?Send)]
-impl Perform for BanFromCommunity {
-  type Response = BanFromCommunityResponse;
+#[tracing::instrument(skip(context))]
+pub async fn ban_from_community(
+  data: Json<BanFromCommunity>,
+  context: Data<LemmyContext>,
+) -> Result<Json<BanFromCommunityResponse>, 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<BanFromCommunityResponse, LemmyError> {
-    let data: &BanFromCommunity = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let banned_person_id = data.person_id;
+  let remove_data = data.remove_data.unwrap_or(false);
+  let expires = data.expires.map(naive_from_unix);
 
-    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(
+    &mut context.pool(),
+    local_user_view.person.id,
+    data.community_id,
+  )
+  .await?;
+  is_valid_body_field(&data.reason, false)?;
 
-    // Verify that only mods or admins can ban
-    is_mod_or_admin(&mut context.pool(), local_user_view.person.id, community_id).await?;
-    is_valid_body_field(&data.reason, false)?;
+  let community_user_ban_form = CommunityPersonBanForm {
+    community_id: data.community_id,
+    person_id: data.person_id,
+    expires: Some(expires),
+  };
 
-    let community_user_ban_form = CommunityPersonBanForm {
+  if data.ban {
+    CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
+
+    // Also unsubscribe them from the community, if they are subscribed
+    let community_follower_form = CommunityFollowerForm {
       community_id: data.community_id,
-      person_id: data.person_id,
-      expires: Some(expires),
+      person_id: banned_person_id,
+      pending: false,
     };
 
-    if data.ban {
-      CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
+    CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
+      .await
+      .ok();
+  } else {
+    CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
+  }
 
-      // 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,
-      };
+  // Remove/Restore their data if that's desired
+  if remove_data {
+    remove_user_data_in_community(data.community_id, banned_person_id, &mut context.pool()).await?;
+  }
 
-      CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
-        .await
-        .ok();
-    } else {
-      CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
-    }
+  // 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: sanitize_html_opt(&data.reason),
+    banned: Some(data.ban),
+    expires,
+  };
 
-    // Remove/Restore their data if that's desired
-    if remove_data {
-      remove_user_data_in_community(community_id, banned_person_id, &mut context.pool()).await?;
-    }
+  ModBanFromCommunity::create(&mut context.pool(), &form).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: sanitize_html_opt(&data.reason),
-      banned: Some(data.ban),
-      expires,
-    };
-
-    ModBanFromCommunity::create(&mut context.pool(), &form).await?;
+  let person_view = PersonView::read(&mut context.pool(), data.person_id).await?;
 
-    let person_id = data.person_id;
-    let person_view = PersonView::read(&mut context.pool(), person_id).await?;
+  ActivityChannel::submit_activity(
+    SendActivityData::BanFromCommunity(
+      local_user_view.person,
+      data.community_id,
+      person_view.person.clone(),
+      data.0.clone(),
+    ),
+    &context,
+  )
+  .await?;
 
-    Ok(BanFromCommunityResponse {
-      person_view,
-      banned: data.ban,
-    })
-  }
+  Ok(Json(BanFromCommunityResponse {
+    person_view,
+    banned: data.ban,
+  }))
 }
diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs
index 66d6adac..f807574e 100644
--- a/crates/api/src/community/block.rs
+++ b/crates/api/src/community/block.rs
@@ -1,8 +1,9 @@
-use crate::Perform;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   community::{BlockCommunity, BlockCommunityResponse},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::local_user_view_from_jwt,
 };
 use lemmy_db_schema::{
@@ -15,52 +16,56 @@ use lemmy_db_schema::{
 use lemmy_db_views_actor::structs::CommunityView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl Perform for BlockCommunity {
-  type Response = BlockCommunityResponse;
+#[tracing::instrument(skip(context))]
+pub async fn block_community(
+  data: Json<BlockCommunity>,
+  context: Data<LemmyContext>,
+) -> Result<Json<BlockCommunityResponse>, 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<BlockCommunityResponse, LemmyError> {
-    let data: &BlockCommunity = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let community_id = data.community_id;
+  let person_id = local_user_view.person.id;
+  let community_block_form = CommunityBlockForm {
+    person_id,
+    community_id,
+  };
 
-    let community_id = data.community_id;
-    let person_id = local_user_view.person.id;
-    let community_block_form = CommunityBlockForm {
+  if data.block {
+    CommunityBlock::block(&mut context.pool(), &community_block_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
+
+    // Also, unfollow the community, and send a federated unfollow
+    let community_follower_form = CommunityFollowerForm {
+      community_id: data.community_id,
       person_id,
-      community_id,
+      pending: false,
     };
 
-    if data.block {
-      CommunityBlock::block(&mut context.pool(), &community_block_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
-
-      // Also, unfollow the community, and send a federated unfollow
-      let community_follower_form = CommunityFollowerForm {
-        community_id: data.community_id,
-        person_id,
-        pending: false,
-      };
+    CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
+      .await
+      .ok();
+  } else {
+    CommunityBlock::unblock(&mut context.pool(), &community_block_form)
+      .await
+      .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
+  }
 
-      CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
-        .await
-        .ok();
-    } else {
-      CommunityBlock::unblock(&mut context.pool(), &community_block_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
-    }
+  let community_view =
+    CommunityView::read(&mut context.pool(), community_id, Some(person_id), None).await?;
 
-    let community_view =
-      CommunityView::read(&mut context.pool(), community_id, Some(person_id), None).await?;
+  ActivityChannel::submit_activity(
+    SendActivityData::FollowCommunity(
+      community_view.community.clone(),
+      local_user_view.person.clone(),
+      false,
+    ),
+    &context,
+  )
+  .await?;
 
-    Ok(BlockCommunityResponse {
-      blocked: data.block,
-      community_view,
-    })
-  }
+  Ok(Json(BlockCommunityResponse {
+    blocked: data.block,
+    community_view,
+  }))
 }
diff --git a/crates/api/src/community/hide.rs b/crates/api/src/community/hide.rs
index 4c05a71c..ce437ad9 100644
--- a/crates/api/src/community/hide.rs
+++ b/crates/api/src/community/hide.rs
@@ -1,9 +1,10 @@
-use crate::Perform;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   build_response::build_community_response,
   community::{CommunityResponse, HideCommunity},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{is_admin, local_user_view_from_jwt, sanitize_html_opt},
 };
 use lemmy_db_schema::{
@@ -15,36 +16,38 @@ use lemmy_db_schema::{
 };
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl Perform for HideCommunity {
-  type Response = CommunityResponse;
-
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommunityResponse, LemmyError> {
-    let data: &HideCommunity = self;
-
-    // Verify its a admin (only admin can hide or unhide it)
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-    is_admin(&local_user_view)?;
-
-    let community_form = CommunityUpdateForm::builder()
-      .hidden(Some(data.hidden))
-      .build();
-
-    let mod_hide_community_form = ModHideCommunityForm {
-      community_id: data.community_id,
-      mod_person_id: local_user_view.person.id,
-      reason: sanitize_html_opt(&data.reason),
-      hidden: Some(data.hidden),
-    };
-
-    let community_id = data.community_id;
-    Community::update(&mut context.pool(), community_id, &community_form)
-      .await
-      .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunityHiddenStatus)?;
-
-    ModHideCommunity::create(&mut context.pool(), &mod_hide_community_form).await?;
-
-    build_community_response(context, local_user_view, community_id).await
-  }
+#[tracing::instrument(skip(context))]
+pub async fn hide_community(
+  data: Json<HideCommunity>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommunityResponse>, LemmyError> {
+  // Verify its a admin (only admin can hide or unhide it)
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  is_admin(&local_user_view)?;
+
+  let community_form = CommunityUpdateForm::builder()
+    .hidden(Some(data.hidden))
+    .build();
+
+  let mod_hide_community_form = ModHideCommunityForm {
+    community_id: data.community_id,
+    mod_person_id: local_user_view.person.id,
+    reason: sanitize_html_opt(&data.reason),
+    hidden: Some(data.hidden),
+  };
+
+  let community_id = data.community_id;
+  let community = Community::update(&mut context.pool(), community_id, &community_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunityHiddenStatus)?;
+
+  ModHideCommunity::create(&mut context.pool(), &mod_hide_community_form).await?;
+
+  ActivityChannel::submit_activity(
+    SendActivityData::UpdateCommunity(local_user_view.person.clone(), community),
+    &context,
+  )
+  .await?;
+
+  build_community_response(&context, local_user_view, community_id).await
 }
diff --git a/crates/api/src/community/mod.rs b/crates/api/src/community/mod.rs
index fc3ef67c..47819222 100644
--- a/crates/api/src/community/mod.rs
+++ b/crates/api/src/community/mod.rs
@@ -1,6 +1,6 @@
-mod add_mod;
-mod ban;
-mod block;
+pub mod add_mod;
+pub mod ban;
+pub mod block;
 pub mod follow;
-mod hide;
-mod transfer;
+pub mod hide;
+pub mod transfer;
diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs
index 77e8e805..6e99ef25 100644
--- a/crates/api/src/local_user/ban_person.rs
+++ b/crates/api/src/local_user/ban_person.rs
@@ -1,8 +1,9 @@
-use crate::Perform;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   person::{BanPerson, BanPersonResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{is_admin, local_user_view_from_jwt, remove_user_data, sanitize_html_opt},
 };
 use lemmy_db_schema::{
@@ -17,65 +18,68 @@ use lemmy_utils::{
   error::{LemmyError, LemmyErrorExt, LemmyErrorType},
   utils::{time::naive_from_unix, validation::is_valid_body_field},
 };
+#[tracing::instrument(skip(context))]
+pub async fn ban_from_site(
+  data: Json<BanPerson>,
+  context: Data<LemmyContext>,
+) -> Result<Json<BanPersonResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-#[async_trait::async_trait(?Send)]
-impl Perform for BanPerson {
-  type Response = BanPersonResponse;
+  // Make sure user is an admin
+  is_admin(&local_user_view)?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<BanPersonResponse, LemmyError> {
-    let data: &BanPerson = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  is_valid_body_field(&data.reason, false)?;
 
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
+  let expires = data.expires.map(naive_from_unix);
 
-    is_valid_body_field(&data.reason, false)?;
+  let person = Person::update(
+    &mut context.pool(),
+    data.person_id,
+    &PersonUpdateForm::builder()
+      .banned(Some(data.ban))
+      .ban_expires(Some(expires))
+      .build(),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
 
-    let ban = data.ban;
-    let banned_person_id = data.person_id;
-    let expires = data.expires.map(naive_from_unix);
-
-    let person = Person::update(
+  // Remove their data if that's desired
+  let remove_data = data.remove_data.unwrap_or(false);
+  if remove_data {
+    remove_user_data(
+      person.id,
       &mut context.pool(),
-      banned_person_id,
-      &PersonUpdateForm::builder()
-        .banned(Some(ban))
-        .ban_expires(Some(expires))
-        .build(),
+      context.settings(),
+      context.client(),
     )
-    .await
-    .with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
+    .await?;
+  }
 
-    // Remove their data if that's desired
-    let remove_data = data.remove_data.unwrap_or(false);
-    if remove_data {
-      remove_user_data(
-        person.id,
-        &mut context.pool(),
-        context.settings(),
-        context.client(),
-      )
-      .await?;
-    }
+  // Mod tables
+  let form = ModBanForm {
+    mod_person_id: local_user_view.person.id,
+    other_person_id: data.person_id,
+    reason: sanitize_html_opt(&data.reason),
+    banned: Some(data.ban),
+    expires,
+  };
 
-    // Mod tables
-    let form = ModBanForm {
-      mod_person_id: local_user_view.person.id,
-      other_person_id: data.person_id,
-      reason: sanitize_html_opt(&data.reason),
-      banned: Some(data.ban),
-      expires,
-    };
+  ModBan::create(&mut context.pool(), &form).await?;
 
-    ModBan::create(&mut context.pool(), &form).await?;
+  let person_view = PersonView::read(&mut context.pool(), data.person_id).await?;
 
-    let person_id = data.person_id;
-    let person_view = PersonView::read(&mut context.pool(), person_id).await?;
+  ActivityChannel::submit_activity(
+    SendActivityData::BanFromSite(
+      local_user_view.person,
+      person_view.person.clone(),
+      data.0.clone(),
+    ),
+    &context,
+  )
+  .await?;
 
-    Ok(BanPersonResponse {
-      person_view,
-      banned: data.ban,
-    })
-  }
+  Ok(Json(BanPersonResponse {
+    person_view,
+    banned: data.ban,
+  }))
 }
diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs
index c59eba31..5e18c2b6 100644
--- a/crates/api/src/post/feature.rs
+++ b/crates/api/src/post/feature.rs
@@ -1,9 +1,10 @@
-use crate::Perform;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   build_response::build_post_response,
   context::LemmyContext,
   post::{FeaturePost, PostResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{
     check_community_ban,
     check_community_deleted_or_removed,
@@ -22,67 +23,65 @@ use lemmy_db_schema::{
 };
 use lemmy_utils::error::LemmyError;
 
-#[async_trait::async_trait(?Send)]
-impl Perform for FeaturePost {
-  type Response = PostResponse;
+#[tracing::instrument(skip(context))]
+pub async fn feature_post(
+  data: Json<FeaturePost>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PostResponse>, 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<PostResponse, LemmyError> {
-    let data: &FeaturePost = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let post_id = data.post_id;
+  let orig_post = Post::read(&mut context.pool(), post_id).await?;
 
-    let post_id = data.post_id;
-    let orig_post = Post::read(&mut context.pool(), post_id).await?;
+  check_community_ban(
+    local_user_view.person.id,
+    orig_post.community_id,
+    &mut context.pool(),
+  )
+  .await?;
+  check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
 
-    check_community_ban(
+  if data.feature_type == PostFeatureType::Community {
+    // Verify that only the mods can feature in community
+    is_mod_or_admin(
+      &mut context.pool(),
       local_user_view.person.id,
       orig_post.community_id,
-      &mut context.pool(),
     )
     .await?;
-    check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
+  } else {
+    is_admin(&local_user_view)?;
+  }
 
-    if data.feature_type == PostFeatureType::Community {
-      // Verify that only the mods can feature in community
-      is_mod_or_admin(
-        &mut context.pool(),
-        local_user_view.person.id,
-        orig_post.community_id,
-      )
-      .await?;
-    } else {
-      is_admin(&local_user_view)?;
-    }
+  // Update the post
+  let post_id = data.post_id;
+  let new_post: PostUpdateForm = if data.feature_type == PostFeatureType::Community {
+    PostUpdateForm::builder()
+      .featured_community(Some(data.featured))
+      .build()
+  } else {
+    PostUpdateForm::builder()
+      .featured_local(Some(data.featured))
+      .build()
+  };
+  let post = Post::update(&mut context.pool(), post_id, &new_post).await?;
 
-    // Update the post
-    let post_id = data.post_id;
-    let new_post: PostUpdateForm = if data.feature_type == PostFeatureType::Community {
-      PostUpdateForm::builder()
-        .featured_community(Some(data.featured))
-        .build()
-    } else {
-      PostUpdateForm::builder()
-        .featured_local(Some(data.featured))
-        .build()
-    };
-    Post::update(&mut context.pool(), post_id, &new_post).await?;
+  // Mod tables
+  let form = ModFeaturePostForm {
+    mod_person_id: local_user_view.person.id,
+    post_id: data.post_id,
+    featured: data.featured,
+    is_featured_community: data.feature_type == PostFeatureType::Community,
+  };
 
-    // Mod tables
-    let form = ModFeaturePostForm {
-      mod_person_id: local_user_view.person.id,
-      post_id: data.post_id,
-      featured: data.featured,
-      is_featured_community: data.feature_type == PostFeatureType::Community,
-    };
+  ModFeaturePost::create(&mut context.pool(), &form).await?;
 
-    ModFeaturePost::create(&mut context.pool(), &form).await?;
+  let person_id = local_user_view.person.id;
+  ActivityChannel::submit_activity(
+    SendActivityData::FeaturePost(post, local_user_view.person, data.featured),
+    &context,
+  )
+  .await?;
 
-    build_post_response(
-      context,
-      orig_post.community_id,
-      local_user_view.person.id,
-      post_id,
-    )
-    .await
-  }
+  build_post_response(&context, orig_post.community_id, person_id, post_id).await
 }
diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs
index 1ff119f0..b9b2bede 100644
--- a/crates/api/src/post/like.rs
+++ b/crates/api/src/post/like.rs
@@ -80,13 +80,11 @@ pub async fn like_post(
   )
   .await?;
 
-  Ok(Json(
-    build_post_response(
-      context.deref(),
-      post.community_id,
-      local_user_view.person.id,
-      post_id,
-    )
-    .await?,
-  ))
+  build_post_response(
+    context.deref(),
+    post.community_id,
+    local_user_view.person.id,
+    post_id,
+  )
+  .await
 }
diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs
index 627e9d8e..45ec3317 100644
--- a/crates/api/src/post/lock.rs
+++ b/crates/api/src/post/lock.rs
@@ -1,9 +1,10 @@
-use crate::Perform;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   build_response::build_post_response,
   context::LemmyContext,
   post::{LockPost, PostResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{
     check_community_ban,
     check_community_deleted_or_removed,
@@ -20,58 +21,56 @@ use lemmy_db_schema::{
 };
 use lemmy_utils::error::LemmyError;
 
-#[async_trait::async_trait(?Send)]
-impl Perform for LockPost {
-  type Response = PostResponse;
+#[tracing::instrument(skip(context))]
+pub async fn lock_post(
+  data: Json<LockPost>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PostResponse>, 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<PostResponse, LemmyError> {
-    let data: &LockPost = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let post_id = data.post_id;
+  let orig_post = Post::read(&mut context.pool(), post_id).await?;
 
-    let post_id = data.post_id;
-    let orig_post = Post::read(&mut context.pool(), post_id).await?;
+  check_community_ban(
+    local_user_view.person.id,
+    orig_post.community_id,
+    &mut context.pool(),
+  )
+  .await?;
+  check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
 
-    check_community_ban(
-      local_user_view.person.id,
-      orig_post.community_id,
-      &mut context.pool(),
-    )
-    .await?;
-    check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
+  // Verify that only the mods can lock
+  is_mod_or_admin(
+    &mut context.pool(),
+    local_user_view.person.id,
+    orig_post.community_id,
+  )
+  .await?;
 
-    // Verify that only the mods can lock
-    is_mod_or_admin(
-      &mut 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 post = Post::update(
+    &mut context.pool(),
+    post_id,
+    &PostUpdateForm::builder().locked(Some(locked)).build(),
+  )
+  .await?;
 
-    // Update the post
-    let post_id = data.post_id;
-    let locked = data.locked;
-    Post::update(
-      &mut context.pool(),
-      post_id,
-      &PostUpdateForm::builder().locked(Some(locked)).build(),
-    )
-    .await?;
+  // Mod tables
+  let form = ModLockPostForm {
+    mod_person_id: local_user_view.person.id,
+    post_id: data.post_id,
+    locked: Some(locked),
+  };
+  ModLockPost::create(&mut context.pool(), &form).await?;
 
-    // Mod tables
-    let form = ModLockPostForm {
-      mod_person_id: local_user_view.person.id,
-      post_id: data.post_id,
-      locked: Some(locked),
-    };
-    ModLockPost::create(&mut context.pool(), &form).await?;
+  let person_id = local_user_view.person.id;
+  ActivityChannel::submit_activity(
+    SendActivityData::LockPost(post, local_user_view.person, data.locked),
+    &context,
+  )
+  .await?;
 
-    build_post_response(
-      context,
-      orig_post.community_id,
-      local_user_view.person.id,
-      post_id,
-    )
-    .await
-  }
+  build_post_response(&context, orig_post.community_id, person_id, post_id).await
 }
diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs
index a4081015..68eb8f7b 100644
--- a/crates/api/src/post_report/create.rs
+++ b/crates/api/src/post_report/create.rs
@@ -1,8 +1,10 @@
-use crate::{check_report_reason, Perform};
-use actix_web::web::Data;
+use crate::check_report_reason;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   post::{CreatePostReport, PostReportResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{
     check_community_ban,
     local_user_view_from_jwt,
@@ -21,51 +23,59 @@ use lemmy_db_views::structs::{PostReportView, PostView};
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
 /// 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))]
+pub async fn create_post_report(
+  data: Json<CreatePostReport>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PostReportResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<PostReportResponse, LemmyError> {
-    let data: &CreatePostReport = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
+  let reason = sanitize_html(data.reason.trim());
+  check_report_reason(&reason, &local_site)?;
 
-    let reason = sanitize_html(self.reason.trim());
-    check_report_reason(&reason, &local_site)?;
+  let person_id = local_user_view.person.id;
+  let post_id = data.post_id;
+  let post_view = PostView::read(&mut context.pool(), post_id, None, None).await?;
 
-    let person_id = local_user_view.person.id;
-    let post_id = data.post_id;
-    let post_view = PostView::read(&mut context.pool(), post_id, None, None).await?;
+  check_community_ban(person_id, post_view.community.id, &mut context.pool()).await?;
 
-    check_community_ban(person_id, post_view.community.id, &mut 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,
+  };
 
-    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,
-    };
+  let report = PostReport::report(&mut context.pool(), &report_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
 
-    let report = PostReport::report(&mut context.pool(), &report_form)
-      .await
-      .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
+  let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?;
 
-    let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?;
+  // Email the admins
+  if local_site.reports_email_admins {
+    send_new_report_email_to_admins(
+      &post_report_view.creator.name,
+      &post_report_view.post_creator.name,
+      &mut context.pool(),
+      context.settings(),
+    )
+    .await?;
+  }
 
-    // Email the admins
-    if local_site.reports_email_admins {
-      send_new_report_email_to_admins(
-        &post_report_view.creator.name,
-        &post_report_view.post_creator.name,
-        &mut context.pool(),
-        context.settings(),
-      )
-      .await?;
-    }
+  ActivityChannel::submit_activity(
+    SendActivityData::CreateReport(
+      post_view.post.ap_id.inner().clone(),
+      local_user_view.person,
+      post_view.community,
+      data.reason.clone(),
+    ),
+    &context,
+  )
+  .await?;
 
-    Ok(PostReportResponse { post_report_view })
-  }
+  Ok(Json(PostReportResponse { post_report_view }))
 }
diff --git a/crates/api/src/post_report/mod.rs b/crates/api/src/post_report/mod.rs
index 375fde4c..3bb1a9b4 100644
--- a/crates/api/src/post_report/mod.rs
+++ b/crates/api/src/post_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_common/src/build_response.rs b/crates/api_common/src/build_response.rs
index b8c02457..8d3bcda1 100644
--- a/crates/api_common/src/build_response.rs
+++ b/crates/api_common/src/build_response.rs
@@ -5,7 +5,7 @@ use crate::{
   post::PostResponse,
   utils::{check_person_block, get_interface_language, is_mod_or_admin, send_email_to_user},
 };
-use actix_web::web::Data;
+use actix_web::web::Json;
 use lemmy_db_schema::{
   newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId},
   source::{
@@ -39,10 +39,10 @@ pub async fn build_comment_response(
 }
 
 pub async fn build_community_response(
-  context: &Data<LemmyContext>,
+  context: &LemmyContext,
   local_user_view: LocalUserView,
   community_id: CommunityId,
-) -> Result<CommunityResponse, LemmyError> {
+) -> Result<Json<CommunityResponse>, LemmyError> {
   let is_mod_or_admin =
     is_mod_or_admin(&mut context.pool(), local_user_view.person.id, community_id)
       .await
@@ -57,10 +57,10 @@ pub async fn build_community_response(
   .await?;
   let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
 
-  Ok(CommunityResponse {
+  Ok(Json(CommunityResponse {
     community_view,
     discussion_languages,
-  })
+  }))
 }
 
 pub async fn build_post_response(
@@ -68,7 +68,7 @@ pub async fn build_post_response(
   community_id: CommunityId,
   person_id: PersonId,
   post_id: PostId,
-) -> Result<PostResponse, LemmyError> {
+) -> Result<Json<PostResponse>, LemmyError> {
   let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), person_id, community_id)
     .await
     .is_ok();
@@ -79,7 +79,7 @@ pub async fn build_post_response(
     Some(is_mod_or_admin),
   )
   .await?;
-  Ok(PostResponse { post_view })
+  Ok(Json(PostResponse { post_view }))
 }
 
 // TODO: this function is a mess and should be split up to handle email seperately
diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs
index 8580c217..1ba9aa64 100644
--- a/crates/api_common/src/send_activity.rs
+++ b/crates/api_common/src/send_activity.rs
@@ -1,10 +1,22 @@
-use crate::context::LemmyContext;
+use crate::{
+  community::BanFromCommunity,
+  context::LemmyContext,
+  person::BanPerson,
+  post::{DeletePost, RemovePost},
+};
 use activitypub_federation::config::Data;
 use futures::future::BoxFuture;
 use lemmy_db_schema::{
-  newtypes::DbUrl,
-  source::{comment::Comment, community::Community, person::Person, post::Post},
+  newtypes::{CommunityId, DbUrl, PersonId},
+  source::{
+    comment::Comment,
+    community::Community,
+    person::Person,
+    post::Post,
+    private_message::PrivateMessage,
+  },
 };
+use lemmy_db_views::structs::PrivateMessageView;
 use lemmy_utils::{error::LemmyResult, SYNCHRONOUS_FEDERATION};
 use once_cell::sync::{Lazy, OnceCell};
 use tokio::{
@@ -15,6 +27,7 @@ use tokio::{
   },
   task::JoinHandle,
 };
+use url::Url;
 
 type MatchOutgoingActivitiesBoxed =
   Box<for<'a> fn(SendActivityData, &'a Data<LemmyContext>) -> BoxFuture<'a, LemmyResult<()>>>;
@@ -26,12 +39,27 @@ pub static MATCH_OUTGOING_ACTIVITIES: OnceCell<MatchOutgoingActivitiesBoxed> = O
 pub enum SendActivityData {
   CreatePost(Post),
   UpdatePost(Post),
+  DeletePost(Post, Person, DeletePost),
+  RemovePost(Post, Person, RemovePost),
+  LockPost(Post, Person, bool),
+  FeaturePost(Post, Person, bool),
   CreateComment(Comment),
+  UpdateComment(Comment),
   DeleteComment(Comment, Person, Community),
   RemoveComment(Comment, Person, Community, Option<String>),
-  UpdateComment(Comment),
   LikePostOrComment(DbUrl, Person, Community, i16),
   FollowCommunity(Community, Person, bool),
+  UpdateCommunity(Person, Community),
+  DeleteCommunity(Person, Community, bool),
+  RemoveCommunity(Person, Community, Option<String>, bool),
+  AddModToCommunity(Person, CommunityId, PersonId, bool),
+  BanFromCommunity(Person, CommunityId, Person, BanFromCommunity),
+  BanFromSite(Person, Person, BanPerson),
+  CreatePrivateMessage(PrivateMessageView),
+  UpdatePrivateMessage(PrivateMessageView),
+  DeletePrivateMessage(Person, PrivateMessage, bool),
+  DeleteUser(Person),
+  CreateReport(Url, Person, Community, String),
 }
 
 // TODO: instead of static, move this into LemmyContext. make sure that stopping the process with
diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs
index 7c84a215..7bfeabd6 100644
--- a/crates/api_crud/src/community/create.rs
+++ b/crates/api_crud/src/community/create.rs
@@ -1,6 +1,5 @@
-use crate::PerformCrud;
-use activitypub_federation::http_signatures::generate_actor_keypair;
-use actix_web::web::Data;
+use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
+use actix_web::web::Json;
 use lemmy_api_common::{
   build_response::build_community_response,
   community::{CommunityResponse, CreateCommunity},
@@ -42,107 +41,104 @@ use lemmy_utils::{
   },
 };
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for CreateCommunity {
-  type Response = CommunityResponse;
-
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommunityResponse, LemmyError> {
-    let data: &CreateCommunity = 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;
-
-    if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
-      return Err(LemmyErrorType::OnlyAdminsCanCreateCommunities)?;
-    }
+#[tracing::instrument(skip(context))]
+pub async fn create_community(
+  data: Json<CreateCommunity>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommunityResponse>, 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;
+
+  if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
+    return Err(LemmyErrorType::OnlyAdminsCanCreateCommunities)?;
+  }
 
-    // Check to make sure the icon and banners are urls
-    let icon = diesel_option_overwrite_to_url_create(&data.icon)?;
-    let banner = diesel_option_overwrite_to_url_create(&data.banner)?;
-
-    let name = sanitize_html(&data.name);
-    let title = sanitize_html(&data.title);
-    let description = sanitize_html_opt(&data.description);
-
-    let slur_regex = local_site_to_slur_regex(&local_site);
-    check_slurs(&name, &slur_regex)?;
-    check_slurs(&title, &slur_regex)?;
-    check_slurs_opt(&description, &slur_regex)?;
-
-    is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
-    is_valid_body_field(&data.description, false)?;
-
-    // Double check for duplicate community actor_ids
-    let community_actor_id = generate_local_apub_endpoint(
-      EndpointType::Community,
-      &data.name,
-      &context.settings().get_protocol_and_hostname(),
-    )?;
-    let community_dupe =
-      Community::read_from_apub_id(&mut context.pool(), &community_actor_id).await?;
-    if community_dupe.is_some() {
-      return Err(LemmyErrorType::CommunityAlreadyExists)?;
-    }
+  // Check to make sure the icon and banners are urls
+  let icon = diesel_option_overwrite_to_url_create(&data.icon)?;
+  let banner = diesel_option_overwrite_to_url_create(&data.banner)?;
+
+  let name = sanitize_html(&data.name);
+  let title = sanitize_html(&data.title);
+  let description = sanitize_html_opt(&data.description);
+
+  let slur_regex = local_site_to_slur_regex(&local_site);
+  check_slurs(&name, &slur_regex)?;
+  check_slurs(&title, &slur_regex)?;
+  check_slurs_opt(&description, &slur_regex)?;
+
+  is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
+  is_valid_body_field(&data.description, false)?;
+
+  // Double check for duplicate community actor_ids
+  let community_actor_id = generate_local_apub_endpoint(
+    EndpointType::Community,
+    &data.name,
+    &context.settings().get_protocol_and_hostname(),
+  )?;
+  let community_dupe =
+    Community::read_from_apub_id(&mut context.pool(), &community_actor_id).await?;
+  if community_dupe.is_some() {
+    return Err(LemmyErrorType::CommunityAlreadyExists)?;
+  }
 
-    // When you create a community, make sure the user becomes a moderator and a follower
-    let keypair = generate_actor_keypair()?;
-
-    let community_form = CommunityInsertForm::builder()
-      .name(name)
-      .title(title)
-      .description(description)
-      .icon(icon)
-      .banner(banner)
-      .nsfw(data.nsfw)
-      .actor_id(Some(community_actor_id.clone()))
-      .private_key(Some(keypair.private_key))
-      .public_key(keypair.public_key)
-      .followers_url(Some(generate_followers_url(&community_actor_id)?))
-      .inbox_url(Some(generate_inbox_url(&community_actor_id)?))
-      .shared_inbox_url(Some(generate_shared_inbox_url(&community_actor_id)?))
-      .posting_restricted_to_mods(data.posting_restricted_to_mods)
-      .instance_id(site_view.site.instance_id)
-      .build();
-
-    let inserted_community = Community::create(&mut context.pool(), &community_form)
-      .await
-      .with_lemmy_type(LemmyErrorType::CommunityAlreadyExists)?;
-
-    // The community creator becomes a moderator
-    let community_moderator_form = CommunityModeratorForm {
-      community_id: inserted_community.id,
-      person_id: local_user_view.person.id,
-    };
-
-    CommunityModerator::join(&mut context.pool(), &community_moderator_form)
-      .await
-      .with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
-
-    // Follow your own community
-    let community_follower_form = CommunityFollowerForm {
-      community_id: inserted_community.id,
-      person_id: local_user_view.person.id,
-      pending: false,
-    };
-
-    CommunityFollower::follow(&mut context.pool(), &community_follower_form)
-      .await
-      .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
-
-    // Update the discussion_languages if that's provided
-    let community_id = inserted_community.id;
-    if let Some(languages) = data.discussion_languages.clone() {
-      let site_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
-      // check that community languages are a subset of site languages
-      // https://stackoverflow.com/a/64227550
-      let is_subset = languages.iter().all(|item| site_languages.contains(item));
-      if !is_subset {
-        return Err(LemmyErrorType::LanguageNotAllowed)?;
-      }
-      CommunityLanguage::update(&mut context.pool(), languages, community_id).await?;
+  // When you create a community, make sure the user becomes a moderator and a follower
+  let keypair = generate_actor_keypair()?;
+
+  let community_form = CommunityInsertForm::builder()
+    .name(name)
+    .title(title)
+    .description(description)
+    .icon(icon)
+    .banner(banner)
+    .nsfw(data.nsfw)
+    .actor_id(Some(community_actor_id.clone()))
+    .private_key(Some(keypair.private_key))
+    .public_key(keypair.public_key)
+    .followers_url(Some(generate_followers_url(&community_actor_id)?))
+    .inbox_url(Some(generate_inbox_url(&community_actor_id)?))
+    .shared_inbox_url(Some(generate_shared_inbox_url(&community_actor_id)?))
+    .posting_restricted_to_mods(data.posting_restricted_to_mods)
+    .instance_id(site_view.site.instance_id)
+    .build();
+
+  let inserted_community = Community::create(&mut context.pool(), &community_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CommunityAlreadyExists)?;
+
+  // The community creator becomes a moderator
+  let community_moderator_form = CommunityModeratorForm {
+    community_id: inserted_community.id,
+    person_id: local_user_view.person.id,
+  };
+
+  CommunityModerator::join(&mut context.pool(), &community_moderator_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
+
+  // Follow your own community
+  let community_follower_form = CommunityFollowerForm {
+    community_id: inserted_community.id,
+    person_id: local_user_view.person.id,
+    pending: false,
+  };
+
+  CommunityFollower::follow(&mut context.pool(), &community_follower_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
+
+  // Update the discussion_languages if that's provided
+  let community_id = inserted_community.id;
+  if let Some(languages) = data.discussion_languages.clone() {
+    let site_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
+    // check that community languages are a subset of site languages
+    // https://stackoverflow.com/a/64227550
+    let is_subset = languages.iter().all(|item| site_languages.contains(item));
+    if !is_subset {
+      return Err(LemmyErrorType::LanguageNotAllowed)?;
     }
-
-    build_community_response(context, local_user_view, community_id).await
+    CommunityLanguage::update(&mut context.pool(), languages, community_id).await?;
   }
+
+  build_community_response(&context, local_user_view, community_id).await
 }
diff --git a/crates/api_crud/src/community/delete.rs b/crates/api_crud/src/community/delete.rs
index d3e58d56..e23b1f40 100644
--- a/crates/api_crud/src/community/delete.rs
+++ b/crates/api_crud/src/community/delete.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_community_response,
   community::{CommunityResponse, DeleteCommunity},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{is_top_mod, local_user_view_from_jwt},
 };
 use lemmy_db_schema::{
@@ -13,36 +14,39 @@ use lemmy_db_schema::{
 use lemmy_db_views_actor::structs::CommunityModeratorView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for DeleteCommunity {
-  type Response = CommunityResponse;
+#[tracing::instrument(skip(context))]
+pub async fn delete_community(
+  data: Json<DeleteCommunity>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommunityResponse>, 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<CommunityResponse, LemmyError> {
-    let data: &DeleteCommunity = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  // Fetch the community mods
+  let community_id = data.community_id;
+  let community_mods =
+    CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
 
-    // Fetch the community mods
-    let community_id = data.community_id;
-    let community_mods =
-      CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
+  // Make sure deleter is the top mod
+  is_top_mod(&local_user_view, &community_mods)?;
 
-    // Make sure deleter is the top mod
-    is_top_mod(&local_user_view, &community_mods)?;
+  // Do the delete
+  let community_id = data.community_id;
+  let deleted = data.deleted;
+  let community = Community::update(
+    &mut context.pool(),
+    community_id,
+    &CommunityUpdateForm::builder()
+      .deleted(Some(deleted))
+      .build(),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunity)?;
 
-    // Do the delete
-    let community_id = data.community_id;
-    let deleted = data.deleted;
-    Community::update(
-      &mut context.pool(),
-      community_id,
-      &CommunityUpdateForm::builder()
-        .deleted(Some(deleted))
-        .build(),
-    )
-    .await
-    .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunity)?;
+  ActivityChannel::submit_activity(
+    SendActivityData::DeleteCommunity(local_user_view.person.clone(), community, data.deleted),
+    &context,
+  )
+  .await?;
 
-    build_community_response(context, local_user_view, community_id).await
-  }
+  build_community_response(&context, local_user_view, community_id).await
 }
diff --git a/crates/api_crud/src/community/remove.rs b/crates/api_crud/src/community/remove.rs
index 2bcd3d85..b79b2a66 100644
--- a/crates/api_crud/src/community/remove.rs
+++ b/crates/api_crud/src/community/remove.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_community_response,
   community::{CommunityResponse, RemoveCommunity},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{is_admin, local_user_view_from_jwt},
 };
 use lemmy_db_schema::{
@@ -18,42 +19,50 @@ use lemmy_utils::{
   utils::time::naive_from_unix,
 };
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for RemoveCommunity {
-  type Response = CommunityResponse;
+#[tracing::instrument(skip(context))]
+pub async fn remove_community(
+  data: Json<RemoveCommunity>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommunityResponse>, 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<CommunityResponse, LemmyError> {
-    let data: &RemoveCommunity = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  // Verify its an admin (only an admin can remove a community)
+  is_admin(&local_user_view)?;
 
-    // Verify its an admin (only an admin can remove a community)
-    is_admin(&local_user_view)?;
+  // Do the remove
+  let community_id = data.community_id;
+  let removed = data.removed;
+  let community = Community::update(
+    &mut context.pool(),
+    community_id,
+    &CommunityUpdateForm::builder()
+      .removed(Some(removed))
+      .build(),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunity)?;
 
-    // Do the remove
-    let community_id = data.community_id;
-    let removed = data.removed;
-    Community::update(
-      &mut context.pool(),
-      community_id,
-      &CommunityUpdateForm::builder()
-        .removed(Some(removed))
-        .build(),
-    )
-    .await
-    .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunity)?;
+  // Mod tables
+  let expires = data.expires.map(naive_from_unix);
+  let form = ModRemoveCommunityForm {
+    mod_person_id: local_user_view.person.id,
+    community_id: data.community_id,
+    removed: Some(removed),
+    reason: data.reason.clone(),
+    expires,
+  };
+  ModRemoveCommunity::create(&mut context.pool(), &form).await?;
 
-    // Mod tables
-    let expires = data.expires.map(naive_from_unix);
-    let form = ModRemoveCommunityForm {
-      mod_person_id: local_user_view.person.id,
-      community_id: data.community_id,
-      removed: Some(removed),
-      reason: data.reason.clone(),
-      expires,
-    };
-    ModRemoveCommunity::create(&mut context.pool(), &form).await?;
+  ActivityChannel::submit_activity(
+    SendActivityData::RemoveCommunity(
+      local_user_view.person.clone(),
+      community,
+      data.reason.clone(),
+      data.removed,
+    ),
+    &context,
+  )
+  .await?;
 
-    build_community_response(context, local_user_view, community_id).await
-  }
+  build_community_response(&context, local_user_view, community_id).await
 }
diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs
index 128be036..baa4e1bd 100644
--- a/crates/api_crud/src/community/update.rs
+++ b/crates/api_crud/src/community/update.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_community_response,
   community::{CommunityResponse, EditCommunity},
   context::LemmyContext,
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{local_site_to_slur_regex, local_user_view_from_jwt, sanitize_html_opt},
 };
 use lemmy_db_schema::{
@@ -22,65 +23,68 @@ use lemmy_utils::{
   utils::{slurs::check_slurs_opt, validation::is_valid_body_field},
 };
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for EditCommunity {
-  type Response = CommunityResponse;
+#[tracing::instrument(skip(context))]
+pub async fn update_community(
+  data: Json<EditCommunity>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CommunityResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
 
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<CommunityResponse, LemmyError> {
-    let data: &EditCommunity = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
+  let slur_regex = local_site_to_slur_regex(&local_site);
+  check_slurs_opt(&data.title, &slur_regex)?;
+  check_slurs_opt(&data.description, &slur_regex)?;
+  is_valid_body_field(&data.description, false)?;
 
-    let slur_regex = local_site_to_slur_regex(&local_site);
-    check_slurs_opt(&data.title, &slur_regex)?;
-    check_slurs_opt(&data.description, &slur_regex)?;
-    is_valid_body_field(&data.description, false)?;
+  let title = sanitize_html_opt(&data.title);
+  let description = sanitize_html_opt(&data.description);
 
-    let title = sanitize_html_opt(&data.title);
-    let description = sanitize_html_opt(&data.description);
+  let icon = diesel_option_overwrite_to_url(&data.icon)?;
+  let banner = diesel_option_overwrite_to_url(&data.banner)?;
+  let description = diesel_option_overwrite(description);
 
-    let icon = diesel_option_overwrite_to_url(&data.icon)?;
-    let banner = diesel_option_overwrite_to_url(&data.banner)?;
-    let description = diesel_option_overwrite(description);
+  // Verify its a mod (only mods can edit it)
+  let community_id = data.community_id;
+  let mods: Vec<PersonId> =
+    CommunityModeratorView::for_community(&mut context.pool(), community_id)
+      .await
+      .map(|v| v.into_iter().map(|m| m.moderator.id).collect())?;
+  if !mods.contains(&local_user_view.person.id) {
+    return Err(LemmyErrorType::NotAModerator)?;
+  }
 
-    // Verify its a mod (only mods can edit it)
-    let community_id = data.community_id;
-    let mods: Vec<PersonId> =
-      CommunityModeratorView::for_community(&mut context.pool(), community_id)
-        .await
-        .map(|v| v.into_iter().map(|m| m.moderator.id).collect())?;
-    if !mods.contains(&local_user_view.person.id) {
-      return Err(LemmyErrorType::NotAModerator)?;
+  let community_id = data.community_id;
+  if let Some(languages) = data.discussion_languages.clone() {
+    let site_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
+    // check that community languages are a subset of site languages
+    // https://stackoverflow.com/a/64227550
+    let is_subset = languages.iter().all(|item| site_languages.contains(item));
+    if !is_subset {
+      return Err(LemmyErrorType::LanguageNotAllowed)?;
     }
+    CommunityLanguage::update(&mut context.pool(), languages, community_id).await?;
+  }
 
-    let community_id = data.community_id;
-    if let Some(languages) = data.discussion_languages.clone() {
-      let site_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
-      // check that community languages are a subset of site languages
-      // https://stackoverflow.com/a/64227550
-      let is_subset = languages.iter().all(|item| site_languages.contains(item));
-      if !is_subset {
-        return Err(LemmyErrorType::LanguageNotAllowed)?;
-      }
-      CommunityLanguage::update(&mut context.pool(), languages, community_id).await?;
-    }
+  let community_form = CommunityUpdateForm::builder()
+    .title(title)
+    .description(description)
+    .icon(icon)
+    .banner(banner)
+    .nsfw(data.nsfw)
+    .posting_restricted_to_mods(data.posting_restricted_to_mods)
+    .updated(Some(Some(naive_now())))
+    .build();
 
-    let community_form = CommunityUpdateForm::builder()
-      .title(title)
-      .description(description)
-      .icon(icon)
-      .banner(banner)
-      .nsfw(data.nsfw)
-      .posting_restricted_to_mods(data.posting_restricted_to_mods)
-      .updated(Some(Some(naive_now())))
-      .build();
+  let community_id = data.community_id;
+  let community = Community::update(&mut context.pool(), community_id, &community_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunity)?;
 
-    let community_id = data.community_id;
-    Community::update(&mut context.pool(), community_id, &community_form)
-      .await
-      .with_lemmy_type(LemmyErrorType::CouldntUpdateCommunity)?;
+  ActivityChannel::submit_activity(
+    SendActivityData::UpdateCommunity(local_user_view.person.clone(), community),
+    &context,
+  )
+  .await?;
 
-    build_community_response(context, local_user_view, community_id).await
-  }
+  build_community_response(&context, local_user_view, community_id).await
 }
diff --git a/crates/api_crud/src/custom_emoji/create.rs b/crates/api_crud/src/custom_emoji/create.rs
index 93e7114a..58917445 100644
--- a/crates/api_crud/src/custom_emoji/create.rs
+++ b/crates/api_crud/src/custom_emoji/create.rs
@@ -1,5 +1,5 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   custom_emoji::{CreateCustomEmoji, CustomEmojiResponse},
@@ -13,41 +13,38 @@ use lemmy_db_schema::source::{
 use lemmy_db_views::structs::CustomEmojiView;
 use lemmy_utils::error::LemmyError;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for CreateCustomEmoji {
-  type Response = CustomEmojiResponse;
+#[tracing::instrument(skip(context))]
+pub async fn create_custom_emoji(
+  data: Json<CreateCustomEmoji>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CustomEmojiResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<CustomEmojiResponse, LemmyError> {
-    let data: &CreateCustomEmoji = 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
+  is_admin(&local_user_view)?;
 
-    let local_site = LocalSite::read(&mut context.pool()).await?;
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
+  let shortcode = sanitize_html(data.shortcode.to_lowercase().trim());
+  let alt_text = sanitize_html(&data.alt_text);
+  let category = sanitize_html(&data.category);
 
-    let shortcode = sanitize_html(data.shortcode.to_lowercase().trim());
-    let alt_text = sanitize_html(&data.alt_text);
-    let category = sanitize_html(&data.category);
-
-    let emoji_form = CustomEmojiInsertForm::builder()
-      .local_site_id(local_site.id)
-      .shortcode(shortcode)
-      .alt_text(alt_text)
-      .category(category)
-      .image_url(data.clone().image_url.into())
+  let emoji_form = CustomEmojiInsertForm::builder()
+    .local_site_id(local_site.id)
+    .shortcode(shortcode)
+    .alt_text(alt_text)
+    .category(category)
+    .image_url(data.clone().image_url.into())
+    .build();
+  let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?;
+  let mut keywords = vec![];
+  for keyword in &data.keywords {
+    let keyword_form = CustomEmojiKeywordInsertForm::builder()
+      .custom_emoji_id(emoji.id)
+      .keyword(keyword.to_lowercase().trim().to_string())
       .build();
-    let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?;
-    let mut keywords = vec![];
-    for keyword in &data.keywords {
-      let keyword_form = CustomEmojiKeywordInsertForm::builder()
-        .custom_emoji_id(emoji.id)
-        .keyword(keyword.to_lowercase().trim().to_string())
-        .build();
-      keywords.push(keyword_form);
-    }
-    CustomEmojiKeyword::create(&mut context.pool(), keywords).await?;
-    let view = CustomEmojiView::get(&mut context.pool(), emoji.id).await?;
-    Ok(CustomEmojiResponse { custom_emoji: view })
+    keywords.push(keyword_form);
   }
+  CustomEmojiKeyword::create(&mut context.pool(), keywords).await?;
+  let view = CustomEmojiView::get(&mut context.pool(), emoji.id).await?;
+  Ok(Json(CustomEmojiResponse { custom_emoji: view }))
 }
diff --git a/crates/api_crud/src/custom_emoji/delete.rs b/crates/api_crud/src/custom_emoji/delete.rs
index 06912923..be88d310 100644
--- a/crates/api_crud/src/custom_emoji/delete.rs
+++ b/crates/api_crud/src/custom_emoji/delete.rs
@@ -1,5 +1,5 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   custom_emoji::{DeleteCustomEmoji, DeleteCustomEmojiResponse},
@@ -8,24 +8,18 @@ use lemmy_api_common::{
 use lemmy_db_schema::source::custom_emoji::CustomEmoji;
 use lemmy_utils::error::LemmyError;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for DeleteCustomEmoji {
-  type Response = DeleteCustomEmojiResponse;
+#[tracing::instrument(skip(context))]
+pub async fn delete_custom_emoji(
+  data: Json<DeleteCustomEmoji>,
+  context: Data<LemmyContext>,
+) -> Result<Json<DeleteCustomEmojiResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<DeleteCustomEmojiResponse, LemmyError> {
-    let data: &DeleteCustomEmoji = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
-    CustomEmoji::delete(&mut context.pool(), data.id).await?;
-    Ok(DeleteCustomEmojiResponse {
-      id: data.id,
-      success: true,
-    })
-  }
+  // Make sure user is an admin
+  is_admin(&local_user_view)?;
+  CustomEmoji::delete(&mut context.pool(), data.id).await?;
+  Ok(Json(DeleteCustomEmojiResponse {
+    id: data.id,
+    success: true,
+  }))
 }
diff --git a/crates/api_crud/src/custom_emoji/mod.rs b/crates/api_crud/src/custom_emoji/mod.rs
index b9d8b557..fdb2f556 100644
--- a/crates/api_crud/src/custom_emoji/mod.rs
+++ b/crates/api_crud/src/custom_emoji/mod.rs
@@ -1,3 +1,3 @@
-mod create;
-mod delete;
-mod update;
+pub mod create;
+pub mod delete;
+pub mod update;
diff --git a/crates/api_crud/src/custom_emoji/update.rs b/crates/api_crud/src/custom_emoji/update.rs
index 93708c37..7a205689 100644
--- a/crates/api_crud/src/custom_emoji/update.rs
+++ b/crates/api_crud/src/custom_emoji/update.rs
@@ -1,5 +1,5 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   custom_emoji::{CustomEmojiResponse, EditCustomEmoji},
@@ -13,40 +13,37 @@ use lemmy_db_schema::source::{
 use lemmy_db_views::structs::CustomEmojiView;
 use lemmy_utils::error::LemmyError;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for EditCustomEmoji {
-  type Response = CustomEmojiResponse;
+#[tracing::instrument(skip(context))]
+pub async fn update_custom_emoji(
+  data: Json<EditCustomEmoji>,
+  context: Data<LemmyContext>,
+) -> Result<Json<CustomEmojiResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<CustomEmojiResponse, LemmyError> {
-    let data: &EditCustomEmoji = 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
+  is_admin(&local_user_view)?;
 
-    let local_site = LocalSite::read(&mut context.pool()).await?;
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
+  let alt_text = sanitize_html(&data.alt_text);
+  let category = sanitize_html(&data.category);
 
-    let alt_text = sanitize_html(&data.alt_text);
-    let category = sanitize_html(&data.category);
-
-    let emoji_form = CustomEmojiUpdateForm::builder()
-      .local_site_id(local_site.id)
-      .alt_text(alt_text)
-      .category(category)
-      .image_url(data.clone().image_url.into())
+  let emoji_form = CustomEmojiUpdateForm::builder()
+    .local_site_id(local_site.id)
+    .alt_text(alt_text)
+    .category(category)
+    .image_url(data.clone().image_url.into())
+    .build();
+  let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?;
+  CustomEmojiKeyword::delete(&mut context.pool(), data.id).await?;
+  let mut keywords = vec![];
+  for keyword in &data.keywords {
+    let keyword_form = CustomEmojiKeywordInsertForm::builder()
+      .custom_emoji_id(emoji.id)
+      .keyword(keyword.to_lowercase().trim().to_string())
       .build();
-    let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?;
-    CustomEmojiKeyword::delete(&mut context.pool(), data.id).await?;
-    let mut keywords = vec![];
-    for keyword in &data.keywords {
-      let keyword_form = CustomEmojiKeywordInsertForm::builder()
-        .custom_emoji_id(emoji.id)
-        .keyword(keyword.to_lowercase().trim().to_string())
-        .build();
-      keywords.push(keyword_form);
-    }
-    CustomEmojiKeyword::create(&mut context.pool(), keywords).await?;
-    let view = CustomEmojiView::get(&mut context.pool(), emoji.id).await?;
-    Ok(CustomEmojiResponse { custom_emoji: view })
+    keywords.push(keyword_form);
   }
+  CustomEmojiKeyword::create(&mut context.pool(), keywords).await?;
+  let view = CustomEmojiView::get(&mut context.pool(), emoji.id).await?;
+  Ok(Json(CustomEmojiResponse { custom_emoji: view }))
 }
diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs
index edd5c46f..aee3e813 100644
--- a/crates/api_crud/src/lib.rs
+++ b/crates/api_crud/src/lib.rs
@@ -1,7 +1,3 @@
-use actix_web::web::Data;
-use lemmy_api_common::context::LemmyContext;
-use lemmy_utils::error::LemmyError;
-
 pub mod comment;
 pub mod community;
 pub mod custom_emoji;
@@ -9,10 +5,3 @@ pub mod post;
 pub mod private_message;
 pub mod site;
 pub mod user;
-
-#[async_trait::async_trait(?Send)]
-pub trait PerformCrud {
-  type Response: serde::ser::Serialize + Send + Clone + Sync;
-
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError>;
-}
diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs
index 264cdbc8..dd88204a 100644
--- a/crates/api_crud/src/post/create.rs
+++ b/crates/api_crud/src/post/create.rs
@@ -194,7 +194,5 @@ pub async fn create_post(
     }
   };
 
-  Ok(Json(
-    build_post_response(&context, community_id, person_id, post_id).await?,
-  ))
+  build_post_response(&context, community_id, person_id, post_id).await
 }
diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs
index eaeb66c4..0d5eea88 100644
--- a/crates/api_crud/src/post/delete.rs
+++ b/crates/api_crud/src/post/delete.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_post_response,
   context::LemmyContext,
   post::{DeletePost, PostResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{check_community_ban, check_community_deleted_or_removed, local_user_view_from_jwt},
 };
 use lemmy_db_schema::{
@@ -12,52 +13,50 @@ use lemmy_db_schema::{
 };
 use lemmy_utils::error::{LemmyError, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for DeletePost {
-  type Response = PostResponse;
-
-  #[tracing::instrument(skip(context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<PostResponse, LemmyError> {
-    let data: &DeletePost = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-
-    let post_id = data.post_id;
-    let orig_post = Post::read(&mut context.pool(), post_id).await?;
-
-    // Dont delete it if its already been deleted.
-    if orig_post.deleted == data.deleted {
-      return Err(LemmyErrorType::CouldntUpdatePost)?;
-    }
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_post.community_id,
-      &mut context.pool(),
-    )
-    .await?;
-    check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
-
-    // Verify that only the creator can delete
-    if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
-      return Err(LemmyErrorType::NoPostEditAllowed)?;
-    }
-
-    // Update the post
-    let post_id = data.post_id;
-    let deleted = data.deleted;
-    Post::update(
-      &mut context.pool(),
-      post_id,
-      &PostUpdateForm::builder().deleted(Some(deleted)).build(),
-    )
-    .await?;
-
-    build_post_response(
-      context,
-      orig_post.community_id,
-      local_user_view.person.id,
-      post_id,
-    )
-    .await
+#[tracing::instrument(skip(context))]
+pub async fn delete_post(
+  data: Json<DeletePost>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PostResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+
+  let post_id = data.post_id;
+  let orig_post = Post::read(&mut context.pool(), post_id).await?;
+
+  // Dont delete it if its already been deleted.
+  if orig_post.deleted == data.deleted {
+    return Err(LemmyErrorType::CouldntUpdatePost)?;
   }
+
+  check_community_ban(
+    local_user_view.person.id,
+    orig_post.community_id,
+    &mut context.pool(),
+  )
+  .await?;
+  check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
+
+  // Verify that only the creator can delete
+  if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
+    return Err(LemmyErrorType::NoPostEditAllowed)?;
+  }
+
+  // Update the post
+  let post = Post::update(
+    &mut context.pool(),
+    data.post_id,
+    &PostUpdateForm::builder()
+      .deleted(Some(data.deleted))
+      .build(),
+  )
+  .await?;
+
+  let person_id = local_user_view.person.id;
+  ActivityChannel::submit_activity(
+    SendActivityData::DeletePost(post, local_user_view.person, data.0.clone()),
+    &context,
+  )
+  .await?;
+
+  build_post_response(&context, orig_post.community_id, person_id, data.post_id).await
 }
diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs
index 7950d504..4a87eb78 100644
--- a/crates/api_crud/src/post/remove.rs
+++ b/crates/api_crud/src/post/remove.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_post_response,
   context::LemmyContext,
   post::{PostResponse, RemovePost},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{check_community_ban, is_mod_or_admin, local_user_view_from_jwt},
 };
 use lemmy_db_schema::{
@@ -15,58 +16,56 @@ use lemmy_db_schema::{
 };
 use lemmy_utils::error::LemmyError;
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for RemovePost {
-  type Response = PostResponse;
+#[tracing::instrument(skip(context))]
+pub async fn remove_post(
+  data: Json<RemovePost>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PostResponse>, 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<PostResponse, LemmyError> {
-    let data: &RemovePost = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  let post_id = data.post_id;
+  let orig_post = Post::read(&mut context.pool(), post_id).await?;
 
-    let post_id = data.post_id;
-    let orig_post = Post::read(&mut context.pool(), post_id).await?;
+  check_community_ban(
+    local_user_view.person.id,
+    orig_post.community_id,
+    &mut context.pool(),
+  )
+  .await?;
 
-    check_community_ban(
-      local_user_view.person.id,
-      orig_post.community_id,
-      &mut context.pool(),
-    )
-    .await?;
+  // Verify that only the mods can remove
+  is_mod_or_admin(
+    &mut context.pool(),
+    local_user_view.person.id,
+    orig_post.community_id,
+  )
+  .await?;
 
-    // Verify that only the mods can remove
-    is_mod_or_admin(
-      &mut context.pool(),
-      local_user_view.person.id,
-      orig_post.community_id,
-    )
-    .await?;
+  // Update the post
+  let post_id = data.post_id;
+  let removed = data.removed;
+  let post = Post::update(
+    &mut context.pool(),
+    post_id,
+    &PostUpdateForm::builder().removed(Some(removed)).build(),
+  )
+  .await?;
 
-    // Update the post
-    let post_id = data.post_id;
-    let removed = data.removed;
-    Post::update(
-      &mut context.pool(),
-      post_id,
-      &PostUpdateForm::builder().removed(Some(removed)).build(),
-    )
-    .await?;
+  // Mod tables
+  let form = ModRemovePostForm {
+    mod_person_id: local_user_view.person.id,
+    post_id: data.post_id,
+    removed: Some(removed),
+    reason: data.reason.clone(),
+  };
+  ModRemovePost::create(&mut context.pool(), &form).await?;
 
-    // Mod tables
-    let form = ModRemovePostForm {
-      mod_person_id: local_user_view.person.id,
-      post_id: data.post_id,
-      removed: Some(removed),
-      reason: data.reason.clone(),
-    };
-    ModRemovePost::create(&mut context.pool(), &form).await?;
+  let person_id = local_user_view.person.id;
+  ActivityChannel::submit_activity(
+    SendActivityData::RemovePost(post, local_user_view.person, data.0),
+    &context,
+  )
+  .await?;
 
-    build_post_response(
-      context,
-      orig_post.community_id,
-      local_user_view.person.id,
-      post_id,
-    )
-    .await
-  }
+  build_post_response(&context, orig_post.community_id, person_id, post_id).await
 }
diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs
index 3341392b..c831c88f 100644
--- a/crates/api_crud/src/post/update.rs
+++ b/crates/api_crud/src/post/update.rs
@@ -113,13 +113,11 @@ pub async fn update_post(
 
   ActivityChannel::submit_activity(SendActivityData::UpdatePost(updated_post), &context).await?;
 
-  Ok(Json(
-    build_post_response(
-      context.deref(),
-      orig_post.community_id,
-      local_user_view.person.id,
-      post_id,
-    )
-    .await?,
-  ))
+  build_post_response(
+    context.deref(),
+    orig_post.community_id,
+    local_user_view.person.id,
+    post_id,
+  )
+  .await
 }
diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs
index 3b1a625f..b9ac916f 100644
--- a/crates/api_crud/src/private_message/create.rs
+++ b/crates/api_crud/src/private_message/create.rs
@@ -1,8 +1,9 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   private_message::{CreatePrivateMessage, PrivateMessageResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{
     check_person_block,
     generate_local_apub_endpoint,
@@ -27,78 +28,77 @@ use lemmy_utils::{
   utils::{slurs::remove_slurs, validation::is_valid_body_field},
 };
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for CreatePrivateMessage {
-  type Response = PrivateMessageResponse;
+#[tracing::instrument(skip(context))]
+pub async fn create_private_message(
+  data: Json<CreatePrivateMessage>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PrivateMessageResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
 
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &CreatePrivateMessage = 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 = sanitize_html(&data.content);
+  let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
+  is_valid_body_field(&Some(content.clone()), false)?;
 
-    let content = sanitize_html(&data.content);
-    let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
-    is_valid_body_field(&Some(content.clone()), false)?;
+  check_person_block(
+    local_user_view.person.id,
+    data.recipient_id,
+    &mut context.pool(),
+  )
+  .await?;
 
-    check_person_block(
-      local_user_view.person.id,
-      data.recipient_id,
-      &mut context.pool(),
-    )
-    .await?;
-
-    let private_message_form = PrivateMessageInsertForm::builder()
-      .content(content.clone())
-      .creator_id(local_user_view.person.id)
-      .recipient_id(data.recipient_id)
-      .build();
+  let private_message_form = PrivateMessageInsertForm::builder()
+    .content(content.clone())
+    .creator_id(local_user_view.person.id)
+    .recipient_id(data.recipient_id)
+    .build();
 
-    let inserted_private_message =
-      PrivateMessage::create(&mut context.pool(), &private_message_form)
-        .await
-        .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
-
-    let inserted_private_message_id = inserted_private_message.id;
-    let protocol_and_hostname = context.settings().get_protocol_and_hostname();
-    let apub_id = generate_local_apub_endpoint(
-      EndpointType::PrivateMessage,
-      &inserted_private_message_id.to_string(),
-      &protocol_and_hostname,
-    )?;
-    PrivateMessage::update(
-      &mut context.pool(),
-      inserted_private_message.id,
-      &PrivateMessageUpdateForm::builder()
-        .ap_id(Some(apub_id))
-        .build(),
-    )
+  let inserted_private_message = PrivateMessage::create(&mut context.pool(), &private_message_form)
     .await
     .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
 
-    let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?;
+  let inserted_private_message_id = inserted_private_message.id;
+  let protocol_and_hostname = context.settings().get_protocol_and_hostname();
+  let apub_id = generate_local_apub_endpoint(
+    EndpointType::PrivateMessage,
+    &inserted_private_message_id.to_string(),
+    &protocol_and_hostname,
+  )?;
+  PrivateMessage::update(
+    &mut context.pool(),
+    inserted_private_message.id,
+    &PrivateMessageUpdateForm::builder()
+      .ap_id(Some(apub_id))
+      .build(),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
 
-    // Send email to the local recipient, if one exists
-    if view.recipient.local {
-      let recipient_id = data.recipient_id;
-      let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?;
-      let lang = get_interface_language(&local_recipient);
-      let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
-      let sender_name = &local_user_view.person.name;
-      send_email_to_user(
-        &local_recipient,
-        &lang.notification_private_message_subject(sender_name),
-        &lang.notification_private_message_body(inbox_link, &content, sender_name),
-        context.settings(),
-      )
-      .await;
-    }
+  let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?;
 
-    Ok(PrivateMessageResponse {
-      private_message_view: view,
-    })
+  // Send email to the local recipient, if one exists
+  if view.recipient.local {
+    let recipient_id = data.recipient_id;
+    let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?;
+    let lang = get_interface_language(&local_recipient);
+    let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
+    let sender_name = &local_user_view.person.name;
+    send_email_to_user(
+      &local_recipient,
+      &lang.notification_private_message_subject(sender_name),
+      &lang.notification_private_message_body(inbox_link, &content, sender_name),
+      context.settings(),
+    )
+    .await;
   }
+
+  ActivityChannel::submit_activity(
+    SendActivityData::CreatePrivateMessage(view.clone()),
+    &context,
+  )
+  .await?;
+
+  Ok(Json(PrivateMessageResponse {
+    private_message_view: view,
+  }))
 }
diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs
index c18e94c0..7657a7ff 100644
--- a/crates/api_crud/src/private_message/delete.rs
+++ b/crates/api_crud/src/private_message/delete.rs
@@ -1,8 +1,9 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   private_message::{DeletePrivateMessage, PrivateMessageResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::local_user_view_from_jwt,
 };
 use lemmy_db_schema::{
@@ -12,42 +13,41 @@ use lemmy_db_schema::{
 use lemmy_db_views::structs::PrivateMessageView;
 use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for DeletePrivateMessage {
-  type Response = PrivateMessageResponse;
+#[tracing::instrument(skip(context))]
+pub async fn delete_private_message(
+  data: Json<DeletePrivateMessage>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PrivateMessageResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
 
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &DeletePrivateMessage = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
+  // Checking permissions
+  let private_message_id = data.private_message_id;
+  let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
+  if local_user_view.person.id != orig_private_message.creator_id {
+    return Err(LemmyErrorType::EditPrivateMessageNotAllowed)?;
+  }
 
-    // Checking permissions
-    let private_message_id = data.private_message_id;
-    let orig_private_message =
-      PrivateMessage::read(&mut context.pool(), private_message_id).await?;
-    if local_user_view.person.id != orig_private_message.creator_id {
-      return Err(LemmyErrorType::EditPrivateMessageNotAllowed)?;
-    }
+  // Doing the update
+  let private_message_id = data.private_message_id;
+  let deleted = data.deleted;
+  let private_message = PrivateMessage::update(
+    &mut context.pool(),
+    private_message_id,
+    &PrivateMessageUpdateForm::builder()
+      .deleted(Some(deleted))
+      .build(),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
 
-    // Doing the update
-    let private_message_id = data.private_message_id;
-    let deleted = data.deleted;
-    PrivateMessage::update(
-      &mut context.pool(),
-      private_message_id,
-      &PrivateMessageUpdateForm::builder()
-        .deleted(Some(deleted))
-        .build(),
-    )
-    .await
-    .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
+  ActivityChannel::submit_activity(
+    SendActivityData::DeletePrivateMessage(local_user_view.person, private_message, data.deleted),
+    &context,
+  )
+  .await?;
 
-    let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
-    Ok(PrivateMessageResponse {
-      private_message_view: view,
-    })
-  }
+  let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
+  Ok(Json(PrivateMessageResponse {
+    private_message_view: view,
+  }))
 }
diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs
index 09b50540..eb24d7c1 100644
--- a/crates/api_crud/src/private_message/update.rs
+++ b/crates/api_crud/src/private_message/update.rs
@@ -1,8 +1,9 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   private_message::{EditPrivateMessage, PrivateMessageResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::{local_site_to_slur_regex, local_user_view_from_jwt, sanitize_html},
 };
 use lemmy_db_schema::{
@@ -19,48 +20,47 @@ use lemmy_utils::{
   utils::{slurs::remove_slurs, validation::is_valid_body_field},
 };
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for EditPrivateMessage {
-  type Response = PrivateMessageResponse;
+#[tracing::instrument(skip(context))]
+pub async fn update_private_message(
+  data: Json<EditPrivateMessage>,
+  context: Data<LemmyContext>,
+) -> Result<Json<PrivateMessageResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
+  let local_site = LocalSite::read(&mut context.pool()).await?;
 
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-  ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &EditPrivateMessage = self;
-    let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
-    let local_site = LocalSite::read(&mut context.pool()).await?;
+  // Checking permissions
+  let private_message_id = data.private_message_id;
+  let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
+  if local_user_view.person.id != orig_private_message.creator_id {
+    return Err(LemmyErrorType::EditPrivateMessageNotAllowed)?;
+  }
 
-    // Checking permissions
-    let private_message_id = data.private_message_id;
-    let orig_private_message =
-      PrivateMessage::read(&mut context.pool(), private_message_id).await?;
-    if local_user_view.person.id != orig_private_message.creator_id {
-      return Err(LemmyErrorType::EditPrivateMessageNotAllowed)?;
-    }
+  // Doing the update
+  let content = sanitize_html(&data.content);
+  let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
+  is_valid_body_field(&Some(content.clone()), false)?;
 
-    // Doing the update
-    let content = sanitize_html(&data.content);
-    let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
-    is_valid_body_field(&Some(content.clone()), false)?;
+  let private_message_id = data.private_message_id;
+  PrivateMessage::update(
+    &mut context.pool(),
+    private_message_id,
+    &PrivateMessageUpdateForm::builder()
+      .content(Some(content))
+      .updated(Some(Some(naive_now())))
+      .build(),
+  )
+  .await
+  .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
 
-    let private_message_id = data.private_message_id;
-    PrivateMessage::update(
-      &mut context.pool(),
-      private_message_id,
-      &PrivateMessageUpdateForm::builder()
-        .content(Some(content))
-        .updated(Some(Some(naive_now())))
-        .build(),
-    )
-    .await
-    .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
+  let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
 
-    let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
+  ActivityChannel::submit_activity(
+    SendActivityData::UpdatePrivateMessage(view.clone()),
+    &context,
+  )
+  .await?;
 
-    Ok(PrivateMessageResponse {
-      private_message_view: view,
-    })
-  }
+  Ok(Json(PrivateMessageResponse {
+    private_message_view: view,
+  }))
 }
diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs
index f2af6940..1665ba19 100644
--- a/crates/api_crud/src/user/create.rs
+++ b/crates/api_crud/src/user/create.rs
@@ -1,6 +1,5 @@
-use crate::PerformCrud;
-use activitypub_federation::http_signatures::generate_actor_keypair;
-use actix_web::web::Data;
+use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
+use actix_web::web::Json;
 use lemmy_api_common::{
   context::LemmyContext,
   person::{LoginResponse, Register},
@@ -38,177 +37,173 @@ use lemmy_utils::{
   },
 };
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for Register {
-  type Response = LoginResponse;
-
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<LoginResponse, LemmyError> {
-    let data: &Register = self;
-
-    let site_view = SiteView::read_local(&mut context.pool()).await?;
-    let local_site = site_view.local_site;
-    let require_registration_application =
-      local_site.registration_mode == RegistrationMode::RequireApplication;
-
-    if local_site.registration_mode == RegistrationMode::Closed {
-      return Err(LemmyErrorType::RegistrationClosed)?;
-    }
+#[tracing::instrument(skip(context))]
+pub async fn register(
+  data: Json<Register>,
+  context: Data<LemmyContext>,
+) -> Result<Json<LoginResponse>, LemmyError> {
+  let site_view = SiteView::read_local(&mut context.pool()).await?;
+  let local_site = site_view.local_site;
+  let require_registration_application =
+    local_site.registration_mode == RegistrationMode::RequireApplication;
+
+  if local_site.registration_mode == RegistrationMode::Closed {
+    return Err(LemmyErrorType::RegistrationClosed)?;
+  }
 
-    password_length_check(&data.password)?;
-    honeypot_check(&data.honeypot)?;
+  password_length_check(&data.password)?;
+  honeypot_check(&data.honeypot)?;
 
-    if local_site.require_email_verification && data.email.is_none() {
-      return Err(LemmyErrorType::EmailRequired)?;
-    }
+  if local_site.require_email_verification && data.email.is_none() {
+    return Err(LemmyErrorType::EmailRequired)?;
+  }
 
-    if local_site.site_setup && require_registration_application && data.answer.is_none() {
-      return Err(LemmyErrorType::RegistrationApplicationAnswerRequired)?;
-    }
+  if local_site.site_setup && require_registration_application && data.answer.is_none() {
+    return Err(LemmyErrorType::RegistrationApplicationAnswerRequired)?;
+  }
 
-    // Make sure passwords match
-    if data.password != data.password_verify {
-      return Err(LemmyErrorType::PasswordsDoNotMatch)?;
-    }
+  // Make sure passwords match
+  if data.password != data.password_verify {
+    return Err(LemmyErrorType::PasswordsDoNotMatch)?;
+  }
 
-    if local_site.site_setup && local_site.captcha_enabled {
-      if let Some(captcha_uuid) = &data.captcha_uuid {
-        let uuid = uuid::Uuid::parse_str(captcha_uuid)?;
-        let check = CaptchaAnswer::check_captcha(
-          &mut context.pool(),
-          CheckCaptchaAnswer {
-            uuid,
-            answer: data.captcha_answer.clone().unwrap_or_default(),
-          },
-        )
-        .await?;
-        if !check {
-          return Err(LemmyErrorType::CaptchaIncorrect)?;
-        }
-      } else {
+  if local_site.site_setup && local_site.captcha_enabled {
+    if let Some(captcha_uuid) = &data.captcha_uuid {
+      let uuid = uuid::Uuid::parse_str(captcha_uuid)?;
+      let check = CaptchaAnswer::check_captcha(
+        &mut context.pool(),
+        CheckCaptchaAnswer {
+          uuid,
+          answer: data.captcha_answer.clone().unwrap_or_default(),
+        },
+      )
+      .await?;
+      if !check {
         return Err(LemmyErrorType::CaptchaIncorrect)?;
       }
+    } else {
+      return Err(LemmyErrorType::CaptchaIncorrect)?;
     }
+  }
 
-    let slur_regex = local_site_to_slur_regex(&local_site);
-    check_slurs(&data.username, &slur_regex)?;
-    check_slurs_opt(&data.answer, &slur_regex)?;
-    let username = sanitize_html(&data.username);
-
-    let actor_keypair = generate_actor_keypair()?;
-    is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?;
-    let actor_id = generate_local_apub_endpoint(
-      EndpointType::Person,
-      &data.username,
-      &context.settings().get_protocol_and_hostname(),
-    )?;
-
-    if let Some(email) = &data.email {
-      if LocalUser::is_email_taken(&mut context.pool(), email).await? {
-        return Err(LemmyErrorType::EmailAlreadyExists)?;
-      }
-    }
-
-    // We have to create both a person, and local_user
-
-    // Register the new person
-    let person_form = PersonInsertForm::builder()
-      .name(username)
-      .actor_id(Some(actor_id.clone()))
-      .private_key(Some(actor_keypair.private_key))
-      .public_key(actor_keypair.public_key)
-      .inbox_url(Some(generate_inbox_url(&actor_id)?))
-      .shared_inbox_url(Some(generate_shared_inbox_url(&actor_id)?))
-      // If its the initial site setup, they are an admin
-      .admin(Some(!local_site.site_setup))
-      .instance_id(site_view.site.instance_id)
-      .build();
-
-    // insert the person
-    let inserted_person = Person::create(&mut context.pool(), &person_form)
-      .await
-      .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?;
-
-    // Automatically set their application as accepted, if they created this with open registration.
-    // Also fixes a bug which allows users to log in when registrations are changed to closed.
-    let accepted_application = Some(!require_registration_application);
-
-    // Create the local user
-    let local_user_form = LocalUserInsertForm::builder()
-      .person_id(inserted_person.id)
-      .email(data.email.as_deref().map(str::to_lowercase))
-      .password_encrypted(data.password.to_string())
-      .show_nsfw(Some(data.show_nsfw))
-      .accepted_application(accepted_application)
-      .default_listing_type(Some(local_site.default_post_listing_type))
-      .build();
-
-    let inserted_local_user = LocalUser::create(&mut context.pool(), &local_user_form).await?;
-
-    if local_site.site_setup && require_registration_application {
-      // Create the registration application
-      let form = RegistrationApplicationInsertForm {
-        local_user_id: inserted_local_user.id,
-        // We already made sure answer was not null above
-        answer: data.answer.clone().expect("must have an answer"),
-      };
-
-      RegistrationApplication::create(&mut context.pool(), &form).await?;
-    }
-
-    // Email the admins
-    if local_site.application_email_admins {
-      send_new_applicant_email_to_admins(&data.username, &mut context.pool(), context.settings())
-        .await?;
+  let slur_regex = local_site_to_slur_regex(&local_site);
+  check_slurs(&data.username, &slur_regex)?;
+  check_slurs_opt(&data.answer, &slur_regex)?;
+  let username = sanitize_html(&data.username);
+
+  let actor_keypair = generate_actor_keypair()?;
+  is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?;
+  let actor_id = generate_local_apub_endpoint(
+    EndpointType::Person,
+    &data.username,
+    &context.settings().get_protocol_and_hostname(),
+  )?;
+
+  if let Some(email) = &data.email {
+    if LocalUser::is_email_taken(&mut context.pool(), email).await? {
+      return Err(LemmyErrorType::EmailAlreadyExists)?;
     }
+  }
 
-    let mut login_response = LoginResponse {
-      jwt: None,
-      registration_created: false,
-      verify_email_sent: false,
+  // We have to create both a person, and local_user
+
+  // Register the new person
+  let person_form = PersonInsertForm::builder()
+    .name(username)
+    .actor_id(Some(actor_id.clone()))
+    .private_key(Some(actor_keypair.private_key))
+    .public_key(actor_keypair.public_key)
+    .inbox_url(Some(generate_inbox_url(&actor_id)?))
+    .shared_inbox_url(Some(generate_shared_inbox_url(&actor_id)?))
+    // If its the initial site setup, they are an admin
+    .admin(Some(!local_site.site_setup))
+    .instance_id(site_view.site.instance_id)
+    .build();
+
+  // insert the person
+  let inserted_person = Person::create(&mut context.pool(), &person_form)
+    .await
+    .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?;
+
+  // Automatically set their application as accepted, if they created this with open registration.
+  // Also fixes a bug which allows users to log in when registrations are changed to closed.
+  let accepted_application = Some(!require_registration_application);
+
+  // Create the local user
+  let local_user_form = LocalUserInsertForm::builder()
+    .person_id(inserted_person.id)
+    .email(data.email.as_deref().map(str::to_lowercase))
+    .password_encrypted(data.password.to_string())
+    .show_nsfw(Some(data.show_nsfw))
+    .accepted_application(accepted_application)
+    .default_listing_type(Some(local_site.default_post_listing_type))
+    .build();
+
+  let inserted_local_user = LocalUser::create(&mut context.pool(), &local_user_form).await?;
+
+  if local_site.site_setup && require_registration_application {
+    // Create the registration application
+    let form = RegistrationApplicationInsertForm {
+      local_user_id: inserted_local_user.id,
+      // We already made sure answer was not null above
+      answer: data.answer.clone().expect("must have an answer"),
     };
 
-    // Log the user in directly if the site is not setup, or email verification and application aren't required
-    if !local_site.site_setup
-      || (!require_registration_application && !local_site.require_email_verification)
-    {
-      login_response.jwt = Some(
-        Claims::jwt(
-          inserted_local_user.id.0,
-          &context.secret().jwt_secret,
-          &context.settings().hostname,
-        )?
-        .into(),
-      );
-    } else {
-      if local_site.require_email_verification {
-        let local_user_view = LocalUserView {
-          local_user: inserted_local_user,
-          person: inserted_person,
-          counts: PersonAggregates::default(),
-        };
-        // we check at the beginning of this method that email is set
-        let email = local_user_view
-          .local_user
-          .email
-          .clone()
-          .expect("email was provided");
-
-        send_verification_email(
-          &local_user_view,
-          &email,
-          &mut context.pool(),
-          context.settings(),
-        )
-        .await?;
-        login_response.verify_email_sent = true;
-      }
+    RegistrationApplication::create(&mut context.pool(), &form).await?;
+  }
 
-      if require_registration_application {
-        login_response.registration_created = true;
-      }
+  // Email the admins
+  if local_site.application_email_admins {
+    send_new_applicant_email_to_admins(&data.username, &mut context.pool(), context.settings())
+      .await?;
+  }
+
+  let mut login_response = LoginResponse {
+    jwt: None,
+    registration_created: false,
+    verify_email_sent: false,
+  };
+
+  // Log the user in directly if the site is not setup, or email verification and application aren't required
+  if !local_site.site_setup
+    || (!require_registration_application && !local_site.require_email_verification)
+  {
+    login_response.jwt = Some(
+      Claims::jwt(
+        inserted_local_user.id.0,
+        &context.secret().jwt_secret,
+        &context.settings().hostname,
+      )?
+      .into(),
+    );
+  } else {
+    if local_site.require_email_verification {
+      let local_user_view = LocalUserView {
+        local_user: inserted_local_user,
+        person: inserted_person,
+        counts: PersonAggregates::default(),
+      };
+      // we check at the beginning of this method that email is set
+      let email = local_user_view
+        .local_user
+        .email
+        .clone()
+        .expect("email was provided");
+
+      send_verification_email(
+        &local_user_view,
+        &email,
+        &mut context.pool(),
+        context.settings(),
+      )
+      .await?;
+      login_response.verify_email_sent = true;
     }
 
-    Ok(login_response)
+    if require_registration_application {
+      login_response.registration_created = true;
+    }
   }
+
+  Ok(Json(login_response))
 }
diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs
index 5a8b4d03..94c547b1 100644
--- a/crates/api_crud/src/user/delete.rs
+++ b/crates/api_crud/src/user/delete.rs
@@ -1,32 +1,36 @@
-use crate::PerformCrud;
-use actix_web::web::Data;
+use activitypub_federation::config::Data;
+use actix_web::web::Json;
 use bcrypt::verify;
 use lemmy_api_common::{
   context::LemmyContext,
   person::{DeleteAccount, DeleteAccountResponse},
+  send_activity::{ActivityChannel, SendActivityData},
   utils::local_user_view_from_jwt,
 };
 use lemmy_utils::error::{LemmyError, LemmyErrorType};
 
-#[async_trait::async_trait(?Send)]
-impl PerformCrud for DeleteAccount {
-  type Response = DeleteAccountResponse;
+#[tracing::instrument(skip(context))]
+pub async fn delete_account(
+  data: Json<DeleteAccount>,
+  context: Data<LemmyContext>,
+) -> Result<Json<DeleteAccountResponse>, LemmyError> {
+  let local_user_view = local_user_view_from_jwt(data.auth.as_ref(), &context).await?;
 
-  #[tracing::instrument(skip(self, context))]
-  async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError> {
-    let data = self;
-    let local_user_view = local_user_view_from_jwt(data.auth.as_ref(), context).await?;
+  // Verify the password
+  let valid: bool = verify(
+    &data.password,
+    &local_user_view.local_user.password_encrypted,
+  )
+  .unwrap_or(false);
+  if !valid {
+    return Err(LemmyErrorType::IncorrectLogin)?;
+  }
 
-    // Verify the password
-    let valid: bool = verify(
-      &data.password,
-      &local_user_view.local_user.password_encrypted,
-    )
-    .unwrap_or(false);
-    if !valid {
-      return Err(LemmyErrorType::IncorrectLogin)?;
-    }
+  ActivityChannel::submit_activity(
+    SendActivityData::DeleteUser(local_user_view.person),
+    &context,
+  )
+  .await?;
 
-    Ok(DeleteAccountResponse {})
-  }
+  Ok(Json(DeleteAccountResponse {}))
 }
diff --git a/crates/api_crud/src/user/mod.rs b/crates/api_crud/src/user/mod.rs
index aeaae9dd..da1aa3ac 100644
--- a/crates/api_crud/src/user/mod.rs
+++ b/crates/api_crud/src/user/mod.rs
@@ -1,2 +1,2 @@
-mod create;
-mod delete;
+pub mod create;
+pub mod delete;
diff --git a/crates/apub/src/activities/block/mod.rs b/crates/apub/src/activities/block/mod.rs
index 7ee9ec17..e9986afc 100644
--- a/crates/apub/src/activities/block/mod.rs
+++ b/crates/apub/src/activities/block/mod.rs
@@ -1,10 +1,9 @@
 use crate::{
-  objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson},
+  objects::{community::ApubCommunity, instance::ApubSite},
   protocol::{
     activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
     objects::{group::Group, instance::Instance},
   },
-  SendActivity,
 };
 use activitypub_federation::{
   config::Data,
@@ -12,19 +11,18 @@ use activitypub_federation::{
   traits::{Actor, Object},
 };
 use chrono::NaiveDateTime;
-use lemmy_api_common::{
-  community::{BanFromCommunity, BanFromCommunityResponse},
-  context::LemmyContext,
-  person::{BanPerson, BanPersonResponse},
-  utils::local_user_view_from_jwt,
-};
+use lemmy_api_common::{community::BanFromCommunity, context::LemmyContext, person::BanPerson};
 use lemmy_db_schema::{
+  newtypes::CommunityId,
   source::{community::Community, person::Person, site::Site},
   traits::Crud,
   utils::DbPool,
 };
 use lemmy_db_views::structs::SiteView;
-use lemmy_utils::{error::LemmyError, utils::time::naive_from_unix};
+use lemmy_utils::{
+  error::{LemmyError, LemmyResult},
+  utils::time::naive_from_unix,
+};
 use serde::Deserialize;
 use url::Url;
 
@@ -132,87 +130,74 @@ async fn generate_cc(
   })
 }
 
-#[async_trait::async_trait]
-impl SendActivity for BanPerson {
-  type Response = BanPersonResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let person = Person::read(&mut context.pool(), request.person_id).await?;
-    let site = SiteOrCommunity::Site(SiteView::read_local(&mut context.pool()).await?.site.into());
-    let expires = request.expires.map(naive_from_unix);
-
-    // if the action affects a local user, federate to other instances
-    if person.local {
-      if request.ban {
-        BlockUser::send(
-          &site,
-          &person.into(),
-          &local_user_view.person.into(),
-          request.remove_data.unwrap_or(false),
-          request.reason.clone(),
-          expires,
-          context,
-        )
-        .await
-      } else {
-        UndoBlockUser::send(
-          &site,
-          &person.into(),
-          &local_user_view.person.into(),
-          request.reason.clone(),
-          context,
-        )
-        .await
-      }
-    } else {
-      Ok(())
-    }
-  }
-}
-
-#[async_trait::async_trait]
-impl SendActivity for BanFromCommunity {
-  type Response = BanFromCommunityResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community: ApubCommunity = Community::read(&mut context.pool(), request.community_id)
-      .await?
-      .into();
-    let banned_person: ApubPerson = Person::read(&mut context.pool(), request.person_id)
-      .await?
-      .into();
-    let expires = request.expires.map(naive_from_unix);
-
-    if request.ban {
+pub(crate) async fn send_ban_from_site(
+  mod_: Person,
+  banned_user: Person,
+  data: BanPerson,
+  context: Data<LemmyContext>,
+) -> Result<(), LemmyError> {
+  let site = SiteOrCommunity::Site(SiteView::read_local(&mut context.pool()).await?.site.into());
+  let expires = data.expires.map(naive_from_unix);
+
+  // if the action affects a local user, federate to other instances
+  if banned_user.local {
+    if data.ban {
       BlockUser::send(
-        &SiteOrCommunity::Community(community),
-        &banned_person,
-        &local_user_view.person.clone().into(),
-        request.remove_data.unwrap_or(false),
-        request.reason.clone(),
+        &site,
+        &banned_user.into(),
+        &mod_.into(),
+        data.remove_data.unwrap_or(false),
+        data.reason.clone(),
         expires,
-        context,
+        &context,
       )
       .await
     } else {
       UndoBlockUser::send(
-        &SiteOrCommunity::Community(community),
-        &banned_person,
-        &local_user_view.person.clone().into(),
-        request.reason.clone(),
-        context,
+        &site,
+        &banned_user.into(),
+        &mod_.into(),
+        data.reason.clone(),
+        &context,
       )
       .await
     }
+  } else {
+    Ok(())
+  }
+}
+
+pub(crate) async fn send_ban_from_community(
+  mod_: Person,
+  community_id: CommunityId,
+  banned_person: Person,
+  data: BanFromCommunity,
+  context: Data<LemmyContext>,
+) -> LemmyResult<()> {
+  let community: ApubCommunity = Community::read(&mut context.pool(), community_id)
+    .await?
+    .into();
+  let expires = data.expires.map(naive_from_unix);
+
+  if data.ban {
+    BlockUser::send(
+      &SiteOrCommunity::Community(community),
+      &banned_person.into(),
+      &mod_.into(),
+      data.remove_data.unwrap_or(false),
+      data.reason.clone(),
+      expires,
+      &context,
+    )
+    .await
+  } else {
+    UndoBlockUser::send(
+      &SiteOrCommunity::Community(community),
+      &banned_person.into(),
+      &mod_.into(),
+      data.reason.clone(),
+      &context,
+    )
+    .await
   }
 }
diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs
index c36a8f0d..e03ded2b 100644
--- a/crates/apub/src/activities/community/collection_add.rs
+++ b/crates/apub/src/activities/community/collection_add.rs
@@ -13,7 +13,6 @@ use crate::{
     activities::community::{collection_add::CollectionAdd, collection_remove::CollectionRemove},
     InCommunity,
   },
-  SendActivity,
 };
 use activitypub_federation::{
   config::Data,
@@ -22,13 +21,12 @@ use activitypub_federation::{
   traits::{ActivityHandler, Actor},
 };
 use lemmy_api_common::{
-  community::{AddModToCommunity, AddModToCommunityResponse},
   context::LemmyContext,
-  post::{FeaturePost, PostResponse},
-  utils::{generate_featured_url, generate_moderators_url, local_user_view_from_jwt},
+  utils::{generate_featured_url, generate_moderators_url},
 };
 use lemmy_db_schema::{
   impls::community::CollectionType,
+  newtypes::{CommunityId, PersonId},
   source::{
     community::{Community, CommunityModerator, CommunityModeratorForm},
     moderator::{ModAddCommunity, ModAddCommunityForm},
@@ -165,61 +163,41 @@ impl ActivityHandler for CollectionAdd {
   }
 }
 
-#[async_trait::async_trait]
-impl SendActivity for AddModToCommunity {
-  type Response = AddModToCommunityResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community: ApubCommunity = Community::read(&mut context.pool(), request.community_id)
-      .await?
-      .into();
-    let updated_mod: ApubPerson = Person::read(&mut context.pool(), request.person_id)
-      .await?
-      .into();
-    if request.added {
-      CollectionAdd::send_add_mod(
-        &community,
-        &updated_mod,
-        &local_user_view.person.into(),
-        context,
-      )
-      .await
-    } else {
-      CollectionRemove::send_remove_mod(
-        &community,
-        &updated_mod,
-        &local_user_view.person.into(),
-        context,
-      )
-      .await
-    }
+pub(crate) async fn send_add_mod_to_community(
+  actor: Person,
+  community_id: CommunityId,
+  updated_mod_id: PersonId,
+  added: bool,
+  context: Data<LemmyContext>,
+) -> Result<(), LemmyError> {
+  let actor: ApubPerson = actor.into();
+  let community: ApubCommunity = Community::read(&mut context.pool(), community_id)
+    .await?
+    .into();
+  let updated_mod: ApubPerson = Person::read(&mut context.pool(), updated_mod_id)
+    .await?
+    .into();
+  if added {
+    CollectionAdd::send_add_mod(&community, &updated_mod, &actor, &context).await
+  } else {
+    CollectionRemove::send_remove_mod(&community, &updated_mod, &actor, &context).await
   }
 }
 
-#[async_trait::async_trait]
-impl SendActivity for FeaturePost {
-  type Response = PostResponse;
-
-  async fn send_activity(
-    request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community = Community::read(&mut context.pool(), response.post_view.community.id)
-      .await?
-      .into();
-    let post = response.post_view.post.clone().into();
-    let person = local_user_view.person.into();
-    if request.featured {
-      CollectionAdd::send_add_featured_post(&community, &post, &person, context).await
-    } else {
-      CollectionRemove::send_remove_featured_post(&community, &post, &person, context).await
-    }
+pub(crate) async fn send_feature_post(
+  post: Post,
+  actor: Person,
+  featured: bool,
+  context: Data<LemmyContext>,
+) -> Result<(), LemmyError> {
+  let actor: ApubPerson = actor.into();
+  let post: ApubPost = post.into();
+  let community = Community::read(&mut context.pool(), post.community_id)
+    .await?
+    .into();
+  if featured {
+    CollectionAdd::send_add_featured_post(&community, &post, &actor, &context).await
+  } else {
+    CollectionRemove::send_remove_featured_post(&community, &post, &actor, &context).await
   }
 }
diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs
index 94135ede..2ceb1838 100644
--- a/crates/apub/src/activities/community/lock_page.rs
+++ b/crates/apub/src/activities/community/lock_page.rs
@@ -9,25 +9,23 @@ use crate::{
   },
   activity_lists::AnnouncableActivities,
   insert_received_activity,
+  objects::community::ApubCommunity,
   protocol::{
     activities::community::lock_page::{LockPage, LockType, UndoLockPage},
     InCommunity,
   },
-  SendActivity,
 };
 use activitypub_federation::{
   config::Data,
+  fetch::object_id::ObjectId,
   kinds::{activity::UndoType, public},
   traits::ActivityHandler,
 };
-use lemmy_api_common::{
-  context::LemmyContext,
-  post::{LockPost, PostResponse},
-  utils::local_user_view_from_jwt,
-};
+use lemmy_api_common::context::LemmyContext;
 use lemmy_db_schema::{
   source::{
     community::Community,
+    person::Person,
     post::{Post, PostUpdateForm},
   },
   traits::Crud,
@@ -102,59 +100,47 @@ impl ActivityHandler for UndoLockPage {
   }
 }
 
-#[async_trait::async_trait]
-impl SendActivity for LockPost {
-  type Response = PostResponse;
-
-  async fn send_activity(
-    request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
+pub(crate) async fn send_lock_post(
+  post: Post,
+  actor: Person,
+  locked: bool,
+  context: Data<LemmyContext>,
+) -> Result<(), LemmyError> {
+  let community: ApubCommunity = Community::read(&mut context.pool(), post.community_id)
+    .await?
+    .into();
+  let id = generate_activity_id(
+    LockType::Lock,
+    &context.settings().get_protocol_and_hostname(),
+  )?;
+  let community_id = community.actor_id.inner().clone();
+  let lock = LockPage {
+    actor: actor.actor_id.clone().into(),
+    to: vec![public()],
+    object: ObjectId::from(post.ap_id),
+    cc: vec![community_id.clone()],
+    kind: LockType::Lock,
+    id,
+    audience: Some(community_id.into()),
+  };
+  let activity = if locked {
+    AnnouncableActivities::LockPost(lock)
+  } else {
     let id = generate_activity_id(
-      LockType::Lock,
+      UndoType::Undo,
       &context.settings().get_protocol_and_hostname(),
     )?;
-    let community_id = response.post_view.community.actor_id.clone();
-    let actor = local_user_view.person.actor_id.clone().into();
-    let lock = LockPage {
-      actor,
+    let undo = UndoLockPage {
+      actor: lock.actor.clone(),
       to: vec![public()],
-      object: response.post_view.post.ap_id.clone().into(),
-      cc: vec![community_id.clone().into()],
-      kind: LockType::Lock,
+      cc: lock.cc.clone(),
+      kind: UndoType::Undo,
       id,
-      audience: Some(community_id.into()),
+      audience: lock.audience.clone(),
+      object: lock,
     };
-    let activity = if request.locked {
-      AnnouncableActivities::LockPost(lock)
-    } else {
-      let id = generate_activity_id(
-        UndoType::Undo,
-        &context.settings().get_protocol_and_hostname(),
-      )?;
-      let undo = UndoLockPage {
-        actor: lock.actor.clone(),
-        to: vec![public()],
-        cc: lock.cc.clone(),
-        kind: UndoType::Undo,
-        id,
-        audience: lock.audience.clone(),
-        object: lock,
-      };
-      AnnouncableActivities::UndoLockPost(undo)
-    };
-    let community = Community::read(&mut context.pool(), response.post_view.community.id).await?;
-    send_activity_in_community(
-      activity,
-      &local_user_view.person.into(),
-      &community.into(),
-      vec![],
-      true,
-      context,
-    )
-    .await?;
-    Ok(())
-  }
+    AnnouncableActivities::UndoLockPost(undo)
+  };
+  send_activity_in_community(activity, &actor.into(), &community, vec![], true, &context).await?;
+  Ok(())
 }
diff --git a/crates/apub/src/activities/community/report.rs b/crates/apub/src/activities/community/report.rs
index 22a8c12b..a17df711 100644
--- a/crates/apub/src/activities/community/report.rs
+++ b/crates/apub/src/activities/community/report.rs
@@ -4,7 +4,6 @@ use crate::{
   objects::{community::ApubCommunity, person::ApubPerson},
   protocol::{activities::community::report::Report, InCommunity},
   PostOrComment,
-  SendActivity,
 };
 use activitypub_federation::{
   config::Data,
@@ -12,15 +11,12 @@ use activitypub_federation::{
   kinds::activity::FlagType,
   traits::{ActivityHandler, Actor},
 };
-use lemmy_api_common::{
-  comment::{CommentReportResponse, CreateCommentReport},
-  context::LemmyContext,
-  post::{CreatePostReport, PostReportResponse},
-  utils::{local_user_view_from_jwt, sanitize_html},
-};
+use lemmy_api_common::{context::LemmyContext, utils::sanitize_html};
 use lemmy_db_schema::{
   source::{
     comment_report::{CommentReport, CommentReportForm},
+    community::Community,
+    person::Person,
     post_report::{PostReport, PostReportForm},
   },
   traits::Reportable,
@@ -28,58 +24,17 @@ use lemmy_db_schema::{
 use lemmy_utils::error::LemmyError;
 use url::Url;
 
-#[async_trait::async_trait]
-impl SendActivity for CreatePostReport {
-  type Response = PostReportResponse;
-
-  async fn send_activity(
-    request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    Report::send(
-      ObjectId::from(response.post_report_view.post.ap_id.clone()),
-      &local_user_view.person.into(),
-      ObjectId::from(response.post_report_view.community.actor_id.clone()),
-      request.reason.to_string(),
-      context,
-    )
-    .await
-  }
-}
-
-#[async_trait::async_trait]
-impl SendActivity for CreateCommentReport {
-  type Response = CommentReportResponse;
-
-  async fn send_activity(
-    request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    Report::send(
-      ObjectId::from(response.comment_report_view.comment.ap_id.clone()),
-      &local_user_view.person.into(),
-      ObjectId::from(response.comment_report_view.community.actor_id.clone()),
-      request.reason.to_string(),
-      context,
-    )
-    .await
-  }
-}
-
 impl Report {
   #[tracing::instrument(skip_all)]
-  async fn send(
+  pub(crate) async fn send(
     object_id: ObjectId<PostOrComment>,
-    actor: &ApubPerson,
-    community_id: ObjectId<ApubCommunity>,
+    actor: Person,
+    community: Community,
     reason: String,
-    context: &Data<LemmyContext>,
+    context: Data<LemmyContext>,
   ) -> Result<(), LemmyError> {
-    let community = community_id.dereference_local(context).await?;
+    let actor: ApubPerson = actor.into();
+    let community: ApubCommunity = community.into();
     let kind = FlagType::Flag;
     let id = generate_activity_id(
       kind.clone(),
@@ -96,7 +51,7 @@ impl Report {
     };
 
     let inbox = vec![community.shared_inbox_or_inbox()];
-    send_lemmy_activity(context, report, actor, inbox, false).await
+    send_lemmy_activity(&context, report, &actor, inbox, false).await
   }
 }
 
diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs
index fe2477d6..c3b2a2ae 100644
--- a/crates/apub/src/activities/community/update.rs
+++ b/crates/apub/src/activities/community/update.rs
@@ -10,61 +10,43 @@ use crate::{
   insert_received_activity,
   objects::{community::ApubCommunity, person::ApubPerson},
   protocol::{activities::community::update::UpdateCommunity, InCommunity},
-  SendActivity,
 };
 use activitypub_federation::{
   config::Data,
   kinds::{activity::UpdateType, public},
   traits::{ActivityHandler, Actor, Object},
 };
-use lemmy_api_common::{
-  community::{CommunityResponse, EditCommunity, HideCommunity},
-  context::LemmyContext,
-  utils::local_user_view_from_jwt,
+use lemmy_api_common::context::LemmyContext;
+use lemmy_db_schema::{
+  source::{community::Community, person::Person},
+  traits::Crud,
 };
-use lemmy_db_schema::{source::community::Community, traits::Crud};
 use lemmy_utils::error::LemmyError;
 use url::Url;
 
-#[async_trait::async_trait]
-impl SendActivity for EditCommunity {
-  type Response = CommunityResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community = Community::read(&mut context.pool(), request.community_id).await?;
-    UpdateCommunity::send(community.into(), &local_user_view.person.into(), context).await
-  }
-}
-
-impl UpdateCommunity {
-  #[tracing::instrument(skip_all)]
-  pub async fn send(
-    community: ApubCommunity,
-    actor: &ApubPerson,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let id = generate_activity_id(
-      UpdateType::Update,
-      &context.settings().get_protocol_and_hostname(),
-    )?;
-    let update = UpdateCommunity {
-      actor: actor.id().into(),
-      to: vec![public()],
-      object: Box::new(community.clone().into_json(context).await?),
-      cc: vec![community.id()],
-      kind: UpdateType::Update,
-      id: id.clone(),
-      audience: Some(community.id().into()),
-    };
+pub(crate) async fn send_update_community(
+  community: Community,
+  actor: Person,
+  context: Data<LemmyContext>,
+) -> Result<(), LemmyError> {
+  let community: ApubCommunity = community.into();
+  let actor: ApubPerson = actor.into();
+  let id = generate_activity_id(
+    UpdateType::Update,
+    &context.settings().get_protocol_and_hostname(),
+  )?;
+  let update = UpdateCommunity {
+    actor: actor.id().into(),
+    to: vec![public()],
+    object: Box::new(community.clone().into_json(&context).await?),
+    cc: vec![community.id()],
+    kind: UpdateType::Update,
+    id: id.clone(),
+    audience: Some(community.id().into()),
+  };
 
-    let activity = AnnouncableActivities::UpdateCommunity(update);
-    send_activity_in_community(activity, actor, &community, vec![], true, context).await
-  }
+  let activity = AnnouncableActivities::UpdateCommunity(update);
+  send_activity_in_community(activity, &actor, &community, vec![], true, &context).await
 }
 
 #[async_trait::async_trait]
@@ -101,18 +83,3 @@ impl ActivityHandler for UpdateCommunity {
     Ok(())
   }
 }
-
-#[async_trait::async_trait]
-impl SendActivity for HideCommunity {
-  type Response = CommunityResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community = Community::read(&mut context.pool(), request.community_id).await?;
-    UpdateCommunity::send(community.into(), &local_user_view.person.into(), context).await
-  }
-}
diff --git a/crates/apub/src/activities/create_or_update/private_message.rs b/crates/apub/src/activities/create_or_update/private_message.rs
index 3eaad2f7..77a430c3 100644
--- a/crates/apub/src/activities/create_or_update/private_message.rs
+++ b/crates/apub/src/activities/create_or_update/private_message.rs
@@ -6,92 +6,40 @@ use crate::{
     create_or_update::chat_message::CreateOrUpdateChatMessage,
     CreateOrUpdateType,
   },
-  SendActivity,
 };
 use activitypub_federation::{
   config::Data,
   protocol::verification::verify_domains_match,
   traits::{ActivityHandler, Actor, Object},
 };
-use lemmy_api_common::{
-  context::LemmyContext,
-  private_message::{CreatePrivateMessage, EditPrivateMessage, PrivateMessageResponse},
-};
-use lemmy_db_schema::{
-  newtypes::PersonId,
-  source::{person::Person, private_message::PrivateMessage},
-  traits::Crud,
-};
+use lemmy_api_common::context::LemmyContext;
+use lemmy_db_views::structs::PrivateMessageView;
 use lemmy_utils::error::LemmyError;
 use url::Url;
 
-#[async_trait::async_trait]
-impl SendActivity for CreatePrivateMessage {
-  type Response = PrivateMessageResponse;
-
-  async fn send_activity(
-    _request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    CreateOrUpdateChatMessage::send(
-      &response.private_message_view.private_message,
-      response.private_message_view.creator.id,
-      CreateOrUpdateType::Create,
-      context,
-    )
-    .await
-  }
-}
-#[async_trait::async_trait]
-impl SendActivity for EditPrivateMessage {
-  type Response = PrivateMessageResponse;
+pub(crate) async fn send_create_or_update_pm(
+  pm_view: PrivateMessageView,
+  kind: CreateOrUpdateType,
+  context: Data<LemmyContext>,
+) -> Result<(), LemmyError> {
+  let actor: ApubPerson = pm_view.creator.into();
+  let recipient: ApubPerson = pm_view.recipient.into();
 
-  async fn send_activity(
-    _request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    CreateOrUpdateChatMessage::send(
-      &response.private_message_view.private_message,
-      response.private_message_view.creator.id,
-      CreateOrUpdateType::Update,
-      context,
-    )
-    .await
-  }
-}
-
-impl CreateOrUpdateChatMessage {
-  #[tracing::instrument(skip_all)]
-  async fn send(
-    private_message: &PrivateMessage,
-    sender_id: PersonId,
-    kind: CreateOrUpdateType,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let recipient_id = private_message.recipient_id;
-    let sender: ApubPerson = Person::read(&mut context.pool(), sender_id).await?.into();
-    let recipient: ApubPerson = Person::read(&mut context.pool(), recipient_id)
-      .await?
-      .into();
-
-    let id = generate_activity_id(
-      kind.clone(),
-      &context.settings().get_protocol_and_hostname(),
-    )?;
-    let create_or_update = CreateOrUpdateChatMessage {
-      id: id.clone(),
-      actor: sender.id().into(),
-      to: [recipient.id().into()],
-      object: ApubPrivateMessage(private_message.clone())
-        .into_json(context)
-        .await?,
-      kind,
-    };
-    let inbox = vec![recipient.shared_inbox_or_inbox()];
-    send_lemmy_activity(context, create_or_update, &sender, inbox, true).await
-  }
+  let id = generate_activity_id(
+    kind.clone(),
+    &context.settings().get_protocol_and_hostname(),
+  )?;
+  let create_or_update = CreateOrUpdateChatMessage {
+    id: id.clone(),
+    actor: actor.id().into(),
+    to: [recipient.id().into()],
+    object: ApubPrivateMessage(pm_view.private_message.clone())
+      .into_json(&context)
+      .await?,
+    kind,
+  };
+  let inbox = vec![recipient.shared_inbox_or_inbox()];
+  send_lemmy_activity(&context, create_or_update, &actor, inbox, true).await
 }
 
 #[async_trait::async_trait]
diff --git a/crates/apub/src/activities/deletion/delete_user.rs b/crates/apub/src/activities/deletion/delete_user.rs
index b388ed9e..cf37dc5a 100644
--- a/crates/apub/src/activities/deletion/delete_user.rs
+++ b/crates/apub/src/activities/deletion/delete_user.rs
@@ -3,7 +3,6 @@ use crate::{
   insert_received_activity,
   objects::{instance::remote_instance_inboxes, person::ApubPerson},
   protocol::activities::deletion::delete_user::DeleteUser,
-  SendActivity,
 };
 use activitypub_federation::{
   config::Data,
@@ -11,50 +10,37 @@ use activitypub_federation::{
   protocol::verification::verify_urls_match,
   traits::{ActivityHandler, Actor},
 };
-use lemmy_api_common::{
-  context::LemmyContext,
-  person::{DeleteAccount, DeleteAccountResponse},
-  utils::{delete_user_account, local_user_view_from_jwt},
-};
+use lemmy_api_common::{context::LemmyContext, utils::delete_user_account};
+use lemmy_db_schema::source::person::Person;
 use lemmy_utils::error::LemmyError;
 use url::Url;
 
-#[async_trait::async_trait]
-impl SendActivity for DeleteAccount {
-  type Response = DeleteAccountResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let actor: ApubPerson = local_user_view.person.into();
-    delete_user_account(
-      actor.id,
-      &mut context.pool(),
-      context.settings(),
-      context.client(),
-    )
-    .await?;
+pub async fn delete_user(person: Person, context: Data<LemmyContext>) -> Result<(), LemmyError> {
+  let actor: ApubPerson = person.into();
+  delete_user_account(
+    actor.id,
+    &mut context.pool(),
+    context.settings(),
+    context.client(),
+  )
+  .await?;
 
-    let id = generate_activity_id(
-      DeleteType::Delete,
-      &context.settings().get_protocol_and_hostname(),
-    )?;
-    let delete = DeleteUser {
-      actor: actor.id().into(),
-      to: vec![public()],
-      object: actor.id().into(),
-      kind: DeleteType::Delete,
-      id: id.clone(),
-      cc: vec![],
-    };
+  let id = generate_activity_id(
+    DeleteType::Delete,
+    &context.settings().get_protocol_and_hostname(),
+  )?;
+  let delete = DeleteUser {
+    actor: actor.id().into(),
+    to: vec![public()],
+    object: actor.id().into(),
+    kind: DeleteType::Delete,
+    id: id.clone(),
+    cc: vec![],
+  };
 
-    let inboxes = remote_instance_inboxes(&mut context.pool()).await?;
-    send_lemmy_activity(context, delete, &actor, inboxes, true).await?;
-    Ok(())
-  }
+  let inboxes = remote_instance_inboxes(&mut context.pool()).await?;
+  send_lemmy_activity(&context, delete, &actor, inboxes, true).await?;
+  Ok(())
 }
 
 /// This can be separate from Delete activity because it doesn't need to be handled in shared inbox
diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs
index c571ac22..535a2af1 100644
--- a/crates/apub/src/activities/deletion/mod.rs
+++ b/crates/apub/src/activities/deletion/mod.rs
@@ -19,7 +19,6 @@ use crate::{
     activities::deletion::{delete::Delete, undo_delete::UndoDelete},
     InCommunity,
   },
-  SendActivity,
 };
 use activitypub_federation::{
   config::Data,
@@ -28,14 +27,9 @@ use activitypub_federation::{
   protocol::verification::verify_domains_match,
   traits::{Actor, Object},
 };
-use lemmy_api_common::{
-  community::{CommunityResponse, DeleteCommunity, RemoveCommunity},
-  context::LemmyContext,
-  post::{DeletePost, PostResponse, RemovePost},
-  private_message::{DeletePrivateMessage, PrivateMessageResponse},
-  utils::local_user_view_from_jwt,
-};
+use lemmy_api_common::context::LemmyContext;
 use lemmy_db_schema::{
+  newtypes::CommunityId,
   source::{
     comment::{Comment, CommentUpdateForm},
     community::{Community, CommunityUpdateForm},
@@ -53,122 +47,6 @@ pub mod delete;
 pub mod delete_user;
 pub mod undo_delete;
 
-#[async_trait::async_trait]
-impl SendActivity for DeletePost {
-  type Response = PostResponse;
-
-  async fn send_activity(
-    request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community = Community::read(&mut context.pool(), response.post_view.community.id).await?;
-    let deletable = DeletableObjects::Post(response.post_view.post.clone().into());
-    send_apub_delete_in_community(
-      local_user_view.person,
-      community,
-      deletable,
-      None,
-      request.deleted,
-      context,
-    )
-    .await
-  }
-}
-
-#[async_trait::async_trait]
-impl SendActivity for RemovePost {
-  type Response = PostResponse;
-
-  async fn send_activity(
-    request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community = Community::read(&mut context.pool(), response.post_view.community.id).await?;
-    let deletable = DeletableObjects::Post(response.post_view.post.clone().into());
-    send_apub_delete_in_community(
-      local_user_view.person,
-      community,
-      deletable,
-      request.reason.clone().or_else(|| Some(String::new())),
-      request.removed,
-      context,
-    )
-    .await
-  }
-}
-
-#[async_trait::async_trait]
-impl SendActivity for DeletePrivateMessage {
-  type Response = PrivateMessageResponse;
-
-  async fn send_activity(
-    request: &Self,
-    response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    send_apub_delete_private_message(
-      &local_user_view.person.into(),
-      response.private_message_view.private_message.clone(),
-      request.deleted,
-      context,
-    )
-    .await
-  }
-}
-
-#[async_trait::async_trait]
-impl SendActivity for DeleteCommunity {
-  type Response = CommunityResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community = Community::read(&mut context.pool(), request.community_id).await?;
-    let deletable = DeletableObjects::Community(community.clone().into());
-    send_apub_delete_in_community(
-      local_user_view.person,
-      community,
-      deletable,
-      None,
-      request.deleted,
-      context,
-    )
-    .await
-  }
-}
-
-#[async_trait::async_trait]
-impl SendActivity for RemoveCommunity {
-  type Response = CommunityResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community = Community::read(&mut context.pool(), request.community_id).await?;
-    let deletable = DeletableObjects::Community(community.clone().into());
-    send_apub_delete_in_community(
-      local_user_view.person,
-      community,
-      deletable,
-      request.reason.clone().or_else(|| Some(String::new())),
-      request.removed,
-      context,
-    )
-    .await
-  }
-}
-
 /// Parameter `reason` being set indicates that this is a removal by a mod. If its unset, this
 /// action was done by a normal user.
 #[tracing::instrument(skip_all)]
@@ -200,12 +78,44 @@ pub(crate) async fn send_apub_delete_in_community(
   .await
 }
 
+/// Parameter `reason` being set indicates that this is a removal by a mod. If its unset, this
+/// action was done by a normal user.
+#[tracing::instrument(skip_all)]
+pub(crate) async fn send_apub_delete_in_community_new(
+  actor: Person,
+  community_id: CommunityId,
+  object: DeletableObjects,
+  reason: Option<String>,
+  deleted: bool,
+  context: Data<LemmyContext>,
+) -> Result<(), LemmyError> {
+  let community = Community::read(&mut context.pool(), community_id).await?;
+  let actor = ApubPerson::from(actor);
+  let is_mod_action = reason.is_some();
+  let activity = if deleted {
+    let delete = Delete::new(&actor, object, public(), Some(&community), reason, &context)?;
+    AnnouncableActivities::Delete(delete)
+  } else {
+    let undo = UndoDelete::new(&actor, object, public(), Some(&community), reason, &context)?;
+    AnnouncableActivities::UndoDelete(undo)
+  };
+  send_activity_in_community(
+    activity,
+    &actor,
+    &community.into(),
+    vec![],
+    is_mod_action,
+    &context,
+  )
+  .await
+}
+
 #[tracing::instrument(skip_all)]
-async fn send_apub_delete_private_message(
+pub(crate) async fn send_apub_delete_private_message(
   actor: &ApubPerson,
   pm: PrivateMessage,
   deleted: bool,
-  context: &Data<LemmyContext>,
+  context: Data<LemmyContext>,
 ) -> Result<(), LemmyError> {
   let recipient_id = pm.recipient_id;
   let recipient: ApubPerson = Person::read(&mut context.pool(), recipient_id)
@@ -215,11 +125,11 @@ async fn send_apub_delete_private_message(
   let deletable = DeletableObjects::PrivateMessage(pm.into());
   let inbox = vec![recipient.shared_inbox_or_inbox()];
   if deleted {
-    let delete = Delete::new(actor, deletable, recipient.id(), None, None, context)?;
-    send_lemmy_activity(context, delete, actor, inbox, true).await?;
+    let delete = Delete::new(actor, deletable, recipient.id(), None, None, &context)?;
+    send_lemmy_activity(&context, delete, actor, inbox, true).await?;
   } else {
-    let undo = UndoDelete::new(actor, deletable, recipient.id(), None, None, context)?;
-    send_lemmy_activity(context, undo, actor, inbox, true).await?;
+    let undo = UndoDelete::new(actor, deletable, recipient.id(), None, None, &context)?;
+    send_lemmy_activity(&context, undo, actor, inbox, true).await?;
   };
   Ok(())
 }
diff --git a/crates/apub/src/activities/following/follow.rs b/crates/apub/src/activities/following/follow.rs
index 2f0f5037..d64041b9 100644
--- a/crates/apub/src/activities/following/follow.rs
+++ b/crates/apub/src/activities/following/follow.rs
@@ -8,12 +8,7 @@ use crate::{
   fetcher::user_or_community::UserOrCommunity,
   insert_received_activity,
   objects::{community::ApubCommunity, person::ApubPerson},
-  protocol::activities::following::{
-    accept::AcceptFollow,
-    follow::Follow,
-    undo_follow::UndoFollow,
-  },
-  SendActivity,
+  protocol::activities::following::{accept::AcceptFollow, follow::Follow},
 };
 use activitypub_federation::{
   config::Data,
@@ -21,17 +16,13 @@ use activitypub_federation::{
   protocol::verification::verify_urls_match,
   traits::{ActivityHandler, Actor},
 };
-use lemmy_api_common::{
-  community::{BlockCommunity, BlockCommunityResponse},
-  context::LemmyContext,
-  utils::local_user_view_from_jwt,
-};
+use lemmy_api_common::context::LemmyContext;
 use lemmy_db_schema::{
   source::{
-    community::{Community, CommunityFollower, CommunityFollowerForm},
+    community::{CommunityFollower, CommunityFollowerForm},
     person::{PersonFollower, PersonFollowerForm},
   },
-  traits::{Crud, Followable},
+  traits::Followable,
 };
 use lemmy_utils::error::LemmyError;
 use url::Url;
@@ -128,18 +119,3 @@ impl ActivityHandler for Follow {
     AcceptFollow::send(self, context).await
   }
 }
-
-#[async_trait::async_trait]
-impl SendActivity for BlockCommunity {
-  type Response = BlockCommunityResponse;
-
-  async fn send_activity(
-    request: &Self,
-    _response: &Self::Response,
-    context: &Data<LemmyContext>,
-  ) -> Result<(), LemmyError> {
-    let local_user_view = local_user_view_from_jwt(&request.auth, context).await?;
-    let community = Community::read(&mut context.pool(), request.community_id).await?;
-    UndoFollow::send(&local_user_view.person.into(), &community.into(), context).await
-  }
-}
diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs
index 78075576..885abc60 100644
--- a/crates/apub/src/activities/mod.rs
+++ b/crates/apub/src/activities/mod.rs
@@ -1,11 +1,25 @@
 use self::following::send_follow_community;
 use crate::{
   activities::{
-    deletion::{send_apub_delete_in_community, DeletableObjects},
+    block::{send_ban_from_community, send_ban_from_site},
+    community::{
+      collection_add::{send_add_mod_to_community, send_feature_post},
+      lock_page::send_lock_post,
+      update::send_update_community,
+    },
+    create_or_update::private_message::send_create_or_update_pm,
+    deletion::{
+      delete_user::delete_user,
+      send_apub_delete_in_community,
+      send_apub_delete_in_community_new,
+      send_apub_delete_private_message,
+      DeletableObjects,
+    },
     voting::send_like_activity,
   },
   objects::{community::ApubCommunity, person::ApubPerson},
   protocol::activities::{
+    community::report::Report,
     create_or_update::{note::CreateOrUpdateNote, page::CreateOrUpdatePage},
     CreateOrUpdateType,
   },
@@ -229,14 +243,46 @@ pub async fn match_outgoing_activities(
   let fed_task = async {
     use SendActivityData::*;
     match data {
-      CreatePost(post) | UpdatePost(post) => {
+      CreatePost(post) => {
         let creator_id = post.creator_id;
         CreateOrUpdatePage::send(post, creator_id, CreateOrUpdateType::Create, context).await
       }
-      CreateComment(comment) | UpdateComment(comment) => {
+      UpdatePost(post) => {
+        let creator_id = post.creator_id;
+        CreateOrUpdatePage::send(post, creator_id, CreateOrUpdateType::Update, context).await
+      }
+      DeletePost(post, person, data) => {
+        send_apub_delete_in_community_new(
+          person,
+          post.community_id,
+          DeletableObjects::Post(post.into()),
+          None,
+          data.deleted,
+          context,
+        )
+        .await
+      }
+      RemovePost(post, person, data) => {
+        send_apub_delete_in_community_new(
+          person,
+          post.community_id,
+          DeletableObjects::Post(post.into()),
+          data.reason.or_else(|| Some(String::new())),
+          data.removed,
+          context,
+        )
+        .await
+      }
+      LockPost(post, actor, locked) => send_lock_post(post, actor, locked, context).await,
+      FeaturePost(post, actor, featured) => send_feature_post(post, actor, featured, context).await,
+      CreateComment(comment) => {
         let creator_id = comment.creator_id;
         CreateOrUpdateNote::send(comment, creator_id, CreateOrUpdateType::Create, context).await
       }
+      UpdateComment(comment) => {
+        let creator_id = comment.creator_id;
+        CreateOrUpdateNote::send(comment, creator_id, CreateOrUpdateType::Update, context).await
+      }
       DeleteComment(comment, actor, community) => {
         let is_deleted = comment.deleted;
         let deletable = DeletableObjects::Comment(comment.into());
@@ -251,9 +297,46 @@ pub async fn match_outgoing_activities(
       LikePostOrComment(object_id, person, community, score) => {
         send_like_activity(object_id, person, community, score, context).await
       }
-      SendActivityData::FollowCommunity(community, person, follow) => {
+      FollowCommunity(community, person, follow) => {
         send_follow_community(community, person, follow, &context).await
       }
+      UpdateCommunity(actor, community) => send_update_community(community, actor, context).await,
+      DeleteCommunity(actor, community, removed) => {
+        let deletable = DeletableObjects::Community(community.clone().into());
+        send_apub_delete_in_community(actor, community, deletable, None, removed, &context).await
+      }
+      RemoveCommunity(actor, community, reason, removed) => {
+        let deletable = DeletableObjects::Community(community.clone().into());
+        send_apub_delete_in_community(
+          actor,
+          community,
+          deletable,
+          reason.clone().or_else(|| Some(String::new())),
+          removed,
+          &context,
+        )
+        .await
+      }
+      AddModToCommunity(actor, community_id, updated_mod_id, added) => {
+        send_add_mod_to_community(actor, community_id, updated_mod_id, added, context).await
+      }
+      BanFromCommunity(mod_, community_id, target, data) => {
+        send_ban_from_community(mod_, community_id, target, data, context).await
+      }
+      BanFromSite(mod_, target, data) => send_ban_from_site(mod_, target, data, context).await,
+      CreatePrivateMessage(pm) => {
+        send_create_or_update_pm(pm, CreateOrUpdateType::Create, context).await
+      }
+      UpdatePrivateMessage(pm) => {
+        send_create_or_update_pm(pm, CreateOrUpdateType::Update, context).await
+      }
+      DeletePrivateMessage(person, pm, deleted) => {
+        send_apub_delete_private_message(&person.into(), pm, deleted, context).await
+      }
+      DeleteUser(person) => delete_user(person, context).await,
+      CreateReport(url, actor, community, reason) => {
+        Report::send(ObjectId::from(url), actor, community, reason, context).await
+      }
     }
   };
   if *SYNCHRONOUS_FEDERATION {
diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs
index dccf18dd..f4a137d9 100644
--- a/src/api_routes_http.rs
+++ b/src/api_routes_http.rs
@@ -1,33 +1,30 @@
 use actix_web::{guard, web, Error, HttpResponse, Result};
 use lemmy_api::{
   comment::{distinguish::distinguish_comment, like::like_comment, save::save_comment},
-  comment_report::{list::list_comment_reports, resolve::resolve_comment_report},
-  community::follow::follow_community,
-  local_user::notifications::mark_reply_read::mark_reply_as_read,
-  post::like::like_post,
+  comment_report::{
+    create::create_comment_report,
+    list::list_comment_reports,
+    resolve::resolve_comment_report,
+  },
+  community::{
+    add_mod::add_mod_to_community,
+    ban::ban_from_community,
+    block::block_community,
+    follow::follow_community,
+    hide::hide_community,
+  },
+  local_user::{ban_person::ban_from_site, notifications::mark_reply_read::mark_reply_as_read},
+  post::{feature::feature_post, like::like_post, lock::lock_post},
+  post_report::create::create_post_report,
   Perform,
 };
 use lemmy_api_common::{
-  comment::CreateCommentReport,
-  community::{
-    AddModToCommunity,
-    BanFromCommunity,
-    BlockCommunity,
-    CreateCommunity,
-    DeleteCommunity,
-    EditCommunity,
-    HideCommunity,
-    RemoveCommunity,
-    TransferCommunity,
-  },
+  community::TransferCommunity,
   context::LemmyContext,
-  custom_emoji::{CreateCustomEmoji, DeleteCustomEmoji, EditCustomEmoji},
   person::{
     AddAdmin,
-    BanPerson,
     BlockPerson,
     ChangePassword,
-    DeleteAccount,
     GetBannedPersons,
     GetCaptcha,
     GetPersonMentions,
@@ -39,27 +36,12 @@ use lemmy_api_common::{
     MarkPersonMentionAsRead,
     PasswordChangeAfterReset,
     PasswordReset,
-    Register,
     SaveUserSettings,
     VerifyEmail,
   },
-  post::{
-    CreatePostReport,
-    DeletePost,
-    FeaturePost,
-    GetSiteMetadata,
-    ListPostReports,
-    LockPost,
-    MarkPostAsRead,
-    RemovePost,
-    ResolvePostReport,
-    SavePost,
-  },
+  post::{GetSiteMetadata, ListPostReports, MarkPostAsRead, ResolvePostReport, SavePost},
   private_message::{
-    CreatePrivateMessage,
     CreatePrivateMessageReport,
-    DeletePrivateMessage,
-    EditPrivateMessage,
     ListPrivateMessageReports,
     MarkPrivateMessageAsRead,
     ResolvePrivateMessageReport,
@@ -85,11 +67,33 @@ use lemmy_api_crud::{
     remove::remove_comment,
     update::update_comment,
   },
-  community::list::list_communities,
-  post::{create::create_post, read::get_post, update::update_post},
-  private_message::read::get_private_message,
+  community::{
+    create::create_community,
+    delete::delete_community,
+    list::list_communities,
+    remove::remove_community,
+    update::update_community,
+  },
+  custom_emoji::{
+    create::create_custom_emoji,
+    delete::delete_custom_emoji,
+    update::update_custom_emoji,
+  },
+  post::{
+    create::create_post,
+    delete::delete_post,
+    read::get_post,
+    remove::remove_post,
+    update::update_post,
+  },
+  private_message::{
+    create::create_private_message,
+    delete::delete_private_message,
+    read::get_private_message,
+    update::update_private_message,
+  },
   site::{create::create_site, read::get_site, update::update_site},
-  PerformCrud,
+  user::{create::register, delete::delete_account},
 };
 use lemmy_apub::{
   api::{
@@ -137,29 +141,23 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
         web::resource("/community")
           .guard(guard::Post())
           .wrap(rate_limit.register())
-          .route(web::post().to(route_post_crud::<CreateCommunity>)),
+          .route(web::post().to(create_community)),
       )
       .service(
         web::scope("/community")
           .wrap(rate_limit.message())
           .route("", web::get().to(get_community))
-          .route("", web::put().to(route_post_crud::<EditCommunity>))
-          .route("/hide", web::put().to(route_post::<HideCommunity>))
+          .route("", web::put().to(update_community))
+          .route("/hide", web::put().to(hide_community))
           .route("/list", web::get().to(list_communities))
           .route("/follow", web::post().to(follow_community))
-          .route("/block", web::post().to(route_post::<BlockCommunity>))
-          .route(
-            "/delete",
-            web::post().to(route_post_crud::<DeleteCommunity>),
-          )
+          .route("/block", web::post().to(block_community))
+          .route("/delete", web::post().to(delete_community))
           // Mod Actions
-          .route(
-            "/remove",
-            web::post().to(route_post_crud::<RemoveCommunity>),
-          )
+          .route("/remove", web::post().to(remove_community))
           .route("/transfer", web::post().to(route_post::<TransferCommunity>))
-          .route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
-          .route("/mod", web::post().to(route_post::<AddModToCommunity>)),
+          .route("/ban_user", web::post().to(ban_from_community))
+          .route("/mod", web::post().to(add_mod_to_community)),
       )
       .service(
         web::scope("/federated_instances")
@@ -179,18 +177,18 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
           .wrap(rate_limit.message())
           .route("", web::get().to(get_post))
           .route("", web::put().to(update_post))
-          .route("/delete", web::post().to(route_post_crud::<DeletePost>))
-          .route("/remove", web::post().to(route_post_crud::<RemovePost>))
+          .route("/delete", web::post().to(delete_post))
+          .route("/remove", web::post().to(remove_post))
           .route(
             "/mark_as_read",
             web::post().to(route_post::<MarkPostAsRead>),
           )
-          .route("/lock", web::post().to(route_post::<LockPost>))
-          .route("/feature", web::post().to(route_post::<FeaturePost>))
+          .route("/lock", web::post().to(lock_post))
+          .route("/feature", web::post().to(feature_post))
           .route("/list", web::get().to(list_posts))
           .route("/like", web::post().to(like_post))
           .route("/save", web::put().to(route_post::<SavePost>))
-          .route("/report", web::post().to(route_post::<CreatePostReport>))
+          .route("/report", web::post().to(create_post_report))
           .route(
             "/report/resolve",
             web::put().to(route_post::<ResolvePostReport>),
@@ -221,7 +219,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
           .route("/like", web::post().to(like_comment))
           .route("/save", web::put().to(save_comment))
           .route("/list", web::get().to(list_comments))
-          .route("/report", web::post().to(route_post::<CreateCommentReport>))
+          .route("/report", web::post().to(create_comment_report))
           .route("/report/resolve", web::put().to(resolve_comment_report))
           .route("/report/list", web::get().to(list_comment_reports)),
       )
@@ -230,12 +228,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
         web::scope("/private_message")
           .wrap(rate_limit.message())
           .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(
-            "/delete",
-            web::post().to(route_post_crud::<DeletePrivateMessage>),
-          )
+          .route("", web::post().to(create_private_message))
+          .route("", web::put().to(update_private_message))
+          .route("/delete", web::post().to(delete_private_message))
           .route(
             "/mark_as_read",
             web::post().to(route_post::<MarkPrivateMessageAsRead>),
@@ -260,7 +255,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
         web::resource("/user/register")
           .guard(guard::Post())
           .wrap(rate_limit.register())
-          .route(web::post().to(route_post_crud::<Register>)),
+          .route(web::post().to(register)),
       )
       .service(
         // Handle captcha separately
@@ -280,15 +275,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
           )
           .route("/replies", web::get().to(route_get::<GetReplies>))
           // Admin action. I don't like that it's in /user
-          .route("/ban", web::post().to(route_post::<BanPerson>))
+          .route("/ban", web::post().to(ban_from_site))
           .route("/banned", web::get().to(route_get::<GetBannedPersons>))
           .route("/block", web::post().to(route_post::<BlockPerson>))
           // Account actions. I don't like that they're in /user maybe /accounts
           .route("/login", web::post().to(route_post::<Login>))
-          .route(
-            "/delete_account",
-            web::post().to(route_post_crud::<DeleteAccount>),
-          )
+          .route("/delete_account", web::post().to(delete_account))
           .route(
             "/password_reset",
             web::post().to(route_post::<PasswordReset>),
@@ -343,12 +335,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
       .service(
         web::scope("/custom_emoji")
           .wrap(rate_limit.message())
-          .route("", web::post().to(route_post_crud::<CreateCustomEmoji>))
-          .route("", web::put().to(route_post_crud::<EditCustomEmoji>))
-          .route(
-            "/delete",
-            web::post().to(route_post_crud::<DeleteCustomEmoji>),
-          ),
+          .route("", web::post().to(create_custom_emoji))
+          .route("", web::put().to(update_custom_emoji))
+          .route("/delete", web::post().to(delete_custom_emoji)),
       ),
   );
 }
@@ -408,43 +397,3 @@ where
 {
   perform::<Data>(data.0, context, apub_data).await
 }
-
-async fn perform_crud<'a, Data>(
-  data: 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,
-{
-  let res = data.perform(&context).await?;
-  let res_clone = res.clone();
-  let fed_task = async move { SendActivity::send_activity(&data, &res_clone, &apub_data).await };
-  if *SYNCHRONOUS_FEDERATION {
-    fed_task.await?;
-  } else {
-    spawn_try_task(fed_task);
-  }
-  Ok(HttpResponse::Ok().json(&res))
-}
-
-async fn route_post_crud<'a, Data>(
-  data: web::Json<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
-}