From 3ffae1f5b85612953a0b31c863762ba2e8faa580 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 9 Mar 2021 18:13:08 +0100 Subject: [PATCH] Allow adding remote users as community mods (ref #1061) --- crates/api/src/community.rs | 36 ++++++++--- crates/apub/src/activities/mod.rs | 2 +- crates/apub/src/activities/send/community.rs | 61 ++++++++++++++++++- crates/apub/src/activities/send/mod.rs | 2 +- .../apub/src/inbox/receive_for_community.rs | 37 ++++++++--- crates/apub/src/inbox/shared_inbox.rs | 1 + crates/apub/src/inbox/user_inbox.rs | 5 ++ 7 files changed, 121 insertions(+), 23 deletions(-) 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), } } -- 2.44.1