From: Felix Ableitner <me@nutomic.com>
Date: Tue, 9 Mar 2021 17:13:08 +0000 (+0100)
Subject: Allow adding remote users as community mods (ref #1061)
X-Git-Url: http://these/git/%7Biframely.url%7D?a=commitdiff_plain;h=3ffae1f5b85612953a0b31c863762ba2e8faa580;p=lemmy.git

Allow adding remote users as community mods (ref #1061)
---

diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs
index cee5d371..faedbed6 100644
--- a/crates/api/src/community.rs
+++ b/crates/api/src/community.rs
@@ -10,6 +10,7 @@ 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,
@@ -34,7 +35,7 @@ use lemmy_db_queries::{
 };
 use lemmy_db_schema::{
   naive_now,
-  source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
+  source::{comment::Comment, community::*, moderator::*, post::Post, site::*, user::User_},
 };
 use lemmy_db_views::comment_view::CommentQueryBuilder;
 use lemmy_db_views_actor::{
@@ -699,16 +700,16 @@ impl Perform for AddModToCommunity {
     let data: &AddModToCommunity = &self;
     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let community_moderator_form = CommunityModeratorForm {
-      community_id: data.community_id,
-      user_id: data.user_id,
-    };
-
     let community_id = data.community_id;
 
     // Verify that only mods or admins can add mod
     is_mod_or_admin(context.pool(), user.id, community_id).await?;
 
+    // Update in local database
+    let community_moderator_form = CommunityModeratorForm {
+      community_id: data.community_id,
+      user_id: data.user_id,
+    };
     if data.added {
       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
       if blocking(context.pool(), join).await?.is_err() {
@@ -733,6 +734,25 @@ impl Perform for AddModToCommunity {
     })
     .await??;
 
+    // Send to federated instances
+    let updated_mod_id = data.user_id;
+    let updated_mod = blocking(context.pool(), move |conn| {
+      User_::read(conn, updated_mod_id)
+    })
+    .await??;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
+    dbg!(data.added);
+    if data.added {
+      send_add_mod(user, updated_mod, community, context).await?;
+    } else {
+      send_remove_mod(user, updated_mod, community, context).await?;
+    }
+
+    // Note: in case a remote mod is added, this returns the old moderators list, it will only get
+    //       updated once we receive an activity from the community (like `Announce/Add/Moderator`)
     let community_id = data.community_id;
     let moderators = blocking(context.pool(), move |conn| {
       CommunityModeratorView::for_community(conn, community_id)
@@ -740,18 +760,18 @@ impl Perform for AddModToCommunity {
     .await??;
 
     let res = AddModToCommunityResponse { moderators };
-
     context.chat_server().do_send(SendCommunityRoomMessage {
       op: UserOperation::AddModToCommunity,
       response: res.clone(),
       community_id,
       websocket_id,
     });
-
     Ok(res)
   }
 }
 
+// TODO: we dont do anything for federation here, it should be updated the next time the community
+//       gets fetched. i hope we can get rid of the community creator role soon.
 #[async_trait::async_trait(?Send)]
 impl Perform for TransferCommunity {
   type Response = GetCommunityResponse;
diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs
index 8e25b512..cb61fcf2 100644
--- a/crates/apub/src/activities/mod.rs
+++ b/crates/apub/src/activities/mod.rs
@@ -1,2 +1,2 @@
 pub(crate) mod receive;
-pub(crate) mod send;
+pub mod send;
diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs
index 3e77248f..ffbb456c 100644
--- a/crates/apub/src/activities/send/community.rs
+++ b/crates/apub/src/activities/send/community.rs
@@ -1,19 +1,22 @@
 use crate::{
   activities::send::generate_activity_id,
-  activity_queue::{send_activity_single_dest, send_to_community_followers},
+  activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers},
   check_is_apub_id_valid,
   extensions::context::lemmy_context,
   fetcher::user::get_or_fetch_and_upsert_user,
+  generate_moderators_url,
   ActorType,
 };
 use activitystreams::{
   activity::{
-    kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
+    kind::{AcceptType, AddType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
     Accept,
     ActorAndObjectRefExt,
+    Add,
     Announce,
     Delete,
     Follow,
+    OptTargetRefExt,
     Remove,
     Undo,
   },
@@ -25,7 +28,7 @@ use anyhow::Context;
 use itertools::Itertools;
 use lemmy_api_structs::blocking;
 use lemmy_db_queries::DbPool;
-use lemmy_db_schema::source::community::Community;
+use lemmy_db_schema::source::{community::Community, user::User_};
 use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
 use lemmy_utils::{location_info, LemmyError};
 use lemmy_websocket::LemmyContext;
@@ -202,3 +205,55 @@ 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_many_tos(vec![community.actor_id.to_owned().into_inner(), public()])
+    .set_target(generate_moderators_url(&community.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?;
+  }
+  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_many_tos(vec![community.actor_id.to_owned().into_inner(), public()])
+    .set_target(generate_moderators_url(&community.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?;
+  }
+  Ok(())
+}
diff --git a/crates/apub/src/activities/send/mod.rs b/crates/apub/src/activities/send/mod.rs
index 2da0b48c..80660044 100644
--- a/crates/apub/src/activities/send/mod.rs
+++ b/crates/apub/src/activities/send/mod.rs
@@ -3,7 +3,7 @@ use url::{ParseError, Url};
 use uuid::Uuid;
 
 pub(crate) mod comment;
-pub(crate) mod community;
+pub mod community;
 pub(crate) mod post;
 pub(crate) mod private_message;
 pub(crate) mod user;
diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs
index 438a8b3d..c857b0b4 100644
--- a/crates/apub/src/inbox/receive_for_community.rs
+++ b/crates/apub/src/inbox/receive_for_community.rs
@@ -37,6 +37,7 @@ use crate::{
   },
   find_post_or_comment_by_id,
   inbox::is_addressed_to_public,
+  ActorType,
   PostOrComment,
 };
 use activitystreams::{
@@ -58,7 +59,7 @@ use activitystreams::{
 use anyhow::{anyhow, Context};
 use diesel::result::Error::NotFound;
 use lemmy_api_structs::blocking;
-use lemmy_db_queries::{ApubObject, Crud, Joinable};
+use lemmy_db_queries::{source::community::CommunityModerator_, ApubObject, Crud, Joinable};
 use lemmy_db_schema::{
   source::{
     community::{Community, CommunityModerator, CommunityModeratorForm},
@@ -213,7 +214,7 @@ pub(in crate::inbox) async fn receive_remove_for_community(
   expected_domain: &Url,
   request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
-  let remove = Remove::from_any_base(activity)?.context(location_info!())?;
+  let remove = Remove::from_any_base(activity.to_owned())?.context(location_info!())?;
   verify_activity_domains_valid(&remove, &expected_domain, false)?;
   is_addressed_to_public(&remove)?;
 
@@ -234,6 +235,8 @@ pub(in crate::inbox) async fn receive_remove_for_community(
       CommunityModerator::leave(conn, &form)
     })
     .await??;
+    community.send_announce(activity, context).await?;
+    // TODO: send websocket notification about removed mod
     Ok(())
   }
   // Remove a post or comment
@@ -385,7 +388,7 @@ pub(in crate::inbox) async fn receive_add_for_community(
   expected_domain: &Url,
   request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
-  let add = Add::from_any_base(activity)?.context(location_info!())?;
+  let add = Add::from_any_base(activity.to_owned())?.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?;
@@ -395,14 +398,28 @@ pub(in crate::inbox) async fn receive_add_for_community(
     .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)
+
+  // 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_user_moderated_communities(conn, new_mod_id)
   })
   .await??;
+  if moderated_communities.contains(&community.id) {
+    let form = CommunityModeratorForm {
+      community_id: community.id,
+      user_id: new_mod.id,
+    };
+    blocking(context.pool(), move |conn| {
+      CommunityModerator::join(conn, &form)
+    })
+    .await??;
+  }
+  if community.local {
+    community.send_announce(activity, context).await?;
+  }
+  // TODO: send websocket notification about added mod
   Ok(())
 }
 
@@ -458,7 +475,7 @@ where
   // should be the moderators collection of a local community
   let target = activity
     .target()
-    .map(|t| t.as_single_xsd_string())
+    .map(|t| t.as_single_xsd_any_uri())
     .flatten()
     .context(location_info!())?;
   // TODO: very hacky, we should probably store the moderators url in db
diff --git a/crates/apub/src/inbox/shared_inbox.rs b/crates/apub/src/inbox/shared_inbox.rs
index 8c197a85..ae40b891 100644
--- a/crates/apub/src/inbox/shared_inbox.rs
+++ b/crates/apub/src/inbox/shared_inbox.rs
@@ -36,6 +36,7 @@ pub enum ValidTypes {
   Undo,
   Remove,
   Announce,
+  Add,
 }
 
 // TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject,
diff --git a/crates/apub/src/inbox/user_inbox.rs b/crates/apub/src/inbox/user_inbox.rs
index 6e047674..d99092fc 100644
--- a/crates/apub/src/inbox/user_inbox.rs
+++ b/crates/apub/src/inbox/user_inbox.rs
@@ -28,6 +28,7 @@ use crate::{
     is_addressed_to_local_user,
     is_addressed_to_public,
     receive_for_community::{
+      receive_add_for_community,
       receive_create_for_community,
       receive_delete_for_community,
       receive_dislike_for_community,
@@ -252,6 +253,7 @@ enum AnnouncableActivities {
   Delete,
   Remove,
   Undo,
+  Add,
 }
 
 /// Takes an announce and passes the inner activity to the appropriate handler.
@@ -302,6 +304,9 @@ pub async fn receive_announce(
     Some(Undo) => {
       receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await
     }
+    Some(Add) => {
+      receive_add_for_community(context, inner_activity, &inner_id, request_counter).await
+    }
     _ => receive_unhandled_activity(inner_activity),
   }
 }