From: Felix Ableitner <me@nutomic.com>
Date: Thu, 28 Oct 2021 21:17:59 +0000 (+0200)
Subject: Move object and collection structs to protocol folder
X-Git-Url: http://these/git/readmes/%7B%60%24%7BwebArchiveUrl%7D/save/static/README.es.md?a=commitdiff_plain;h=5ff044346f1f94a7d37107b4ca081cf1fbd6eff8;p=lemmy.git

Move object and collection structs to protocol folder
---

diff --git a/.cargo-husky/hooks/pre-commit b/.cargo-husky/hooks/pre-commit
index 4b50f2a5..1c2858d4 100755
--- a/.cargo-husky/hooks/pre-commit
+++ b/.cargo-husky/hooks/pre-commit
@@ -1,7 +1,7 @@
 #!/bin/bash
 set -e
 
-cargo +nightly fmt
+cargo +nightly fmt -- --check
 
 cargo +nightly clippy --workspace --tests --all-targets --all-features -- \
     -D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro
diff --git a/crates/apub/src/activities/comment/create_or_update.rs b/crates/apub/src/activities/comment/create_or_update.rs
index 18661cbf..d53801aa 100644
--- a/crates/apub/src/activities/comment/create_or_update.rs
+++ b/crates/apub/src/activities/comment/create_or_update.rs
@@ -1,3 +1,20 @@
+use activitystreams::{link::Mention, public, unparsed::Unparsed};
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+use lemmy_api_common::{blocking, check_post_deleted_or_removed};
+use lemmy_apub_lib::{
+  data::Data,
+  traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
+  verify::verify_domains_match,
+};
+use lemmy_db_schema::{
+  source::{community::Community, post::Post},
+  traits::Crud,
+};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{send::send_comment_ws_message, LemmyContext, UserOperationCrud};
+
 use crate::{
   activities::{
     check_community_deleted_or_removed,
@@ -13,27 +30,9 @@ use crate::{
     CreateOrUpdateType,
   },
   fetcher::object_id::ObjectId,
-  objects::{
-    comment::{ApubComment, Note},
-    community::ApubCommunity,
-    person::ApubPerson,
-  },
+  objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson},
+  protocol::objects::note::Note,
 };
-use activitystreams::{link::Mention, public, unparsed::Unparsed};
-use lemmy_api_common::{blocking, check_post_deleted_or_removed};
-use lemmy_apub_lib::{
-  data::Data,
-  traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
-  verify::verify_domains_match,
-};
-use lemmy_db_schema::{
-  source::{community::Community, post::Post},
-  traits::Crud,
-};
-use lemmy_utils::LemmyError;
-use lemmy_websocket::{send::send_comment_ws_message, LemmyContext, UserOperationCrud};
-use serde::{Deserialize, Serialize};
-use url::Url;
 
 #[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)]
 #[serde(rename_all = "camelCase")]
@@ -153,10 +152,12 @@ impl GetCommunity for CreateOrUpdateComment {
 
 #[cfg(test)]
 mod tests {
-  use super::*;
-  use crate::objects::tests::file_to_json_object;
   use serial_test::serial;
 
+  use crate::objects::tests::file_to_json_object;
+
+  use super::*;
+
   #[actix_rt::test]
   #[serial]
   async fn test_parse_pleroma_create_comment() {
diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs
index 78fce324..df34ca28 100644
--- a/crates/apub/src/activities/community/update.rs
+++ b/crates/apub/src/activities/community/update.rs
@@ -1,3 +1,19 @@
+use activitystreams::{activity::kind::UpdateType, public, unparsed::Unparsed};
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+  data::Data,
+  traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
+};
+use lemmy_db_schema::{
+  source::community::{Community, CommunityForm},
+  traits::Crud,
+};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{send::send_community_ws_message, LemmyContext, UserOperationCrud};
+
 use crate::{
   activities::{
     community::{
@@ -11,25 +27,9 @@ use crate::{
     verify_person_in_community,
   },
   fetcher::object_id::ObjectId,
-  objects::{
-    community::{ApubCommunity, Group},
-    person::ApubPerson,
-  },
-};
-use activitystreams::{activity::kind::UpdateType, public, unparsed::Unparsed};
-use lemmy_api_common::blocking;
-use lemmy_apub_lib::{
-  data::Data,
-  traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
+  objects::{community::ApubCommunity, person::ApubPerson},
+  protocol::objects::group::Group,
 };
-use lemmy_db_schema::{
-  source::community::{Community, CommunityForm},
-  traits::Crud,
-};
-use lemmy_utils::LemmyError;
-use lemmy_websocket::{send::send_community_ws_message, LemmyContext, UserOperationCrud};
-use serde::{Deserialize, Serialize};
-use url::Url;
 
 /// This activity is received from a remote community mod, and updates the description or other
 /// fields of a local community.
diff --git a/crates/apub/src/activities/post/create_or_update.rs b/crates/apub/src/activities/post/create_or_update.rs
index b5d9a202..ee1bf19c 100644
--- a/crates/apub/src/activities/post/create_or_update.rs
+++ b/crates/apub/src/activities/post/create_or_update.rs
@@ -1,3 +1,18 @@
+use activitystreams::{public, unparsed::Unparsed};
+use anyhow::anyhow;
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+  data::Data,
+  traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
+  verify::{verify_domains_match, verify_urls_match},
+};
+use lemmy_db_schema::{source::community::Community, traits::Crud};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud};
+
 use crate::{
   activities::{
     check_community_deleted_or_removed,
@@ -13,25 +28,9 @@ use crate::{
     CreateOrUpdateType,
   },
   fetcher::object_id::ObjectId,
-  objects::{
-    community::ApubCommunity,
-    person::ApubPerson,
-    post::{ApubPost, Page},
-  },
-};
-use activitystreams::{public, unparsed::Unparsed};
-use anyhow::anyhow;
-use lemmy_api_common::blocking;
-use lemmy_apub_lib::{
-  data::Data,
-  traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
-  verify::{verify_domains_match, verify_urls_match},
+  objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
+  protocol::objects::page::Page,
 };
-use lemmy_db_schema::{source::community::Community, traits::Crud};
-use lemmy_utils::LemmyError;
-use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud};
-use serde::{Deserialize, Serialize};
-use url::Url;
 
 #[derive(Clone, Debug, Deserialize, Serialize, ActivityFields)]
 #[serde(rename_all = "camelCase")]
diff --git a/crates/apub/src/activities/private_message/create_or_update.rs b/crates/apub/src/activities/private_message/create_or_update.rs
index 72a38e5e..0067607e 100644
--- a/crates/apub/src/activities/private_message/create_or_update.rs
+++ b/crates/apub/src/activities/private_message/create_or_update.rs
@@ -7,10 +7,8 @@ use crate::{
     CreateOrUpdateType,
   },
   fetcher::object_id::ObjectId,
-  objects::{
-    person::ApubPerson,
-    private_message::{ApubPrivateMessage, ChatMessage},
-  },
+  objects::{person::ApubPerson, private_message::ApubPrivateMessage},
+  protocol::objects::chat_message::ChatMessage,
 };
 use activitystreams::unparsed::Unparsed;
 use lemmy_api_common::blocking;
diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs
index d91b41c4..1a5a96d3 100644
--- a/crates/apub/src/collections/community_moderators.rs
+++ b/crates/apub/src/collections/community_moderators.rs
@@ -3,6 +3,7 @@ use crate::{
   fetcher::object_id::ObjectId,
   generate_moderators_url,
   objects::person::ApubPerson,
+  protocol::collections::group_moderators::GroupModerators,
 };
 use activitystreams::{chrono::NaiveDateTime, collection::kind::OrderedCollectionType};
 use lemmy_api_common::blocking;
@@ -13,17 +14,8 @@ use lemmy_db_schema::{
 };
 use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
 use lemmy_utils::LemmyError;
-use serde::{Deserialize, Serialize};
 use url::Url;
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GroupModerators {
-  r#type: OrderedCollectionType,
-  id: Url,
-  ordered_items: Vec<ObjectId<ApubPerson>>,
-}
-
 #[derive(Clone, Debug)]
 pub(crate) struct ApubCommunityModerators(pub(crate) Vec<CommunityModeratorView>);
 
diff --git a/crates/apub/src/collections/community_outbox.rs b/crates/apub/src/collections/community_outbox.rs
index 98fb6644..cf5ad8b2 100644
--- a/crates/apub/src/collections/community_outbox.rs
+++ b/crates/apub/src/collections/community_outbox.rs
@@ -3,6 +3,7 @@ use crate::{
   collections::CommunityContext,
   generate_outbox_url,
   objects::{person::ApubPerson, post::ApubPost},
+  protocol::collections::group_outbox::GroupOutbox,
 };
 use activitystreams::collection::kind::OrderedCollectionType;
 use chrono::NaiveDateTime;
@@ -17,18 +18,8 @@ use lemmy_db_schema::{
   traits::Crud,
 };
 use lemmy_utils::LemmyError;
-use serde::{Deserialize, Serialize};
 use url::Url;
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GroupOutbox {
-  r#type: OrderedCollectionType,
-  id: Url,
-  total_items: i32,
-  ordered_items: Vec<CreateOrUpdatePost>,
-}
-
 #[derive(Clone, Debug)]
 pub(crate) struct ApubCommunityOutbox(Vec<ApubPost>);
 
diff --git a/crates/apub/src/collections/mod.rs b/crates/apub/src/collections/mod.rs
index 378e14c0..a2e77d1b 100644
--- a/crates/apub/src/collections/mod.rs
+++ b/crates/apub/src/collections/mod.rs
@@ -1,10 +1,9 @@
-use crate::objects::community::ApubCommunity;
 use lemmy_websocket::LemmyContext;
 
-pub(crate) mod community_followers;
+use crate::objects::community::ApubCommunity;
+
 pub(crate) mod community_moderators;
 pub(crate) mod community_outbox;
-pub(crate) mod user_outbox;
 
 /// Put community in the data, so we dont have to read it again from the database.
 pub(crate) struct CommunityContext(pub ApubCommunity, pub LemmyContext);
diff --git a/crates/apub/src/fetcher/post_or_comment.rs b/crates/apub/src/fetcher/post_or_comment.rs
index ac391120..c0bc46a8 100644
--- a/crates/apub/src/fetcher/post_or_comment.rs
+++ b/crates/apub/src/fetcher/post_or_comment.rs
@@ -1,14 +1,16 @@
-use crate::objects::{
-  comment::{ApubComment, Note},
-  post::{ApubPost, Page},
-};
 use chrono::NaiveDateTime;
+use serde::Deserialize;
+use url::Url;
+
 use lemmy_apub_lib::traits::ApubObject;
 use lemmy_db_schema::source::{comment::CommentForm, post::PostForm};
 use lemmy_utils::LemmyError;
 use lemmy_websocket::LemmyContext;
-use serde::Deserialize;
-use url::Url;
+
+use crate::{
+  objects::{comment::ApubComment, post::ApubPost},
+  protocol::objects::{note::Note, page::Page},
+};
 
 #[derive(Clone, Debug)]
 pub enum PostOrComment {
diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs
index 5c2a3e7b..77a23828 100644
--- a/crates/apub/src/fetcher/search.rs
+++ b/crates/apub/src/fetcher/search.rs
@@ -1,15 +1,9 @@
-use crate::{
-  fetcher::object_id::ObjectId,
-  objects::{
-    comment::{ApubComment, Note},
-    community::{ApubCommunity, Group},
-    person::{ApubPerson, Person},
-    post::{ApubPost, Page},
-  },
-};
 use anyhow::anyhow;
 use chrono::NaiveDateTime;
 use itertools::Itertools;
+use serde::Deserialize;
+use url::Url;
+
 use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
   traits::ApubObject,
@@ -21,8 +15,17 @@ use lemmy_db_schema::{
 };
 use lemmy_utils::LemmyError;
 use lemmy_websocket::LemmyContext;
-use serde::Deserialize;
-use url::Url;
+
+use crate::{
+  fetcher::object_id::ObjectId,
+  objects::{
+    comment::ApubComment,
+    community::ApubCommunity,
+    person::{ApubPerson, Person},
+    post::ApubPost,
+  },
+  protocol::objects::{group::Group, note::Note, page::Page},
+};
 
 /// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
 ///
diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs
index 73f59d03..0a7cb664 100644
--- a/crates/apub/src/http/community.rs
+++ b/crates/apub/src/http/community.rs
@@ -1,3 +1,16 @@
+use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
+use log::info;
+use serde::{Deserialize, Serialize};
+
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+  traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
+  verify::verify_domains_match,
+};
+use lemmy_db_schema::source::community::Community;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+
 use crate::{
   activities::{
     community::announce::{AnnouncableActivities, AnnounceActivity, GetCommunity},
@@ -6,7 +19,6 @@ use crate::{
     verify_person_in_community,
   },
   collections::{
-    community_followers::CommunityFollowers,
     community_moderators::ApubCommunityModerators,
     community_outbox::ApubCommunityOutbox,
     CommunityContext,
@@ -21,18 +33,8 @@ use crate::{
     receive_activity,
   },
   objects::community::ApubCommunity,
+  protocol::collections::group_followers::CommunityFollowers,
 };
-use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
-use lemmy_api_common::blocking;
-use lemmy_apub_lib::{
-  traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
-  verify::verify_domains_match,
-};
-use lemmy_db_schema::source::community::Community;
-use lemmy_utils::LemmyError;
-use lemmy_websocket::LemmyContext;
-use log::info;
-use serde::{Deserialize, Serialize};
 
 #[derive(Deserialize)]
 pub(crate) struct CommunityQuery {
diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs
index a2d4fedd..77bfd694 100644
--- a/crates/apub/src/http/person.rs
+++ b/crates/apub/src/http/person.rs
@@ -1,3 +1,13 @@
+use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
+use log::info;
+use serde::{Deserialize, Serialize};
+
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ApubObject};
+use lemmy_db_schema::source::person::Person;
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+
 use crate::{
   activities::{
     community::announce::{AnnouncableActivities, AnnounceActivity},
@@ -8,7 +18,6 @@ use crate::{
       undo_delete::UndoDeletePrivateMessage,
     },
   },
-  collections::user_outbox::UserOutbox,
   context::WithContext,
   http::{
     create_apub_response,
@@ -17,15 +26,8 @@ use crate::{
     receive_activity,
   },
   objects::person::ApubPerson,
+  protocol::collections::person_outbox::UserOutbox,
 };
-use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
-use lemmy_api_common::blocking;
-use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ApubObject};
-use lemmy_db_schema::source::person::Person;
-use lemmy_utils::LemmyError;
-use lemmy_websocket::LemmyContext;
-use log::info;
-use serde::{Deserialize, Serialize};
 
 #[derive(Deserialize)]
 pub struct PersonQuery {
diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs
index f60e04af..d188274b 100644
--- a/crates/apub/src/lib.rs
+++ b/crates/apub/src/lib.rs
@@ -5,6 +5,7 @@ pub mod fetcher;
 pub mod http;
 pub mod migrations;
 pub mod objects;
+pub(crate) mod protocol;
 
 #[macro_use]
 extern crate lazy_static;
diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs
index 5698640d..cd491d87 100644
--- a/crates/apub/src/objects/comment.rs
+++ b/crates/apub/src/objects/comment.rs
@@ -1,27 +1,17 @@
-use crate::{
-  activities::{verify_is_public, verify_person_in_community},
-  fetcher::object_id::ObjectId,
-  objects::{
-    community::ApubCommunity,
-    person::ApubPerson,
-    post::ApubPost,
-    tombstone::Tombstone,
-    Source,
-  },
-  PostOrComment,
-};
-use activitystreams::{object::kind::NoteType, public, unparsed::Unparsed};
+use std::ops::Deref;
+
+use activitystreams::{object::kind::NoteType, public};
 use anyhow::anyhow;
-use chrono::{DateTime, FixedOffset, NaiveDateTime};
+use chrono::NaiveDateTime;
 use html2md::parse_html;
+use url::Url;
+
 use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
   traits::ApubObject,
   values::{MediaTypeHtml, MediaTypeMarkdown},
-  verify::verify_domains_match,
 };
 use lemmy_db_schema::{
-  newtypes::CommentId,
   source::{
     comment::{Comment, CommentForm},
     community::Community,
@@ -35,100 +25,19 @@ use lemmy_utils::{
   LemmyError,
 };
 use lemmy_websocket::LemmyContext;
-use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use std::ops::Deref;
-use url::Url;
 
-#[skip_serializing_none]
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Note {
-  r#type: NoteType,
-  id: Url,
-  pub(crate) attributed_to: ObjectId<ApubPerson>,
-  /// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain
-  /// the community ID, as it would be incompatible with Pleroma (and we can get the community from
-  /// the post in [`in_reply_to`]).
-  to: Vec<Url>,
-  content: String,
-  media_type: Option<MediaTypeHtml>,
-  source: SourceCompat,
-  in_reply_to: ObjectId<PostOrComment>,
-  published: Option<DateTime<FixedOffset>>,
-  updated: Option<DateTime<FixedOffset>>,
-  #[serde(flatten)]
-  unparsed: Unparsed,
-}
-
-/// Pleroma puts a raw string in the source, so we have to handle it here for deserialization to work
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-#[serde(untagged)]
-enum SourceCompat {
-  Lemmy(Source),
-  Pleroma(String),
-}
-
-impl Note {
-  pub(crate) fn id_unchecked(&self) -> &Url {
-    &self.id
-  }
-  pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
-    verify_domains_match(&self.id, expected_domain)?;
-    Ok(&self.id)
-  }
-
-  pub(crate) async fn get_parents(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<(ApubPost, Option<CommentId>), LemmyError> {
-    // Fetch parent comment chain in a box, otherwise it can cause a stack overflow.
-    let parent = Box::pin(
-      self
-        .in_reply_to
-        .dereference(context, request_counter)
-        .await?,
-    );
-    match parent.deref() {
-      PostOrComment::Post(p) => {
-        // Workaround because I cant figure out how to get the post out of the box (and we dont
-        // want to stackoverflow in a deep comment hierarchy).
-        let post_id = p.id;
-        let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-        Ok((post.into(), None))
-      }
-      PostOrComment::Comment(c) => {
-        let post_id = c.post_id;
-        let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
-        Ok((post.into(), Some(c.id)))
-      }
-    }
-  }
-
-  pub(crate) async fn verify(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    let (post, _parent_comment_id) = self.get_parents(context, request_counter).await?;
-    let community_id = post.community_id;
-    let community: ApubCommunity = blocking(context.pool(), move |conn| {
-      Community::read(conn, community_id)
-    })
-    .await??
-    .into();
-
-    if post.locked {
-      return Err(anyhow!("Post is locked").into());
-    }
-    verify_domains_match(self.attributed_to.inner(), &self.id)?;
-    verify_person_in_community(&self.attributed_to, &community, context, request_counter).await?;
-    verify_is_public(&self.to)?;
-    Ok(())
-  }
-}
+use crate::{
+  activities::verify_person_in_community,
+  fetcher::object_id::ObjectId,
+  protocol::{
+    objects::{
+      note::{Note, SourceCompat},
+      tombstone::Tombstone,
+    },
+    Source,
+  },
+  PostOrComment,
+};
 
 #[derive(Clone, Debug)]
 pub struct ApubComment(Comment);
@@ -277,13 +186,16 @@ impl ApubObject for ApubComment {
 
 #[cfg(test)]
 pub(crate) mod tests {
-  use super::*;
+  use assert_json_diff::assert_json_include;
+  use serial_test::serial;
+
   use crate::objects::{
     community::ApubCommunity,
     tests::{file_to_json_object, init_context},
   };
-  use assert_json_diff::assert_json_include;
-  use serial_test::serial;
+
+  use super::*;
+  use crate::objects::{person::ApubPerson, post::ApubPost};
 
   pub(crate) async fn prepare_comment_test(
     url: &Url,
diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs
index c7d1dd3e..0a4c06a4 100644
--- a/crates/apub/src/objects/community.rs
+++ b/crates/apub/src/objects/community.rs
@@ -1,117 +1,40 @@
-use crate::{
-  check_is_apub_id_valid,
-  collections::{
-    community_moderators::ApubCommunityModerators,
-    community_outbox::ApubCommunityOutbox,
-    CommunityContext,
-  },
-  fetcher::object_id::ObjectId,
-  generate_moderators_url,
-  generate_outbox_url,
-  objects::{get_summary_from_string_or_source, tombstone::Tombstone, ImageObject, Source},
-};
+use std::ops::Deref;
+
 use activitystreams::{
   actor::{kind::GroupType, Endpoints},
   object::kind::ImageType,
-  unparsed::Unparsed,
 };
-use chrono::{DateTime, FixedOffset, NaiveDateTime};
+use chrono::NaiveDateTime;
 use itertools::Itertools;
+use log::debug;
+use url::Url;
+
 use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
-  signatures::PublicKey,
   traits::{ActorType, ApubObject},
   values::MediaTypeMarkdown,
-  verify::verify_domains_match,
-};
-use lemmy_db_schema::{
-  naive_now,
-  source::community::{Community, CommunityForm},
-  DbPool,
 };
+use lemmy_db_schema::{source::community::Community, DbPool};
 use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
 use lemmy_utils::{
   settings::structs::Settings,
-  utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
+  utils::{convert_datetime, markdown_to_html},
   LemmyError,
 };
 use lemmy_websocket::LemmyContext;
-use log::debug;
-use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use std::ops::Deref;
-use url::Url;
-
-#[skip_serializing_none]
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Group {
-  #[serde(rename = "type")]
-  kind: GroupType,
-  pub(crate) id: Url,
-  /// username, set at account creation and can never be changed
-  preferred_username: String,
-  /// title (can be changed at any time)
-  name: String,
-  summary: Option<String>,
-  source: Option<Source>,
-  icon: Option<ImageObject>,
-  /// banner
-  image: Option<ImageObject>,
-  // lemmy extension
-  sensitive: Option<bool>,
-  // lemmy extension
-  pub(crate) moderators: Option<ObjectId<ApubCommunityModerators>>,
-  inbox: Url,
-  pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
-  followers: Url,
-  endpoints: Endpoints<Url>,
-  public_key: PublicKey,
-  published: Option<DateTime<FixedOffset>>,
-  updated: Option<DateTime<FixedOffset>>,
-  #[serde(flatten)]
-  unparsed: Unparsed,
-}
 
-impl Group {
-  pub(crate) async fn from_apub_to_form(
-    group: &Group,
-    expected_domain: &Url,
-    settings: &Settings,
-  ) -> Result<CommunityForm, LemmyError> {
-    verify_domains_match(expected_domain, &group.id)?;
-    let name = group.preferred_username.clone();
-    let title = group.name.clone();
-    let description = get_summary_from_string_or_source(&group.summary, &group.source);
-    let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into());
-
-    let slur_regex = &settings.slur_regex();
-    check_slurs(&name, slur_regex)?;
-    check_slurs(&title, slur_regex)?;
-    check_slurs_opt(&description, slur_regex)?;
-
-    Ok(CommunityForm {
-      name,
-      title,
-      description,
-      removed: None,
-      published: group.published.map(|u| u.naive_local()),
-      updated: group.updated.map(|u| u.naive_local()),
-      deleted: None,
-      nsfw: Some(group.sensitive.unwrap_or(false)),
-      actor_id: Some(group.id.clone().into()),
-      local: Some(false),
-      private_key: None,
-      public_key: Some(group.public_key.public_key_pem.clone()),
-      last_refreshed_at: Some(naive_now()),
-      icon: Some(group.icon.clone().map(|i| i.url.into())),
-      banner: Some(group.image.clone().map(|i| i.url.into())),
-      followers_url: Some(group.followers.clone().into()),
-      inbox_url: Some(group.inbox.clone().into()),
-      shared_inbox_url: Some(shared_inbox),
-    })
-  }
-}
+use crate::{
+  check_is_apub_id_valid,
+  collections::{community_moderators::ApubCommunityModerators, CommunityContext},
+  fetcher::object_id::ObjectId,
+  generate_moderators_url,
+  generate_outbox_url,
+  protocol::{
+    objects::{group::Group, tombstone::Tombstone},
+    ImageObject,
+    Source,
+  },
+};
 
 #[derive(Clone, Debug)]
 pub struct ApubCommunity(Community);
@@ -300,12 +223,15 @@ impl ApubCommunity {
 
 #[cfg(test)]
 mod tests {
-  use super::*;
-  use crate::objects::tests::{file_to_json_object, init_context};
   use assert_json_diff::assert_json_include;
-  use lemmy_db_schema::traits::Crud;
   use serial_test::serial;
 
+  use lemmy_db_schema::traits::Crud;
+
+  use crate::objects::tests::{file_to_json_object, init_context};
+
+  use super::*;
+
   #[actix_rt::test]
   #[serial]
   async fn test_parse_lemmy_community() {
diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs
index d0cb1341..b577dabe 100644
--- a/crates/apub/src/objects/mod.rs
+++ b/crates/apub/src/objects/mod.rs
@@ -1,32 +1,13 @@
-use activitystreams::object::kind::ImageType;
+use crate::protocol::Source;
 use html2md::parse_html;
-use lemmy_apub_lib::values::MediaTypeMarkdown;
-use serde::{Deserialize, Serialize};
-use url::Url;
 
 pub mod comment;
 pub mod community;
 pub mod person;
 pub mod post;
 pub mod private_message;
-pub mod tombstone;
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Source {
-  content: String,
-  media_type: MediaTypeMarkdown,
-}
-
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ImageObject {
-  #[serde(rename = "type")]
-  kind: ImageType,
-  url: Url,
-}
-
-fn get_summary_from_string_or_source(
+pub(crate) fn get_summary_from_string_or_source(
   raw: &Option<String>,
   source: &Option<Source>,
 ) -> Option<String> {
diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs
index 49ba7eb1..1d914e32 100644
--- a/crates/apub/src/objects/person.rs
+++ b/crates/apub/src/objects/person.rs
@@ -1,7 +1,8 @@
 use crate::{
   check_is_apub_id_valid,
   generate_outbox_url,
-  objects::{get_summary_from_string_or_source, ImageObject, Source},
+  objects::get_summary_from_string_or_source,
+  protocol::{ImageObject, Source},
 };
 use activitystreams::{actor::Endpoints, object::kind::ImageType, unparsed::Unparsed};
 use chrono::{DateTime, FixedOffset, NaiveDateTime};
diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs
index ee9aa7b8..3ade8de0 100644
--- a/crates/apub/src/objects/post.rs
+++ b/crates/apub/src/objects/post.rs
@@ -1,10 +1,8 @@
 use crate::{
-  activities::{verify_is_public, verify_person_in_community},
+  activities::verify_person_in_community,
   fetcher::object_id::ObjectId,
-  objects::{
-    community::ApubCommunity,
-    person::ApubPerson,
-    tombstone::Tombstone,
+  protocol::{
+    objects::{page::Page, tombstone::Tombstone},
     ImageObject,
     Source,
   },
@@ -12,15 +10,12 @@ use crate::{
 use activitystreams::{
   object::kind::{ImageType, PageType},
   public,
-  unparsed::Unparsed,
 };
-use anyhow::anyhow;
-use chrono::{DateTime, FixedOffset, NaiveDateTime};
+use chrono::NaiveDateTime;
 use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
   traits::ApubObject,
   values::{MediaTypeHtml, MediaTypeMarkdown},
-  verify::verify_domains_match,
 };
 use lemmy_db_schema::{
   self,
@@ -33,97 +28,13 @@ use lemmy_db_schema::{
 };
 use lemmy_utils::{
   request::fetch_site_data,
-  utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs},
+  utils::{convert_datetime, markdown_to_html, remove_slurs},
   LemmyError,
 };
 use lemmy_websocket::LemmyContext;
-use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
 use std::ops::Deref;
 use url::Url;
 
-#[skip_serializing_none]
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Page {
-  r#type: PageType,
-  id: Url,
-  pub(crate) attributed_to: ObjectId<ApubPerson>,
-  to: Vec<Url>,
-  name: String,
-  content: Option<String>,
-  media_type: Option<MediaTypeHtml>,
-  source: Option<Source>,
-  url: Option<Url>,
-  image: Option<ImageObject>,
-  pub(crate) comments_enabled: Option<bool>,
-  sensitive: Option<bool>,
-  pub(crate) stickied: Option<bool>,
-  published: Option<DateTime<FixedOffset>>,
-  updated: Option<DateTime<FixedOffset>>,
-  #[serde(flatten)]
-  unparsed: Unparsed,
-}
-
-impl Page {
-  pub(crate) fn id_unchecked(&self) -> &Url {
-    &self.id
-  }
-  pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
-    verify_domains_match(&self.id, expected_domain)?;
-    Ok(&self.id)
-  }
-
-  /// Only mods can change the post's stickied/locked status. So if either of these is changed from
-  /// the current value, it is a mod action and needs to be verified as such.
-  ///
-  /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
-  pub(crate) async fn is_mod_action(&self, context: &LemmyContext) -> Result<bool, LemmyError> {
-    let old_post = ObjectId::<ApubPost>::new(self.id.clone())
-      .dereference_local(context)
-      .await;
-
-    let is_mod_action = if let Ok(old_post) = old_post {
-      self.stickied != Some(old_post.stickied) || self.comments_enabled != Some(!old_post.locked)
-    } else {
-      false
-    };
-    Ok(is_mod_action)
-  }
-
-  pub(crate) async fn verify(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    let community = self.extract_community(context, request_counter).await?;
-
-    check_slurs(&self.name, &context.settings().slur_regex())?;
-    verify_domains_match(self.attributed_to.inner(), &self.id.clone())?;
-    verify_person_in_community(&self.attributed_to, &community, context, request_counter).await?;
-    verify_is_public(&self.to.clone())?;
-    Ok(())
-  }
-
-  pub(crate) async fn extract_community(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<ApubCommunity, LemmyError> {
-    let mut to_iter = self.to.iter();
-    loop {
-      if let Some(cid) = to_iter.next() {
-        let cid = ObjectId::new(cid.clone());
-        if let Ok(c) = cid.dereference(context, request_counter).await {
-          break Ok(c);
-        }
-      } else {
-        return Err(anyhow!("No community found in cc").into());
-      }
-    }
-  }
-}
-
 #[derive(Clone, Debug)]
 pub struct ApubPost(Post);
 
@@ -283,6 +194,8 @@ mod tests {
   use super::*;
   use crate::objects::{
     community::ApubCommunity,
+    person::ApubPerson,
+    post::ApubPost,
     tests::{file_to_json_object, init_context},
   };
   use assert_json_diff::assert_json_include;
diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs
index e971ebe3..ee0aec95 100644
--- a/crates/apub/src/objects/private_message.rs
+++ b/crates/apub/src/objects/private_message.rs
@@ -1,16 +1,16 @@
 use crate::{
   fetcher::object_id::ObjectId,
-  objects::{person::ApubPerson, Source},
+  protocol::{
+    objects::chat_message::{ChatMessage, ChatMessageType},
+    Source,
+  },
 };
-use activitystreams::unparsed::Unparsed;
-use anyhow::anyhow;
-use chrono::{DateTime, FixedOffset, NaiveDateTime};
+use chrono::NaiveDateTime;
 use html2md::parse_html;
 use lemmy_api_common::blocking;
 use lemmy_apub_lib::{
   traits::ApubObject,
   values::{MediaTypeHtml, MediaTypeMarkdown},
-  verify::verify_domains_match,
 };
 use lemmy_db_schema::{
   source::{
@@ -21,60 +21,9 @@ use lemmy_db_schema::{
 };
 use lemmy_utils::{utils::convert_datetime, LemmyError};
 use lemmy_websocket::LemmyContext;
-use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
 use std::ops::Deref;
 use url::Url;
 
-#[skip_serializing_none]
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ChatMessage {
-  r#type: ChatMessageType,
-  id: Url,
-  pub(crate) attributed_to: ObjectId<ApubPerson>,
-  to: [ObjectId<ApubPerson>; 1],
-  content: String,
-  media_type: Option<MediaTypeHtml>,
-  source: Option<Source>,
-  published: Option<DateTime<FixedOffset>>,
-  updated: Option<DateTime<FixedOffset>>,
-  #[serde(flatten)]
-  unparsed: Unparsed,
-}
-
-/// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
-#[derive(Clone, Debug, Deserialize, Serialize)]
-pub enum ChatMessageType {
-  ChatMessage,
-}
-
-impl ChatMessage {
-  pub(crate) fn id_unchecked(&self) -> &Url {
-    &self.id
-  }
-  pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
-    verify_domains_match(&self.id, expected_domain)?;
-    Ok(&self.id)
-  }
-
-  pub(crate) async fn verify(
-    &self,
-    context: &LemmyContext,
-    request_counter: &mut i32,
-  ) -> Result<(), LemmyError> {
-    verify_domains_match(self.attributed_to.inner(), &self.id)?;
-    let person = self
-      .attributed_to
-      .dereference(context, request_counter)
-      .await?;
-    if person.banned {
-      return Err(anyhow!("Person is banned from site").into());
-    }
-    Ok(())
-  }
-}
-
 #[derive(Clone, Debug)]
 pub struct ApubPrivateMessage(PrivateMessage);
 
@@ -189,7 +138,10 @@ impl ApubObject for ApubPrivateMessage {
 #[cfg(test)]
 mod tests {
   use super::*;
-  use crate::objects::tests::{file_to_json_object, init_context};
+  use crate::objects::{
+    person::ApubPerson,
+    tests::{file_to_json_object, init_context},
+  };
   use assert_json_diff::assert_json_include;
   use serial_test::serial;
 
diff --git a/crates/apub/src/collections/community_followers.rs b/crates/apub/src/protocol/collections/group_followers.rs
similarity index 100%
rename from crates/apub/src/collections/community_followers.rs
rename to crates/apub/src/protocol/collections/group_followers.rs
diff --git a/crates/apub/src/protocol/collections/group_moderators.rs b/crates/apub/src/protocol/collections/group_moderators.rs
new file mode 100644
index 00000000..d37751a1
--- /dev/null
+++ b/crates/apub/src/protocol/collections/group_moderators.rs
@@ -0,0 +1,12 @@
+use crate::{fetcher::object_id::ObjectId, objects::person::ApubPerson};
+use activitystreams::collection::kind::OrderedCollectionType;
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GroupModerators {
+  pub(crate) r#type: OrderedCollectionType,
+  pub(crate) id: Url,
+  pub(crate) ordered_items: Vec<ObjectId<ApubPerson>>,
+}
diff --git a/crates/apub/src/protocol/collections/group_outbox.rs b/crates/apub/src/protocol/collections/group_outbox.rs
new file mode 100644
index 00000000..26da4b6f
--- /dev/null
+++ b/crates/apub/src/protocol/collections/group_outbox.rs
@@ -0,0 +1,13 @@
+use crate::activities::post::create_or_update::CreateOrUpdatePost;
+use activitystreams::collection::kind::OrderedCollectionType;
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GroupOutbox {
+  pub(crate) r#type: OrderedCollectionType,
+  pub(crate) id: Url,
+  pub(crate) total_items: i32,
+  pub(crate) ordered_items: Vec<CreateOrUpdatePost>,
+}
diff --git a/crates/apub/src/protocol/collections/mod.rs b/crates/apub/src/protocol/collections/mod.rs
new file mode 100644
index 00000000..646abbeb
--- /dev/null
+++ b/crates/apub/src/protocol/collections/mod.rs
@@ -0,0 +1,4 @@
+pub(crate) mod group_followers;
+pub(crate) mod group_moderators;
+pub(crate) mod group_outbox;
+pub(crate) mod person_outbox;
diff --git a/crates/apub/src/collections/user_outbox.rs b/crates/apub/src/protocol/collections/person_outbox.rs
similarity index 100%
rename from crates/apub/src/collections/user_outbox.rs
rename to crates/apub/src/protocol/collections/person_outbox.rs
diff --git a/crates/apub/src/protocol/mod.rs b/crates/apub/src/protocol/mod.rs
new file mode 100644
index 00000000..f4ad9e23
--- /dev/null
+++ b/crates/apub/src/protocol/mod.rs
@@ -0,0 +1,23 @@
+use activitystreams::object::kind::ImageType;
+use serde::{Deserialize, Serialize};
+use url::Url;
+
+use lemmy_apub_lib::values::MediaTypeMarkdown;
+
+pub(crate) mod collections;
+pub(crate) mod objects;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Source {
+  pub(crate) content: String,
+  pub(crate) media_type: MediaTypeMarkdown,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ImageObject {
+  #[serde(rename = "type")]
+  pub(crate) kind: ImageType,
+  pub(crate) url: Url,
+}
diff --git a/crates/apub/src/protocol/objects/chat_message.rs b/crates/apub/src/protocol/objects/chat_message.rs
new file mode 100644
index 00000000..038af4ed
--- /dev/null
+++ b/crates/apub/src/protocol/objects/chat_message.rs
@@ -0,0 +1,61 @@
+use crate::{fetcher::object_id::ObjectId, objects::person::ApubPerson, protocol::Source};
+use activitystreams::{
+  chrono::{DateTime, FixedOffset},
+  unparsed::Unparsed,
+};
+use anyhow::anyhow;
+use lemmy_apub_lib::{values::MediaTypeHtml, verify::verify_domains_match};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use url::Url;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ChatMessage {
+  pub(crate) r#type: ChatMessageType,
+  pub(crate) id: Url,
+  pub(crate) attributed_to: ObjectId<ApubPerson>,
+  pub(crate) to: [ObjectId<ApubPerson>; 1],
+  pub(crate) content: String,
+  pub(crate) media_type: Option<MediaTypeHtml>,
+  pub(crate) source: Option<Source>,
+  pub(crate) published: Option<DateTime<FixedOffset>>,
+  pub(crate) updated: Option<DateTime<FixedOffset>>,
+  #[serde(flatten)]
+  pub(crate) unparsed: Unparsed,
+}
+
+/// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub enum ChatMessageType {
+  ChatMessage,
+}
+
+impl ChatMessage {
+  pub(crate) fn id_unchecked(&self) -> &Url {
+    &self.id
+  }
+  pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
+    verify_domains_match(&self.id, expected_domain)?;
+    Ok(&self.id)
+  }
+
+  pub(crate) async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    verify_domains_match(self.attributed_to.inner(), &self.id)?;
+    let person = self
+      .attributed_to
+      .dereference(context, request_counter)
+      .await?;
+    if person.banned {
+      return Err(anyhow!("Person is banned from site").into());
+    }
+    Ok(())
+  }
+}
diff --git a/crates/apub/src/protocol/objects/group.rs b/crates/apub/src/protocol/objects/group.rs
new file mode 100644
index 00000000..94587890
--- /dev/null
+++ b/crates/apub/src/protocol/objects/group.rs
@@ -0,0 +1,95 @@
+use crate::{
+  collections::{
+    community_moderators::ApubCommunityModerators,
+    community_outbox::ApubCommunityOutbox,
+  },
+  fetcher::object_id::ObjectId,
+  objects::get_summary_from_string_or_source,
+  protocol::{ImageObject, Source},
+};
+use activitystreams::{
+  actor::{kind::GroupType, Endpoints},
+  unparsed::Unparsed,
+};
+use chrono::{DateTime, FixedOffset};
+use lemmy_apub_lib::{signatures::PublicKey, verify::verify_domains_match};
+use lemmy_db_schema::{naive_now, source::community::CommunityForm};
+use lemmy_utils::{
+  settings::structs::Settings,
+  utils::{check_slurs, check_slurs_opt},
+  LemmyError,
+};
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use url::Url;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Group {
+  #[serde(rename = "type")]
+  pub(crate) kind: GroupType,
+  pub(crate) id: Url,
+  /// username, set at account creation and can never be changed
+  pub(crate) preferred_username: String,
+  /// title (can be changed at any time)
+  pub(crate) name: String,
+  pub(crate) summary: Option<String>,
+  pub(crate) source: Option<Source>,
+  pub(crate) icon: Option<ImageObject>,
+  /// banner
+  pub(crate) image: Option<ImageObject>,
+  // lemmy extension
+  pub(crate) sensitive: Option<bool>,
+  // lemmy extension
+  pub(crate) moderators: Option<ObjectId<ApubCommunityModerators>>,
+  pub(crate) inbox: Url,
+  pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
+  pub(crate) followers: Url,
+  pub(crate) endpoints: Endpoints<Url>,
+  pub(crate) public_key: PublicKey,
+  pub(crate) published: Option<DateTime<FixedOffset>>,
+  pub(crate) updated: Option<DateTime<FixedOffset>>,
+  #[serde(flatten)]
+  pub(crate) unparsed: Unparsed,
+}
+
+impl Group {
+  pub(crate) async fn from_apub_to_form(
+    group: &Group,
+    expected_domain: &Url,
+    settings: &Settings,
+  ) -> Result<CommunityForm, LemmyError> {
+    verify_domains_match(expected_domain, &group.id)?;
+    let name = group.preferred_username.clone();
+    let title = group.name.clone();
+    let description = get_summary_from_string_or_source(&group.summary, &group.source);
+    let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into());
+
+    let slur_regex = &settings.slur_regex();
+    check_slurs(&name, slur_regex)?;
+    check_slurs(&title, slur_regex)?;
+    check_slurs_opt(&description, slur_regex)?;
+
+    Ok(CommunityForm {
+      name,
+      title,
+      description,
+      removed: None,
+      published: group.published.map(|u| u.naive_local()),
+      updated: group.updated.map(|u| u.naive_local()),
+      deleted: None,
+      nsfw: Some(group.sensitive.unwrap_or(false)),
+      actor_id: Some(group.id.clone().into()),
+      local: Some(false),
+      private_key: None,
+      public_key: Some(group.public_key.public_key_pem.clone()),
+      last_refreshed_at: Some(naive_now()),
+      icon: Some(group.icon.clone().map(|i| i.url.into())),
+      banner: Some(group.image.clone().map(|i| i.url.into())),
+      followers_url: Some(group.followers.clone().into()),
+      inbox_url: Some(group.inbox.clone().into()),
+      shared_inbox_url: Some(shared_inbox),
+    })
+  }
+}
diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs
new file mode 100644
index 00000000..3e133831
--- /dev/null
+++ b/crates/apub/src/protocol/objects/mod.rs
@@ -0,0 +1,5 @@
+pub(crate) mod chat_message;
+pub(crate) mod group;
+pub(crate) mod note;
+pub(crate) mod page;
+pub(crate) mod tombstone;
diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs
new file mode 100644
index 00000000..bdc4da66
--- /dev/null
+++ b/crates/apub/src/protocol/objects/note.rs
@@ -0,0 +1,112 @@
+use crate::{
+  activities::{verify_is_public, verify_person_in_community},
+  fetcher::{object_id::ObjectId, post_or_comment::PostOrComment},
+  objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
+  protocol::Source,
+};
+use activitystreams::{object::kind::NoteType, unparsed::Unparsed};
+use anyhow::anyhow;
+use chrono::{DateTime, FixedOffset};
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{values::MediaTypeHtml, verify::verify_domains_match};
+use lemmy_db_schema::{
+  newtypes::CommentId,
+  source::{community::Community, post::Post},
+  traits::Crud,
+};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use std::ops::Deref;
+use url::Url;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Note {
+  pub(crate) r#type: NoteType,
+  pub(crate) id: Url,
+  pub(crate) attributed_to: ObjectId<ApubPerson>,
+  /// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain
+  /// the community ID, as it would be incompatible with Pleroma (and we can get the community from
+  /// the post in [`in_reply_to`]).
+  pub(crate) to: Vec<Url>,
+  pub(crate) content: String,
+  pub(crate) media_type: Option<MediaTypeHtml>,
+  pub(crate) source: SourceCompat,
+  pub(crate) in_reply_to: ObjectId<PostOrComment>,
+  pub(crate) published: Option<DateTime<FixedOffset>>,
+  pub(crate) updated: Option<DateTime<FixedOffset>>,
+  #[serde(flatten)]
+  pub(crate) unparsed: Unparsed,
+}
+
+/// Pleroma puts a raw string in the source, so we have to handle it here for deserialization to work
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+#[serde(untagged)]
+pub(crate) enum SourceCompat {
+  Lemmy(Source),
+  Pleroma(String),
+}
+
+impl Note {
+  pub(crate) fn id_unchecked(&self) -> &Url {
+    &self.id
+  }
+  pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
+    verify_domains_match(&self.id, expected_domain)?;
+    Ok(&self.id)
+  }
+
+  pub(crate) async fn get_parents(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(ApubPost, Option<CommentId>), LemmyError> {
+    // Fetch parent comment chain in a box, otherwise it can cause a stack overflow.
+    let parent = Box::pin(
+      self
+        .in_reply_to
+        .dereference(context, request_counter)
+        .await?,
+    );
+    match parent.deref() {
+      PostOrComment::Post(p) => {
+        // Workaround because I cant figure out how to get the post out of the box (and we dont
+        // want to stackoverflow in a deep comment hierarchy).
+        let post_id = p.id;
+        let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+        Ok((post.into(), None))
+      }
+      PostOrComment::Comment(c) => {
+        let post_id = c.post_id;
+        let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+        Ok((post.into(), Some(c.id)))
+      }
+    }
+  }
+
+  pub(crate) async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let (post, _parent_comment_id) = self.get_parents(context, request_counter).await?;
+    let community_id = post.community_id;
+    let community: ApubCommunity = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??
+    .into();
+
+    if post.locked {
+      return Err(anyhow!("Post is locked").into());
+    }
+    verify_domains_match(self.attributed_to.inner(), &self.id)?;
+    verify_person_in_community(&self.attributed_to, &community, context, request_counter).await?;
+    verify_is_public(&self.to)?;
+    Ok(())
+  }
+}
diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs
new file mode 100644
index 00000000..7887f19c
--- /dev/null
+++ b/crates/apub/src/protocol/objects/page.rs
@@ -0,0 +1,97 @@
+use crate::{
+  activities::{verify_is_public, verify_person_in_community},
+  fetcher::object_id::ObjectId,
+  objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
+  protocol::{ImageObject, Source},
+};
+use activitystreams::{object::kind::PageType, unparsed::Unparsed};
+use anyhow::anyhow;
+use chrono::{DateTime, FixedOffset};
+use lemmy_apub_lib::{values::MediaTypeHtml, verify::verify_domains_match};
+use lemmy_utils::{utils::check_slurs, LemmyError};
+use lemmy_websocket::LemmyContext;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use url::Url;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Page {
+  pub(crate) r#type: PageType,
+  pub(crate) id: Url,
+  pub(crate) attributed_to: ObjectId<ApubPerson>,
+  pub(crate) to: Vec<Url>,
+  pub(crate) name: String,
+  pub(crate) content: Option<String>,
+  pub(crate) media_type: Option<MediaTypeHtml>,
+  pub(crate) source: Option<Source>,
+  pub(crate) url: Option<Url>,
+  pub(crate) image: Option<ImageObject>,
+  pub(crate) comments_enabled: Option<bool>,
+  pub(crate) sensitive: Option<bool>,
+  pub(crate) stickied: Option<bool>,
+  pub(crate) published: Option<DateTime<FixedOffset>>,
+  pub(crate) updated: Option<DateTime<FixedOffset>>,
+  #[serde(flatten)]
+  pub(crate) unparsed: Unparsed,
+}
+
+impl Page {
+  pub(crate) fn id_unchecked(&self) -> &Url {
+    &self.id
+  }
+  pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
+    verify_domains_match(&self.id, expected_domain)?;
+    Ok(&self.id)
+  }
+
+  /// Only mods can change the post's stickied/locked status. So if either of these is changed from
+  /// the current value, it is a mod action and needs to be verified as such.
+  ///
+  /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
+  pub(crate) async fn is_mod_action(&self, context: &LemmyContext) -> Result<bool, LemmyError> {
+    let old_post = ObjectId::<ApubPost>::new(self.id.clone())
+      .dereference_local(context)
+      .await;
+
+    let is_mod_action = if let Ok(old_post) = old_post {
+      self.stickied != Some(old_post.stickied) || self.comments_enabled != Some(!old_post.locked)
+    } else {
+      false
+    };
+    Ok(is_mod_action)
+  }
+
+  pub(crate) async fn verify(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<(), LemmyError> {
+    let community = self.extract_community(context, request_counter).await?;
+
+    check_slurs(&self.name, &context.settings().slur_regex())?;
+    verify_domains_match(self.attributed_to.inner(), &self.id.clone())?;
+    verify_person_in_community(&self.attributed_to, &community, context, request_counter).await?;
+    verify_is_public(&self.to.clone())?;
+    Ok(())
+  }
+
+  pub(crate) async fn extract_community(
+    &self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<ApubCommunity, LemmyError> {
+    let mut to_iter = self.to.iter();
+    loop {
+      if let Some(cid) = to_iter.next() {
+        let cid = ObjectId::new(cid.clone());
+        if let Ok(c) = cid.dereference(context, request_counter).await {
+          break Ok(c);
+        }
+      } else {
+        return Err(anyhow!("No community found in cc").into());
+      }
+    }
+  }
+}
diff --git a/crates/apub/src/objects/tombstone.rs b/crates/apub/src/protocol/objects/tombstone.rs
similarity index 100%
rename from crates/apub/src/objects/tombstone.rs
rename to crates/apub/src/protocol/objects/tombstone.rs