From b3a5b4eb820b7d431bb9748ef1797bc9407caacb Mon Sep 17 00:00:00 2001
From: Felix Ableitner <me@nutomic.com>
Date: Wed, 17 Mar 2021 18:12:37 +0100
Subject: [PATCH] Refactor activitypub code

---
 crates/api/src/community.rs                   |   9 +-
 crates/apub/src/activities/receive/comment.rs |   3 +-
 .../apub/src/activities/receive/community.rs  |  59 +---------
 crates/apub/src/activities/receive/post.rs    |   3 +-
 crates/apub/src/activities/send/community.rs  | 106 +++++++-----------
 crates/apub/src/activities/send/user.rs       |  44 +-------
 crates/apub/src/activity_queue.rs             |   1 +
 crates/apub/src/inbox/community_inbox.rs      |   1 +
 .../apub/src/inbox/receive_for_community.rs   |  16 ++-
 crates/apub/src/inbox/user_inbox.rs           |  41 ++++++-
 crates/apub/src/lib.rs                        |  82 ++++++++------
 11 files changed, 158 insertions(+), 207 deletions(-)

diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs
index bb341294..0bb64f37 100644
--- a/crates/api/src/community.rs
+++ b/crates/api/src/community.rs
@@ -10,13 +10,14 @@ use actix_web::web::Data;
 use anyhow::Context;
 use lemmy_api_structs::{blocking, community::*};
 use lemmy_apub::{
-  activities::send::community::{send_add_mod, send_remove_mod},
   generate_apub_endpoint,
   generate_followers_url,
   generate_inbox_url,
   generate_shared_inbox_url,
   ActorType,
+  CommunityType,
   EndpointType,
+  UserType,
 };
 use lemmy_db_queries::{
   diesel_option_overwrite_to_url,
@@ -745,9 +746,11 @@ impl Perform for AddModToCommunity {
     })
     .await??;
     if data.added {
-      send_add_mod(user, updated_mod, community, context).await?;
+      community.send_add_mod(&user, updated_mod, context).await?;
     } else {
-      send_remove_mod(user, updated_mod, community, context).await?;
+      community
+        .send_remove_mod(&user, updated_mod, context)
+        .await?;
     }
 
     // Note: in case a remote mod is added, this returns the old moderators list, it will only get
diff --git a/crates/apub/src/activities/receive/comment.rs b/crates/apub/src/activities/receive/comment.rs
index bc150793..07f25eec 100644
--- a/crates/apub/src/activities/receive/comment.rs
+++ b/crates/apub/src/activities/receive/comment.rs
@@ -1,6 +1,6 @@
 use crate::{activities::receive::get_actor_as_user, objects::FromApub, ActorType, NoteExt};
 use activitystreams::{
-  activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update},
+  activity::{ActorAndObjectRefExt, Create, Dislike, Like, Update},
   base::ExtendsExt,
 };
 use anyhow::Context;
@@ -221,7 +221,6 @@ pub(crate) async fn receive_delete_comment(
 
 pub(crate) async fn receive_remove_comment(
   context: &LemmyContext,
-  _remove: Remove,
   comment: Comment,
 ) -> Result<(), LemmyError> {
   let removed_comment = blocking(context.pool(), move |conn| {
diff --git a/crates/apub/src/activities/receive/community.rs b/crates/apub/src/activities/receive/community.rs
index cf85ad10..48f6b295 100644
--- a/crates/apub/src/activities/receive/community.rs
+++ b/crates/apub/src/activities/receive/community.rs
@@ -1,19 +1,9 @@
-use crate::{
-  activities::receive::verify_activity_domains_valid,
-  inbox::verify_is_addressed_to_public,
-};
-use activitystreams::{
-  activity::{ActorAndObjectRefExt, Delete, Remove, Undo},
-  base::{AnyBase, ExtendsExt},
-};
-use anyhow::Context;
 use lemmy_api_structs::{blocking, community::CommunityResponse};
-use lemmy_db_queries::{source::community::Community_, ApubObject};
+use lemmy_db_queries::source::community::Community_;
 use lemmy_db_schema::source::community::Community;
 use lemmy_db_views_actor::community_view::CommunityView;
-use lemmy_utils::{location_info, LemmyError};
+use lemmy_utils::LemmyError;
 use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
-use url::Url;
 
 pub(crate) async fn receive_delete_community(
   context: &LemmyContext,
@@ -45,23 +35,8 @@ pub(crate) async fn receive_delete_community(
 
 pub(crate) async fn receive_remove_community(
   context: &LemmyContext,
-  activity: AnyBase,
-  expected_domain: &Url,
+  community: Community,
 ) -> Result<(), LemmyError> {
-  let remove = Remove::from_any_base(activity)?.context(location_info!())?;
-  verify_activity_domains_valid(&remove, expected_domain, true)?;
-  verify_is_addressed_to_public(&remove)?;
-
-  let community_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, &community_uri.into())
-  })
-  .await??;
-
   let removed_community = blocking(context.pool(), move |conn| {
     Community::update_removed(conn, community.id, true)
   })
@@ -88,16 +63,8 @@ pub(crate) async fn receive_remove_community(
 
 pub(crate) async fn receive_undo_delete_community(
   context: &LemmyContext,
-  undo: Undo,
   community: Community,
-  expected_domain: &Url,
 ) -> Result<(), LemmyError> {
-  verify_is_addressed_to_public(&undo)?;
-  let inner = undo.object().to_owned().one().context(location_info!())?;
-  let delete = Delete::from_any_base(inner)?.context(location_info!())?;
-  verify_activity_domains_valid(&delete, expected_domain, true)?;
-  verify_is_addressed_to_public(&delete)?;
-
   let deleted_community = blocking(context.pool(), move |conn| {
     Community::update_deleted(conn, community.id, false)
   })
@@ -124,26 +91,8 @@ pub(crate) async fn receive_undo_delete_community(
 
 pub(crate) async fn receive_undo_remove_community(
   context: &LemmyContext,
-  undo: Undo,
-  expected_domain: &Url,
+  community: Community,
 ) -> Result<(), LemmyError> {
-  verify_is_addressed_to_public(&undo)?;
-
-  let inner = undo.object().to_owned().one().context(location_info!())?;
-  let remove = Remove::from_any_base(inner)?.context(location_info!())?;
-  verify_activity_domains_valid(&remove, &expected_domain, true)?;
-  verify_is_addressed_to_public(&remove)?;
-
-  let community_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, &community_uri.into())
-  })
-  .await??;
-
   let removed_community = blocking(context.pool(), move |conn| {
     Community::update_removed(conn, community.id, false)
   })
diff --git a/crates/apub/src/activities/receive/post.rs b/crates/apub/src/activities/receive/post.rs
index 33b2ce67..b0582e60 100644
--- a/crates/apub/src/activities/receive/post.rs
+++ b/crates/apub/src/activities/receive/post.rs
@@ -6,7 +6,7 @@ use crate::{
   PageExt,
 };
 use activitystreams::{
-  activity::{Announce, Create, Dislike, Like, Remove, Update},
+  activity::{Announce, Create, Dislike, Like, Update},
   prelude::*,
 };
 use anyhow::Context;
@@ -216,7 +216,6 @@ pub(crate) async fn receive_delete_post(
 
 pub(crate) async fn receive_remove_post(
   context: &LemmyContext,
-  _remove: Remove,
   post: Post,
 ) -> Result<(), LemmyError> {
   let removed_post = blocking(context.pool(), move |conn| {
diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs
index ff445ac6..c6c90d50 100644
--- a/crates/apub/src/activities/send/community.rs
+++ b/crates/apub/src/activities/send/community.rs
@@ -6,6 +6,7 @@ use crate::{
   fetcher::user::get_or_fetch_and_upsert_user,
   generate_moderators_url,
   ActorType,
+  CommunityType,
 };
 use activitystreams::{
   activity::{
@@ -56,23 +57,10 @@ impl ActorType for Community {
       .unwrap_or_else(|| self.inbox_url.to_owned())
       .into()
   }
+}
 
-  async fn send_follow(
-    &self,
-    _follow_actor_id: &Url,
-    _context: &LemmyContext,
-  ) -> Result<(), LemmyError> {
-    unimplemented!()
-  }
-
-  async fn send_unfollow(
-    &self,
-    _follow_actor_id: &Url,
-    _context: &LemmyContext,
-  ) -> Result<(), LemmyError> {
-    unimplemented!()
-  }
-
+#[async_trait::async_trait(?Send)]
+impl CommunityType for Community {
   /// As a local community, accept the follow request from a remote user.
   async fn send_accept_follow(
     &self,
@@ -177,7 +165,7 @@ impl ActorType for Community {
       .set_many_contexts(lemmy_context()?)
       .set_id(generate_activity_id(AnnounceType::Announce)?)
       .set_to(public())
-      .set_many_ccs(vec![self.followers_url.clone().into_inner()]);
+      .set_many_ccs(vec![self.actor_id()]);
 
     send_to_community_followers(announce, self, context).await?;
 
@@ -204,58 +192,46 @@ impl ActorType for Community {
 
     Ok(inboxes)
   }
-}
 
-pub async fn send_add_mod(
-  actor: User_,
-  added_mod: User_,
-  community: Community,
-  context: &LemmyContext,
-) -> Result<(), LemmyError> {
-  let mut add = Add::new(
-    actor.actor_id.clone().into_inner(),
-    added_mod.actor_id.into_inner(),
-  );
-  add
-    .set_many_contexts(lemmy_context()?)
-    .set_id(generate_activity_id(AddType::Add)?)
-    .set_to(public())
-    .set_many_ccs(vec![community.actor_id()])
-    .set_target(generate_moderators_url(&community.actor_id)?.into_inner());
+  async fn send_add_mod(
+    &self,
+    actor: &User_,
+    added_mod: User_,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError> {
+    let mut add = Add::new(
+      actor.actor_id.clone().into_inner(),
+      added_mod.actor_id.into_inner(),
+    );
+    add
+      .set_many_contexts(lemmy_context()?)
+      .set_id(generate_activity_id(AddType::Add)?)
+      .set_to(public())
+      .set_many_ccs(vec![self.actor_id()])
+      .set_target(generate_moderators_url(&self.actor_id)?.into_inner());
 
-  if community.local {
-    community
-      .send_announce(add.into_any_base()?, context)
-      .await?;
-  } else {
-    send_to_community(add, &actor, &community, context).await?;
+    send_to_community(add, actor, self, context).await?;
+    Ok(())
   }
-  Ok(())
-}
 
-pub async fn send_remove_mod(
-  actor: User_,
-  removed_mod: User_,
-  community: Community,
-  context: &LemmyContext,
-) -> Result<(), LemmyError> {
-  let mut remove = Remove::new(
-    actor.actor_id.clone().into_inner(),
-    removed_mod.actor_id.into_inner(),
-  );
-  remove
-    .set_many_contexts(lemmy_context()?)
-    .set_id(generate_activity_id(RemoveType::Remove)?)
-    .set_to(public())
-    .set_many_ccs(vec![community.actor_id()])
-    .set_target(generate_moderators_url(&community.actor_id)?.into_inner());
+  async fn send_remove_mod(
+    &self,
+    actor: &User_,
+    removed_mod: User_,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError> {
+    let mut remove = Remove::new(
+      actor.actor_id.clone().into_inner(),
+      removed_mod.actor_id.into_inner(),
+    );
+    remove
+      .set_many_contexts(lemmy_context()?)
+      .set_id(generate_activity_id(RemoveType::Remove)?)
+      .set_to(public())
+      .set_many_ccs(vec![self.actor_id()])
+      .set_target(generate_moderators_url(&self.actor_id)?.into_inner());
 
-  if community.local {
-    community
-      .send_announce(remove.into_any_base()?, context)
-      .await?;
-  } else {
-    send_to_community(remove, &actor, &community, context).await?;
+    send_to_community(remove, &actor, self, context).await?;
+    Ok(())
   }
-  Ok(())
 }
diff --git a/crates/apub/src/activities/send/user.rs b/crates/apub/src/activities/send/user.rs
index 1dc62e0b..a0778c08 100644
--- a/crates/apub/src/activities/send/user.rs
+++ b/crates/apub/src/activities/send/user.rs
@@ -3,6 +3,7 @@ use crate::{
   activity_queue::send_activity_single_dest,
   extensions::context::lemmy_context,
   ActorType,
+  UserType,
 };
 use activitystreams::{
   activity::{
@@ -10,11 +11,11 @@ use activitystreams::{
     Follow,
     Undo,
   },
-  base::{AnyBase, BaseExt, ExtendsExt},
+  base::{BaseExt, ExtendsExt},
   object::ObjectExt,
 };
 use lemmy_api_structs::blocking;
-use lemmy_db_queries::{ApubObject, DbPool, Followable};
+use lemmy_db_queries::{ApubObject, Followable};
 use lemmy_db_schema::source::{
   community::{Community, CommunityFollower, CommunityFollowerForm},
   user::User_,
@@ -47,7 +48,10 @@ impl ActorType for User_ {
       .unwrap_or_else(|| self.inbox_url.to_owned())
       .into()
   }
+}
 
+#[async_trait::async_trait(?Send)]
+impl UserType for User_ {
   /// As a given local user, send out a follow request to a remote community.
   async fn send_follow(
     &self,
@@ -110,40 +114,4 @@ impl ActorType for User_ {
     send_activity_single_dest(undo, self, community.inbox_url.into(), context).await?;
     Ok(())
   }
-
-  async fn send_accept_follow(
-    &self,
-    _follow: Follow,
-    _context: &LemmyContext,
-  ) -> Result<(), LemmyError> {
-    unimplemented!()
-  }
-
-  async fn send_delete(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
-    unimplemented!()
-  }
-
-  async fn send_undo_delete(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
-    unimplemented!()
-  }
-
-  async fn send_remove(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
-    unimplemented!()
-  }
-
-  async fn send_undo_remove(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
-    unimplemented!()
-  }
-
-  async fn send_announce(
-    &self,
-    _activity: AnyBase,
-    _context: &LemmyContext,
-  ) -> Result<(), LemmyError> {
-    unimplemented!()
-  }
-
-  async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
-    unimplemented!()
-  }
 }
diff --git a/crates/apub/src/activity_queue.rs b/crates/apub/src/activity_queue.rs
index f607dafe..9cdc3886 100644
--- a/crates/apub/src/activity_queue.rs
+++ b/crates/apub/src/activity_queue.rs
@@ -3,6 +3,7 @@ use crate::{
   extensions::signatures::sign_and_send,
   insert_activity,
   ActorType,
+  CommunityType,
   APUB_JSON_CONTENT_TYPE,
 };
 use activitystreams::{
diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs
index 1495c4bb..cd1c775b 100644
--- a/crates/apub/src/inbox/community_inbox.rs
+++ b/crates/apub/src/inbox/community_inbox.rs
@@ -20,6 +20,7 @@ use crate::{
   },
   insert_activity,
   ActorType,
+  CommunityType,
 };
 use activitystreams::{
   activity::{kind::FollowType, ActorAndObject, Follow, Undo},
diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs
index 78c4107a..cd8f32bb 100644
--- a/crates/apub/src/inbox/receive_for_community.rs
+++ b/crates/apub/src/inbox/receive_for_community.rs
@@ -35,10 +35,13 @@ use crate::{
     objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
     user::get_or_fetch_and_upsert_user,
   },
+  find_object_by_id,
   find_post_or_comment_by_id,
   generate_moderators_url,
   inbox::verify_is_addressed_to_public,
   ActorType,
+  CommunityType,
+  Object,
   PostOrComment,
 };
 use activitystreams::{
@@ -254,8 +257,8 @@ pub(in crate::inbox) async fn receive_remove_for_community(
       .context(location_info!())?;
 
     match find_post_or_comment_by_id(context, object).await {
-      Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await,
-      Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).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(()),
     }
@@ -600,9 +603,12 @@ where
     .map(|o| o.id())
     .flatten()
     .context(location_info!())?;
-  let original_id = match find_post_or_comment_by_id(context, object_id.to_owned()).await? {
-    PostOrComment::Post(p) => p.ap_id.into_inner(),
-    PostOrComment::Comment(c) => c.ap_id.into_inner(),
+  let original_id = match find_object_by_id(context, object_id.to_owned()).await? {
+    Object::Post(p) => p.ap_id.into_inner(),
+    Object::Comment(c) => c.ap_id.into_inner(),
+    Object::Community(c) => c.actor_id(),
+    Object::User(u) => u.actor_id(),
+    Object::PrivateMessage(p) => p.ap_id.into_inner(),
   };
   if actor_id.domain() != original_id.domain() {
     let community = extract_community_from_cc(activity, context).await?;
diff --git a/crates/apub/src/inbox/user_inbox.rs b/crates/apub/src/inbox/user_inbox.rs
index 2ea2e1d1..3d16403b 100644
--- a/crates/apub/src/inbox/user_inbox.rs
+++ b/crates/apub/src/inbox/user_inbox.rs
@@ -41,7 +41,7 @@ use crate::{
   ActorType,
 };
 use activitystreams::{
-  activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Undo, Update},
+  activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Remove, Undo, Update},
   base::AnyBase,
   prelude::*,
 };
@@ -165,7 +165,7 @@ pub(crate) async fn user_receive_message(
       receive_delete(context, any_base, &actor_url, request_counter).await?
     }
     UserValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?,
-    UserValidTypes::Remove => receive_remove_community(&context, any_base, &actor_url).await?,
+    UserValidTypes::Remove => receive_remove(context, any_base, &actor_url).await?,
   };
 
   // TODO: would be logical to move websocket notification code here
@@ -370,13 +370,31 @@ async fn receive_delete(
   }
 }
 
+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> {
-  use CommunityOrPrivateMessage::*;
   let undo = Undo::from_any_base(any_base)?.context(location_info!())?;
   verify_activity_domains_valid(&undo, expected_domain, true)?;
 
@@ -391,15 +409,28 @@ async fn receive_undo(
         .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, undo, c, expected_domain).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") => receive_undo_remove_community(context, undo, expected_domain).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),
   }
 }
diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs
index 307a8c8c..ec5c6d94 100644
--- a/crates/apub/src/lib.rs
+++ b/crates/apub/src/lib.rs
@@ -152,38 +152,6 @@ pub trait ActorType {
   fn public_key(&self) -> Option<String>;
   fn private_key(&self) -> Option<String>;
 
-  async fn send_follow(
-    &self,
-    follow_actor_id: &Url,
-    context: &LemmyContext,
-  ) -> Result<(), LemmyError>;
-  async fn send_unfollow(
-    &self,
-    follow_actor_id: &Url,
-    context: &LemmyContext,
-  ) -> Result<(), LemmyError>;
-
-  async fn send_accept_follow(
-    &self,
-    follow: Follow,
-    context: &LemmyContext,
-  ) -> Result<(), LemmyError>;
-
-  async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
-  async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
-
-  async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
-  async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
-
-  async fn send_announce(
-    &self,
-    activity: AnyBase,
-    context: &LemmyContext,
-  ) -> Result<(), LemmyError>;
-
-  /// For a given community, returns the inboxes of all followers.
-  async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
-
   fn get_shared_inbox_or_inbox_url(&self) -> Url;
 
   /// Outbox URL is not generally used by Lemmy, so it can be generated on the fly (but only for
@@ -207,6 +175,55 @@ pub trait ActorType {
   }
 }
 
+#[async_trait::async_trait(?Send)]
+pub trait CommunityType {
+  async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
+  async fn send_accept_follow(
+    &self,
+    follow: Follow,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError>;
+
+  async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
+  async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
+
+  async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
+  async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
+
+  async fn send_announce(
+    &self,
+    activity: AnyBase,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError>;
+
+  async fn send_add_mod(
+    &self,
+    actor: &User_,
+    added_mod: User_,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError>;
+  async fn send_remove_mod(
+    &self,
+    actor: &User_,
+    removed_mod: User_,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError>;
+}
+
+#[async_trait::async_trait(?Send)]
+pub trait UserType {
+  async fn send_follow(
+    &self,
+    follow_actor_id: &Url,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError>;
+  async fn send_unfollow(
+    &self,
+    follow_actor_id: &Url,
+    context: &LemmyContext,
+  ) -> Result<(), LemmyError>;
+}
+
 pub enum EndpointType {
   Community,
   User,
@@ -319,6 +336,7 @@ pub(crate) async fn find_post_or_comment_by_id(
   Err(NotFound.into())
 }
 
+#[derive(Debug)]
 pub(crate) enum Object {
   Comment(Comment),
   Post(Post),
-- 
2.44.1