From c7de1fcf24185c7f72161bf7d3536ccb7c4e96c5 Mon Sep 17 00:00:00 2001
From: Nutomic <me@nutomic.com>
Date: Sat, 17 Jul 2021 16:08:46 +0000
Subject: [PATCH] Apub inbox rewrite (#1652)

* start to implement apub inbox routing lib

* got something that almost works

* it compiles!

* implemented some more

* move library code to separate crate (most of it)

* convert private message handlers

* convert all comment receivers (except undo comment)

* convert post receiver

* add verify trait

* convert community receivers

* add cc field for all activities which i forgot before

* convert inbox functions, add missing checks

* convert undo like/dislike receivers

* convert undo_delete and undo_remove receivers

* move block/unblock activities

* convert remaining activity receivers

* reimplement http signature verification and other checks

* also use actor type for routing, VerifyActivity and SendActivity traits

* cleanup and restructure apub_receive code

* wip: try to fix activity routing

* implement a (very bad) derive macro for activityhandler

* working activity routing!

* rework pm verify(), fix tests and confirm manually

also remove inbox username check which was broken

* rework following verify(), fix tests and test manually

* fix post/comment create/update, rework voting

* Rewrite remove/delete post/comment, fix tests, test manually

* Rework and fix (un)block user, announce, update post

* some code cleanup

* rework delete/remove activity receivers (still quite messy)

* rewrite, test and fix add/remove mod, update community handlers

* add docs for ActivityHandler derive macro

* dont try to compile macro comments
---
 Cargo.lock                                    |  61 ++
 Cargo.toml                                    |   2 +
 api_tests/prepare-drone-federation-test.sh    |   1 +
 crates/apub/src/activities/send/community.rs  |   4 +-
 crates/apub/src/activities/send/person.rs     |   4 +-
 crates/apub/src/activity_queue.rs             |   6 +-
 crates/apub/src/extensions/signatures.rs      |   8 +-
 crates/apub/src/fetcher/objects.rs            |  24 +-
 crates/apub/src/lib.rs                        |   3 +-
 crates/apub/src/objects/comment.rs            |  15 +-
 crates/apub_lib/Cargo.toml                    |  14 +
 crates/apub_lib/src/lib.rs                    |  72 ++
 crates/apub_lib_derive/Cargo.toml             |  15 +
 crates/apub_lib_derive/src/lib.rs             | 149 ++++
 crates/apub_receive/Cargo.toml                |   1 +
 .../src/activities/comment/create.rs          |  68 ++
 .../src/activities/comment/mod.rs             |  65 ++
 .../src/activities/comment/remove.rs          |  56 ++
 .../src/activities/comment/undo_remove.rs     |  65 ++
 .../src/activities/comment/update.rs          |  67 ++
 .../src/activities/community/add_mod.rs       |  86 ++
 .../src/activities/community/announce.rs      | 104 +++
 .../src/activities/community/block_user.rs    |  83 ++
 .../src/activities/community/mod.rs           |  35 +
 .../activities/community/undo_block_user.rs   |  72 ++
 .../src/activities/community/update.rs        |  89 ++
 .../src/activities/deletion/delete.rs         | 158 ++++
 .../src/activities/deletion/mod.rs            |   2 +
 .../src/activities/deletion/undo_delete.rs    | 125 +++
 .../src/activities/following/accept.rs        |  62 ++
 .../src/activities/following/follow.rs        |  73 ++
 .../src/activities/following/mod.rs           |   3 +
 .../src/activities/following/undo.rs          |  67 ++
 crates/apub_receive/src/activities/mod.rs     | 122 ++-
 .../src/activities/post/create.rs             |  66 ++
 .../apub_receive/src/activities/post/mod.rs   |  31 +
 .../src/activities/post/update.rs             |  96 +++
 .../src/activities/private_message/create.rs  |  61 ++
 .../src/activities/private_message/delete.rs  |  63 ++
 .../src/activities/private_message/mod.rs     |  42 +
 .../activities/private_message/undo_delete.rs |  75 ++
 .../src/activities/private_message/update.rs  |  61 ++
 .../src/activities/receive/comment.rs         | 262 ------
 .../src/activities/receive/comment_undo.rs    | 150 ----
 .../src/activities/receive/community.rs       | 232 -----
 .../src/activities/receive/mod.rs             |  81 --
 .../src/activities/receive/post.rs            | 242 ------
 .../src/activities/receive/post_undo.rs       | 125 ---
 .../src/activities/receive/private_message.rs | 228 -----
 .../src/activities/removal/mod.rs             |   2 +
 .../src/activities/removal/remove.rs          | 155 ++++
 .../src/activities/removal/undo_remove.rs     | 120 +++
 .../src/activities/voting/dislike.rs          |  54 ++
 .../src/activities/voting/like.rs             |  54 ++
 .../apub_receive/src/activities/voting/mod.rs | 157 ++++
 .../src/activities/voting/undo_dislike.rs     |  55 ++
 .../src/activities/voting/undo_like.rs        |  55 ++
 crates/apub_receive/src/http/community.rs     |  21 +-
 crates/apub_receive/src/http/inbox_enums.rs   | 100 +++
 crates/apub_receive/src/http/mod.rs           | 142 +++-
 crates/apub_receive/src/http/person.rs        |  20 +-
 crates/apub_receive/src/{ => http}/routes.rs  |  31 +-
 .../apub_receive/src/inbox/community_inbox.rs | 346 --------
 crates/apub_receive/src/inbox/mod.rs          | 153 ----
 crates/apub_receive/src/inbox/person_inbox.rs | 515 -----------
 .../src/inbox/receive_for_community.rs        | 802 ------------------
 crates/apub_receive/src/inbox/shared_inbox.rs | 151 ----
 crates/apub_receive/src/lib.rs                |   4 +-
 docker/federation/start-local-instances.bash  |   2 +-
 scripts/compilation_benchmark.sh              |   6 +-
 src/main.rs                                   |   2 +-
 71 files changed, 3191 insertions(+), 3352 deletions(-)
 create mode 100644 crates/apub_lib/Cargo.toml
 create mode 100644 crates/apub_lib/src/lib.rs
 create mode 100644 crates/apub_lib_derive/Cargo.toml
 create mode 100644 crates/apub_lib_derive/src/lib.rs
 create mode 100644 crates/apub_receive/src/activities/comment/create.rs
 create mode 100644 crates/apub_receive/src/activities/comment/mod.rs
 create mode 100644 crates/apub_receive/src/activities/comment/remove.rs
 create mode 100644 crates/apub_receive/src/activities/comment/undo_remove.rs
 create mode 100644 crates/apub_receive/src/activities/comment/update.rs
 create mode 100644 crates/apub_receive/src/activities/community/add_mod.rs
 create mode 100644 crates/apub_receive/src/activities/community/announce.rs
 create mode 100644 crates/apub_receive/src/activities/community/block_user.rs
 create mode 100644 crates/apub_receive/src/activities/community/mod.rs
 create mode 100644 crates/apub_receive/src/activities/community/undo_block_user.rs
 create mode 100644 crates/apub_receive/src/activities/community/update.rs
 create mode 100644 crates/apub_receive/src/activities/deletion/delete.rs
 create mode 100644 crates/apub_receive/src/activities/deletion/mod.rs
 create mode 100644 crates/apub_receive/src/activities/deletion/undo_delete.rs
 create mode 100644 crates/apub_receive/src/activities/following/accept.rs
 create mode 100644 crates/apub_receive/src/activities/following/follow.rs
 create mode 100644 crates/apub_receive/src/activities/following/mod.rs
 create mode 100644 crates/apub_receive/src/activities/following/undo.rs
 create mode 100644 crates/apub_receive/src/activities/post/create.rs
 create mode 100644 crates/apub_receive/src/activities/post/mod.rs
 create mode 100644 crates/apub_receive/src/activities/post/update.rs
 create mode 100644 crates/apub_receive/src/activities/private_message/create.rs
 create mode 100644 crates/apub_receive/src/activities/private_message/delete.rs
 create mode 100644 crates/apub_receive/src/activities/private_message/mod.rs
 create mode 100644 crates/apub_receive/src/activities/private_message/undo_delete.rs
 create mode 100644 crates/apub_receive/src/activities/private_message/update.rs
 delete mode 100644 crates/apub_receive/src/activities/receive/comment.rs
 delete mode 100644 crates/apub_receive/src/activities/receive/comment_undo.rs
 delete mode 100644 crates/apub_receive/src/activities/receive/community.rs
 delete mode 100644 crates/apub_receive/src/activities/receive/mod.rs
 delete mode 100644 crates/apub_receive/src/activities/receive/post.rs
 delete mode 100644 crates/apub_receive/src/activities/receive/post_undo.rs
 delete mode 100644 crates/apub_receive/src/activities/receive/private_message.rs
 create mode 100644 crates/apub_receive/src/activities/removal/mod.rs
 create mode 100644 crates/apub_receive/src/activities/removal/remove.rs
 create mode 100644 crates/apub_receive/src/activities/removal/undo_remove.rs
 create mode 100644 crates/apub_receive/src/activities/voting/dislike.rs
 create mode 100644 crates/apub_receive/src/activities/voting/like.rs
 create mode 100644 crates/apub_receive/src/activities/voting/mod.rs
 create mode 100644 crates/apub_receive/src/activities/voting/undo_dislike.rs
 create mode 100644 crates/apub_receive/src/activities/voting/undo_like.rs
 create mode 100644 crates/apub_receive/src/http/inbox_enums.rs
 rename crates/apub_receive/src/{ => http}/routes.rs (83%)
 delete mode 100644 crates/apub_receive/src/inbox/community_inbox.rs
 delete mode 100644 crates/apub_receive/src/inbox/mod.rs
 delete mode 100644 crates/apub_receive/src/inbox/person_inbox.rs
 delete mode 100644 crates/apub_receive/src/inbox/receive_for_community.rs
 delete mode 100644 crates/apub_receive/src/inbox/shared_inbox.rs

diff --git a/Cargo.lock b/Cargo.lock
index d6b98150..31492222 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -988,6 +988,12 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
 
+[[package]]
+name = "dissimilar"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc4b29f4b9bb94bf267d57269fd0706d343a160937108e9619fe380645428abb"
+
 [[package]]
 name = "either"
 version = "1.6.1"
@@ -1261,6 +1267,12 @@ version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189"
 
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
 [[package]]
 name = "h2"
 version = "0.3.3"
@@ -1736,6 +1748,30 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "lemmy_apub_lib"
+version = "0.1.0"
+dependencies = [
+ "activitystreams",
+ "activitystreams-ext",
+ "async-trait",
+ "lemmy_apub_lib_derive",
+ "lemmy_utils",
+ "lemmy_websocket",
+ "serde",
+ "url",
+]
+
+[[package]]
+name = "lemmy_apub_lib_derive"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2 1.0.27",
+ "quote 1.0.9",
+ "syn 1.0.73",
+ "trybuild",
+]
+
 [[package]]
 name = "lemmy_apub_receive"
 version = "0.1.0"
@@ -1760,6 +1796,7 @@ dependencies = [
  "itertools",
  "lemmy_api_common",
  "lemmy_apub",
+ "lemmy_apub_lib",
  "lemmy_db_queries",
  "lemmy_db_schema",
  "lemmy_db_views",
@@ -3361,6 +3398,15 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "tower-service"
 version = "0.3.1"
@@ -3393,6 +3439,21 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
+[[package]]
+name = "trybuild"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1768998d9a3b179411618e377dbb134c58a88cda284b0aa71c42c40660127d46"
+dependencies = [
+ "dissimilar",
+ "glob",
+ "lazy_static",
+ "serde",
+ "serde_json",
+ "termcolor",
+ "toml",
+]
+
 [[package]]
 name = "twoway"
 version = "0.2.2"
diff --git a/Cargo.toml b/Cargo.toml
index 03028579..2352a7d6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,8 @@ members = [
     "crates/api",
     "crates/api_crud",
     "crates/api_common",
+    "crates/apub_lib",
+    "crates/apub_lib_derive",
     "crates/apub",
     "crates/apub_receive",
     "crates/utils",
diff --git a/api_tests/prepare-drone-federation-test.sh b/api_tests/prepare-drone-federation-test.sh
index ae9c1293..c2c7805a 100755
--- a/api_tests/prepare-drone-federation-test.sh
+++ b/api_tests/prepare-drone-federation-test.sh
@@ -3,6 +3,7 @@ set -e
 
 export LEMMY_TEST_SEND_SYNC=1
 export RUST_BACKTRACE=1
+export RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_apub_receive=debug,lemmy_db_queries=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
 
 for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
   psql "${LEMMY_DATABASE_URL}/lemmy" -c "DROP DATABASE IF EXISTS $INSTANCE"
diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs
index 378c5bd6..bf751479 100644
--- a/crates/apub/src/activities/send/community.rs
+++ b/crates/apub/src/activities/send/community.rs
@@ -49,7 +49,6 @@ use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
 use lemmy_websocket::LemmyContext;
 use url::Url;
 
-#[async_trait::async_trait(?Send)]
 impl ActorType for Community {
   fn is_local(&self) -> bool {
     self.local
@@ -57,6 +56,9 @@ impl ActorType for Community {
   fn actor_id(&self) -> Url {
     self.actor_id.to_owned().into_inner()
   }
+  fn name(&self) -> String {
+    self.name.clone()
+  }
   fn public_key(&self) -> Option<String> {
     self.public_key.to_owned()
   }
diff --git a/crates/apub/src/activities/send/person.rs b/crates/apub/src/activities/send/person.rs
index 1e72a857..b1fc0cd2 100644
--- a/crates/apub/src/activities/send/person.rs
+++ b/crates/apub/src/activities/send/person.rs
@@ -24,7 +24,6 @@ use lemmy_utils::LemmyError;
 use lemmy_websocket::LemmyContext;
 use url::Url;
 
-#[async_trait::async_trait(?Send)]
 impl ActorType for Person {
   fn is_local(&self) -> bool {
     self.local
@@ -32,6 +31,9 @@ impl ActorType for Person {
   fn actor_id(&self) -> Url {
     self.actor_id.to_owned().into_inner()
   }
+  fn name(&self) -> String {
+    self.name.clone()
+  }
 
   fn public_key(&self) -> Option<String> {
     self.public_key.to_owned()
diff --git a/crates/apub/src/activity_queue.rs b/crates/apub/src/activity_queue.rs
index 7016e0ca..22b88d14 100644
--- a/crates/apub/src/activity_queue.rs
+++ b/crates/apub/src/activity_queue.rs
@@ -49,7 +49,7 @@ where
   if check_is_apub_id_valid(&inbox, false).is_ok() {
     debug!(
       "Sending activity {:?} to {}",
-      &activity.id_unchecked(),
+      &activity.id_unchecked().map(ToString::to_string),
       &inbox
     );
     send_activity_internal(context, activity, creator, vec![inbox], true, true).await?;
@@ -88,7 +88,7 @@ where
   .collect();
   debug!(
     "Sending activity {:?} to followers of {}",
-    &activity.id_unchecked().map(|i| i.to_string()),
+    &activity.id_unchecked().map(ToString::to_string),
     &community.actor_id
   );
 
@@ -127,7 +127,7 @@ where
     check_is_apub_id_valid(&inbox, false)?;
     debug!(
       "Sending activity {:?} to community {}",
-      &activity.id_unchecked(),
+      &activity.id_unchecked().map(ToString::to_string),
       &community.actor_id
     );
     // dont send to object_actor here, as that is responsibility of the community itself
diff --git a/crates/apub/src/extensions/signatures.rs b/crates/apub/src/extensions/signatures.rs
index 47b8e5a2..be323d51 100644
--- a/crates/apub/src/extensions/signatures.rs
+++ b/crates/apub/src/extensions/signatures.rs
@@ -1,12 +1,11 @@
-use crate::ActorType;
 use activitystreams::unparsed::UnparsedMutExt;
 use activitystreams_ext::UnparsedExtension;
 use actix_web::HttpRequest;
-use anyhow::{anyhow, Context};
+use anyhow::anyhow;
 use http::{header::HeaderName, HeaderMap, HeaderValue};
 use http_signature_normalization_actix::Config as ConfigActix;
 use http_signature_normalization_reqwest::prelude::{Config, SignExt};
-use lemmy_utils::{location_info, LemmyError};
+use lemmy_utils::LemmyError;
 use log::debug;
 use openssl::{
   hash::MessageDigest,
@@ -65,8 +64,7 @@ pub(crate) async fn sign_and_send(
 }
 
 /// Verifies the HTTP signature on an incoming inbox request.
-pub fn verify_signature(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> {
-  let public_key = actor.public_key().context(location_info!())?;
+pub fn verify_signature(request: &HttpRequest, public_key: &str) -> Result<(), LemmyError> {
   let verified = CONFIG2
     .begin_verify(
       request.method(),
diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs
index 41bc075a..af8a59f7 100644
--- a/crates/apub/src/fetcher/objects.rs
+++ b/crates/apub/src/fetcher/objects.rs
@@ -1,4 +1,10 @@
-use crate::{fetcher::fetch::fetch_remote_object, objects::FromApub, NoteExt, PageExt};
+use crate::{
+  fetcher::fetch::fetch_remote_object,
+  objects::FromApub,
+  NoteExt,
+  PageExt,
+  PostOrComment,
+};
 use anyhow::anyhow;
 use diesel::result::Error::NotFound;
 use lemmy_api_common::blocking;
@@ -89,3 +95,19 @@ pub async fn get_or_fetch_and_insert_comment(
     Err(e) => Err(e.into()),
   }
 }
+
+pub async fn get_or_fetch_and_insert_post_or_comment(
+  ap_id: &Url,
+  context: &LemmyContext,
+  recursion_counter: &mut i32,
+) -> Result<PostOrComment, LemmyError> {
+  Ok(
+    match get_or_fetch_and_insert_post(ap_id, context, recursion_counter).await {
+      Ok(p) => PostOrComment::Post(Box::new(p)),
+      Err(_) => {
+        let c = get_or_fetch_and_insert_comment(ap_id, context, recursion_counter).await?;
+        PostOrComment::Comment(Box::new(c))
+      }
+    },
+  )
+}
diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs
index fe63d4d5..6bb97675 100644
--- a/crates/apub/src/lib.rs
+++ b/crates/apub/src/lib.rs
@@ -52,6 +52,7 @@ pub type GroupExt =
 /// Activitystreams type for person
 type PersonExt =
   Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
+pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
 /// Activitystreams type for post
 pub type PageExt = Ext1<ApObject<Page>, PageExtension>;
 pub type NoteExt = ApObject<Note>;
@@ -170,10 +171,10 @@ pub trait ApubLikeableType {
 
 /// Common methods provided by ActivityPub actors (community and person). Not all methods are
 /// implemented by all actors.
-#[async_trait::async_trait(?Send)]
 pub trait ActorType {
   fn is_local(&self) -> bool;
   fn actor_id(&self) -> Url;
+  fn name(&self) -> String;
 
   // TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db)
   fn public_key(&self) -> Option<String>;
diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs
index 361da462..7b181eff 100644
--- a/crates/apub/src/objects/comment.rs
+++ b/crates/apub/src/objects/comment.rs
@@ -123,17 +123,7 @@ impl FromApub for Comment {
     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
     check_object_for_community_or_site_ban(note, post.community_id, context, request_counter)
       .await?;
-    if post.locked {
-      // This is not very efficient because a comment gets inserted just to be deleted right
-      // afterwards, but it seems to be the easiest way to implement it.
-      blocking(context.pool(), move |conn| {
-        Comment::delete(conn, comment.id)
-      })
-      .await??;
-      Err(anyhow!("Post is locked").into())
-    } else {
-      Ok(comment)
-    }
+    Ok(comment)
   }
 }
 
@@ -174,6 +164,9 @@ impl FromApubToForm<NoteExt> for CommentForm {
       request_counter,
     ))
     .await?;
+    if post.locked {
+      return Err(anyhow!("Post is locked").into());
+    }
 
     // The 2nd item, if it exists, is the parent comment apub_id
     // For deeply nested comments, FromApub automatically gets called recursively
diff --git a/crates/apub_lib/Cargo.toml b/crates/apub_lib/Cargo.toml
new file mode 100644
index 00000000..327670b5
--- /dev/null
+++ b/crates/apub_lib/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "lemmy_apub_lib"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+lemmy_utils = { path = "../utils" }
+lemmy_websocket = { path = "../websocket" }
+lemmy_apub_lib_derive = { path = "../apub_lib_derive" }
+activitystreams = "0.7.0-alpha.11"
+activitystreams-ext = "0.1.0-alpha.2"
+serde = { version = "1.0.123", features = ["derive"] }
+async-trait = "0.1.42"
+url = { version = "2.2.1", features = ["serde"] }
diff --git a/crates/apub_lib/src/lib.rs b/crates/apub_lib/src/lib.rs
new file mode 100644
index 00000000..66bba9f4
--- /dev/null
+++ b/crates/apub_lib/src/lib.rs
@@ -0,0 +1,72 @@
+use activitystreams::{
+  base::AnyBase,
+  error::DomainError,
+  primitives::OneOrMany,
+  unparsed::Unparsed,
+};
+pub use lemmy_apub_lib_derive::*;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
+pub enum PublicUrl {
+  #[serde(rename = "https://www.w3.org/ns/activitystreams#Public")]
+  Public,
+}
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ActivityCommonFields {
+  #[serde(rename = "@context")]
+  pub context: OneOrMany<AnyBase>,
+  id: Url,
+  pub actor: Url,
+
+  // unparsed fields
+  #[serde(flatten)]
+  pub unparsed: Unparsed,
+}
+
+impl ActivityCommonFields {
+  pub fn id_unchecked(&self) -> &Url {
+    &self.id
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+pub trait ActivityHandler {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError>;
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError>;
+  fn common(&self) -> &ActivityCommonFields;
+}
+
+pub fn verify_domains_match(a: &Url, b: &Url) -> Result<(), LemmyError> {
+  if a.domain() != b.domain() {
+    return Err(DomainError.into());
+  }
+  Ok(())
+}
+
+pub fn verify_domains_match_opt(a: &Url, b: Option<&Url>) -> Result<(), LemmyError> {
+  if let Some(b2) = b {
+    return verify_domains_match(a, b2);
+  }
+  Ok(())
+}
+
+pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), LemmyError> {
+  if a != b {
+    return Err(DomainError.into());
+  }
+  Ok(())
+}
diff --git a/crates/apub_lib_derive/Cargo.toml b/crates/apub_lib_derive/Cargo.toml
new file mode 100644
index 00000000..293a15b8
--- /dev/null
+++ b/crates/apub_lib_derive/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "lemmy_apub_lib_derive"
+version = "0.1.0"
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+[dev-dependencies]
+trybuild = { version = "1.0", features = ["diff"] }
+
+[dependencies]
+proc-macro2 = "1.0"
+syn = "1.0"
+quote = "1.0"
\ No newline at end of file
diff --git a/crates/apub_lib_derive/src/lib.rs b/crates/apub_lib_derive/src/lib.rs
new file mode 100644
index 00000000..f8750680
--- /dev/null
+++ b/crates/apub_lib_derive/src/lib.rs
@@ -0,0 +1,149 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, Data, DeriveInput};
+
+/// Generates implementation ActivityHandler for an enum, which looks like the following (handling
+/// all enum variants).
+///
+/// Based on this code:
+/// ```ignore
+/// #[derive(serde::Deserialize, serde::Serialize, ActivityHandler)]
+/// #[serde(untagged)]
+/// pub enum PersonInboxActivities {
+///  CreateNote(CreateNote),
+///  UpdateNote(UpdateNote),
+/// ```
+/// It will generate this:
+/// ```ignore
+/// impl ActivityHandler for PersonInboxActivities {
+///
+///     async fn verify(
+///     &self,
+///     context: &LemmyContext,
+///     request_counter: &mut i32,
+///   ) -> Result<(), LemmyError> {
+///     match self {
+///       PersonInboxActivities::CreateNote(a) => a.verify(context, request_counter).await,
+///       PersonInboxActivities::UpdateNote(a) => a.verify(context, request_counter).await,
+///     }
+///   }
+///
+///   async fn receive(
+///   &self,
+///   context: &LemmyContext,
+///   request_counter: &mut i32,
+/// ) -> Result<(), LemmyError> {
+///     match self {
+///       PersonInboxActivities::CreateNote(a) => a.receive(context, request_counter).await,
+///       PersonInboxActivities::UpdateNote(a) => a.receive(context, request_counter).await,
+///     }
+///   }
+/// fn common(&self) -> &ActivityCommonFields  {
+///     match self {
+///       PersonInboxActivities::CreateNote(a) => a.common(),
+///       PersonInboxActivities::UpdateNote(a) => a.common(),
+///     }
+///   }
+///
+/// ```
+///
+/// TODO: consider replacing this macro with https://crates.io/crates/typetag crate, though it
+///       doesnt support untagged enums which we need for apub.
+#[proc_macro_derive(ActivityHandler)]
+pub fn derive_activity_handler(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+  // Parse the input tokens into a syntax tree.
+  let input = parse_macro_input!(input as DeriveInput);
+
+  // Used in the quasi-quotation below as `#name`.
+  let name = input.ident;
+
+  let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+  let input_enum = if let Data::Enum(d) = input.data {
+    d
+  } else {
+    unimplemented!()
+  };
+
+  let impl_verify = input_enum
+    .variants
+    .iter()
+    .map(|variant| variant_impl_verify(&name, variant));
+  let impl_receive = input_enum
+    .variants
+    .iter()
+    .map(|variant| variant_impl_receive(&name, variant));
+  let impl_common = input_enum
+    .variants
+    .iter()
+    .map(|variant| variant_impl_common(&name, variant));
+
+  // The generated impl.
+  let expanded = quote! {
+      #[async_trait::async_trait(?Send)]
+      impl #impl_generics lemmy_apub_lib::ActivityHandler for #name #ty_generics #where_clause {
+          async fn verify(
+              &self,
+              context: &LemmyContext,
+              request_counter: &mut i32,
+            ) -> Result<(), LemmyError> {
+            match self {
+              #(#impl_verify)*
+            }
+          }
+          async fn receive(
+            &self,
+            context: &LemmyContext,
+            request_counter: &mut i32,
+          ) -> Result<(), LemmyError> {
+            match self {
+              #(#impl_receive)*
+            }
+          }
+          fn common(&self) -> &ActivityCommonFields {
+            match self {
+              #(#impl_common)*
+            }
+          }
+      }
+  };
+
+  // Hand the output tokens back to the compiler.
+  proc_macro::TokenStream::from(expanded)
+}
+
+fn variant_impl_common(name: &syn::Ident, variant: &syn::Variant) -> TokenStream {
+  let id = &variant.ident;
+  match &variant.fields {
+    syn::Fields::Unnamed(_) => {
+      quote! {
+        #name::#id(a) => a.common(),
+      }
+    }
+    _ => unimplemented!(),
+  }
+}
+
+fn variant_impl_verify(name: &syn::Ident, variant: &syn::Variant) -> TokenStream {
+  let id = &variant.ident;
+  match &variant.fields {
+    syn::Fields::Unnamed(_) => {
+      quote! {
+        #name::#id(a) => a.verify(context, request_counter).await,
+      }
+    }
+    _ => unimplemented!(),
+  }
+}
+
+fn variant_impl_receive(name: &syn::Ident, variant: &syn::Variant) -> TokenStream {
+  let id = &variant.ident;
+  match &variant.fields {
+    syn::Fields::Unnamed(_) => {
+      quote! {
+        #name::#id(a) => a.receive(context, request_counter).await,
+      }
+    }
+    _ => unimplemented!(),
+  }
+}
diff --git a/crates/apub_receive/Cargo.toml b/crates/apub_receive/Cargo.toml
index e62f8e49..ca21787c 100644
--- a/crates/apub_receive/Cargo.toml
+++ b/crates/apub_receive/Cargo.toml
@@ -5,6 +5,7 @@ edition = "2018"
 
 [dependencies]
 lemmy_utils = { path = "../utils" }
+lemmy_apub_lib = { path = "../apub_lib" }
 lemmy_apub = { path = "../apub" }
 lemmy_db_queries = { path = "../db_queries" }
 lemmy_db_schema = { path = "../db_schema" }
diff --git a/crates/apub_receive/src/activities/comment/create.rs b/crates/apub_receive/src/activities/comment/create.rs
new file mode 100644
index 00000000..7ce5d09a
--- /dev/null
+++ b/crates/apub_receive/src/activities/comment/create.rs
@@ -0,0 +1,68 @@
+use crate::activities::{
+  comment::{get_notif_recipients, send_websocket_message},
+  verify_activity,
+  verify_person_in_community,
+};
+use activitystreams::{activity::kind::CreateType, base::BaseExt};
+use lemmy_apub::{objects::FromApub, NoteExt};
+use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_schema::source::comment::Comment;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateComment {
+  to: PublicUrl,
+  object: NoteExt,
+  cc: Vec<Url>,
+  #[serde(rename = "type")]
+  kind: CreateType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for CreateComment {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+    // TODO: should add a check that the correct community is in cc (probably needs changes to
+    //       comment deserialization)
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let comment = Comment::from_apub(
+      &self.object,
+      context,
+      self.common.actor.clone(),
+      request_counter,
+      false,
+    )
+    .await?;
+    let recipients =
+      get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?;
+    send_websocket_message(
+      comment.id,
+      recipients,
+      UserOperationCrud::CreateComment,
+      context,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/comment/mod.rs b/crates/apub_receive/src/activities/comment/mod.rs
new file mode 100644
index 00000000..e5cdb4dd
--- /dev/null
+++ b/crates/apub_receive/src/activities/comment/mod.rs
@@ -0,0 +1,65 @@
+use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs};
+use lemmy_apub::fetcher::person::get_or_fetch_and_upsert_person;
+use lemmy_db_queries::Crud;
+use lemmy_db_schema::{
+  source::{comment::Comment, post::Post},
+  CommentId,
+  LocalUserId,
+};
+use lemmy_db_views::comment_view::CommentView;
+use lemmy_utils::{utils::scrape_text_for_mentions, LemmyError};
+use lemmy_websocket::{messages::SendComment, LemmyContext};
+use url::Url;
+
+pub mod create;
+pub mod update;
+
+async fn get_notif_recipients(
+  actor: &Url,
+  comment: &Comment,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<Vec<LocalUserId>, LemmyError> {
+  let post_id = comment.post_id;
+  let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+  let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
+
+  // Note:
+  // Although mentions could be gotten from the post tags (they are included there), or the ccs,
+  // Its much easier to scrape them from the comment body, since the API has to do that
+  // anyway.
+  // TODO: for compatibility with other projects, it would be much better to read this from cc or tags
+  let mentions = scrape_text_for_mentions(&comment.content);
+  send_local_notifs(mentions, comment.clone(), actor, post, context.pool(), true).await
+}
+
+// TODO: in many call sites we are setting an empty vec for recipient_ids, we should get the actual
+//       recipient actors from somewhere
+pub(crate) async fn send_websocket_message<
+  OP: ToString + Send + lemmy_websocket::OperationType + 'static,
+>(
+  comment_id: CommentId,
+  recipient_ids: Vec<LocalUserId>,
+  op: OP,
+  context: &LemmyContext,
+) -> Result<(), LemmyError> {
+  // Refetch the view
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  let res = CommentResponse {
+    comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op,
+    comment: res,
+    websocket_id: None,
+  });
+
+  Ok(())
+}
diff --git a/crates/apub_receive/src/activities/comment/remove.rs b/crates/apub_receive/src/activities/comment/remove.rs
new file mode 100644
index 00000000..53e69b65
--- /dev/null
+++ b/crates/apub_receive/src/activities/comment/remove.rs
@@ -0,0 +1,56 @@
+use crate::activities::{comment::send_websocket_message, verify_mod_action};
+use activitystreams::activity::kind::RemoveType;
+use lemmy_api_common::blocking;
+use lemmy_apub::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment};
+use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl};
+use lemmy_db_queries::source::comment::Comment_;
+use lemmy_db_schema::source::comment::Comment;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RemoveComment {
+  to: PublicUrl,
+  pub(in crate::activities::comment) object: Url,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: RemoveType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandlerNew for RemoveComment {
+  async fn verify(&self, context: &LemmyContext, _: &mut i32) -> Result<(), LemmyError> {
+    verify_domains_match(&self.common.actor, self.common.id_unchecked())?;
+    check_is_apub_id_valid(&self.common.actor, false)?;
+    verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let comment = get_or_fetch_and_insert_comment(&self.object, context, request_counter).await?;
+
+    let removed_comment = blocking(context.pool(), move |conn| {
+      Comment::update_removed(conn, comment.id, true)
+    })
+    .await??;
+
+    send_websocket_message(
+      removed_comment.id,
+      vec![],
+      UserOperationCrud::EditComment,
+      context,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/comment/undo_remove.rs b/crates/apub_receive/src/activities/comment/undo_remove.rs
new file mode 100644
index 00000000..444f3ce4
--- /dev/null
+++ b/crates/apub_receive/src/activities/comment/undo_remove.rs
@@ -0,0 +1,65 @@
+use crate::activities::{
+  comment::{remove::RemoveComment, send_websocket_message},
+  verify_mod_action,
+};
+use activitystreams::activity::kind::UndoType;
+use lemmy_api_common::blocking;
+use lemmy_apub::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment};
+use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl};
+use lemmy_db_queries::source::comment::Comment_;
+use lemmy_db_schema::source::comment::Comment;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UndoRemoveComment {
+  to: PublicUrl,
+  object: RemoveComment,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: UndoType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandlerNew for UndoRemoveComment {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_domains_match(&self.common.actor, self.common.id_unchecked())?;
+    check_is_apub_id_valid(&self.common.actor, false)?;
+    verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
+    self.object.verify(context, request_counter).await
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let comment =
+      get_or_fetch_and_insert_comment(&self.object.object, context, request_counter).await?;
+
+    let removed_comment = blocking(context.pool(), move |conn| {
+      Comment::update_removed(conn, comment.id, false)
+    })
+    .await??;
+
+    send_websocket_message(
+      removed_comment.id,
+      vec![],
+      UserOperationCrud::EditComment,
+      context,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/comment/update.rs b/crates/apub_receive/src/activities/comment/update.rs
new file mode 100644
index 00000000..c07837e3
--- /dev/null
+++ b/crates/apub_receive/src/activities/comment/update.rs
@@ -0,0 +1,67 @@
+use crate::activities::{
+  comment::{get_notif_recipients, send_websocket_message},
+  verify_activity,
+  verify_person_in_community,
+};
+use activitystreams::{activity::kind::UpdateType, base::BaseExt};
+use lemmy_apub::{objects::FromApub, NoteExt};
+use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_schema::source::comment::Comment;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateComment {
+  to: PublicUrl,
+  object: NoteExt,
+  cc: Vec<Url>,
+  #[serde(rename = "type")]
+  kind: UpdateType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UpdateComment {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let comment = Comment::from_apub(
+      &self.object,
+      context,
+      self.common.actor.clone(),
+      request_counter,
+      false,
+    )
+    .await?;
+
+    let recipients =
+      get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?;
+    send_websocket_message(
+      comment.id,
+      recipients,
+      UserOperationCrud::EditComment,
+      context,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/community/add_mod.rs b/crates/apub_receive/src/activities/community/add_mod.rs
new file mode 100644
index 00000000..2cf64f71
--- /dev/null
+++ b/crates/apub_receive/src/activities/community/add_mod.rs
@@ -0,0 +1,86 @@
+use crate::activities::{
+  verify_activity,
+  verify_add_remove_moderator_target,
+  verify_mod_action,
+  verify_person_in_community,
+};
+use activitystreams::{activity::kind::AddType, base::AnyBase};
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
+  CommunityType,
+};
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::{source::community::CommunityModerator_, Joinable};
+use lemmy_db_schema::source::community::{CommunityModerator, CommunityModeratorForm};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AddMod {
+  to: PublicUrl,
+  object: Url,
+  target: Url,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: AddType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for AddMod {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
+    verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let community =
+      get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?;
+    let new_mod = get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?;
+
+    // If we had to refetch the community while parsing the activity, then the new mod has already
+    // been added. Skip it here as it would result in a duplicate key error.
+    let new_mod_id = new_mod.id;
+    let moderated_communities = blocking(context.pool(), move |conn| {
+      CommunityModerator::get_person_moderated_communities(conn, new_mod_id)
+    })
+    .await??;
+    if !moderated_communities.contains(&community.id) {
+      let form = CommunityModeratorForm {
+        community_id: community.id,
+        person_id: new_mod.id,
+      };
+      blocking(context.pool(), move |conn| {
+        CommunityModerator::join(conn, &form)
+      })
+      .await??;
+    }
+    if community.local {
+      let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
+      community
+        .send_announce(anybase, Some(self.object.clone()), context)
+        .await?;
+    }
+    // TODO: send websocket notification about added mod
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/community/announce.rs b/crates/apub_receive/src/activities/community/announce.rs
new file mode 100644
index 00000000..09f51a7e
--- /dev/null
+++ b/crates/apub_receive/src/activities/community/announce.rs
@@ -0,0 +1,104 @@
+use crate::{
+  activities::{
+    comment::{create::CreateComment, update::UpdateComment},
+    community::{
+      add_mod::AddMod,
+      block_user::BlockUserFromCommunity,
+      undo_block_user::UndoBlockUserFromCommunity,
+    },
+    deletion::{
+      delete::DeletePostCommentOrCommunity,
+      undo_delete::UndoDeletePostCommentOrCommunity,
+    },
+    post::{create::CreatePost, update::UpdatePost},
+    removal::{
+      remove::RemovePostCommentCommunityOrMod,
+      undo_remove::UndoRemovePostCommentOrCommunity,
+    },
+    verify_activity,
+    verify_community,
+    voting::{
+      dislike::DislikePostOrComment,
+      like::LikePostOrComment,
+      undo_dislike::UndoDislikePostOrComment,
+      undo_like::UndoLikePostOrComment,
+    },
+  },
+  http::is_activity_already_known,
+};
+use activitystreams::activity::kind::AnnounceType;
+use lemmy_apub::insert_activity;
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
+#[serde(untagged)]
+pub enum AnnouncableActivities {
+  CreateComment(CreateComment),
+  UpdateComment(UpdateComment),
+  CreatePost(CreatePost),
+  UpdatePost(UpdatePost),
+  LikePostOrComment(LikePostOrComment),
+  DislikePostOrComment(DislikePostOrComment),
+  UndoLikePostOrComment(UndoLikePostOrComment),
+  UndoDislikePostOrComment(UndoDislikePostOrComment),
+  DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
+  UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
+  RemovePostCommentCommunityOrMod(RemovePostCommentCommunityOrMod),
+  UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity),
+  BlockUserFromCommunity(BlockUserFromCommunity),
+  UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
+  AddMod(AddMod),
+}
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AnnounceActivity {
+  to: PublicUrl,
+  object: AnnouncableActivities,
+  cc: Vec<Url>,
+  #[serde(rename = "type")]
+  kind: AnnounceType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for AnnounceActivity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_community(&self.common.actor, context, request_counter).await?;
+    self.object.verify(context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    if is_activity_already_known(context.pool(), self.object.common().id_unchecked()).await? {
+      return Ok(());
+    }
+    insert_activity(
+      self.object.common().id_unchecked(),
+      self.object.clone(),
+      false,
+      true,
+      context.pool(),
+    )
+    .await?;
+    self.object.receive(context, request_counter).await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/community/block_user.rs b/crates/apub_receive/src/activities/community/block_user.rs
new file mode 100644
index 00000000..03a19e9c
--- /dev/null
+++ b/crates/apub_receive/src/activities/community/block_user.rs
@@ -0,0 +1,83 @@
+use crate::activities::{verify_activity, verify_mod_action, verify_person_in_community};
+use activitystreams::activity::kind::BlockType;
+use lemmy_api_common::blocking;
+use lemmy_apub::fetcher::{
+  community::get_or_fetch_and_upsert_community,
+  person::get_or_fetch_and_upsert_person,
+};
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::{Bannable, Followable};
+use lemmy_db_schema::source::community::{
+  CommunityFollower,
+  CommunityFollowerForm,
+  CommunityPersonBan,
+  CommunityPersonBanForm,
+};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BlockUserFromCommunity {
+  to: PublicUrl,
+  pub(in crate::activities::community) object: Url,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: BlockType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for BlockUserFromCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let community =
+      get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?;
+    let blocked_user =
+      get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?;
+
+    let community_user_ban_form = CommunityPersonBanForm {
+      community_id: community.id,
+      person_id: blocked_user.id,
+    };
+
+    blocking(context.pool(), move |conn: &'_ _| {
+      CommunityPersonBan::ban(conn, &community_user_ban_form)
+    })
+    .await??;
+
+    // Also unsubscribe them from the community, if they are subscribed
+    let community_follower_form = CommunityFollowerForm {
+      community_id: community.id,
+      person_id: blocked_user.id,
+      pending: false,
+    };
+    blocking(context.pool(), move |conn: &'_ _| {
+      CommunityFollower::unfollow(conn, &community_follower_form)
+    })
+    .await?
+    .ok();
+
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/community/mod.rs b/crates/apub_receive/src/activities/community/mod.rs
new file mode 100644
index 00000000..81152d92
--- /dev/null
+++ b/crates/apub_receive/src/activities/community/mod.rs
@@ -0,0 +1,35 @@
+use lemmy_api_common::{blocking, community::CommunityResponse};
+use lemmy_db_schema::CommunityId;
+use lemmy_db_views_actor::community_view::CommunityView;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext};
+
+pub mod add_mod;
+pub mod announce;
+pub mod block_user;
+pub mod undo_block_user;
+pub mod update;
+
+pub(crate) async fn send_websocket_message<
+  OP: ToString + Send + lemmy_websocket::OperationType + 'static,
+>(
+  community_id: CommunityId,
+  op: OP,
+  context: &LemmyContext,
+) -> Result<(), LemmyError> {
+  let community_view = blocking(context.pool(), move |conn| {
+    CommunityView::read(conn, community_id, None)
+  })
+  .await??;
+
+  let res = CommunityResponse { community_view };
+
+  context.chat_server().do_send(SendCommunityRoomMessage {
+    op,
+    response: res,
+    community_id,
+    websocket_id: None,
+  });
+
+  Ok(())
+}
diff --git a/crates/apub_receive/src/activities/community/undo_block_user.rs b/crates/apub_receive/src/activities/community/undo_block_user.rs
new file mode 100644
index 00000000..88518385
--- /dev/null
+++ b/crates/apub_receive/src/activities/community/undo_block_user.rs
@@ -0,0 +1,72 @@
+use crate::activities::{
+  community::block_user::BlockUserFromCommunity,
+  verify_activity,
+  verify_mod_action,
+  verify_person_in_community,
+};
+use activitystreams::activity::kind::UndoType;
+use lemmy_api_common::blocking;
+use lemmy_apub::fetcher::{
+  community::get_or_fetch_and_upsert_community,
+  person::get_or_fetch_and_upsert_person,
+};
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::Bannable;
+use lemmy_db_schema::source::community::{CommunityPersonBan, CommunityPersonBanForm};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UndoBlockUserFromCommunity {
+  to: PublicUrl,
+  object: BlockUserFromCommunity,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: UndoType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UndoBlockUserFromCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
+    self.object.verify(context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let community =
+      get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?;
+    let blocked_user =
+      get_or_fetch_and_upsert_person(&self.object.object, context, request_counter).await?;
+
+    let community_user_ban_form = CommunityPersonBanForm {
+      community_id: community.id,
+      person_id: blocked_user.id,
+    };
+
+    blocking(context.pool(), move |conn: &'_ _| {
+      CommunityPersonBan::unban(conn, &community_user_ban_form)
+    })
+    .await??;
+
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/community/update.rs b/crates/apub_receive/src/activities/community/update.rs
new file mode 100644
index 00000000..a333c1bb
--- /dev/null
+++ b/crates/apub_receive/src/activities/community/update.rs
@@ -0,0 +1,89 @@
+use crate::activities::{
+  community::send_websocket_message,
+  verify_activity,
+  verify_mod_action,
+  verify_person_in_community,
+};
+use activitystreams::activity::kind::UpdateType;
+use lemmy_api_common::blocking;
+use lemmy_apub::{objects::FromApubToForm, GroupExt};
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::{ApubObject, Crud};
+use lemmy_db_schema::source::community::{Community, CommunityForm};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+/// This activity is received from a remote community mod, and updates the description or other
+/// fields of a local community.
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateCommunity {
+  to: PublicUrl,
+  object: GroupExt,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: UpdateType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UpdateCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let cc = self.cc[0].clone().into();
+    let community = blocking(context.pool(), move |conn| {
+      Community::read_from_apub_id(conn, &cc)
+    })
+    .await??;
+
+    let updated_community = CommunityForm::from_apub(
+      &self.object,
+      context,
+      community.actor_id.clone().into(),
+      request_counter,
+      false,
+    )
+    .await?;
+    let cf = CommunityForm {
+      name: updated_community.name,
+      title: updated_community.title,
+      description: updated_community.description,
+      nsfw: updated_community.nsfw,
+      // TODO: icon and banner would be hosted on the other instance, ideally we would copy it to ours
+      icon: updated_community.icon,
+      banner: updated_community.banner,
+      ..CommunityForm::default()
+    };
+    let updated_community = blocking(context.pool(), move |conn| {
+      Community::update(conn, community.id, &cf)
+    })
+    .await??;
+
+    send_websocket_message(
+      updated_community.id,
+      UserOperationCrud::EditCommunity,
+      context,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/deletion/delete.rs b/crates/apub_receive/src/activities/deletion/delete.rs
new file mode 100644
index 00000000..0276516d
--- /dev/null
+++ b/crates/apub_receive/src/activities/deletion/delete.rs
@@ -0,0 +1,158 @@
+use crate::activities::{
+  comment::send_websocket_message as send_comment_message,
+  community::send_websocket_message as send_community_message,
+  post::send_websocket_message as send_post_message,
+  verify_activity,
+  verify_mod_action,
+  verify_person_in_community,
+};
+use activitystreams::activity::kind::DeleteType;
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  fetcher::{
+    community::get_or_fetch_and_upsert_community,
+    objects::get_or_fetch_and_insert_post_or_comment,
+    person::get_or_fetch_and_upsert_person,
+  },
+  ActorType,
+  CommunityType,
+  PostOrComment,
+};
+use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::{
+  source::{comment::Comment_, community::Community_, post::Post_},
+  Crud,
+};
+use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+/// This is very confusing, because there are four distinct cases to handle:
+/// - user deletes their post
+/// - user deletes their comment
+/// - remote community mod deletes local community
+/// - remote community deletes itself (triggered by a mod)
+///
+/// TODO: we should probably change how community deletions work to simplify this. Probably by
+/// wrapping it in an announce just like other activities, instead of having the community send it.
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DeletePostCommentOrCommunity {
+  to: PublicUrl,
+  pub(in crate::activities::deletion) object: Url,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: DeleteType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for DeletePostCommentOrCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    let object_community =
+      get_or_fetch_and_upsert_community(&self.object, context, request_counter).await;
+    // deleting a community (set counter 0 to only fetch from local db)
+    if object_community.is_ok() {
+      verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
+    }
+    // deleting a post or comment
+    else {
+      verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?;
+      let object_creator =
+        get_post_or_comment_actor_id(&self.object, context, request_counter).await?;
+      verify_urls_match(&self.common.actor, &object_creator)?;
+    }
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let object_community =
+      get_or_fetch_and_upsert_community(&self.object, context, request_counter).await;
+    // deleting a community
+    if let Ok(community) = object_community {
+      if community.local {
+        // repeat these checks just to be sure
+        verify_person_in_community(&self.common().actor, &self.cc, context, request_counter)
+          .await?;
+        verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
+        let mod_ =
+          get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
+        community.send_delete(mod_, context).await?;
+      }
+      let deleted_community = blocking(context.pool(), move |conn| {
+        Community::update_deleted(conn, community.id, true)
+      })
+      .await??;
+
+      send_community_message(
+        deleted_community.id,
+        UserOperationCrud::DeleteCommunity,
+        context,
+      )
+      .await
+    }
+    // deleting a post or comment
+    else {
+      match get_or_fetch_and_insert_post_or_comment(&self.object, context, request_counter).await? {
+        PostOrComment::Post(post) => {
+          let deleted_post = blocking(context.pool(), move |conn| {
+            Post::update_deleted(conn, post.id, true)
+          })
+          .await??;
+          send_post_message(deleted_post.id, UserOperationCrud::EditPost, context).await
+        }
+        PostOrComment::Comment(comment) => {
+          let deleted_comment = blocking(context.pool(), move |conn| {
+            Comment::update_deleted(conn, comment.id, true)
+          })
+          .await??;
+          send_comment_message(
+            deleted_comment.id,
+            vec![],
+            UserOperationCrud::EditComment,
+            context,
+          )
+          .await
+        }
+      }
+    }
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
+
+async fn get_post_or_comment_actor_id(
+  object: &Url,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<Url, LemmyError> {
+  let actor_id =
+    match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? {
+      PostOrComment::Post(post) => {
+        let creator_id = post.creator_id;
+        blocking(context.pool(), move |conn| Person::read(conn, creator_id))
+          .await??
+          .actor_id()
+      }
+      PostOrComment::Comment(comment) => {
+        let creator_id = comment.creator_id;
+        blocking(context.pool(), move |conn| Person::read(conn, creator_id))
+          .await??
+          .actor_id()
+      }
+    };
+  Ok(actor_id)
+}
diff --git a/crates/apub_receive/src/activities/deletion/mod.rs b/crates/apub_receive/src/activities/deletion/mod.rs
new file mode 100644
index 00000000..b440edd6
--- /dev/null
+++ b/crates/apub_receive/src/activities/deletion/mod.rs
@@ -0,0 +1,2 @@
+pub mod delete;
+pub mod undo_delete;
diff --git a/crates/apub_receive/src/activities/deletion/undo_delete.rs b/crates/apub_receive/src/activities/deletion/undo_delete.rs
new file mode 100644
index 00000000..9875a2a9
--- /dev/null
+++ b/crates/apub_receive/src/activities/deletion/undo_delete.rs
@@ -0,0 +1,125 @@
+use crate::activities::{
+  comment::send_websocket_message as send_comment_message,
+  community::send_websocket_message as send_community_message,
+  deletion::delete::DeletePostCommentOrCommunity,
+  post::send_websocket_message as send_post_message,
+  verify_activity,
+  verify_mod_action,
+  verify_person_in_community,
+};
+use activitystreams::activity::kind::UndoType;
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  fetcher::{
+    community::get_or_fetch_and_upsert_community,
+    objects::get_or_fetch_and_insert_post_or_comment,
+    person::get_or_fetch_and_upsert_person,
+  },
+  CommunityType,
+  PostOrComment,
+};
+use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_};
+use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UndoDeletePostCommentOrCommunity {
+  to: PublicUrl,
+  object: DeletePostCommentOrCommunity,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: UndoType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UndoDeletePostCommentOrCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    self.object.verify(context, request_counter).await?;
+    let object_community =
+      get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await;
+    // restoring a community
+    if object_community.is_ok() {
+      verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
+    }
+    // restoring a post or comment
+    else {
+      verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?;
+      verify_urls_match(&self.common.actor, &self.object.common().actor)?;
+    }
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let object_community =
+      get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await;
+    // restoring a community
+    if let Ok(community) = object_community {
+      if community.local {
+        // repeat these checks just to be sure
+        verify_person_in_community(&self.common().actor, &self.cc, context, request_counter)
+          .await?;
+        verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
+        let mod_ =
+          get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
+        community.send_undo_delete(mod_, context).await?;
+      }
+      let deleted_community = blocking(context.pool(), move |conn| {
+        Community::update_deleted(conn, community.id, false)
+      })
+      .await??;
+
+      send_community_message(
+        deleted_community.id,
+        UserOperationCrud::EditCommunity,
+        context,
+      )
+      .await
+    }
+    // restoring a post or comment
+    else {
+      match get_or_fetch_and_insert_post_or_comment(&self.object.object, context, request_counter)
+        .await?
+      {
+        PostOrComment::Post(post) => {
+          let deleted_post = blocking(context.pool(), move |conn| {
+            Post::update_deleted(conn, post.id, false)
+          })
+          .await??;
+          send_post_message(deleted_post.id, UserOperationCrud::EditPost, context).await
+        }
+        PostOrComment::Comment(comment) => {
+          let deleted_comment = blocking(context.pool(), move |conn| {
+            Comment::update_deleted(conn, comment.id, false)
+          })
+          .await??;
+          send_comment_message(
+            deleted_comment.id,
+            vec![],
+            UserOperationCrud::EditComment,
+            context,
+          )
+          .await
+        }
+      }
+    }
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/following/accept.rs b/crates/apub_receive/src/activities/following/accept.rs
new file mode 100644
index 00000000..d5cf474d
--- /dev/null
+++ b/crates/apub_receive/src/activities/following/accept.rs
@@ -0,0 +1,62 @@
+use crate::activities::{following::follow::FollowCommunity, verify_activity, verify_community};
+use activitystreams::activity::kind::AcceptType;
+use lemmy_api_common::blocking;
+use lemmy_apub::fetcher::{
+  community::get_or_fetch_and_upsert_community,
+  person::get_or_fetch_and_upsert_person,
+};
+use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
+use lemmy_db_queries::Followable;
+use lemmy_db_schema::source::community::CommunityFollower;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AcceptFollowCommunity {
+  to: Url,
+  object: FollowCommunity,
+  #[serde(rename = "type")]
+  kind: AcceptType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+/// Handle accepted follows
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for AcceptFollowCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_urls_match(&self.to, &self.object.common.actor)?;
+    verify_urls_match(&self.common.actor, &self.object.to)?;
+    verify_community(&self.common.actor, context, request_counter).await?;
+    self.object.verify(context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let actor =
+      get_or_fetch_and_upsert_community(&self.common.actor, context, request_counter).await?;
+    let to = get_or_fetch_and_upsert_person(&self.to, context, request_counter).await?;
+    // This will throw an error if no follow was requested
+    blocking(context.pool(), move |conn| {
+      CommunityFollower::follow_accepted(conn, actor.id, to.id)
+    })
+    .await??;
+
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/following/follow.rs b/crates/apub_receive/src/activities/following/follow.rs
new file mode 100644
index 00000000..fd0649f1
--- /dev/null
+++ b/crates/apub_receive/src/activities/following/follow.rs
@@ -0,0 +1,73 @@
+use crate::activities::{verify_activity, verify_person};
+use activitystreams::{
+  activity::{kind::FollowType, Follow},
+  base::{AnyBase, ExtendsExt},
+};
+use anyhow::Context;
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
+  CommunityType,
+};
+use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
+use lemmy_db_queries::Followable;
+use lemmy_db_schema::source::community::{CommunityFollower, CommunityFollowerForm};
+use lemmy_utils::{location_info, LemmyError};
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FollowCommunity {
+  pub(in crate::activities::following) to: Url,
+  pub(in crate::activities::following) object: Url,
+  #[serde(rename = "type")]
+  kind: FollowType,
+  #[serde(flatten)]
+  pub(in crate::activities::following) common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for FollowCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_urls_match(&self.to, &self.object)?;
+    verify_person(&self.common.actor, context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let actor =
+      get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
+    let community =
+      get_or_fetch_and_upsert_community(&self.object, context, request_counter).await?;
+    let community_follower_form = CommunityFollowerForm {
+      community_id: community.id,
+      person_id: actor.id,
+      pending: false,
+    };
+
+    // This will fail if they're already a follower, but ignore the error.
+    blocking(context.pool(), move |conn| {
+      CommunityFollower::follow(conn, &community_follower_form).ok()
+    })
+    .await?;
+
+    // TODO: avoid the conversion and pass our own follow struct directly
+    let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
+    let anybase = Follow::from_any_base(anybase)?.context(location_info!())?;
+    community.send_accept_follow(anybase, context).await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/following/mod.rs b/crates/apub_receive/src/activities/following/mod.rs
new file mode 100644
index 00000000..050c3691
--- /dev/null
+++ b/crates/apub_receive/src/activities/following/mod.rs
@@ -0,0 +1,3 @@
+pub mod accept;
+pub mod follow;
+pub mod undo;
diff --git a/crates/apub_receive/src/activities/following/undo.rs b/crates/apub_receive/src/activities/following/undo.rs
new file mode 100644
index 00000000..1f3d12e6
--- /dev/null
+++ b/crates/apub_receive/src/activities/following/undo.rs
@@ -0,0 +1,67 @@
+use crate::activities::{following::follow::FollowCommunity, verify_activity, verify_person};
+use activitystreams::activity::kind::UndoType;
+use lemmy_api_common::blocking;
+use lemmy_apub::fetcher::{
+  community::get_or_fetch_and_upsert_community,
+  person::get_or_fetch_and_upsert_person,
+};
+use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler};
+use lemmy_db_queries::Followable;
+use lemmy_db_schema::source::community::{CommunityFollower, CommunityFollowerForm};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UndoFollowCommunity {
+  to: Url,
+  object: FollowCommunity,
+  #[serde(rename = "type")]
+  kind: UndoType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UndoFollowCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_urls_match(&self.to, &self.object.object)?;
+    verify_urls_match(&self.common.actor, &self.object.common.actor)?;
+    verify_person(&self.common.actor, context, request_counter).await?;
+    self.object.verify(context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let actor =
+      get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
+    let community = get_or_fetch_and_upsert_community(&self.to, context, request_counter).await?;
+
+    let community_follower_form = CommunityFollowerForm {
+      community_id: community.id,
+      person_id: actor.id,
+      pending: false,
+    };
+
+    // This will fail if they aren't a follower, but ignore the error.
+    blocking(context.pool(), move |conn| {
+      CommunityFollower::unfollow(conn, &community_follower_form).ok()
+    })
+    .await?;
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/mod.rs b/crates/apub_receive/src/activities/mod.rs
index cbdeafaf..098d209f 100644
--- a/crates/apub_receive/src/activities/mod.rs
+++ b/crates/apub_receive/src/activities/mod.rs
@@ -1 +1,121 @@
-pub(crate) mod receive;
+use anyhow::anyhow;
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  check_community_or_site_ban,
+  check_is_apub_id_valid,
+  fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
+  generate_moderators_url,
+};
+use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields};
+use lemmy_db_queries::ApubObject;
+use lemmy_db_schema::{
+  source::{community::Community, person::Person},
+  DbUrl,
+};
+use lemmy_db_views_actor::community_view::CommunityView;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+pub mod comment;
+pub mod community;
+pub mod deletion;
+pub mod following;
+pub mod post;
+pub mod private_message;
+pub mod removal;
+pub mod voting;
+
+/// Checks that the specified Url actually identifies a Person (by fetching it), and that the person
+/// doesn't have a site ban.
+async fn verify_person(
+  person_id: &Url,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
+  if person.banned {
+    return Err(anyhow!("Person {} is banned", person_id).into());
+  }
+  Ok(())
+}
+
+/// Fetches the person and community to verify their type, then checks if person is banned from site
+/// or community.
+async fn verify_person_in_community(
+  person_id: &Url,
+  cc: &[Url],
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<Community, LemmyError> {
+  let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
+  let mut cc_iter = cc.iter();
+  let community: Community = loop {
+    if let Some(cid) = cc_iter.next() {
+      if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await {
+        break c;
+      }
+    } else {
+      return Err(anyhow!("No community found in cc").into());
+    }
+  };
+  check_community_or_site_ban(&person, community.id, context.pool()).await?;
+  Ok(community)
+}
+
+/// Simply check that the url actually refers to a valid group.
+async fn verify_community(
+  community_id: &Url,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  get_or_fetch_and_upsert_community(community_id, context, request_counter).await?;
+  Ok(())
+}
+
+fn verify_activity(common: &ActivityCommonFields) -> Result<(), LemmyError> {
+  check_is_apub_id_valid(&common.actor, false)?;
+  verify_domains_match(common.id_unchecked(), &common.actor)?;
+  Ok(())
+}
+
+async fn verify_mod_action(
+  actor_id: &Url,
+  activity_cc: Url,
+  context: &LemmyContext,
+) -> Result<(), LemmyError> {
+  let community = blocking(context.pool(), move |conn| {
+    Community::read_from_apub_id(conn, &activity_cc.into())
+  })
+  .await??;
+
+  if community.local {
+    let actor_id: DbUrl = actor_id.clone().into();
+    let actor = blocking(context.pool(), move |conn| {
+      Person::read_from_apub_id(conn, &actor_id)
+    })
+    .await??;
+
+    // Note: this will also return true for admins in addition to mods, but as we dont know about
+    //       remote admins, it doesnt make any difference.
+    let community_id = community.id;
+    let actor_id = actor.id;
+    let is_mod_or_admin = blocking(context.pool(), move |conn| {
+      CommunityView::is_mod_or_admin(conn, actor_id, community_id)
+    })
+    .await?;
+    if !is_mod_or_admin {
+      return Err(anyhow!("Not a mod").into());
+    }
+  }
+  Ok(())
+}
+
+/// For Add/Remove community moderator activities, check that the target field actually contains
+/// /c/community/moderators. Any different values are unsupported.
+fn verify_add_remove_moderator_target(target: &Url, community: Url) -> Result<(), LemmyError> {
+  if target != &generate_moderators_url(&community.into())?.into_inner() {
+    return Err(anyhow!("Unkown target url").into());
+  }
+  Ok(())
+}
diff --git a/crates/apub_receive/src/activities/post/create.rs b/crates/apub_receive/src/activities/post/create.rs
new file mode 100644
index 00000000..2d31fa0d
--- /dev/null
+++ b/crates/apub_receive/src/activities/post/create.rs
@@ -0,0 +1,66 @@
+use crate::activities::{
+  post::send_websocket_message,
+  verify_activity,
+  verify_person_in_community,
+};
+use activitystreams::{activity::kind::CreateType, base::BaseExt};
+use lemmy_apub::{
+  fetcher::person::get_or_fetch_and_upsert_person,
+  objects::FromApub,
+  ActorType,
+  PageExt,
+};
+use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_schema::source::post::Post;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreatePost {
+  to: PublicUrl,
+  object: PageExt,
+  cc: Vec<Url>,
+  #[serde(rename = "type")]
+  kind: CreateType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for CreatePost {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let actor =
+      get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
+    let post = Post::from_apub(
+      &self.object,
+      context,
+      actor.actor_id(),
+      request_counter,
+      false,
+    )
+    .await?;
+
+    send_websocket_message(post.id, UserOperationCrud::CreatePost, context).await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/post/mod.rs b/crates/apub_receive/src/activities/post/mod.rs
new file mode 100644
index 00000000..a4662040
--- /dev/null
+++ b/crates/apub_receive/src/activities/post/mod.rs
@@ -0,0 +1,31 @@
+use lemmy_api_common::{blocking, post::PostResponse};
+use lemmy_db_schema::PostId;
+use lemmy_db_views::post_view::PostView;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{messages::SendPost, LemmyContext};
+
+pub mod create;
+pub mod update;
+
+pub(crate) async fn send_websocket_message<
+  OP: ToString + Send + lemmy_websocket::OperationType + 'static,
+>(
+  post_id: PostId,
+  op: OP,
+  context: &LemmyContext,
+) -> Result<(), LemmyError> {
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post_view };
+
+  context.chat_server().do_send(SendPost {
+    op,
+    post: res,
+    websocket_id: None,
+  });
+
+  Ok(())
+}
diff --git a/crates/apub_receive/src/activities/post/update.rs b/crates/apub_receive/src/activities/post/update.rs
new file mode 100644
index 00000000..1f592492
--- /dev/null
+++ b/crates/apub_receive/src/activities/post/update.rs
@@ -0,0 +1,96 @@
+use crate::activities::{
+  post::send_websocket_message,
+  verify_activity,
+  verify_mod_action,
+  verify_person_in_community,
+};
+use activitystreams::{activity::kind::UpdateType, base::BaseExt};
+use anyhow::Context;
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  objects::{FromApub, FromApubToForm},
+  ActorType,
+  PageExt,
+};
+use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::ApubObject;
+use lemmy_db_schema::{
+  source::post::{Post, PostForm},
+  DbUrl,
+};
+use lemmy_utils::{location_info, LemmyError};
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdatePost {
+  to: PublicUrl,
+  object: PageExt,
+  cc: Vec<Url>,
+  #[serde(rename = "type")]
+  kind: UpdateType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UpdatePost {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    let community =
+      verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+
+    let temp_post = PostForm::from_apub(
+      &self.object,
+      context,
+      self.common.actor.clone(),
+      request_counter,
+      true,
+    )
+    .await?;
+    let post_id: DbUrl = temp_post.ap_id.context(location_info!())?;
+    let old_post = blocking(context.pool(), move |conn| {
+      Post::read_from_apub_id(conn, &post_id)
+    })
+    .await??;
+    let stickied = temp_post.stickied.context(location_info!())?;
+    let locked = temp_post.locked.context(location_info!())?;
+    // community mod changed locked/sticky status
+    if (stickied != old_post.stickied) || (locked != old_post.locked) {
+      verify_mod_action(&self.common.actor, community.actor_id(), context).await?;
+    }
+    // user edited their own post
+    else {
+      verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+    }
+
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let post = Post::from_apub(
+      &self.object,
+      context,
+      self.common.actor.clone(),
+      request_counter,
+      // TODO: we already check here if the mod action is valid, can remove that check param
+      true,
+    )
+    .await?;
+
+    send_websocket_message(post.id, UserOperationCrud::EditPost, context).await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/private_message/create.rs b/crates/apub_receive/src/activities/private_message/create.rs
new file mode 100644
index 00000000..2d766565
--- /dev/null
+++ b/crates/apub_receive/src/activities/private_message/create.rs
@@ -0,0 +1,61 @@
+use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person};
+use activitystreams::{activity::kind::CreateType, base::BaseExt};
+use lemmy_apub::{objects::FromApub, NoteExt};
+use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler};
+use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreatePrivateMessage {
+  to: Url,
+  object: NoteExt,
+  #[serde(rename = "type")]
+  kind: CreateType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for CreatePrivateMessage {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person(&self.common.actor, context, request_counter).await?;
+    verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let private_message = PrivateMessage::from_apub(
+      &self.object,
+      context,
+      self.common.actor.clone(),
+      request_counter,
+      false,
+    )
+    .await?;
+
+    send_websocket_message(
+      private_message.id,
+      UserOperationCrud::CreatePrivateMessage,
+      context,
+    )
+    .await?;
+
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/private_message/delete.rs b/crates/apub_receive/src/activities/private_message/delete.rs
new file mode 100644
index 00000000..b7f1a3cd
--- /dev/null
+++ b/crates/apub_receive/src/activities/private_message/delete.rs
@@ -0,0 +1,63 @@
+use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person};
+use activitystreams::activity::kind::DeleteType;
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandler};
+use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject};
+use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DeletePrivateMessage {
+  to: Url,
+  pub(in crate::activities::private_message) object: Url,
+  #[serde(rename = "type")]
+  kind: DeleteType,
+  #[serde(flatten)]
+  pub(in crate::activities::private_message) common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for DeletePrivateMessage {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person(&self.common.actor, context, request_counter).await?;
+    verify_domains_match(&self.common.actor, &self.object)?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    _request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let ap_id = self.object.clone();
+    let private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::read_from_apub_id(conn, &ap_id.into())
+    })
+    .await??;
+    let deleted_private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::update_deleted(conn, private_message.id, true)
+    })
+    .await??;
+
+    send_websocket_message(
+      deleted_private_message.id,
+      UserOperationCrud::DeletePrivateMessage,
+      context,
+    )
+    .await?;
+
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/private_message/mod.rs b/crates/apub_receive/src/activities/private_message/mod.rs
new file mode 100644
index 00000000..beb28b29
--- /dev/null
+++ b/crates/apub_receive/src/activities/private_message/mod.rs
@@ -0,0 +1,42 @@
+use lemmy_api_common::{blocking, person::PrivateMessageResponse};
+use lemmy_db_schema::PrivateMessageId;
+use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud};
+
+pub mod create;
+pub mod delete;
+pub mod undo_delete;
+pub mod update;
+
+async fn send_websocket_message(
+  private_message_id: PrivateMessageId,
+  op: UserOperationCrud,
+  context: &LemmyContext,
+) -> Result<(), LemmyError> {
+  let message = blocking(context.pool(), move |conn| {
+    PrivateMessageView::read(conn, private_message_id)
+  })
+  .await??;
+  let res = PrivateMessageResponse {
+    private_message_view: message,
+  };
+
+  // Send notifications to the local recipient, if one exists
+  let recipient_id = res.private_message_view.recipient.id;
+  let local_recipient_id = blocking(context.pool(), move |conn| {
+    LocalUserView::read_person(conn, recipient_id)
+  })
+  .await??
+  .local_user
+  .id;
+
+  context.chat_server().do_send(SendUserRoomMessage {
+    op,
+    response: res,
+    local_recipient_id,
+    websocket_id: None,
+  });
+
+  Ok(())
+}
diff --git a/crates/apub_receive/src/activities/private_message/undo_delete.rs b/crates/apub_receive/src/activities/private_message/undo_delete.rs
new file mode 100644
index 00000000..dc297b5a
--- /dev/null
+++ b/crates/apub_receive/src/activities/private_message/undo_delete.rs
@@ -0,0 +1,75 @@
+use crate::activities::{
+  private_message::{delete::DeletePrivateMessage, send_websocket_message},
+  verify_activity,
+  verify_person,
+};
+use activitystreams::activity::kind::UndoType;
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+  verify_domains_match,
+  verify_urls_match,
+  ActivityCommonFields,
+  ActivityHandler,
+};
+use lemmy_db_queries::{source::private_message::PrivateMessage_, ApubObject};
+use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UndoDeletePrivateMessage {
+  to: Url,
+  object: DeletePrivateMessage,
+  #[serde(rename = "type")]
+  kind: UndoType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UndoDeletePrivateMessage {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person(&self.common.actor, context, request_counter).await?;
+    verify_urls_match(&self.common.actor, &self.object.common.actor)?;
+    verify_domains_match(&self.common.actor, &self.object.object)?;
+    self.object.verify(context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    _request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let ap_id = self.object.object.clone();
+    let private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::read_from_apub_id(conn, &ap_id.into())
+    })
+    .await??;
+
+    let deleted_private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::update_deleted(conn, private_message.id, false)
+    })
+    .await??;
+
+    send_websocket_message(
+      deleted_private_message.id,
+      UserOperationCrud::EditPrivateMessage,
+      context,
+    )
+    .await?;
+
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/private_message/update.rs b/crates/apub_receive/src/activities/private_message/update.rs
new file mode 100644
index 00000000..56a79f8e
--- /dev/null
+++ b/crates/apub_receive/src/activities/private_message/update.rs
@@ -0,0 +1,61 @@
+use crate::activities::{private_message::send_websocket_message, verify_activity, verify_person};
+use activitystreams::{activity::kind::UpdateType, base::BaseExt};
+use lemmy_apub::{objects::FromApub, NoteExt};
+use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler};
+use lemmy_db_schema::source::private_message::PrivateMessage;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdatePrivateMessage {
+  to: Url,
+  object: NoteExt,
+  #[serde(rename = "type")]
+  kind: UpdateType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UpdatePrivateMessage {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person(&self.common.actor, context, request_counter).await?;
+    verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let private_message = PrivateMessage::from_apub(
+      &self.object,
+      context,
+      self.common.actor.clone(),
+      request_counter,
+      false,
+    )
+    .await?;
+
+    send_websocket_message(
+      private_message.id,
+      UserOperationCrud::EditPrivateMessage,
+      context,
+    )
+    .await?;
+
+    Ok(())
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/receive/comment.rs b/crates/apub_receive/src/activities/receive/comment.rs
deleted file mode 100644
index 18508fa2..00000000
--- a/crates/apub_receive/src/activities/receive/comment.rs
+++ /dev/null
@@ -1,262 +0,0 @@
-use crate::activities::receive::get_actor_as_person;
-use activitystreams::{
-  activity::{ActorAndObjectRefExt, Create, Dislike, Like, Update},
-  base::ExtendsExt,
-};
-use anyhow::Context;
-use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs};
-use lemmy_apub::{objects::FromApub, ActorType, NoteExt};
-use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable};
-use lemmy_db_schema::source::{
-  comment::{Comment, CommentLike, CommentLikeForm},
-  post::Post,
-};
-use lemmy_db_views::comment_view::CommentView;
-use lemmy_utils::{location_info, utils::scrape_text_for_mentions, LemmyError};
-use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation, UserOperationCrud};
-
-pub(crate) async fn receive_create_comment(
-  create: Create,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(&create, context, request_counter).await?;
-  let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-
-  let comment =
-    Comment::from_apub(&note, context, person.actor_id(), request_counter, false).await?;
-
-  let post_id = comment.post_id;
-  let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-
-  // Note:
-  // Although mentions could be gotten from the post tags (they are included there), or the ccs,
-  // Its much easier to scrape them from the comment body, since the API has to do that
-  // anyway.
-  let mentions = scrape_text_for_mentions(&comment.content);
-  let recipient_ids = send_local_notifs(
-    mentions,
-    comment.clone(),
-    person,
-    post,
-    context.pool(),
-    true,
-  )
-  .await?;
-
-  // Refetch the view
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment.id, None)
-  })
-  .await??;
-
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-
-  context.chat_server().do_send(SendComment {
-    op: UserOperationCrud::CreateComment,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_update_comment(
-  update: Update,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-  let person = get_actor_as_person(&update, context, request_counter).await?;
-
-  let comment =
-    Comment::from_apub(&note, context, person.actor_id(), request_counter, false).await?;
-
-  let comment_id = comment.id;
-  let post_id = comment.post_id;
-  let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-
-  let mentions = scrape_text_for_mentions(&comment.content);
-  let recipient_ids =
-    send_local_notifs(mentions, comment, person, post, context.pool(), false).await?;
-
-  // Refetch the view
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-
-  context.chat_server().do_send(SendComment {
-    op: UserOperationCrud::EditComment,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_like_comment(
-  like: Like,
-  comment: Comment,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(&like, context, request_counter).await?;
-
-  let comment_id = comment.id;
-  let like_form = CommentLikeForm {
-    comment_id,
-    post_id: comment.post_id,
-    person_id: person.id,
-    score: 1,
-  };
-  let person_id = person.id;
-  blocking(context.pool(), move |conn| {
-    CommentLike::remove(conn, person_id, comment_id)?;
-    CommentLike::like(conn, &like_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-
-  context.chat_server().do_send(SendComment {
-    op: UserOperation::CreateCommentLike,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_dislike_comment(
-  dislike: Dislike,
-  comment: Comment,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(&dislike, context, request_counter).await?;
-
-  let comment_id = comment.id;
-  let like_form = CommentLikeForm {
-    comment_id,
-    post_id: comment.post_id,
-    person_id: person.id,
-    score: -1,
-  };
-  let person_id = person.id;
-  blocking(context.pool(), move |conn| {
-    CommentLike::remove(conn, person_id, comment_id)?;
-    CommentLike::like(conn, &like_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-
-  context.chat_server().do_send(SendComment {
-    op: UserOperation::CreateCommentLike,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_delete_comment(
-  context: &LemmyContext,
-  comment: Comment,
-) -> Result<(), LemmyError> {
-  let deleted_comment = blocking(context.pool(), move |conn| {
-    Comment::update_deleted(conn, comment.id, true)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_id = deleted_comment.id;
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-  context.chat_server().do_send(SendComment {
-    op: UserOperationCrud::EditComment,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_remove_comment(
-  context: &LemmyContext,
-  comment: Comment,
-) -> Result<(), LemmyError> {
-  let removed_comment = blocking(context.pool(), move |conn| {
-    Comment::update_removed(conn, comment.id, true)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_id = removed_comment.id;
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-  context.chat_server().do_send(SendComment {
-    op: UserOperationCrud::EditComment,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
diff --git a/crates/apub_receive/src/activities/receive/comment_undo.rs b/crates/apub_receive/src/activities/receive/comment_undo.rs
deleted file mode 100644
index 7214c8f0..00000000
--- a/crates/apub_receive/src/activities/receive/comment_undo.rs
+++ /dev/null
@@ -1,150 +0,0 @@
-use crate::activities::receive::get_actor_as_person;
-use activitystreams::activity::{Dislike, Like};
-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;
-use lemmy_utils::LemmyError;
-use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation, UserOperationCrud};
-
-pub(crate) async fn receive_undo_like_comment(
-  like: &Like,
-  comment: Comment,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(like, context, request_counter).await?;
-
-  let comment_id = comment.id;
-  let person_id = person.id;
-  blocking(context.pool(), move |conn| {
-    CommentLike::remove(conn, person_id, comment_id)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-
-  context.chat_server().do_send(SendComment {
-    op: UserOperation::CreateCommentLike,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_dislike_comment(
-  dislike: &Dislike,
-  comment: Comment,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(dislike, context, request_counter).await?;
-
-  let comment_id = comment.id;
-  let person_id = person.id;
-  blocking(context.pool(), move |conn| {
-    CommentLike::remove(conn, person_id, comment_id)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-
-  context.chat_server().do_send(SendComment {
-    op: UserOperation::CreateCommentLike,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_delete_comment(
-  context: &LemmyContext,
-  comment: Comment,
-) -> Result<(), LemmyError> {
-  let deleted_comment = blocking(context.pool(), move |conn| {
-    Comment::update_deleted(conn, comment.id, false)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_id = deleted_comment.id;
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-
-  context.chat_server().do_send(SendComment {
-    op: UserOperationCrud::EditComment,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_remove_comment(
-  context: &LemmyContext,
-  comment: Comment,
-) -> Result<(), LemmyError> {
-  let removed_comment = blocking(context.pool(), move |conn| {
-    Comment::update_removed(conn, comment.id, false)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_id = removed_comment.id;
-  let comment_view = blocking(context.pool(), move |conn| {
-    CommentView::read(conn, comment_id, None)
-  })
-  .await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment_view,
-    recipient_ids,
-    form_id: None,
-  };
-
-  context.chat_server().do_send(SendComment {
-    op: UserOperationCrud::EditComment,
-    comment: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
diff --git a/crates/apub_receive/src/activities/receive/community.rs b/crates/apub_receive/src/activities/receive/community.rs
deleted file mode 100644
index 18ebbed3..00000000
--- a/crates/apub_receive/src/activities/receive/community.rs
+++ /dev/null
@@ -1,232 +0,0 @@
-use crate::{
-  activities::receive::get_actor_as_person,
-  inbox::receive_for_community::verify_actor_is_community_mod,
-};
-use activitystreams::{
-  activity::{ActorAndObjectRefExt, Delete, Undo, Update},
-  base::ExtendsExt,
-};
-use anyhow::{anyhow, Context};
-use lemmy_api_common::{blocking, community::CommunityResponse};
-use lemmy_apub::{
-  get_community_from_to_or_cc,
-  objects::FromApubToForm,
-  ActorType,
-  CommunityType,
-  GroupExt,
-};
-use lemmy_db_queries::{source::community::Community_, Crud};
-use lemmy_db_schema::source::{
-  community::{Community, CommunityForm},
-  person::Person,
-};
-use lemmy_db_views_actor::{
-  community_moderator_view::CommunityModeratorView,
-  community_view::CommunityView,
-};
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperationCrud};
-
-/// This activity is received from a remote community mod, and updates the description or other
-/// fields of a local community.
-pub(crate) async fn receive_remote_mod_update_community(
-  update: Update,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let community = get_community_from_to_or_cc(&update, context, request_counter).await?;
-  verify_actor_is_community_mod(&update, &community, context).await?;
-  let group = GroupExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-  let updated_community = CommunityForm::from_apub(
-    &group,
-    context,
-    community.actor_id(),
-    request_counter,
-    false,
-  )
-  .await?;
-  let cf = CommunityForm {
-    name: updated_community.name,
-    title: updated_community.title,
-    description: updated_community.description,
-    nsfw: updated_community.nsfw,
-    // TODO: icon and banner would be hosted on the other instance, ideally we would copy it to ours
-    icon: updated_community.icon,
-    banner: updated_community.banner,
-    ..CommunityForm::default()
-  };
-  blocking(context.pool(), move |conn| {
-    Community::update(conn, community.id, &cf)
-  })
-  .await??;
-
-  Ok(())
-}
-
-pub(crate) async fn receive_remote_mod_delete_community(
-  delete: Delete,
-  community: Community,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  verify_actor_is_community_mod(&delete, &community, context).await?;
-  let actor = get_actor_as_person(&delete, context, request_counter).await?;
-  verify_is_remote_community_creator(&actor, &community, context).await?;
-  let community_id = community.id;
-  blocking(context.pool(), move |conn| {
-    Community::update_deleted(conn, community_id, true)
-  })
-  .await??;
-  community.send_delete(actor, context).await
-}
-
-pub(crate) async fn receive_delete_community(
-  context: &LemmyContext,
-  community: Community,
-) -> Result<(), LemmyError> {
-  let deleted_community = blocking(context.pool(), move |conn| {
-    Community::update_deleted(conn, community.id, true)
-  })
-  .await??;
-
-  let community_id = deleted_community.id;
-  let res = CommunityResponse {
-    community_view: blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, None)
-    })
-    .await??,
-  };
-
-  let community_id = res.community_view.community.id;
-  context.chat_server().do_send(SendCommunityRoomMessage {
-    op: UserOperationCrud::EditCommunity,
-    response: res,
-    community_id,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_remove_community(
-  context: &LemmyContext,
-  community: Community,
-) -> Result<(), LemmyError> {
-  let removed_community = blocking(context.pool(), move |conn| {
-    Community::update_removed(conn, community.id, true)
-  })
-  .await??;
-
-  let community_id = removed_community.id;
-  let res = CommunityResponse {
-    community_view: blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, None)
-    })
-    .await??,
-  };
-
-  let community_id = res.community_view.community.id;
-  context.chat_server().do_send(SendCommunityRoomMessage {
-    op: UserOperationCrud::EditCommunity,
-    response: res,
-    community_id,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_remote_mod_undo_delete_community(
-  undo: Undo,
-  community: Community,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  verify_actor_is_community_mod(&undo, &community, context).await?;
-  let actor = get_actor_as_person(&undo, context, request_counter).await?;
-  verify_is_remote_community_creator(&actor, &community, context).await?;
-  let community_id = community.id;
-  blocking(context.pool(), move |conn| {
-    Community::update_deleted(conn, community_id, false)
-  })
-  .await??;
-  community.send_undo_delete(actor, context).await
-}
-
-pub(crate) async fn receive_undo_delete_community(
-  context: &LemmyContext,
-  community: Community,
-) -> Result<(), LemmyError> {
-  let deleted_community = blocking(context.pool(), move |conn| {
-    Community::update_deleted(conn, community.id, false)
-  })
-  .await??;
-
-  let community_id = deleted_community.id;
-  let res = CommunityResponse {
-    community_view: blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, None)
-    })
-    .await??,
-  };
-
-  let community_id = res.community_view.community.id;
-  context.chat_server().do_send(SendCommunityRoomMessage {
-    op: UserOperationCrud::EditCommunity,
-    response: res,
-    community_id,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_remove_community(
-  context: &LemmyContext,
-  community: Community,
-) -> Result<(), LemmyError> {
-  let removed_community = blocking(context.pool(), move |conn| {
-    Community::update_removed(conn, community.id, false)
-  })
-  .await??;
-
-  let community_id = removed_community.id;
-  let res = CommunityResponse {
-    community_view: blocking(context.pool(), move |conn| {
-      CommunityView::read(conn, community_id, None)
-    })
-    .await??,
-  };
-
-  let community_id = res.community_view.community.id;
-
-  context.chat_server().do_send(SendCommunityRoomMessage {
-    op: UserOperationCrud::EditCommunity,
-    response: res,
-    community_id,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-/// Checks if the remote user is creator of the local community. This can only happen if a community
-/// is created by a local user, and then transferred to a remote user.
-async fn verify_is_remote_community_creator(
-  user: &Person,
-  community: &Community,
-  context: &LemmyContext,
-) -> Result<(), LemmyError> {
-  let community_id = community.id;
-  let community_mods = blocking(context.pool(), move |conn| {
-    CommunityModeratorView::for_community(conn, community_id)
-  })
-  .await??;
-
-  if user.id != community_mods[0].moderator.id {
-    Err(anyhow!("Actor is not community creator").into())
-  } else {
-    Ok(())
-  }
-}
diff --git a/crates/apub_receive/src/activities/receive/mod.rs b/crates/apub_receive/src/activities/receive/mod.rs
deleted file mode 100644
index f421a45e..00000000
--- a/crates/apub_receive/src/activities/receive/mod.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-use activitystreams::{
-  activity::{ActorAndObjectRef, ActorAndObjectRefExt},
-  base::{AsBase, BaseExt},
-  error::DomainError,
-};
-use anyhow::{anyhow, Context};
-use lemmy_apub::fetcher::person::get_or_fetch_and_upsert_person;
-use lemmy_db_schema::source::person::Person;
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::LemmyContext;
-use log::debug;
-use std::fmt::Debug;
-use url::Url;
-
-pub(crate) mod comment;
-pub(crate) mod comment_undo;
-pub(crate) mod community;
-pub(crate) mod post;
-pub(crate) mod post_undo;
-pub(crate) mod private_message;
-
-/// Return HTTP 501 for unsupported activities in inbox.
-pub(crate) fn receive_unhandled_activity<A>(activity: A) -> Result<(), LemmyError>
-where
-  A: Debug,
-{
-  debug!("received unhandled activity type: {:?}", activity);
-  Err(anyhow!("Activity not supported").into())
-}
-
-/// Reads the actor field of an activity and returns the corresponding `Person`.
-pub(crate) async fn get_actor_as_person<T, A>(
-  activity: &T,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<Person, LemmyError>
-where
-  T: AsBase<A> + ActorAndObjectRef,
-{
-  let actor = activity.actor()?;
-  let person_uri = actor.as_single_xsd_any_uri().context(location_info!())?;
-  get_or_fetch_and_upsert_person(person_uri, context, request_counter).await
-}
-
-/// Ensure that the ID of an incoming activity comes from the same domain as the actor. Optionally
-/// also checks the ID of the inner object.
-///
-/// The reason that this starts with the actor ID is that it was already confirmed as correct by the
-/// HTTP signature.
-pub(crate) fn verify_activity_domains_valid<T, Kind>(
-  activity: &T,
-  actor_id: &Url,
-  object_domain_must_match: bool,
-) -> Result<(), LemmyError>
-where
-  T: AsBase<Kind> + ActorAndObjectRef,
-{
-  let expected_domain = actor_id.domain().context(location_info!())?;
-
-  activity.id(expected_domain)?;
-
-  let object_id = match activity.object().to_owned().single_xsd_any_uri() {
-    // object is just an ID
-    Some(id) => id,
-    // object is something like an activity, a comment or a post
-    None => activity
-      .object()
-      .to_owned()
-      .one()
-      .context(location_info!())?
-      .id()
-      .context(location_info!())?
-      .to_owned(),
-  };
-
-  if object_domain_must_match && object_id.domain() != Some(expected_domain) {
-    return Err(DomainError.into());
-  }
-
-  Ok(())
-}
diff --git a/crates/apub_receive/src/activities/receive/post.rs b/crates/apub_receive/src/activities/receive/post.rs
deleted file mode 100644
index 07f6a665..00000000
--- a/crates/apub_receive/src/activities/receive/post.rs
+++ /dev/null
@@ -1,242 +0,0 @@
-use crate::{
-  activities::receive::get_actor_as_person,
-  inbox::receive_for_community::verify_mod_activity,
-};
-use activitystreams::{
-  activity::{Announce, Create, Dislike, Like, Update},
-  prelude::*,
-};
-use anyhow::Context;
-use lemmy_api_common::{blocking, post::PostResponse};
-use lemmy_apub::{objects::FromApub, ActorType, PageExt};
-use lemmy_db_queries::{source::post::Post_, ApubObject, Crud, Likeable};
-use lemmy_db_schema::{
-  source::{
-    community::Community,
-    post::{Post, PostLike, PostLikeForm},
-  },
-  DbUrl,
-};
-use lemmy_db_views::post_view::PostView;
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation, UserOperationCrud};
-
-pub(crate) async fn receive_create_post(
-  create: Create,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(&create, context, request_counter).await?;
-  let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-
-  let post = Post::from_apub(&page, context, person.actor_id(), request_counter, false).await?;
-
-  // Refetch the view
-  let post_id = post.id;
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-
-  context.chat_server().do_send(SendPost {
-    op: UserOperationCrud::CreatePost,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_update_post(
-  update: Update,
-  announce: Option<Announce>,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(&update, context, request_counter).await?;
-  let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-
-  let post_id: DbUrl = page
-    .id_unchecked()
-    .context(location_info!())?
-    .to_owned()
-    .into();
-  let old_post = blocking(context.pool(), move |conn| {
-    Post::read_from_apub_id(conn, &post_id)
-  })
-  .await??;
-
-  // If sticked or locked state was changed, make sure the actor is a mod
-  let stickied = page.ext_one.stickied.context(location_info!())?;
-  let locked = !page.ext_one.comments_enabled.context(location_info!())?;
-  let mut mod_action_allowed = false;
-  if (stickied != old_post.stickied) || (locked != old_post.locked) {
-    let community = blocking(context.pool(), move |conn| {
-      Community::read(conn, old_post.community_id)
-    })
-    .await??;
-    // Only check mod status if the community is local, otherwise we trust that it was sent correctly.
-    if community.local {
-      verify_mod_activity(&update, announce, &community, context).await?;
-    }
-    mod_action_allowed = true;
-  }
-
-  let post = Post::from_apub(
-    &page,
-    context,
-    person.actor_id(),
-    request_counter,
-    mod_action_allowed,
-  )
-  .await?;
-
-  let post_id = post.id;
-  // Refetch the view
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-
-  context.chat_server().do_send(SendPost {
-    op: UserOperationCrud::EditPost,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_like_post(
-  like: Like,
-  post: Post,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(&like, context, request_counter).await?;
-
-  let post_id = post.id;
-  let like_form = PostLikeForm {
-    post_id,
-    person_id: person.id,
-    score: 1,
-  };
-  let person_id = person.id;
-  blocking(context.pool(), move |conn| {
-    PostLike::remove(conn, person_id, post_id)?;
-    PostLike::like(conn, &like_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-
-  context.chat_server().do_send(SendPost {
-    op: UserOperation::CreatePostLike,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_dislike_post(
-  dislike: Dislike,
-  post: Post,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(&dislike, context, request_counter).await?;
-
-  let post_id = post.id;
-  let like_form = PostLikeForm {
-    post_id,
-    person_id: person.id,
-    score: -1,
-  };
-  let person_id = person.id;
-  blocking(context.pool(), move |conn| {
-    PostLike::remove(conn, person_id, post_id)?;
-    PostLike::like(conn, &like_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-
-  context.chat_server().do_send(SendPost {
-    op: UserOperation::CreatePostLike,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_delete_post(
-  context: &LemmyContext,
-  post: Post,
-) -> Result<(), LemmyError> {
-  let deleted_post = blocking(context.pool(), move |conn| {
-    Post::update_deleted(conn, post.id, true)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_id = deleted_post.id;
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-  context.chat_server().do_send(SendPost {
-    op: UserOperationCrud::EditPost,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_remove_post(
-  context: &LemmyContext,
-  post: Post,
-) -> Result<(), LemmyError> {
-  let removed_post = blocking(context.pool(), move |conn| {
-    Post::update_removed(conn, post.id, true)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_id = removed_post.id;
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-  context.chat_server().do_send(SendPost {
-    op: UserOperationCrud::EditPost,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
diff --git a/crates/apub_receive/src/activities/receive/post_undo.rs b/crates/apub_receive/src/activities/receive/post_undo.rs
deleted file mode 100644
index 2ed6ecac..00000000
--- a/crates/apub_receive/src/activities/receive/post_undo.rs
+++ /dev/null
@@ -1,125 +0,0 @@
-use crate::activities::receive::get_actor_as_person;
-use activitystreams::activity::{Dislike, Like};
-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;
-use lemmy_utils::LemmyError;
-use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation, UserOperationCrud};
-
-pub(crate) async fn receive_undo_like_post(
-  like: &Like,
-  post: Post,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(like, context, request_counter).await?;
-
-  let post_id = post.id;
-  let person_id = person.id;
-  blocking(context.pool(), move |conn| {
-    PostLike::remove(conn, person_id, post_id)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-
-  context.chat_server().do_send(SendPost {
-    op: UserOperation::CreatePostLike,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_dislike_post(
-  dislike: &Dislike,
-  post: Post,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let person = get_actor_as_person(dislike, context, request_counter).await?;
-
-  let post_id = post.id;
-  let person_id = person.id;
-  blocking(context.pool(), move |conn| {
-    PostLike::remove(conn, person_id, post_id)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-
-  context.chat_server().do_send(SendPost {
-    op: UserOperation::CreatePostLike,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_delete_post(
-  context: &LemmyContext,
-  post: Post,
-) -> Result<(), LemmyError> {
-  let deleted_post = blocking(context.pool(), move |conn| {
-    Post::update_deleted(conn, post.id, false)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_id = deleted_post.id;
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-  context.chat_server().do_send(SendPost {
-    op: UserOperationCrud::EditPost,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_remove_post(
-  context: &LemmyContext,
-  post: Post,
-) -> Result<(), LemmyError> {
-  let removed_post = blocking(context.pool(), move |conn| {
-    Post::update_removed(conn, post.id, false)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_id = removed_post.id;
-  let post_view = blocking(context.pool(), move |conn| {
-    PostView::read(conn, post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post_view };
-
-  context.chat_server().do_send(SendPost {
-    op: UserOperationCrud::EditPost,
-    post: res,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
diff --git a/crates/apub_receive/src/activities/receive/private_message.rs b/crates/apub_receive/src/activities/receive/private_message.rs
deleted file mode 100644
index 3d82c5c2..00000000
--- a/crates/apub_receive/src/activities/receive/private_message.rs
+++ /dev/null
@@ -1,228 +0,0 @@
-use crate::activities::receive::verify_activity_domains_valid;
-use activitystreams::{
-  activity::{ActorAndObjectRefExt, Create, Delete, Undo, Update},
-  base::{AsBase, ExtendsExt},
-  object::AsObject,
-  public,
-};
-use anyhow::{anyhow, Context};
-use lemmy_api_common::{blocking, person::PrivateMessageResponse};
-use lemmy_apub::{
-  check_is_apub_id_valid,
-  fetcher::person::get_or_fetch_and_upsert_person,
-  get_activity_to_and_cc,
-  objects::FromApub,
-  NoteExt,
-};
-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};
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud};
-use url::Url;
-
-pub(crate) async fn receive_create_private_message(
-  context: &LemmyContext,
-  create: Create,
-  expected_domain: Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  check_private_message_activity_valid(&create, context, request_counter).await?;
-
-  let note = NoteExt::from_any_base(
-    create
-      .object()
-      .as_one()
-      .context(location_info!())?
-      .to_owned(),
-  )?
-  .context(location_info!())?;
-
-  let private_message =
-    PrivateMessage::from_apub(&note, context, expected_domain, request_counter, false).await?;
-
-  let message = blocking(context.pool(), move |conn| {
-    PrivateMessageView::read(conn, private_message.id)
-  })
-  .await??;
-
-  let res = PrivateMessageResponse {
-    private_message_view: message,
-  };
-
-  // Send notifications to the local recipient, if one exists
-  let recipient_id = res.private_message_view.recipient.id;
-  let local_recipient_id = blocking(context.pool(), move |conn| {
-    LocalUserView::read_person(conn, recipient_id)
-  })
-  .await??
-  .local_user
-  .id;
-
-  context.chat_server().do_send(SendUserRoomMessage {
-    op: UserOperationCrud::CreatePrivateMessage,
-    response: res,
-    local_recipient_id,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_update_private_message(
-  context: &LemmyContext,
-  update: Update,
-  expected_domain: Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  check_private_message_activity_valid(&update, context, request_counter).await?;
-
-  let object = update
-    .object()
-    .as_one()
-    .context(location_info!())?
-    .to_owned();
-  let note = NoteExt::from_any_base(object)?.context(location_info!())?;
-
-  let private_message =
-    PrivateMessage::from_apub(&note, context, expected_domain, request_counter, false).await?;
-
-  let private_message_id = private_message.id;
-  let message = blocking(context.pool(), move |conn| {
-    PrivateMessageView::read(conn, private_message_id)
-  })
-  .await??;
-
-  let res = PrivateMessageResponse {
-    private_message_view: message,
-  };
-
-  let recipient_id = res.private_message_view.recipient.id;
-  let local_recipient_id = blocking(context.pool(), move |conn| {
-    LocalUserView::read_person(conn, recipient_id)
-  })
-  .await??
-  .local_user
-  .id;
-
-  context.chat_server().do_send(SendUserRoomMessage {
-    op: UserOperationCrud::EditPrivateMessage,
-    response: res,
-    local_recipient_id,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_delete_private_message(
-  context: &LemmyContext,
-  delete: Delete,
-  private_message: PrivateMessage,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  check_private_message_activity_valid(&delete, context, request_counter).await?;
-
-  let deleted_private_message = blocking(context.pool(), move |conn| {
-    PrivateMessage::update_deleted(conn, private_message.id, true)
-  })
-  .await??;
-
-  let message = blocking(context.pool(), move |conn| {
-    PrivateMessageView::read(conn, deleted_private_message.id)
-  })
-  .await??;
-
-  let res = PrivateMessageResponse {
-    private_message_view: message,
-  };
-
-  let recipient_id = res.private_message_view.recipient.id;
-  let local_recipient_id = blocking(context.pool(), move |conn| {
-    LocalUserView::read_person(conn, recipient_id)
-  })
-  .await??
-  .local_user
-  .id;
-
-  context.chat_server().do_send(SendUserRoomMessage {
-    op: UserOperationCrud::EditPrivateMessage,
-    response: res,
-    local_recipient_id,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_delete_private_message(
-  context: &LemmyContext,
-  undo: Undo,
-  expected_domain: &Url,
-  private_message: PrivateMessage,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  check_private_message_activity_valid(&undo, context, request_counter).await?;
-  let object = undo.object().to_owned().one().context(location_info!())?;
-  let delete = Delete::from_any_base(object)?.context(location_info!())?;
-  verify_activity_domains_valid(&delete, expected_domain, true)?;
-  check_private_message_activity_valid(&delete, context, request_counter).await?;
-
-  let deleted_private_message = blocking(context.pool(), move |conn| {
-    PrivateMessage::update_deleted(conn, private_message.id, false)
-  })
-  .await??;
-
-  let message = blocking(context.pool(), move |conn| {
-    PrivateMessageView::read(conn, deleted_private_message.id)
-  })
-  .await??;
-
-  let res = PrivateMessageResponse {
-    private_message_view: message,
-  };
-
-  let recipient_id = res.private_message_view.recipient.id;
-  let local_recipient_id = blocking(context.pool(), move |conn| {
-    LocalUserView::read_person(conn, recipient_id)
-  })
-  .await??
-  .local_user
-  .id;
-
-  context.chat_server().do_send(SendUserRoomMessage {
-    op: UserOperationCrud::EditPrivateMessage,
-    response: res,
-    local_recipient_id,
-    websocket_id: None,
-  });
-
-  Ok(())
-}
-
-async fn check_private_message_activity_valid<T, Kind>(
-  activity: &T,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError>
-where
-  T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
-{
-  let to_and_cc = get_activity_to_and_cc(activity);
-  if to_and_cc.len() != 1 {
-    return Err(anyhow!("Private message can only be addressed to one person").into());
-  }
-  if to_and_cc.contains(&public()) {
-    return Err(anyhow!("Private message cant be public").into());
-  }
-  let person_id = activity
-    .actor()?
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-  check_is_apub_id_valid(&person_id, false)?;
-  // check that the sender is a person, not a community
-  get_or_fetch_and_upsert_person(&person_id, context, request_counter).await?;
-
-  Ok(())
-}
diff --git a/crates/apub_receive/src/activities/removal/mod.rs b/crates/apub_receive/src/activities/removal/mod.rs
new file mode 100644
index 00000000..01c031dd
--- /dev/null
+++ b/crates/apub_receive/src/activities/removal/mod.rs
@@ -0,0 +1,2 @@
+pub mod remove;
+pub mod undo_remove;
diff --git a/crates/apub_receive/src/activities/removal/remove.rs b/crates/apub_receive/src/activities/removal/remove.rs
new file mode 100644
index 00000000..bb1c307b
--- /dev/null
+++ b/crates/apub_receive/src/activities/removal/remove.rs
@@ -0,0 +1,155 @@
+use crate::activities::{
+  comment::send_websocket_message as send_comment_message,
+  community::send_websocket_message as send_community_message,
+  post::send_websocket_message as send_post_message,
+  verify_activity,
+  verify_add_remove_moderator_target,
+  verify_mod_action,
+  verify_person_in_community,
+};
+use activitystreams::{activity::kind::RemoveType, base::AnyBase};
+use anyhow::anyhow;
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  fetcher::{
+    community::get_or_fetch_and_upsert_community,
+    objects::get_or_fetch_and_insert_post_or_comment,
+    person::get_or_fetch_and_upsert_person,
+  },
+  CommunityType,
+  PostOrComment,
+};
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::{
+  source::{comment::Comment_, community::Community_, post::Post_},
+  Joinable,
+};
+use lemmy_db_schema::source::{
+  comment::Comment,
+  community::{Community, CommunityModerator, CommunityModeratorForm},
+  post::Post,
+};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+// TODO: we can probably deduplicate a bunch of code between this and DeletePostCommentOrCommunity
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RemovePostCommentCommunityOrMod {
+  to: PublicUrl,
+  pub(in crate::activities::removal) object: Url,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: RemoveType,
+  // if target is set, this is means remove mod from community
+  target: Option<Url>,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for RemovePostCommentCommunityOrMod {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    let object_community =
+      get_or_fetch_and_upsert_community(&self.object, context, request_counter).await;
+    // removing a community
+    if object_community.is_ok() {
+      verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
+    }
+    // removing community mod
+    else if let Some(target) = &self.target {
+      verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+      verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
+      verify_add_remove_moderator_target(target, self.cc[0].clone())?;
+    }
+    // removing a post or comment
+    else {
+      verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+      verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
+    }
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let object_community =
+      get_or_fetch_and_upsert_community(&self.object, context, request_counter).await;
+    // removing a community
+    if let Ok(community) = object_community {
+      if community.local {
+        return Err(anyhow!("Only local admin can remove community").into());
+      }
+      let deleted_community = blocking(context.pool(), move |conn| {
+        Community::update_removed(conn, community.id, true)
+      })
+      .await??;
+
+      send_community_message(
+        deleted_community.id,
+        UserOperationCrud::RemoveCommunity,
+        context,
+      )
+      .await
+    }
+    // removing community mod
+    else if self.target.is_some() {
+      let community =
+        get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter).await?;
+      let remove_mod =
+        get_or_fetch_and_upsert_person(&self.object, context, request_counter).await?;
+
+      let form = CommunityModeratorForm {
+        community_id: community.id,
+        person_id: remove_mod.id,
+      };
+      blocking(context.pool(), move |conn| {
+        CommunityModerator::leave(conn, &form)
+      })
+      .await??;
+      let anybase = AnyBase::from_arbitrary_json(serde_json::to_string(self)?)?;
+      community
+        .send_announce(anybase, Some(self.object.clone()), context)
+        .await?;
+      // TODO: send websocket notification about removed mod
+      Ok(())
+    }
+    // removing a post or comment
+    else {
+      match get_or_fetch_and_insert_post_or_comment(&self.object, context, request_counter).await? {
+        PostOrComment::Post(post) => {
+          let removed_post = blocking(context.pool(), move |conn| {
+            Post::update_removed(conn, post.id, true)
+          })
+          .await??;
+          send_post_message(removed_post.id, UserOperationCrud::EditPost, context).await
+        }
+        PostOrComment::Comment(comment) => {
+          let removed_comment = blocking(context.pool(), move |conn| {
+            Comment::update_removed(conn, comment.id, true)
+          })
+          .await??;
+          send_comment_message(
+            removed_comment.id,
+            vec![],
+            UserOperationCrud::EditComment,
+            context,
+          )
+          .await
+        }
+      }
+    }
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/removal/undo_remove.rs b/crates/apub_receive/src/activities/removal/undo_remove.rs
new file mode 100644
index 00000000..e19c4194
--- /dev/null
+++ b/crates/apub_receive/src/activities/removal/undo_remove.rs
@@ -0,0 +1,120 @@
+use crate::activities::{
+  comment::send_websocket_message as send_comment_message,
+  community::send_websocket_message as send_community_message,
+  post::send_websocket_message as send_post_message,
+  removal::remove::RemovePostCommentCommunityOrMod,
+  verify_activity,
+  verify_mod_action,
+  verify_person_in_community,
+};
+use activitystreams::activity::kind::UndoType;
+use anyhow::anyhow;
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  fetcher::{
+    community::get_or_fetch_and_upsert_community,
+    objects::get_or_fetch_and_insert_post_or_comment,
+  },
+  PostOrComment,
+};
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_};
+use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperationCrud};
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UndoRemovePostCommentOrCommunity {
+  to: PublicUrl,
+  object: RemovePostCommentCommunityOrMod,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: UndoType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UndoRemovePostCommentOrCommunity {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    let object_community =
+      get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await;
+    // removing a community
+    if object_community.is_ok() {
+      verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
+    }
+    // removing a post or comment
+    else {
+      verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+      verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
+    }
+    self.object.verify(context, request_counter).await?;
+    // dont check that actor and object.actor are identical, so that one mod can
+    // undo the action of another
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let object_community =
+      get_or_fetch_and_upsert_community(&self.object.object, context, request_counter).await;
+    // restoring a community
+    if let Ok(community) = object_community {
+      if community.local {
+        return Err(anyhow!("Only local admin can undo remove community").into());
+      }
+      let deleted_community = blocking(context.pool(), move |conn| {
+        Community::update_removed(conn, community.id, false)
+      })
+      .await??;
+
+      send_community_message(
+        deleted_community.id,
+        UserOperationCrud::EditCommunity,
+        context,
+      )
+      .await
+    }
+    // restoring a post or comment
+    else {
+      match get_or_fetch_and_insert_post_or_comment(&self.object.object, context, request_counter)
+        .await?
+      {
+        PostOrComment::Post(post) => {
+          let removed_post = blocking(context.pool(), move |conn| {
+            Post::update_removed(conn, post.id, false)
+          })
+          .await??;
+          send_post_message(removed_post.id, UserOperationCrud::EditPost, context).await
+        }
+        PostOrComment::Comment(comment) => {
+          let removed_comment = blocking(context.pool(), move |conn| {
+            Comment::update_removed(conn, comment.id, false)
+          })
+          .await??;
+          send_comment_message(
+            removed_comment.id,
+            vec![],
+            UserOperationCrud::EditComment,
+            context,
+          )
+          .await
+        }
+      }
+    }
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/voting/dislike.rs b/crates/apub_receive/src/activities/voting/dislike.rs
new file mode 100644
index 00000000..18d72f39
--- /dev/null
+++ b/crates/apub_receive/src/activities/voting/dislike.rs
@@ -0,0 +1,54 @@
+use crate::activities::{
+  verify_activity,
+  verify_person_in_community,
+  voting::receive_like_or_dislike,
+};
+use activitystreams::activity::kind::DislikeType;
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DislikePostOrComment {
+  to: PublicUrl,
+  pub(in crate::activities) object: Url,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: DislikeType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for DislikePostOrComment {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    receive_like_or_dislike(
+      -1,
+      &self.common.actor,
+      &self.object,
+      context,
+      request_counter,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/voting/like.rs b/crates/apub_receive/src/activities/voting/like.rs
new file mode 100644
index 00000000..ca899d3d
--- /dev/null
+++ b/crates/apub_receive/src/activities/voting/like.rs
@@ -0,0 +1,54 @@
+use crate::activities::{
+  verify_activity,
+  verify_person_in_community,
+  voting::receive_like_or_dislike,
+};
+use activitystreams::activity::kind::LikeType;
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LikePostOrComment {
+  to: PublicUrl,
+  pub(in crate::activities::voting) object: Url,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: LikeType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for LikePostOrComment {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    receive_like_or_dislike(
+      1,
+      &self.common.actor,
+      &self.object,
+      context,
+      request_counter,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/voting/mod.rs b/crates/apub_receive/src/activities/voting/mod.rs
new file mode 100644
index 00000000..123523e2
--- /dev/null
+++ b/crates/apub_receive/src/activities/voting/mod.rs
@@ -0,0 +1,157 @@
+use crate::activities::{
+  comment::send_websocket_message as send_comment_message,
+  post::send_websocket_message as send_post_message,
+};
+use lemmy_api_common::blocking;
+use lemmy_apub::{
+  fetcher::{
+    objects::get_or_fetch_and_insert_post_or_comment,
+    person::get_or_fetch_and_upsert_person,
+  },
+  PostOrComment,
+};
+use lemmy_db_queries::Likeable;
+use lemmy_db_schema::source::{
+  comment::{Comment, CommentLike, CommentLikeForm},
+  post::{Post, PostLike, PostLikeForm},
+};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{LemmyContext, UserOperation};
+use std::ops::Deref;
+use url::Url;
+
+pub mod dislike;
+pub mod like;
+pub mod undo_dislike;
+pub mod undo_like;
+
+pub(in crate::activities::voting) async fn receive_like_or_dislike(
+  score: i16,
+  actor: &Url,
+  object: &Url,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? {
+    PostOrComment::Post(p) => {
+      like_or_dislike_post(score, actor, p.deref(), context, request_counter).await
+    }
+    PostOrComment::Comment(c) => {
+      like_or_dislike_comment(score, actor, c.deref(), context, request_counter).await
+    }
+  }
+}
+
+async fn like_or_dislike_comment(
+  score: i16,
+  actor: &Url,
+  comment: &Comment,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
+
+  let comment_id = comment.id;
+  let like_form = CommentLikeForm {
+    comment_id,
+    post_id: comment.post_id,
+    person_id: actor.id,
+    score,
+  };
+  let person_id = actor.id;
+  blocking(context.pool(), move |conn| {
+    CommentLike::remove(conn, person_id, comment_id)?;
+    CommentLike::like(conn, &like_form)
+  })
+  .await??;
+
+  send_comment_message(
+    comment_id,
+    vec![],
+    UserOperation::CreateCommentLike,
+    context,
+  )
+  .await
+}
+
+async fn like_or_dislike_post(
+  score: i16,
+  actor: &Url,
+  post: &Post,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
+
+  let post_id = post.id;
+  let like_form = PostLikeForm {
+    post_id: post.id,
+    person_id: actor.id,
+    score,
+  };
+  let person_id = actor.id;
+  blocking(context.pool(), move |conn| {
+    PostLike::remove(conn, person_id, post_id)?;
+    PostLike::like(conn, &like_form)
+  })
+  .await??;
+
+  send_post_message(post.id, UserOperation::CreatePostLike, context).await
+}
+
+pub(in crate::activities::voting) async fn receive_undo_like_or_dislike(
+  actor: &Url,
+  object: &Url,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? {
+    PostOrComment::Post(p) => {
+      undo_like_or_dislike_post(actor, p.deref(), context, request_counter).await
+    }
+    PostOrComment::Comment(c) => {
+      undo_like_or_dislike_comment(actor, c.deref(), context, request_counter).await
+    }
+  }
+}
+
+async fn undo_like_or_dislike_comment(
+  actor: &Url,
+  comment: &Comment,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
+
+  let comment_id = comment.id;
+  let person_id = actor.id;
+  blocking(context.pool(), move |conn| {
+    CommentLike::remove(conn, person_id, comment_id)
+  })
+  .await??;
+
+  send_comment_message(
+    comment.id,
+    vec![],
+    UserOperation::CreateCommentLike,
+    context,
+  )
+  .await
+}
+
+async fn undo_like_or_dislike_post(
+  actor: &Url,
+  post: &Post,
+  context: &LemmyContext,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
+
+  let post_id = post.id;
+  let person_id = actor.id;
+  blocking(context.pool(), move |conn| {
+    PostLike::remove(conn, person_id, post_id)
+  })
+  .await??;
+  send_post_message(post.id, UserOperation::CreatePostLike, context).await
+}
diff --git a/crates/apub_receive/src/activities/voting/undo_dislike.rs b/crates/apub_receive/src/activities/voting/undo_dislike.rs
new file mode 100644
index 00000000..11871e79
--- /dev/null
+++ b/crates/apub_receive/src/activities/voting/undo_dislike.rs
@@ -0,0 +1,55 @@
+use crate::activities::{
+  verify_activity,
+  verify_person_in_community,
+  voting::{dislike::DislikePostOrComment, receive_undo_like_or_dislike},
+};
+use activitystreams::activity::kind::UndoType;
+use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UndoDislikePostOrComment {
+  to: PublicUrl,
+  object: DislikePostOrComment,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: UndoType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UndoDislikePostOrComment {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_urls_match(&self.common.actor, &self.object.common().actor)?;
+    self.object.verify(context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    receive_undo_like_or_dislike(
+      &self.common.actor,
+      &self.object.object,
+      context,
+      request_counter,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/activities/voting/undo_like.rs b/crates/apub_receive/src/activities/voting/undo_like.rs
new file mode 100644
index 00000000..07c3c470
--- /dev/null
+++ b/crates/apub_receive/src/activities/voting/undo_like.rs
@@ -0,0 +1,55 @@
+use crate::activities::{
+  verify_activity,
+  verify_person_in_community,
+  voting::{like::LikePostOrComment, receive_undo_like_or_dislike},
+};
+use activitystreams::activity::kind::UndoType;
+use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use url::Url;
+
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UndoLikePostOrComment {
+  to: PublicUrl,
+  object: LikePostOrComment,
+  cc: [Url; 1],
+  #[serde(rename = "type")]
+  kind: UndoType,
+  #[serde(flatten)]
+  common: ActivityCommonFields,
+}
+
+#[async_trait::async_trait(?Send)]
+impl ActivityHandler for UndoLikePostOrComment {
+  async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_activity(self.common())?;
+    verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
+    verify_urls_match(&self.common.actor, &self.object.common().actor)?;
+    self.object.verify(context, request_counter).await?;
+    Ok(())
+  }
+
+  async fn receive(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    receive_undo_like_or_dislike(
+      &self.common.actor,
+      &self.object.object,
+      context,
+      request_counter,
+    )
+    .await
+  }
+
+  fn common(&self) -> &ActivityCommonFields {
+    &self.common
+  }
+}
diff --git a/crates/apub_receive/src/http/community.rs b/crates/apub_receive/src/http/community.rs
index 30daa3b1..106eeff2 100644
--- a/crates/apub_receive/src/http/community.rs
+++ b/crates/apub_receive/src/http/community.rs
@@ -1,10 +1,16 @@
-use crate::http::{create_apub_response, create_apub_tombstone_response};
+use crate::http::{
+  create_apub_response,
+  create_apub_tombstone_response,
+  inbox_enums::GroupInboxActivities,
+  payload_to_string,
+  receive_activity,
+};
 use activitystreams::{
   base::{AnyBase, BaseExt},
   collection::{CollectionExt, OrderedCollection, UnorderedCollection},
   url::Url,
 };
-use actix_web::{body::Body, web, HttpResponse};
+use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
 use lemmy_api_common::blocking;
 use lemmy_apub::{
   extensions::context::lemmy_context,
@@ -46,6 +52,17 @@ pub(crate) async fn get_apub_community_http(
   }
 }
 
+/// Handler for all incoming receive to community inboxes.
+pub async fn community_inbox(
+  request: HttpRequest,
+  payload: Payload,
+  _path: web::Path<String>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError> {
+  let unparsed = payload_to_string(payload).await?;
+  receive_activity::<GroupInboxActivities>(request, &unparsed, context).await
+}
+
 /// Returns an empty followers collection, only populating the size (for privacy).
 pub(crate) async fn get_apub_community_followers(
   info: web::Path<CommunityQuery>,
diff --git a/crates/apub_receive/src/http/inbox_enums.rs b/crates/apub_receive/src/http/inbox_enums.rs
new file mode 100644
index 00000000..0f31f534
--- /dev/null
+++ b/crates/apub_receive/src/http/inbox_enums.rs
@@ -0,0 +1,100 @@
+use crate::activities::{
+  comment::{create::CreateComment, update::UpdateComment},
+  community::{
+    add_mod::AddMod,
+    announce::AnnounceActivity,
+    block_user::BlockUserFromCommunity,
+    undo_block_user::UndoBlockUserFromCommunity,
+    update::UpdateCommunity,
+  },
+  deletion::{delete::DeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity},
+  following::{accept::AcceptFollowCommunity, follow::FollowCommunity, undo::UndoFollowCommunity},
+  post::{create::CreatePost, update::UpdatePost},
+  private_message::{
+    create::CreatePrivateMessage,
+    delete::DeletePrivateMessage,
+    undo_delete::UndoDeletePrivateMessage,
+    update::UpdatePrivateMessage,
+  },
+  removal::{
+    remove::RemovePostCommentCommunityOrMod,
+    undo_remove::UndoRemovePostCommentOrCommunity,
+  },
+  voting::{
+    dislike::DislikePostOrComment,
+    like::LikePostOrComment,
+    undo_dislike::UndoDislikePostOrComment,
+    undo_like::UndoLikePostOrComment,
+  },
+};
+use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
+#[serde(untagged)]
+pub enum PersonInboxActivities {
+  AcceptFollowCommunity(AcceptFollowCommunity),
+  CreatePrivateMessage(CreatePrivateMessage),
+  UpdatePrivateMessage(UpdatePrivateMessage),
+  DeletePrivateMessage(DeletePrivateMessage),
+  UndoDeletePrivateMessage(UndoDeletePrivateMessage),
+  AnnounceActivity(Box<AnnounceActivity>),
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
+#[serde(untagged)]
+pub enum GroupInboxActivities {
+  FollowCommunity(FollowCommunity),
+  UndoFollowCommunity(UndoFollowCommunity),
+  CreateComment(CreateComment),
+  UpdateComment(UpdateComment),
+  CreatePost(CreatePost),
+  UpdatePost(UpdatePost),
+  LikePostOrComment(LikePostOrComment),
+  DislikePostOrComment(DislikePostOrComment),
+  UndoLikePostOrComment(UndoLikePostOrComment),
+  UndoDislikePostOrComment(UndoDislikePostOrComment),
+  DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
+  UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
+  RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod),
+  UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity),
+  UpdateCommunity(Box<UpdateCommunity>),
+  BlockUserFromCommunity(BlockUserFromCommunity),
+  UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
+  AddMod(AddMod),
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
+#[serde(untagged)]
+pub enum SharedInboxActivities {
+  // received by group
+  FollowCommunity(FollowCommunity),
+  UndoFollowCommunity(UndoFollowCommunity),
+  CreateComment(CreateComment),
+  UpdateComment(UpdateComment),
+  CreatePost(CreatePost),
+  UpdatePost(UpdatePost),
+  LikePostOrComment(LikePostOrComment),
+  DislikePostOrComment(DislikePostOrComment),
+  UndoDislikePostOrComment(UndoDislikePostOrComment),
+  UndoLikePostOrComment(UndoLikePostOrComment),
+  DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
+  UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
+  RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod),
+  UndoRemovePostCommentOrCommunity(UndoRemovePostCommentOrCommunity),
+  UpdateCommunity(Box<UpdateCommunity>),
+  BlockUserFromCommunity(BlockUserFromCommunity),
+  UndoBlockUserFromCommunity(UndoBlockUserFromCommunity),
+  AddMod(AddMod),
+  // received by person
+  AcceptFollowCommunity(AcceptFollowCommunity),
+  // Note, pm activities need to be at the end, otherwise comments will end up here. We can probably
+  // avoid this problem by replacing createpm.object with our own struct, instead of NoteExt.
+  CreatePrivateMessage(CreatePrivateMessage),
+  UpdatePrivateMessage(UpdatePrivateMessage),
+  DeletePrivateMessage(DeletePrivateMessage),
+  UndoDeletePrivateMessage(UndoDeletePrivateMessage),
+  AnnounceActivity(Box<AnnounceActivity>),
+}
diff --git a/crates/apub_receive/src/http/mod.rs b/crates/apub_receive/src/http/mod.rs
index 4f332849..44302d8d 100644
--- a/crates/apub_receive/src/http/mod.rs
+++ b/crates/apub_receive/src/http/mod.rs
@@ -1,18 +1,104 @@
-use actix_web::{body::Body, web, HttpResponse};
+use crate::http::inbox_enums::SharedInboxActivities;
+use actix_web::{
+  body::Body,
+  web,
+  web::{Bytes, BytesMut, Payload},
+  HttpRequest,
+  HttpResponse,
+};
+use anyhow::{anyhow, Context};
+use futures::StreamExt;
 use http::StatusCode;
 use lemmy_api_common::blocking;
-use lemmy_apub::APUB_JSON_CONTENT_TYPE;
-use lemmy_db_queries::source::activity::Activity_;
+use lemmy_apub::{
+  check_is_apub_id_valid,
+  extensions::signatures::verify_signature,
+  fetcher::get_or_fetch_and_upsert_actor,
+  insert_activity,
+  APUB_JSON_CONTENT_TYPE,
+};
+use lemmy_apub_lib::ActivityHandler;
+use lemmy_db_queries::{source::activity::Activity_, DbPool};
 use lemmy_db_schema::source::activity::Activity;
-use lemmy_utils::{settings::structs::Settings, LemmyError};
+use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
 use lemmy_websocket::LemmyContext;
 use serde::{Deserialize, Serialize};
+use std::{fmt::Debug, io::Read};
 use url::Url;
 
-pub mod comment;
-pub mod community;
-pub mod person;
-pub mod post;
+mod comment;
+mod community;
+mod inbox_enums;
+mod person;
+mod post;
+pub mod routes;
+
+pub async fn shared_inbox(
+  request: HttpRequest,
+  payload: Payload,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError> {
+  let unparsed = payload_to_string(payload).await?;
+  receive_activity::<SharedInboxActivities>(request, &unparsed, context).await
+}
+
+async fn payload_to_string(mut payload: Payload) -> Result<String, LemmyError> {
+  let mut bytes = BytesMut::new();
+  while let Some(item) = payload.next().await {
+    bytes.extend_from_slice(&item?);
+  }
+  let mut unparsed = String::new();
+  Bytes::from(bytes).as_ref().read_to_string(&mut unparsed)?;
+  Ok(unparsed)
+}
+
+// TODO: move most of this code to library
+async fn receive_activity<'a, T>(
+  request: HttpRequest,
+  activity: &'a str,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError>
+where
+  T: ActivityHandler + Clone + Deserialize<'a> + Serialize + std::fmt::Debug + Send + 'static,
+{
+  let activity = serde_json::from_str::<T>(activity)?;
+  let activity_data = activity.common();
+
+  let request_counter = &mut 0;
+  let actor =
+    get_or_fetch_and_upsert_actor(&activity_data.actor, &context, request_counter).await?;
+  verify_signature(&request, &actor.public_key().context(location_info!())?)?;
+
+  // Do nothing if we received the same activity before
+  if is_activity_already_known(context.pool(), activity_data.id_unchecked()).await? {
+    return Ok(HttpResponse::Ok().finish());
+  }
+  check_is_apub_id_valid(&activity_data.actor, false)?;
+  println!(
+    "Verifying activity {}",
+    activity_data.id_unchecked().to_string()
+  );
+  activity.verify(&context, request_counter).await?;
+  assert_activity_not_local(&activity)?;
+
+  // Log the activity, so we avoid receiving and parsing it twice. Note that this could still happen
+  // if we receive the same activity twice in very quick succession.
+  insert_activity(
+    activity_data.id_unchecked(),
+    activity.clone(),
+    false,
+    true,
+    context.pool(),
+  )
+  .await?;
+
+  println!(
+    "Receiving activity {}",
+    activity_data.id_unchecked().to_string()
+  );
+  activity.receive(&context, request_counter).await?;
+  Ok(HttpResponse::Ok().finish())
+}
 
 /// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
 /// headers.
@@ -36,14 +122,14 @@ where
 }
 
 #[derive(Deserialize)]
-pub struct CommunityQuery {
+pub struct ActivityQuery {
   type_: String,
   id: String,
 }
 
-/// Return the ActivityPub json representation of a local community over HTTP.
+/// Return the ActivityPub json representation of a local activity over HTTP.
 pub(crate) async fn get_activity(
-  info: web::Path<CommunityQuery>,
+  info: web::Path<ActivityQuery>,
   context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse<Body>, LemmyError> {
   let settings = Settings::get();
@@ -66,3 +152,37 @@ pub(crate) async fn get_activity(
     Ok(create_apub_response(&activity.data))
   }
 }
+
+pub(crate) async fn is_activity_already_known(
+  pool: &DbPool,
+  activity_id: &Url,
+) -> Result<bool, LemmyError> {
+  let activity_id = activity_id.to_owned().into();
+  let existing = blocking(pool, move |conn| {
+    Activity::read_from_apub_id(conn, &activity_id)
+  })
+  .await?;
+  match existing {
+    Ok(_) => Ok(true),
+    Err(_) => Ok(false),
+  }
+}
+
+fn assert_activity_not_local<T: Debug + ActivityHandler>(activity: &T) -> Result<(), LemmyError> {
+  let activity_domain = activity
+    .common()
+    .id_unchecked()
+    .domain()
+    .context(location_info!())?;
+
+  if activity_domain == Settings::get().hostname() {
+    return Err(
+      anyhow!(
+        "Error: received activity which was sent by local instance: {:?}",
+        activity
+      )
+      .into(),
+    );
+  }
+  Ok(())
+}
diff --git a/crates/apub_receive/src/http/person.rs b/crates/apub_receive/src/http/person.rs
index 0d0b8f76..59cc8697 100644
--- a/crates/apub_receive/src/http/person.rs
+++ b/crates/apub_receive/src/http/person.rs
@@ -1,9 +1,15 @@
-use crate::http::{create_apub_response, create_apub_tombstone_response};
+use crate::http::{
+  create_apub_response,
+  create_apub_tombstone_response,
+  inbox_enums::PersonInboxActivities,
+  payload_to_string,
+  receive_activity,
+};
 use activitystreams::{
   base::BaseExt,
   collection::{CollectionExt, OrderedCollection},
 };
-use actix_web::{body::Body, web, HttpResponse};
+use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
 use lemmy_api_common::blocking;
 use lemmy_apub::{extensions::context::lemmy_context, objects::ToApub, ActorType};
 use lemmy_db_queries::source::person::Person_;
@@ -39,6 +45,16 @@ pub(crate) async fn get_apub_person_http(
   }
 }
 
+pub async fn person_inbox(
+  request: HttpRequest,
+  payload: Payload,
+  _path: web::Path<String>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError> {
+  let unparsed = payload_to_string(payload).await?;
+  receive_activity::<PersonInboxActivities>(request, &unparsed, context).await
+}
+
 pub(crate) async fn get_apub_person_outbox(
   info: web::Path<PersonQuery>,
   context: web::Data<LemmyContext>,
diff --git a/crates/apub_receive/src/routes.rs b/crates/apub_receive/src/http/routes.rs
similarity index 83%
rename from crates/apub_receive/src/routes.rs
rename to crates/apub_receive/src/http/routes.rs
index d112afbe..929df38b 100644
--- a/crates/apub_receive/src/routes.rs
+++ b/crates/apub_receive/src/http/routes.rs
@@ -1,22 +1,17 @@
-use crate::{
-  http::{
-    comment::get_apub_comment,
-    community::{
-      get_apub_community_followers,
-      get_apub_community_http,
-      get_apub_community_inbox,
-      get_apub_community_moderators,
-      get_apub_community_outbox,
-    },
-    get_activity,
-    person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox},
-    post::get_apub_post,
-  },
-  inbox::{
-    community_inbox::community_inbox,
-    person_inbox::person_inbox,
-    shared_inbox::shared_inbox,
+use crate::http::{
+  comment::get_apub_comment,
+  community::{
+    community_inbox,
+    get_apub_community_followers,
+    get_apub_community_http,
+    get_apub_community_inbox,
+    get_apub_community_moderators,
+    get_apub_community_outbox,
   },
+  get_activity,
+  person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox, person_inbox},
+  post::get_apub_post,
+  shared_inbox,
 };
 use actix_web::*;
 use http_signature_normalization_actix::digest::middleware::VerifyDigest;
diff --git a/crates/apub_receive/src/inbox/community_inbox.rs b/crates/apub_receive/src/inbox/community_inbox.rs
deleted file mode 100644
index d851b7c0..00000000
--- a/crates/apub_receive/src/inbox/community_inbox.rs
+++ /dev/null
@@ -1,346 +0,0 @@
-use crate::{
-  activities::receive::verify_activity_domains_valid,
-  inbox::{
-    assert_activity_not_local,
-    get_activity_id,
-    inbox_verify_http_signature,
-    is_activity_already_known,
-    receive_for_community::{
-      receive_add_for_community,
-      receive_block_user_for_community,
-      receive_create_for_community,
-      receive_delete_for_community,
-      receive_dislike_for_community,
-      receive_like_for_community,
-      receive_remove_for_community,
-      receive_undo_for_community,
-      receive_update_for_community,
-    },
-    verify_is_addressed_to_public,
-  },
-};
-use activitystreams::{
-  activity::{kind::FollowType, ActorAndObject, Follow, Undo},
-  base::AnyBase,
-  prelude::*,
-};
-use actix_web::{web, HttpRequest, HttpResponse};
-use anyhow::{anyhow, Context};
-use lemmy_api_common::blocking;
-use lemmy_apub::{
-  check_community_or_site_ban,
-  get_activity_to_and_cc,
-  insert_activity,
-  ActorType,
-  CommunityType,
-};
-use lemmy_db_queries::{source::community::Community_, ApubObject, Followable};
-use lemmy_db_schema::source::{
-  community::{Community, CommunityFollower, CommunityFollowerForm},
-  person::Person,
-};
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::LemmyContext;
-use log::info;
-use serde::{Deserialize, Serialize};
-use std::fmt::Debug;
-use url::Url;
-
-/// Allowed activities for community inbox.
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum CommunityValidTypes {
-  Follow,  // follow request from a person
-  Undo,    // unfollow from a person
-  Create,  // create post or comment
-  Update,  // update post or comment
-  Like,    // upvote post or comment
-  Dislike, // downvote post or comment
-  Delete,  // post or comment deleted by creator
-  Remove,  // post or comment removed by mod or admin, or mod removed from community
-  Add,     // mod added to community
-  Block,   // user blocked by community
-}
-
-pub type CommunityAcceptedActivities = ActorAndObject<CommunityValidTypes>;
-
-/// Handler for all incoming receive to community inboxes.
-pub async fn community_inbox(
-  request: HttpRequest,
-  input: web::Json<CommunityAcceptedActivities>,
-  path: web::Path<String>,
-  context: web::Data<LemmyContext>,
-) -> Result<HttpResponse, LemmyError> {
-  let activity = input.into_inner();
-  // First of all check the http signature
-  let request_counter = &mut 0;
-  let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
-
-  // Do nothing if we received the same activity before
-  let activity_id = get_activity_id(&activity, &actor.actor_id())?;
-  if is_activity_already_known(context.pool(), &activity_id).await? {
-    return Ok(HttpResponse::Ok().finish());
-  }
-
-  // Check if the activity is actually meant for us
-  let path = path.into_inner();
-  let community = blocking(context.pool(), move |conn| {
-    Community::read_from_name(conn, &path)
-  })
-  .await??;
-  let to_and_cc = get_activity_to_and_cc(&activity);
-  if !to_and_cc.contains(&community.actor_id()) {
-    return Err(anyhow!("Activity delivered to wrong community").into());
-  }
-
-  assert_activity_not_local(&activity)?;
-  insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
-
-  community_receive_message(
-    activity.clone(),
-    community.clone(),
-    actor.as_ref(),
-    &context,
-    request_counter,
-  )
-  .await
-}
-
-/// Receives Follow, Undo/Follow, post actions, comment actions (including votes)
-pub(crate) async fn community_receive_message(
-  activity: CommunityAcceptedActivities,
-  to_community: Community,
-  actor: &dyn ActorType,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<HttpResponse, LemmyError> {
-  // Only persons can send activities to the community, so we can get the actor as person
-  // unconditionally.
-  let actor_id = actor.actor_id();
-  let person = blocking(context.pool(), move |conn| {
-    Person::read_from_apub_id(conn, &actor_id.into())
-  })
-  .await??;
-  check_community_or_site_ban(&person, to_community.id, context.pool()).await?;
-
-  info!(
-    "Community {} received activity {} from {}",
-    to_community.name,
-    &activity
-      .id_unchecked()
-      .context(location_info!())?
-      .to_string(),
-    &person.actor_id().to_string()
-  );
-
-  let any_base = activity.clone().into_any_base()?;
-  let actor_url = actor.actor_id();
-  let activity_kind = activity.kind().context(location_info!())?;
-  let do_announce = match activity_kind {
-    CommunityValidTypes::Follow => {
-      Box::pin(handle_follow(
-        any_base.clone(),
-        person,
-        &to_community,
-        context,
-      ))
-      .await?;
-      false
-    }
-    CommunityValidTypes::Undo => {
-      Box::pin(handle_undo(
-        context,
-        activity.clone(),
-        actor_url,
-        &to_community,
-        request_counter,
-      ))
-      .await?
-    }
-    CommunityValidTypes::Create => {
-      Box::pin(receive_create_for_community(
-        context,
-        any_base.clone(),
-        &actor_url,
-        request_counter,
-      ))
-      .await?;
-      true
-    }
-    CommunityValidTypes::Update => {
-      Box::pin(receive_update_for_community(
-        context,
-        any_base.clone(),
-        None,
-        &actor_url,
-        request_counter,
-      ))
-      .await?;
-      true
-    }
-    CommunityValidTypes::Like => {
-      Box::pin(receive_like_for_community(
-        context,
-        any_base.clone(),
-        &actor_url,
-        request_counter,
-      ))
-      .await?;
-      true
-    }
-    CommunityValidTypes::Dislike => {
-      Box::pin(receive_dislike_for_community(
-        context,
-        any_base.clone(),
-        &actor_url,
-        request_counter,
-      ))
-      .await?;
-      true
-    }
-    CommunityValidTypes::Delete => {
-      Box::pin(receive_delete_for_community(
-        context,
-        any_base.clone(),
-        None,
-        &actor_url,
-        request_counter,
-      ))
-      .await?;
-      true
-    }
-    CommunityValidTypes::Add => {
-      Box::pin(receive_add_for_community(
-        context,
-        any_base.clone(),
-        None,
-        request_counter,
-      ))
-      .await?;
-      true
-    }
-    CommunityValidTypes::Remove => {
-      Box::pin(receive_remove_for_community(
-        context,
-        any_base.clone(),
-        None,
-        request_counter,
-      ))
-      .await?;
-      true
-    }
-    CommunityValidTypes::Block => {
-      Box::pin(receive_block_user_for_community(
-        context,
-        any_base.clone(),
-        None,
-        request_counter,
-      ))
-      .await?;
-      true
-    }
-  };
-
-  if do_announce {
-    // Check again that the activity is public, just to be sure
-    verify_is_addressed_to_public(&activity)?;
-    let mut object_actor = activity.object().clone().single_xsd_any_uri();
-    // If activity is something like Undo/Block, we need to access activity.object.object
-    if object_actor.is_none() {
-      object_actor = activity
-        .object()
-        .as_one()
-        .map(|a| ActorAndObject::from_any_base(a.to_owned()).ok())
-        .flatten()
-        .flatten()
-        .map(|a: ActorAndObject<CommunityValidTypes>| a.object().to_owned().single_xsd_any_uri())
-        .flatten();
-    }
-    to_community
-      .send_announce(activity.into_any_base()?, object_actor, context)
-      .await?;
-  }
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-/// Handle a follow request from a remote person, adding the person as follower and returning an
-/// Accept activity.
-async fn handle_follow(
-  activity: AnyBase,
-  person: Person,
-  community: &Community,
-  context: &LemmyContext,
-) -> Result<HttpResponse, LemmyError> {
-  let follow = Follow::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
-
-  let community_follower_form = CommunityFollowerForm {
-    community_id: community.id,
-    person_id: person.id,
-    pending: false,
-  };
-
-  // This will fail if they're already a follower, but ignore the error.
-  blocking(context.pool(), move |conn| {
-    CommunityFollower::follow(conn, &community_follower_form).ok()
-  })
-  .await?;
-
-  community.send_accept_follow(follow, context).await?;
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn handle_undo(
-  context: &LemmyContext,
-  activity: CommunityAcceptedActivities,
-  actor_url: Url,
-  to_community: &Community,
-  request_counter: &mut i32,
-) -> Result<bool, LemmyError> {
-  let inner_kind = activity
-    .object()
-    .is_single_kind(&FollowType::Follow.to_string());
-  let any_base = activity.into_any_base()?;
-  if inner_kind {
-    handle_undo_follow(any_base, actor_url, to_community, context).await?;
-    Ok(false)
-  } else {
-    receive_undo_for_community(context, any_base, None, &actor_url, request_counter).await?;
-    Ok(true)
-  }
-}
-
-/// Handle `Undo/Follow` from a person, removing the person from followers list.
-async fn handle_undo_follow(
-  activity: AnyBase,
-  person_url: Url,
-  community: &Community,
-  context: &LemmyContext,
-) -> Result<(), LemmyError> {
-  let undo = Undo::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&undo, &person_url, true)?;
-
-  let object = undo.object().to_owned().one().context(location_info!())?;
-  let follow = Follow::from_any_base(object)?.context(location_info!())?;
-  verify_activity_domains_valid(&follow, &person_url, false)?;
-
-  let person = blocking(context.pool(), move |conn| {
-    Person::read_from_apub_id(conn, &person_url.into())
-  })
-  .await??;
-  let community_follower_form = CommunityFollowerForm {
-    community_id: community.id,
-    person_id: person.id,
-    pending: false,
-  };
-
-  // This will fail if they aren't a follower, but ignore the error.
-  blocking(context.pool(), move |conn| {
-    CommunityFollower::unfollow(conn, &community_follower_form).ok()
-  })
-  .await?;
-
-  Ok(())
-}
diff --git a/crates/apub_receive/src/inbox/mod.rs b/crates/apub_receive/src/inbox/mod.rs
deleted file mode 100644
index 6b6f7b3f..00000000
--- a/crates/apub_receive/src/inbox/mod.rs
+++ /dev/null
@@ -1,153 +0,0 @@
-use activitystreams::{
-  activity::ActorAndObjectRefExt,
-  base::{AsBase, BaseExt, Extends},
-  object::AsObject,
-  public,
-};
-use actix_web::HttpRequest;
-use anyhow::{anyhow, Context};
-use lemmy_api_common::blocking;
-use lemmy_apub::{
-  check_is_apub_id_valid,
-  extensions::signatures::verify_signature,
-  fetcher::get_or_fetch_and_upsert_actor,
-  get_activity_to_and_cc,
-  ActorType,
-};
-use lemmy_db_queries::{
-  source::{activity::Activity_, community::Community_},
-  ApubObject,
-  DbPool,
-};
-use lemmy_db_schema::source::{activity::Activity, community::Community, person::Person};
-use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
-use lemmy_websocket::LemmyContext;
-use serde::Serialize;
-use std::fmt::Debug;
-use url::Url;
-
-pub mod community_inbox;
-pub mod person_inbox;
-pub(crate) mod receive_for_community;
-pub mod shared_inbox;
-
-pub(crate) fn get_activity_id<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError>
-where
-  T: BaseExt<Kind> + Extends<Kind> + Debug,
-  Kind: Serialize,
-  <T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
-{
-  let creator_domain = creator_uri.host_str().context(location_info!())?;
-  let activity_id = activity.id(creator_domain)?;
-  Ok(activity_id.context(location_info!())?.to_owned())
-}
-
-pub(crate) async fn is_activity_already_known(
-  pool: &DbPool,
-  activity_id: &Url,
-) -> Result<bool, LemmyError> {
-  let activity_id = activity_id.to_owned().into();
-  let existing = blocking(pool, move |conn| {
-    Activity::read_from_apub_id(conn, &activity_id)
-  })
-  .await?;
-  match existing {
-    Ok(_) => Ok(true),
-    Err(_) => Ok(false),
-  }
-}
-
-pub(crate) fn verify_is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError>
-where
-  T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
-{
-  let to_and_cc = get_activity_to_and_cc(activity);
-  if to_and_cc.contains(&public()) {
-    Ok(())
-  } else {
-    Err(anyhow!("Activity is not addressed to public").into())
-  }
-}
-
-pub(crate) async fn inbox_verify_http_signature<T, Kind>(
-  activity: &T,
-  context: &LemmyContext,
-  request: HttpRequest,
-  request_counter: &mut i32,
-) -> Result<Box<dyn ActorType>, LemmyError>
-where
-  T: AsObject<Kind> + ActorAndObjectRefExt + Extends<Kind> + AsBase<Kind>,
-  Kind: Serialize,
-  <T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
-{
-  let actor_id = activity
-    .actor()?
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-  check_is_apub_id_valid(&actor_id, false)?;
-  let actor = get_or_fetch_and_upsert_actor(&actor_id, context, request_counter).await?;
-  verify_signature(&request, actor.as_ref())?;
-  Ok(actor)
-}
-
-/// Returns true if `to_and_cc` contains at least one local user.
-pub(crate) async fn is_addressed_to_local_person(
-  to_and_cc: &[Url],
-  pool: &DbPool,
-) -> Result<bool, LemmyError> {
-  for url in to_and_cc {
-    let url = url.to_owned();
-    let person = blocking(pool, move |conn| {
-      Person::read_from_apub_id(conn, &url.into())
-    })
-    .await?;
-    if let Ok(u) = person {
-      if u.local {
-        return Ok(true);
-      }
-    }
-  }
-  Ok(false)
-}
-
-/// If `to_and_cc` contains the followers collection of a remote community, returns this community
-/// (like `https://example.com/c/main/followers`)
-pub(crate) async fn is_addressed_to_community_followers(
-  to_and_cc: &[Url],
-  pool: &DbPool,
-) -> Result<Option<Community>, LemmyError> {
-  for url in to_and_cc {
-    let url = url.to_owned().into();
-    let community = blocking(pool, move |conn| {
-      // ignore errors here, because the current url might not actually be a followers url
-      Community::read_from_followers_url(conn, &url).ok()
-    })
-    .await?;
-    if let Some(c) = community {
-      if !c.local {
-        return Ok(Some(c));
-      }
-    }
-  }
-  Ok(None)
-}
-
-pub(in crate::inbox) fn assert_activity_not_local<T, Kind>(activity: &T) -> Result<(), LemmyError>
-where
-  T: BaseExt<Kind> + Debug,
-{
-  let id = activity.id_unchecked().context(location_info!())?;
-  let activity_domain = id.domain().context(location_info!())?;
-
-  if activity_domain == Settings::get().hostname() {
-    return Err(
-      anyhow!(
-        "Error: received activity which was sent by local instance: {:?}",
-        activity
-      )
-      .into(),
-    );
-  }
-  Ok(())
-}
diff --git a/crates/apub_receive/src/inbox/person_inbox.rs b/crates/apub_receive/src/inbox/person_inbox.rs
deleted file mode 100644
index 080e143c..00000000
--- a/crates/apub_receive/src/inbox/person_inbox.rs
+++ /dev/null
@@ -1,515 +0,0 @@
-use crate::{
-  activities::receive::{
-    comment::{receive_create_comment, receive_update_comment},
-    community::{
-      receive_delete_community,
-      receive_remove_community,
-      receive_undo_delete_community,
-      receive_undo_remove_community,
-    },
-    private_message::{
-      receive_create_private_message,
-      receive_delete_private_message,
-      receive_undo_delete_private_message,
-      receive_update_private_message,
-    },
-    receive_unhandled_activity,
-    verify_activity_domains_valid,
-  },
-  inbox::{
-    assert_activity_not_local,
-    get_activity_id,
-    inbox_verify_http_signature,
-    is_activity_already_known,
-    is_addressed_to_community_followers,
-    is_addressed_to_local_person,
-    receive_for_community::{
-      receive_add_for_community,
-      receive_block_user_for_community,
-      receive_create_for_community,
-      receive_delete_for_community,
-      receive_dislike_for_community,
-      receive_like_for_community,
-      receive_remove_for_community,
-      receive_undo_for_community,
-      receive_update_for_community,
-    },
-    verify_is_addressed_to_public,
-  },
-};
-use activitystreams::{
-  activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Remove, Undo, Update},
-  base::AnyBase,
-  prelude::*,
-};
-use actix_web::{web, HttpRequest, HttpResponse};
-use anyhow::{anyhow, Context};
-use diesel::NotFound;
-use lemmy_api_common::blocking;
-use lemmy_apub::{
-  check_is_apub_id_valid,
-  fetcher::community::get_or_fetch_and_upsert_community,
-  get_activity_to_and_cc,
-  insert_activity,
-  ActorType,
-};
-use lemmy_db_queries::{source::person::Person_, ApubObject, Followable};
-use lemmy_db_schema::source::{
-  community::{Community, CommunityFollower},
-  person::Person,
-  private_message::PrivateMessage,
-};
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::LemmyContext;
-use log::info;
-use serde::{Deserialize, Serialize};
-use std::fmt::Debug;
-use strum_macros::EnumString;
-use url::Url;
-
-/// Allowed activities for person inbox.
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum PersonValidTypes {
-  Accept,   // community accepted our follow request
-  Create,   // create private message
-  Update,   // edit private message
-  Delete,   // private message or community deleted by creator
-  Undo,     // private message or community restored
-  Remove,   // community removed by admin
-  Announce, // post, comment or vote in community
-}
-
-pub type PersonAcceptedActivities = ActorAndObject<PersonValidTypes>;
-
-/// Handler for all incoming activities to person inboxes.
-pub async fn person_inbox(
-  request: HttpRequest,
-  input: web::Json<PersonAcceptedActivities>,
-  path: web::Path<String>,
-  context: web::Data<LemmyContext>,
-) -> Result<HttpResponse, LemmyError> {
-  let activity = input.into_inner();
-  // First of all check the http signature
-  let request_counter = &mut 0;
-  let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
-
-  // Do nothing if we received the same activity before
-  let activity_id = get_activity_id(&activity, &actor.actor_id())?;
-  if is_activity_already_known(context.pool(), &activity_id).await? {
-    return Ok(HttpResponse::Ok().finish());
-  }
-
-  // Check if the activity is actually meant for us
-  let username = path.into_inner();
-  let person = blocking(context.pool(), move |conn| {
-    Person::find_by_name(conn, &username)
-  })
-  .await??;
-  let to_and_cc = get_activity_to_and_cc(&activity);
-  // TODO: we should also accept activities that are sent to community followers
-  if !to_and_cc.contains(&person.actor_id()) {
-    return Err(anyhow!("Activity delivered to wrong person").into());
-  }
-
-  assert_activity_not_local(&activity)?;
-  insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
-
-  person_receive_message(
-    activity.clone(),
-    Some(person.clone()),
-    actor.as_ref(),
-    &context,
-    request_counter,
-  )
-  .await
-}
-
-/// Receives Accept/Follow, Announce, private messages and community (undo) remove, (undo) delete
-pub(crate) async fn person_receive_message(
-  activity: PersonAcceptedActivities,
-  to_person: Option<Person>,
-  actor: &dyn ActorType,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<HttpResponse, LemmyError> {
-  is_for_person_inbox(context, &activity).await?;
-
-  info!(
-    "User received activity {:?} from {}",
-    &activity
-      .id_unchecked()
-      .context(location_info!())?
-      .to_string(),
-    &actor.actor_id().to_string()
-  );
-
-  let any_base = activity.clone().into_any_base()?;
-  let kind = activity.kind().context(location_info!())?;
-  let actor_url = actor.actor_id();
-  match kind {
-    PersonValidTypes::Accept => {
-      receive_accept(
-        context,
-        any_base,
-        actor,
-        to_person.expect("person provided"),
-        request_counter,
-      )
-      .await?;
-    }
-    PersonValidTypes::Announce => {
-      Box::pin(receive_announce(context, any_base, actor, request_counter)).await?
-    }
-    PersonValidTypes::Create => {
-      Box::pin(receive_create(
-        context,
-        any_base,
-        actor_url,
-        request_counter,
-      ))
-      .await?
-    }
-    PersonValidTypes::Update => {
-      Box::pin(receive_update(
-        context,
-        any_base,
-        actor_url,
-        request_counter,
-      ))
-      .await?
-    }
-    PersonValidTypes::Delete => {
-      Box::pin(receive_delete(
-        context,
-        any_base,
-        &actor_url,
-        request_counter,
-      ))
-      .await?
-    }
-    PersonValidTypes::Undo => {
-      Box::pin(receive_undo(context, any_base, &actor_url, request_counter)).await?
-    }
-    PersonValidTypes::Remove => Box::pin(receive_remove(context, any_base, &actor_url)).await?,
-  };
-
-  // TODO: would be logical to move websocket notification code here
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-/// Returns true if the activity is addressed directly to one or more local persons, or if it is
-/// addressed to the followers collection of a remote community, and at least one local person follows
-/// it.
-async fn is_for_person_inbox(
-  context: &LemmyContext,
-  activity: &PersonAcceptedActivities,
-) -> Result<(), LemmyError> {
-  let to_and_cc = get_activity_to_and_cc(activity);
-  // Check if it is addressed directly to any local person
-  if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
-    return Ok(());
-  }
-
-  // Check if it is addressed to any followers collection of a remote community, and that the
-  // community has local followers.
-  let community = is_addressed_to_community_followers(&to_and_cc, context.pool()).await?;
-  if let Some(c) = community {
-    let community_id = c.id;
-    let has_local_followers = blocking(context.pool(), move |conn| {
-      CommunityFollower::has_local_followers(conn, community_id)
-    })
-    .await??;
-    if c.local {
-      return Err(
-        anyhow!("Remote activity cant be addressed to followers of local community").into(),
-      );
-    }
-    if has_local_followers {
-      return Ok(());
-    }
-  }
-
-  Err(anyhow!("Not addressed for any local person").into())
-}
-
-/// Handle accepted follows.
-async fn receive_accept(
-  context: &LemmyContext,
-  activity: AnyBase,
-  actor: &dyn ActorType,
-  person: Person,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let accept = Accept::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&accept, &actor.actor_id(), false)?;
-
-  let object = accept.object().to_owned().one().context(location_info!())?;
-  let follow = Follow::from_any_base(object)?.context(location_info!())?;
-  verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
-
-  let community_uri = accept
-    .actor()?
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-
-  let community =
-    get_or_fetch_and_upsert_community(&community_uri, context, request_counter).await?;
-
-  let community_id = community.id;
-  let person_id = person.id;
-  // This will throw an error if no follow was requested
-  blocking(context.pool(), move |conn| {
-    CommunityFollower::follow_accepted(conn, community_id, person_id)
-  })
-  .await??;
-
-  Ok(())
-}
-
-#[derive(EnumString)]
-enum AnnouncableActivities {
-  Create,
-  Update,
-  Like,
-  Dislike,
-  Delete,
-  Remove,
-  Undo,
-  Add,
-  Block,
-}
-
-/// Takes an announce and passes the inner activity to the appropriate handler.
-pub async fn receive_announce(
-  context: &LemmyContext,
-  activity: AnyBase,
-  actor: &dyn ActorType,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let announce = Announce::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&announce, &actor.actor_id(), false)?;
-  verify_is_addressed_to_public(&announce)?;
-
-  let kind = announce
-    .object()
-    .as_single_kind_str()
-    .and_then(|s| s.parse().ok());
-  let inner_activity = announce
-    .object()
-    .to_owned()
-    .one()
-    .context(location_info!())?;
-
-  let inner_id = inner_activity.id().context(location_info!())?.to_owned();
-  check_is_apub_id_valid(&inner_id, false)?;
-  if is_activity_already_known(context.pool(), &inner_id).await? {
-    return Ok(());
-  }
-
-  use AnnouncableActivities::*;
-  match kind {
-    Some(Create) => {
-      receive_create_for_community(context, inner_activity, &inner_id, request_counter).await
-    }
-    Some(Update) => {
-      receive_update_for_community(
-        context,
-        inner_activity,
-        Some(announce),
-        &inner_id,
-        request_counter,
-      )
-      .await
-    }
-    Some(Like) => {
-      receive_like_for_community(context, inner_activity, &inner_id, request_counter).await
-    }
-    Some(Dislike) => {
-      receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await
-    }
-    Some(Delete) => {
-      receive_delete_for_community(
-        context,
-        inner_activity,
-        Some(announce),
-        &inner_id,
-        request_counter,
-      )
-      .await
-    }
-    Some(Remove) => {
-      receive_remove_for_community(context, inner_activity, Some(announce), request_counter).await
-    }
-    Some(Undo) => {
-      receive_undo_for_community(
-        context,
-        inner_activity,
-        Some(announce),
-        &inner_id,
-        request_counter,
-      )
-      .await
-    }
-    Some(Add) => {
-      receive_add_for_community(context, inner_activity, Some(announce), request_counter).await
-    }
-    Some(Block) => {
-      receive_block_user_for_community(context, inner_activity, Some(announce), request_counter)
-        .await
-    }
-    _ => receive_unhandled_activity(inner_activity),
-  }
-}
-
-/// Receive either a new private message, or a new comment mention. We distinguish them by checking
-/// whether the activity is public.
-async fn receive_create(
-  context: &LemmyContext,
-  activity: AnyBase,
-  expected_domain: Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let create = Create::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&create, &expected_domain, true)?;
-  if verify_is_addressed_to_public(&create).is_ok() {
-    receive_create_comment(create, context, request_counter).await
-  } else {
-    receive_create_private_message(context, create, expected_domain, request_counter).await
-  }
-}
-
-/// Receive either an updated private message, or an updated comment mention. We distinguish
-/// them by checking whether the activity is public.
-async fn receive_update(
-  context: &LemmyContext,
-  activity: AnyBase,
-  expected_domain: Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let update = Update::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&update, &expected_domain, true)?;
-  if verify_is_addressed_to_public(&update).is_ok() {
-    receive_update_comment(update, context, request_counter).await
-  } else {
-    receive_update_private_message(context, update, expected_domain, request_counter).await
-  }
-}
-
-async fn receive_delete(
-  context: &LemmyContext,
-  any_base: AnyBase,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  use CommunityOrPrivateMessage::*;
-
-  let delete = Delete::from_any_base(any_base.clone())?.context(location_info!())?;
-  verify_activity_domains_valid(&delete, expected_domain, true)?;
-  let object_uri = delete
-    .object()
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-
-  match find_community_or_private_message_by_id(context, object_uri).await? {
-    Community(c) => receive_delete_community(context, c).await,
-    PrivateMessage(p) => receive_delete_private_message(context, delete, p, request_counter).await,
-  }
-}
-
-async fn receive_remove(
-  context: &LemmyContext,
-  any_base: AnyBase,
-  expected_domain: &Url,
-) -> Result<(), LemmyError> {
-  let remove = Remove::from_any_base(any_base.clone())?.context(location_info!())?;
-  verify_activity_domains_valid(&remove, expected_domain, true)?;
-  let object_uri = remove
-    .object()
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-  let community = blocking(context.pool(), move |conn| {
-    Community::read_from_apub_id(conn, &object_uri.into())
-  })
-  .await??;
-  receive_remove_community(context, community).await
-}
-
-async fn receive_undo(
-  context: &LemmyContext,
-  any_base: AnyBase,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let undo = Undo::from_any_base(any_base)?.context(location_info!())?;
-  verify_activity_domains_valid(&undo, expected_domain, true)?;
-
-  let inner_activity = undo.object().to_owned().one().context(location_info!())?;
-  let kind = inner_activity.kind_str();
-  match kind {
-    Some("Delete") => {
-      let delete = Delete::from_any_base(inner_activity)?.context(location_info!())?;
-      verify_activity_domains_valid(&delete, expected_domain, true)?;
-      let object_uri = delete
-        .object()
-        .to_owned()
-        .single_xsd_any_uri()
-        .context(location_info!())?;
-      use CommunityOrPrivateMessage::*;
-      match find_community_or_private_message_by_id(context, object_uri).await? {
-        Community(c) => receive_undo_delete_community(context, c).await,
-        PrivateMessage(p) => {
-          receive_undo_delete_private_message(context, undo, expected_domain, p, request_counter)
-            .await
-        }
-      }
-    }
-    Some("Remove") => {
-      let remove = Remove::from_any_base(inner_activity)?.context(location_info!())?;
-      let object_uri = remove
-        .object()
-        .to_owned()
-        .single_xsd_any_uri()
-        .context(location_info!())?;
-      let community = blocking(context.pool(), move |conn| {
-        Community::read_from_apub_id(conn, &object_uri.into())
-      })
-      .await??;
-      receive_undo_remove_community(context, community).await
-    }
-    _ => receive_unhandled_activity(undo),
-  }
-}
-enum CommunityOrPrivateMessage {
-  Community(Community),
-  PrivateMessage(PrivateMessage),
-}
-
-async fn find_community_or_private_message_by_id(
-  context: &LemmyContext,
-  apub_id: Url,
-) -> Result<CommunityOrPrivateMessage, LemmyError> {
-  let ap_id = apub_id.to_owned();
-  let community = blocking(context.pool(), move |conn| {
-    Community::read_from_apub_id(conn, &ap_id.into())
-  })
-  .await?;
-  if let Ok(c) = community {
-    return Ok(CommunityOrPrivateMessage::Community(c));
-  }
-
-  let ap_id = apub_id.to_owned();
-  let private_message = blocking(context.pool(), move |conn| {
-    PrivateMessage::read_from_apub_id(conn, &ap_id.into())
-  })
-  .await?;
-  if let Ok(p) = private_message {
-    return Ok(CommunityOrPrivateMessage::PrivateMessage(p));
-  }
-
-  Err(NotFound.into())
-}
diff --git a/crates/apub_receive/src/inbox/receive_for_community.rs b/crates/apub_receive/src/inbox/receive_for_community.rs
deleted file mode 100644
index 4bb8d325..00000000
--- a/crates/apub_receive/src/inbox/receive_for_community.rs
+++ /dev/null
@@ -1,802 +0,0 @@
-use crate::{
-  activities::receive::{
-    comment::{
-      receive_create_comment,
-      receive_delete_comment,
-      receive_dislike_comment,
-      receive_like_comment,
-      receive_remove_comment,
-      receive_update_comment,
-    },
-    comment_undo::{
-      receive_undo_delete_comment,
-      receive_undo_dislike_comment,
-      receive_undo_like_comment,
-      receive_undo_remove_comment,
-    },
-    community::{
-      receive_remote_mod_delete_community,
-      receive_remote_mod_undo_delete_community,
-      receive_remote_mod_update_community,
-    },
-    post::{
-      receive_create_post,
-      receive_delete_post,
-      receive_dislike_post,
-      receive_like_post,
-      receive_remove_post,
-      receive_update_post,
-    },
-    post_undo::{
-      receive_undo_delete_post,
-      receive_undo_dislike_post,
-      receive_undo_like_post,
-      receive_undo_remove_post,
-    },
-    receive_unhandled_activity,
-    verify_activity_domains_valid,
-  },
-  inbox::verify_is_addressed_to_public,
-};
-use activitystreams::{
-  activity::{
-    ActorAndObjectRef,
-    Add,
-    Announce,
-    Block,
-    Create,
-    Delete,
-    Dislike,
-    Like,
-    OptTargetRef,
-    Remove,
-    Undo,
-    Update,
-  },
-  base::AnyBase,
-  object::AsObject,
-  prelude::*,
-};
-use anyhow::{anyhow, Context};
-use diesel::result::Error::NotFound;
-use lemmy_api_common::blocking;
-use lemmy_apub::{
-  fetcher::{
-    objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
-    person::get_or_fetch_and_upsert_person,
-  },
-  find_object_by_id,
-  find_post_or_comment_by_id,
-  generate_moderators_url,
-  ActorType,
-  CommunityType,
-  Object,
-  PostOrComment,
-};
-use lemmy_db_queries::{
-  source::community::CommunityModerator_,
-  ApubObject,
-  Bannable,
-  Crud,
-  Followable,
-  Joinable,
-};
-use lemmy_db_schema::{
-  source::{
-    community::{
-      Community,
-      CommunityFollower,
-      CommunityFollowerForm,
-      CommunityModerator,
-      CommunityModeratorForm,
-      CommunityPersonBan,
-      CommunityPersonBanForm,
-    },
-    person::Person,
-    site::Site,
-  },
-  DbUrl,
-};
-use lemmy_db_views_actor::community_view::CommunityView;
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::LemmyContext;
-use strum_macros::EnumString;
-use url::Url;
-
-#[derive(EnumString)]
-enum PageOrNote {
-  Page,
-  Note,
-}
-
-#[derive(EnumString)]
-enum ObjectTypes {
-  Page,
-  Note,
-  Group,
-  Person,
-}
-
-/// This file is for post/comment activities received by the community, and for post/comment
-///       activities announced by the community and received by the person.
-
-/// A post or comment being created
-pub(in crate::inbox) async fn receive_create_for_community(
-  context: &LemmyContext,
-  activity: AnyBase,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let create = Create::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&create, expected_domain, true)?;
-  verify_is_addressed_to_public(&create)?;
-
-  let kind = create
-    .object()
-    .as_single_kind_str()
-    .and_then(|s| s.parse().ok());
-  match kind {
-    Some(ObjectTypes::Page) => receive_create_post(create, context, request_counter).await,
-    Some(ObjectTypes::Note) => receive_create_comment(create, context, request_counter).await,
-    _ => receive_unhandled_activity(create),
-  }
-}
-
-/// A post or comment being edited
-pub(in crate::inbox) async fn receive_update_for_community(
-  context: &LemmyContext,
-  activity: AnyBase,
-  announce: Option<Announce>,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let update = Update::from_any_base(activity.to_owned())?.context(location_info!())?;
-  verify_activity_domains_valid(&update, expected_domain, false)?;
-  verify_is_addressed_to_public(&update)?;
-  verify_modification_actor_instance(&update, &announce, context, request_counter).await?;
-
-  let kind = update
-    .object()
-    .as_single_kind_str()
-    .and_then(|s| s.parse().ok());
-  match kind {
-    Some(ObjectTypes::Page) => {
-      receive_update_post(update, announce, context, request_counter).await
-    }
-    Some(ObjectTypes::Note) => receive_update_comment(update, context, request_counter).await,
-    Some(ObjectTypes::Group) => {
-      receive_remote_mod_update_community(update, context, request_counter).await
-    }
-    _ => receive_unhandled_activity(update),
-  }
-}
-
-/// A post or comment being upvoted
-pub(in crate::inbox) async fn receive_like_for_community(
-  context: &LemmyContext,
-  activity: AnyBase,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let like = Like::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&like, expected_domain, false)?;
-  verify_is_addressed_to_public(&like)?;
-
-  let object_id = like
-    .object()
-    .as_single_xsd_any_uri()
-    .context(location_info!())?;
-  match fetch_post_or_comment_by_id(object_id, context, request_counter).await? {
-    PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await,
-    PostOrComment::Comment(comment) => {
-      receive_like_comment(like, *comment, context, request_counter).await
-    }
-  }
-}
-
-/// A post or comment being downvoted
-pub(in crate::inbox) async fn receive_dislike_for_community(
-  context: &LemmyContext,
-  activity: AnyBase,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let enable_downvotes = blocking(context.pool(), move |conn| {
-    Site::read(conn, 1).map(|s| s.enable_downvotes)
-  })
-  .await??;
-  if !enable_downvotes {
-    return Ok(());
-  }
-
-  let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&dislike, expected_domain, false)?;
-  verify_is_addressed_to_public(&dislike)?;
-
-  let object_id = dislike
-    .object()
-    .as_single_xsd_any_uri()
-    .context(location_info!())?;
-  match fetch_post_or_comment_by_id(object_id, context, request_counter).await? {
-    PostOrComment::Post(post) => {
-      receive_dislike_post(dislike, *post, context, request_counter).await
-    }
-    PostOrComment::Comment(comment) => {
-      receive_dislike_comment(dislike, *comment, context, request_counter).await
-    }
-  }
-}
-
-/// A post or comment being deleted by its creator
-pub(in crate::inbox) async fn receive_delete_for_community(
-  context: &LemmyContext,
-  activity: AnyBase,
-  announce: Option<Announce>,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let delete = Delete::from_any_base(activity)?.context(location_info!())?;
-  // TODO: skip this check if action is done by remote mod
-  verify_is_addressed_to_public(&delete)?;
-  verify_modification_actor_instance(&delete, &announce, context, request_counter).await?;
-
-  let object = delete
-    .object()
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-
-  match find_object_by_id(context, object).await {
-    Ok(Object::Post(p)) => {
-      verify_activity_domains_valid(&delete, expected_domain, true)?;
-      receive_delete_post(context, *p).await
-    }
-    Ok(Object::Comment(c)) => {
-      verify_activity_domains_valid(&delete, expected_domain, true)?;
-      receive_delete_comment(context, *c).await
-    }
-    Ok(Object::Community(c)) => {
-      receive_remote_mod_delete_community(delete, *c, context, request_counter).await
-    }
-    // if we dont have the object or dont support its deletion, no need to do anything
-    _ => Ok(()),
-  }
-}
-
-/// A post or comment being removed by a mod/admin
-pub(in crate::inbox) async fn receive_remove_for_community(
-  context: &LemmyContext,
-  remove_any_base: AnyBase,
-  announce: Option<Announce>,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?;
-  let community = extract_community_from_cc(&remove, context).await?;
-
-  verify_mod_activity(&remove, announce, &community, context).await?;
-  verify_is_addressed_to_public(&remove)?;
-
-  if remove.target().is_some() {
-    let remove_mod = remove
-      .object()
-      .as_single_xsd_any_uri()
-      .context(location_info!())?;
-    let remove_mod = get_or_fetch_and_upsert_person(remove_mod, context, request_counter).await?;
-    let form = CommunityModeratorForm {
-      community_id: community.id,
-      person_id: remove_mod.id,
-    };
-    blocking(context.pool(), move |conn| {
-      CommunityModerator::leave(conn, &form)
-    })
-    .await??;
-    community
-      .send_announce(
-        remove_any_base,
-        remove.object().clone().single_xsd_any_uri(),
-        context,
-      )
-      .await?;
-    // TODO: send websocket notification about removed mod
-    Ok(())
-  }
-  // Remove a post or comment
-  else {
-    let object = remove
-      .object()
-      .to_owned()
-      .single_xsd_any_uri()
-      .context(location_info!())?;
-
-    match find_post_or_comment_by_id(context, object).await {
-      Ok(PostOrComment::Post(p)) => receive_remove_post(context, *p).await,
-      Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, *c).await,
-      // if we dont have the object, no need to do anything
-      Err(_) => Ok(()),
-    }
-  }
-}
-
-#[derive(EnumString)]
-enum UndoableActivities {
-  Delete,
-  Remove,
-  Like,
-  Dislike,
-  Block,
-}
-
-/// A post/comment action being reverted (either a delete, remove, upvote or downvote)
-pub(in crate::inbox) async fn receive_undo_for_community(
-  context: &LemmyContext,
-  activity: AnyBase,
-  announce: Option<Announce>,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let undo = Undo::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
-  verify_is_addressed_to_public(&undo)?;
-
-  use UndoableActivities::*;
-  match undo
-    .object()
-    .as_single_kind_str()
-    .and_then(|s| s.parse().ok())
-  {
-    Some(Delete) => {
-      receive_undo_delete_for_community(context, undo, expected_domain, request_counter).await
-    }
-    Some(Remove) => {
-      receive_undo_remove_for_community(context, undo, announce, expected_domain).await
-    }
-    Some(Like) => {
-      receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
-    }
-    Some(Dislike) => {
-      receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
-    }
-    Some(Block) => {
-      receive_undo_block_user_for_community(
-        context,
-        undo,
-        announce,
-        expected_domain,
-        request_counter,
-      )
-      .await
-    }
-    _ => receive_unhandled_activity(undo),
-  }
-}
-
-/// A post, comment or community deletion being reverted
-pub(in crate::inbox) async fn receive_undo_delete_for_community(
-  context: &LemmyContext,
-  undo: Undo,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-  verify_is_addressed_to_public(&delete)?;
-
-  let object = delete
-    .object()
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-  match find_object_by_id(context, object).await {
-    Ok(Object::Post(p)) => {
-      verify_activity_domains_valid(&delete, expected_domain, true)?;
-      receive_undo_delete_post(context, *p).await
-    }
-    Ok(Object::Comment(c)) => {
-      verify_activity_domains_valid(&delete, expected_domain, true)?;
-      receive_undo_delete_comment(context, *c).await
-    }
-    Ok(Object::Community(c)) => {
-      verify_actor_is_community_mod(&undo, &c, context).await?;
-      receive_remote_mod_undo_delete_community(undo, *c, context, request_counter).await
-    }
-    // if we dont have the object or dont support its deletion, no need to do anything
-    _ => Ok(()),
-  }
-}
-
-/// A post or comment removal being reverted
-pub(in crate::inbox) async fn receive_undo_remove_for_community(
-  context: &LemmyContext,
-  undo: Undo,
-  announce: Option<Announce>,
-  expected_domain: &Url,
-) -> Result<(), LemmyError> {
-  let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-  verify_activity_domains_valid(&remove, expected_domain, false)?;
-  verify_is_addressed_to_public(&remove)?;
-  verify_undo_remove_actor_instance(&undo, &remove, &announce, context).await?;
-
-  let object = remove
-    .object()
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-  match find_post_or_comment_by_id(context, object).await {
-    Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, *p).await,
-    Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, *c).await,
-    // if we dont have the object, no need to do anything
-    Err(_) => Ok(()),
-  }
-}
-
-/// A post or comment upvote being reverted
-pub(in crate::inbox) async fn receive_undo_like_for_community(
-  context: &LemmyContext,
-  undo: Undo,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-  verify_activity_domains_valid(&like, expected_domain, false)?;
-  verify_is_addressed_to_public(&like)?;
-
-  let object_id = like
-    .object()
-    .as_single_xsd_any_uri()
-    .context(location_info!())?;
-  match fetch_post_or_comment_by_id(object_id, context, request_counter).await? {
-    PostOrComment::Post(post) => {
-      receive_undo_like_post(&like, *post, context, request_counter).await
-    }
-    PostOrComment::Comment(comment) => {
-      receive_undo_like_comment(&like, *comment, context, request_counter).await
-    }
-  }
-}
-
-/// Add a new mod to the community (can only be done by an existing mod).
-pub(in crate::inbox) async fn receive_add_for_community(
-  context: &LemmyContext,
-  add_any_base: AnyBase,
-  announce: Option<Announce>,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let add = Add::from_any_base(add_any_base.to_owned())?.context(location_info!())?;
-  let community = extract_community_from_cc(&add, context).await?;
-
-  verify_mod_activity(&add, announce, &community, context).await?;
-  verify_is_addressed_to_public(&add)?;
-  verify_add_remove_moderator_target(&add, &community)?;
-
-  let new_mod = add
-    .object()
-    .as_single_xsd_any_uri()
-    .context(location_info!())?;
-  let new_mod = get_or_fetch_and_upsert_person(new_mod, context, request_counter).await?;
-
-  // If we had to refetch the community while parsing the activity, then the new mod has already
-  // been added. Skip it here as it would result in a duplicate key error.
-  let new_mod_id = new_mod.id;
-  let moderated_communities = blocking(context.pool(), move |conn| {
-    CommunityModerator::get_person_moderated_communities(conn, new_mod_id)
-  })
-  .await??;
-  if !moderated_communities.contains(&community.id) {
-    let form = CommunityModeratorForm {
-      community_id: community.id,
-      person_id: new_mod.id,
-    };
-    blocking(context.pool(), move |conn| {
-      CommunityModerator::join(conn, &form)
-    })
-    .await??;
-  }
-  if community.local {
-    community
-      .send_announce(
-        add_any_base,
-        add.object().clone().single_xsd_any_uri(),
-        context,
-      )
-      .await?;
-  }
-  // TODO: send websocket notification about added mod
-  Ok(())
-}
-
-/// A post or comment downvote being reverted
-pub(in crate::inbox) async fn receive_undo_dislike_for_community(
-  context: &LemmyContext,
-  undo: Undo,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
-    .context(location_info!())?;
-  verify_activity_domains_valid(&dislike, expected_domain, false)?;
-  verify_is_addressed_to_public(&dislike)?;
-
-  let object_id = dislike
-    .object()
-    .as_single_xsd_any_uri()
-    .context(location_info!())?;
-  match fetch_post_or_comment_by_id(object_id, context, request_counter).await? {
-    PostOrComment::Post(post) => {
-      receive_undo_dislike_post(&dislike, *post, context, request_counter).await
-    }
-    PostOrComment::Comment(comment) => {
-      receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
-    }
-  }
-}
-
-pub(crate) async fn receive_block_user_for_community(
-  context: &LemmyContext,
-  block_any_base: AnyBase,
-  announce: Option<Announce>,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let block = Block::from_any_base(block_any_base.to_owned())?.context(location_info!())?;
-  let community = extract_community_from_cc(&block, context).await?;
-
-  verify_mod_activity(&block, announce, &community, context).await?;
-  verify_is_addressed_to_public(&block)?;
-
-  let blocked_user = block
-    .object()
-    .as_single_xsd_any_uri()
-    .context(location_info!())?;
-  let blocked_user = get_or_fetch_and_upsert_person(blocked_user, context, request_counter).await?;
-
-  let community_user_ban_form = CommunityPersonBanForm {
-    community_id: community.id,
-    person_id: blocked_user.id,
-  };
-
-  blocking(context.pool(), move |conn: &'_ _| {
-    CommunityPersonBan::ban(conn, &community_user_ban_form)
-  })
-  .await??;
-
-  // Also unsubscribe them from the community, if they are subscribed
-  let community_follower_form = CommunityFollowerForm {
-    community_id: community.id,
-    person_id: blocked_user.id,
-    pending: false,
-  };
-  blocking(context.pool(), move |conn: &'_ _| {
-    CommunityFollower::unfollow(conn, &community_follower_form)
-  })
-  .await?
-  .ok();
-
-  Ok(())
-}
-
-pub(crate) async fn receive_undo_block_user_for_community(
-  context: &LemmyContext,
-  undo: Undo,
-  announce: Option<Announce>,
-  expected_domain: &Url,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError> {
-  let object = undo.object().clone().one().context(location_info!())?;
-  let block = Block::from_any_base(object)?.context(location_info!())?;
-  let community = extract_community_from_cc(&block, context).await?;
-
-  verify_activity_domains_valid(&block, expected_domain, false)?;
-  verify_is_addressed_to_public(&block)?;
-  verify_undo_remove_actor_instance(&undo, &block, &announce, context).await?;
-
-  let blocked_user = block
-    .object()
-    .as_single_xsd_any_uri()
-    .context(location_info!())?;
-  let blocked_user = get_or_fetch_and_upsert_person(blocked_user, context, request_counter).await?;
-
-  let community_user_ban_form = CommunityPersonBanForm {
-    community_id: community.id,
-    person_id: blocked_user.id,
-  };
-
-  blocking(context.pool(), move |conn: &'_ _| {
-    CommunityPersonBan::unban(conn, &community_user_ban_form)
-  })
-  .await??;
-
-  Ok(())
-}
-
-async fn fetch_post_or_comment_by_id(
-  apub_id: &Url,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<PostOrComment, LemmyError> {
-  if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await {
-    return Ok(PostOrComment::Post(Box::new(post)));
-  }
-
-  if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
-    return Ok(PostOrComment::Comment(Box::new(comment)));
-  }
-
-  Err(NotFound.into())
-}
-
-/// Searches the activity's cc field for a Community ID, and returns the community.
-async fn extract_community_from_cc<T, Kind>(
-  activity: &T,
-  context: &LemmyContext,
-) -> Result<Community, LemmyError>
-where
-  T: AsObject<Kind>,
-{
-  let cc = activity
-    .cc()
-    .map(|c| c.as_many())
-    .flatten()
-    .context(location_info!())?;
-  let community_id = cc
-    .first()
-    .map(|c| c.as_xsd_any_uri())
-    .flatten()
-    .context(location_info!())?;
-  let community_id: DbUrl = community_id.to_owned().into();
-  let community = blocking(context.pool(), move |conn| {
-    Community::read_from_apub_id(conn, &community_id)
-  })
-  .await??;
-  Ok(community)
-}
-
-/// Checks that a moderation activity was sent by a user who is listed as mod for the community.
-/// This is only used in the case of remote mods, as local mod actions don't go through the
-/// community inbox.
-///
-/// This method should only be used for activities received by the community, not for activities
-/// used by community followers.
-pub(crate) async fn verify_actor_is_community_mod<T, Kind>(
-  activity: &T,
-  community: &Community,
-  context: &LemmyContext,
-) -> Result<(), LemmyError>
-where
-  T: ActorAndObjectRef + BaseExt<Kind>,
-{
-  let actor = activity
-    .actor()?
-    .as_single_xsd_any_uri()
-    .context(location_info!())?
-    .to_owned();
-  let actor = blocking(context.pool(), move |conn| {
-    Person::read_from_apub_id(conn, &actor.into())
-  })
-  .await??;
-
-  // Note: this will also return true for admins in addition to mods, but as we dont know about
-  //       remote admins, it doesnt make any difference.
-  let community_id = community.id;
-  let actor_id = actor.id;
-  let is_mod_or_admin = blocking(context.pool(), move |conn| {
-    CommunityView::is_mod_or_admin(conn, actor_id, community_id)
-  })
-  .await?;
-  if !is_mod_or_admin {
-    return Err(anyhow!("Not a mod").into());
-  }
-
-  Ok(())
-}
-
-/// This method behaves differently, depending if it is called via community inbox (activity
-/// received by community from a remote user), or via user inbox (activity received by user from
-/// community). We distinguish the cases by checking if the activity is wrapper in an announce
-/// (only true when sent from user to community).
-///
-/// In the first case, we check that the actor is listed as community mod. In the second case, we
-/// only check that the announce comes from the same domain as the activity. We trust the
-/// community's instance to have validated the inner activity correctly. We can't do this validation
-/// here, because we don't know who the instance admins are. Plus this allows for compatibility with
-/// software that uses different rules for mod actions.
-pub(crate) async fn verify_mod_activity<T, Kind>(
-  mod_action: &T,
-  announce: Option<Announce>,
-  community: &Community,
-  context: &LemmyContext,
-) -> Result<(), LemmyError>
-where
-  T: ActorAndObjectRef + BaseExt<Kind>,
-{
-  match announce {
-    None => verify_actor_is_community_mod(mod_action, community, context).await?,
-    Some(a) => verify_activity_domains_valid(&a, &community.actor_id.to_owned().into(), false)?,
-  }
-
-  Ok(())
-}
-
-/// For Add/Remove community moderator activities, check that the target field actually contains
-/// /c/community/moderators. Any different values are unsupported.
-fn verify_add_remove_moderator_target<T, Kind>(
-  activity: &T,
-  community: &Community,
-) -> Result<(), LemmyError>
-where
-  T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
-{
-  let target = activity
-    .target()
-    .map(|t| t.as_single_xsd_any_uri())
-    .flatten()
-    .context(location_info!())?;
-  if target != &generate_moderators_url(&community.actor_id)?.into_inner() {
-    return Err(anyhow!("Unkown target url").into());
-  }
-  Ok(())
-}
-
-/// For activities like Update, Delete or Remove, check that the actor is from the same instance
-/// as the original object itself (or is a remote mod).
-///
-/// Note: This is only needed for mod actions. Normal user actions (edit post, undo vote etc) are
-///       already verified with `expected_domain`, so this serves as an additional check.
-async fn verify_modification_actor_instance<T, Kind>(
-  activity: &T,
-  announce: &Option<Announce>,
-  context: &LemmyContext,
-  request_counter: &mut i32,
-) -> Result<(), LemmyError>
-where
-  T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
-{
-  let actor_id = activity
-    .actor()?
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-  let object_id = activity
-    .object()
-    .as_one()
-    .map(|o| o.id())
-    .flatten()
-    .context(location_info!())?;
-  let original_id = match fetch_post_or_comment_by_id(object_id, context, request_counter).await {
-    Ok(PostOrComment::Post(p)) => p.ap_id.into_inner(),
-    Ok(PostOrComment::Comment(c)) => c.ap_id.into_inner(),
-    Err(_) => {
-      // We can also receive Update activity from remote mod for local activity
-      let object_id = object_id.to_owned().into();
-      blocking(context.pool(), move |conn| {
-        Community::read_from_apub_id(conn, &object_id)
-      })
-      .await??
-      .actor_id()
-    }
-  };
-  if actor_id.domain() != original_id.domain() {
-    let community = extract_community_from_cc(activity, context).await?;
-    verify_mod_activity(activity, announce.to_owned(), &community, context).await?;
-  }
-
-  Ok(())
-}
-
-pub(crate) async fn verify_undo_remove_actor_instance<T, Kind>(
-  undo: &Undo,
-  inner: &T,
-  announce: &Option<Announce>,
-  context: &LemmyContext,
-) -> Result<(), LemmyError>
-where
-  T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
-{
-  if announce.is_none() {
-    let community = extract_community_from_cc(undo, context).await?;
-    verify_mod_activity(undo, announce.to_owned(), &community, context).await?;
-    verify_mod_activity(inner, announce.to_owned(), &community, context).await?;
-  }
-
-  Ok(())
-}
diff --git a/crates/apub_receive/src/inbox/shared_inbox.rs b/crates/apub_receive/src/inbox/shared_inbox.rs
deleted file mode 100644
index 17691d4f..00000000
--- a/crates/apub_receive/src/inbox/shared_inbox.rs
+++ /dev/null
@@ -1,151 +0,0 @@
-use crate::inbox::{
-  assert_activity_not_local,
-  community_inbox::{community_receive_message, CommunityAcceptedActivities},
-  get_activity_id,
-  inbox_verify_http_signature,
-  is_activity_already_known,
-  is_addressed_to_community_followers,
-  is_addressed_to_local_person,
-  person_inbox::{person_receive_message, PersonAcceptedActivities},
-};
-use activitystreams::{activity::ActorAndObject, prelude::*};
-use actix_web::{web, HttpRequest, HttpResponse};
-use anyhow::Context;
-use lemmy_api_common::blocking;
-use lemmy_apub::{get_activity_to_and_cc, insert_activity};
-use lemmy_db_queries::{ApubObject, DbPool};
-use lemmy_db_schema::source::community::Community;
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::LemmyContext;
-use serde::{Deserialize, Serialize};
-use std::fmt::Debug;
-use url::Url;
-
-/// Allowed activity types for shared inbox.
-#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum ValidTypes {
-  Create,
-  Update,
-  Like,
-  Dislike,
-  Delete,
-  Undo,
-  Remove,
-  Announce,
-  Add,
-  Block,
-}
-
-// TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject,
-//       but it still works due to the anybase conversion
-pub type AcceptedActivities = ActorAndObject<ValidTypes>;
-
-/// Handler for all incoming requests to shared inbox.
-pub async fn shared_inbox(
-  request: HttpRequest,
-  input: web::Json<AcceptedActivities>,
-  context: web::Data<LemmyContext>,
-) -> Result<HttpResponse, LemmyError> {
-  let activity = input.into_inner();
-  // First of all check the http signature
-  let request_counter = &mut 0;
-  let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
-
-  // Do nothing if we received the same activity before
-  let actor_id = actor.actor_id();
-  let activity_id = get_activity_id(&activity, &actor_id)?;
-  if is_activity_already_known(context.pool(), &activity_id).await? {
-    return Ok(HttpResponse::Ok().finish());
-  }
-
-  assert_activity_not_local(&activity)?;
-  // Log the activity, so we avoid receiving and parsing it twice. Note that this could still happen
-  // if we receive the same activity twice in very quick succession.
-  insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
-
-  let activity_any_base = activity.clone().into_any_base()?;
-  let mut res: Option<HttpResponse> = None;
-  let to_and_cc = get_activity_to_and_cc(&activity);
-  // Handle community first, so in case the sender is banned by the community, it will error out.
-  // If we handled the person receive first, the activity would be inserted to the database before the
-  // community could check for bans.
-  // Note that an activity can be addressed to a community and to a person (or multiple persons) at the
-  // same time. In this case we still only handle it once, to avoid duplicate websocket
-  // notifications.
-  let community = extract_local_community_from_destinations(&to_and_cc, context.pool()).await?;
-  if let Some(community) = community {
-    let community_activity = CommunityAcceptedActivities::from_any_base(activity_any_base.clone())?
-      .context(location_info!())?;
-    res = Some(
-      Box::pin(community_receive_message(
-        community_activity,
-        community,
-        actor.as_ref(),
-        &context,
-        request_counter,
-      ))
-      .await?,
-    );
-  } else if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
-    let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
-      .context(location_info!())?;
-    // `to_person` is only used for follow activities (which we dont receive here), so no need to pass
-    // it in
-    Box::pin(person_receive_message(
-      person_activity,
-      None,
-      actor.as_ref(),
-      &context,
-      request_counter,
-    ))
-    .await?;
-  } else if is_addressed_to_community_followers(&to_and_cc, context.pool())
-    .await?
-    .is_some()
-  {
-    let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
-      .context(location_info!())?;
-    res = Some(
-      Box::pin(person_receive_message(
-        person_activity,
-        None,
-        actor.as_ref(),
-        &context,
-        request_counter,
-      ))
-      .await?,
-    );
-  }
-
-  // If none of those, throw an error
-  if let Some(r) = res {
-    Ok(r)
-  } else {
-    Ok(HttpResponse::NotImplemented().finish())
-  }
-}
-
-/// If `to_and_cc` contains the ID of a local community, return that community, otherwise return
-/// None.
-///
-/// This doesnt handle the case where an activity is addressed to multiple communities (because
-/// Lemmy doesnt generate such activities).
-async fn extract_local_community_from_destinations(
-  to_and_cc: &[Url],
-  pool: &DbPool,
-) -> Result<Option<Community>, LemmyError> {
-  for url in to_and_cc {
-    let url = url.to_owned();
-    let community = blocking(pool, move |conn| {
-      Community::read_from_apub_id(conn, &url.into())
-    })
-    .await?;
-    if let Ok(c) = community {
-      if c.local {
-        return Ok(Some(c));
-      }
-    }
-  }
-  Ok(None)
-}
diff --git a/crates/apub_receive/src/lib.rs b/crates/apub_receive/src/lib.rs
index 69c32b35..dadc76b6 100644
--- a/crates/apub_receive/src/lib.rs
+++ b/crates/apub_receive/src/lib.rs
@@ -1,4 +1,2 @@
 mod activities;
-mod http;
-mod inbox;
-pub mod routes;
+pub mod http;
diff --git a/docker/federation/start-local-instances.bash b/docker/federation/start-local-instances.bash
index 74f629e9..ad261c6a 100755
--- a/docker/federation/start-local-instances.bash
+++ b/docker/federation/start-local-instances.bash
@@ -8,5 +8,5 @@ for Item in alpha beta gamma delta epsilon ; do
   sudo chown -R 991:991 volumes/pictrs_$Item
 done
 
-sudo docker-compose pull --ignore-pull-failures || true
+#sudo docker-compose pull --ignore-pull-failures || true
 sudo docker-compose up
diff --git a/scripts/compilation_benchmark.sh b/scripts/compilation_benchmark.sh
index 6d454795..af355734 100755
--- a/scripts/compilation_benchmark.sh
+++ b/scripts/compilation_benchmark.sh
@@ -8,8 +8,8 @@ for ((i=0; i < times; i++)) ; do
     echo "cargo clean"
     # to benchmark incremental compilation time, do a full build with the same compiler version first,
     # and use the following clean command:
-    #cargo clean -p lemmy_utils
-    cargo clean
+    cargo clean -p lemmy_utils
+    #cargo clean
     echo "cargo build"
     start=$(date +%s.%N)
     RUSTC_WRAPPER='' cargo build -q
@@ -20,4 +20,4 @@ done
 
 average=$(bc <<< "scale=0; $duration / $times")
 
-echo "Average compilation time over $times runs is $average seconds"
\ No newline at end of file
+echo "Average compilation time over $times runs is $average seconds"
diff --git a/src/main.rs b/src/main.rs
index efccece1..fd5394fc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -91,7 +91,7 @@ async fn main() -> Result<(), LemmyError> {
       .app_data(Data::new(context))
       // The routes
       .configure(|cfg| api_routes::config(cfg, &rate_limiter))
-      .configure(lemmy_apub_receive::routes::config)
+      .configure(lemmy_apub_receive::http::routes::config)
       .configure(feeds::config)
       .configure(|cfg| images::config(cfg, &rate_limiter))
       .configure(nodeinfo::config)
-- 
2.44.1