From: Dessalines <tyhou13@gmx.com>
Date: Mon, 29 Mar 2021 20:24:50 +0000 (-0400)
Subject: Merge remote-tracking branch 'yerba/split-api-crate' into test_merge_api_crates_reorg
X-Git-Url: http://these/git/%7B%60%24%7BghostArchiveUrl%7D/static/%7Bpost.ap_id%7D?a=commitdiff_plain;h=4c8f2e976effe381d4ea1914462c43242a8c64fd;p=lemmy.git

Merge remote-tracking branch 'yerba/split-api-crate' into test_merge_api_crates_reorg
---

4c8f2e976effe381d4ea1914462c43242a8c64fd
diff --cc crates/api_common/src/lib.rs
index 00000000,2337f10b..dccc3bce
mode 000000,100644..100644
--- a/crates/api_common/src/lib.rs
+++ b/crates/api_common/src/lib.rs
@@@ -1,0 -1,420 +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 {
++  if !local_user_view.person.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 --cc crates/api_crud/src/comment/create.rs
index 00000000,57c85013..0cdde729
mode 000000,100644..100644
--- a/crates/api_crud/src/comment/create.rs
+++ b/crates/api_crud/src/comment/create.rs
@@@ -1,0 -1,170 +1,164 @@@
+ 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, UserOperationCrud};
+ 
+ #[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,
++      ..CommentForm::default()
+     };
+ 
+     // 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: UserOperationCrud::CreateComment,
+       comment: res.clone(),
+       websocket_id,
+     });
+ 
+     res.recipient_ids = Vec::new(); // Necessary to avoid doubles
+ 
+     Ok(res)
+   }
+ }
diff --cc crates/api_crud/src/community/create.rs
index 00000000,104975b7..bef376f0
mode 000000,100644..100644
--- a/crates/api_crud/src/community/create.rs
+++ b/crates/api_crud/src/community/create.rs
@@@ -1,0 -1,134 +1,129 @@@
+ 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)?)),
++      ..CommunityForm::default()
+     };
+ 
+     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 --cc crates/api_crud/src/community/update.rs
index 00000000,1e5b9d0c..0a0540fa
mode 000000,100644..100644
--- a/crates/api_crud/src/community/update.rs
+++ b/crates/api_crud/src/community/update.rs
@@@ -1,0 -1,114 +1,104 @@@
+ 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, UserOperationCrud};
+ 
+ #[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(),
++      creator_id: read_community.creator_id,
+       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,
++      ..CommunityForm::default()
+     };
+ 
+     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,
+       UserOperationCrud::EditCommunity,
+     );
+ 
+     Ok(res)
+   }
+ }
diff --cc crates/api_crud/src/post/create.rs
index 00000000,f8bce061..2fe86414
mode 000000,100644..100644
--- a/crates/api_crud/src/post/create.rs
+++ b/crates/api_crud/src/post/create.rs
@@@ -1,0 -1,130 +1,123 @@@
+ 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, UserOperationCrud};
+ 
+ #[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,
++      ..PostForm::default()
+     };
+ 
+     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: UserOperationCrud::CreatePost,
+       post: res.clone(),
+       websocket_id,
+     });
+ 
+     Ok(res)
+   }
+ }
diff --cc crates/api_crud/src/post/update.rs
index 00000000,e1efc790..8ca0dcd1
mode 000000,100644..100644
--- a/crates/api_crud/src/post/update.rs
+++ b/crates/api_crud/src/post/update.rs
@@@ -1,0 -1,116 +1,110 @@@
+ 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, UserOperationCrud};
+ 
+ #[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 {
++      creator_id: orig_post.creator_id.to_owned(),
++      community_id: orig_post.community_id,
+       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,
++      ..PostForm::default()
+     };
+ 
+     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: UserOperationCrud::EditPost,
+       post: res.clone(),
+       websocket_id,
+     });
+ 
+     Ok(res)
+   }
+ }
diff --cc crates/api_crud/src/private_message/create.rs
index 00000000,02561263..53c0b950
mode 000000,100644..100644
--- a/crates/api_crud/src/private_message/create.rs
+++ b/crates/api_crud/src/private_message/create.rs
@@@ -1,0 -1,112 +1,107 @@@
+ 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, UserOperationCrud};
+ 
+ #[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,
++      ..PrivateMessageForm::default()
+     };
+ 
+     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: UserOperationCrud::CreatePrivateMessage,
+         response: res.clone(),
+         local_recipient_id,
+         websocket_id,
+       });
+     }
+ 
+     Ok(res)
+   }
+ }
diff --cc crates/api_crud/src/user/create.rs
index 00000000,81c7f5d2..63a6474d
mode 000000,100644..100644
--- a/crates/api_crud/src/user/create.rs
+++ b/crates/api_crud/src/user/create.rs
@@@ -1,0 -1,244 +1,226 @@@
+ 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)?)),
++      admin: Some(no_admins),
++      ..PersonForm::default()
+     };
+ 
+     // 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)?)),
++          ..CommunityForm::default()
+         };
+         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 --cc crates/apub/src/objects/person.rs
index 662871dc,c0dee8c1..25c785d8
--- a/crates/apub/src/objects/person.rs
+++ b/crates/apub/src/objects/person.rs
@@@ -16,9 -16,9 +16,9 @@@ use activitystreams::
    object::{ApObject, Image, Tombstone},
    prelude::*,
  };
 -use activitystreams_ext::Ext1;
 +use activitystreams_ext::Ext2;
  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 --cc src/api_routes.rs
index 5c29073e,34501519..692daedf
--- a/src/api_routes.rs
+++ b/src/api_routes.rs
@@@ -7,9 -8,7 +8,7 @@@ use serde::Deserialize
  
  pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
    cfg.service(
 -    web::scope("/api/v2")
 +    web::scope("/api/v3")
-       // Websockets
-       .service(web::resource("/ws").to(chat_route))
        // Site
        .service(
          web::scope("/site")