From: Felix Ableitner <me@nutomic.com>
Date: Mon, 8 Mar 2021 16:34:54 +0000 (+0100)
Subject: Implemented receiving activities to add/remove remote mods
X-Git-Url: http://these/git/%7Bthis.getImageSrc%28%29%7D?a=commitdiff_plain;h=9172eff65a368300d48e1ace69092a4788721ba7;p=lemmy.git

Implemented receiving activities to add/remove remote mods
---

diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs
index f003fb16..43072c51 100644
--- a/crates/apub/src/inbox/community_inbox.rs
+++ b/crates/apub/src/inbox/community_inbox.rs
@@ -8,10 +8,12 @@ use crate::{
     is_activity_already_known,
     is_addressed_to_public,
     receive_for_community::{
+      receive_add_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,
     },
@@ -51,7 +53,8 @@ pub enum CommunityValidTypes {
   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
+  Remove,  // post or comment removed by mod or admin, or mod removed from community
+  Add,     // mod added to community
 }
 
 pub type CommunityAcceptedActivities = ActorAndObject<CommunityValidTypes>;
@@ -160,10 +163,13 @@ pub(crate) async fn community_receive_message(
       receive_delete_for_community(context, any_base.clone(), &actor_url).await?;
       true
     }
+    CommunityValidTypes::Add => {
+      receive_add_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
+      true
+    }
     CommunityValidTypes::Remove => {
-      // TODO: we dont support remote mods, so this is ignored for now
-      //receive_remove_for_community(context, any_base.clone(), &user_url).await?
-      false
+      receive_remove_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
+      true
     }
   };
 
diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs
index a3ffbf11..aab17840 100644
--- a/crates/apub/src/inbox/receive_for_community.rs
+++ b/crates/apub/src/inbox/receive_for_community.rs
@@ -31,21 +31,32 @@ use crate::{
     receive_unhandled_activity,
     verify_activity_domains_valid,
   },
-  fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
+  fetcher::{
+    objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
+    user::get_or_fetch_and_upsert_user,
+  },
   find_post_or_comment_by_id,
   inbox::is_addressed_to_public,
   PostOrComment,
 };
 use activitystreams::{
-  activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
+  activity::{ActorAndObjectRef, Add, Create, Delete, Dislike, Like, Remove, Undo, Update},
   base::AnyBase,
   prelude::*,
 };
-use anyhow::Context;
+use anyhow::{anyhow, Context};
 use diesel::result::Error::NotFound;
 use lemmy_api_structs::blocking;
-use lemmy_db_queries::Crud;
-use lemmy_db_schema::source::site::Site;
+use lemmy_db_queries::{ApubObject, Crud, Joinable};
+use lemmy_db_schema::{
+  source::{
+    community::{Community, CommunityModerator, CommunityModeratorForm},
+    site::Site,
+    user::User_,
+  },
+  DbUrl,
+};
+use lemmy_db_views_actor::community_view::CommunityView;
 use lemmy_utils::{location_info, LemmyError};
 use lemmy_websocket::LemmyContext;
 use strum_macros::EnumString;
@@ -189,36 +200,59 @@ pub(in crate::inbox) async fn receive_remove_for_community(
   context: &LemmyContext,
   activity: AnyBase,
   expected_domain: &Url,
+  request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
   let remove = Remove::from_any_base(activity)?.context(location_info!())?;
   verify_activity_domains_valid(&remove, &expected_domain, false)?;
   is_addressed_to_public(&remove)?;
 
-  let cc = remove
-    .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 object = remove
-    .object()
-    .to_owned()
-    .single_xsd_any_uri()
-    .context(location_info!())?;
-
-  // Ensure that remove activity comes from the same domain as the community
-  remove.id(community_id.domain().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,
-    // if we dont have the object, no need to do anything
-    Err(_) => Ok(()),
+  // Remove a moderator from community
+  if remove.target().is_some() {
+    let community = verify_actor_is_community_mod(&remove, context).await?;
+
+    let remove_mod = remove
+      .object()
+      .as_single_xsd_any_uri()
+      .context(location_info!())?;
+    let remove_mod = get_or_fetch_and_upsert_user(&remove_mod, context, request_counter).await?;
+    let form = CommunityModeratorForm {
+      community_id: community.id,
+      user_id: remove_mod.id,
+    };
+    blocking(context.pool(), move |conn| {
+      CommunityModerator::leave(conn, &form)
+    })
+    .await??;
+    Ok(())
+  }
+  // Remove a post or comment
+  else {
+    let cc = remove
+      .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 object = remove
+      .object()
+      .to_owned()
+      .single_xsd_any_uri()
+      .context(location_info!())?;
+
+    // Ensure that remove activity comes from the same domain as the community
+    remove.id(community_id.domain().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,
+      // if we dont have the object, no need to do anything
+      Err(_) => Ok(()),
+    }
   }
 }
 
@@ -333,6 +367,34 @@ pub(in crate::inbox) async fn receive_undo_like_for_community(
   }
 }
 
+/// 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,
+  activity: AnyBase,
+  expected_domain: &Url,
+  request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+  let add = Add::from_any_base(activity)?.context(location_info!())?;
+  verify_activity_domains_valid(&add, &expected_domain, false)?;
+  is_addressed_to_public(&add)?;
+  let community = verify_actor_is_community_mod(&add, context).await?;
+
+  let new_mod = add
+    .object()
+    .as_single_xsd_any_uri()
+    .context(location_info!())?;
+  let new_mod = get_or_fetch_and_upsert_user(&new_mod, context, request_counter).await?;
+  let form = CommunityModeratorForm {
+    community_id: community.id,
+    user_id: new_mod.id,
+  };
+  blocking(context.pool(), move |conn| {
+    CommunityModerator::join(conn, &form)
+  })
+  .await??;
+  Ok(())
+}
+
 /// A post or comment downvote being reverted
 pub(in crate::inbox) async fn receive_undo_dislike_for_community(
   context: &LemmyContext,
@@ -374,3 +436,46 @@ async fn fetch_post_or_comment_by_id(
 
   Err(NotFound.into())
 }
+
+async fn verify_actor_is_community_mod<T, Kind>(
+  activity: &T,
+  context: &LemmyContext,
+) -> Result<Community, LemmyError>
+where
+  T: ActorAndObjectRef + BaseExt<Kind>,
+{
+  // should be the moderators collection of a local community
+  // TODO: not compiling, seems to be a bug in activitystreams crate
+  let target = Url::parse("")?; //activity.target().as_single_xsd_any_uri().context(location_info!())?;
+                                // TODO: very hacky
+  let community_id: DbUrl = Url::parse(&target.to_string().replace("/moderators", ""))?.into();
+  let community = blocking(&context.pool(), move |conn| {
+    Community::read_from_apub_id(&conn, &community_id)
+  })
+  .await??;
+
+  let actor = activity
+    .actor()?
+    .as_single_xsd_any_uri()
+    .context(location_info!())?
+    .to_owned();
+  let actor = blocking(&context.pool(), move |conn| {
+    User_::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());
+  }
+
+  // TODO: the function name doesnt make sense if we return the community
+  Ok(community)
+}
diff --git a/crates/apub/src/inbox/user_inbox.rs b/crates/apub/src/inbox/user_inbox.rs
index 1a906d62..6e047674 100644
--- a/crates/apub/src/inbox/user_inbox.rs
+++ b/crates/apub/src/inbox/user_inbox.rs
@@ -296,7 +296,9 @@ pub async fn receive_announce(
       receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await
     }
     Some(Delete) => receive_delete_for_community(context, inner_activity, &inner_id).await,
-    Some(Remove) => receive_remove_for_community(context, inner_activity, &inner_id).await,
+    Some(Remove) => {
+      receive_remove_for_community(context, inner_activity, &inner_id, request_counter).await
+    }
     Some(Undo) => {
       receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await
     }