From: Felix Ableitner <me@nutomic.com>
Date: Thu, 25 Mar 2021 19:19:40 +0000 (+0100)
Subject: Split api crate into api_structs and api
X-Git-Url: http://these/git/readmes/%7B%60%24%7BwebArchiveUrl%7D/save/static/%24%7Bargs.pageFn.jump%20n%7D?a=commitdiff_plain;h=249fcc5066c30158eb3a3094a21ff5e021534cd2;p=lemmy.git

Split api crate into api_structs and api
---

diff --git a/Cargo.lock b/Cargo.lock
index b525d65a..004b31d0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1746,7 +1746,7 @@ dependencies = [
  "http-signature-normalization-actix",
  "itertools",
  "lazy_static",
- "lemmy_api_structs",
+ "lemmy_api_common",
  "lemmy_apub",
  "lemmy_db_queries",
  "lemmy_db_schema",
@@ -1771,7 +1771,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "lemmy_api_structs"
+name = "lemmy_api_common"
 version = "0.1.0"
 dependencies = [
  "actix-web",
@@ -1789,6 +1789,51 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "lemmy_api_crud"
+version = "0.1.0"
+dependencies = [
+ "actix",
+ "actix-rt",
+ "actix-web",
+ "anyhow",
+ "async-trait",
+ "awc",
+ "background-jobs",
+ "base64 0.13.0",
+ "bcrypt",
+ "captcha",
+ "chrono",
+ "diesel",
+ "futures",
+ "http",
+ "http-signature-normalization-actix",
+ "itertools",
+ "lazy_static",
+ "lemmy_api_common",
+ "lemmy_apub",
+ "lemmy_db_queries",
+ "lemmy_db_schema",
+ "lemmy_db_views",
+ "lemmy_db_views_actor",
+ "lemmy_db_views_moderator",
+ "lemmy_utils",
+ "lemmy_websocket",
+ "log",
+ "openssl",
+ "rand 0.8.3",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2",
+ "strum",
+ "strum_macros",
+ "thiserror",
+ "tokio 0.3.7",
+ "url",
+ "uuid",
+]
+
 [[package]]
 name = "lemmy_apub"
 version = "0.1.0"
@@ -1813,7 +1858,7 @@ dependencies = [
  "http-signature-normalization-reqwest",
  "itertools",
  "lazy_static",
- "lemmy_api_structs",
+ "lemmy_api_common",
  "lemmy_db_queries",
  "lemmy_db_schema",
  "lemmy_db_views",
@@ -1916,7 +1961,7 @@ dependencies = [
  "chrono",
  "diesel",
  "lazy_static",
- "lemmy_api_structs",
+ "lemmy_api_common",
  "lemmy_db_queries",
  "lemmy_db_schema",
  "lemmy_db_views",
@@ -1948,7 +1993,8 @@ dependencies = [
  "env_logger",
  "http-signature-normalization-actix",
  "lemmy_api",
- "lemmy_api_structs",
+ "lemmy_api_common",
+ "lemmy_api_crud",
  "lemmy_apub",
  "lemmy_db_queries",
  "lemmy_db_schema",
@@ -2013,7 +2059,7 @@ dependencies = [
  "background-jobs",
  "chrono",
  "diesel",
- "lemmy_api_structs",
+ "lemmy_api_common",
  "lemmy_db_queries",
  "lemmy_db_schema",
  "lemmy_utils",
diff --git a/Cargo.toml b/Cargo.toml
index e7d92fdf..4f338f9b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,8 @@ debug = 0
 [workspace]
 members = [
     "crates/api",
+    "crates/api_crud",
+    "crates/api_common",
     "crates/apub",
     "crates/utils",
     "crates/db_queries",
@@ -19,13 +21,13 @@ members = [
     "crates/db_views",
     "crates/db_views_actor",
     "crates/db_views_actor",
-    "crates/api_structs",
     "crates/websocket",
     "crates/routes"
 ]
 
 [dependencies]
 lemmy_api = { path = "./crates/api" }
+lemmy_api_crud = { path = "./crates/api_crud" }
 lemmy_apub = { path = "./crates/apub" }
 lemmy_utils = { path = "./crates/utils" }
 lemmy_db_schema = { path = "./crates/db_schema" }
@@ -33,7 +35,7 @@ lemmy_db_queries = { path = "./crates/db_queries" }
 lemmy_db_views = { path = "./crates/db_views" }
 lemmy_db_views_moderator = { path = "./crates/db_views_moderator" }
 lemmy_db_views_actor = { path = "./crates/db_views_actor" }
-lemmy_api_structs = { path = "crates/api_structs" }
+lemmy_api_common = { path = "crates/api_common" }
 lemmy_websocket = { path = "./crates/websocket" }
 lemmy_routes = { path = "./crates/routes" }
 diesel = "1.4.5"
diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml
index ea3cd625..1aaa0528 100644
--- a/crates/api/Cargo.toml
+++ b/crates/api/Cargo.toml
@@ -16,7 +16,7 @@ lemmy_db_schema = { path = "../db_schema" }
 lemmy_db_views = { path = "../db_views" }
 lemmy_db_views_moderator = { path = "../db_views_moderator" }
 lemmy_db_views_actor = { path = "../db_views_actor" }
-lemmy_api_structs = { path = "../api_structs" }
+lemmy_api_common = { path = "../api_common" }
 lemmy_websocket = { path = "../websocket" }
 diesel = "1.4.5"
 bcrypt = "0.9.0"
diff --git a/crates/api/src/comment.rs b/crates/api/src/comment.rs
index bcee72b0..2237204f 100644
--- a/crates/api/src/comment.rs
+++ b/crates/api/src/comment.rs
@@ -1,472 +1,18 @@
-use crate::{
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
   check_community_ban,
   check_downvotes_enabled,
-  collect_moderated_communities,
+  comment::*,
   get_local_user_view_from_jwt,
-  get_local_user_view_from_jwt_opt,
-  get_post,
-  is_mod_or_admin,
-  Perform,
-};
-use actix_web::web::Data;
-use lemmy_api_structs::{blocking, comment::*, send_local_notifs};
-use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
-use lemmy_db_queries::{
-  source::comment::Comment_,
-  Crud,
-  Likeable,
-  ListingType,
-  Reportable,
-  Saveable,
-  SortType,
-};
-use lemmy_db_schema::{
-  source::{comment::*, comment_report::*, moderator::*},
-  LocalUserId,
-};
-use lemmy_db_views::{
-  comment_report_view::{CommentReportQueryBuilder, CommentReportView},
-  comment_view::{CommentQueryBuilder, CommentView},
-  local_user_view::LocalUserView,
-};
-use lemmy_utils::{
-  utils::{remove_slurs, scrape_text_for_mentions},
-  ApiError,
-  ConnectionId,
-  LemmyError,
 };
-use lemmy_websocket::{
-  messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
-  LemmyContext,
-  UserOperation,
-};
-use std::str::FromStr;
-
-#[async_trait::async_trait(?Send)]
-impl Perform for CreateComment {
-  type Response = CommentResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommentResponse, LemmyError> {
-    let data: &CreateComment = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let content_slurs_removed = remove_slurs(&data.content.to_owned());
-
-    // Check for a community ban
-    let post_id = data.post_id;
-    let post = get_post(post_id, context.pool()).await?;
-
-    check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
-
-    // Check if post is locked, no new comments
-    if post.locked {
-      return Err(ApiError::err("locked").into());
-    }
-
-    // If there's a parent_id, check to make sure that comment is in that post
-    if let Some(parent_id) = data.parent_id {
-      // Make sure the parent comment exists
-      let parent =
-        match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? {
-          Ok(comment) => comment,
-          Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
-        };
-      if parent.post_id != post_id {
-        return Err(ApiError::err("couldnt_create_comment").into());
-      }
-    }
-
-    let comment_form = CommentForm {
-      content: content_slurs_removed,
-      parent_id: data.parent_id.to_owned(),
-      post_id: data.post_id,
-      creator_id: local_user_view.person.id,
-      removed: None,
-      deleted: None,
-      read: None,
-      published: None,
-      updated: None,
-      ap_id: None,
-      local: true,
-    };
-
-    // Create the comment
-    let comment_form2 = comment_form.clone();
-    let inserted_comment = match blocking(context.pool(), move |conn| {
-      Comment::create(&conn, &comment_form2)
-    })
-    .await?
-    {
-      Ok(comment) => comment,
-      Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
-    };
-
-    // Necessary to update the ap_id
-    let inserted_comment_id = inserted_comment.id;
-    let updated_comment: Comment =
-      match blocking(context.pool(), move |conn| -> Result<Comment, LemmyError> {
-        let apub_id =
-          generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?;
-        Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?)
-      })
-      .await?
-      {
-        Ok(comment) => comment,
-        Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
-      };
-
-    updated_comment
-      .send_create(&local_user_view.person, context)
-      .await?;
-
-    // Scan the comment for user mentions, add those rows
-    let post_id = post.id;
-    let mentions = scrape_text_for_mentions(&comment_form.content);
-    let recipient_ids = send_local_notifs(
-      mentions,
-      updated_comment.clone(),
-      local_user_view.person.clone(),
-      post,
-      context.pool(),
-      true,
-    )
-    .await?;
-
-    // You like your own comment by default
-    let like_form = CommentLikeForm {
-      comment_id: inserted_comment.id,
-      post_id,
-      person_id: local_user_view.person.id,
-      score: 1,
-    };
-
-    let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
-    if blocking(context.pool(), like).await?.is_err() {
-      return Err(ApiError::err("couldnt_like_comment").into());
-    }
-
-    updated_comment
-      .send_like(&local_user_view.person, context)
-      .await?;
-
-    let person_id = local_user_view.person.id;
-    let mut comment_view = blocking(context.pool(), move |conn| {
-      CommentView::read(&conn, inserted_comment.id, Some(person_id))
-    })
-    .await??;
-
-    // If its a comment to yourself, mark it as read
-    let comment_id = comment_view.comment.id;
-    if local_user_view.person.id == comment_view.get_recipient_id() {
-      match blocking(context.pool(), move |conn| {
-        Comment::update_read(conn, comment_id, true)
-      })
-      .await?
-      {
-        Ok(comment) => comment,
-        Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
-      };
-      comment_view.comment.read = true;
-    }
-
-    let mut res = CommentResponse {
-      comment_view,
-      recipient_ids,
-      form_id: data.form_id.to_owned(),
-    };
-
-    context.chat_server().do_send(SendComment {
-      op: UserOperation::CreateComment,
-      comment: res.clone(),
-      websocket_id,
-    });
-
-    res.recipient_ids = Vec::new(); // Necessary to avoid doubles
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for EditComment {
-  type Response = CommentResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommentResponse, LemmyError> {
-    let data: &EditComment = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let comment_id = data.comment_id;
-    let orig_comment = blocking(context.pool(), move |conn| {
-      CommentView::read(&conn, comment_id, None)
-    })
-    .await??;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_comment.community.id,
-      context.pool(),
-    )
-    .await?;
-
-    // Verify that only the creator can edit
-    if local_user_view.person.id != orig_comment.creator.id {
-      return Err(ApiError::err("no_comment_edit_allowed").into());
-    }
-
-    // Do the update
-    let content_slurs_removed = remove_slurs(&data.content.to_owned());
-    let comment_id = data.comment_id;
-    let updated_comment = match blocking(context.pool(), move |conn| {
-      Comment::update_content(conn, comment_id, &content_slurs_removed)
-    })
-    .await?
-    {
-      Ok(comment) => comment,
-      Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
-    };
-
-    // Send the apub update
-    updated_comment
-      .send_update(&local_user_view.person, context)
-      .await?;
-
-    // Do the mentions / recipients
-    let updated_comment_content = updated_comment.content.to_owned();
-    let mentions = scrape_text_for_mentions(&updated_comment_content);
-    let recipient_ids = send_local_notifs(
-      mentions,
-      updated_comment,
-      local_user_view.person.clone(),
-      orig_comment.post,
-      context.pool(),
-      false,
-    )
-    .await?;
-
-    let comment_id = data.comment_id;
-    let person_id = local_user_view.person.id;
-    let comment_view = blocking(context.pool(), move |conn| {
-      CommentView::read(conn, comment_id, Some(person_id))
-    })
-    .await??;
-
-    let res = CommentResponse {
-      comment_view,
-      recipient_ids,
-      form_id: data.form_id.to_owned(),
-    };
-
-    context.chat_server().do_send(SendComment {
-      op: UserOperation::EditComment,
-      comment: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for DeleteComment {
-  type Response = CommentResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommentResponse, LemmyError> {
-    let data: &DeleteComment = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let comment_id = data.comment_id;
-    let orig_comment = blocking(context.pool(), move |conn| {
-      CommentView::read(&conn, comment_id, None)
-    })
-    .await??;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_comment.community.id,
-      context.pool(),
-    )
-    .await?;
-
-    // Verify that only the creator can delete
-    if local_user_view.person.id != orig_comment.creator.id {
-      return Err(ApiError::err("no_comment_edit_allowed").into());
-    }
-
-    // Do the delete
-    let deleted = data.deleted;
-    let updated_comment = match blocking(context.pool(), move |conn| {
-      Comment::update_deleted(conn, comment_id, deleted)
-    })
-    .await?
-    {
-      Ok(comment) => comment,
-      Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
-    };
-
-    // Send the apub message
-    if deleted {
-      updated_comment
-        .send_delete(&local_user_view.person, context)
-        .await?;
-    } else {
-      updated_comment
-        .send_undo_delete(&local_user_view.person, context)
-        .await?;
-    }
-
-    // Refetch it
-    let comment_id = data.comment_id;
-    let person_id = local_user_view.person.id;
-    let comment_view = blocking(context.pool(), move |conn| {
-      CommentView::read(conn, comment_id, Some(person_id))
-    })
-    .await??;
-
-    // Build the recipients
-    let comment_view_2 = comment_view.clone();
-    let mentions = vec![];
-    let recipient_ids = send_local_notifs(
-      mentions,
-      updated_comment,
-      local_user_view.person.clone(),
-      comment_view_2.post,
-      context.pool(),
-      false,
-    )
-    .await?;
-
-    let res = CommentResponse {
-      comment_view,
-      recipient_ids,
-      form_id: None, // TODO a comment delete might clear forms?
-    };
-
-    context.chat_server().do_send(SendComment {
-      op: UserOperation::DeleteComment,
-      comment: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for RemoveComment {
-  type Response = CommentResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommentResponse, LemmyError> {
-    let data: &RemoveComment = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let comment_id = data.comment_id;
-    let orig_comment = blocking(context.pool(), move |conn| {
-      CommentView::read(&conn, comment_id, None)
-    })
-    .await??;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_comment.community.id,
-      context.pool(),
-    )
-    .await?;
-
-    // Verify that only a mod or admin can remove
-    is_mod_or_admin(
-      context.pool(),
-      local_user_view.person.id,
-      orig_comment.community.id,
-    )
-    .await?;
-
-    // Do the remove
-    let removed = data.removed;
-    let updated_comment = match blocking(context.pool(), move |conn| {
-      Comment::update_removed(conn, comment_id, removed)
-    })
-    .await?
-    {
-      Ok(comment) => comment,
-      Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
-    };
-
-    // Mod tables
-    let form = ModRemoveCommentForm {
-      mod_person_id: local_user_view.person.id,
-      comment_id: data.comment_id,
-      removed: Some(removed),
-      reason: data.reason.to_owned(),
-    };
-    blocking(context.pool(), move |conn| {
-      ModRemoveComment::create(conn, &form)
-    })
-    .await??;
-
-    // Send the apub message
-    if removed {
-      updated_comment
-        .send_remove(&local_user_view.person, context)
-        .await?;
-    } else {
-      updated_comment
-        .send_undo_remove(&local_user_view.person, context)
-        .await?;
-    }
-
-    // Refetch it
-    let comment_id = data.comment_id;
-    let person_id = local_user_view.person.id;
-    let comment_view = blocking(context.pool(), move |conn| {
-      CommentView::read(conn, comment_id, Some(person_id))
-    })
-    .await??;
-
-    // Build the recipients
-    let comment_view_2 = comment_view.clone();
-
-    let mentions = vec![];
-    let recipient_ids = send_local_notifs(
-      mentions,
-      updated_comment,
-      local_user_view.person.clone(),
-      comment_view_2.post,
-      context.pool(),
-      false,
-    )
-    .await?;
-
-    let res = CommentResponse {
-      comment_view,
-      recipient_ids,
-      form_id: None, // TODO maybe this might clear other forms
-    };
-
-    context.chat_server().do_send(SendComment {
-      op: UserOperation::RemoveComment,
-      comment: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
+use lemmy_apub::ApubLikeableType;
+use lemmy_db_queries::{source::comment::Comment_, Likeable, Saveable};
+use lemmy_db_schema::{source::comment::*, LocalUserId};
+use lemmy_db_views::{comment_view::CommentView, local_user_view::LocalUserView};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
 
 #[async_trait::async_trait(?Send)]
 impl Perform for MarkCommentAsRead {
@@ -671,208 +217,3 @@ impl Perform for CreateCommentLike {
     Ok(res)
   }
 }
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetComments {
-  type Response = GetCommentsResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetCommentsResponse, LemmyError> {
-    let data: &GetComments = &self;
-    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
-    let person_id = local_user_view.map(|u| u.person.id);
-
-    let type_ = ListingType::from_str(&data.type_)?;
-    let sort = SortType::from_str(&data.sort)?;
-
-    let community_id = data.community_id;
-    let community_name = data.community_name.to_owned();
-    let saved_only = data.saved_only;
-    let page = data.page;
-    let limit = data.limit;
-    let comments = blocking(context.pool(), move |conn| {
-      CommentQueryBuilder::create(conn)
-        .listing_type(type_)
-        .sort(&sort)
-        .saved_only(saved_only)
-        .community_id(community_id)
-        .community_name(community_name)
-        .my_person_id(person_id)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await?;
-    let comments = match comments {
-      Ok(comments) => comments,
-      Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
-    };
-
-    Ok(GetCommentsResponse { comments })
-  }
-}
-
-/// Creates a comment report and notifies the moderators of the community
-#[async_trait::async_trait(?Send)]
-impl Perform for CreateCommentReport {
-  type Response = CreateCommentReportResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CreateCommentReportResponse, LemmyError> {
-    let data: &CreateCommentReport = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    // check size of report and check for whitespace
-    let reason = data.reason.trim();
-    if reason.is_empty() {
-      return Err(ApiError::err("report_reason_required").into());
-    }
-    if reason.chars().count() > 1000 {
-      return Err(ApiError::err("report_too_long").into());
-    }
-
-    let person_id = local_user_view.person.id;
-    let comment_id = data.comment_id;
-    let comment_view = blocking(context.pool(), move |conn| {
-      CommentView::read(&conn, comment_id, None)
-    })
-    .await??;
-
-    check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
-
-    let report_form = CommentReportForm {
-      creator_id: person_id,
-      comment_id,
-      original_comment_text: comment_view.comment.content,
-      reason: data.reason.to_owned(),
-    };
-
-    let report = match blocking(context.pool(), move |conn| {
-      CommentReport::report(conn, &report_form)
-    })
-    .await?
-    {
-      Ok(report) => report,
-      Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
-    };
-
-    let res = CreateCommentReportResponse { success: true };
-
-    context.chat_server().do_send(SendUserRoomMessage {
-      op: UserOperation::CreateCommentReport,
-      response: res.clone(),
-      local_recipient_id: local_user_view.local_user.id,
-      websocket_id,
-    });
-
-    context.chat_server().do_send(SendModRoomMessage {
-      op: UserOperation::CreateCommentReport,
-      response: report,
-      community_id: comment_view.community.id,
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-/// Resolves or unresolves a comment report and notifies the moderators of the community
-#[async_trait::async_trait(?Send)]
-impl Perform for ResolveCommentReport {
-  type Response = ResolveCommentReportResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<ResolveCommentReportResponse, LemmyError> {
-    let data: &ResolveCommentReport = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let report_id = data.report_id;
-    let report = blocking(context.pool(), move |conn| {
-      CommentReportView::read(&conn, report_id)
-    })
-    .await??;
-
-    let person_id = local_user_view.person.id;
-    is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
-
-    let resolved = data.resolved;
-    let resolve_fun = move |conn: &'_ _| {
-      if resolved {
-        CommentReport::resolve(conn, report_id, person_id)
-      } else {
-        CommentReport::unresolve(conn, report_id, person_id)
-      }
-    };
-
-    if blocking(context.pool(), resolve_fun).await?.is_err() {
-      return Err(ApiError::err("couldnt_resolve_report").into());
-    };
-
-    let report_id = data.report_id;
-    let res = ResolveCommentReportResponse {
-      report_id,
-      resolved,
-    };
-
-    context.chat_server().do_send(SendModRoomMessage {
-      op: UserOperation::ResolveCommentReport,
-      response: res.clone(),
-      community_id: report.community.id,
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-/// Lists comment reports for a community if an id is supplied
-/// or returns all comment reports for communities a user moderates
-#[async_trait::async_trait(?Send)]
-impl Perform for ListCommentReports {
-  type Response = ListCommentReportsResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<ListCommentReportsResponse, LemmyError> {
-    let data: &ListCommentReports = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let person_id = local_user_view.person.id;
-    let community_id = data.community;
-    let community_ids =
-      collect_moderated_communities(person_id, community_id, context.pool()).await?;
-
-    let page = data.page;
-    let limit = data.limit;
-    let comments = blocking(context.pool(), move |conn| {
-      CommentReportQueryBuilder::create(conn)
-        .community_ids(community_ids)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    let res = ListCommentReportsResponse { comments };
-
-    context.chat_server().do_send(SendUserRoomMessage {
-      op: UserOperation::ListCommentReports,
-      response: res.clone(),
-      local_recipient_id: local_user_view.local_user.id,
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
diff --git a/crates/api/src/comment_report.rs b/crates/api/src/comment_report.rs
new file mode 100644
index 00000000..9cd504b5
--- /dev/null
+++ b/crates/api/src/comment_report.rs
@@ -0,0 +1,184 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  collect_moderated_communities,
+  comment::*,
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+};
+use lemmy_db_queries::Reportable;
+use lemmy_db_schema::source::comment_report::*;
+use lemmy_db_views::{
+  comment_report_view::{CommentReportQueryBuilder, CommentReportView},
+  comment_view::CommentView,
+};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{
+  messages::{SendModRoomMessage, SendUserRoomMessage},
+  LemmyContext,
+  UserOperation,
+};
+
+/// Creates a comment report and notifies the moderators of the community
+#[async_trait::async_trait(?Send)]
+impl Perform for CreateCommentReport {
+  type Response = CreateCommentReportResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CreateCommentReportResponse, LemmyError> {
+    let data: &CreateCommentReport = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    // check size of report and check for whitespace
+    let reason = data.reason.trim();
+    if reason.is_empty() {
+      return Err(ApiError::err("report_reason_required").into());
+    }
+    if reason.chars().count() > 1000 {
+      return Err(ApiError::err("report_too_long").into());
+    }
+
+    let person_id = local_user_view.person.id;
+    let comment_id = data.comment_id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, comment_id, None)
+    })
+    .await??;
+
+    check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
+
+    let report_form = CommentReportForm {
+      creator_id: person_id,
+      comment_id,
+      original_comment_text: comment_view.comment.content,
+      reason: data.reason.to_owned(),
+    };
+
+    let report = match blocking(context.pool(), move |conn| {
+      CommentReport::report(conn, &report_form)
+    })
+    .await?
+    {
+      Ok(report) => report,
+      Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
+    };
+
+    let res = CreateCommentReportResponse { success: true };
+
+    context.chat_server().do_send(SendUserRoomMessage {
+      op: UserOperation::CreateCommentReport,
+      response: res.clone(),
+      local_recipient_id: local_user_view.local_user.id,
+      websocket_id,
+    });
+
+    context.chat_server().do_send(SendModRoomMessage {
+      op: UserOperation::CreateCommentReport,
+      response: report,
+      community_id: comment_view.community.id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+/// Resolves or unresolves a comment report and notifies the moderators of the community
+#[async_trait::async_trait(?Send)]
+impl Perform for ResolveCommentReport {
+  type Response = ResolveCommentReportResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ResolveCommentReportResponse, LemmyError> {
+    let data: &ResolveCommentReport = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let report_id = data.report_id;
+    let report = blocking(context.pool(), move |conn| {
+      CommentReportView::read(&conn, report_id)
+    })
+    .await??;
+
+    let person_id = local_user_view.person.id;
+    is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
+
+    let resolved = data.resolved;
+    let resolve_fun = move |conn: &'_ _| {
+      if resolved {
+        CommentReport::resolve(conn, report_id, person_id)
+      } else {
+        CommentReport::unresolve(conn, report_id, person_id)
+      }
+    };
+
+    if blocking(context.pool(), resolve_fun).await?.is_err() {
+      return Err(ApiError::err("couldnt_resolve_report").into());
+    };
+
+    let report_id = data.report_id;
+    let res = ResolveCommentReportResponse {
+      report_id,
+      resolved,
+    };
+
+    context.chat_server().do_send(SendModRoomMessage {
+      op: UserOperation::ResolveCommentReport,
+      response: res.clone(),
+      community_id: report.community.id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+/// Lists comment reports for a community if an id is supplied
+/// or returns all comment reports for communities a user moderates
+#[async_trait::async_trait(?Send)]
+impl Perform for ListCommentReports {
+  type Response = ListCommentReportsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ListCommentReportsResponse, LemmyError> {
+    let data: &ListCommentReports = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let person_id = local_user_view.person.id;
+    let community_id = data.community;
+    let community_ids =
+      collect_moderated_communities(person_id, community_id, context.pool()).await?;
+
+    let page = data.page;
+    let limit = data.limit;
+    let comments = blocking(context.pool(), move |conn| {
+      CommentReportQueryBuilder::create(conn)
+        .community_ids(community_ids)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    let res = ListCommentReportsResponse { comments };
+
+    context.chat_server().do_send(SendUserRoomMessage {
+      op: UserOperation::ListCommentReports,
+      response: res.clone(),
+      local_recipient_id: local_user_view.local_user.id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs
index f7e8e23c..1ee2a9a7 100644
--- a/crates/api/src/community.rs
+++ b/crates/api/src/community.rs
@@ -1,483 +1,41 @@
-use crate::{
+use crate::Perform;
+use actix_web::web::Data;
+use anyhow::Context;
+use lemmy_api_common::{
+  blocking,
   check_community_ban,
+  community::*,
   get_local_user_view_from_jwt,
-  get_local_user_view_from_jwt_opt,
-  is_admin,
   is_mod_or_admin,
-  Perform,
-};
-use actix_web::web::Data;
-use anyhow::Context;
-use lemmy_api_structs::{blocking, community::*};
-use lemmy_apub::{
-  generate_apub_endpoint,
-  generate_followers_url,
-  generate_inbox_url,
-  generate_shared_inbox_url,
-  ActorType,
-  CommunityType,
-  EndpointType,
-  UserType,
 };
+use lemmy_apub::{ActorType, CommunityType, UserType};
 use lemmy_db_queries::{
-  diesel_option_overwrite_to_url,
   source::{
     comment::Comment_,
     community::{CommunityModerator_, Community_},
     post::Post_,
   },
-  ApubObject,
   Bannable,
   Crud,
   Followable,
   Joinable,
-  ListingType,
-  SortType,
 };
-use lemmy_db_schema::{
-  naive_now,
-  source::{comment::Comment, community::*, moderator::*, person::Person, post::Post, site::*},
-  PersonId,
+use lemmy_db_schema::source::{
+  comment::Comment,
+  community::*,
+  moderator::*,
+  person::Person,
+  post::Post,
+  site::*,
 };
 use lemmy_db_views::comment_view::CommentQueryBuilder;
 use lemmy_db_views_actor::{
-  community_follower_view::CommunityFollowerView,
   community_moderator_view::CommunityModeratorView,
-  community_view::{CommunityQueryBuilder, CommunityView},
+  community_view::CommunityView,
   person_view::PersonViewSafe,
 };
-use lemmy_utils::{
-  apub::generate_actor_keypair,
-  location_info,
-  utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
-  ApiError,
-  ConnectionId,
-  LemmyError,
-};
-use lemmy_websocket::{
-  messages::{GetCommunityUsersOnline, SendCommunityRoomMessage},
-  LemmyContext,
-  UserOperation,
-};
-use std::str::FromStr;
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetCommunity {
-  type Response = GetCommunityResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetCommunityResponse, LemmyError> {
-    let data: &GetCommunity = &self;
-    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
-    let person_id = local_user_view.map(|u| u.person.id);
-
-    let community_id = match data.id {
-      Some(id) => id,
-      None => {
-        let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
-        match blocking(context.pool(), move |conn| {
-          Community::read_from_name(conn, &name)
-        })
-        .await?
-        {
-          Ok(community) => community,
-          Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
-        }
-        .id
-      }
-    };
-
-    let community_view = match blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, person_id)
-    })
-    .await?
-    {
-      Ok(community) => community,
-      Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
-    };
-
-    let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
-      CommunityModeratorView::for_community(conn, community_id)
-    })
-    .await?
-    {
-      Ok(moderators) => moderators,
-      Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
-    };
-
-    let online = context
-      .chat_server()
-      .send(GetCommunityUsersOnline { community_id })
-      .await
-      .unwrap_or(1);
-
-    let res = GetCommunityResponse {
-      community_view,
-      moderators,
-      online,
-    };
-
-    // Return the jwt
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for CreateCommunity {
-  type Response = CommunityResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<CommunityResponse, LemmyError> {
-    let data: &CreateCommunity = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    check_slurs(&data.name)?;
-    check_slurs(&data.title)?;
-    check_slurs_opt(&data.description)?;
-
-    if !is_valid_community_name(&data.name) {
-      return Err(ApiError::err("invalid_community_name").into());
-    }
-
-    // Double check for duplicate community actor_ids
-    let community_actor_id = generate_apub_endpoint(EndpointType::Community, &data.name)?;
-    let actor_id_cloned = community_actor_id.to_owned();
-    let community_dupe = blocking(context.pool(), move |conn| {
-      Community::read_from_apub_id(conn, &actor_id_cloned)
-    })
-    .await?;
-    if community_dupe.is_ok() {
-      return Err(ApiError::err("community_already_exists").into());
-    }
-
-    // Check to make sure the icon and banners are urls
-    let icon = diesel_option_overwrite_to_url(&data.icon)?;
-    let banner = diesel_option_overwrite_to_url(&data.banner)?;
-
-    // When you create a community, make sure the user becomes a moderator and a follower
-    let keypair = generate_actor_keypair()?;
-
-    let community_form = CommunityForm {
-      name: data.name.to_owned(),
-      title: data.title.to_owned(),
-      description: data.description.to_owned(),
-      icon,
-      banner,
-      creator_id: local_user_view.person.id,
-      removed: None,
-      deleted: None,
-      nsfw: data.nsfw,
-      updated: None,
-      actor_id: Some(community_actor_id.to_owned()),
-      local: true,
-      private_key: Some(keypair.private_key),
-      public_key: Some(keypair.public_key),
-      last_refreshed_at: None,
-      published: None,
-      followers_url: Some(generate_followers_url(&community_actor_id)?),
-      inbox_url: Some(generate_inbox_url(&community_actor_id)?),
-      shared_inbox_url: Some(Some(generate_shared_inbox_url(&community_actor_id)?)),
-    };
-
-    let inserted_community = match blocking(context.pool(), move |conn| {
-      Community::create(conn, &community_form)
-    })
-    .await?
-    {
-      Ok(community) => community,
-      Err(_e) => return Err(ApiError::err("community_already_exists").into()),
-    };
-
-    // The community creator becomes a moderator
-    let community_moderator_form = CommunityModeratorForm {
-      community_id: inserted_community.id,
-      person_id: local_user_view.person.id,
-    };
-
-    let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
-    if blocking(context.pool(), join).await?.is_err() {
-      return Err(ApiError::err("community_moderator_already_exists").into());
-    }
-
-    // Follow your own community
-    let community_follower_form = CommunityFollowerForm {
-      community_id: inserted_community.id,
-      person_id: local_user_view.person.id,
-      pending: false,
-    };
-
-    let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
-    if blocking(context.pool(), follow).await?.is_err() {
-      return Err(ApiError::err("community_follower_already_exists").into());
-    }
-
-    let person_id = local_user_view.person.id;
-    let community_view = blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, inserted_community.id, Some(person_id))
-    })
-    .await??;
-
-    Ok(CommunityResponse { community_view })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for EditCommunity {
-  type Response = CommunityResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommunityResponse, LemmyError> {
-    let data: &EditCommunity = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    check_slurs(&data.title)?;
-    check_slurs_opt(&data.description)?;
-
-    // Verify its a mod (only mods can edit it)
-    let community_id = data.community_id;
-    let mods: Vec<PersonId> = blocking(context.pool(), move |conn| {
-      CommunityModeratorView::for_community(conn, community_id)
-        .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
-    })
-    .await??;
-    if !mods.contains(&local_user_view.person.id) {
-      return Err(ApiError::err("not_a_moderator").into());
-    }
-
-    let community_id = data.community_id;
-    let read_community = blocking(context.pool(), move |conn| {
-      Community::read(conn, community_id)
-    })
-    .await??;
-
-    let icon = diesel_option_overwrite_to_url(&data.icon)?;
-    let banner = diesel_option_overwrite_to_url(&data.banner)?;
-
-    let community_form = CommunityForm {
-      name: read_community.name,
-      title: data.title.to_owned(),
-      description: data.description.to_owned(),
-      icon,
-      banner,
-      creator_id: read_community.creator_id,
-      removed: Some(read_community.removed),
-      deleted: Some(read_community.deleted),
-      nsfw: data.nsfw,
-      updated: Some(naive_now()),
-      actor_id: Some(read_community.actor_id),
-      local: read_community.local,
-      private_key: read_community.private_key,
-      public_key: read_community.public_key,
-      last_refreshed_at: None,
-      published: None,
-      followers_url: None,
-      inbox_url: None,
-      shared_inbox_url: None,
-    };
-
-    let community_id = data.community_id;
-    match blocking(context.pool(), move |conn| {
-      Community::update(conn, community_id, &community_form)
-    })
-    .await?
-    {
-      Ok(community) => community,
-      Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
-    };
-
-    // TODO there needs to be some kind of an apub update
-    // process for communities and users
-
-    let community_id = data.community_id;
-    let person_id = local_user_view.person.id;
-    let community_view = blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, Some(person_id))
-    })
-    .await??;
-
-    let res = CommunityResponse { community_view };
-
-    send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for DeleteCommunity {
-  type Response = CommunityResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommunityResponse, LemmyError> {
-    let data: &DeleteCommunity = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    // Verify its the creator (only a creator can delete the community)
-    let community_id = data.community_id;
-    let read_community = blocking(context.pool(), move |conn| {
-      Community::read(conn, community_id)
-    })
-    .await??;
-    if read_community.creator_id != local_user_view.person.id {
-      return Err(ApiError::err("no_community_edit_allowed").into());
-    }
-
-    // Do the delete
-    let community_id = data.community_id;
-    let deleted = data.deleted;
-    let updated_community = match blocking(context.pool(), move |conn| {
-      Community::update_deleted(conn, community_id, deleted)
-    })
-    .await?
-    {
-      Ok(community) => community,
-      Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
-    };
-
-    // Send apub messages
-    if deleted {
-      updated_community.send_delete(context).await?;
-    } else {
-      updated_community.send_undo_delete(context).await?;
-    }
-
-    let community_id = data.community_id;
-    let person_id = local_user_view.person.id;
-    let community_view = blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, Some(person_id))
-    })
-    .await??;
-
-    let res = CommunityResponse { community_view };
-
-    send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for RemoveCommunity {
-  type Response = CommunityResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CommunityResponse, LemmyError> {
-    let data: &RemoveCommunity = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    // 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 updated_community = match blocking(context.pool(), move |conn| {
-      Community::update_removed(conn, community_id, removed)
-    })
-    .await?
-    {
-      Ok(community) => community,
-      Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
-    };
-
-    // Mod tables
-    let expires = match data.expires {
-      Some(time) => Some(naive_from_unix(time)),
-      None => None,
-    };
-    let form = ModRemoveCommunityForm {
-      mod_person_id: local_user_view.person.id,
-      community_id: data.community_id,
-      removed: Some(removed),
-      reason: data.reason.to_owned(),
-      expires,
-    };
-    blocking(context.pool(), move |conn| {
-      ModRemoveCommunity::create(conn, &form)
-    })
-    .await??;
-
-    // Apub messages
-    if removed {
-      updated_community.send_remove(context).await?;
-    } else {
-      updated_community.send_undo_remove(context).await?;
-    }
-
-    let community_id = data.community_id;
-    let person_id = local_user_view.person.id;
-    let community_view = blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, Some(person_id))
-    })
-    .await??;
-
-    let res = CommunityResponse { community_view };
-
-    send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for ListCommunities {
-  type Response = ListCommunitiesResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<ListCommunitiesResponse, LemmyError> {
-    let data: &ListCommunities = &self;
-    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
-
-    let person_id = match &local_user_view {
-      Some(uv) => Some(uv.person.id),
-      None => None,
-    };
-
-    // Don't show NSFW by default
-    let show_nsfw = match &local_user_view {
-      Some(uv) => uv.local_user.show_nsfw,
-      None => false,
-    };
-
-    let type_ = ListingType::from_str(&data.type_)?;
-    let sort = SortType::from_str(&data.sort)?;
-
-    let page = data.page;
-    let limit = data.limit;
-    let communities = blocking(context.pool(), move |conn| {
-      CommunityQueryBuilder::create(conn)
-        .listing_type(&type_)
-        .sort(&sort)
-        .show_nsfw(show_nsfw)
-        .my_person_id(person_id)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    // Return the jwt
-    Ok(ListCommunitiesResponse { communities })
-  }
-}
+use lemmy_utils::{location_info, utils::naive_from_unix, ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
 
 #[async_trait::async_trait(?Send)]
 impl Perform for FollowCommunity {
@@ -553,33 +111,6 @@ impl Perform for FollowCommunity {
   }
 }
 
-#[async_trait::async_trait(?Send)]
-impl Perform for GetFollowedCommunities {
-  type Response = GetFollowedCommunitiesResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
-    let data: &GetFollowedCommunities = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let person_id = local_user_view.person.id;
-    let communities = match blocking(context.pool(), move |conn| {
-      CommunityFollowerView::for_person(conn, person_id)
-    })
-    .await?
-    {
-      Ok(communities) => communities,
-      _ => return Err(ApiError::err("system_err_login").into()),
-    };
-
-    // Return the jwt
-    Ok(GetFollowedCommunitiesResponse { communities })
-  }
-}
-
 #[async_trait::async_trait(?Send)]
 impl Perform for BanFromCommunity {
   type Response = BanFromCommunityResponse;
@@ -907,21 +438,3 @@ impl Perform for TransferCommunity {
     })
   }
 }
-
-fn send_community_websocket(
-  res: &CommunityResponse,
-  context: &Data<LemmyContext>,
-  websocket_id: Option<ConnectionId>,
-  op: UserOperation,
-) {
-  // Strip out the person id and subscribed when sending to others
-  let mut res_sent = res.clone();
-  res_sent.community_view.subscribed = false;
-
-  context.chat_server().do_send(SendCommunityRoomMessage {
-    op,
-    response: res_sent,
-    community_id: res.community_view.community.id,
-    websocket_id,
-  });
-}
diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs
index 529a13cf..277498f1 100644
--- a/crates/api/src/lib.rs
+++ b/crates/api/src/lib.rs
@@ -1,56 +1,20 @@
 use actix_web::{web, web::Data};
-use lemmy_api_structs::{
-  blocking,
-  comment::*,
-  community::*,
-  person::*,
-  post::*,
-  site::*,
-  websocket::*,
-};
-use lemmy_db_queries::{
-  source::{
-    community::{CommunityModerator_, Community_},
-    site::Site_,
-  },
-  Crud,
-  DbPool,
-};
-use lemmy_db_schema::{
-  source::{
-    community::{Community, CommunityModerator},
-    post::Post,
-    site::Site,
-  },
-  CommunityId,
-  LocalUserId,
-  PersonId,
-  PostId,
-};
-use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
-use lemmy_db_views_actor::{
-  community_person_ban_view::CommunityPersonBanView,
-  community_view::CommunityView,
-};
-use lemmy_utils::{
-  claims::Claims,
-  settings::structs::Settings,
-  ApiError,
-  ConnectionId,
-  LemmyError,
-};
+use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*, websocket::*};
+use lemmy_utils::{ConnectionId, LemmyError};
 use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
 use serde::Deserialize;
 use std::{env, process::Command};
-use url::Url;
 
-pub mod comment;
-pub mod community;
-pub mod local_user;
-pub mod post;
+mod comment;
+mod comment_report;
+mod community;
+mod local_user;
+mod post;
+mod post_report;
+mod private_message;
 pub mod routes;
-pub mod site;
-pub mod websocket;
+mod site;
+mod websocket;
 
 #[async_trait::async_trait(?Send)]
 pub trait Perform {
@@ -63,221 +27,35 @@ pub trait Perform {
   ) -> Result<Self::Response, LemmyError>;
 }
 
-pub(crate) async fn is_mod_or_admin(
-  pool: &DbPool,
-  person_id: PersonId,
-  community_id: CommunityId,
-) -> Result<(), LemmyError> {
-  let is_mod_or_admin = blocking(pool, move |conn| {
-    CommunityView::is_mod_or_admin(conn, person_id, community_id)
-  })
-  .await?;
-  if !is_mod_or_admin {
-    return Err(ApiError::err("not_a_mod_or_admin").into());
-  }
-  Ok(())
-}
-
-pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
-  if !local_user_view.local_user.admin {
-    return Err(ApiError::err("not_an_admin").into());
-  }
-  Ok(())
-}
-
-pub(crate) async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
-  match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
-    Ok(post) => Ok(post),
-    Err(_e) => Err(ApiError::err("couldnt_find_post").into()),
-  }
-}
-
-pub(crate) async fn get_local_user_view_from_jwt(
-  jwt: &str,
-  pool: &DbPool,
-) -> Result<LocalUserView, LemmyError> {
-  let claims = match Claims::decode(&jwt) {
-    Ok(claims) => claims.claims,
-    Err(_e) => return Err(ApiError::err("not_logged_in").into()),
-  };
-  let local_user_id = LocalUserId(claims.sub);
-  let local_user_view =
-    blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
-  // Check for a site ban
-  if local_user_view.person.banned {
-    return Err(ApiError::err("site_ban").into());
-  }
-
-  check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
-
-  Ok(local_user_view)
-}
-
-/// Checks if user's token was issued before user's password reset.
-pub(crate) fn check_validator_time(
-  validator_time: &chrono::NaiveDateTime,
-  claims: &Claims,
-) -> Result<(), LemmyError> {
-  let user_validation_time = validator_time.timestamp();
-  if user_validation_time > claims.iat {
-    Err(ApiError::err("not_logged_in").into())
-  } else {
-    Ok(())
-  }
-}
-
-pub(crate) async fn get_local_user_view_from_jwt_opt(
-  jwt: &Option<String>,
-  pool: &DbPool,
-) -> Result<Option<LocalUserView>, LemmyError> {
-  match jwt {
-    Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)),
-    None => Ok(None),
-  }
-}
-
-pub(crate) async fn get_local_user_settings_view_from_jwt(
-  jwt: &str,
-  pool: &DbPool,
-) -> Result<LocalUserSettingsView, LemmyError> {
-  let claims = match Claims::decode(&jwt) {
-    Ok(claims) => claims.claims,
-    Err(_e) => return Err(ApiError::err("not_logged_in").into()),
-  };
-  let local_user_id = LocalUserId(claims.sub);
-  let local_user_view = blocking(pool, move |conn| {
-    LocalUserSettingsView::read(conn, local_user_id)
-  })
-  .await??;
-  // Check for a site ban
-  if local_user_view.person.banned {
-    return Err(ApiError::err("site_ban").into());
-  }
-
-  check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
-
-  Ok(local_user_view)
-}
-
-pub(crate) async fn get_local_user_settings_view_from_jwt_opt(
-  jwt: &Option<String>,
-  pool: &DbPool,
-) -> Result<Option<LocalUserSettingsView>, LemmyError> {
-  match jwt {
-    Some(jwt) => Ok(Some(
-      get_local_user_settings_view_from_jwt(jwt, pool).await?,
-    )),
-    None => Ok(None),
-  }
-}
-
-pub(crate) async fn check_community_ban(
-  person_id: PersonId,
-  community_id: CommunityId,
-  pool: &DbPool,
-) -> Result<(), LemmyError> {
-  let is_banned =
-    move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
-  if blocking(pool, is_banned).await? {
-    Err(ApiError::err("community_ban").into())
-  } else {
-    Ok(())
-  }
-}
-
-pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
-  if score == -1 {
-    let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
-    if !site.enable_downvotes {
-      return Err(ApiError::err("downvotes_disabled").into());
-    }
-  }
-  Ok(())
-}
-
-/// Returns a list of communities that the user moderates
-/// or if a community_id is supplied validates the user is a moderator
-/// of that community and returns the community id in a vec
-///
-/// * `person_id` - the person id of the moderator
-/// * `community_id` - optional community id to check for moderator privileges
-/// * `pool` - the diesel db pool
-pub(crate) async fn collect_moderated_communities(
-  person_id: PersonId,
-  community_id: Option<CommunityId>,
-  pool: &DbPool,
-) -> Result<Vec<CommunityId>, LemmyError> {
-  if let Some(community_id) = community_id {
-    // if the user provides a community_id, just check for mod/admin privileges
-    is_mod_or_admin(pool, person_id, community_id).await?;
-    Ok(vec![community_id])
-  } else {
-    let ids = blocking(pool, move |conn: &'_ _| {
-      CommunityModerator::get_person_moderated_communities(conn, person_id)
-    })
-    .await??;
-    Ok(ids)
-  }
-}
-
-pub(crate) async fn build_federated_instances(
-  pool: &DbPool,
-) -> Result<Option<FederatedInstances>, LemmyError> {
-  if Settings::get().federation().enabled {
-    let distinct_communities = blocking(pool, move |conn| {
-      Community::distinct_federated_communities(conn)
-    })
-    .await??;
-
-    let allowed = Settings::get().get_allowed_instances();
-    let blocked = Settings::get().get_blocked_instances();
-
-    let mut linked = distinct_communities
-      .iter()
-      .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
-      .collect::<Result<Vec<String>, LemmyError>>()?;
-
-    if let Some(allowed) = allowed.as_ref() {
-      linked.extend_from_slice(allowed);
-    }
-
-    if let Some(blocked) = blocked.as_ref() {
-      linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname()));
-    }
-
-    // Sort and remove dupes
-    linked.sort_unstable();
-    linked.dedup();
-
-    Ok(Some(FederatedInstances {
-      linked,
-      allowed,
-      blocked,
-    }))
-  } else {
-    Ok(None)
-  }
-}
-
 pub async fn match_websocket_operation(
   context: LemmyContext,
   id: ConnectionId,
   op: UserOperation,
   data: &str,
 ) -> Result<String, LemmyError> {
+  //TODO: handle commented out actions in crud crate
+
   match op {
     // User ops
-    UserOperation::Login => do_websocket_operation::<Login>(context, id, op, data).await,
-    UserOperation::Register => do_websocket_operation::<Register>(context, id, op, data).await,
+    UserOperation::Login => {
+      //do_websocket_operation::<Login>(context, id, op, data).await
+      todo!()
+    }
+    UserOperation::Register => {
+      //do_websocket_operation::<Register>(context, id, op, data).await
+      todo!()
+    }
     UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
     UserOperation::GetPersonDetails => {
-      do_websocket_operation::<GetPersonDetails>(context, id, op, data).await
+      //do_websocket_operation::<GetPersonDetails>(context, id, op, data).await
+      todo!()
     }
     UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
     UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
     UserOperation::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await,
     UserOperation::GetPersonMentions => {
-      do_websocket_operation::<GetPersonMentions>(context, id, op, data).await
+      //do_websocket_operation::<GetPersonMentions>(context, id, op, data).await
+      todo!()
     }
     UserOperation::MarkPersonMentionAsRead => {
       do_websocket_operation::<MarkPersonMentionAsRead>(context, id, op, data).await
@@ -286,7 +64,8 @@ pub async fn match_websocket_operation(
       do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
     }
     UserOperation::DeleteAccount => {
-      do_websocket_operation::<DeleteAccount>(context, id, op, data).await
+      //do_websocket_operation::<DeleteAccount>(context, id, op, data).await
+      todo!()
     }
     UserOperation::PasswordReset => {
       do_websocket_operation::<PasswordReset>(context, id, op, data).await
@@ -309,26 +88,39 @@ pub async fn match_websocket_operation(
 
     // Private Message ops
     UserOperation::CreatePrivateMessage => {
-      do_websocket_operation::<CreatePrivateMessage>(context, id, op, data).await
+      //do_websocket_operation::<CreatePrivateMessage>(context, id, op, data).await
+      todo!()
     }
     UserOperation::EditPrivateMessage => {
-      do_websocket_operation::<EditPrivateMessage>(context, id, op, data).await
+      //do_websocket_operation::<EditPrivateMessage>(context, id, op, data).await
+      todo!()
     }
     UserOperation::DeletePrivateMessage => {
-      do_websocket_operation::<DeletePrivateMessage>(context, id, op, data).await
+      //do_websocket_operation::<DeletePrivateMessage>(context, id, op, data).await
+      todo!()
     }
     UserOperation::MarkPrivateMessageAsRead => {
       do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await
     }
     UserOperation::GetPrivateMessages => {
-      do_websocket_operation::<GetPrivateMessages>(context, id, op, data).await
+      //do_websocket_operation::<GetPrivateMessages>(context, id, op, data).await
+      todo!()
     }
 
     // Site ops
     UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await,
-    UserOperation::CreateSite => do_websocket_operation::<CreateSite>(context, id, op, data).await,
-    UserOperation::EditSite => do_websocket_operation::<EditSite>(context, id, op, data).await,
-    UserOperation::GetSite => do_websocket_operation::<GetSite>(context, id, op, data).await,
+    UserOperation::CreateSite => {
+      //do_websocket_operation::<CreateSite>(context, id, op, data).await
+      todo!()
+    }
+    UserOperation::EditSite => {
+      //do_websocket_operation::<EditSite>(context, id, op, data).await
+      todo!()
+    }
+    UserOperation::GetSite => {
+      //do_websocket_operation::<GetSite>(context, id, op, data).await
+      todo!()
+    }
     UserOperation::GetSiteConfig => {
       do_websocket_operation::<GetSiteConfig>(context, id, op, data).await
     }
@@ -345,22 +137,28 @@ pub async fn match_websocket_operation(
 
     // Community ops
     UserOperation::GetCommunity => {
-      do_websocket_operation::<GetCommunity>(context, id, op, data).await
+      //do_websocket_operation::<GetCommunity>(context, id, op, data).await
+      todo!()
     }
     UserOperation::ListCommunities => {
-      do_websocket_operation::<ListCommunities>(context, id, op, data).await
+      //do_websocket_operation::<ListCommunities>(context, id, op, data).await
+      todo!()
     }
     UserOperation::CreateCommunity => {
-      do_websocket_operation::<CreateCommunity>(context, id, op, data).await
+      //do_websocket_operation::<CreateCommunity>(context, id, op, data).await
+      todo!()
     }
     UserOperation::EditCommunity => {
-      do_websocket_operation::<EditCommunity>(context, id, op, data).await
+      //do_websocket_operation::<EditCommunity>(context, id, op, data).await
+      todo!()
     }
     UserOperation::DeleteCommunity => {
-      do_websocket_operation::<DeleteCommunity>(context, id, op, data).await
+      //do_websocket_operation::<DeleteCommunity>(context, id, op, data).await
+      todo!()
     }
     UserOperation::RemoveCommunity => {
-      do_websocket_operation::<RemoveCommunity>(context, id, op, data).await
+      //do_websocket_operation::<RemoveCommunity>(context, id, op, data).await
+      todo!()
     }
     UserOperation::FollowCommunity => {
       do_websocket_operation::<FollowCommunity>(context, id, op, data).await
@@ -376,12 +174,30 @@ pub async fn match_websocket_operation(
     }
 
     // Post ops
-    UserOperation::CreatePost => do_websocket_operation::<CreatePost>(context, id, op, data).await,
-    UserOperation::GetPost => do_websocket_operation::<GetPost>(context, id, op, data).await,
-    UserOperation::GetPosts => do_websocket_operation::<GetPosts>(context, id, op, data).await,
-    UserOperation::EditPost => do_websocket_operation::<EditPost>(context, id, op, data).await,
-    UserOperation::DeletePost => do_websocket_operation::<DeletePost>(context, id, op, data).await,
-    UserOperation::RemovePost => do_websocket_operation::<RemovePost>(context, id, op, data).await,
+    UserOperation::CreatePost => {
+      //do_websocket_operation::<CreatePost>(context, id, op, data).await
+      todo!()
+    }
+    UserOperation::GetPost => {
+      //do_websocket_operation::<GetPost>(context, id, op, data).await
+      todo!()
+    }
+    UserOperation::GetPosts => {
+      //do_websocket_operation::<GetPosts>(context, id, op, data).await
+      todo!()
+    }
+    UserOperation::EditPost => {
+      //do_websocket_operation::<EditPost>(context, id, op, data).await
+      todo!()
+    }
+    UserOperation::DeletePost => {
+      //do_websocket_operation::<DeletePost>(context, id, op, data).await
+      todo!()
+    }
+    UserOperation::RemovePost => {
+      //do_websocket_operation::<RemovePost>(context, id, op, data).await
+      todo!()
+    }
     UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
     UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
     UserOperation::CreatePostLike => {
@@ -400,16 +216,20 @@ pub async fn match_websocket_operation(
 
     // Comment ops
     UserOperation::CreateComment => {
-      do_websocket_operation::<CreateComment>(context, id, op, data).await
+      //do_websocket_operation::<CreateComment>(context, id, op, data).await
+      todo!()
     }
     UserOperation::EditComment => {
-      do_websocket_operation::<EditComment>(context, id, op, data).await
+      //do_websocket_operation::<EditComment>(context, id, op, data).await
+      todo!()
     }
     UserOperation::DeleteComment => {
-      do_websocket_operation::<DeleteComment>(context, id, op, data).await
+      //do_websocket_operation::<DeleteComment>(context, id, op, data).await
+      todo!()
     }
     UserOperation::RemoveComment => {
-      do_websocket_operation::<RemoveComment>(context, id, op, data).await
+      //do_websocket_operation::<RemoveComment>(context, id, op, data).await
+      todo!()
     }
     UserOperation::MarkCommentAsRead => {
       do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
@@ -418,7 +238,8 @@ pub async fn match_websocket_operation(
       do_websocket_operation::<SaveComment>(context, id, op, data).await
     }
     UserOperation::GetComments => {
-      do_websocket_operation::<GetComments>(context, id, op, data).await
+      //do_websocket_operation::<GetComments>(context, id, op, data).await
+      todo!()
     }
     UserOperation::CreateCommentLike => {
       do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
@@ -503,18 +324,10 @@ pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
   Ok(base64)
 }
 
-/// Checks the password length
-pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> {
-  if pass.len() > 60 {
-    Err(ApiError::err("invalid_password").into())
-  } else {
-    Ok(())
-  }
-}
-
 #[cfg(test)]
 mod tests {
   use crate::{captcha_espeak_wav_base64, check_validator_time};
+  use lemmy_api_common::check_validator_time;
   use lemmy_db_queries::{establish_unpooled_connection, source::local_user::LocalUser_, Crud};
   use lemmy_db_schema::source::{
     local_user::{LocalUser, LocalUserForm},
diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs
index 6aa8b26c..aacb7d0b 100644
--- a/crates/api/src/local_user.rs
+++ b/crates/api/src/local_user.rs
@@ -1,25 +1,17 @@
-use crate::{
-  captcha_espeak_wav_base64,
-  collect_moderated_communities,
-  get_local_user_view_from_jwt,
-  get_local_user_view_from_jwt_opt,
-  is_admin,
-  password_length_check,
-  Perform,
-};
+use crate::{captcha_espeak_wav_base64, Perform};
 use actix_web::web::Data;
 use anyhow::Context;
 use bcrypt::verify;
 use captcha::{gen, Difficulty};
 use chrono::Duration;
-use lemmy_api_structs::{blocking, person::*, send_email_to_user};
-use lemmy_apub::{
-  generate_apub_endpoint,
-  generate_followers_url,
-  generate_inbox_url,
-  generate_shared_inbox_url,
-  ApubObjectType,
-  EndpointType,
+use lemmy_api_common::{
+  blocking,
+  collect_moderated_communities,
+  community::{GetFollowedCommunities, GetFollowedCommunitiesResponse},
+  get_local_user_view_from_jwt,
+  is_admin,
+  password_length_check,
+  person::*,
 };
 use lemmy_db_queries::{
   diesel_option_overwrite,
@@ -33,12 +25,8 @@ use lemmy_db_queries::{
     person_mention::PersonMention_,
     post::Post_,
     private_message::PrivateMessage_,
-    site::Site_,
   },
   Crud,
-  Followable,
-  Joinable,
-  ListingType,
   SortType,
 };
 use lemmy_db_schema::{
@@ -52,45 +40,33 @@ use lemmy_db_schema::{
     person::*,
     person_mention::*,
     post::Post,
-    private_message::*,
+    private_message::PrivateMessage,
     site::*,
   },
-  CommunityId,
 };
 use lemmy_db_views::{
   comment_report_view::CommentReportView,
   comment_view::CommentQueryBuilder,
   local_user_view::LocalUserView,
   post_report_view::PostReportView,
-  post_view::PostQueryBuilder,
-  private_message_view::{PrivateMessageQueryBuilder, PrivateMessageView},
 };
 use lemmy_db_views_actor::{
   community_follower_view::CommunityFollowerView,
-  community_moderator_view::CommunityModeratorView,
   person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
   person_view::PersonViewSafe,
 };
 use lemmy_utils::{
-  apub::generate_actor_keypair,
   claims::Claims,
   email::send_email,
   location_info,
   settings::structs::Settings,
-  utils::{
-    check_slurs,
-    generate_random_string,
-    is_valid_preferred_username,
-    is_valid_username,
-    naive_from_unix,
-    remove_slurs,
-  },
+  utils::{generate_random_string, is_valid_preferred_username, naive_from_unix},
   ApiError,
   ConnectionId,
   LemmyError,
 };
 use lemmy_websocket::{
-  messages::{CaptchaItem, CheckCaptcha, SendAllMessage, SendUserRoomMessage},
+  messages::{CaptchaItem, SendAllMessage, SendUserRoomMessage},
   LemmyContext,
   UserOperation,
 };
@@ -135,212 +111,6 @@ impl Perform for Login {
   }
 }
 
-#[async_trait::async_trait(?Send)]
-impl Perform for Register {
-  type Response = LoginResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<LoginResponse, LemmyError> {
-    let data: &Register = &self;
-
-    // Make sure site has open registration
-    if let Ok(site) = blocking(context.pool(), move |conn| Site::read_simple(conn)).await? {
-      if !site.open_registration {
-        return Err(ApiError::err("registration_closed").into());
-      }
-    }
-
-    password_length_check(&data.password)?;
-
-    // Make sure passwords match
-    if data.password != data.password_verify {
-      return Err(ApiError::err("passwords_dont_match").into());
-    }
-
-    // Check if there are admins. False if admins exist
-    let no_admins = blocking(context.pool(), move |conn| {
-      PersonViewSafe::admins(conn).map(|a| a.is_empty())
-    })
-    .await??;
-
-    // If its not the admin, check the captcha
-    if !no_admins && Settings::get().captcha().enabled {
-      let check = context
-        .chat_server()
-        .send(CheckCaptcha {
-          uuid: data
-            .captcha_uuid
-            .to_owned()
-            .unwrap_or_else(|| "".to_string()),
-          answer: data
-            .captcha_answer
-            .to_owned()
-            .unwrap_or_else(|| "".to_string()),
-        })
-        .await?;
-      if !check {
-        return Err(ApiError::err("captcha_incorrect").into());
-      }
-    }
-
-    check_slurs(&data.username)?;
-
-    let actor_keypair = generate_actor_keypair()?;
-    if !is_valid_username(&data.username) {
-      return Err(ApiError::err("invalid_username").into());
-    }
-    let actor_id = generate_apub_endpoint(EndpointType::Person, &data.username)?;
-
-    // We have to create both a person, and local_user
-
-    // Register the new person
-    let person_form = PersonForm {
-      name: data.username.to_owned(),
-      avatar: None,
-      banner: None,
-      preferred_username: None,
-      published: None,
-      updated: None,
-      banned: None,
-      deleted: None,
-      actor_id: Some(actor_id.clone()),
-      bio: None,
-      local: Some(true),
-      private_key: Some(Some(actor_keypair.private_key)),
-      public_key: Some(Some(actor_keypair.public_key)),
-      last_refreshed_at: None,
-      inbox_url: Some(generate_inbox_url(&actor_id)?),
-      shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)),
-    };
-
-    // insert the person
-    let inserted_person = match blocking(context.pool(), move |conn| {
-      Person::create(conn, &person_form)
-    })
-    .await?
-    {
-      Ok(u) => u,
-      Err(_) => {
-        return Err(ApiError::err("user_already_exists").into());
-      }
-    };
-
-    // Create the local user
-    let local_user_form = LocalUserForm {
-      person_id: inserted_person.id,
-      email: Some(data.email.to_owned()),
-      matrix_user_id: None,
-      password_encrypted: data.password.to_owned(),
-      admin: Some(no_admins),
-      show_nsfw: Some(data.show_nsfw),
-      theme: Some("browser".into()),
-      default_sort_type: Some(SortType::Active as i16),
-      default_listing_type: Some(ListingType::Subscribed as i16),
-      lang: Some("browser".into()),
-      show_avatars: Some(true),
-      send_notifications_to_email: Some(false),
-    };
-
-    let inserted_local_user = match blocking(context.pool(), move |conn| {
-      LocalUser::register(conn, &local_user_form)
-    })
-    .await?
-    {
-      Ok(lu) => lu,
-      Err(e) => {
-        let err_type = if e.to_string()
-          == "duplicate key value violates unique constraint \"local_user_email_key\""
-        {
-          "email_already_exists"
-        } else {
-          "user_already_exists"
-        };
-
-        // If the local user creation errored, then delete that person
-        blocking(context.pool(), move |conn| {
-          Person::delete(&conn, inserted_person.id)
-        })
-        .await??;
-
-        return Err(ApiError::err(err_type).into());
-      }
-    };
-
-    let main_community_keypair = generate_actor_keypair()?;
-
-    // Create the main community if it doesn't exist
-    let main_community = match blocking(context.pool(), move |conn| {
-      Community::read(conn, CommunityId(2))
-    })
-    .await?
-    {
-      Ok(c) => c,
-      Err(_e) => {
-        let default_community_name = "main";
-        let actor_id = generate_apub_endpoint(EndpointType::Community, default_community_name)?;
-        let community_form = CommunityForm {
-          name: default_community_name.to_string(),
-          title: "The Default Community".to_string(),
-          description: Some("The Default Community".to_string()),
-          nsfw: false,
-          creator_id: inserted_person.id,
-          removed: None,
-          deleted: None,
-          updated: None,
-          actor_id: Some(actor_id.to_owned()),
-          local: true,
-          private_key: Some(main_community_keypair.private_key),
-          public_key: Some(main_community_keypair.public_key),
-          last_refreshed_at: None,
-          published: None,
-          icon: None,
-          banner: None,
-          followers_url: Some(generate_followers_url(&actor_id)?),
-          inbox_url: Some(generate_inbox_url(&actor_id)?),
-          shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)),
-        };
-        blocking(context.pool(), move |conn| {
-          Community::create(conn, &community_form)
-        })
-        .await??
-      }
-    };
-
-    // Sign them up for main community no matter what
-    let community_follower_form = CommunityFollowerForm {
-      community_id: main_community.id,
-      person_id: inserted_person.id,
-      pending: false,
-    };
-
-    let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
-    if blocking(context.pool(), follow).await?.is_err() {
-      return Err(ApiError::err("community_follower_already_exists").into());
-    };
-
-    // If its an admin, add them as a mod and follower to main
-    if no_admins {
-      let community_moderator_form = CommunityModeratorForm {
-        community_id: main_community.id,
-        person_id: inserted_person.id,
-      };
-
-      let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
-      if blocking(context.pool(), join).await?.is_err() {
-        return Err(ApiError::err("community_moderator_already_exists").into());
-      }
-    }
-
-    // Return the jwt
-    Ok(LoginResponse {
-      jwt: Claims::jwt(inserted_local_user.id.0)?,
-    })
-  }
-}
-
 #[async_trait::async_trait(?Send)]
 impl Perform for GetCaptcha {
   type Response = GetCaptchaResponse;
@@ -531,114 +301,6 @@ impl Perform for SaveUserSettings {
   }
 }
 
-#[async_trait::async_trait(?Send)]
-impl Perform for GetPersonDetails {
-  type Response = GetPersonDetailsResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetPersonDetailsResponse, LemmyError> {
-    let data: &GetPersonDetails = &self;
-    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
-
-    let show_nsfw = match &local_user_view {
-      Some(uv) => uv.local_user.show_nsfw,
-      None => false,
-    };
-
-    let sort = SortType::from_str(&data.sort)?;
-
-    let username = data
-      .username
-      .to_owned()
-      .unwrap_or_else(|| "admin".to_string());
-    let person_details_id = match data.person_id {
-      Some(id) => id,
-      None => {
-        let person = blocking(context.pool(), move |conn| {
-          Person::find_by_name(conn, &username)
-        })
-        .await?;
-        match person {
-          Ok(p) => p.id,
-          Err(_e) => return Err(ApiError::err("couldnt_find_that_username_or_email").into()),
-        }
-      }
-    };
-
-    let person_id = local_user_view.map(|uv| uv.person.id);
-
-    // You don't need to return settings for the user, since this comes back with GetSite
-    // `my_user`
-    let person_view = blocking(context.pool(), move |conn| {
-      PersonViewSafe::read(conn, person_details_id)
-    })
-    .await??;
-
-    let page = data.page;
-    let limit = data.limit;
-    let saved_only = data.saved_only;
-    let community_id = data.community_id;
-
-    let (posts, comments) = blocking(context.pool(), move |conn| {
-      let mut posts_query = PostQueryBuilder::create(conn)
-        .sort(&sort)
-        .show_nsfw(show_nsfw)
-        .saved_only(saved_only)
-        .community_id(community_id)
-        .my_person_id(person_id)
-        .page(page)
-        .limit(limit);
-
-      let mut comments_query = CommentQueryBuilder::create(conn)
-        .my_person_id(person_id)
-        .sort(&sort)
-        .saved_only(saved_only)
-        .community_id(community_id)
-        .page(page)
-        .limit(limit);
-
-      // If its saved only, you don't care what creator it was
-      // Or, if its not saved, then you only want it for that specific creator
-      if !saved_only {
-        posts_query = posts_query.creator_id(person_details_id);
-        comments_query = comments_query.creator_id(person_details_id);
-      }
-
-      let posts = posts_query.list()?;
-      let comments = comments_query.list()?;
-
-      Ok((posts, comments)) as Result<_, LemmyError>
-    })
-    .await??;
-
-    let mut follows = vec![];
-    if let Some(pid) = person_id {
-      if pid == person_details_id {
-        follows = blocking(context.pool(), move |conn| {
-          CommunityFollowerView::for_person(conn, person_details_id)
-        })
-        .await??;
-      }
-    };
-    let moderates = blocking(context.pool(), move |conn| {
-      CommunityModeratorView::for_person(conn, person_details_id)
-    })
-    .await??;
-
-    // Return the jwt
-    Ok(GetPersonDetailsResponse {
-      person_view,
-      follows,
-      moderates,
-      comments,
-      posts,
-    })
-  }
-}
-
 #[async_trait::async_trait(?Send)]
 impl Perform for AddAdmin {
   type Response = AddAdminResponse;
@@ -947,52 +609,6 @@ impl Perform for MarkAllAsRead {
   }
 }
 
-#[async_trait::async_trait(?Send)]
-impl Perform for DeleteAccount {
-  type Response = LoginResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<LoginResponse, LemmyError> {
-    let data: &DeleteAccount = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    // Verify the password
-    let valid: bool = verify(
-      &data.password,
-      &local_user_view.local_user.password_encrypted,
-    )
-    .unwrap_or(false);
-    if !valid {
-      return Err(ApiError::err("password_incorrect").into());
-    }
-
-    // Comments
-    let person_id = local_user_view.person.id;
-    let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id);
-    if blocking(context.pool(), permadelete).await?.is_err() {
-      return Err(ApiError::err("couldnt_update_comment").into());
-    }
-
-    // Posts
-    let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id);
-    if blocking(context.pool(), permadelete).await?.is_err() {
-      return Err(ApiError::err("couldnt_update_post").into());
-    }
-
-    blocking(context.pool(), move |conn| {
-      Person::delete_account(conn, person_id)
-    })
-    .await??;
-
-    Ok(LoginResponse {
-      jwt: data.auth.to_owned(),
-    })
-  }
-}
-
 #[async_trait::async_trait(?Send)]
 impl Perform for PasswordReset {
   type Response = PasswordResetResponse;
@@ -1084,344 +700,6 @@ impl Perform for PasswordChange {
   }
 }
 
-#[async_trait::async_trait(?Send)]
-impl Perform for CreatePrivateMessage {
-  type Response = PrivateMessageResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &CreatePrivateMessage = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let content_slurs_removed = remove_slurs(&data.content.to_owned());
-
-    let private_message_form = PrivateMessageForm {
-      content: content_slurs_removed.to_owned(),
-      creator_id: local_user_view.person.id,
-      recipient_id: data.recipient_id,
-      deleted: None,
-      read: None,
-      updated: None,
-      ap_id: None,
-      local: true,
-      published: None,
-    };
-
-    let inserted_private_message = match blocking(context.pool(), move |conn| {
-      PrivateMessage::create(conn, &private_message_form)
-    })
-    .await?
-    {
-      Ok(private_message) => private_message,
-      Err(_e) => {
-        return Err(ApiError::err("couldnt_create_private_message").into());
-      }
-    };
-
-    let inserted_private_message_id = inserted_private_message.id;
-    let updated_private_message = match blocking(
-      context.pool(),
-      move |conn| -> Result<PrivateMessage, LemmyError> {
-        let apub_id = generate_apub_endpoint(
-          EndpointType::PrivateMessage,
-          &inserted_private_message_id.to_string(),
-        )?;
-        Ok(PrivateMessage::update_ap_id(
-          &conn,
-          inserted_private_message_id,
-          apub_id,
-        )?)
-      },
-    )
-    .await?
-    {
-      Ok(private_message) => private_message,
-      Err(_e) => return Err(ApiError::err("couldnt_create_private_message").into()),
-    };
-
-    updated_private_message
-      .send_create(&local_user_view.person, context)
-      .await?;
-
-    let private_message_view = blocking(context.pool(), move |conn| {
-      PrivateMessageView::read(conn, inserted_private_message.id)
-    })
-    .await??;
-
-    let res = PrivateMessageResponse {
-      private_message_view,
-    };
-
-    // Send notifications to the local recipient, if one exists
-    let recipient_id = data.recipient_id;
-    if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
-      LocalUserView::read_person(conn, recipient_id)
-    })
-    .await?
-    {
-      send_email_to_user(
-        &local_recipient,
-        "Private Message from",
-        "Private Message",
-        &content_slurs_removed,
-      );
-
-      let local_recipient_id = local_recipient.local_user.id;
-      context.chat_server().do_send(SendUserRoomMessage {
-        op: UserOperation::CreatePrivateMessage,
-        response: res.clone(),
-        local_recipient_id,
-        websocket_id,
-      });
-    }
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for EditPrivateMessage {
-  type Response = PrivateMessageResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &EditPrivateMessage = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    // Checking permissions
-    let private_message_id = data.private_message_id;
-    let orig_private_message = blocking(context.pool(), move |conn| {
-      PrivateMessage::read(conn, private_message_id)
-    })
-    .await??;
-    if local_user_view.person.id != orig_private_message.creator_id {
-      return Err(ApiError::err("no_private_message_edit_allowed").into());
-    }
-
-    // Doing the update
-    let content_slurs_removed = remove_slurs(&data.content);
-    let private_message_id = data.private_message_id;
-    let updated_private_message = match blocking(context.pool(), move |conn| {
-      PrivateMessage::update_content(conn, private_message_id, &content_slurs_removed)
-    })
-    .await?
-    {
-      Ok(private_message) => private_message,
-      Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
-    };
-
-    // Send the apub update
-    updated_private_message
-      .send_update(&local_user_view.person, context)
-      .await?;
-
-    let private_message_id = data.private_message_id;
-    let private_message_view = blocking(context.pool(), move |conn| {
-      PrivateMessageView::read(conn, private_message_id)
-    })
-    .await??;
-
-    let res = PrivateMessageResponse {
-      private_message_view,
-    };
-
-    // Send notifications to the local recipient, if one exists
-    let recipient_id = orig_private_message.recipient_id;
-    if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
-      LocalUserView::read_person(conn, recipient_id)
-    })
-    .await?
-    {
-      let local_recipient_id = local_recipient.local_user.id;
-      context.chat_server().do_send(SendUserRoomMessage {
-        op: UserOperation::EditPrivateMessage,
-        response: res.clone(),
-        local_recipient_id,
-        websocket_id,
-      });
-    }
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for DeletePrivateMessage {
-  type Response = PrivateMessageResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &DeletePrivateMessage = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    // Checking permissions
-    let private_message_id = data.private_message_id;
-    let orig_private_message = blocking(context.pool(), move |conn| {
-      PrivateMessage::read(conn, private_message_id)
-    })
-    .await??;
-    if local_user_view.person.id != orig_private_message.creator_id {
-      return Err(ApiError::err("no_private_message_edit_allowed").into());
-    }
-
-    // Doing the update
-    let private_message_id = data.private_message_id;
-    let deleted = data.deleted;
-    let updated_private_message = match blocking(context.pool(), move |conn| {
-      PrivateMessage::update_deleted(conn, private_message_id, deleted)
-    })
-    .await?
-    {
-      Ok(private_message) => private_message,
-      Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
-    };
-
-    // Send the apub update
-    if data.deleted {
-      updated_private_message
-        .send_delete(&local_user_view.person, context)
-        .await?;
-    } else {
-      updated_private_message
-        .send_undo_delete(&local_user_view.person, context)
-        .await?;
-    }
-
-    let private_message_id = data.private_message_id;
-    let private_message_view = blocking(context.pool(), move |conn| {
-      PrivateMessageView::read(conn, private_message_id)
-    })
-    .await??;
-
-    let res = PrivateMessageResponse {
-      private_message_view,
-    };
-
-    // Send notifications to the local recipient, if one exists
-    let recipient_id = orig_private_message.recipient_id;
-    if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
-      LocalUserView::read_person(conn, recipient_id)
-    })
-    .await?
-    {
-      let local_recipient_id = local_recipient.local_user.id;
-      context.chat_server().do_send(SendUserRoomMessage {
-        op: UserOperation::DeletePrivateMessage,
-        response: res.clone(),
-        local_recipient_id,
-        websocket_id,
-      });
-    }
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for MarkPrivateMessageAsRead {
-  type Response = PrivateMessageResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &MarkPrivateMessageAsRead = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    // Checking permissions
-    let private_message_id = data.private_message_id;
-    let orig_private_message = blocking(context.pool(), move |conn| {
-      PrivateMessage::read(conn, private_message_id)
-    })
-    .await??;
-    if local_user_view.person.id != orig_private_message.recipient_id {
-      return Err(ApiError::err("couldnt_update_private_message").into());
-    }
-
-    // Doing the update
-    let private_message_id = data.private_message_id;
-    let read = data.read;
-    match blocking(context.pool(), move |conn| {
-      PrivateMessage::update_read(conn, private_message_id, read)
-    })
-    .await?
-    {
-      Ok(private_message) => private_message,
-      Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
-    };
-
-    // No need to send an apub update
-    let private_message_id = data.private_message_id;
-    let private_message_view = blocking(context.pool(), move |conn| {
-      PrivateMessageView::read(conn, private_message_id)
-    })
-    .await??;
-
-    let res = PrivateMessageResponse {
-      private_message_view,
-    };
-
-    // Send notifications to the local recipient, if one exists
-    let recipient_id = orig_private_message.recipient_id;
-    if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
-      LocalUserView::read_person(conn, recipient_id)
-    })
-    .await?
-    {
-      let local_recipient_id = local_recipient.local_user.id;
-      context.chat_server().do_send(SendUserRoomMessage {
-        op: UserOperation::MarkPrivateMessageAsRead,
-        response: res.clone(),
-        local_recipient_id,
-        websocket_id,
-      });
-    }
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetPrivateMessages {
-  type Response = PrivateMessagesResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<PrivateMessagesResponse, LemmyError> {
-    let data: &GetPrivateMessages = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-    let person_id = local_user_view.person.id;
-
-    let page = data.page;
-    let limit = data.limit;
-    let unread_only = data.unread_only;
-    let messages = blocking(context.pool(), move |conn| {
-      PrivateMessageQueryBuilder::create(&conn, person_id)
-        .page(page)
-        .limit(limit)
-        .unread_only(unread_only)
-        .list()
-    })
-    .await??;
-
-    Ok(PrivateMessagesResponse {
-      private_messages: messages,
-    })
-  }
-}
-
 #[async_trait::async_trait(?Send)]
 impl Perform for GetReportCount {
   type Response = GetReportCountResponse;
@@ -1477,3 +755,30 @@ impl Perform for GetReportCount {
     Ok(res)
   }
 }
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetFollowedCommunities {
+  type Response = GetFollowedCommunitiesResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
+    let data: &GetFollowedCommunities = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let person_id = local_user_view.person.id;
+    let communities = match blocking(context.pool(), move |conn| {
+      CommunityFollowerView::for_person(conn, person_id)
+    })
+    .await?
+    {
+      Ok(communities) => communities,
+      _ => return Err(ApiError::err("system_err_login").into()),
+    };
+
+    // Return the jwt
+    Ok(GetFollowedCommunitiesResponse { communities })
+  }
+}
diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs
index bbc3e04b..be39cf53 100644
--- a/crates/api/src/post.rs
+++ b/crates/api/src/post.rs
@@ -1,289 +1,19 @@
-use crate::{
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
   check_community_ban,
   check_downvotes_enabled,
-  collect_moderated_communities,
   get_local_user_view_from_jwt,
-  get_local_user_view_from_jwt_opt,
   is_mod_or_admin,
-  Perform,
-};
-use actix_web::web::Data;
-use lemmy_api_structs::{blocking, post::*};
-use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
-use lemmy_db_queries::{
-  source::post::Post_,
-  Crud,
-  Likeable,
-  ListingType,
-  Reportable,
-  Saveable,
-  SortType,
-};
-use lemmy_db_schema::{
-  naive_now,
-  source::{
-    moderator::*,
-    post::*,
-    post_report::{PostReport, PostReportForm},
-  },
-};
-use lemmy_db_views::{
-  comment_view::CommentQueryBuilder,
-  post_report_view::{PostReportQueryBuilder, PostReportView},
-  post_view::{PostQueryBuilder, PostView},
-};
-use lemmy_db_views_actor::{
-  community_moderator_view::CommunityModeratorView,
-  community_view::CommunityView,
+  post::*,
 };
-use lemmy_utils::{
-  request::fetch_iframely_and_pictrs_data,
-  utils::{check_slurs, check_slurs_opt, is_valid_post_title},
-  ApiError,
-  ConnectionId,
-  LemmyError,
-};
-use lemmy_websocket::{
-  messages::{GetPostUsersOnline, SendModRoomMessage, SendPost, SendUserRoomMessage},
-  LemmyContext,
-  UserOperation,
-};
-use std::str::FromStr;
-
-#[async_trait::async_trait(?Send)]
-impl Perform for CreatePost {
-  type Response = PostResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostResponse, LemmyError> {
-    let data: &CreatePost = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    check_slurs(&data.name)?;
-    check_slurs_opt(&data.body)?;
-
-    if !is_valid_post_title(&data.name) {
-      return Err(ApiError::err("invalid_post_title").into());
-    }
-
-    check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?;
-
-    // Fetch Iframely and pictrs cached image
-    let data_url = data.url.as_ref();
-    let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
-      fetch_iframely_and_pictrs_data(context.client(), data_url).await;
-
-    let post_form = PostForm {
-      name: data.name.trim().to_owned(),
-      url: data_url.map(|u| u.to_owned().into()),
-      body: data.body.to_owned(),
-      community_id: data.community_id,
-      creator_id: local_user_view.person.id,
-      removed: None,
-      deleted: None,
-      nsfw: data.nsfw,
-      locked: None,
-      stickied: None,
-      updated: None,
-      embed_title: iframely_title,
-      embed_description: iframely_description,
-      embed_html: iframely_html,
-      thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
-      ap_id: None,
-      local: true,
-      published: None,
-    };
-
-    let inserted_post =
-      match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
-        Ok(post) => post,
-        Err(e) => {
-          let err_type = if e.to_string() == "value too long for type character varying(200)" {
-            "post_title_too_long"
-          } else {
-            "couldnt_create_post"
-          };
-
-          return Err(ApiError::err(err_type).into());
-        }
-      };
-
-    let inserted_post_id = inserted_post.id;
-    let updated_post = match blocking(context.pool(), move |conn| -> Result<Post, LemmyError> {
-      let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?;
-      Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
-    })
-    .await?
-    {
-      Ok(post) => post,
-      Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
-    };
-
-    updated_post
-      .send_create(&local_user_view.person, context)
-      .await?;
-
-    // They like their own post by default
-    let like_form = PostLikeForm {
-      post_id: inserted_post.id,
-      person_id: local_user_view.person.id,
-      score: 1,
-    };
-
-    let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
-    if blocking(context.pool(), like).await?.is_err() {
-      return Err(ApiError::err("couldnt_like_post").into());
-    }
-
-    updated_post
-      .send_like(&local_user_view.person, context)
-      .await?;
-
-    // Refetch the view
-    let inserted_post_id = inserted_post.id;
-    let post_view = match blocking(context.pool(), move |conn| {
-      PostView::read(conn, inserted_post_id, Some(local_user_view.person.id))
-    })
-    .await?
-    {
-      Ok(post) => post,
-      Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
-    };
-
-    let res = PostResponse { post_view };
-
-    context.chat_server().do_send(SendPost {
-      op: UserOperation::CreatePost,
-      post: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetPost {
-  type Response = GetPostResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetPostResponse, LemmyError> {
-    let data: &GetPost = &self;
-    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
-    let person_id = local_user_view.map(|u| u.person.id);
-
-    let id = data.id;
-    let post_view = match blocking(context.pool(), move |conn| {
-      PostView::read(conn, id, person_id)
-    })
-    .await?
-    {
-      Ok(post) => post,
-      Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
-    };
-
-    let id = data.id;
-    let comments = blocking(context.pool(), move |conn| {
-      CommentQueryBuilder::create(conn)
-        .my_person_id(person_id)
-        .post_id(id)
-        .limit(9999)
-        .list()
-    })
-    .await??;
-
-    let community_id = post_view.community.id;
-    let moderators = blocking(context.pool(), move |conn| {
-      CommunityModeratorView::for_community(conn, community_id)
-    })
-    .await??;
-
-    // Necessary for the sidebar
-    let community_view = match blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, person_id)
-    })
-    .await?
-    {
-      Ok(community) => community,
-      Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
-    };
-
-    let online = context
-      .chat_server()
-      .send(GetPostUsersOnline { post_id: data.id })
-      .await
-      .unwrap_or(1);
-
-    // Return the jwt
-    Ok(GetPostResponse {
-      post_view,
-      community_view,
-      comments,
-      moderators,
-      online,
-    })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetPosts {
-  type Response = GetPostsResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<GetPostsResponse, LemmyError> {
-    let data: &GetPosts = &self;
-    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
-
-    let person_id = match &local_user_view {
-      Some(uv) => Some(uv.person.id),
-      None => None,
-    };
-
-    let show_nsfw = match &local_user_view {
-      Some(uv) => uv.local_user.show_nsfw,
-      None => false,
-    };
-
-    let type_ = ListingType::from_str(&data.type_)?;
-    let sort = SortType::from_str(&data.sort)?;
-
-    let page = data.page;
-    let limit = data.limit;
-    let community_id = data.community_id;
-    let community_name = data.community_name.to_owned();
-    let saved_only = data.saved_only;
-
-    let posts = match blocking(context.pool(), move |conn| {
-      PostQueryBuilder::create(conn)
-        .listing_type(&type_)
-        .sort(&sort)
-        .show_nsfw(show_nsfw)
-        .community_id(community_id)
-        .community_name(community_name)
-        .saved_only(saved_only)
-        .my_person_id(person_id)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await?
-    {
-      Ok(posts) => posts,
-      Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
-    };
-
-    Ok(GetPostsResponse { posts })
-  }
-}
+use lemmy_apub::{ApubLikeableType, ApubObjectType};
+use lemmy_db_queries::{source::post::Post_, Crud, Likeable, Saveable};
+use lemmy_db_schema::source::{moderator::*, post::*};
+use lemmy_db_views::post_view::PostView;
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
 
 #[async_trait::async_trait(?Send)]
 impl Perform for CreatePostLike {
@@ -362,253 +92,6 @@ impl Perform for CreatePostLike {
   }
 }
 
-#[async_trait::async_trait(?Send)]
-impl Perform for EditPost {
-  type Response = PostResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostResponse, LemmyError> {
-    let data: &EditPost = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    check_slurs(&data.name)?;
-    check_slurs_opt(&data.body)?;
-
-    if !is_valid_post_title(&data.name) {
-      return Err(ApiError::err("invalid_post_title").into());
-    }
-
-    let post_id = data.post_id;
-    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_post.community_id,
-      context.pool(),
-    )
-    .await?;
-
-    // Verify that only the creator can edit
-    if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
-      return Err(ApiError::err("no_post_edit_allowed").into());
-    }
-
-    // Fetch Iframely and Pictrs cached image
-    let data_url = data.url.as_ref();
-    let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
-      fetch_iframely_and_pictrs_data(context.client(), data_url).await;
-
-    let post_form = PostForm {
-      name: data.name.trim().to_owned(),
-      url: data_url.map(|u| u.to_owned().into()),
-      body: data.body.to_owned(),
-      nsfw: data.nsfw,
-      creator_id: orig_post.creator_id.to_owned(),
-      community_id: orig_post.community_id,
-      removed: Some(orig_post.removed),
-      deleted: Some(orig_post.deleted),
-      locked: Some(orig_post.locked),
-      stickied: Some(orig_post.stickied),
-      updated: Some(naive_now()),
-      embed_title: iframely_title,
-      embed_description: iframely_description,
-      embed_html: iframely_html,
-      thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
-      ap_id: Some(orig_post.ap_id),
-      local: orig_post.local,
-      published: None,
-    };
-
-    let post_id = data.post_id;
-    let res = blocking(context.pool(), move |conn| {
-      Post::update(conn, post_id, &post_form)
-    })
-    .await?;
-    let updated_post: Post = match res {
-      Ok(post) => post,
-      Err(e) => {
-        let err_type = if e.to_string() == "value too long for type character varying(200)" {
-          "post_title_too_long"
-        } else {
-          "couldnt_update_post"
-        };
-
-        return Err(ApiError::err(err_type).into());
-      }
-    };
-
-    // Send apub update
-    updated_post
-      .send_update(&local_user_view.person, context)
-      .await?;
-
-    let post_id = data.post_id;
-    let post_view = blocking(context.pool(), move |conn| {
-      PostView::read(conn, post_id, Some(local_user_view.person.id))
-    })
-    .await??;
-
-    let res = PostResponse { post_view };
-
-    context.chat_server().do_send(SendPost {
-      op: UserOperation::EditPost,
-      post: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for DeletePost {
-  type Response = PostResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostResponse, LemmyError> {
-    let data: &DeletePost = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let post_id = data.post_id;
-    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_post.community_id,
-      context.pool(),
-    )
-    .await?;
-
-    // Verify that only the creator can delete
-    if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
-      return Err(ApiError::err("no_post_edit_allowed").into());
-    }
-
-    // Update the post
-    let post_id = data.post_id;
-    let deleted = data.deleted;
-    let updated_post = blocking(context.pool(), move |conn| {
-      Post::update_deleted(conn, post_id, deleted)
-    })
-    .await??;
-
-    // apub updates
-    if deleted {
-      updated_post
-        .send_delete(&local_user_view.person, context)
-        .await?;
-    } else {
-      updated_post
-        .send_undo_delete(&local_user_view.person, context)
-        .await?;
-    }
-
-    // Refetch the post
-    let post_id = data.post_id;
-    let post_view = blocking(context.pool(), move |conn| {
-      PostView::read(conn, post_id, Some(local_user_view.person.id))
-    })
-    .await??;
-
-    let res = PostResponse { post_view };
-
-    context.chat_server().do_send(SendPost {
-      op: UserOperation::DeletePost,
-      post: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for RemovePost {
-  type Response = PostResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<PostResponse, LemmyError> {
-    let data: &RemovePost = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let post_id = data.post_id;
-    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-
-    check_community_ban(
-      local_user_view.person.id,
-      orig_post.community_id,
-      context.pool(),
-    )
-    .await?;
-
-    // Verify that only the mods can remove
-    is_mod_or_admin(
-      context.pool(),
-      local_user_view.person.id,
-      orig_post.community_id,
-    )
-    .await?;
-
-    // Update the post
-    let post_id = data.post_id;
-    let removed = data.removed;
-    let updated_post = blocking(context.pool(), move |conn| {
-      Post::update_removed(conn, post_id, removed)
-    })
-    .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.to_owned(),
-    };
-    blocking(context.pool(), move |conn| {
-      ModRemovePost::create(conn, &form)
-    })
-    .await??;
-
-    // apub updates
-    if removed {
-      updated_post
-        .send_remove(&local_user_view.person, context)
-        .await?;
-    } else {
-      updated_post
-        .send_undo_remove(&local_user_view.person, context)
-        .await?;
-    }
-
-    // Refetch the post
-    let post_id = data.post_id;
-    let person_id = local_user_view.person.id;
-    let post_view = blocking(context.pool(), move |conn| {
-      PostView::read(conn, post_id, Some(person_id))
-    })
-    .await??;
-
-    let res = PostResponse { post_view };
-
-    context.chat_server().do_send(SendPost {
-      op: UserOperation::RemovePost,
-      post: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
 #[async_trait::async_trait(?Send)]
 impl Perform for LockPost {
   type Response = PostResponse;
@@ -792,166 +275,3 @@ impl Perform for SavePost {
     Ok(PostResponse { post_view })
   }
 }
-
-/// Creates a post report and notifies the moderators of the community
-#[async_trait::async_trait(?Send)]
-impl Perform for CreatePostReport {
-  type Response = CreatePostReportResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<CreatePostReportResponse, LemmyError> {
-    let data: &CreatePostReport = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    // check size of report and check for whitespace
-    let reason = data.reason.trim();
-    if reason.is_empty() {
-      return Err(ApiError::err("report_reason_required").into());
-    }
-    if reason.chars().count() > 1000 {
-      return Err(ApiError::err("report_too_long").into());
-    }
-
-    let person_id = local_user_view.person.id;
-    let post_id = data.post_id;
-    let post_view = blocking(context.pool(), move |conn| {
-      PostView::read(&conn, post_id, None)
-    })
-    .await??;
-
-    check_community_ban(person_id, post_view.community.id, context.pool()).await?;
-
-    let report_form = PostReportForm {
-      creator_id: person_id,
-      post_id,
-      original_post_name: post_view.post.name,
-      original_post_url: post_view.post.url,
-      original_post_body: post_view.post.body,
-      reason: data.reason.to_owned(),
-    };
-
-    let report = match blocking(context.pool(), move |conn| {
-      PostReport::report(conn, &report_form)
-    })
-    .await?
-    {
-      Ok(report) => report,
-      Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
-    };
-
-    let res = CreatePostReportResponse { success: true };
-
-    context.chat_server().do_send(SendUserRoomMessage {
-      op: UserOperation::CreatePostReport,
-      response: res.clone(),
-      local_recipient_id: local_user_view.local_user.id,
-      websocket_id,
-    });
-
-    context.chat_server().do_send(SendModRoomMessage {
-      op: UserOperation::CreatePostReport,
-      response: report,
-      community_id: post_view.community.id,
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-/// Resolves or unresolves a post report and notifies the moderators of the community
-#[async_trait::async_trait(?Send)]
-impl Perform for ResolvePostReport {
-  type Response = ResolvePostReportResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<ResolvePostReportResponse, LemmyError> {
-    let data: &ResolvePostReport = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let report_id = data.report_id;
-    let report = blocking(context.pool(), move |conn| {
-      PostReportView::read(&conn, report_id)
-    })
-    .await??;
-
-    let person_id = local_user_view.person.id;
-    is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
-
-    let resolved = data.resolved;
-    let resolve_fun = move |conn: &'_ _| {
-      if resolved {
-        PostReport::resolve(conn, report_id, person_id)
-      } else {
-        PostReport::unresolve(conn, report_id, person_id)
-      }
-    };
-
-    let res = ResolvePostReportResponse {
-      report_id,
-      resolved: true,
-    };
-
-    if blocking(context.pool(), resolve_fun).await?.is_err() {
-      return Err(ApiError::err("couldnt_resolve_report").into());
-    };
-
-    context.chat_server().do_send(SendModRoomMessage {
-      op: UserOperation::ResolvePostReport,
-      response: res.clone(),
-      community_id: report.community.id,
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-/// Lists post reports for a community if an id is supplied
-/// or returns all post reports for communities a user moderates
-#[async_trait::async_trait(?Send)]
-impl Perform for ListPostReports {
-  type Response = ListPostReportsResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<ListPostReportsResponse, LemmyError> {
-    let data: &ListPostReports = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    let person_id = local_user_view.person.id;
-    let community_id = data.community;
-    let community_ids =
-      collect_moderated_communities(person_id, community_id, context.pool()).await?;
-
-    let page = data.page;
-    let limit = data.limit;
-    let posts = blocking(context.pool(), move |conn| {
-      PostReportQueryBuilder::create(conn)
-        .community_ids(community_ids)
-        .page(page)
-        .limit(limit)
-        .list()
-    })
-    .await??;
-
-    let res = ListPostReportsResponse { posts };
-
-    context.chat_server().do_send(SendUserRoomMessage {
-      op: UserOperation::ListPostReports,
-      response: res.clone(),
-      local_recipient_id: local_user_view.local_user.id,
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
diff --git a/crates/api/src/post_report.rs b/crates/api/src/post_report.rs
new file mode 100644
index 00000000..c81ee2aa
--- /dev/null
+++ b/crates/api/src/post_report.rs
@@ -0,0 +1,192 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  collect_moderated_communities,
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+  post::{
+    CreatePostReport,
+    CreatePostReportResponse,
+    ListPostReports,
+    ListPostReportsResponse,
+    ResolvePostReport,
+    ResolvePostReportResponse,
+  },
+};
+use lemmy_db_queries::Reportable;
+use lemmy_db_schema::source::post_report::{PostReport, PostReportForm};
+use lemmy_db_views::{
+  post_report_view::{PostReportQueryBuilder, PostReportView},
+  post_view::PostView,
+};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{
+  messages::{SendModRoomMessage, SendUserRoomMessage},
+  LemmyContext,
+  UserOperation,
+};
+
+/// Creates a post report and notifies the moderators of the community
+#[async_trait::async_trait(?Send)]
+impl Perform for CreatePostReport {
+  type Response = CreatePostReportResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CreatePostReportResponse, LemmyError> {
+    let data: &CreatePostReport = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    // check size of report and check for whitespace
+    let reason = data.reason.trim();
+    if reason.is_empty() {
+      return Err(ApiError::err("report_reason_required").into());
+    }
+    if reason.chars().count() > 1000 {
+      return Err(ApiError::err("report_too_long").into());
+    }
+
+    let person_id = local_user_view.person.id;
+    let post_id = data.post_id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(&conn, post_id, None)
+    })
+    .await??;
+
+    check_community_ban(person_id, post_view.community.id, context.pool()).await?;
+
+    let report_form = PostReportForm {
+      creator_id: person_id,
+      post_id,
+      original_post_name: post_view.post.name,
+      original_post_url: post_view.post.url,
+      original_post_body: post_view.post.body,
+      reason: data.reason.to_owned(),
+    };
+
+    let report = match blocking(context.pool(), move |conn| {
+      PostReport::report(conn, &report_form)
+    })
+    .await?
+    {
+      Ok(report) => report,
+      Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
+    };
+
+    let res = CreatePostReportResponse { success: true };
+
+    context.chat_server().do_send(SendUserRoomMessage {
+      op: UserOperation::CreatePostReport,
+      response: res.clone(),
+      local_recipient_id: local_user_view.local_user.id,
+      websocket_id,
+    });
+
+    context.chat_server().do_send(SendModRoomMessage {
+      op: UserOperation::CreatePostReport,
+      response: report,
+      community_id: post_view.community.id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+/// Resolves or unresolves a post report and notifies the moderators of the community
+#[async_trait::async_trait(?Send)]
+impl Perform for ResolvePostReport {
+  type Response = ResolvePostReportResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ResolvePostReportResponse, LemmyError> {
+    let data: &ResolvePostReport = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let report_id = data.report_id;
+    let report = blocking(context.pool(), move |conn| {
+      PostReportView::read(&conn, report_id)
+    })
+    .await??;
+
+    let person_id = local_user_view.person.id;
+    is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
+
+    let resolved = data.resolved;
+    let resolve_fun = move |conn: &'_ _| {
+      if resolved {
+        PostReport::resolve(conn, report_id, person_id)
+      } else {
+        PostReport::unresolve(conn, report_id, person_id)
+      }
+    };
+
+    let res = ResolvePostReportResponse {
+      report_id,
+      resolved: true,
+    };
+
+    if blocking(context.pool(), resolve_fun).await?.is_err() {
+      return Err(ApiError::err("couldnt_resolve_report").into());
+    };
+
+    context.chat_server().do_send(SendModRoomMessage {
+      op: UserOperation::ResolvePostReport,
+      response: res.clone(),
+      community_id: report.community.id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+/// Lists post reports for a community if an id is supplied
+/// or returns all post reports for communities a user moderates
+#[async_trait::async_trait(?Send)]
+impl Perform for ListPostReports {
+  type Response = ListPostReportsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<ListPostReportsResponse, LemmyError> {
+    let data: &ListPostReports = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let person_id = local_user_view.person.id;
+    let community_id = data.community;
+    let community_ids =
+      collect_moderated_communities(person_id, community_id, context.pool()).await?;
+
+    let page = data.page;
+    let limit = data.limit;
+    let posts = blocking(context.pool(), move |conn| {
+      PostReportQueryBuilder::create(conn)
+        .community_ids(community_ids)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    let res = ListPostReportsResponse { posts };
+
+    context.chat_server().do_send(SendUserRoomMessage {
+      op: UserOperation::ListPostReports,
+      response: res.clone(),
+      local_recipient_id: local_user_view.local_user.id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/private_message.rs b/crates/api/src/private_message.rs
new file mode 100644
index 00000000..5420084d
--- /dev/null
+++ b/crates/api/src/private_message.rs
@@ -0,0 +1,77 @@
+use crate::Perform;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{MarkPrivateMessageAsRead, PrivateMessageResponse},
+};
+use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud};
+use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl Perform for MarkPrivateMessageAsRead {
+  type Response = PrivateMessageResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PrivateMessageResponse, LemmyError> {
+    let data: &MarkPrivateMessageAsRead = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    // Checking permissions
+    let private_message_id = data.private_message_id;
+    let orig_private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::read(conn, private_message_id)
+    })
+    .await??;
+    if local_user_view.person.id != orig_private_message.recipient_id {
+      return Err(ApiError::err("couldnt_update_private_message").into());
+    }
+
+    // Doing the update
+    let private_message_id = data.private_message_id;
+    let read = data.read;
+    match blocking(context.pool(), move |conn| {
+      PrivateMessage::update_read(conn, private_message_id, read)
+    })
+    .await?
+    {
+      Ok(private_message) => private_message,
+      Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
+    };
+
+    // No need to send an apub update
+    let private_message_id = data.private_message_id;
+    let private_message_view = blocking(context.pool(), move |conn| {
+      PrivateMessageView::read(conn, private_message_id)
+    })
+    .await??;
+
+    let res = PrivateMessageResponse {
+      private_message_view,
+    };
+
+    // Send notifications to the local recipient, if one exists
+    let recipient_id = orig_private_message.recipient_id;
+    if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
+      LocalUserView::read_person(conn, recipient_id)
+    })
+    .await?
+    {
+      let local_recipient_id = local_recipient.local_user.id;
+      context.chat_server().do_send(SendUserRoomMessage {
+        op: UserOperation::MarkPrivateMessageAsRead,
+        response: res.clone(),
+        local_recipient_id,
+        websocket_id,
+      });
+    }
+
+    Ok(res)
+  }
+}
diff --git a/crates/api/src/routes.rs b/crates/api/src/routes.rs
index 6fc46ca4..cdc9e736 100644
--- a/crates/api/src/routes.rs
+++ b/crates/api/src/routes.rs
@@ -1,6 +1,6 @@
 use crate::Perform;
 use actix_web::{error::ErrorBadRequest, *};
-use lemmy_api_structs::{comment::*, community::*, person::*, post::*, site::*, websocket::*};
+use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*, websocket::*};
 use lemmy_utils::rate_limit::RateLimit;
 use lemmy_websocket::{routes::chat_route, LemmyContext};
 use serde::Deserialize;
@@ -14,10 +14,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
       .service(
         web::scope("/site")
           .wrap(rate_limit.message())
-          .route("", web::get().to(route_get::<GetSite>))
           // Admin Actions
-          .route("", web::post().to(route_post::<CreateSite>))
-          .route("", web::put().to(route_post::<EditSite>))
           .route("/transfer", web::post().to(route_post::<TransferSite>))
           .route("/config", web::get().to(route_get::<GetSiteConfig>))
           .route("/config", web::put().to(route_post::<SaveSiteConfig>)),
@@ -33,22 +30,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .route(web::get().to(route_get::<Search>)),
       )
       // Community
-      .service(
-        web::resource("/community")
-          .guard(guard::Post())
-          .wrap(rate_limit.register())
-          .route(web::post().to(route_post::<CreateCommunity>)),
-      )
       .service(
         web::scope("/community")
           .wrap(rate_limit.message())
-          .route("", web::get().to(route_get::<GetCommunity>))
-          .route("", web::put().to(route_post::<EditCommunity>))
-          .route("/list", web::get().to(route_get::<ListCommunities>))
           .route("/follow", web::post().to(route_post::<FollowCommunity>))
-          .route("/delete", web::post().to(route_post::<DeleteCommunity>))
-          // Mod Actions
-          .route("/remove", web::post().to(route_post::<RemoveCommunity>))
           .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>))
@@ -56,23 +41,11 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .route("/mod/join", web::post().to(route_post::<ModJoin>)),
       )
       // Post
-      .service(
-        // Handle POST to /post separately to add the post() rate limitter
-        web::resource("/post")
-          .guard(guard::Post())
-          .wrap(rate_limit.post())
-          .route(web::post().to(route_post::<CreatePost>)),
-      )
       .service(
         web::scope("/post")
           .wrap(rate_limit.message())
-          .route("", web::get().to(route_get::<GetPost>))
-          .route("", web::put().to(route_post::<EditPost>))
-          .route("/delete", web::post().to(route_post::<DeletePost>))
-          .route("/remove", web::post().to(route_post::<RemovePost>))
           .route("/lock", web::post().to(route_post::<LockPost>))
           .route("/sticky", web::post().to(route_post::<StickyPost>))
-          .route("/list", web::get().to(route_get::<GetPosts>))
           .route("/like", web::post().to(route_post::<CreatePostLike>))
           .route("/save", web::put().to(route_post::<SavePost>))
           .route("/join", web::post().to(route_post::<PostJoin>))
@@ -87,17 +60,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
       .service(
         web::scope("/comment")
           .wrap(rate_limit.message())
-          .route("", web::post().to(route_post::<CreateComment>))
-          .route("", web::put().to(route_post::<EditComment>))
-          .route("/delete", web::post().to(route_post::<DeleteComment>))
-          .route("/remove", web::post().to(route_post::<RemoveComment>))
           .route(
             "/mark_as_read",
             web::post().to(route_post::<MarkCommentAsRead>),
           )
           .route("/like", web::post().to(route_post::<CreateCommentLike>))
           .route("/save", web::put().to(route_post::<SaveComment>))
-          .route("/list", web::get().to(route_get::<GetComments>))
           .route("/report", web::post().to(route_post::<CreateCommentReport>))
           .route(
             "/report/resolve",
@@ -112,32 +80,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
       .service(
         web::scope("/private_message")
           .wrap(rate_limit.message())
-          .route("/list", web::get().to(route_get::<GetPrivateMessages>))
-          .route("", web::post().to(route_post::<CreatePrivateMessage>))
-          .route("", web::put().to(route_post::<EditPrivateMessage>))
-          .route(
-            "/delete",
-            web::post().to(route_post::<DeletePrivateMessage>),
-          )
           .route(
             "/mark_as_read",
             web::post().to(route_post::<MarkPrivateMessageAsRead>),
           ),
       )
-      // User
-      .service(
-        // Account action, I don't like that it's in /user maybe /accounts
-        // Handle /user/register separately to add the register() rate limitter
-        web::resource("/user/register")
-          .guard(guard::Post())
-          .wrap(rate_limit.register())
-          .route(web::post().to(route_post::<Register>)),
-      )
       // User actions
       .service(
         web::scope("/user")
           .wrap(rate_limit.message())
-          .route("", web::get().to(route_get::<GetPersonDetails>))
           .route("/mention", web::get().to(route_get::<GetPersonMentions>))
           .route(
             "/mention/mark_as_read",
@@ -154,10 +105,6 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           // Account actions. I don't like that they're in /user maybe /accounts
           .route("/login", web::post().to(route_post::<Login>))
           .route("/get_captcha", web::get().to(route_get::<GetCaptcha>))
-          .route(
-            "/delete_account",
-            web::post().to(route_post::<DeleteAccount>),
-          )
           .route(
             "/password_reset",
             web::post().to(route_post::<PasswordReset>),
diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs
index 4d561ab2..91402da1 100644
--- a/crates/api/src/site.rs
+++ b/crates/api/src/site.rs
@@ -1,30 +1,18 @@
-use crate::{
+use crate::Perform;
+use actix_web::web::Data;
+use anyhow::Context;
+use lemmy_api_common::{
+  blocking,
   build_federated_instances,
   get_local_user_settings_view_from_jwt,
-  get_local_user_settings_view_from_jwt_opt,
   get_local_user_view_from_jwt,
   get_local_user_view_from_jwt_opt,
   is_admin,
-  Perform,
+  site::*,
 };
-use actix_web::web::Data;
-use anyhow::Context;
-use lemmy_api_structs::{blocking, person::Register, site::*};
 use lemmy_apub::fetcher::search::search_by_apub_id;
-use lemmy_db_queries::{
-  diesel_option_overwrite_to_url,
-  source::site::Site_,
-  Crud,
-  SearchType,
-  SortType,
-};
-use lemmy_db_schema::{
-  naive_now,
-  source::{
-    moderator::*,
-    site::{Site, *},
-  },
-};
+use lemmy_db_queries::{source::site::Site_, Crud, SearchType, SortType};
+use lemmy_db_schema::source::{moderator::*, site::Site};
 use lemmy_db_views::{
   comment_view::CommentQueryBuilder,
   post_view::PostQueryBuilder,
@@ -48,18 +36,13 @@ use lemmy_db_views_moderator::{
 use lemmy_utils::{
   location_info,
   settings::structs::Settings,
-  utils::{check_slurs, check_slurs_opt},
   version,
   ApiError,
   ConnectionId,
   LemmyError,
 };
-use lemmy_websocket::{
-  messages::{GetUsersOnline, SendAllMessage},
-  LemmyContext,
-  UserOperation,
-};
-use log::{debug, info};
+use lemmy_websocket::LemmyContext;
+use log::debug;
 use std::str::FromStr;
 
 #[async_trait::async_trait(?Send)]
@@ -136,189 +119,6 @@ impl Perform for GetModlog {
   }
 }
 
-#[async_trait::async_trait(?Send)]
-impl Perform for CreateSite {
-  type Response = SiteResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    _websocket_id: Option<ConnectionId>,
-  ) -> Result<SiteResponse, LemmyError> {
-    let data: &CreateSite = &self;
-
-    let read_site = move |conn: &'_ _| Site::read_simple(conn);
-    if blocking(context.pool(), read_site).await?.is_ok() {
-      return Err(ApiError::err("site_already_exists").into());
-    };
-
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    check_slurs(&data.name)?;
-    check_slurs_opt(&data.description)?;
-
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
-
-    let site_form = SiteForm {
-      name: data.name.to_owned(),
-      description: data.description.to_owned(),
-      icon: Some(data.icon.to_owned().map(|url| url.into())),
-      banner: Some(data.banner.to_owned().map(|url| url.into())),
-      creator_id: local_user_view.person.id,
-      enable_downvotes: data.enable_downvotes,
-      open_registration: data.open_registration,
-      enable_nsfw: data.enable_nsfw,
-      updated: None,
-    };
-
-    let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
-    if blocking(context.pool(), create_site).await?.is_err() {
-      return Err(ApiError::err("site_already_exists").into());
-    }
-
-    let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
-
-    Ok(SiteResponse { site_view })
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for EditSite {
-  type Response = SiteResponse;
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<SiteResponse, LemmyError> {
-    let data: &EditSite = &self;
-    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
-
-    check_slurs(&data.name)?;
-    check_slurs_opt(&data.description)?;
-
-    // Make sure user is an admin
-    is_admin(&local_user_view)?;
-
-    let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
-
-    let icon = diesel_option_overwrite_to_url(&data.icon)?;
-    let banner = diesel_option_overwrite_to_url(&data.banner)?;
-
-    let site_form = SiteForm {
-      name: data.name.to_owned(),
-      description: data.description.to_owned(),
-      icon,
-      banner,
-      creator_id: found_site.creator_id,
-      updated: Some(naive_now()),
-      enable_downvotes: data.enable_downvotes,
-      open_registration: data.open_registration,
-      enable_nsfw: data.enable_nsfw,
-    };
-
-    let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
-    if blocking(context.pool(), update_site).await?.is_err() {
-      return Err(ApiError::err("couldnt_update_site").into());
-    }
-
-    let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
-
-    let res = SiteResponse { site_view };
-
-    context.chat_server().do_send(SendAllMessage {
-      op: UserOperation::EditSite,
-      response: res.clone(),
-      websocket_id,
-    });
-
-    Ok(res)
-  }
-}
-
-#[async_trait::async_trait(?Send)]
-impl Perform for GetSite {
-  type Response = GetSiteResponse;
-
-  async fn perform(
-    &self,
-    context: &Data<LemmyContext>,
-    websocket_id: Option<ConnectionId>,
-  ) -> Result<GetSiteResponse, LemmyError> {
-    let data: &GetSite = &self;
-
-    let site_view = match blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
-      Ok(site_view) => Some(site_view),
-      // If the site isn't created yet, check the setup
-      Err(_) => {
-        if let Some(setup) = Settings::get().setup().as_ref() {
-          let register = Register {
-            username: setup.admin_username.to_owned(),
-            email: setup.admin_email.to_owned(),
-            password: setup.admin_password.to_owned(),
-            password_verify: setup.admin_password.to_owned(),
-            show_nsfw: true,
-            captcha_uuid: None,
-            captcha_answer: None,
-          };
-          let login_response = register.perform(context, websocket_id).await?;
-          info!("Admin {} created", setup.admin_username);
-
-          let create_site = CreateSite {
-            name: setup.site_name.to_owned(),
-            description: None,
-            icon: None,
-            banner: None,
-            enable_downvotes: true,
-            open_registration: true,
-            enable_nsfw: true,
-            auth: login_response.jwt,
-          };
-          create_site.perform(context, websocket_id).await?;
-          info!("Site {} created", setup.site_name);
-          Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??)
-        } else {
-          None
-        }
-      }
-    };
-
-    let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
-
-    // Make sure the site creator is the top admin
-    if let Some(site_view) = site_view.to_owned() {
-      let site_creator_id = site_view.creator.id;
-      // TODO investigate why this is sometimes coming back null
-      // Maybe user_.admin isn't being set to true?
-      if let Some(creator_index) = admins.iter().position(|r| r.person.id == site_creator_id) {
-        let creator_person = admins.remove(creator_index);
-        admins.insert(0, creator_person);
-      }
-    }
-
-    let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
-
-    let online = context
-      .chat_server()
-      .send(GetUsersOnline)
-      .await
-      .unwrap_or(1);
-
-    let my_user = get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await?;
-    let federated_instances = build_federated_instances(context.pool()).await?;
-
-    Ok(GetSiteResponse {
-      site_view,
-      admins,
-      banned,
-      online,
-      version: version::VERSION.to_string(),
-      my_user,
-      federated_instances,
-    })
-  }
-}
-
 #[async_trait::async_trait(?Send)]
 impl Perform for Search {
   type Response = SearchResponse;
diff --git a/crates/api/src/websocket.rs b/crates/api/src/websocket.rs
index ae5ba894..683b4c4d 100644
--- a/crates/api/src/websocket.rs
+++ b/crates/api/src/websocket.rs
@@ -1,6 +1,6 @@
-use crate::{get_local_user_view_from_jwt, Perform};
+use crate::Perform;
 use actix_web::web::Data;
-use lemmy_api_structs::websocket::*;
+use lemmy_api_common::{get_local_user_view_from_jwt, websocket::*};
 use lemmy_utils::{ConnectionId, LemmyError};
 use lemmy_websocket::{
   messages::{JoinCommunityRoom, JoinModRoom, JoinPostRoom, JoinUserRoom},
diff --git a/crates/api_structs/Cargo.toml b/crates/api_common/Cargo.toml
similarity index 92%
rename from crates/api_structs/Cargo.toml
rename to crates/api_common/Cargo.toml
index 242383e6..f870a266 100644
--- a/crates/api_structs/Cargo.toml
+++ b/crates/api_common/Cargo.toml
@@ -1,10 +1,10 @@
 [package]
-name = "lemmy_api_structs"
+name = "lemmy_api_common"
 version = "0.1.0"
 edition = "2018"
 
 [lib]
-name = "lemmy_api_structs"
+name = "lemmy_api_common"
 path = "src/lib.rs"
 doctest = false
 
diff --git a/crates/api_structs/src/comment.rs b/crates/api_common/src/comment.rs
similarity index 100%
rename from crates/api_structs/src/comment.rs
rename to crates/api_common/src/comment.rs
diff --git a/crates/api_structs/src/community.rs b/crates/api_common/src/community.rs
similarity index 100%
rename from crates/api_structs/src/community.rs
rename to crates/api_common/src/community.rs
diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs
new file mode 100644
index 00000000..2337f10b
--- /dev/null
+++ b/crates/api_common/src/lib.rs
@@ -0,0 +1,420 @@
+pub mod comment;
+pub mod community;
+pub mod person;
+pub mod post;
+pub mod site;
+pub mod websocket;
+
+use crate::site::FederatedInstances;
+use diesel::PgConnection;
+use lemmy_db_queries::{
+  source::{
+    community::{CommunityModerator_, Community_},
+    site::Site_,
+  },
+  Crud,
+  DbPool,
+};
+use lemmy_db_schema::{
+  source::{
+    comment::Comment,
+    community::{Community, CommunityModerator},
+    person::Person,
+    person_mention::{PersonMention, PersonMentionForm},
+    post::Post,
+    site::Site,
+  },
+  CommunityId,
+  LocalUserId,
+  PersonId,
+  PostId,
+};
+use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
+use lemmy_db_views_actor::{
+  community_person_ban_view::CommunityPersonBanView,
+  community_view::CommunityView,
+};
+use lemmy_utils::{
+  claims::Claims,
+  email::send_email,
+  settings::structs::Settings,
+  utils::MentionData,
+  ApiError,
+  LemmyError,
+};
+use log::error;
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct WebFingerLink {
+  pub rel: Option<String>,
+  #[serde(rename(serialize = "type", deserialize = "type"))]
+  pub type_: Option<String>,
+  pub href: Option<Url>,
+  #[serde(skip_serializing_if = "Option::is_none")]
+  pub template: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct WebFingerResponse {
+  pub subject: String,
+  pub aliases: Vec<Url>,
+  pub links: Vec<WebFingerLink>,
+}
+
+pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
+where
+  F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
+  T: Send + 'static,
+{
+  let pool = pool.clone();
+  let res = actix_web::web::block(move || {
+    let conn = pool.get()?;
+    let res = (f)(&conn);
+    Ok(res) as Result<_, LemmyError>
+  })
+  .await?;
+
+  Ok(res)
+}
+
+pub async fn send_local_notifs(
+  mentions: Vec<MentionData>,
+  comment: Comment,
+  person: Person,
+  post: Post,
+  pool: &DbPool,
+  do_send_email: bool,
+) -> Result<Vec<LocalUserId>, LemmyError> {
+  let ids = blocking(pool, move |conn| {
+    do_send_local_notifs(conn, &mentions, &comment, &person, &post, do_send_email)
+  })
+  .await?;
+
+  Ok(ids)
+}
+
+fn do_send_local_notifs(
+  conn: &PgConnection,
+  mentions: &[MentionData],
+  comment: &Comment,
+  person: &Person,
+  post: &Post,
+  do_send_email: bool,
+) -> Vec<LocalUserId> {
+  let mut recipient_ids = Vec::new();
+
+  // Send the local mentions
+  for mention in mentions
+    .iter()
+    .filter(|m| m.is_local() && m.name.ne(&person.name))
+    .collect::<Vec<&MentionData>>()
+  {
+    if let Ok(mention_user_view) = LocalUserView::read_from_name(&conn, &mention.name) {
+      // TODO
+      // At some point, make it so you can't tag the parent creator either
+      // This can cause two notifications, one for reply and the other for mention
+      recipient_ids.push(mention_user_view.local_user.id);
+
+      let user_mention_form = PersonMentionForm {
+        recipient_id: mention_user_view.person.id,
+        comment_id: comment.id,
+        read: None,
+      };
+
+      // Allow this to fail softly, since comment edits might re-update or replace it
+      // Let the uniqueness handle this fail
+      PersonMention::create(&conn, &user_mention_form).ok();
+
+      // Send an email to those local users that have notifications on
+      if do_send_email {
+        send_email_to_user(
+          &mention_user_view,
+          "Mentioned by",
+          "Person Mention",
+          &comment.content,
+        )
+      }
+    }
+  }
+
+  // Send notifs to the parent commenter / poster
+  match comment.parent_id {
+    Some(parent_id) => {
+      if let Ok(parent_comment) = Comment::read(&conn, parent_id) {
+        // Don't send a notif to yourself
+        if parent_comment.creator_id != person.id {
+          // Get the parent commenter local_user
+          if let Ok(parent_user_view) = LocalUserView::read_person(&conn, parent_comment.creator_id)
+          {
+            recipient_ids.push(parent_user_view.local_user.id);
+
+            if do_send_email {
+              send_email_to_user(
+                &parent_user_view,
+                "Reply from",
+                "Comment Reply",
+                &comment.content,
+              )
+            }
+          }
+        }
+      }
+    }
+    // Its a post
+    None => {
+      if post.creator_id != person.id {
+        if let Ok(parent_user_view) = LocalUserView::read_person(&conn, post.creator_id) {
+          recipient_ids.push(parent_user_view.local_user.id);
+
+          if do_send_email {
+            send_email_to_user(
+              &parent_user_view,
+              "Reply from",
+              "Post Reply",
+              &comment.content,
+            )
+          }
+        }
+      }
+    }
+  };
+  recipient_ids
+}
+
+pub fn send_email_to_user(
+  local_user_view: &LocalUserView,
+  subject_text: &str,
+  body_text: &str,
+  comment_content: &str,
+) {
+  if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
+    return;
+  }
+
+  if let Some(user_email) = &local_user_view.local_user.email {
+    let subject = &format!(
+      "{} - {} {}",
+      subject_text,
+      Settings::get().hostname(),
+      local_user_view.person.name,
+    );
+    let html = &format!(
+      "<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
+      body_text,
+      local_user_view.person.name,
+      comment_content,
+      Settings::get().get_protocol_and_hostname()
+    );
+    match send_email(subject, &user_email, &local_user_view.person.name, html) {
+      Ok(_o) => _o,
+      Err(e) => error!("{}", e),
+    };
+  }
+}
+
+pub async fn is_mod_or_admin(
+  pool: &DbPool,
+  person_id: PersonId,
+  community_id: CommunityId,
+) -> Result<(), LemmyError> {
+  let is_mod_or_admin = blocking(pool, move |conn| {
+    CommunityView::is_mod_or_admin(conn, person_id, community_id)
+  })
+  .await?;
+  if !is_mod_or_admin {
+    return Err(ApiError::err("not_a_mod_or_admin").into());
+  }
+  Ok(())
+}
+
+pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
+  if !local_user_view.local_user.admin {
+    return Err(ApiError::err("not_an_admin").into());
+  }
+  Ok(())
+}
+
+pub async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
+  match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
+    Ok(post) => Ok(post),
+    Err(_e) => Err(ApiError::err("couldnt_find_post").into()),
+  }
+}
+
+pub async fn get_local_user_view_from_jwt(
+  jwt: &str,
+  pool: &DbPool,
+) -> Result<LocalUserView, LemmyError> {
+  let claims = match Claims::decode(&jwt) {
+    Ok(claims) => claims.claims,
+    Err(_e) => return Err(ApiError::err("not_logged_in").into()),
+  };
+  let local_user_id = LocalUserId(claims.sub);
+  let local_user_view =
+    blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
+  // Check for a site ban
+  if local_user_view.person.banned {
+    return Err(ApiError::err("site_ban").into());
+  }
+
+  check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
+
+  Ok(local_user_view)
+}
+
+/// Checks if user's token was issued before user's password reset.
+pub fn check_validator_time(
+  validator_time: &chrono::NaiveDateTime,
+  claims: &Claims,
+) -> Result<(), LemmyError> {
+  let user_validation_time = validator_time.timestamp();
+  if user_validation_time > claims.iat {
+    Err(ApiError::err("not_logged_in").into())
+  } else {
+    Ok(())
+  }
+}
+
+pub async fn get_local_user_view_from_jwt_opt(
+  jwt: &Option<String>,
+  pool: &DbPool,
+) -> Result<Option<LocalUserView>, LemmyError> {
+  match jwt {
+    Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)),
+    None => Ok(None),
+  }
+}
+
+pub async fn get_local_user_settings_view_from_jwt(
+  jwt: &str,
+  pool: &DbPool,
+) -> Result<LocalUserSettingsView, LemmyError> {
+  let claims = match Claims::decode(&jwt) {
+    Ok(claims) => claims.claims,
+    Err(_e) => return Err(ApiError::err("not_logged_in").into()),
+  };
+  let local_user_id = LocalUserId(claims.sub);
+  let local_user_view = blocking(pool, move |conn| {
+    LocalUserSettingsView::read(conn, local_user_id)
+  })
+  .await??;
+  // Check for a site ban
+  if local_user_view.person.banned {
+    return Err(ApiError::err("site_ban").into());
+  }
+
+  check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
+
+  Ok(local_user_view)
+}
+
+pub async fn get_local_user_settings_view_from_jwt_opt(
+  jwt: &Option<String>,
+  pool: &DbPool,
+) -> Result<Option<LocalUserSettingsView>, LemmyError> {
+  match jwt {
+    Some(jwt) => Ok(Some(
+      get_local_user_settings_view_from_jwt(jwt, pool).await?,
+    )),
+    None => Ok(None),
+  }
+}
+
+pub async fn check_community_ban(
+  person_id: PersonId,
+  community_id: CommunityId,
+  pool: &DbPool,
+) -> Result<(), LemmyError> {
+  let is_banned =
+    move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
+  if blocking(pool, is_banned).await? {
+    Err(ApiError::err("community_ban").into())
+  } else {
+    Ok(())
+  }
+}
+
+pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
+  if score == -1 {
+    let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
+    if !site.enable_downvotes {
+      return Err(ApiError::err("downvotes_disabled").into());
+    }
+  }
+  Ok(())
+}
+
+/// Returns a list of communities that the user moderates
+/// or if a community_id is supplied validates the user is a moderator
+/// of that community and returns the community id in a vec
+///
+/// * `person_id` - the person id of the moderator
+/// * `community_id` - optional community id to check for moderator privileges
+/// * `pool` - the diesel db pool
+pub async fn collect_moderated_communities(
+  person_id: PersonId,
+  community_id: Option<CommunityId>,
+  pool: &DbPool,
+) -> Result<Vec<CommunityId>, LemmyError> {
+  if let Some(community_id) = community_id {
+    // if the user provides a community_id, just check for mod/admin privileges
+    is_mod_or_admin(pool, person_id, community_id).await?;
+    Ok(vec![community_id])
+  } else {
+    let ids = blocking(pool, move |conn: &'_ _| {
+      CommunityModerator::get_person_moderated_communities(conn, person_id)
+    })
+    .await??;
+    Ok(ids)
+  }
+}
+
+pub async fn build_federated_instances(
+  pool: &DbPool,
+) -> Result<Option<FederatedInstances>, LemmyError> {
+  if Settings::get().federation().enabled {
+    let distinct_communities = blocking(pool, move |conn| {
+      Community::distinct_federated_communities(conn)
+    })
+    .await??;
+
+    let allowed = Settings::get().get_allowed_instances();
+    let blocked = Settings::get().get_blocked_instances();
+
+    let mut linked = distinct_communities
+      .iter()
+      .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
+      .collect::<Result<Vec<String>, LemmyError>>()?;
+
+    if let Some(allowed) = allowed.as_ref() {
+      linked.extend_from_slice(allowed);
+    }
+
+    if let Some(blocked) = blocked.as_ref() {
+      linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname()));
+    }
+
+    // Sort and remove dupes
+    linked.sort_unstable();
+    linked.dedup();
+
+    Ok(Some(FederatedInstances {
+      linked,
+      allowed,
+      blocked,
+    }))
+  } else {
+    Ok(None)
+  }
+}
+
+/// Checks the password length
+pub fn password_length_check(pass: &str) -> Result<(), LemmyError> {
+  if pass.len() > 60 {
+    Err(ApiError::err("invalid_password").into())
+  } else {
+    Ok(())
+  }
+}
diff --git a/crates/api_structs/src/person.rs b/crates/api_common/src/person.rs
similarity index 100%
rename from crates/api_structs/src/person.rs
rename to crates/api_common/src/person.rs
diff --git a/crates/api_structs/src/post.rs b/crates/api_common/src/post.rs
similarity index 100%
rename from crates/api_structs/src/post.rs
rename to crates/api_common/src/post.rs
diff --git a/crates/api_structs/src/site.rs b/crates/api_common/src/site.rs
similarity index 100%
rename from crates/api_structs/src/site.rs
rename to crates/api_common/src/site.rs
diff --git a/crates/api_structs/src/websocket.rs b/crates/api_common/src/websocket.rs
similarity index 100%
rename from crates/api_structs/src/websocket.rs
rename to crates/api_common/src/websocket.rs
diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml
new file mode 100644
index 00000000..f7fde4a6
--- /dev/null
+++ b/crates/api_crud/Cargo.toml
@@ -0,0 +1,45 @@
+[package]
+name = "lemmy_api_crud"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+lemmy_apub = { path = "../apub" }
+lemmy_utils = { path = "../utils" }
+lemmy_db_queries = { path = "../db_queries" }
+lemmy_db_schema = { path = "../db_schema" }
+lemmy_db_views = { path = "../db_views" }
+lemmy_db_views_moderator = { path = "../db_views_moderator" }
+lemmy_db_views_actor = { path = "../db_views_actor" }
+lemmy_api_common = { path = "../api_common" }
+lemmy_websocket = { path = "../websocket" }
+diesel = "1.4.5"
+bcrypt = "0.9.0"
+chrono = { version = "0.4.19", features = ["serde"] }
+serde_json = { version = "1.0.61", features = ["preserve_order"] }
+serde = { version = "1.0.123", features = ["derive"] }
+actix = "0.10.0"
+actix-web = { version = "3.3.2", default-features = false }
+actix-rt = { version = "1.1.1", default-features = false }
+awc = { version = "2.0.3", default-features = false }
+log = "0.4.14"
+rand = "0.8.3"
+strum = "0.20.0"
+strum_macros = "0.20.1"
+lazy_static = "1.4.0"
+url = { version = "2.2.1", features = ["serde"] }
+openssl = "0.10.32"
+http = "0.2.3"
+http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] }
+base64 = "0.13.0"
+tokio = "0.3.6"
+futures = "0.3.12"
+itertools = "0.10.0"
+uuid = { version = "0.8.2", features = ["serde", "v4"] }
+sha2 = "0.9.3"
+async-trait = "0.1.42"
+captcha = "0.0.8"
+anyhow = "1.0.38"
+thiserror = "1.0.23"
+background-jobs = "0.8.0"
+reqwest = { version = "0.10.10", features = ["json"] }
diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs
new file mode 100644
index 00000000..74ef27f1
--- /dev/null
+++ b/crates/api_crud/src/comment/create.rs
@@ -0,0 +1,170 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  comment::*,
+  get_local_user_view_from_jwt,
+  get_post,
+  send_local_notifs,
+};
+use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
+use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable};
+use lemmy_db_schema::source::comment::*;
+use lemmy_db_views::comment_view::CommentView;
+use lemmy_utils::{
+  utils::{remove_slurs, scrape_text_for_mentions},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for CreateComment {
+  type Response = CommentResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &CreateComment = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let content_slurs_removed = remove_slurs(&data.content.to_owned());
+
+    // Check for a community ban
+    let post_id = data.post_id;
+    let post = get_post(post_id, context.pool()).await?;
+
+    check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
+
+    // Check if post is locked, no new comments
+    if post.locked {
+      return Err(ApiError::err("locked").into());
+    }
+
+    // If there's a parent_id, check to make sure that comment is in that post
+    if let Some(parent_id) = data.parent_id {
+      // Make sure the parent comment exists
+      let parent =
+        match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? {
+          Ok(comment) => comment,
+          Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
+        };
+      if parent.post_id != post_id {
+        return Err(ApiError::err("couldnt_create_comment").into());
+      }
+    }
+
+    let comment_form = CommentForm {
+      content: content_slurs_removed,
+      parent_id: data.parent_id.to_owned(),
+      post_id: data.post_id,
+      creator_id: local_user_view.person.id,
+      removed: None,
+      deleted: None,
+      read: None,
+      published: None,
+      updated: None,
+      ap_id: None,
+      local: true,
+    };
+
+    // Create the comment
+    let comment_form2 = comment_form.clone();
+    let inserted_comment = match blocking(context.pool(), move |conn| {
+      Comment::create(&conn, &comment_form2)
+    })
+    .await?
+    {
+      Ok(comment) => comment,
+      Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
+    };
+
+    // Necessary to update the ap_id
+    let inserted_comment_id = inserted_comment.id;
+    let updated_comment: Comment =
+      match blocking(context.pool(), move |conn| -> Result<Comment, LemmyError> {
+        let apub_id =
+          generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?;
+        Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?)
+      })
+      .await?
+      {
+        Ok(comment) => comment,
+        Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
+      };
+
+    updated_comment
+      .send_create(&local_user_view.person, context)
+      .await?;
+
+    // Scan the comment for user mentions, add those rows
+    let post_id = post.id;
+    let mentions = scrape_text_for_mentions(&comment_form.content);
+    let recipient_ids = send_local_notifs(
+      mentions,
+      updated_comment.clone(),
+      local_user_view.person.clone(),
+      post,
+      context.pool(),
+      true,
+    )
+    .await?;
+
+    // You like your own comment by default
+    let like_form = CommentLikeForm {
+      comment_id: inserted_comment.id,
+      post_id,
+      person_id: local_user_view.person.id,
+      score: 1,
+    };
+
+    let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
+    if blocking(context.pool(), like).await?.is_err() {
+      return Err(ApiError::err("couldnt_like_comment").into());
+    }
+
+    updated_comment
+      .send_like(&local_user_view.person, context)
+      .await?;
+
+    let person_id = local_user_view.person.id;
+    let mut comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, inserted_comment.id, Some(person_id))
+    })
+    .await??;
+
+    // If its a comment to yourself, mark it as read
+    let comment_id = comment_view.comment.id;
+    if local_user_view.person.id == comment_view.get_recipient_id() {
+      match blocking(context.pool(), move |conn| {
+        Comment::update_read(conn, comment_id, true)
+      })
+      .await?
+      {
+        Ok(comment) => comment,
+        Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
+      };
+      comment_view.comment.read = true;
+    }
+
+    let mut res = CommentResponse {
+      comment_view,
+      recipient_ids,
+      form_id: data.form_id.to_owned(),
+    };
+
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::CreateComment,
+      comment: res.clone(),
+      websocket_id,
+    });
+
+    res.recipient_ids = Vec::new(); // Necessary to avoid doubles
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs
new file mode 100644
index 00000000..1980106b
--- /dev/null
+++ b/crates/api_crud/src/comment/delete.rs
@@ -0,0 +1,210 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  comment::*,
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+  send_local_notifs,
+};
+use lemmy_apub::ApubObjectType;
+use lemmy_db_queries::{source::comment::Comment_, Crud};
+use lemmy_db_schema::source::{comment::*, moderator::*};
+use lemmy_db_views::comment_view::CommentView;
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for DeleteComment {
+  type Response = CommentResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &DeleteComment = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let comment_id = data.comment_id;
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, comment_id, None)
+    })
+    .await??;
+
+    check_community_ban(
+      local_user_view.person.id,
+      orig_comment.community.id,
+      context.pool(),
+    )
+    .await?;
+
+    // Verify that only the creator can delete
+    if local_user_view.person.id != orig_comment.creator.id {
+      return Err(ApiError::err("no_comment_edit_allowed").into());
+    }
+
+    // Do the delete
+    let deleted = data.deleted;
+    let updated_comment = match blocking(context.pool(), move |conn| {
+      Comment::update_deleted(conn, comment_id, deleted)
+    })
+    .await?
+    {
+      Ok(comment) => comment,
+      Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
+    };
+
+    // Send the apub message
+    if deleted {
+      updated_comment
+        .send_delete(&local_user_view.person, context)
+        .await?;
+    } else {
+      updated_comment
+        .send_undo_delete(&local_user_view.person, context)
+        .await?;
+    }
+
+    // Refetch it
+    let comment_id = data.comment_id;
+    let person_id = local_user_view.person.id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, comment_id, Some(person_id))
+    })
+    .await??;
+
+    // Build the recipients
+    let comment_view_2 = comment_view.clone();
+    let mentions = vec![];
+    let recipient_ids = send_local_notifs(
+      mentions,
+      updated_comment,
+      local_user_view.person.clone(),
+      comment_view_2.post,
+      context.pool(),
+      false,
+    )
+    .await?;
+
+    let res = CommentResponse {
+      comment_view,
+      recipient_ids,
+      form_id: None, // TODO a comment delete might clear forms?
+    };
+
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::DeleteComment,
+      comment: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for RemoveComment {
+  type Response = CommentResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &RemoveComment = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let comment_id = data.comment_id;
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, comment_id, None)
+    })
+    .await??;
+
+    check_community_ban(
+      local_user_view.person.id,
+      orig_comment.community.id,
+      context.pool(),
+    )
+    .await?;
+
+    // Verify that only a mod or admin can remove
+    is_mod_or_admin(
+      context.pool(),
+      local_user_view.person.id,
+      orig_comment.community.id,
+    )
+    .await?;
+
+    // Do the remove
+    let removed = data.removed;
+    let updated_comment = match blocking(context.pool(), move |conn| {
+      Comment::update_removed(conn, comment_id, removed)
+    })
+    .await?
+    {
+      Ok(comment) => comment,
+      Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
+    };
+
+    // Mod tables
+    let form = ModRemoveCommentForm {
+      mod_person_id: local_user_view.person.id,
+      comment_id: data.comment_id,
+      removed: Some(removed),
+      reason: data.reason.to_owned(),
+    };
+    blocking(context.pool(), move |conn| {
+      ModRemoveComment::create(conn, &form)
+    })
+    .await??;
+
+    // Send the apub message
+    if removed {
+      updated_comment
+        .send_remove(&local_user_view.person, context)
+        .await?;
+    } else {
+      updated_comment
+        .send_undo_remove(&local_user_view.person, context)
+        .await?;
+    }
+
+    // Refetch it
+    let comment_id = data.comment_id;
+    let person_id = local_user_view.person.id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, comment_id, Some(person_id))
+    })
+    .await??;
+
+    // Build the recipients
+    let comment_view_2 = comment_view.clone();
+
+    let mentions = vec![];
+    let recipient_ids = send_local_notifs(
+      mentions,
+      updated_comment,
+      local_user_view.person.clone(),
+      comment_view_2.post,
+      context.pool(),
+      false,
+    )
+    .await?;
+
+    let res = CommentResponse {
+      comment_view,
+      recipient_ids,
+      form_id: None, // TODO maybe this might clear other forms
+    };
+
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::RemoveComment,
+      comment: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/comment/mod.rs b/crates/api_crud/src/comment/mod.rs
new file mode 100644
index 00000000..71683237
--- /dev/null
+++ b/crates/api_crud/src/comment/mod.rs
@@ -0,0 +1,4 @@
+mod create;
+mod delete;
+mod read;
+mod update;
diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs
new file mode 100644
index 00000000..a7ad3aca
--- /dev/null
+++ b/crates/api_crud/src/comment/read.rs
@@ -0,0 +1,51 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, comment::*, get_local_user_view_from_jwt_opt};
+use lemmy_db_queries::{ListingType, SortType};
+use lemmy_db_views::comment_view::CommentQueryBuilder;
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+use std::str::FromStr;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetComments {
+  type Response = GetCommentsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetCommentsResponse, LemmyError> {
+    let data: &GetComments = &self;
+    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
+    let person_id = local_user_view.map(|u| u.person.id);
+
+    let type_ = ListingType::from_str(&data.type_)?;
+    let sort = SortType::from_str(&data.sort)?;
+
+    let community_id = data.community_id;
+    let community_name = data.community_name.to_owned();
+    let saved_only = data.saved_only;
+    let page = data.page;
+    let limit = data.limit;
+    let comments = blocking(context.pool(), move |conn| {
+      CommentQueryBuilder::create(conn)
+        .listing_type(type_)
+        .sort(&sort)
+        .saved_only(saved_only)
+        .community_id(community_id)
+        .community_name(community_name)
+        .my_person_id(person_id)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await?;
+    let comments = match comments {
+      Ok(comments) => comments,
+      Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
+    };
+
+    Ok(GetCommentsResponse { comments })
+  }
+}
diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs
new file mode 100644
index 00000000..46d99e93
--- /dev/null
+++ b/crates/api_crud/src/comment/update.rs
@@ -0,0 +1,103 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  comment::*,
+  get_local_user_view_from_jwt,
+  send_local_notifs,
+};
+use lemmy_apub::ApubObjectType;
+use lemmy_db_queries::source::comment::Comment_;
+use lemmy_db_schema::source::comment::*;
+use lemmy_db_views::comment_view::CommentView;
+use lemmy_utils::{
+  utils::{remove_slurs, scrape_text_for_mentions},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for EditComment {
+  type Response = CommentResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &EditComment = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let comment_id = data.comment_id;
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, comment_id, None)
+    })
+    .await??;
+
+    check_community_ban(
+      local_user_view.person.id,
+      orig_comment.community.id,
+      context.pool(),
+    )
+    .await?;
+
+    // Verify that only the creator can edit
+    if local_user_view.person.id != orig_comment.creator.id {
+      return Err(ApiError::err("no_comment_edit_allowed").into());
+    }
+
+    // Do the update
+    let content_slurs_removed = remove_slurs(&data.content.to_owned());
+    let comment_id = data.comment_id;
+    let updated_comment = match blocking(context.pool(), move |conn| {
+      Comment::update_content(conn, comment_id, &content_slurs_removed)
+    })
+    .await?
+    {
+      Ok(comment) => comment,
+      Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
+    };
+
+    // Send the apub update
+    updated_comment
+      .send_update(&local_user_view.person, context)
+      .await?;
+
+    // Do the mentions / recipients
+    let updated_comment_content = updated_comment.content.to_owned();
+    let mentions = scrape_text_for_mentions(&updated_comment_content);
+    let recipient_ids = send_local_notifs(
+      mentions,
+      updated_comment,
+      local_user_view.person.clone(),
+      orig_comment.post,
+      context.pool(),
+      false,
+    )
+    .await?;
+
+    let comment_id = data.comment_id;
+    let person_id = local_user_view.person.id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, comment_id, Some(person_id))
+    })
+    .await??;
+
+    let res = CommentResponse {
+      comment_view,
+      recipient_ids,
+      form_id: data.form_id.to_owned(),
+    };
+
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::EditComment,
+      comment: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs
new file mode 100644
index 00000000..104975b7
--- /dev/null
+++ b/crates/api_crud/src/community/create.rs
@@ -0,0 +1,134 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  community::{CommunityResponse, CreateCommunity},
+  get_local_user_view_from_jwt,
+};
+use lemmy_apub::{
+  generate_apub_endpoint,
+  generate_followers_url,
+  generate_inbox_url,
+  generate_shared_inbox_url,
+  EndpointType,
+};
+use lemmy_db_queries::{diesel_option_overwrite_to_url, ApubObject, Crud, Followable, Joinable};
+use lemmy_db_schema::source::community::{
+  Community,
+  CommunityFollower,
+  CommunityFollowerForm,
+  CommunityForm,
+  CommunityModerator,
+  CommunityModeratorForm,
+};
+use lemmy_db_views_actor::community_view::CommunityView;
+use lemmy_utils::{
+  apub::generate_actor_keypair,
+  utils::{check_slurs, check_slurs_opt, is_valid_community_name},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for CreateCommunity {
+  type Response = CommunityResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<CommunityResponse, LemmyError> {
+    let data: &CreateCommunity = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    check_slurs(&data.name)?;
+    check_slurs(&data.title)?;
+    check_slurs_opt(&data.description)?;
+
+    if !is_valid_community_name(&data.name) {
+      return Err(ApiError::err("invalid_community_name").into());
+    }
+
+    // Double check for duplicate community actor_ids
+    let community_actor_id = generate_apub_endpoint(EndpointType::Community, &data.name)?;
+    let actor_id_cloned = community_actor_id.to_owned();
+    let community_dupe = blocking(context.pool(), move |conn| {
+      Community::read_from_apub_id(conn, &actor_id_cloned)
+    })
+    .await?;
+    if community_dupe.is_ok() {
+      return Err(ApiError::err("community_already_exists").into());
+    }
+
+    // Check to make sure the icon and banners are urls
+    let icon = diesel_option_overwrite_to_url(&data.icon)?;
+    let banner = diesel_option_overwrite_to_url(&data.banner)?;
+
+    // When you create a community, make sure the user becomes a moderator and a follower
+    let keypair = generate_actor_keypair()?;
+
+    let community_form = CommunityForm {
+      name: data.name.to_owned(),
+      title: data.title.to_owned(),
+      description: data.description.to_owned(),
+      icon,
+      banner,
+      creator_id: local_user_view.person.id,
+      removed: None,
+      deleted: None,
+      nsfw: data.nsfw,
+      updated: None,
+      actor_id: Some(community_actor_id.to_owned()),
+      local: true,
+      private_key: Some(keypair.private_key),
+      public_key: Some(keypair.public_key),
+      last_refreshed_at: None,
+      published: None,
+      followers_url: Some(generate_followers_url(&community_actor_id)?),
+      inbox_url: Some(generate_inbox_url(&community_actor_id)?),
+      shared_inbox_url: Some(Some(generate_shared_inbox_url(&community_actor_id)?)),
+    };
+
+    let inserted_community = match blocking(context.pool(), move |conn| {
+      Community::create(conn, &community_form)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(ApiError::err("community_already_exists").into()),
+    };
+
+    // The community creator becomes a moderator
+    let community_moderator_form = CommunityModeratorForm {
+      community_id: inserted_community.id,
+      person_id: local_user_view.person.id,
+    };
+
+    let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
+    if blocking(context.pool(), join).await?.is_err() {
+      return Err(ApiError::err("community_moderator_already_exists").into());
+    }
+
+    // Follow your own community
+    let community_follower_form = CommunityFollowerForm {
+      community_id: inserted_community.id,
+      person_id: local_user_view.person.id,
+      pending: false,
+    };
+
+    let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
+    if blocking(context.pool(), follow).await?.is_err() {
+      return Err(ApiError::err("community_follower_already_exists").into());
+    }
+
+    let person_id = local_user_view.person.id;
+    let community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, inserted_community.id, Some(person_id))
+    })
+    .await??;
+
+    Ok(CommunityResponse { community_view })
+  }
+}
diff --git a/crates/api_crud/src/community/delete.rs b/crates/api_crud/src/community/delete.rs
new file mode 100644
index 00000000..e59ccd6b
--- /dev/null
+++ b/crates/api_crud/src/community/delete.rs
@@ -0,0 +1,134 @@
+use crate::{community::send_community_websocket, PerformCrud};
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt, is_admin};
+use lemmy_apub::CommunityType;
+use lemmy_db_queries::{source::community::Community_, Crud};
+use lemmy_db_schema::source::{
+  community::*,
+  moderator::{ModRemoveCommunity, ModRemoveCommunityForm},
+};
+use lemmy_db_views_actor::community_view::CommunityView;
+use lemmy_utils::{utils::naive_from_unix, ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for DeleteCommunity {
+  type Response = CommunityResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommunityResponse, LemmyError> {
+    let data: &DeleteCommunity = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    // Verify its the creator (only a creator can delete the community)
+    let community_id = data.community_id;
+    let read_community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
+    if read_community.creator_id != local_user_view.person.id {
+      return Err(ApiError::err("no_community_edit_allowed").into());
+    }
+
+    // Do the delete
+    let community_id = data.community_id;
+    let deleted = data.deleted;
+    let updated_community = match blocking(context.pool(), move |conn| {
+      Community::update_deleted(conn, community_id, deleted)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
+    };
+
+    // Send apub messages
+    if deleted {
+      updated_community.send_delete(context).await?;
+    } else {
+      updated_community.send_undo_delete(context).await?;
+    }
+
+    let community_id = data.community_id;
+    let person_id = local_user_view.person.id;
+    let community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, Some(person_id))
+    })
+    .await??;
+
+    let res = CommunityResponse { community_view };
+
+    send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for RemoveCommunity {
+  type Response = CommunityResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommunityResponse, LemmyError> {
+    let data: &RemoveCommunity = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    // 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 updated_community = match blocking(context.pool(), move |conn| {
+      Community::update_removed(conn, community_id, removed)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
+    };
+
+    // Mod tables
+    let expires = match data.expires {
+      Some(time) => Some(naive_from_unix(time)),
+      None => None,
+    };
+    let form = ModRemoveCommunityForm {
+      mod_person_id: local_user_view.person.id,
+      community_id: data.community_id,
+      removed: Some(removed),
+      reason: data.reason.to_owned(),
+      expires,
+    };
+    blocking(context.pool(), move |conn| {
+      ModRemoveCommunity::create(conn, &form)
+    })
+    .await??;
+
+    // Apub messages
+    if removed {
+      updated_community.send_remove(context).await?;
+    } else {
+      updated_community.send_undo_remove(context).await?;
+    }
+
+    let community_id = data.community_id;
+    let person_id = local_user_view.person.id;
+    let community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, Some(person_id))
+    })
+    .await??;
+
+    let res = CommunityResponse { community_view };
+
+    send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/community/mod.rs b/crates/api_crud/src/community/mod.rs
new file mode 100644
index 00000000..874aba9a
--- /dev/null
+++ b/crates/api_crud/src/community/mod.rs
@@ -0,0 +1,27 @@
+use actix_web::web::Data;
+use lemmy_api_common::community::CommunityResponse;
+use lemmy_utils::ConnectionId;
+use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
+
+mod create;
+mod delete;
+mod read;
+mod update;
+
+pub(in crate::community) fn send_community_websocket(
+  res: &CommunityResponse,
+  context: &Data<LemmyContext>,
+  websocket_id: Option<ConnectionId>,
+  op: UserOperation,
+) {
+  // Strip out the person id and subscribed when sending to others
+  let mut res_sent = res.clone();
+  res_sent.community_view.subscribed = false;
+
+  context.chat_server().do_send(SendCommunityRoomMessage {
+    op,
+    response: res_sent,
+    community_id: res.community_view.community.id,
+    websocket_id,
+  });
+}
diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs
new file mode 100644
index 00000000..af83a774
--- /dev/null
+++ b/crates/api_crud/src/community/read.rs
@@ -0,0 +1,121 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt_opt};
+use lemmy_db_queries::{source::community::Community_, ListingType, SortType};
+use lemmy_db_schema::source::community::*;
+use lemmy_db_views_actor::{
+  community_moderator_view::CommunityModeratorView,
+  community_view::{CommunityQueryBuilder, CommunityView},
+};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::GetCommunityUsersOnline, LemmyContext};
+use std::str::FromStr;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetCommunity {
+  type Response = GetCommunityResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetCommunityResponse, LemmyError> {
+    let data: &GetCommunity = &self;
+    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
+    let person_id = local_user_view.map(|u| u.person.id);
+
+    let community_id = match data.id {
+      Some(id) => id,
+      None => {
+        let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
+        match blocking(context.pool(), move |conn| {
+          Community::read_from_name(conn, &name)
+        })
+        .await?
+        {
+          Ok(community) => community,
+          Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
+        }
+        .id
+      }
+    };
+
+    let community_view = match blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, person_id)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
+    };
+
+    let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
+      CommunityModeratorView::for_community(conn, community_id)
+    })
+    .await?
+    {
+      Ok(moderators) => moderators,
+      Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
+    };
+
+    let online = context
+      .chat_server()
+      .send(GetCommunityUsersOnline { community_id })
+      .await
+      .unwrap_or(1);
+
+    let res = GetCommunityResponse {
+      community_view,
+      moderators,
+      online,
+    };
+
+    // Return the jwt
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for ListCommunities {
+  type Response = ListCommunitiesResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<ListCommunitiesResponse, LemmyError> {
+    let data: &ListCommunities = &self;
+    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
+
+    let person_id = match &local_user_view {
+      Some(uv) => Some(uv.person.id),
+      None => None,
+    };
+
+    // Don't show NSFW by default
+    let show_nsfw = match &local_user_view {
+      Some(uv) => uv.local_user.show_nsfw,
+      None => false,
+    };
+
+    let type_ = ListingType::from_str(&data.type_)?;
+    let sort = SortType::from_str(&data.sort)?;
+
+    let page = data.page;
+    let limit = data.limit;
+    let communities = blocking(context.pool(), move |conn| {
+      CommunityQueryBuilder::create(conn)
+        .listing_type(&type_)
+        .sort(&sort)
+        .show_nsfw(show_nsfw)
+        .my_person_id(person_id)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    // Return the jwt
+    Ok(ListCommunitiesResponse { communities })
+  }
+}
diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs
new file mode 100644
index 00000000..d7fa3061
--- /dev/null
+++ b/crates/api_crud/src/community/update.rs
@@ -0,0 +1,109 @@
+use crate::{community::send_community_websocket, PerformCrud};
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  community::{CommunityResponse, EditCommunity},
+  get_local_user_view_from_jwt,
+};
+use lemmy_db_queries::{diesel_option_overwrite_to_url, Crud};
+use lemmy_db_schema::{
+  naive_now,
+  source::community::{Community, CommunityForm},
+  PersonId,
+};
+use lemmy_db_views_actor::{
+  community_moderator_view::CommunityModeratorView,
+  community_view::CommunityView,
+};
+use lemmy_utils::{
+  utils::{check_slurs, check_slurs_opt},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::{LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for EditCommunity {
+  type Response = CommunityResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommunityResponse, LemmyError> {
+    let data: &EditCommunity = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    check_slurs(&data.title)?;
+    check_slurs_opt(&data.description)?;
+
+    // Verify its a mod (only mods can edit it)
+    let community_id = data.community_id;
+    let mods: Vec<PersonId> = blocking(context.pool(), move |conn| {
+      CommunityModeratorView::for_community(conn, community_id)
+        .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
+    })
+    .await??;
+    if !mods.contains(&local_user_view.person.id) {
+      return Err(ApiError::err("not_a_moderator").into());
+    }
+
+    let community_id = data.community_id;
+    let read_community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
+
+    let icon = diesel_option_overwrite_to_url(&data.icon)?;
+    let banner = diesel_option_overwrite_to_url(&data.banner)?;
+
+    let community_form = CommunityForm {
+      name: read_community.name,
+      title: data.title.to_owned(),
+      description: data.description.to_owned(),
+      icon,
+      banner,
+      creator_id: read_community.creator_id,
+      removed: Some(read_community.removed),
+      deleted: Some(read_community.deleted),
+      nsfw: data.nsfw,
+      updated: Some(naive_now()),
+      actor_id: Some(read_community.actor_id),
+      local: read_community.local,
+      private_key: read_community.private_key,
+      public_key: read_community.public_key,
+      last_refreshed_at: None,
+      published: None,
+      followers_url: None,
+      inbox_url: None,
+      shared_inbox_url: None,
+    };
+
+    let community_id = data.community_id;
+    match blocking(context.pool(), move |conn| {
+      Community::update(conn, community_id, &community_form)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
+    };
+
+    // TODO there needs to be some kind of an apub update
+    // process for communities and users
+
+    let community_id = data.community_id;
+    let person_id = local_user_view.person.id;
+    let community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, Some(person_id))
+    })
+    .await??;
+
+    let res = CommunityResponse { community_view };
+
+    send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs
new file mode 100644
index 00000000..77a900dd
--- /dev/null
+++ b/crates/api_crud/src/lib.rs
@@ -0,0 +1,22 @@
+use actix_web::web::Data;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+mod comment;
+mod community;
+mod post;
+mod private_message;
+pub mod routes;
+mod site;
+mod user;
+
+#[async_trait::async_trait(?Send)]
+pub trait PerformCrud {
+  type Response: serde::ser::Serialize + Send;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError>;
+}
diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs
new file mode 100644
index 00000000..231a891d
--- /dev/null
+++ b/crates/api_crud/src/post/create.rs
@@ -0,0 +1,130 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*};
+use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
+use lemmy_db_queries::{source::post::Post_, Crud, Likeable};
+use lemmy_db_schema::source::post::*;
+use lemmy_db_views::post_view::PostView;
+use lemmy_utils::{
+  request::fetch_iframely_and_pictrs_data,
+  utils::{check_slurs, check_slurs_opt, is_valid_post_title},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for CreatePost {
+  type Response = PostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &CreatePost = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.body)?;
+
+    if !is_valid_post_title(&data.name) {
+      return Err(ApiError::err("invalid_post_title").into());
+    }
+
+    check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?;
+
+    // Fetch Iframely and pictrs cached image
+    let data_url = data.url.as_ref();
+    let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
+      fetch_iframely_and_pictrs_data(context.client(), data_url).await;
+
+    let post_form = PostForm {
+      name: data.name.trim().to_owned(),
+      url: data_url.map(|u| u.to_owned().into()),
+      body: data.body.to_owned(),
+      community_id: data.community_id,
+      creator_id: local_user_view.person.id,
+      removed: None,
+      deleted: None,
+      nsfw: data.nsfw,
+      locked: None,
+      stickied: None,
+      updated: None,
+      embed_title: iframely_title,
+      embed_description: iframely_description,
+      embed_html: iframely_html,
+      thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
+      ap_id: None,
+      local: true,
+      published: None,
+    };
+
+    let inserted_post =
+      match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
+        Ok(post) => post,
+        Err(e) => {
+          let err_type = if e.to_string() == "value too long for type character varying(200)" {
+            "post_title_too_long"
+          } else {
+            "couldnt_create_post"
+          };
+
+          return Err(ApiError::err(err_type).into());
+        }
+      };
+
+    let inserted_post_id = inserted_post.id;
+    let updated_post = match blocking(context.pool(), move |conn| -> Result<Post, LemmyError> {
+      let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?;
+      Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
+    })
+    .await?
+    {
+      Ok(post) => post,
+      Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
+    };
+
+    updated_post
+      .send_create(&local_user_view.person, context)
+      .await?;
+
+    // They like their own post by default
+    let like_form = PostLikeForm {
+      post_id: inserted_post.id,
+      person_id: local_user_view.person.id,
+      score: 1,
+    };
+
+    let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
+    if blocking(context.pool(), like).await?.is_err() {
+      return Err(ApiError::err("couldnt_like_post").into());
+    }
+
+    updated_post
+      .send_like(&local_user_view.person, context)
+      .await?;
+
+    // Refetch the view
+    let inserted_post_id = inserted_post.id;
+    let post_view = match blocking(context.pool(), move |conn| {
+      PostView::read(conn, inserted_post_id, Some(local_user_view.person.id))
+    })
+    .await?
+    {
+      Ok(post) => post,
+      Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
+    };
+
+    let res = PostResponse { post_view };
+
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::CreatePost,
+      post: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs
new file mode 100644
index 00000000..ca25f3b1
--- /dev/null
+++ b/crates/api_crud/src/post/delete.rs
@@ -0,0 +1,161 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  check_community_ban,
+  get_local_user_view_from_jwt,
+  is_mod_or_admin,
+  post::*,
+};
+use lemmy_apub::ApubObjectType;
+use lemmy_db_queries::{source::post::Post_, Crud};
+use lemmy_db_schema::source::{moderator::*, post::*};
+use lemmy_db_views::post_view::PostView;
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for DeletePost {
+  type Response = PostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &DeletePost = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let post_id = data.post_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+
+    check_community_ban(
+      local_user_view.person.id,
+      orig_post.community_id,
+      context.pool(),
+    )
+    .await?;
+
+    // Verify that only the creator can delete
+    if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
+      return Err(ApiError::err("no_post_edit_allowed").into());
+    }
+
+    // Update the post
+    let post_id = data.post_id;
+    let deleted = data.deleted;
+    let updated_post = blocking(context.pool(), move |conn| {
+      Post::update_deleted(conn, post_id, deleted)
+    })
+    .await??;
+
+    // apub updates
+    if deleted {
+      updated_post
+        .send_delete(&local_user_view.person, context)
+        .await?;
+    } else {
+      updated_post
+        .send_undo_delete(&local_user_view.person, context)
+        .await?;
+    }
+
+    // Refetch the post
+    let post_id = data.post_id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, post_id, Some(local_user_view.person.id))
+    })
+    .await??;
+
+    let res = PostResponse { post_view };
+
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::DeletePost,
+      post: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for RemovePost {
+  type Response = PostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &RemovePost = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let post_id = data.post_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+
+    check_community_ban(
+      local_user_view.person.id,
+      orig_post.community_id,
+      context.pool(),
+    )
+    .await?;
+
+    // Verify that only the mods can remove
+    is_mod_or_admin(
+      context.pool(),
+      local_user_view.person.id,
+      orig_post.community_id,
+    )
+    .await?;
+
+    // Update the post
+    let post_id = data.post_id;
+    let removed = data.removed;
+    let updated_post = blocking(context.pool(), move |conn| {
+      Post::update_removed(conn, post_id, removed)
+    })
+    .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.to_owned(),
+    };
+    blocking(context.pool(), move |conn| {
+      ModRemovePost::create(conn, &form)
+    })
+    .await??;
+
+    // apub updates
+    if removed {
+      updated_post
+        .send_remove(&local_user_view.person, context)
+        .await?;
+    } else {
+      updated_post
+        .send_undo_remove(&local_user_view.person, context)
+        .await?;
+    }
+
+    // Refetch the post
+    let post_id = data.post_id;
+    let person_id = local_user_view.person.id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, post_id, Some(person_id))
+    })
+    .await??;
+
+    let res = PostResponse { post_view };
+
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::RemovePost,
+      post: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/post/mod.rs b/crates/api_crud/src/post/mod.rs
new file mode 100644
index 00000000..71683237
--- /dev/null
+++ b/crates/api_crud/src/post/mod.rs
@@ -0,0 +1,4 @@
+mod create;
+mod delete;
+mod read;
+mod update;
diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs
new file mode 100644
index 00000000..1b173418
--- /dev/null
+++ b/crates/api_crud/src/post/read.rs
@@ -0,0 +1,135 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, post::*};
+use lemmy_db_queries::{ListingType, SortType};
+use lemmy_db_views::{
+  comment_view::CommentQueryBuilder,
+  post_view::{PostQueryBuilder, PostView},
+};
+use lemmy_db_views_actor::{
+  community_moderator_view::CommunityModeratorView,
+  community_view::CommunityView,
+};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::GetPostUsersOnline, LemmyContext};
+use std::str::FromStr;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetPost {
+  type Response = GetPostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetPostResponse, LemmyError> {
+    let data: &GetPost = &self;
+    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
+    let person_id = local_user_view.map(|u| u.person.id);
+
+    let id = data.id;
+    let post_view = match blocking(context.pool(), move |conn| {
+      PostView::read(conn, id, person_id)
+    })
+    .await?
+    {
+      Ok(post) => post,
+      Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
+    };
+
+    let id = data.id;
+    let comments = blocking(context.pool(), move |conn| {
+      CommentQueryBuilder::create(conn)
+        .my_person_id(person_id)
+        .post_id(id)
+        .limit(9999)
+        .list()
+    })
+    .await??;
+
+    let community_id = post_view.community.id;
+    let moderators = blocking(context.pool(), move |conn| {
+      CommunityModeratorView::for_community(conn, community_id)
+    })
+    .await??;
+
+    // Necessary for the sidebar
+    let community_view = match blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, person_id)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
+    };
+
+    let online = context
+      .chat_server()
+      .send(GetPostUsersOnline { post_id: data.id })
+      .await
+      .unwrap_or(1);
+
+    // Return the jwt
+    Ok(GetPostResponse {
+      post_view,
+      community_view,
+      comments,
+      moderators,
+      online,
+    })
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetPosts {
+  type Response = GetPostsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetPostsResponse, LemmyError> {
+    let data: &GetPosts = &self;
+    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
+
+    let person_id = match &local_user_view {
+      Some(uv) => Some(uv.person.id),
+      None => None,
+    };
+
+    let show_nsfw = match &local_user_view {
+      Some(uv) => uv.local_user.show_nsfw,
+      None => false,
+    };
+
+    let type_ = ListingType::from_str(&data.type_)?;
+    let sort = SortType::from_str(&data.sort)?;
+
+    let page = data.page;
+    let limit = data.limit;
+    let community_id = data.community_id;
+    let community_name = data.community_name.to_owned();
+    let saved_only = data.saved_only;
+
+    let posts = match blocking(context.pool(), move |conn| {
+      PostQueryBuilder::create(conn)
+        .listing_type(&type_)
+        .sort(&sort)
+        .show_nsfw(show_nsfw)
+        .community_id(community_id)
+        .community_name(community_name)
+        .saved_only(saved_only)
+        .my_person_id(person_id)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await?
+    {
+      Ok(posts) => posts,
+      Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
+    };
+
+    Ok(GetPostsResponse { posts })
+  }
+}
diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs
new file mode 100644
index 00000000..c03bddf8
--- /dev/null
+++ b/crates/api_crud/src/post/update.rs
@@ -0,0 +1,116 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*};
+use lemmy_apub::ApubObjectType;
+use lemmy_db_queries::{source::post::Post_, Crud};
+use lemmy_db_schema::{naive_now, source::post::*};
+use lemmy_db_views::post_view::PostView;
+use lemmy_utils::{
+  request::fetch_iframely_and_pictrs_data,
+  utils::{check_slurs, check_slurs_opt, is_valid_post_title},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for EditPost {
+  type Response = PostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &EditPost = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.body)?;
+
+    if !is_valid_post_title(&data.name) {
+      return Err(ApiError::err("invalid_post_title").into());
+    }
+
+    let post_id = data.post_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+
+    check_community_ban(
+      local_user_view.person.id,
+      orig_post.community_id,
+      context.pool(),
+    )
+    .await?;
+
+    // Verify that only the creator can edit
+    if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
+      return Err(ApiError::err("no_post_edit_allowed").into());
+    }
+
+    // Fetch Iframely and Pictrs cached image
+    let data_url = data.url.as_ref();
+    let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
+      fetch_iframely_and_pictrs_data(context.client(), data_url).await;
+
+    let post_form = PostForm {
+      name: data.name.trim().to_owned(),
+      url: data_url.map(|u| u.to_owned().into()),
+      body: data.body.to_owned(),
+      nsfw: data.nsfw,
+      creator_id: orig_post.creator_id.to_owned(),
+      community_id: orig_post.community_id,
+      removed: Some(orig_post.removed),
+      deleted: Some(orig_post.deleted),
+      locked: Some(orig_post.locked),
+      stickied: Some(orig_post.stickied),
+      updated: Some(naive_now()),
+      embed_title: iframely_title,
+      embed_description: iframely_description,
+      embed_html: iframely_html,
+      thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
+      ap_id: Some(orig_post.ap_id),
+      local: orig_post.local,
+      published: None,
+    };
+
+    let post_id = data.post_id;
+    let res = blocking(context.pool(), move |conn| {
+      Post::update(conn, post_id, &post_form)
+    })
+    .await?;
+    let updated_post: Post = match res {
+      Ok(post) => post,
+      Err(e) => {
+        let err_type = if e.to_string() == "value too long for type character varying(200)" {
+          "post_title_too_long"
+        } else {
+          "couldnt_update_post"
+        };
+
+        return Err(ApiError::err(err_type).into());
+      }
+    };
+
+    // Send apub update
+    updated_post
+      .send_update(&local_user_view.person, context)
+      .await?;
+
+    let post_id = data.post_id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, post_id, Some(local_user_view.person.id))
+    })
+    .await??;
+
+    let res = PostResponse { post_view };
+
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::EditPost,
+      post: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs
new file mode 100644
index 00000000..9654d22f
--- /dev/null
+++ b/crates/api_crud/src/private_message/create.rs
@@ -0,0 +1,112 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{CreatePrivateMessage, PrivateMessageResponse},
+  send_email_to_user,
+};
+use lemmy_apub::{generate_apub_endpoint, ApubObjectType, EndpointType};
+use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud};
+use lemmy_db_schema::source::private_message::{PrivateMessage, PrivateMessageForm};
+use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
+use lemmy_utils::{utils::remove_slurs, ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for CreatePrivateMessage {
+  type Response = PrivateMessageResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PrivateMessageResponse, LemmyError> {
+    let data: &CreatePrivateMessage = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    let content_slurs_removed = remove_slurs(&data.content.to_owned());
+
+    let private_message_form = PrivateMessageForm {
+      content: content_slurs_removed.to_owned(),
+      creator_id: local_user_view.person.id,
+      recipient_id: data.recipient_id,
+      deleted: None,
+      read: None,
+      updated: None,
+      ap_id: None,
+      local: true,
+      published: None,
+    };
+
+    let inserted_private_message = match blocking(context.pool(), move |conn| {
+      PrivateMessage::create(conn, &private_message_form)
+    })
+    .await?
+    {
+      Ok(private_message) => private_message,
+      Err(_e) => {
+        return Err(ApiError::err("couldnt_create_private_message").into());
+      }
+    };
+
+    let inserted_private_message_id = inserted_private_message.id;
+    let updated_private_message = match blocking(
+      context.pool(),
+      move |conn| -> Result<PrivateMessage, LemmyError> {
+        let apub_id = generate_apub_endpoint(
+          EndpointType::PrivateMessage,
+          &inserted_private_message_id.to_string(),
+        )?;
+        Ok(PrivateMessage::update_ap_id(
+          &conn,
+          inserted_private_message_id,
+          apub_id,
+        )?)
+      },
+    )
+    .await?
+    {
+      Ok(private_message) => private_message,
+      Err(_e) => return Err(ApiError::err("couldnt_create_private_message").into()),
+    };
+
+    updated_private_message
+      .send_create(&local_user_view.person, context)
+      .await?;
+
+    let private_message_view = blocking(context.pool(), move |conn| {
+      PrivateMessageView::read(conn, inserted_private_message.id)
+    })
+    .await??;
+
+    let res = PrivateMessageResponse {
+      private_message_view,
+    };
+
+    // Send notifications to the local recipient, if one exists
+    let recipient_id = data.recipient_id;
+    if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
+      LocalUserView::read_person(conn, recipient_id)
+    })
+    .await?
+    {
+      send_email_to_user(
+        &local_recipient,
+        "Private Message from",
+        "Private Message",
+        &content_slurs_removed,
+      );
+
+      let local_recipient_id = local_recipient.local_user.id;
+      context.chat_server().do_send(SendUserRoomMessage {
+        op: UserOperation::CreatePrivateMessage,
+        response: res.clone(),
+        local_recipient_id,
+        websocket_id,
+      });
+    }
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs
new file mode 100644
index 00000000..120f57aa
--- /dev/null
+++ b/crates/api_crud/src/private_message/delete.rs
@@ -0,0 +1,88 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{DeletePrivateMessage, PrivateMessageResponse},
+};
+use lemmy_apub::ApubObjectType;
+use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud};
+use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for DeletePrivateMessage {
+  type Response = PrivateMessageResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PrivateMessageResponse, LemmyError> {
+    let data: &DeletePrivateMessage = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    // Checking permissions
+    let private_message_id = data.private_message_id;
+    let orig_private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::read(conn, private_message_id)
+    })
+    .await??;
+    if local_user_view.person.id != orig_private_message.creator_id {
+      return Err(ApiError::err("no_private_message_edit_allowed").into());
+    }
+
+    // Doing the update
+    let private_message_id = data.private_message_id;
+    let deleted = data.deleted;
+    let updated_private_message = match blocking(context.pool(), move |conn| {
+      PrivateMessage::update_deleted(conn, private_message_id, deleted)
+    })
+    .await?
+    {
+      Ok(private_message) => private_message,
+      Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
+    };
+
+    // Send the apub update
+    if data.deleted {
+      updated_private_message
+        .send_delete(&local_user_view.person, context)
+        .await?;
+    } else {
+      updated_private_message
+        .send_undo_delete(&local_user_view.person, context)
+        .await?;
+    }
+
+    let private_message_id = data.private_message_id;
+    let private_message_view = blocking(context.pool(), move |conn| {
+      PrivateMessageView::read(conn, private_message_id)
+    })
+    .await??;
+
+    let res = PrivateMessageResponse {
+      private_message_view,
+    };
+
+    // Send notifications to the local recipient, if one exists
+    let recipient_id = orig_private_message.recipient_id;
+    if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
+      LocalUserView::read_person(conn, recipient_id)
+    })
+    .await?
+    {
+      let local_recipient_id = local_recipient.local_user.id;
+      context.chat_server().do_send(SendUserRoomMessage {
+        op: UserOperation::DeletePrivateMessage,
+        response: res.clone(),
+        local_recipient_id,
+        websocket_id,
+      });
+    }
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/private_message/mod.rs b/crates/api_crud/src/private_message/mod.rs
new file mode 100644
index 00000000..71683237
--- /dev/null
+++ b/crates/api_crud/src/private_message/mod.rs
@@ -0,0 +1,4 @@
+mod create;
+mod delete;
+mod read;
+mod update;
diff --git a/crates/api_crud/src/private_message/read.rs b/crates/api_crud/src/private_message/read.rs
new file mode 100644
index 00000000..79bc85f4
--- /dev/null
+++ b/crates/api_crud/src/private_message/read.rs
@@ -0,0 +1,41 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{GetPrivateMessages, PrivateMessagesResponse},
+};
+use lemmy_db_views::private_message_view::PrivateMessageQueryBuilder;
+use lemmy_utils::{ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetPrivateMessages {
+  type Response = PrivateMessagesResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<PrivateMessagesResponse, LemmyError> {
+    let data: &GetPrivateMessages = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+    let person_id = local_user_view.person.id;
+
+    let page = data.page;
+    let limit = data.limit;
+    let unread_only = data.unread_only;
+    let messages = blocking(context.pool(), move |conn| {
+      PrivateMessageQueryBuilder::create(&conn, person_id)
+        .page(page)
+        .limit(limit)
+        .unread_only(unread_only)
+        .list()
+    })
+    .await??;
+
+    Ok(PrivateMessagesResponse {
+      private_messages: messages,
+    })
+  }
+}
diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs
new file mode 100644
index 00000000..b6baa036
--- /dev/null
+++ b/crates/api_crud/src/private_message/update.rs
@@ -0,0 +1,82 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  person::{EditPrivateMessage, PrivateMessageResponse},
+};
+use lemmy_apub::ApubObjectType;
+use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud};
+use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
+use lemmy_utils::{utils::remove_slurs, ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for EditPrivateMessage {
+  type Response = PrivateMessageResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PrivateMessageResponse, LemmyError> {
+    let data: &EditPrivateMessage = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    // Checking permissions
+    let private_message_id = data.private_message_id;
+    let orig_private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::read(conn, private_message_id)
+    })
+    .await??;
+    if local_user_view.person.id != orig_private_message.creator_id {
+      return Err(ApiError::err("no_private_message_edit_allowed").into());
+    }
+
+    // Doing the update
+    let content_slurs_removed = remove_slurs(&data.content);
+    let private_message_id = data.private_message_id;
+    let updated_private_message = match blocking(context.pool(), move |conn| {
+      PrivateMessage::update_content(conn, private_message_id, &content_slurs_removed)
+    })
+    .await?
+    {
+      Ok(private_message) => private_message,
+      Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
+    };
+
+    // Send the apub update
+    updated_private_message
+      .send_update(&local_user_view.person, context)
+      .await?;
+
+    let private_message_id = data.private_message_id;
+    let private_message_view = blocking(context.pool(), move |conn| {
+      PrivateMessageView::read(conn, private_message_id)
+    })
+    .await??;
+
+    let res = PrivateMessageResponse {
+      private_message_view,
+    };
+
+    // Send notifications to the local recipient, if one exists
+    let recipient_id = orig_private_message.recipient_id;
+    if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
+      LocalUserView::read_person(conn, recipient_id)
+    })
+    .await?
+    {
+      let local_recipient_id = local_recipient.local_user.id;
+      context.chat_server().do_send(SendUserRoomMessage {
+        op: UserOperation::EditPrivateMessage,
+        response: res.clone(),
+        local_recipient_id,
+        websocket_id,
+      });
+    }
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/routes.rs b/crates/api_crud/src/routes.rs
new file mode 100644
index 00000000..774268b6
--- /dev/null
+++ b/crates/api_crud/src/routes.rs
@@ -0,0 +1,133 @@
+use crate::PerformCrud;
+use actix_web::{error::ErrorBadRequest, *};
+use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*};
+use lemmy_utils::rate_limit::RateLimit;
+use lemmy_websocket::LemmyContext;
+use serde::Deserialize;
+
+pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
+  cfg
+    .service(
+      web::scope("/api/v2")
+        // Site
+        .service(
+          web::scope("/site")
+            .wrap(rate_limit.message())
+            .route("", web::get().to(route_get::<GetSite>))
+            // Admin Actions
+            .route("", web::post().to(route_post::<CreateSite>))
+            .route("", web::put().to(route_post::<EditSite>)),
+        )
+        // Community
+        .service(
+          web::resource("/community")
+            .guard(guard::Post())
+            .wrap(rate_limit.register())
+            .route(web::post().to(route_post::<CreateCommunity>)),
+        )
+        .service(
+          web::scope("/community")
+            .wrap(rate_limit.message())
+            .route("", web::get().to(route_get::<GetCommunity>))
+            .route("", web::put().to(route_post::<EditCommunity>))
+            .route("/list", web::get().to(route_get::<ListCommunities>))
+            .route("/delete", web::post().to(route_post::<DeleteCommunity>))
+            // Mod Actions
+            .route("/remove", web::post().to(route_post::<RemoveCommunity>)),
+        )
+        // Post
+        .service(
+          // Handle POST to /post separately to add the post() rate limitter
+          web::resource("/post")
+            .guard(guard::Post())
+            .wrap(rate_limit.post())
+            .route(web::post().to(route_post::<CreatePost>)),
+        )
+        .service(
+          web::scope("/post")
+            .wrap(rate_limit.message())
+            .route("", web::get().to(route_get::<GetPost>))
+            .route("", web::put().to(route_post::<EditPost>))
+            .route("/delete", web::post().to(route_post::<DeletePost>))
+            .route("/remove", web::post().to(route_post::<RemovePost>))
+            .route("/list", web::get().to(route_get::<GetPosts>)),
+        )
+        // Comment
+        .service(
+          web::scope("/comment")
+            .wrap(rate_limit.message())
+            .route("", web::post().to(route_post::<CreateComment>))
+            .route("", web::put().to(route_post::<EditComment>))
+            .route("/delete", web::post().to(route_post::<DeleteComment>))
+            .route("/remove", web::post().to(route_post::<RemoveComment>))
+            .route("/list", web::get().to(route_get::<GetComments>)),
+        ),
+    )
+    // Private Message
+    .service(
+      web::scope("/private_message")
+        .wrap(rate_limit.message())
+        .route("/list", web::get().to(route_get::<GetPrivateMessages>))
+        .route("", web::post().to(route_post::<CreatePrivateMessage>))
+        .route("", web::put().to(route_post::<EditPrivateMessage>))
+        .route(
+          "/delete",
+          web::post().to(route_post::<DeletePrivateMessage>),
+        ),
+    )
+    // User
+    .service(
+      // Account action, I don't like that it's in /user maybe /accounts
+      // Handle /user/register separately to add the register() rate limitter
+      web::resource("/user/register")
+        .guard(guard::Post())
+        .wrap(rate_limit.register())
+        .route(web::post().to(route_post::<Register>)),
+    )
+    // User actions
+    .service(
+      web::scope("/user")
+        .wrap(rate_limit.message())
+        .route("", web::get().to(route_get::<GetPersonDetails>))
+        .route(
+          "/delete_account",
+          web::post().to(route_post::<DeleteAccount>),
+        ),
+    );
+}
+
+async fn perform<Request>(
+  data: Request,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, Error>
+where
+  Request: PerformCrud,
+  Request: Send + 'static,
+{
+  let res = data
+    .perform(&context, None)
+    .await
+    .map(|json| HttpResponse::Ok().json(json))
+    .map_err(ErrorBadRequest)?;
+  Ok(res)
+}
+
+async fn route_get<'a, Data>(
+  data: web::Query<Data>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, Error>
+where
+  Data: Deserialize<'a> + Send + 'static + PerformCrud,
+{
+  perform::<Data>(data.0, context).await
+}
+
+async fn route_post<'a, Data>(
+  data: web::Json<Data>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, Error>
+where
+  Data: Deserialize<'a> + Send + 'static + PerformCrud,
+{
+  perform::<Data>(data.0, context).await
+}
diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs
new file mode 100644
index 00000000..855e41c1
--- /dev/null
+++ b/crates/api_crud/src/site/create.rs
@@ -0,0 +1,60 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, get_local_user_view_from_jwt, is_admin, site::*};
+use lemmy_db_queries::{source::site::Site_, Crud};
+use lemmy_db_schema::source::site::{Site, *};
+use lemmy_db_views::site_view::SiteView;
+use lemmy_utils::{
+  utils::{check_slurs, check_slurs_opt},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for CreateSite {
+  type Response = SiteResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<SiteResponse, LemmyError> {
+    let data: &CreateSite = &self;
+
+    let read_site = move |conn: &'_ _| Site::read_simple(conn);
+    if blocking(context.pool(), read_site).await?.is_ok() {
+      return Err(ApiError::err("site_already_exists").into());
+    };
+
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.description)?;
+
+    // Make sure user is an admin
+    is_admin(&local_user_view)?;
+
+    let site_form = SiteForm {
+      name: data.name.to_owned(),
+      description: data.description.to_owned(),
+      icon: Some(data.icon.to_owned().map(|url| url.into())),
+      banner: Some(data.banner.to_owned().map(|url| url.into())),
+      creator_id: local_user_view.person.id,
+      enable_downvotes: data.enable_downvotes,
+      open_registration: data.open_registration,
+      enable_nsfw: data.enable_nsfw,
+      updated: None,
+    };
+
+    let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
+    if blocking(context.pool(), create_site).await?.is_err() {
+      return Err(ApiError::err("site_already_exists").into());
+    }
+
+    let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
+
+    Ok(SiteResponse { site_view })
+  }
+}
diff --git a/crates/api_crud/src/site/mod.rs b/crates/api_crud/src/site/mod.rs
new file mode 100644
index 00000000..845da049
--- /dev/null
+++ b/crates/api_crud/src/site/mod.rs
@@ -0,0 +1,3 @@
+mod create;
+mod read;
+mod update;
diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs
new file mode 100644
index 00000000..27066519
--- /dev/null
+++ b/crates/api_crud/src/site/read.rs
@@ -0,0 +1,97 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  build_federated_instances,
+  get_local_user_settings_view_from_jwt_opt,
+  person::Register,
+  site::*,
+};
+use lemmy_db_views::site_view::SiteView;
+use lemmy_db_views_actor::person_view::PersonViewSafe;
+use lemmy_utils::{settings::structs::Settings, version, ConnectionId, LemmyError};
+use lemmy_websocket::{messages::GetUsersOnline, LemmyContext};
+use log::info;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetSite {
+  type Response = GetSiteResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<GetSiteResponse, LemmyError> {
+    let data: &GetSite = &self;
+
+    let site_view = match blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
+      Ok(site_view) => Some(site_view),
+      // If the site isn't created yet, check the setup
+      Err(_) => {
+        if let Some(setup) = Settings::get().setup().as_ref() {
+          let register = Register {
+            username: setup.admin_username.to_owned(),
+            email: setup.admin_email.to_owned(),
+            password: setup.admin_password.to_owned(),
+            password_verify: setup.admin_password.to_owned(),
+            show_nsfw: true,
+            captcha_uuid: None,
+            captcha_answer: None,
+          };
+          let login_response = register.perform(context, websocket_id).await?;
+          info!("Admin {} created", setup.admin_username);
+
+          let create_site = CreateSite {
+            name: setup.site_name.to_owned(),
+            description: None,
+            icon: None,
+            banner: None,
+            enable_downvotes: true,
+            open_registration: true,
+            enable_nsfw: true,
+            auth: login_response.jwt,
+          };
+          create_site.perform(context, websocket_id).await?;
+          info!("Site {} created", setup.site_name);
+          Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??)
+        } else {
+          None
+        }
+      }
+    };
+
+    let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
+
+    // Make sure the site creator is the top admin
+    if let Some(site_view) = site_view.to_owned() {
+      let site_creator_id = site_view.creator.id;
+      // TODO investigate why this is sometimes coming back null
+      // Maybe user_.admin isn't being set to true?
+      if let Some(creator_index) = admins.iter().position(|r| r.person.id == site_creator_id) {
+        let creator_person = admins.remove(creator_index);
+        admins.insert(0, creator_person);
+      }
+    }
+
+    let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
+
+    let online = context
+      .chat_server()
+      .send(GetUsersOnline)
+      .await
+      .unwrap_or(1);
+
+    let my_user = get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await?;
+    let federated_instances = build_federated_instances(context.pool()).await?;
+
+    Ok(GetSiteResponse {
+      site_view,
+      admins,
+      banned,
+      online,
+      version: version::VERSION.to_string(),
+      my_user,
+      federated_instances,
+    })
+  }
+}
diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs
new file mode 100644
index 00000000..3a4f5072
--- /dev/null
+++ b/crates/api_crud/src/site/update.rs
@@ -0,0 +1,74 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{
+  blocking,
+  get_local_user_view_from_jwt,
+  is_admin,
+  site::{EditSite, SiteResponse},
+};
+use lemmy_db_queries::{diesel_option_overwrite_to_url, source::site::Site_, Crud};
+use lemmy_db_schema::{
+  naive_now,
+  source::site::{Site, SiteForm},
+};
+use lemmy_db_views::site_view::SiteView;
+use lemmy_utils::{
+  utils::{check_slurs, check_slurs_opt},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperation};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for EditSite {
+  type Response = SiteResponse;
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<SiteResponse, LemmyError> {
+    let data: &EditSite = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.description)?;
+
+    // Make sure user is an admin
+    is_admin(&local_user_view)?;
+
+    let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
+
+    let icon = diesel_option_overwrite_to_url(&data.icon)?;
+    let banner = diesel_option_overwrite_to_url(&data.banner)?;
+
+    let site_form = SiteForm {
+      name: data.name.to_owned(),
+      description: data.description.to_owned(),
+      icon,
+      banner,
+      creator_id: found_site.creator_id,
+      updated: Some(naive_now()),
+      enable_downvotes: data.enable_downvotes,
+      open_registration: data.open_registration,
+      enable_nsfw: data.enable_nsfw,
+    };
+
+    let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
+    if blocking(context.pool(), update_site).await?.is_err() {
+      return Err(ApiError::err("couldnt_update_site").into());
+    }
+
+    let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
+
+    let res = SiteResponse { site_view };
+
+    context.chat_server().do_send(SendAllMessage {
+      op: UserOperation::EditSite,
+      response: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs
new file mode 100644
index 00000000..81c7f5d2
--- /dev/null
+++ b/crates/api_crud/src/user/create.rs
@@ -0,0 +1,244 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, password_length_check, person::*};
+use lemmy_apub::{
+  generate_apub_endpoint,
+  generate_followers_url,
+  generate_inbox_url,
+  generate_shared_inbox_url,
+  EndpointType,
+};
+use lemmy_db_queries::{
+  source::{local_user::LocalUser_, site::Site_},
+  Crud,
+  Followable,
+  Joinable,
+  ListingType,
+  SortType,
+};
+use lemmy_db_schema::{
+  source::{
+    community::*,
+    local_user::{LocalUser, LocalUserForm},
+    person::*,
+    site::*,
+  },
+  CommunityId,
+};
+use lemmy_db_views_actor::person_view::PersonViewSafe;
+use lemmy_utils::{
+  apub::generate_actor_keypair,
+  claims::Claims,
+  settings::structs::Settings,
+  utils::{check_slurs, is_valid_username},
+  ApiError,
+  ConnectionId,
+  LemmyError,
+};
+use lemmy_websocket::{messages::CheckCaptcha, LemmyContext};
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for Register {
+  type Response = LoginResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<LoginResponse, LemmyError> {
+    let data: &Register = &self;
+
+    // Make sure site has open registration
+    if let Ok(site) = blocking(context.pool(), move |conn| Site::read_simple(conn)).await? {
+      if !site.open_registration {
+        return Err(ApiError::err("registration_closed").into());
+      }
+    }
+
+    password_length_check(&data.password)?;
+
+    // Make sure passwords match
+    if data.password != data.password_verify {
+      return Err(ApiError::err("passwords_dont_match").into());
+    }
+
+    // Check if there are admins. False if admins exist
+    let no_admins = blocking(context.pool(), move |conn| {
+      PersonViewSafe::admins(conn).map(|a| a.is_empty())
+    })
+    .await??;
+
+    // If its not the admin, check the captcha
+    if !no_admins && Settings::get().captcha().enabled {
+      let check = context
+        .chat_server()
+        .send(CheckCaptcha {
+          uuid: data
+            .captcha_uuid
+            .to_owned()
+            .unwrap_or_else(|| "".to_string()),
+          answer: data
+            .captcha_answer
+            .to_owned()
+            .unwrap_or_else(|| "".to_string()),
+        })
+        .await?;
+      if !check {
+        return Err(ApiError::err("captcha_incorrect").into());
+      }
+    }
+
+    check_slurs(&data.username)?;
+
+    let actor_keypair = generate_actor_keypair()?;
+    if !is_valid_username(&data.username) {
+      return Err(ApiError::err("invalid_username").into());
+    }
+    let actor_id = generate_apub_endpoint(EndpointType::Person, &data.username)?;
+
+    // We have to create both a person, and local_user
+
+    // Register the new person
+    let person_form = PersonForm {
+      name: data.username.to_owned(),
+      avatar: None,
+      banner: None,
+      preferred_username: None,
+      published: None,
+      updated: None,
+      banned: None,
+      deleted: None,
+      actor_id: Some(actor_id.clone()),
+      bio: None,
+      local: Some(true),
+      private_key: Some(Some(actor_keypair.private_key)),
+      public_key: Some(Some(actor_keypair.public_key)),
+      last_refreshed_at: None,
+      inbox_url: Some(generate_inbox_url(&actor_id)?),
+      shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)),
+    };
+
+    // insert the person
+    let inserted_person = match blocking(context.pool(), move |conn| {
+      Person::create(conn, &person_form)
+    })
+    .await?
+    {
+      Ok(u) => u,
+      Err(_) => {
+        return Err(ApiError::err("user_already_exists").into());
+      }
+    };
+
+    // Create the local user
+    let local_user_form = LocalUserForm {
+      person_id: inserted_person.id,
+      email: Some(data.email.to_owned()),
+      matrix_user_id: None,
+      password_encrypted: data.password.to_owned(),
+      admin: Some(no_admins),
+      show_nsfw: Some(data.show_nsfw),
+      theme: Some("browser".into()),
+      default_sort_type: Some(SortType::Active as i16),
+      default_listing_type: Some(ListingType::Subscribed as i16),
+      lang: Some("browser".into()),
+      show_avatars: Some(true),
+      send_notifications_to_email: Some(false),
+    };
+
+    let inserted_local_user = match blocking(context.pool(), move |conn| {
+      LocalUser::register(conn, &local_user_form)
+    })
+    .await?
+    {
+      Ok(lu) => lu,
+      Err(e) => {
+        let err_type = if e.to_string()
+          == "duplicate key value violates unique constraint \"local_user_email_key\""
+        {
+          "email_already_exists"
+        } else {
+          "user_already_exists"
+        };
+
+        // If the local user creation errored, then delete that person
+        blocking(context.pool(), move |conn| {
+          Person::delete(&conn, inserted_person.id)
+        })
+        .await??;
+
+        return Err(ApiError::err(err_type).into());
+      }
+    };
+
+    let main_community_keypair = generate_actor_keypair()?;
+
+    // Create the main community if it doesn't exist
+    let main_community = match blocking(context.pool(), move |conn| {
+      Community::read(conn, CommunityId(2))
+    })
+    .await?
+    {
+      Ok(c) => c,
+      Err(_e) => {
+        let default_community_name = "main";
+        let actor_id = generate_apub_endpoint(EndpointType::Community, default_community_name)?;
+        let community_form = CommunityForm {
+          name: default_community_name.to_string(),
+          title: "The Default Community".to_string(),
+          description: Some("The Default Community".to_string()),
+          nsfw: false,
+          creator_id: inserted_person.id,
+          removed: None,
+          deleted: None,
+          updated: None,
+          actor_id: Some(actor_id.to_owned()),
+          local: true,
+          private_key: Some(main_community_keypair.private_key),
+          public_key: Some(main_community_keypair.public_key),
+          last_refreshed_at: None,
+          published: None,
+          icon: None,
+          banner: None,
+          followers_url: Some(generate_followers_url(&actor_id)?),
+          inbox_url: Some(generate_inbox_url(&actor_id)?),
+          shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)),
+        };
+        blocking(context.pool(), move |conn| {
+          Community::create(conn, &community_form)
+        })
+        .await??
+      }
+    };
+
+    // Sign them up for main community no matter what
+    let community_follower_form = CommunityFollowerForm {
+      community_id: main_community.id,
+      person_id: inserted_person.id,
+      pending: false,
+    };
+
+    let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
+    if blocking(context.pool(), follow).await?.is_err() {
+      return Err(ApiError::err("community_follower_already_exists").into());
+    };
+
+    // If its an admin, add them as a mod and follower to main
+    if no_admins {
+      let community_moderator_form = CommunityModeratorForm {
+        community_id: main_community.id,
+        person_id: inserted_person.id,
+      };
+
+      let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
+      if blocking(context.pool(), join).await?.is_err() {
+        return Err(ApiError::err("community_moderator_already_exists").into());
+      }
+    }
+
+    // Return the jwt
+    Ok(LoginResponse {
+      jwt: Claims::jwt(inserted_local_user.id.0)?,
+    })
+  }
+}
diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs
new file mode 100644
index 00000000..ca88830c
--- /dev/null
+++ b/crates/api_crud/src/user/delete.rs
@@ -0,0 +1,54 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use bcrypt::verify;
+use lemmy_api_common::{blocking, get_local_user_view_from_jwt, person::*};
+use lemmy_db_queries::source::{comment::Comment_, person::Person_, post::Post_};
+use lemmy_db_schema::source::{comment::Comment, person::*, post::Post};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for DeleteAccount {
+  type Response = LoginResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<LoginResponse, LemmyError> {
+    let data: &DeleteAccount = &self;
+    let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
+
+    // Verify the password
+    let valid: bool = verify(
+      &data.password,
+      &local_user_view.local_user.password_encrypted,
+    )
+    .unwrap_or(false);
+    if !valid {
+      return Err(ApiError::err("password_incorrect").into());
+    }
+
+    // Comments
+    let person_id = local_user_view.person.id;
+    let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id);
+    if blocking(context.pool(), permadelete).await?.is_err() {
+      return Err(ApiError::err("couldnt_update_comment").into());
+    }
+
+    // Posts
+    let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id);
+    if blocking(context.pool(), permadelete).await?.is_err() {
+      return Err(ApiError::err("couldnt_update_post").into());
+    }
+
+    blocking(context.pool(), move |conn| {
+      Person::delete_account(conn, person_id)
+    })
+    .await??;
+
+    Ok(LoginResponse {
+      jwt: data.auth.to_owned(),
+    })
+  }
+}
diff --git a/crates/api_crud/src/user/mod.rs b/crates/api_crud/src/user/mod.rs
new file mode 100644
index 00000000..84307241
--- /dev/null
+++ b/crates/api_crud/src/user/mod.rs
@@ -0,0 +1,3 @@
+mod create;
+mod delete;
+mod read;
diff --git a/crates/api_crud/src/user/read.rs b/crates/api_crud/src/user/read.rs
new file mode 100644
index 00000000..3136e100
--- /dev/null
+++ b/crates/api_crud/src/user/read.rs
@@ -0,0 +1,122 @@
+use crate::PerformCrud;
+use actix_web::web::Data;
+use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, person::*};
+use lemmy_db_queries::{source::person::Person_, SortType};
+use lemmy_db_schema::source::person::*;
+use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder};
+use lemmy_db_views_actor::{
+  community_follower_view::CommunityFollowerView,
+  community_moderator_view::CommunityModeratorView,
+  person_view::PersonViewSafe,
+};
+use lemmy_utils::{ApiError, ConnectionId, LemmyError};
+use lemmy_websocket::LemmyContext;
+use std::str::FromStr;
+
+#[async_trait::async_trait(?Send)]
+impl PerformCrud for GetPersonDetails {
+  type Response = GetPersonDetailsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<GetPersonDetailsResponse, LemmyError> {
+    let data: &GetPersonDetails = &self;
+    let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
+
+    let show_nsfw = match &local_user_view {
+      Some(uv) => uv.local_user.show_nsfw,
+      None => false,
+    };
+
+    let sort = SortType::from_str(&data.sort)?;
+
+    let username = data
+      .username
+      .to_owned()
+      .unwrap_or_else(|| "admin".to_string());
+    let person_details_id = match data.person_id {
+      Some(id) => id,
+      None => {
+        let person = blocking(context.pool(), move |conn| {
+          Person::find_by_name(conn, &username)
+        })
+        .await?;
+        match person {
+          Ok(p) => p.id,
+          Err(_e) => return Err(ApiError::err("couldnt_find_that_username_or_email").into()),
+        }
+      }
+    };
+
+    let person_id = local_user_view.map(|uv| uv.person.id);
+
+    // You don't need to return settings for the user, since this comes back with GetSite
+    // `my_user`
+    let person_view = blocking(context.pool(), move |conn| {
+      PersonViewSafe::read(conn, person_details_id)
+    })
+    .await??;
+
+    let page = data.page;
+    let limit = data.limit;
+    let saved_only = data.saved_only;
+    let community_id = data.community_id;
+
+    let (posts, comments) = blocking(context.pool(), move |conn| {
+      let mut posts_query = PostQueryBuilder::create(conn)
+        .sort(&sort)
+        .show_nsfw(show_nsfw)
+        .saved_only(saved_only)
+        .community_id(community_id)
+        .my_person_id(person_id)
+        .page(page)
+        .limit(limit);
+
+      let mut comments_query = CommentQueryBuilder::create(conn)
+        .my_person_id(person_id)
+        .sort(&sort)
+        .saved_only(saved_only)
+        .community_id(community_id)
+        .page(page)
+        .limit(limit);
+
+      // If its saved only, you don't care what creator it was
+      // Or, if its not saved, then you only want it for that specific creator
+      if !saved_only {
+        posts_query = posts_query.creator_id(person_details_id);
+        comments_query = comments_query.creator_id(person_details_id);
+      }
+
+      let posts = posts_query.list()?;
+      let comments = comments_query.list()?;
+
+      Ok((posts, comments)) as Result<_, LemmyError>
+    })
+    .await??;
+
+    let mut follows = vec![];
+    if let Some(pid) = person_id {
+      if pid == person_details_id {
+        follows = blocking(context.pool(), move |conn| {
+          CommunityFollowerView::for_person(conn, person_details_id)
+        })
+        .await??;
+      }
+    };
+    let moderates = blocking(context.pool(), move |conn| {
+      CommunityModeratorView::for_person(conn, person_details_id)
+    })
+    .await??;
+
+    // Return the jwt
+    Ok(GetPersonDetailsResponse {
+      person_view,
+      follows,
+      moderates,
+      comments,
+      posts,
+    })
+  }
+}
diff --git a/crates/api_structs/src/lib.rs b/crates/api_structs/src/lib.rs
deleted file mode 100644
index f57d7f2b..00000000
--- a/crates/api_structs/src/lib.rs
+++ /dev/null
@@ -1,191 +0,0 @@
-pub mod comment;
-pub mod community;
-pub mod person;
-pub mod post;
-pub mod site;
-pub mod websocket;
-
-use diesel::PgConnection;
-use lemmy_db_queries::{Crud, DbPool};
-use lemmy_db_schema::{
-  source::{
-    comment::Comment,
-    person::Person,
-    person_mention::{PersonMention, PersonMentionForm},
-    post::Post,
-  },
-  LocalUserId,
-};
-use lemmy_db_views::local_user_view::LocalUserView;
-use lemmy_utils::{email::send_email, settings::structs::Settings, utils::MentionData, LemmyError};
-use log::error;
-use serde::{Deserialize, Serialize};
-use url::Url;
-
-#[derive(Serialize, Deserialize, Debug)]
-pub struct WebFingerLink {
-  pub rel: Option<String>,
-  #[serde(rename(serialize = "type", deserialize = "type"))]
-  pub type_: Option<String>,
-  pub href: Option<Url>,
-  #[serde(skip_serializing_if = "Option::is_none")]
-  pub template: Option<String>,
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-pub struct WebFingerResponse {
-  pub subject: String,
-  pub aliases: Vec<Url>,
-  pub links: Vec<WebFingerLink>,
-}
-
-pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
-where
-  F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
-  T: Send + 'static,
-{
-  let pool = pool.clone();
-  let res = actix_web::web::block(move || {
-    let conn = pool.get()?;
-    let res = (f)(&conn);
-    Ok(res) as Result<_, LemmyError>
-  })
-  .await?;
-
-  Ok(res)
-}
-
-pub async fn send_local_notifs(
-  mentions: Vec<MentionData>,
-  comment: Comment,
-  person: Person,
-  post: Post,
-  pool: &DbPool,
-  do_send_email: bool,
-) -> Result<Vec<LocalUserId>, LemmyError> {
-  let ids = blocking(pool, move |conn| {
-    do_send_local_notifs(conn, &mentions, &comment, &person, &post, do_send_email)
-  })
-  .await?;
-
-  Ok(ids)
-}
-
-fn do_send_local_notifs(
-  conn: &PgConnection,
-  mentions: &[MentionData],
-  comment: &Comment,
-  person: &Person,
-  post: &Post,
-  do_send_email: bool,
-) -> Vec<LocalUserId> {
-  let mut recipient_ids = Vec::new();
-
-  // Send the local mentions
-  for mention in mentions
-    .iter()
-    .filter(|m| m.is_local() && m.name.ne(&person.name))
-    .collect::<Vec<&MentionData>>()
-  {
-    if let Ok(mention_user_view) = LocalUserView::read_from_name(&conn, &mention.name) {
-      // TODO
-      // At some point, make it so you can't tag the parent creator either
-      // This can cause two notifications, one for reply and the other for mention
-      recipient_ids.push(mention_user_view.local_user.id);
-
-      let user_mention_form = PersonMentionForm {
-        recipient_id: mention_user_view.person.id,
-        comment_id: comment.id,
-        read: None,
-      };
-
-      // Allow this to fail softly, since comment edits might re-update or replace it
-      // Let the uniqueness handle this fail
-      PersonMention::create(&conn, &user_mention_form).ok();
-
-      // Send an email to those local users that have notifications on
-      if do_send_email {
-        send_email_to_user(
-          &mention_user_view,
-          "Mentioned by",
-          "Person Mention",
-          &comment.content,
-        )
-      }
-    }
-  }
-
-  // Send notifs to the parent commenter / poster
-  match comment.parent_id {
-    Some(parent_id) => {
-      if let Ok(parent_comment) = Comment::read(&conn, parent_id) {
-        // Don't send a notif to yourself
-        if parent_comment.creator_id != person.id {
-          // Get the parent commenter local_user
-          if let Ok(parent_user_view) = LocalUserView::read_person(&conn, parent_comment.creator_id)
-          {
-            recipient_ids.push(parent_user_view.local_user.id);
-
-            if do_send_email {
-              send_email_to_user(
-                &parent_user_view,
-                "Reply from",
-                "Comment Reply",
-                &comment.content,
-              )
-            }
-          }
-        }
-      }
-    }
-    // Its a post
-    None => {
-      if post.creator_id != person.id {
-        if let Ok(parent_user_view) = LocalUserView::read_person(&conn, post.creator_id) {
-          recipient_ids.push(parent_user_view.local_user.id);
-
-          if do_send_email {
-            send_email_to_user(
-              &parent_user_view,
-              "Reply from",
-              "Post Reply",
-              &comment.content,
-            )
-          }
-        }
-      }
-    }
-  };
-  recipient_ids
-}
-
-pub fn send_email_to_user(
-  local_user_view: &LocalUserView,
-  subject_text: &str,
-  body_text: &str,
-  comment_content: &str,
-) {
-  if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
-    return;
-  }
-
-  if let Some(user_email) = &local_user_view.local_user.email {
-    let subject = &format!(
-      "{} - {} {}",
-      subject_text,
-      Settings::get().hostname(),
-      local_user_view.person.name,
-    );
-    let html = &format!(
-      "<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
-      body_text,
-      local_user_view.person.name,
-      comment_content,
-      Settings::get().get_protocol_and_hostname()
-    );
-    match send_email(subject, &user_email, &local_user_view.person.name, html) {
-      Ok(_o) => _o,
-      Err(e) => error!("{}", e),
-    };
-  }
-}
diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml
index acb19db0..7d413f85 100644
--- a/crates/apub/Cargo.toml
+++ b/crates/apub/Cargo.toml
@@ -14,7 +14,7 @@ lemmy_db_queries = { path = "../db_queries" }
 lemmy_db_schema = { path = "../db_schema" }
 lemmy_db_views = { path = "../db_views" }
 lemmy_db_views_actor = { path = "../db_views_actor" }
-lemmy_api_structs = { path = "../api_structs" }
+lemmy_api_common = { path = "../api_common" }
 lemmy_websocket = { path = "../websocket" }
 diesel = "1.4.5"
 activitystreams = "0.7.0-alpha.11"
diff --git a/crates/apub/src/activities/receive/comment.rs b/crates/apub/src/activities/receive/comment.rs
index 95b51d64..2575035b 100644
--- a/crates/apub/src/activities/receive/comment.rs
+++ b/crates/apub/src/activities/receive/comment.rs
@@ -4,7 +4,7 @@ use activitystreams::{
   base::ExtendsExt,
 };
 use anyhow::Context;
-use lemmy_api_structs::{blocking, comment::CommentResponse, send_local_notifs};
+use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs};
 use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable};
 use lemmy_db_schema::source::{
   comment::{Comment, CommentLike, CommentLikeForm},
diff --git a/crates/apub/src/activities/receive/comment_undo.rs b/crates/apub/src/activities/receive/comment_undo.rs
index 22594f33..12a49ee3 100644
--- a/crates/apub/src/activities/receive/comment_undo.rs
+++ b/crates/apub/src/activities/receive/comment_undo.rs
@@ -1,6 +1,6 @@
 use crate::activities::receive::get_actor_as_person;
 use activitystreams::activity::{Dislike, Like};
-use lemmy_api_structs::{blocking, comment::CommentResponse};
+use lemmy_api_common::{blocking, comment::CommentResponse};
 use lemmy_db_queries::{source::comment::Comment_, Likeable};
 use lemmy_db_schema::source::comment::{Comment, CommentLike};
 use lemmy_db_views::comment_view::CommentView;
diff --git a/crates/apub/src/activities/receive/community.rs b/crates/apub/src/activities/receive/community.rs
index 48f6b295..d6dba673 100644
--- a/crates/apub/src/activities/receive/community.rs
+++ b/crates/apub/src/activities/receive/community.rs
@@ -1,4 +1,4 @@
-use lemmy_api_structs::{blocking, community::CommunityResponse};
+use lemmy_api_common::{blocking, community::CommunityResponse};
 use lemmy_db_queries::source::community::Community_;
 use lemmy_db_schema::source::community::Community;
 use lemmy_db_views_actor::community_view::CommunityView;
diff --git a/crates/apub/src/activities/receive/post.rs b/crates/apub/src/activities/receive/post.rs
index d1c935d5..e490964d 100644
--- a/crates/apub/src/activities/receive/post.rs
+++ b/crates/apub/src/activities/receive/post.rs
@@ -10,7 +10,7 @@ use activitystreams::{
   prelude::*,
 };
 use anyhow::Context;
-use lemmy_api_structs::{blocking, post::PostResponse};
+use lemmy_api_common::{blocking, post::PostResponse};
 use lemmy_db_queries::{source::post::Post_, ApubObject, Crud, Likeable};
 use lemmy_db_schema::{
   source::{
diff --git a/crates/apub/src/activities/receive/post_undo.rs b/crates/apub/src/activities/receive/post_undo.rs
index 67cc20df..589b0d22 100644
--- a/crates/apub/src/activities/receive/post_undo.rs
+++ b/crates/apub/src/activities/receive/post_undo.rs
@@ -1,6 +1,6 @@
 use crate::activities::receive::get_actor_as_person;
 use activitystreams::activity::{Dislike, Like};
-use lemmy_api_structs::{blocking, post::PostResponse};
+use lemmy_api_common::{blocking, post::PostResponse};
 use lemmy_db_queries::{source::post::Post_, Likeable};
 use lemmy_db_schema::source::post::{Post, PostLike};
 use lemmy_db_views::post_view::PostView;
diff --git a/crates/apub/src/activities/receive/private_message.rs b/crates/apub/src/activities/receive/private_message.rs
index 04954b71..47067b7a 100644
--- a/crates/apub/src/activities/receive/private_message.rs
+++ b/crates/apub/src/activities/receive/private_message.rs
@@ -13,7 +13,7 @@ use activitystreams::{
   public,
 };
 use anyhow::{anyhow, Context};
-use lemmy_api_structs::{blocking, person::PrivateMessageResponse};
+use lemmy_api_common::{blocking, person::PrivateMessageResponse};
 use lemmy_db_queries::source::private_message::PrivateMessage_;
 use lemmy_db_schema::source::private_message::PrivateMessage;
 use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
diff --git a/crates/apub/src/activities/send/comment.rs b/crates/apub/src/activities/send/comment.rs
index 867e45e9..76371b99 100644
--- a/crates/apub/src/activities/send/comment.rs
+++ b/crates/apub/src/activities/send/comment.rs
@@ -26,7 +26,7 @@ use activitystreams::{
 };
 use anyhow::anyhow;
 use itertools::Itertools;
-use lemmy_api_structs::{blocking, WebFingerResponse};
+use lemmy_api_common::{blocking, WebFingerResponse};
 use lemmy_db_queries::{Crud, DbPool};
 use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post};
 use lemmy_utils::{
diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs
index 80f0a42c..f31eb260 100644
--- a/crates/apub/src/activities/send/community.rs
+++ b/crates/apub/src/activities/send/community.rs
@@ -28,7 +28,7 @@ use activitystreams::{
 };
 use anyhow::Context;
 use itertools::Itertools;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::DbPool;
 use lemmy_db_schema::source::{community::Community, person::Person};
 use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
diff --git a/crates/apub/src/activities/send/person.rs b/crates/apub/src/activities/send/person.rs
index 9560c2fb..c034f593 100644
--- a/crates/apub/src/activities/send/person.rs
+++ b/crates/apub/src/activities/send/person.rs
@@ -14,7 +14,7 @@ use activitystreams::{
   base::{BaseExt, ExtendsExt},
   object::ObjectExt,
 };
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{ApubObject, Followable};
 use lemmy_db_schema::source::{
   community::{Community, CommunityFollower, CommunityFollowerForm},
diff --git a/crates/apub/src/activities/send/post.rs b/crates/apub/src/activities/send/post.rs
index 4d3bb9d1..9f8be1e1 100644
--- a/crates/apub/src/activities/send/post.rs
+++ b/crates/apub/src/activities/send/post.rs
@@ -21,7 +21,7 @@ use activitystreams::{
   prelude::*,
   public,
 };
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::Crud;
 use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
 use lemmy_utils::LemmyError;
diff --git a/crates/apub/src/activities/send/private_message.rs b/crates/apub/src/activities/send/private_message.rs
index 92d818ab..e5a30585 100644
--- a/crates/apub/src/activities/send/private_message.rs
+++ b/crates/apub/src/activities/send/private_message.rs
@@ -16,7 +16,7 @@ use activitystreams::{
   },
   prelude::*,
 };
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::Crud;
 use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage};
 use lemmy_utils::LemmyError;
diff --git a/crates/apub/src/fetcher/community.rs b/crates/apub/src/fetcher/community.rs
index c27116dc..c657bfad 100644
--- a/crates/apub/src/fetcher/community.rs
+++ b/crates/apub/src/fetcher/community.rs
@@ -15,7 +15,7 @@ use activitystreams::{
 };
 use anyhow::Context;
 use diesel::result::Error::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{source::community::Community_, ApubObject, Joinable};
 use lemmy_db_schema::{
   source::community::{Community, CommunityModerator, CommunityModeratorForm},
diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs
index 4ba2a56f..b8f8bbde 100644
--- a/crates/apub/src/fetcher/objects.rs
+++ b/crates/apub/src/fetcher/objects.rs
@@ -1,7 +1,7 @@
 use crate::{fetcher::fetch::fetch_remote_object, objects::FromApub, NoteExt, PageExt};
 use anyhow::anyhow;
 use diesel::result::Error::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{ApubObject, Crud};
 use lemmy_db_schema::source::{comment::Comment, post::Post};
 use lemmy_utils::LemmyError;
diff --git a/crates/apub/src/fetcher/person.rs b/crates/apub/src/fetcher/person.rs
index 3788163b..81ba731b 100644
--- a/crates/apub/src/fetcher/person.rs
+++ b/crates/apub/src/fetcher/person.rs
@@ -5,7 +5,7 @@ use crate::{
 };
 use anyhow::anyhow;
 use diesel::result::Error::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{source::person::Person_, ApubObject};
 use lemmy_db_schema::source::person::Person;
 use lemmy_utils::LemmyError;
diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs
index 23044297..d2d00f32 100644
--- a/crates/apub/src/fetcher/search.rs
+++ b/crates/apub/src/fetcher/search.rs
@@ -15,7 +15,7 @@ use crate::{
 };
 use activitystreams::base::BaseExt;
 use anyhow::{anyhow, Context};
-use lemmy_api_structs::{blocking, site::SearchResponse};
+use lemmy_api_common::{blocking, site::SearchResponse};
 use lemmy_db_queries::{
   source::{
     comment::Comment_,
diff --git a/crates/apub/src/http/comment.rs b/crates/apub/src/http/comment.rs
index 3071445b..4f63d89d 100644
--- a/crates/apub/src/http/comment.rs
+++ b/crates/apub/src/http/comment.rs
@@ -4,7 +4,7 @@ use crate::{
 };
 use actix_web::{body::Body, web, web::Path, HttpResponse};
 use diesel::result::Error::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::Crud;
 use lemmy_db_schema::{source::comment::Comment, CommentId};
 use lemmy_utils::LemmyError;
diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs
index fcf20748..4d3d8481 100644
--- a/crates/apub/src/http/community.rs
+++ b/crates/apub/src/http/community.rs
@@ -11,7 +11,7 @@ use activitystreams::{
   url::Url,
 };
 use actix_web::{body::Body, web, HttpResponse};
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::source::{activity::Activity_, community::Community_};
 use lemmy_db_schema::source::{activity::Activity, community::Community};
 use lemmy_db_views_actor::{
diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs
index 8702bb5f..5d0cf71f 100644
--- a/crates/apub/src/http/mod.rs
+++ b/crates/apub/src/http/mod.rs
@@ -1,7 +1,7 @@
 use crate::APUB_JSON_CONTENT_TYPE;
 use actix_web::{body::Body, web, HttpResponse};
 use http::StatusCode;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::source::activity::Activity_;
 use lemmy_db_schema::source::activity::Activity;
 use lemmy_utils::{settings::structs::Settings, LemmyError};
diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs
index d523d641..6a5a9a27 100644
--- a/crates/apub/src/http/person.rs
+++ b/crates/apub/src/http/person.rs
@@ -9,7 +9,7 @@ use activitystreams::{
   collection::{CollectionExt, OrderedCollection},
 };
 use actix_web::{body::Body, web, HttpResponse};
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::source::person::Person_;
 use lemmy_db_schema::source::person::Person;
 use lemmy_utils::LemmyError;
diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs
index 03218b68..324bb7da 100644
--- a/crates/apub/src/http/post.rs
+++ b/crates/apub/src/http/post.rs
@@ -4,7 +4,7 @@ use crate::{
 };
 use actix_web::{body::Body, web, HttpResponse};
 use diesel::result::Error::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::Crud;
 use lemmy_db_schema::{source::post::Post, PostId};
 use lemmy_utils::LemmyError;
diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs
index c36d4db1..61123d06 100644
--- a/crates/apub/src/inbox/community_inbox.rs
+++ b/crates/apub/src/inbox/community_inbox.rs
@@ -29,7 +29,7 @@ use activitystreams::{
 };
 use actix_web::{web, HttpRequest, HttpResponse};
 use anyhow::{anyhow, Context};
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{source::community::Community_, ApubObject, DbPool, Followable};
 use lemmy_db_schema::{
   source::{
diff --git a/crates/apub/src/inbox/mod.rs b/crates/apub/src/inbox/mod.rs
index 87d6d182..72b00c33 100644
--- a/crates/apub/src/inbox/mod.rs
+++ b/crates/apub/src/inbox/mod.rs
@@ -12,7 +12,7 @@ use activitystreams::{
 };
 use actix_web::HttpRequest;
 use anyhow::{anyhow, Context};
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{
   source::{activity::Activity_, community::Community_},
   ApubObject,
diff --git a/crates/apub/src/inbox/person_inbox.rs b/crates/apub/src/inbox/person_inbox.rs
index 38e4167a..99c0f18f 100644
--- a/crates/apub/src/inbox/person_inbox.rs
+++ b/crates/apub/src/inbox/person_inbox.rs
@@ -49,7 +49,7 @@ use activitystreams::{
 use actix_web::{web, HttpRequest, HttpResponse};
 use anyhow::{anyhow, Context};
 use diesel::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{source::person::Person_, ApubObject, Followable};
 use lemmy_db_schema::source::{
   community::{Community, CommunityFollower},
diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs
index 2a077e11..5fe2bdf7 100644
--- a/crates/apub/src/inbox/receive_for_community.rs
+++ b/crates/apub/src/inbox/receive_for_community.rs
@@ -61,7 +61,7 @@ use activitystreams::{
 };
 use anyhow::{anyhow, Context};
 use diesel::result::Error::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{source::community::CommunityModerator_, ApubObject, Crud, Joinable};
 use lemmy_db_schema::{
   source::{
diff --git a/crates/apub/src/inbox/shared_inbox.rs b/crates/apub/src/inbox/shared_inbox.rs
index 633388a5..710f34b4 100644
--- a/crates/apub/src/inbox/shared_inbox.rs
+++ b/crates/apub/src/inbox/shared_inbox.rs
@@ -15,7 +15,7 @@ use crate::{
 use activitystreams::{activity::ActorAndObject, prelude::*};
 use actix_web::{web, HttpRequest, HttpResponse};
 use anyhow::Context;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{ApubObject, DbPool};
 use lemmy_db_schema::source::community::Community;
 use lemmy_utils::{location_info, LemmyError};
diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs
index 74d4cbef..6fa93743 100644
--- a/crates/apub/src/lib.rs
+++ b/crates/apub/src/lib.rs
@@ -24,7 +24,7 @@ use activitystreams::{
 use activitystreams_ext::{Ext1, Ext2};
 use anyhow::{anyhow, Context};
 use diesel::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{source::activity::Activity_, ApubObject, DbPool};
 use lemmy_db_schema::{
   source::{
diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs
index bd6c1a33..39f58683 100644
--- a/crates/apub/src/objects/comment.rs
+++ b/crates/apub/src/objects/comment.rs
@@ -21,7 +21,7 @@ use activitystreams::{
   public,
 };
 use anyhow::{anyhow, Context};
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{Crud, DbPool};
 use lemmy_db_schema::{
   source::{
diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs
index d7e42c4a..ad1b7d61 100644
--- a/crates/apub/src/objects/community.rs
+++ b/crates/apub/src/objects/community.rs
@@ -23,7 +23,7 @@ use activitystreams::{
 };
 use activitystreams_ext::Ext2;
 use anyhow::Context;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::DbPool;
 use lemmy_db_schema::{
   naive_now,
diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs
index 77e9ffbf..08822abf 100644
--- a/crates/apub/src/objects/mod.rs
+++ b/crates/apub/src/objects/mod.rs
@@ -13,7 +13,7 @@ use activitystreams::{
 use anyhow::{anyhow, Context};
 use chrono::NaiveDateTime;
 use diesel::result::Error::NotFound;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{ApubObject, Crud, DbPool};
 use lemmy_db_schema::{source::community::Community, CommunityId, DbUrl};
 use lemmy_utils::{
diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs
index 87227dd1..c0dee8c1 100644
--- a/crates/apub/src/objects/person.rs
+++ b/crates/apub/src/objects/person.rs
@@ -18,7 +18,7 @@ use activitystreams::{
 };
 use activitystreams_ext::Ext1;
 use anyhow::Context;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{ApubObject, DbPool};
 use lemmy_db_schema::{
   naive_now,
diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs
index f532fcc1..f26da74e 100644
--- a/crates/apub/src/objects/post.rs
+++ b/crates/apub/src/objects/post.rs
@@ -23,7 +23,7 @@ use activitystreams::{
 };
 use activitystreams_ext::Ext1;
 use anyhow::Context;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{Crud, DbPool};
 use lemmy_db_schema::{
   self,
diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs
index 6eec9fa8..7cbb10f4 100644
--- a/crates/apub/src/objects/private_message.rs
+++ b/crates/apub/src/objects/private_message.rs
@@ -19,7 +19,7 @@ use activitystreams::{
   prelude::*,
 };
 use anyhow::Context;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{Crud, DbPool};
 use lemmy_db_schema::source::{
   person::Person,
diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml
index b6b464c3..181d4c50 100644
--- a/crates/routes/Cargo.toml
+++ b/crates/routes/Cargo.toml
@@ -13,7 +13,7 @@ lemmy_db_queries = { path = "../db_queries" }
 lemmy_db_views = { path = "../db_views" }
 lemmy_db_views_actor = { path = "../db_views_actor" }
 lemmy_db_schema = { path = "../db_schema" }
-lemmy_api_structs = { path = "../api_structs" }
+lemmy_api_common = { path = "../api_common" }
 diesel = "1.4.5"
 actix = "0.10.0"
 actix-web = { version = "3.3.2", default-features = false, features = ["rustls"] }
diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs
index 6fc370ed..9181a129 100644
--- a/crates/routes/src/feeds.rs
+++ b/crates/routes/src/feeds.rs
@@ -2,7 +2,7 @@ use actix_web::{error::ErrorBadRequest, *};
 use anyhow::anyhow;
 use chrono::{DateTime, NaiveDateTime, Utc};
 use diesel::PgConnection;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_queries::{
   source::{community::Community_, person::Person_},
   Crud,
diff --git a/crates/routes/src/nodeinfo.rs b/crates/routes/src/nodeinfo.rs
index 1333279c..d06f6092 100644
--- a/crates/routes/src/nodeinfo.rs
+++ b/crates/routes/src/nodeinfo.rs
@@ -1,6 +1,6 @@
 use actix_web::{body::Body, error::ErrorBadRequest, *};
 use anyhow::anyhow;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_db_views::site_view::SiteView;
 use lemmy_utils::{settings::structs::Settings, version, LemmyError};
 use lemmy_websocket::LemmyContext;
diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs
index 8ab2a5b6..82c993da 100644
--- a/crates/routes/src/webfinger.rs
+++ b/crates/routes/src/webfinger.rs
@@ -1,6 +1,6 @@
 use actix_web::{error::ErrorBadRequest, web::Query, *};
 use anyhow::anyhow;
-use lemmy_api_structs::{blocking, WebFingerLink, WebFingerResponse};
+use lemmy_api_common::{blocking, WebFingerLink, WebFingerResponse};
 use lemmy_db_queries::source::{community::Community_, person::Person_};
 use lemmy_db_schema::source::{community::Community, person::Person};
 use lemmy_utils::{
diff --git a/crates/websocket/Cargo.toml b/crates/websocket/Cargo.toml
index b957e944..e550d2b5 100644
--- a/crates/websocket/Cargo.toml
+++ b/crates/websocket/Cargo.toml
@@ -10,7 +10,7 @@ doctest = false
 
 [dependencies]
 lemmy_utils = { path = "../utils" }
-lemmy_api_structs = { path = "../api_structs" }
+lemmy_api_common = { path = "../api_common" }
 lemmy_db_queries = { path = "../db_queries" }
 lemmy_db_schema = { path = "../db_schema" }
 reqwest = { version = "0.10.10", features = ["json"] }
diff --git a/crates/websocket/src/chat_server.rs b/crates/websocket/src/chat_server.rs
index c9016a25..f1c936d6 100644
--- a/crates/websocket/src/chat_server.rs
+++ b/crates/websocket/src/chat_server.rs
@@ -6,7 +6,7 @@ use diesel::{
   r2d2::{ConnectionManager, Pool},
   PgConnection,
 };
-use lemmy_api_structs::{comment::*, post::*};
+use lemmy_api_common::{comment::*, post::*};
 use lemmy_db_schema::{CommunityId, LocalUserId, PostId};
 use lemmy_utils::{
   location_info,
diff --git a/crates/websocket/src/messages.rs b/crates/websocket/src/messages.rs
index a1d4396b..31ca755f 100644
--- a/crates/websocket/src/messages.rs
+++ b/crates/websocket/src/messages.rs
@@ -1,6 +1,6 @@
 use crate::UserOperation;
 use actix::{prelude::*, Recipient};
-use lemmy_api_structs::{comment::CommentResponse, post::PostResponse};
+use lemmy_api_common::{comment::CommentResponse, post::PostResponse};
 use lemmy_db_schema::{CommunityId, LocalUserId, PostId};
 use lemmy_utils::{ConnectionId, IpAddr};
 use serde::{Deserialize, Serialize};
diff --git a/src/main.rs b/src/main.rs
index fa110b51..3cdab3ec 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,7 +8,7 @@ use diesel::{
   PgConnection,
 };
 use lemmy_api::match_websocket_operation;
-use lemmy_api_structs::blocking;
+use lemmy_api_common::blocking;
 use lemmy_apub::activity_queue::create_activity_queue;
 use lemmy_db_queries::get_database_url_from_env;
 use lemmy_routes::{feeds, images, nodeinfo, webfinger};
@@ -88,6 +88,7 @@ async fn main() -> Result<(), LemmyError> {
       .wrap(middleware::Logger::default())
       .data(context)
       // The routes
+      .configure(|cfg| lemmy_api_crud::routes::config(cfg, &rate_limiter))
       .configure(|cfg| lemmy_api::routes::config(cfg, &rate_limiter))
       .configure(lemmy_apub::routes::config)
       .configure(feeds::config)