From beb8b9fe695f38cb9b08eb27b208f4f7f0e24e2e Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 5 Mar 2021 14:45:30 +0100 Subject: [PATCH 01/19] Use collection for moderators, instead of `attributedTo` (ref #1061) --- crates/apub/src/extensions/context.rs | 3 +- .../apub/src/extensions/group_extensions.rs | 15 ++- crates/apub/src/fetcher/community.rs | 46 +-------- crates/apub/src/objects/community.rs | 94 +++++++++++++++---- docker/federation/docker-compose.yml | 4 +- docker/federation/lemmy_alpha.hjson | 1 + 6 files changed, 95 insertions(+), 68 deletions(-) diff --git a/crates/apub/src/extensions/context.rs b/crates/apub/src/extensions/context.rs index b670e60d8..2dc5685a9 100644 --- a/crates/apub/src/extensions/context.rs +++ b/crates/apub/src/extensions/context.rs @@ -11,7 +11,8 @@ pub(crate) fn lemmy_context() -> Result, LemmyError> { "comments_enabled": { "kind": "sc:Boolean", "id": "pt:commentsEnabled" - } + }, + "moderators": "as:moderators" }))?; Ok(vec![AnyBase::from(context()), context_ext]) } diff --git a/crates/apub/src/extensions/group_extensions.rs b/crates/apub/src/extensions/group_extensions.rs index face43cab..11ea83822 100644 --- a/crates/apub/src/extensions/group_extensions.rs +++ b/crates/apub/src/extensions/group_extensions.rs @@ -1,7 +1,11 @@ -use activitystreams::unparsed::UnparsedMutExt; +use activitystreams::{ + collection::{CollectionExt, OrderedCollection}, + unparsed::UnparsedMutExt, +}; use activitystreams_ext::UnparsedExtension; use lemmy_utils::LemmyError; use serde::{Deserialize, Serialize}; +use url::Url; /// Activitystreams extension to allow (de)serializing additional Community field /// `sensitive` (called 'nsfw' in Lemmy). @@ -9,12 +13,17 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct GroupExtension { pub sensitive: Option, + pub moderators: Option, } impl GroupExtension { - pub fn new(sensitive: bool) -> Result { + pub fn new(sensitive: bool, moderators: Vec) -> Result { + let mut mods = OrderedCollection::new(); + mods.set_total_items(moderators.len() as u64); + mods.set_many_items(moderators); Ok(GroupExtension { sensitive: Some(sensitive), + moderators: Some(mods), }) } } @@ -28,11 +37,13 @@ where fn try_from_unparsed(unparsed_mut: &mut U) -> Result { Ok(GroupExtension { sensitive: unparsed_mut.remove("sensitive")?, + moderators: unparsed_mut.remove("moderators")?, }) } fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { unparsed_mut.insert("sensitive", self.sensitive)?; + unparsed_mut.insert("moderators", self.moderators)?; Ok(()) } } diff --git a/crates/apub/src/fetcher/community.rs b/crates/apub/src/fetcher/community.rs index fb545ed6a..b2440bf2b 100644 --- a/crates/apub/src/fetcher/community.rs +++ b/crates/apub/src/fetcher/community.rs @@ -1,10 +1,5 @@ use crate::{ - fetcher::{ - fetch::fetch_remote_object, - get_or_fetch_and_upsert_user, - is_deleted, - should_refetch_actor, - }, + fetcher::{fetch::fetch_remote_object, is_deleted, should_refetch_actor}, inbox::user_inbox::receive_announce, objects::FromApub, GroupExt, @@ -12,13 +7,12 @@ use crate::{ use activitystreams::{ actor::ApActorExt, collection::{CollectionExt, OrderedCollection}, - object::ObjectExt, }; use anyhow::Context; use diesel::result::Error::NotFound; use lemmy_api_structs::blocking; -use lemmy_db_queries::{source::community::Community_, ApubObject, Joinable}; -use lemmy_db_schema::source::community::{Community, CommunityModerator, CommunityModeratorForm}; +use lemmy_db_queries::{source::community::Community_, ApubObject}; +use lemmy_db_schema::source::community::Community; use lemmy_utils::{location_info, LemmyError}; use lemmy_websocket::LemmyContext; use log::debug; @@ -80,40 +74,6 @@ async fn fetch_remote_community( let community = Community::from_apub(&group, context, apub_id.to_owned(), recursion_counter).await?; - // Also add the community moderators too - let attributed_to = group.inner.attributed_to().context(location_info!())?; - let creator_and_moderator_uris: Vec<&Url> = attributed_to - .as_many() - .context(location_info!())? - .iter() - .map(|a| a.as_xsd_any_uri().context("")) - .collect::, anyhow::Error>>()?; - - let mut creator_and_moderators = Vec::new(); - - for uri in creator_and_moderator_uris { - let c_or_m = get_or_fetch_and_upsert_user(uri, context, recursion_counter).await?; - - creator_and_moderators.push(c_or_m); - } - - // TODO: need to make this work to update mods of existing communities - if old_community.is_none() { - let community_id = community.id; - blocking(context.pool(), move |conn| { - for mod_ in creator_and_moderators { - let community_moderator_form = CommunityModeratorForm { - community_id, - user_id: mod_.id, - }; - - CommunityModerator::join(conn, &community_moderator_form)?; - } - Ok(()) as Result<(), LemmyError> - }) - .await??; - } - // only fetch outbox for new communities, otherwise this can create an infinite loop if old_community.is_none() { let outbox = group.inner.outbox()?.context(location_info!())?; diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 200497b73..9431f71cd 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -23,10 +23,11 @@ use activitystreams::{ use activitystreams_ext::Ext2; use anyhow::Context; use lemmy_api_structs::blocking; -use lemmy_db_queries::DbPool; +use lemmy_db_queries::{DbPool, Joinable}; use lemmy_db_schema::{ naive_now, - source::community::{Community, CommunityForm}, + source::community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm}, + DbUrl, }; use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_utils::{ @@ -51,18 +52,13 @@ impl ToApub for Community { CommunityModeratorView::for_community(&conn, id) }) .await??; - let moderators: Vec = moderators - .into_iter() - .map(|m| m.moderator.actor_id.into_inner()) - .collect(); let mut group = ApObject::new(Group::new()); group .set_many_contexts(lemmy_context()?) .set_id(self.actor_id.to_owned().into()) .set_name(self.title.to_owned()) - .set_published(convert_datetime(self.published)) - .set_many_attributed_tos(moderators); + .set_published(convert_datetime(self.published)); if let Some(u) = self.updated.to_owned() { group.set_updated(convert_datetime(u)); @@ -93,9 +89,14 @@ impl ToApub for Community { ..Default::default() }); + let moderators: Vec = moderators + .into_iter() + .map(|m| m.moderator.actor_id.into_inner()) + .collect(); + Ok(Ext2::new( ap_actor, - GroupExtension::new(self.nsfw)?, + GroupExtension::new(self.nsfw, moderators)?, self.get_public_key_ext()?, )) } @@ -114,14 +115,57 @@ impl ToApub for Community { impl FromApub for Community { type ApubType = GroupExt; - /// Converts a `Group` to `Community`. + /// Converts a `Group` to `Community`, inserts it into the database and updates moderators. async fn from_apub( group: &GroupExt, context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, ) -> Result { - get_object_from_apub(group, context, expected_domain, request_counter).await + let community: Community = + get_object_from_apub(group, context, expected_domain, request_counter).await?; + + let new_moderators = get_community_moderators(group)?; + let community_id = community.id; + let current_moderators = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(&conn, community_id) + }) + .await??; + // Remove old mods from database which arent in the moderators collection anymore + for mod_user in ¤t_moderators { + if !new_moderators.contains(&&mod_user.moderator.actor_id.clone().into()) { + let community_moderator_form = CommunityModeratorForm { + community_id: mod_user.community.id, + user_id: mod_user.moderator.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::leave(conn, &community_moderator_form) + }) + .await??; + } + } + + // Add new mods to database which have been added to moderators collection + for mod_uri in new_moderators { + let mod_user = get_or_fetch_and_upsert_user(&mod_uri, context, request_counter).await?; + let current_mod_uris: Vec = current_moderators + .clone() + .iter() + .map(|c| c.moderator.actor_id.clone()) + .collect(); + if !current_mod_uris.contains(&mod_user.actor_id) { + let community_moderator_form = CommunityModeratorForm { + community_id: community.id, + user_id: mod_user.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::join(conn, &community_moderator_form) + }) + .await??; + } + } + + Ok(community) } } @@ -133,15 +177,8 @@ impl FromApubToForm for CommunityForm { expected_domain: Url, request_counter: &mut i32, ) -> Result { - let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?; - let creator_uri = creator_and_moderator_uris - .as_many() - .context(location_info!())? - .iter() - .next() - .context(location_info!())? - .as_xsd_any_uri() - .context(location_info!())?; + let moderator_uris = get_community_moderators(group)?; + let creator_uri = moderator_uris.first().context(location_info!())?; let creator = get_or_fetch_and_upsert_user(creator_uri, context, request_counter).await?; let name = group @@ -226,3 +263,20 @@ impl FromApubToForm for CommunityForm { }) } } + +fn get_community_moderators(group: &GroupExt) -> Result, LemmyError> { + if let Some(moderators) = &group.ext_one.moderators { + Ok( + moderators + .items() + .map(|i| i.as_many()) + .flatten() + .context(location_info!())? + .iter() + .filter_map(|i| i.as_xsd_any_uri()) + .collect(), + ) + } else { + Ok(vec![]) + } +} diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index c40de9022..a2ee7f267 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -29,7 +29,7 @@ services: - ./volumes/pictrs_alpha:/mnt lemmy-alpha-ui: - image: dessalines/lemmy-ui:0.9.9 + image: lemmy-ui:test environment: - LEMMY_INTERNAL_HOST=lemmy-alpha:8541 - LEMMY_EXTERNAL_HOST=localhost:8541 @@ -58,7 +58,7 @@ services: - ./volumes/postgres_alpha:/var/lib/postgresql/data lemmy-beta-ui: - image: dessalines/lemmy-ui:0.9.9 + image: lemmy-ui:test environment: - LEMMY_INTERNAL_HOST=lemmy-beta:8551 - LEMMY_EXTERNAL_HOST=localhost:8551 diff --git a/docker/federation/lemmy_alpha.hjson b/docker/federation/lemmy_alpha.hjson index e806397a8..4819fb26a 100644 --- a/docker/federation/lemmy_alpha.hjson +++ b/docker/federation/lemmy_alpha.hjson @@ -1,4 +1,5 @@ { + hostname: lemmy-alpha:8541 port: 8541 tls_enabled: false jwt_secret: changeme From 0c484e8c763aca299e9eb0449d38714746f65bea Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 8 Mar 2021 14:40:28 +0100 Subject: [PATCH 02/19] Move moderators collection to separate HTTP endpoint --- .../apub/src/extensions/group_extensions.rs | 14 ++--- crates/apub/src/fetcher/community.rs | 24 +++++++++ crates/apub/src/http/comment.rs | 4 +- crates/apub/src/http/community.rs | 51 ++++++++++++++++--- crates/apub/src/http/mod.rs | 2 +- crates/apub/src/http/post.rs | 4 +- crates/apub/src/http/user.rs | 10 ++-- crates/apub/src/lib.rs | 4 ++ crates/apub/src/objects/community.rs | 43 +++------------- crates/apub/src/routes.rs | 5 ++ 10 files changed, 97 insertions(+), 64 deletions(-) diff --git a/crates/apub/src/extensions/group_extensions.rs b/crates/apub/src/extensions/group_extensions.rs index 11ea83822..c83becf21 100644 --- a/crates/apub/src/extensions/group_extensions.rs +++ b/crates/apub/src/extensions/group_extensions.rs @@ -1,7 +1,4 @@ -use activitystreams::{ - collection::{CollectionExt, OrderedCollection}, - unparsed::UnparsedMutExt, -}; +use activitystreams::unparsed::UnparsedMutExt; use activitystreams_ext::UnparsedExtension; use lemmy_utils::LemmyError; use serde::{Deserialize, Serialize}; @@ -13,17 +10,14 @@ use url::Url; #[serde(rename_all = "camelCase")] pub struct GroupExtension { pub sensitive: Option, - pub moderators: Option, + pub moderators: Option, } impl GroupExtension { - pub fn new(sensitive: bool, moderators: Vec) -> Result { - let mut mods = OrderedCollection::new(); - mods.set_total_items(moderators.len() as u64); - mods.set_many_items(moderators); + pub fn new(sensitive: bool, moderators_url: Url) -> Result { Ok(GroupExtension { sensitive: Some(sensitive), - moderators: Some(mods), + moderators: Some(moderators_url), }) } } diff --git a/crates/apub/src/fetcher/community.rs b/crates/apub/src/fetcher/community.rs index b2440bf2b..9cc1bbd6a 100644 --- a/crates/apub/src/fetcher/community.rs +++ b/crates/apub/src/fetcher/community.rs @@ -103,3 +103,27 @@ async fn fetch_community_outbox( Ok(()) } + +pub(crate) async fn fetch_community_mods( + context: &LemmyContext, + group: &GroupExt, + recursion_counter: &mut i32, +) -> Result, LemmyError> { + if let Some(mods_url) = &group.ext_one.moderators { + let mods = + fetch_remote_object::(context.client(), mods_url, recursion_counter) + .await?; + let mods = mods + .items() + .map(|i| i.as_many()) + .flatten() + .context(location_info!())? + .iter() + .filter_map(|i| i.as_xsd_any_uri()) + .map(|u| u.to_owned()) + .collect(); + Ok(mods) + } else { + Ok(vec![]) + } +} diff --git a/crates/apub/src/http/comment.rs b/crates/apub/src/http/comment.rs index d4287224c..ee777fff9 100644 --- a/crates/apub/src/http/comment.rs +++ b/crates/apub/src/http/comment.rs @@ -12,12 +12,12 @@ use lemmy_websocket::LemmyContext; use serde::Deserialize; #[derive(Deserialize)] -pub struct CommentQuery { +pub(crate) struct CommentQuery { comment_id: String, } /// Return the ActivityPub json representation of a local comment over HTTP. -pub async fn get_apub_comment( +pub(crate) async fn get_apub_comment( info: Path, context: web::Data, ) -> Result, LemmyError> { diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 2306286a7..fcf20748a 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -1,5 +1,6 @@ use crate::{ extensions::context::lemmy_context, + generate_moderators_url, http::{create_apub_response, create_apub_tombstone_response}, objects::ToApub, ActorType, @@ -7,23 +8,27 @@ use crate::{ use activitystreams::{ base::{AnyBase, BaseExt}, collection::{CollectionExt, OrderedCollection, UnorderedCollection}, + url::Url, }; use actix_web::{body::Body, web, HttpResponse}; use lemmy_api_structs::blocking; use lemmy_db_queries::source::{activity::Activity_, community::Community_}; use lemmy_db_schema::source::{activity::Activity, community::Community}; -use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; +use lemmy_db_views_actor::{ + community_follower_view::CommunityFollowerView, + community_moderator_view::CommunityModeratorView, +}; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use serde::Deserialize; #[derive(Deserialize)] -pub struct CommunityQuery { +pub(crate) struct CommunityQuery { community_name: String, } /// Return the ActivityPub json representation of a local community over HTTP. -pub async fn get_apub_community_http( +pub(crate) async fn get_apub_community_http( info: web::Path, context: web::Data, ) -> Result, LemmyError> { @@ -42,7 +47,7 @@ pub async fn get_apub_community_http( } /// Returns an empty followers collection, only populating the size (for privacy). -pub async fn get_apub_community_followers( +pub(crate) async fn get_apub_community_followers( info: web::Path, context: web::Data, ) -> Result, LemmyError> { @@ -67,7 +72,7 @@ pub async fn get_apub_community_followers( /// Returns the community outbox, which is populated by a maximum of 20 posts (but no other /// activites like votes or comments). -pub async fn get_apub_community_outbox( +pub(crate) async fn get_apub_community_outbox( info: web::Path, context: web::Data, ) -> Result, LemmyError> { @@ -96,7 +101,7 @@ pub async fn get_apub_community_outbox( Ok(create_apub_response(&collection)) } -pub async fn get_apub_community_inbox( +pub(crate) async fn get_apub_community_inbox( info: web::Path, context: web::Data, ) -> Result, LemmyError> { @@ -107,7 +112,39 @@ pub async fn get_apub_community_inbox( let mut collection = OrderedCollection::new(); collection - .set_id(format!("{}/inbox", community.actor_id).parse()?) + .set_id(community.inbox_url.into()) + .set_many_contexts(lemmy_context()?); + Ok(create_apub_response(&collection)) +} + +pub(crate) async fn get_apub_community_moderators( + info: web::Path, + context: web::Data, +) -> Result, LemmyError> { + let community = blocking(context.pool(), move |conn| { + Community::read_from_name(&conn, &info.community_name) + }) + .await??; + + // The attributed to, is an ordered vector with the creator actor_ids first, + // then the rest of the moderators + // TODO Technically the instance admins can mod the community, but lets + // ignore that for now + let cid = community.id; + let moderators = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(&conn, cid) + }) + .await??; + + let moderators: Vec = moderators + .into_iter() + .map(|m| m.moderator.actor_id.into_inner()) + .collect(); + let mut collection = OrderedCollection::new(); + collection + .set_id(generate_moderators_url(&community.actor_id)?.into()) + .set_total_items(moderators.len() as u64) + .set_many_items(moderators) .set_many_contexts(lemmy_context()?); Ok(create_apub_response(&collection)) } diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index 6bf4bbde6..b343a6e8d 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -42,7 +42,7 @@ pub struct CommunityQuery { } /// Return the ActivityPub json representation of a local community over HTTP. -pub async fn get_activity( +pub(crate) async fn get_activity( info: web::Path, context: web::Data, ) -> Result, LemmyError> { diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs index 8bdded2a0..1b5589a0f 100644 --- a/crates/apub/src/http/post.rs +++ b/crates/apub/src/http/post.rs @@ -12,12 +12,12 @@ use lemmy_websocket::LemmyContext; use serde::Deserialize; #[derive(Deserialize)] -pub struct PostQuery { +pub(crate) struct PostQuery { post_id: String, } /// Return the ActivityPub json representation of a local post over HTTP. -pub async fn get_apub_post( +pub(crate) async fn get_apub_post( info: web::Path, context: web::Data, ) -> Result, LemmyError> { diff --git a/crates/apub/src/http/user.rs b/crates/apub/src/http/user.rs index 77c40d855..dcb73e3b0 100644 --- a/crates/apub/src/http/user.rs +++ b/crates/apub/src/http/user.rs @@ -18,12 +18,12 @@ use serde::Deserialize; use url::Url; #[derive(Deserialize)] -pub struct UserQuery { +pub(crate) struct UserQuery { user_name: String, } /// Return the ActivityPub json representation of a local user over HTTP. -pub async fn get_apub_user_http( +pub(crate) async fn get_apub_user_http( info: web::Path, context: web::Data, ) -> Result, LemmyError> { @@ -43,7 +43,7 @@ pub async fn get_apub_user_http( } } -pub async fn get_apub_user_outbox( +pub(crate) async fn get_apub_user_outbox( info: web::Path, context: web::Data, ) -> Result, LemmyError> { @@ -61,7 +61,7 @@ pub async fn get_apub_user_outbox( Ok(create_apub_response(&collection)) } -pub async fn get_apub_user_inbox( +pub(crate) async fn get_apub_user_inbox( info: web::Path, context: web::Data, ) -> Result, LemmyError> { @@ -72,7 +72,7 @@ pub async fn get_apub_user_inbox( let mut collection = OrderedCollection::new(); collection - .set_id(format!("{}/inbox", user.actor_id.into_inner()).parse()?) + .set_id(user.inbox_url.into()) .set_many_contexts(lemmy_context()?); Ok(create_apub_response(&collection)) } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 850ef503e..307a8c8c0 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -262,6 +262,10 @@ pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result Ok(Url::parse(&url)?.into()) } +pub(crate) fn generate_moderators_url(community_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{}/moderators", community_id))?.into()) +} + /// Store a sent or received activity in the database, for logging purposes. These records are not /// persistent. pub(crate) async fn insert_activity( diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 9431f71cd..278bd7b12 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -1,6 +1,7 @@ use crate::{ extensions::{context::lemmy_context, group_extensions::GroupExtension}, - fetcher::user::get_or_fetch_and_upsert_user, + fetcher::{community::fetch_community_mods, user::get_or_fetch_and_upsert_user}, + generate_moderators_url, objects::{ check_object_domain, create_tombstone, @@ -42,17 +43,7 @@ use url::Url; impl ToApub for Community { type ApubType = GroupExt; - async fn to_apub(&self, pool: &DbPool) -> Result { - // The attributed to, is an ordered vector with the creator actor_ids first, - // then the rest of the moderators - // TODO Technically the instance admins can mod the community, but lets - // ignore that for now - let id = self.id; - let moderators = blocking(pool, move |conn| { - CommunityModeratorView::for_community(&conn, id) - }) - .await??; - + async fn to_apub(&self, _pool: &DbPool) -> Result { let mut group = ApObject::new(Group::new()); group .set_many_contexts(lemmy_context()?) @@ -89,14 +80,9 @@ impl ToApub for Community { ..Default::default() }); - let moderators: Vec = moderators - .into_iter() - .map(|m| m.moderator.actor_id.into_inner()) - .collect(); - Ok(Ext2::new( ap_actor, - GroupExtension::new(self.nsfw, moderators)?, + GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?, self.get_public_key_ext()?, )) } @@ -125,7 +111,7 @@ impl FromApub for Community { let community: Community = get_object_from_apub(group, context, expected_domain, request_counter).await?; - let new_moderators = get_community_moderators(group)?; + let new_moderators = fetch_community_mods(context, group, request_counter).await?; let community_id = community.id; let current_moderators = blocking(context.pool(), move |conn| { CommunityModeratorView::for_community(&conn, community_id) @@ -177,7 +163,7 @@ impl FromApubToForm for CommunityForm { expected_domain: Url, request_counter: &mut i32, ) -> Result { - let moderator_uris = get_community_moderators(group)?; + let moderator_uris = fetch_community_mods(context, group, request_counter).await?; let creator_uri = moderator_uris.first().context(location_info!())?; let creator = get_or_fetch_and_upsert_user(creator_uri, context, request_counter).await?; @@ -263,20 +249,3 @@ impl FromApubToForm for CommunityForm { }) } } - -fn get_community_moderators(group: &GroupExt) -> Result, LemmyError> { - if let Some(moderators) = &group.ext_one.moderators { - Ok( - moderators - .items() - .map(|i| i.as_many()) - .flatten() - .context(location_info!())? - .iter() - .filter_map(|i| i.as_xsd_any_uri()) - .collect(), - ) - } else { - Ok(vec![]) - } -} diff --git a/crates/apub/src/routes.rs b/crates/apub/src/routes.rs index 07dcc7f88..2ec8c26d0 100644 --- a/crates/apub/src/routes.rs +++ b/crates/apub/src/routes.rs @@ -5,6 +5,7 @@ use crate::{ get_apub_community_followers, get_apub_community_http, get_apub_community_inbox, + get_apub_community_moderators, get_apub_community_outbox, }, get_activity, @@ -53,6 +54,10 @@ pub fn config(cfg: &mut web::ServiceConfig) { "/c/{community_name}/inbox", web::get().to(get_apub_community_inbox), ) + .route( + "/c/{community_name}/moderators", + web::get().to(get_apub_community_moderators), + ) .route("/u/{user_name}", web::get().to(get_apub_user_http)) .route("/u/{user_name}/outbox", web::get().to(get_apub_user_outbox)) .route("/u/{user_name}/inbox", web::get().to(get_apub_user_inbox)) From 9172eff65a368300d48e1ace69092a4788721ba7 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 8 Mar 2021 17:34:54 +0100 Subject: [PATCH 03/19] Implemented receiving activities to add/remove remote mods --- crates/apub/src/inbox/community_inbox.rs | 14 +- .../apub/src/inbox/receive_for_community.rs | 159 +++++++++++++++--- crates/apub/src/inbox/user_inbox.rs | 4 +- 3 files changed, 145 insertions(+), 32 deletions(-) diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs index f003fb16f..43072c512 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; @@ -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 a3ffbf116..aab178401 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!())?; + // Remove a moderator from community + if remove.target().is_some() { + let community = verify_actor_is_community_mod(&remove, context).await?; - let object = remove - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; + 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!())?; - // Ensure that remove activity comes from the same domain as the community - remove.id(community_id.domain().context(location_info!())?)?; + let object = remove + .object() + .to_owned() + .single_xsd_any_uri() + .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(()), + // 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( + activity: &T, + context: &LemmyContext, +) -> Result +where + T: ActorAndObjectRef + BaseExt, +{ + // 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 1a906d627..6e0476740 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 } From dcf40db22535ebd2dbf1fa26b42c5426875b7ced Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 9 Mar 2021 14:18:24 +0100 Subject: [PATCH 04/19] Update activitystreams to 0.7.0-alpha.11 --- Cargo.lock | 4 ++-- crates/apub/Cargo.toml | 2 +- .../apub/src/inbox/receive_for_community.rs | 24 +++++++++++++++---- crates/apub/src/objects/mod.rs | 10 ++++---- crates/apub/src/objects/post.rs | 3 +-- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 959aec106..a4b9fcc82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "activitystreams" -version = "0.7.0-alpha.10" +version = "0.7.0-alpha.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7ceed015dfca322d3bcec3653909c77557e7e57df72e98cb8806e2c93cc919" +checksum = "3a5da1d857ec9ca65ef8d0469cdd64e7b93b59d6cad26f1444bf84b62f3eadd4" dependencies = [ "chrono", "mime", diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index b0538af6a..acb19db09 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -17,7 +17,7 @@ lemmy_db_views_actor = { path = "../db_views_actor" } lemmy_api_structs = { path = "../api_structs" } lemmy_websocket = { path = "../websocket" } diesel = "1.4.5" -activitystreams = "0.7.0-alpha.10" +activitystreams = "0.7.0-alpha.11" activitystreams-ext = "0.1.0-alpha.2" bcrypt = "0.9.0" chrono = { version = "0.4.19", features = ["serde"] } diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index aab178401..438a8b3d8 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -40,7 +40,18 @@ use crate::{ PostOrComment, }; use activitystreams::{ - activity::{ActorAndObjectRef, Add, Create, Delete, Dislike, Like, Remove, Undo, Update}, + activity::{ + ActorAndObjectRef, + Add, + Create, + Delete, + Dislike, + Like, + OptTargetRef, + Remove, + Undo, + Update, + }, base::AnyBase, prelude::*, }; @@ -442,12 +453,15 @@ async fn verify_actor_is_community_mod( context: &LemmyContext, ) -> Result where - T: ActorAndObjectRef + BaseExt, + T: ActorAndObjectRef + BaseExt + OptTargetRef, { // 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 target = activity + .target() + .map(|t| t.as_single_xsd_string()) + .flatten() + .context(location_info!())?; + // TODO: very hacky, we should probably store the moderators url in db 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) diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 6b59e5776..47d80cc23 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -132,19 +132,17 @@ where { let content = object .content() - .map(|s| s.as_single_xsd_string()) - .flatten() - .map(|s| s.to_string()); + .map(|s| s.as_single_xsd_string().map(|s2| s2.to_string())) + .flatten(); if content.is_some() { let source = object.source().context(location_info!())?; let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?; check_is_markdown(source.media_type())?; let source_content = source .content() - .map(|s| s.as_single_xsd_string()) + .map(|s| s.as_single_xsd_string().map(|s2| s2.to_string())) .flatten() - .context(location_info!())? - .to_string(); + .context(location_info!())?; return Ok(Some(source_content)); } Ok(None) diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index b066e6f8c..e9052bdd5 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -178,10 +178,9 @@ impl FromApubToForm for PostForm { let name = page .inner .name() - .map(|s| s.map(|s2| s2.to_owned())) // The following is for compatibility with lemmy v0.9.9 and older // TODO: remove it after some time (along with the map above) - .or_else(|| page.inner.summary().map(|s| s.to_owned())) + .or_else(|| page.inner.summary()) .context(location_info!())? .as_single_xsd_string() .context(location_info!())? From 3ffae1f5b85612953a0b31c863762ba2e8faa580 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 9 Mar 2021 18:13:08 +0100 Subject: [PATCH 05/19] 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 cee5d3710..faedbed68 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 8e25b5128..cb61fcf21 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 3e77248f8..ffbb456c1 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 2da0b48c2..80660044d 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 438a8b3d8..c857b0b40 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 8c197a85f..ae40b8914 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 6e0476740..d99092fc4 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), } } From a2698dea9234a5dbbbd018a48524f3fb25743f6e Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 11 Mar 2021 17:21:45 +0100 Subject: [PATCH 06/19] Allow for remote mods to remove posts/comments --- .../apub/src/activities/receive/community.rs | 15 +- crates/apub/src/activities/send/community.rs | 6 +- crates/apub/src/inbox/community_inbox.rs | 8 +- crates/apub/src/inbox/mod.rs | 2 +- .../apub/src/inbox/receive_for_community.rs | 144 +++++++++++------- crates/apub/src/inbox/user_inbox.rs | 12 +- 6 files changed, 117 insertions(+), 70 deletions(-) diff --git a/crates/apub/src/activities/receive/community.rs b/crates/apub/src/activities/receive/community.rs index 854e75f26..cf85ad106 100644 --- a/crates/apub/src/activities/receive/community.rs +++ b/crates/apub/src/activities/receive/community.rs @@ -1,4 +1,7 @@ -use crate::{activities::receive::verify_activity_domains_valid, inbox::is_addressed_to_public}; +use crate::{ + activities::receive::verify_activity_domains_valid, + inbox::verify_is_addressed_to_public, +}; use activitystreams::{ activity::{ActorAndObjectRefExt, Delete, Remove, Undo}, base::{AnyBase, ExtendsExt}, @@ -47,7 +50,7 @@ pub(crate) async fn receive_remove_community( ) -> Result<(), LemmyError> { let remove = Remove::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&remove, expected_domain, true)?; - is_addressed_to_public(&remove)?; + verify_is_addressed_to_public(&remove)?; let community_uri = remove .object() @@ -89,11 +92,11 @@ pub(crate) async fn receive_undo_delete_community( community: Community, expected_domain: &Url, ) -> Result<(), LemmyError> { - is_addressed_to_public(&undo)?; + 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)?; - is_addressed_to_public(&delete)?; + verify_is_addressed_to_public(&delete)?; let deleted_community = blocking(context.pool(), move |conn| { Community::update_deleted(conn, community.id, false) @@ -124,12 +127,12 @@ pub(crate) async fn receive_undo_remove_community( undo: Undo, expected_domain: &Url, ) -> Result<(), LemmyError> { - is_addressed_to_public(&undo)?; + 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)?; - is_addressed_to_public(&remove)?; + verify_is_addressed_to_public(&remove)?; let community_uri = remove .object() diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs index ffbb456c1..ff445ac60 100644 --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@ -219,7 +219,8 @@ pub async fn send_add_mod( 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_to(public()) + .set_many_ccs(vec![community.actor_id()]) .set_target(generate_moderators_url(&community.actor_id)?.into_inner()); if community.local { @@ -245,7 +246,8 @@ pub async fn send_remove_mod( 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_to(public()) + .set_many_ccs(vec![community.actor_id()]) .set_target(generate_moderators_url(&community.actor_id)?.into_inner()); if community.local { diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs index 43072c512..d357ee07c 100644 --- a/crates/apub/src/inbox/community_inbox.rs +++ b/crates/apub/src/inbox/community_inbox.rs @@ -6,7 +6,6 @@ use crate::{ get_activity_to_and_cc, inbox_verify_http_signature, is_activity_already_known, - is_addressed_to_public, receive_for_community::{ receive_add_for_community, receive_create_for_community, @@ -17,6 +16,7 @@ use crate::{ receive_undo_for_community, receive_update_for_community, }, + verify_is_addressed_to_public, }, insert_activity, ActorType, @@ -164,18 +164,18 @@ pub(crate) async fn community_receive_message( true } CommunityValidTypes::Add => { - receive_add_for_community(context, any_base.clone(), &actor_url, request_counter).await?; + receive_add_for_community(context, any_base.clone(), None, request_counter).await?; true } CommunityValidTypes::Remove => { - receive_remove_for_community(context, any_base.clone(), &actor_url, request_counter).await?; + receive_remove_for_community(context, any_base.clone(), None, request_counter).await?; true } }; if do_announce { // Check again that the activity is public, just to be sure - is_addressed_to_public(&activity)?; + verify_is_addressed_to_public(&activity)?; to_community .send_announce(activity.into_any_base()?, context) .await?; diff --git a/crates/apub/src/inbox/mod.rs b/crates/apub/src/inbox/mod.rs index 21585aa6a..314f57ca1 100644 --- a/crates/apub/src/inbox/mod.rs +++ b/crates/apub/src/inbox/mod.rs @@ -84,7 +84,7 @@ where to_and_cc } -pub(crate) fn is_addressed_to_public(activity: &T) -> Result<(), LemmyError> +pub(crate) fn verify_is_addressed_to_public(activity: &T) -> Result<(), LemmyError> where T: AsBase + AsObject + ActorAndObjectRefExt, { diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index c857b0b40..4a548bc08 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -36,7 +36,8 @@ use crate::{ user::get_or_fetch_and_upsert_user, }, find_post_or_comment_by_id, - inbox::is_addressed_to_public, + generate_moderators_url, + inbox::verify_is_addressed_to_public, ActorType, PostOrComment, }; @@ -44,6 +45,7 @@ use activitystreams::{ activity::{ ActorAndObjectRef, Add, + Announce, Create, Delete, Dislike, @@ -54,6 +56,7 @@ use activitystreams::{ Update, }, base::AnyBase, + object::AsObject, prelude::*, }; use anyhow::{anyhow, Context}; @@ -92,7 +95,7 @@ pub(in crate::inbox) async fn receive_create_for_community( ) -> Result<(), LemmyError> { let create = Create::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&create, &expected_domain, true)?; - is_addressed_to_public(&create)?; + verify_is_addressed_to_public(&create)?; let kind = create .object() @@ -114,7 +117,7 @@ pub(in crate::inbox) async fn receive_update_for_community( ) -> Result<(), LemmyError> { let update = Update::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&update, &expected_domain, true)?; - is_addressed_to_public(&update)?; + verify_is_addressed_to_public(&update)?; let kind = update .object() @@ -136,7 +139,7 @@ pub(in crate::inbox) async fn receive_like_for_community( ) -> Result<(), LemmyError> { let like = Like::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&like, &expected_domain, false)?; - is_addressed_to_public(&like)?; + verify_is_addressed_to_public(&like)?; let object_id = like .object() @@ -167,7 +170,7 @@ pub(in crate::inbox) async fn receive_dislike_for_community( let dislike = Dislike::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&dislike, &expected_domain, false)?; - is_addressed_to_public(&dislike)?; + verify_is_addressed_to_public(&dislike)?; let object_id = dislike .object() @@ -191,7 +194,7 @@ pub(in crate::inbox) async fn receive_delete_for_community( ) -> Result<(), LemmyError> { let delete = Delete::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&delete, &expected_domain, true)?; - is_addressed_to_public(&delete)?; + verify_is_addressed_to_public(&delete)?; let object = delete .object() @@ -210,18 +213,18 @@ pub(in crate::inbox) async fn receive_delete_for_community( /// A post or comment being removed by a mod/admin pub(in crate::inbox) async fn receive_remove_for_community( context: &LemmyContext, - activity: AnyBase, - expected_domain: &Url, + remove_any_base: AnyBase, + announce: Option, request_counter: &mut i32, ) -> Result<(), LemmyError> { - 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)?; + let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?; + let community = extract_community_from_cc(&remove, context).await?; + + verify_mod_activity(&remove, announce, &community, context).await?; + verify_is_addressed_to_public(&remove)?; + verify_actor_is_community_mod(&remove, &community, context).await?; - // 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() @@ -235,32 +238,18 @@ pub(in crate::inbox) async fn receive_remove_for_community( CommunityModerator::leave(conn, &form) }) .await??; - community.send_announce(activity, context).await?; + community.send_announce(remove_any_base, context).await?; // TODO: send websocket notification about removed mod 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, @@ -287,7 +276,7 @@ pub(in crate::inbox) async fn receive_undo_for_community( ) -> Result<(), LemmyError> { let undo = Undo::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?; - is_addressed_to_public(&undo)?; + verify_is_addressed_to_public(&undo)?; use UndoableActivities::*; match undo @@ -316,7 +305,7 @@ pub(in crate::inbox) async fn receive_undo_delete_for_community( let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; verify_activity_domains_valid(&delete, &expected_domain, true)?; - is_addressed_to_public(&delete)?; + verify_is_addressed_to_public(&delete)?; let object = delete .object() @@ -340,7 +329,7 @@ pub(in crate::inbox) async fn receive_undo_remove_for_community( let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; verify_activity_domains_valid(&remove, &expected_domain, false)?; - is_addressed_to_public(&remove)?; + verify_is_addressed_to_public(&remove)?; let object = remove .object() @@ -365,7 +354,7 @@ pub(in crate::inbox) async fn receive_undo_like_for_community( let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; verify_activity_domains_valid(&like, &expected_domain, false)?; - is_addressed_to_public(&like)?; + verify_is_addressed_to_public(&like)?; let object_id = like .object() @@ -384,14 +373,17 @@ 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, + add_any_base: AnyBase, + announce: Option, request_counter: &mut i32, ) -> Result<(), LemmyError> { - 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?; + let add = Add::from_any_base(add_any_base.to_owned())?.context(location_info!())?; + let community = extract_community_from_cc(&add, context).await?; + + verify_mod_activity(&add, announce, &community, context).await?; + verify_is_addressed_to_public(&add)?; + verify_actor_is_community_mod(&add, &community, context).await?; + verify_add_remove_moderator_target(&add, &community)?; let new_mod = add .object() @@ -417,7 +409,7 @@ pub(in crate::inbox) async fn receive_add_for_community( .await??; } if community.local { - community.send_announce(activity, context).await?; + community.send_announce(add_any_base, context).await?; } // TODO: send websocket notification about added mod Ok(()) @@ -433,7 +425,7 @@ pub(in crate::inbox) async fn receive_undo_dislike_for_community( let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; verify_activity_domains_valid(&dislike, &expected_domain, false)?; - is_addressed_to_public(&dislike)?; + verify_is_addressed_to_public(&dislike)?; let object_id = dislike .object() @@ -465,26 +457,39 @@ async fn fetch_post_or_comment_by_id( Err(NotFound.into()) } -async fn verify_actor_is_community_mod( +async fn extract_community_from_cc( activity: &T, context: &LemmyContext, ) -> Result where - T: ActorAndObjectRef + BaseExt + OptTargetRef, + T: AsObject, { - // should be the moderators collection of a local community - let target = activity - .target() - .map(|t| t.as_single_xsd_any_uri()) + let cc = activity + .cc() + .map(|c| c.as_many()) .flatten() .context(location_info!())?; - // TODO: very hacky, we should probably store the moderators url in db - let community_id: DbUrl = Url::parse(&target.to_string().replace("/moderators", ""))?.into(); + let community_id = cc + .first() + .map(|c| c.as_xsd_any_uri()) + .flatten() + .context(location_info!())?; + let community_id: DbUrl = community_id.to_owned().into(); let community = blocking(&context.pool(), move |conn| { Community::read_from_apub_id(&conn, &community_id) }) .await??; + Ok(community) +} +async fn verify_actor_is_community_mod( + activity: &T, + community: &Community, + context: &LemmyContext, +) -> Result<(), LemmyError> +where + T: ActorAndObjectRef + BaseExt, +{ let actor = activity .actor()? .as_single_xsd_any_uri() @@ -507,6 +512,43 @@ where return Err(anyhow!("Not a mod").into()); } - // TODO: the function name doesnt make sense if we return the community - Ok(community) + Ok(()) +} + +async fn verify_mod_activity( + mod_action: &T, + announce: Option, + community: &Community, + context: &LemmyContext, +) -> Result<(), LemmyError> +where + T: ActorAndObjectRef + OptTargetRef + BaseExt, +{ + // Remove was sent by community to user, we just check that it came from the right domain + if let Some(announce) = announce { + verify_activity_domains_valid(&announce, &community.actor_id.to_owned().into(), false)?; + } + // Remove was sent by a remote mod to community, check that they are actually mod + else { + verify_actor_is_community_mod(mod_action, community, context).await?; + } + + Ok(()) +} +fn verify_add_remove_moderator_target( + activity: &T, + community: &Community, +) -> Result<(), LemmyError> +where + T: ActorAndObjectRef + BaseExt + OptTargetRef, +{ + let target = activity + .target() + .map(|t| t.as_single_xsd_any_uri()) + .flatten() + .context(location_info!())?; + if target != &generate_moderators_url(&community.actor_id)?.into_inner() { + return Err(anyhow!("Unkown target url").into()); + } + Ok(()) } diff --git a/crates/apub/src/inbox/user_inbox.rs b/crates/apub/src/inbox/user_inbox.rs index d99092fc4..28e1365f7 100644 --- a/crates/apub/src/inbox/user_inbox.rs +++ b/crates/apub/src/inbox/user_inbox.rs @@ -26,7 +26,6 @@ use crate::{ is_activity_already_known, is_addressed_to_community_followers, is_addressed_to_local_user, - is_addressed_to_public, receive_for_community::{ receive_add_for_community, receive_create_for_community, @@ -37,6 +36,7 @@ use crate::{ receive_undo_for_community, receive_update_for_community, }, + verify_is_addressed_to_public, }, insert_activity, ActorType, @@ -265,7 +265,7 @@ pub async fn receive_announce( ) -> Result<(), LemmyError> { let announce = Announce::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&announce, &actor.actor_id(), false)?; - is_addressed_to_public(&announce)?; + verify_is_addressed_to_public(&announce)?; let kind = announce .object() @@ -299,13 +299,13 @@ pub async fn receive_announce( } Some(Delete) => receive_delete_for_community(context, inner_activity, &inner_id).await, Some(Remove) => { - receive_remove_for_community(context, inner_activity, &inner_id, request_counter).await + receive_remove_for_community(context, inner_activity, Some(announce), request_counter).await } 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_add_for_community(context, inner_activity, Some(announce), request_counter).await } _ => receive_unhandled_activity(inner_activity), } @@ -319,7 +319,7 @@ async fn receive_create( ) -> Result<(), LemmyError> { let create = Create::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&create, &expected_domain, true)?; - if is_addressed_to_public(&create).is_ok() { + if verify_is_addressed_to_public(&create).is_ok() { receive_create_comment(create, context, request_counter).await } else { receive_create_private_message(&context, create, expected_domain, request_counter).await @@ -334,7 +334,7 @@ async fn receive_update( ) -> Result<(), LemmyError> { let update = Update::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&update, &expected_domain, true)?; - if is_addressed_to_public(&update).is_ok() { + if verify_is_addressed_to_public(&update).is_ok() { receive_update_comment(update, context, request_counter).await } else { receive_update_private_message(&context, update, expected_domain, request_counter).await From 803aad3b3ea31d5985213c5124ee3cbfbe28f331 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 11 Mar 2021 17:50:47 +0100 Subject: [PATCH 07/19] Add check so only author or mods can edit posts/comments --- crates/apub/src/inbox/community_inbox.rs | 3 ++- .../apub/src/inbox/receive_for_community.rs | 26 ++++++++++++++++++- crates/apub/src/inbox/user_inbox.rs | 9 ++++++- docker/federation/docker-compose.yml | 2 +- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs index d357ee07c..de44ef6f0 100644 --- a/crates/apub/src/inbox/community_inbox.rs +++ b/crates/apub/src/inbox/community_inbox.rs @@ -148,7 +148,8 @@ pub(crate) async fn community_receive_message( true } CommunityValidTypes::Update => { - receive_update_for_community(context, any_base.clone(), &actor_url, request_counter).await?; + receive_update_for_community(context, any_base.clone(), None, &actor_url, request_counter) + .await?; true } CommunityValidTypes::Like => { diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index 4a548bc08..58b400459 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -112,6 +112,7 @@ pub(in crate::inbox) async fn receive_create_for_community( pub(in crate::inbox) async fn receive_update_for_community( context: &LemmyContext, activity: AnyBase, + announce: Option, expected_domain: &Url, request_counter: &mut i32, ) -> Result<(), LemmyError> { @@ -119,6 +120,28 @@ pub(in crate::inbox) async fn receive_update_for_community( verify_activity_domains_valid(&update, &expected_domain, true)?; verify_is_addressed_to_public(&update)?; + // Check that actor is the creator (or a mod) + let actor = update + .actor()? + .to_owned() + .single_xsd_any_uri() + .context(location_info!())?; + let actor = get_or_fetch_and_upsert_user(&actor, context, request_counter).await?; + let object_id = update + .object() + .as_one() + .map(|o| o.id()) + .flatten() + .context(location_info!())?; + let original_author = match find_post_or_comment_by_id(context, object_id.to_owned()).await? { + PostOrComment::Post(p) => p.creator_id, + PostOrComment::Comment(c) => c.creator_id, + }; + if actor.id != original_author { + let community = extract_community_from_cc(&update, context).await?; + verify_mod_activity(&update, announce, &community, context).await?; + } + let kind = update .object() .as_single_kind_str() @@ -522,7 +545,7 @@ async fn verify_mod_activity( context: &LemmyContext, ) -> Result<(), LemmyError> where - T: ActorAndObjectRef + OptTargetRef + BaseExt, + T: ActorAndObjectRef + BaseExt, { // Remove was sent by community to user, we just check that it came from the right domain if let Some(announce) = announce { @@ -535,6 +558,7 @@ where Ok(()) } + fn verify_add_remove_moderator_target( activity: &T, community: &Community, diff --git a/crates/apub/src/inbox/user_inbox.rs b/crates/apub/src/inbox/user_inbox.rs index 28e1365f7..571e33294 100644 --- a/crates/apub/src/inbox/user_inbox.rs +++ b/crates/apub/src/inbox/user_inbox.rs @@ -289,7 +289,14 @@ pub async fn receive_announce( receive_create_for_community(context, inner_activity, &inner_id, request_counter).await } Some(Update) => { - receive_update_for_community(context, inner_activity, &inner_id, request_counter).await + receive_update_for_community( + context, + inner_activity, + Some(announce), + &inner_id, + request_counter, + ) + .await } Some(Like) => { receive_like_for_community(context, inner_activity, &inner_id, request_counter).await diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index a2ee7f267..142c4fa44 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -87,7 +87,7 @@ services: - ./volumes/postgres_beta:/var/lib/postgresql/data lemmy-gamma-ui: - image: dessalines/lemmy-ui:0.9.9 + image: lemmy-ui:test environment: - LEMMY_INTERNAL_HOST=lemmy-gamma:8561 - LEMMY_EXTERNAL_HOST=localhost:8561 From 50559de6d2a1656343072485c5bab3b30e23b660 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 11 Mar 2021 18:11:59 +0100 Subject: [PATCH 08/19] Add check so only mods can change stickied/locked state of posts --- crates/apub/src/activities/receive/post.rs | 42 +++++++++++++++++-- crates/apub/src/inbox/mod.rs | 2 +- .../apub/src/inbox/receive_for_community.rs | 6 +-- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/crates/apub/src/activities/receive/post.rs b/crates/apub/src/activities/receive/post.rs index 0fb6c8807..5bd84ef84 100644 --- a/crates/apub/src/activities/receive/post.rs +++ b/crates/apub/src/activities/receive/post.rs @@ -1,12 +1,24 @@ -use crate::{activities::receive::get_actor_as_user, objects::FromApub, ActorType, PageExt}; +use crate::{ + activities::receive::get_actor_as_user, + inbox::receive_for_community::verify_mod_activity, + objects::FromApub, + ActorType, + PageExt, +}; use activitystreams::{ - activity::{Create, Dislike, Like, Remove, Update}, + activity::{Announce, Create, Dislike, Like, Remove, Update}, prelude::*, }; use anyhow::Context; use lemmy_api_structs::{blocking, post::PostResponse}; -use lemmy_db_queries::{source::post::Post_, Likeable}; -use lemmy_db_schema::source::post::{Post, PostLike, PostLikeForm}; +use lemmy_db_queries::{source::post::Post_, ApubObject, Crud, Likeable}; +use lemmy_db_schema::{ + source::{ + community::Community, + post::{Post, PostLike, PostLikeForm}, + }, + DbUrl, +}; use lemmy_db_views::post_view::PostView; use lemmy_utils::{location_info, LemmyError}; use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation}; @@ -42,6 +54,7 @@ pub(crate) async fn receive_create_post( pub(crate) async fn receive_update_post( update: Update, + announce: Option, context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { @@ -49,6 +62,27 @@ pub(crate) async fn receive_update_post( let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; + let post_id: DbUrl = page + .id_unchecked() + .context(location_info!())? + .to_owned() + .into(); + let old_post = blocking(context.pool(), move |conn| { + Post::read_from_apub_id(conn, &post_id) + }) + .await??; + + // If sticked or locked state was changed, make sure the actor is a mod + let stickied = page.ext_one.stickied.context(location_info!())?; + let locked = !page.ext_one.comments_enabled.context(location_info!())?; + if stickied != old_post.stickied || locked != old_post.locked { + let community = blocking(context.pool(), move |conn| { + Community::read(conn, old_post.community_id) + }) + .await??; + verify_mod_activity(&update, announce, &community, context).await?; + } + let post = Post::from_apub(&page, context, user.actor_id(), request_counter).await?; let post_id = post.id; diff --git a/crates/apub/src/inbox/mod.rs b/crates/apub/src/inbox/mod.rs index 314f57ca1..ea884183c 100644 --- a/crates/apub/src/inbox/mod.rs +++ b/crates/apub/src/inbox/mod.rs @@ -26,7 +26,7 @@ use std::fmt::Debug; use url::Url; pub mod community_inbox; -mod receive_for_community; +pub(crate) mod receive_for_community; pub mod shared_inbox; pub mod user_inbox; diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index 58b400459..2a1427f6a 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -139,7 +139,7 @@ pub(in crate::inbox) async fn receive_update_for_community( }; if actor.id != original_author { let community = extract_community_from_cc(&update, context).await?; - verify_mod_activity(&update, announce, &community, context).await?; + verify_mod_activity(&update, announce.to_owned(), &community, context).await?; } let kind = update @@ -147,7 +147,7 @@ pub(in crate::inbox) async fn receive_update_for_community( .as_single_kind_str() .and_then(|s| s.parse().ok()); match kind { - Some(PageOrNote::Page) => receive_update_post(update, context, request_counter).await, + Some(PageOrNote::Page) => receive_update_post(update, announce, context, request_counter).await, Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await, _ => receive_unhandled_activity(update), } @@ -538,7 +538,7 @@ where Ok(()) } -async fn verify_mod_activity( +pub(crate) async fn verify_mod_activity( mod_action: &T, announce: Option, community: &Community, From b1ca85b910e18a131f4c32d50806d7f0c304d3df Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 11 Mar 2021 18:18:01 +0100 Subject: [PATCH 09/19] Fix clippy warning --- crates/api/src/community.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs index faedbed68..bb341294b 100644 --- a/crates/api/src/community.rs +++ b/crates/api/src/community.rs @@ -744,7 +744,6 @@ impl Perform for AddModToCommunity { Community::read(conn, community_id) }) .await??; - dbg!(data.added); if data.added { send_add_mod(user, updated_mod, community, context).await?; } else { From de39d575923205432c1aca235550d659deb26bfd Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 12 Mar 2021 14:47:55 +0100 Subject: [PATCH 10/19] WIP: check that modifications are made by same user, add docs --- crates/apub/src/inbox/community_inbox.rs | 4 +- .../apub/src/inbox/receive_for_community.rs | 91 ++++++++++++------- crates/apub/src/inbox/user_inbox.rs | 13 ++- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs index de44ef6f0..1495c4bbc 100644 --- a/crates/apub/src/inbox/community_inbox.rs +++ b/crates/apub/src/inbox/community_inbox.rs @@ -161,7 +161,7 @@ pub(crate) async fn community_receive_message( true } CommunityValidTypes::Delete => { - receive_delete_for_community(context, any_base.clone(), &actor_url).await?; + receive_delete_for_community(context, any_base.clone(), None, &actor_url).await?; true } CommunityValidTypes::Add => { @@ -228,7 +228,7 @@ async fn handle_undo( handle_undo_follow(any_base, actor_url, to_community, &context).await?; Ok(false) } else { - receive_undo_for_community(context, any_base, &actor_url, request_counter).await?; + receive_undo_for_community(context, any_base, None, &actor_url, request_counter).await?; Ok(true) } } diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index 2a1427f6a..fed0c4622 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -119,28 +119,7 @@ pub(in crate::inbox) async fn receive_update_for_community( let update = Update::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&update, &expected_domain, true)?; verify_is_addressed_to_public(&update)?; - - // Check that actor is the creator (or a mod) - let actor = update - .actor()? - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - let actor = get_or_fetch_and_upsert_user(&actor, context, request_counter).await?; - let object_id = update - .object() - .as_one() - .map(|o| o.id()) - .flatten() - .context(location_info!())?; - let original_author = match find_post_or_comment_by_id(context, object_id.to_owned()).await? { - PostOrComment::Post(p) => p.creator_id, - PostOrComment::Comment(c) => c.creator_id, - }; - if actor.id != original_author { - let community = extract_community_from_cc(&update, context).await?; - verify_mod_activity(&update, announce.to_owned(), &community, context).await?; - } + verify_modification_actor_instance(&update, &announce, context).await?; let kind = update .object() @@ -213,11 +192,13 @@ pub(in crate::inbox) async fn receive_dislike_for_community( pub(in crate::inbox) async fn receive_delete_for_community( context: &LemmyContext, activity: AnyBase, + announce: Option, expected_domain: &Url, ) -> Result<(), LemmyError> { let delete = Delete::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&delete, &expected_domain, true)?; verify_is_addressed_to_public(&delete)?; + verify_modification_actor_instance(&delete, &announce, context).await?; let object = delete .object() @@ -245,7 +226,6 @@ pub(in crate::inbox) async fn receive_remove_for_community( verify_mod_activity(&remove, announce, &community, context).await?; verify_is_addressed_to_public(&remove)?; - verify_actor_is_community_mod(&remove, &community, context).await?; if remove.target().is_some() { let remove_mod = remove @@ -294,12 +274,14 @@ enum UndoableActivities { pub(in crate::inbox) async fn receive_undo_for_community( context: &LemmyContext, activity: AnyBase, + announce: Option, expected_domain: &Url, request_counter: &mut i32, ) -> Result<(), LemmyError> { let undo = Undo::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?; verify_is_addressed_to_public(&undo)?; + verify_modification_actor_instance(&undo, &announce, context).await?; use UndoableActivities::*; match undo @@ -405,7 +387,6 @@ pub(in crate::inbox) async fn receive_add_for_community( verify_mod_activity(&add, announce, &community, context).await?; verify_is_addressed_to_public(&add)?; - verify_actor_is_community_mod(&add, &community, context).await?; verify_add_remove_moderator_target(&add, &community)?; let new_mod = add @@ -480,6 +461,7 @@ async fn fetch_post_or_comment_by_id( Err(NotFound.into()) } +/// Searches the activity's cc field for a Community ID, and returns the community. async fn extract_community_from_cc( activity: &T, context: &LemmyContext, @@ -505,6 +487,12 @@ where Ok(community) } +/// Checks that a moderation activity was sent by a user who is listed as mod for the community. +/// This is only used in the case of remote mods, as local mod actions don't go through the +/// community inbox. +/// +/// This method should only be used for activities received by the community, not for activities +/// used by community followers. async fn verify_actor_is_community_mod( activity: &T, community: &Community, @@ -538,6 +526,16 @@ where Ok(()) } +/// This method behaves differently, depending if it is called via community inbox (activity +/// received by community from a remote user), or via user inbox (activity received by user from +/// community). We distinguish the cases by checking if the activity is wrapper in an announce +/// (only true when sent from user to community). +/// +/// In the first case, we check that the actor is listed as community mod. In the second case, we +/// only check that the announce comes from the same domain as the activity. We trust the +/// community's instance to have validated the inner activity correctly. We can't do this validation +/// here, because we don't know who the instance admins are. Plus this allows for compatibility with +/// software that uses different rules for mod actions. pub(crate) async fn verify_mod_activity( mod_action: &T, announce: Option, @@ -547,18 +545,16 @@ pub(crate) async fn verify_mod_activity( where T: ActorAndObjectRef + BaseExt, { - // Remove was sent by community to user, we just check that it came from the right domain - if let Some(announce) = announce { - verify_activity_domains_valid(&announce, &community.actor_id.to_owned().into(), false)?; - } - // Remove was sent by a remote mod to community, check that they are actually mod - else { - verify_actor_is_community_mod(mod_action, community, context).await?; + match announce { + None => verify_actor_is_community_mod(mod_action, community, context).await?, + Some(a) => verify_activity_domains_valid(&a, &community.actor_id.to_owned().into(), false)?, } Ok(()) } +/// For Add/Remove community moderator activities, check that the target field actually contains +/// /c/community/moderators. Any different values are unsupported. fn verify_add_remove_moderator_target( activity: &T, community: &Community, @@ -576,3 +572,36 @@ where } Ok(()) } + +/// For activities like Update, Delete or Undo, check that the actor is from the same instance +/// as the original object itself (or is a remote mod). +async fn verify_modification_actor_instance( + activity: &T, + announce: &Option, + context: &LemmyContext, +) -> Result<(), LemmyError> +where + T: ActorAndObjectRef + BaseExt + AsObject, +{ + let actor_id = activity + .actor()? + .to_owned() + .single_xsd_any_uri() + .context(location_info!())?; + let object_id = activity + .object() + .as_one() + .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(), + }; + if actor_id.domain() != original_id.domain() { + let community = extract_community_from_cc(activity, context).await?; + verify_mod_activity(activity, announce.to_owned(), &community, context).await?; + } + + Ok(()) +} diff --git a/crates/apub/src/inbox/user_inbox.rs b/crates/apub/src/inbox/user_inbox.rs index 571e33294..691a5d416 100644 --- a/crates/apub/src/inbox/user_inbox.rs +++ b/crates/apub/src/inbox/user_inbox.rs @@ -304,12 +304,21 @@ pub async fn receive_announce( Some(Dislike) => { 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(Delete) => { + receive_delete_for_community(context, inner_activity, Some(announce), &inner_id).await + } Some(Remove) => { receive_remove_for_community(context, inner_activity, Some(announce), request_counter).await } Some(Undo) => { - receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await + receive_undo_for_community( + context, + inner_activity, + Some(announce), + &inner_id, + request_counter, + ) + .await } Some(Add) => { receive_add_for_community(context, inner_activity, Some(announce), request_counter).await From de14636e1073f1c9ac2b9683d230ded891f912fb Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 12 Mar 2021 16:43:01 +0100 Subject: [PATCH 11/19] Fix code to allow sticky/lock from remote moderators --- crates/apub/src/activities/receive/comment.rs | 4 ++-- crates/apub/src/activities/receive/post.rs | 6 ++++-- .../src/activities/receive/private_message.rs | 4 ++-- crates/apub/src/fetcher/community.rs | 2 +- crates/apub/src/fetcher/objects.rs | 10 ++++++++-- crates/apub/src/fetcher/search.rs | 4 ++-- crates/apub/src/fetcher/user.rs | 16 +++++++++++++-- .../apub/src/inbox/receive_for_community.rs | 7 +++++-- crates/apub/src/objects/comment.rs | 5 +++-- crates/apub/src/objects/community.rs | 5 +++-- crates/apub/src/objects/mod.rs | 8 ++++---- crates/apub/src/objects/post.rs | 20 ++++++++++++++++--- crates/apub/src/objects/private_message.rs | 5 +++-- crates/apub/src/objects/user.rs | 5 +++-- 14 files changed, 71 insertions(+), 30 deletions(-) diff --git a/crates/apub/src/activities/receive/comment.rs b/crates/apub/src/activities/receive/comment.rs index 591b6f33a..2b11ad18b 100644 --- a/crates/apub/src/activities/receive/comment.rs +++ b/crates/apub/src/activities/receive/comment.rs @@ -23,7 +23,7 @@ pub(crate) async fn receive_create_comment( let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; - let comment = Comment::from_apub(¬e, context, user.actor_id(), request_counter).await?; + let comment = Comment::from_apub(¬e, context, Some(user.actor_id()), request_counter).await?; let post_id = comment.post_id; let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; @@ -66,7 +66,7 @@ pub(crate) async fn receive_update_comment( .context(location_info!())?; let user = get_actor_as_user(&update, context, request_counter).await?; - let comment = Comment::from_apub(¬e, context, user.actor_id(), request_counter).await?; + let comment = Comment::from_apub(¬e, context, Some(user.actor_id()), request_counter).await?; let comment_id = comment.id; let post_id = comment.post_id; diff --git a/crates/apub/src/activities/receive/post.rs b/crates/apub/src/activities/receive/post.rs index 5bd84ef84..c63706790 100644 --- a/crates/apub/src/activities/receive/post.rs +++ b/crates/apub/src/activities/receive/post.rs @@ -32,7 +32,7 @@ pub(crate) async fn receive_create_post( let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; - let post = Post::from_apub(&page, context, user.actor_id(), request_counter).await?; + let post = Post::from_apub(&page, context, Some(user.actor_id()), request_counter).await?; // Refetch the view let post_id = post.id; @@ -72,6 +72,7 @@ pub(crate) async fn receive_update_post( }) .await??; + let mut expected_domain = Some(user.actor_id()); // If sticked or locked state was changed, make sure the actor is a mod let stickied = page.ext_one.stickied.context(location_info!())?; let locked = !page.ext_one.comments_enabled.context(location_info!())?; @@ -81,9 +82,10 @@ pub(crate) async fn receive_update_post( }) .await??; verify_mod_activity(&update, announce, &community, context).await?; + expected_domain = None; } - let post = Post::from_apub(&page, context, user.actor_id(), request_counter).await?; + let post = Post::from_apub(&page, context, expected_domain, request_counter).await?; let post_id = post.id; // Refetch the view diff --git a/crates/apub/src/activities/receive/private_message.rs b/crates/apub/src/activities/receive/private_message.rs index 98c25a588..54379f2ae 100644 --- a/crates/apub/src/activities/receive/private_message.rs +++ b/crates/apub/src/activities/receive/private_message.rs @@ -39,7 +39,7 @@ pub(crate) async fn receive_create_private_message( .context(location_info!())?; let private_message = - PrivateMessage::from_apub(¬e, context, expected_domain, request_counter).await?; + PrivateMessage::from_apub(¬e, context, Some(expected_domain), request_counter).await?; let message = blocking(&context.pool(), move |conn| { PrivateMessageView::read(conn, private_message.id) @@ -78,7 +78,7 @@ pub(crate) async fn receive_update_private_message( let note = NoteExt::from_any_base(object)?.context(location_info!())?; let private_message = - PrivateMessage::from_apub(¬e, context, expected_domain, request_counter).await?; + PrivateMessage::from_apub(¬e, context, Some(expected_domain), request_counter).await?; let private_message_id = private_message.id; let message = blocking(&context.pool(), move |conn| { diff --git a/crates/apub/src/fetcher/community.rs b/crates/apub/src/fetcher/community.rs index 9cc1bbd6a..01b30f93b 100644 --- a/crates/apub/src/fetcher/community.rs +++ b/crates/apub/src/fetcher/community.rs @@ -72,7 +72,7 @@ async fn fetch_remote_community( let group = group?; let community = - Community::from_apub(&group, context, apub_id.to_owned(), recursion_counter).await?; + Community::from_apub(&group, context, Some(apub_id.to_owned()), recursion_counter).await?; // only fetch outbox for new communities, otherwise this can create an infinite loop if old_community.is_none() { diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs index 6e0369bde..f2030a066 100644 --- a/crates/apub/src/fetcher/objects.rs +++ b/crates/apub/src/fetcher/objects.rs @@ -30,7 +30,13 @@ pub(crate) async fn get_or_fetch_and_insert_post( debug!("Fetching and creating remote post: {}", post_ap_id); let page = fetch_remote_object::(context.client(), post_ap_id, recursion_counter).await?; - let post = Post::from_apub(&page, context, post_ap_id.to_owned(), recursion_counter).await?; + let post = Post::from_apub( + &page, + context, + Some(post_ap_id.to_owned()), + recursion_counter, + ) + .await?; Ok(post) } @@ -65,7 +71,7 @@ pub(crate) async fn get_or_fetch_and_insert_comment( let comment = Comment::from_apub( &comment, context, - comment_ap_id.to_owned(), + Some(comment_ap_id.to_owned()), recursion_counter, ) .await?; diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index acaccff28..f5ae9dfd4 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -147,13 +147,13 @@ async fn build_response( ]; } SearchAcceptedObjects::Page(p) => { - let p = Post::from_apub(&p, context, query_url, recursion_counter).await?; + let p = Post::from_apub(&p, context, Some(query_url), recursion_counter).await?; response.posts = vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??]; } SearchAcceptedObjects::Comment(c) => { - let c = Comment::from_apub(&c, context, query_url, recursion_counter).await?; + let c = Comment::from_apub(&c, context, Some(query_url), recursion_counter).await?; response.comments = vec![ blocking(context.pool(), move |conn| { diff --git a/crates/apub/src/fetcher/user.rs b/crates/apub/src/fetcher/user.rs index e3ef41c74..e3ea70ea5 100644 --- a/crates/apub/src/fetcher/user.rs +++ b/crates/apub/src/fetcher/user.rs @@ -46,7 +46,13 @@ pub(crate) async fn get_or_fetch_and_upsert_user( return Ok(u); } - let user = User_::from_apub(&person?, context, apub_id.to_owned(), recursion_counter).await?; + let user = User_::from_apub( + &person?, + context, + Some(apub_id.to_owned()), + recursion_counter, + ) + .await?; let user_id = user.id; blocking(context.pool(), move |conn| { @@ -62,7 +68,13 @@ pub(crate) async fn get_or_fetch_and_upsert_user( let person = fetch_remote_object::(context.client(), apub_id, recursion_counter).await?; - let user = User_::from_apub(&person, context, apub_id.to_owned(), recursion_counter).await?; + let user = User_::from_apub( + &person, + context, + Some(apub_id.to_owned()), + recursion_counter, + ) + .await?; Ok(user) } diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index fed0c4622..0448cccb5 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -117,7 +117,7 @@ pub(in crate::inbox) async fn receive_update_for_community( request_counter: &mut i32, ) -> Result<(), LemmyError> { let update = Update::from_any_base(activity)?.context(location_info!())?; - verify_activity_domains_valid(&update, &expected_domain, true)?; + verify_activity_domains_valid(&update, &expected_domain, false)?; verify_is_addressed_to_public(&update)?; verify_modification_actor_instance(&update, &announce, context).await?; @@ -402,7 +402,7 @@ pub(in crate::inbox) async fn receive_add_for_community( CommunityModerator::get_user_moderated_communities(conn, new_mod_id) }) .await??; - if moderated_communities.contains(&community.id) { + if !moderated_communities.contains(&community.id) { let form = CommunityModeratorForm { community_id: community.id, user_id: new_mod.id, @@ -575,6 +575,9 @@ where /// For activities like Update, Delete or Undo, check that the actor is from the same instance /// as the original object itself (or is a remote mod). +/// +/// Note: This is only needed for mod actions. Normal user actions (edit post, undo vote etc) are +/// already verified with `expected_domain`, so this serves as an additional check. async fn verify_modification_actor_instance( activity: &T, announce: &Option, diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 6c218190a..3fe90738e 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -97,7 +97,7 @@ impl FromApub for Comment { async fn from_apub( note: &NoteExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { let comment: Comment = @@ -126,9 +126,10 @@ impl FromApubToForm for CommentForm { async fn from_apub( note: &NoteExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { + let expected_domain = expected_domain.expect("expected_domain must be set for comment"); let creator_actor_id = ¬e .attributed_to() .context(location_info!())? diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 278bd7b12..efeb7eea5 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -105,7 +105,7 @@ impl FromApub for Community { async fn from_apub( group: &GroupExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { let community: Community = @@ -160,9 +160,10 @@ impl FromApubToForm for CommunityForm { async fn from_apub( group: &GroupExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { + let expected_domain = expected_domain.expect("expected_domain must be set for community"); let moderator_uris = fetch_community_mods(context, group, request_counter).await?; let creator_uri = moderator_uris.first().context(location_info!())?; diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 47d80cc23..1dff81026 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -45,11 +45,11 @@ pub(crate) trait FromApub { /// /// * `apub` The object to read from /// * `context` LemmyContext which holds DB pool, HTTP client etc - /// * `expected_domain` Domain where the object was received from + /// * `expected_domain` Domain where the object was received from. None in case of mod action. async fn from_apub( apub: &Self::ApubType, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result where @@ -61,7 +61,7 @@ pub(in crate::objects) trait FromApubToForm { async fn from_apub( apub: &ApubType, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result where @@ -173,7 +173,7 @@ pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), L pub(in crate::objects) async fn get_object_from_apub( from: &From, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result where diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index e9052bdd5..d15f82b70 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,4 +1,5 @@ use crate::{ + check_is_apub_id_valid, extensions::{context::lemmy_context, page_extension::PageExtension}, fetcher::user::get_or_fetch_and_upsert_user, objects::{ @@ -115,7 +116,7 @@ impl FromApub for Post { async fn from_apub( page: &PageExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { let post: Post = get_object_from_apub(page, context, expected_domain, request_counter).await?; @@ -130,9 +131,17 @@ impl FromApubToForm for PostForm { async fn from_apub( page: &PageExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { + let ap_id = match expected_domain { + Some(e) => check_object_domain(page, e)?, + None => { + let id = page.id_unchecked().context(location_info!())?; + check_is_apub_id_valid(id)?; + id.to_owned().into() + } + }; let ext = &page.ext_one; let creator_actor_id = page .inner @@ -187,6 +196,11 @@ impl FromApubToForm for PostForm { .to_string(); let body = get_source_markdown_value(page)?; + // TODO: expected_domain is wrong in this case, because it simply takes the domain of the actor + // maybe we need to take id_unchecked() if the activity is from community to user? + // why did this work before? -> i dont think it did? + // -> try to make expected_domain optional and set it null if it is a mod action + check_slurs(&name)?; let body_slurs_removed = body.map(|b| remove_slurs(&b)); Ok(PostForm { @@ -214,7 +228,7 @@ impl FromApubToForm for PostForm { embed_description: iframely_description, embed_html: iframely_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ap_id: Some(check_object_domain(page, expected_domain)?), + ap_id: Some(ap_id), local: false, }) } diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 0bb753e28..0dfa102f4 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -75,7 +75,7 @@ impl FromApub for PrivateMessage { async fn from_apub( note: &NoteExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { get_object_from_apub(note, context, expected_domain, request_counter).await @@ -87,9 +87,10 @@ impl FromApubToForm for PrivateMessageForm { async fn from_apub( note: &NoteExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { + let expected_domain = expected_domain.expect("expected_domain must be set for private message"); let creator_actor_id = note .attributed_to() .context(location_info!())? diff --git a/crates/apub/src/objects/user.rs b/crates/apub/src/objects/user.rs index 83f75e49b..e822fcd92 100644 --- a/crates/apub/src/objects/user.rs +++ b/crates/apub/src/objects/user.rs @@ -91,7 +91,7 @@ impl FromApub for User_ { async fn from_apub( person: &PersonExt, context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, request_counter: &mut i32, ) -> Result { let user_id = person.id_unchecked().context(location_info!())?.to_owned(); @@ -116,9 +116,10 @@ impl FromApubToForm for UserForm { async fn from_apub( person: &PersonExt, _context: &LemmyContext, - expected_domain: Url, + expected_domain: Option, _request_counter: &mut i32, ) -> Result { + let expected_domain = expected_domain.expect("expected_domain must be set for user"); let avatar = match person.icon() { Some(any_image) => Some( Image::from_any_base(any_image.as_one().context(location_info!())?.clone())? From be00f63fb26b806ebc6a9d99bcf69ad973729ab5 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 16 Mar 2021 18:06:59 +0100 Subject: [PATCH 12/19] Allow remot moderators to do Remove/Post and Remove/Comment --- .../apub/src/inbox/receive_for_community.rs | 27 ++++++++++++++++--- crates/apub/src/inbox/user_inbox.rs | 13 ++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index 0448cccb5..78c4107a1 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -281,7 +281,6 @@ pub(in crate::inbox) async fn receive_undo_for_community( let undo = Undo::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?; verify_is_addressed_to_public(&undo)?; - verify_modification_actor_instance(&undo, &announce, context).await?; use UndoableActivities::*; match undo @@ -290,7 +289,9 @@ pub(in crate::inbox) async fn receive_undo_for_community( .and_then(|s| s.parse().ok()) { Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await, - Some(Remove) => receive_undo_remove_for_community(context, undo, expected_domain).await, + Some(Remove) => { + receive_undo_remove_for_community(context, undo, announce, expected_domain).await + } Some(Like) => { receive_undo_like_for_community(context, undo, expected_domain, request_counter).await } @@ -329,12 +330,14 @@ pub(in crate::inbox) async fn receive_undo_delete_for_community( pub(in crate::inbox) async fn receive_undo_remove_for_community( context: &LemmyContext, undo: Undo, + announce: Option, expected_domain: &Url, ) -> Result<(), LemmyError> { let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; verify_activity_domains_valid(&remove, &expected_domain, false)?; verify_is_addressed_to_public(&remove)?; + verify_undo_remove_actor_instance(&undo, &remove, &announce, context).await?; let object = remove .object() @@ -573,7 +576,7 @@ where Ok(()) } -/// For activities like Update, Delete or Undo, check that the actor is from the same instance +/// For activities like Update, Delete or Remove, check that the actor is from the same instance /// as the original object itself (or is a remote mod). /// /// Note: This is only needed for mod actions. Normal user actions (edit post, undo vote etc) are @@ -608,3 +611,21 @@ where Ok(()) } + +pub(crate) async fn verify_undo_remove_actor_instance( + undo: &Undo, + inner: &T, + announce: &Option, + context: &LemmyContext, +) -> Result<(), LemmyError> +where + T: ActorAndObjectRef + BaseExt + AsObject, +{ + if announce.is_none() { + let community = extract_community_from_cc(undo, context).await?; + verify_mod_activity(undo, announce.to_owned(), &community, context).await?; + verify_mod_activity(inner, announce.to_owned(), &community, context).await?; + } + + Ok(()) +} diff --git a/crates/apub/src/inbox/user_inbox.rs b/crates/apub/src/inbox/user_inbox.rs index 691a5d416..2ea2e1d19 100644 --- a/crates/apub/src/inbox/user_inbox.rs +++ b/crates/apub/src/inbox/user_inbox.rs @@ -1,6 +1,5 @@ use crate::{ activities::receive::{ - comment::{receive_create_comment, receive_update_comment}, community::{ receive_delete_community, receive_remove_community, @@ -335,11 +334,7 @@ async fn receive_create( ) -> Result<(), LemmyError> { let create = Create::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&create, &expected_domain, true)?; - if verify_is_addressed_to_public(&create).is_ok() { - receive_create_comment(create, context, request_counter).await - } else { - receive_create_private_message(&context, create, expected_domain, request_counter).await - } + receive_create_private_message(&context, create, expected_domain, request_counter).await } async fn receive_update( @@ -350,11 +345,7 @@ async fn receive_update( ) -> Result<(), LemmyError> { let update = Update::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&update, &expected_domain, true)?; - if verify_is_addressed_to_public(&update).is_ok() { - receive_update_comment(update, context, request_counter).await - } else { - receive_update_private_message(&context, update, expected_domain, request_counter).await - } + receive_update_private_message(&context, update, expected_domain, request_counter).await } async fn receive_delete( From 71067a8cb58a6d1d1b064d3c01852fd53057b609 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 16 Mar 2021 18:26:19 +0100 Subject: [PATCH 13/19] For FromApub trait, use `is_mod_action: bool` instead --- crates/apub/src/activities/receive/comment.rs | 4 +-- crates/apub/src/activities/receive/post.rs | 15 +++++++--- .../src/activities/receive/private_message.rs | 4 +-- crates/apub/src/fetcher/community.rs | 10 +++++-- crates/apub/src/fetcher/objects.rs | 6 ++-- crates/apub/src/fetcher/search.rs | 4 +-- crates/apub/src/fetcher/user.rs | 6 ++-- crates/apub/src/objects/comment.rs | 17 +++++++---- crates/apub/src/objects/community.rs | 17 +++++++---- crates/apub/src/objects/mod.rs | 19 ++++++++++--- crates/apub/src/objects/post.rs | 28 ++++++++++++------- crates/apub/src/objects/private_message.rs | 16 ++++++++--- crates/apub/src/objects/user.rs | 17 +++++++---- 13 files changed, 114 insertions(+), 49 deletions(-) diff --git a/crates/apub/src/activities/receive/comment.rs b/crates/apub/src/activities/receive/comment.rs index 2b11ad18b..bc1507931 100644 --- a/crates/apub/src/activities/receive/comment.rs +++ b/crates/apub/src/activities/receive/comment.rs @@ -23,7 +23,7 @@ pub(crate) async fn receive_create_comment( let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; - let comment = Comment::from_apub(¬e, context, Some(user.actor_id()), request_counter).await?; + let comment = Comment::from_apub(¬e, context, user.actor_id(), request_counter, false).await?; let post_id = comment.post_id; let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; @@ -66,7 +66,7 @@ pub(crate) async fn receive_update_comment( .context(location_info!())?; let user = get_actor_as_user(&update, context, request_counter).await?; - let comment = Comment::from_apub(¬e, context, Some(user.actor_id()), request_counter).await?; + let comment = Comment::from_apub(¬e, context, user.actor_id(), request_counter, false).await?; let comment_id = comment.id; let post_id = comment.post_id; diff --git a/crates/apub/src/activities/receive/post.rs b/crates/apub/src/activities/receive/post.rs index c63706790..33b2ce67c 100644 --- a/crates/apub/src/activities/receive/post.rs +++ b/crates/apub/src/activities/receive/post.rs @@ -32,7 +32,7 @@ pub(crate) async fn receive_create_post( let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; - let post = Post::from_apub(&page, context, Some(user.actor_id()), request_counter).await?; + let post = Post::from_apub(&page, context, user.actor_id(), request_counter, false).await?; // Refetch the view let post_id = post.id; @@ -72,20 +72,27 @@ pub(crate) async fn receive_update_post( }) .await??; - let mut expected_domain = Some(user.actor_id()); // If sticked or locked state was changed, make sure the actor is a mod let stickied = page.ext_one.stickied.context(location_info!())?; let locked = !page.ext_one.comments_enabled.context(location_info!())?; + let mut is_mod_action = false; if stickied != old_post.stickied || locked != old_post.locked { let community = blocking(context.pool(), move |conn| { Community::read(conn, old_post.community_id) }) .await??; verify_mod_activity(&update, announce, &community, context).await?; - expected_domain = None; + is_mod_action = true; } - let post = Post::from_apub(&page, context, expected_domain, request_counter).await?; + let post = Post::from_apub( + &page, + context, + user.actor_id(), + request_counter, + is_mod_action, + ) + .await?; let post_id = post.id; // Refetch the view diff --git a/crates/apub/src/activities/receive/private_message.rs b/crates/apub/src/activities/receive/private_message.rs index 54379f2ae..b20494cf9 100644 --- a/crates/apub/src/activities/receive/private_message.rs +++ b/crates/apub/src/activities/receive/private_message.rs @@ -39,7 +39,7 @@ pub(crate) async fn receive_create_private_message( .context(location_info!())?; let private_message = - PrivateMessage::from_apub(¬e, context, Some(expected_domain), request_counter).await?; + PrivateMessage::from_apub(¬e, context, expected_domain, request_counter, false).await?; let message = blocking(&context.pool(), move |conn| { PrivateMessageView::read(conn, private_message.id) @@ -78,7 +78,7 @@ pub(crate) async fn receive_update_private_message( let note = NoteExt::from_any_base(object)?.context(location_info!())?; let private_message = - PrivateMessage::from_apub(¬e, context, Some(expected_domain), request_counter).await?; + PrivateMessage::from_apub(¬e, context, expected_domain, request_counter, false).await?; let private_message_id = private_message.id; let message = blocking(&context.pool(), move |conn| { diff --git a/crates/apub/src/fetcher/community.rs b/crates/apub/src/fetcher/community.rs index 01b30f93b..4ae98be6d 100644 --- a/crates/apub/src/fetcher/community.rs +++ b/crates/apub/src/fetcher/community.rs @@ -71,8 +71,14 @@ async fn fetch_remote_community( } let group = group?; - let community = - Community::from_apub(&group, context, Some(apub_id.to_owned()), recursion_counter).await?; + let community = Community::from_apub( + &group, + context, + apub_id.to_owned(), + recursion_counter, + false, + ) + .await?; // only fetch outbox for new communities, otherwise this can create an infinite loop if old_community.is_none() { diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs index f2030a066..4ba2a56fa 100644 --- a/crates/apub/src/fetcher/objects.rs +++ b/crates/apub/src/fetcher/objects.rs @@ -33,8 +33,9 @@ pub(crate) async fn get_or_fetch_and_insert_post( let post = Post::from_apub( &page, context, - Some(post_ap_id.to_owned()), + post_ap_id.to_owned(), recursion_counter, + false, ) .await?; @@ -71,8 +72,9 @@ pub(crate) async fn get_or_fetch_and_insert_comment( let comment = Comment::from_apub( &comment, context, - Some(comment_ap_id.to_owned()), + comment_ap_id.to_owned(), recursion_counter, + false, ) .await?; diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index f5ae9dfd4..10f4ac5d2 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -147,13 +147,13 @@ async fn build_response( ]; } SearchAcceptedObjects::Page(p) => { - let p = Post::from_apub(&p, context, Some(query_url), recursion_counter).await?; + let p = Post::from_apub(&p, context, query_url, recursion_counter, false).await?; response.posts = vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??]; } SearchAcceptedObjects::Comment(c) => { - let c = Comment::from_apub(&c, context, Some(query_url), recursion_counter).await?; + let c = Comment::from_apub(&c, context, query_url, recursion_counter, false).await?; response.comments = vec![ blocking(context.pool(), move |conn| { diff --git a/crates/apub/src/fetcher/user.rs b/crates/apub/src/fetcher/user.rs index e3ea70ea5..7f998ac75 100644 --- a/crates/apub/src/fetcher/user.rs +++ b/crates/apub/src/fetcher/user.rs @@ -49,8 +49,9 @@ pub(crate) async fn get_or_fetch_and_upsert_user( let user = User_::from_apub( &person?, context, - Some(apub_id.to_owned()), + apub_id.to_owned(), recursion_counter, + false, ) .await?; @@ -71,8 +72,9 @@ pub(crate) async fn get_or_fetch_and_upsert_user( let user = User_::from_apub( &person, context, - Some(apub_id.to_owned()), + apub_id.to_owned(), recursion_counter, + false, ) .await?; diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 3fe90738e..43bd86595 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -97,11 +97,18 @@ impl FromApub for Comment { async fn from_apub( note: &NoteExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result { - let comment: Comment = - get_object_from_apub(note, context, expected_domain, request_counter).await?; + let comment: Comment = get_object_from_apub( + note, + context, + expected_domain, + request_counter, + is_mod_action, + ) + .await?; let post_id = comment.post_id; let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; @@ -126,10 +133,10 @@ impl FromApubToForm for CommentForm { async fn from_apub( note: &NoteExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + _is_mod_action: bool, ) -> Result { - let expected_domain = expected_domain.expect("expected_domain must be set for comment"); let creator_actor_id = ¬e .attributed_to() .context(location_info!())? diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index efeb7eea5..9ae801b74 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -105,11 +105,18 @@ impl FromApub for Community { async fn from_apub( group: &GroupExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result { - let community: Community = - get_object_from_apub(group, context, expected_domain, request_counter).await?; + let community: Community = get_object_from_apub( + group, + context, + expected_domain, + request_counter, + is_mod_action, + ) + .await?; let new_moderators = fetch_community_mods(context, group, request_counter).await?; let community_id = community.id; @@ -160,10 +167,10 @@ impl FromApubToForm for CommunityForm { async fn from_apub( group: &GroupExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + _is_mod_action: bool, ) -> Result { - let expected_domain = expected_domain.expect("expected_domain must be set for community"); let moderator_uris = fetch_community_mods(context, group, request_counter).await?; let creator_uri = moderator_uris.first().context(location_info!())?; diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 1dff81026..9f5007204 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -46,11 +46,13 @@ pub(crate) trait FromApub { /// * `apub` The object to read from /// * `context` LemmyContext which holds DB pool, HTTP client etc /// * `expected_domain` Domain where the object was received from. None in case of mod action. + /// * `is_mod_action` True if the object was sent in a mod activity, ignore `expected_domain` in this case async fn from_apub( apub: &Self::ApubType, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result where Self: Sized; @@ -61,8 +63,9 @@ pub(in crate::objects) trait FromApubToForm { async fn from_apub( apub: &ApubType, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result where Self: Sized; @@ -173,8 +176,9 @@ pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), L pub(in crate::objects) async fn get_object_from_apub( from: &From, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result where From: BaseExt, @@ -194,7 +198,14 @@ where } // otherwise parse and insert, assuring that it comes from the right domain else { - let to_form = ToForm::from_apub(&from, context, expected_domain, request_counter).await?; + let to_form = ToForm::from_apub( + &from, + context, + expected_domain, + request_counter, + is_mod_action, + ) + .await?; let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??; Ok(to) diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index d15f82b70..92d542644 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -116,10 +116,18 @@ impl FromApub for Post { async fn from_apub( page: &PageExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result { - let post: Post = get_object_from_apub(page, context, expected_domain, request_counter).await?; + let post: Post = get_object_from_apub( + page, + context, + expected_domain, + request_counter, + is_mod_action, + ) + .await?; check_object_for_community_or_site_ban(page, post.community_id, context, request_counter) .await?; Ok(post) @@ -131,16 +139,16 @@ impl FromApubToForm for PostForm { async fn from_apub( page: &PageExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result { - let ap_id = match expected_domain { - Some(e) => check_object_domain(page, e)?, - None => { - let id = page.id_unchecked().context(location_info!())?; - check_is_apub_id_valid(id)?; - id.to_owned().into() - } + let ap_id = if is_mod_action { + let id = page.id_unchecked().context(location_info!())?; + check_is_apub_id_valid(id)?; + id.to_owned().into() + } else { + check_object_domain(page, expected_domain)? }; let ext = &page.ext_one; let creator_actor_id = page diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 0dfa102f4..93d13503a 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -75,10 +75,18 @@ impl FromApub for PrivateMessage { async fn from_apub( note: &NoteExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result { - get_object_from_apub(note, context, expected_domain, request_counter).await + get_object_from_apub( + note, + context, + expected_domain, + request_counter, + is_mod_action, + ) + .await } } @@ -87,10 +95,10 @@ impl FromApubToForm for PrivateMessageForm { async fn from_apub( note: &NoteExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + _is_mod_action: bool, ) -> Result { - let expected_domain = expected_domain.expect("expected_domain must be set for private message"); let creator_actor_id = note .attributed_to() .context(location_info!())? diff --git a/crates/apub/src/objects/user.rs b/crates/apub/src/objects/user.rs index e822fcd92..5b33331b9 100644 --- a/crates/apub/src/objects/user.rs +++ b/crates/apub/src/objects/user.rs @@ -91,8 +91,9 @@ impl FromApub for User_ { async fn from_apub( person: &PersonExt, context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, request_counter: &mut i32, + is_mod_action: bool, ) -> Result { let user_id = person.id_unchecked().context(location_info!())?.to_owned(); let domain = user_id.domain().context(location_info!())?; @@ -103,8 +104,14 @@ impl FromApub for User_ { .await??; Ok(user) } else { - let user_form = - UserForm::from_apub(person, context, expected_domain, request_counter).await?; + let user_form = UserForm::from_apub( + person, + context, + expected_domain, + request_counter, + is_mod_action, + ) + .await?; let user = blocking(context.pool(), move |conn| User_::upsert(conn, &user_form)).await??; Ok(user) } @@ -116,10 +123,10 @@ impl FromApubToForm for UserForm { async fn from_apub( person: &PersonExt, _context: &LemmyContext, - expected_domain: Option, + expected_domain: Url, _request_counter: &mut i32, + _is_mod_action: bool, ) -> Result { - let expected_domain = expected_domain.expect("expected_domain must be set for user"); let avatar = match person.icon() { Some(any_image) => Some( Image::from_any_base(any_image.as_one().context(location_info!())?.clone())? From b3a5b4eb820b7d431bb9748ef1797bc9407caacb Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Wed, 17 Mar 2021 18:12:37 +0100 Subject: [PATCH 14/19] 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 bb341294b..0bb64f373 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 bc1507931..07f25eec7 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 cf85ad106..48f6b295d 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 33b2ce67c..b0582e60e 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 ff445ac60..c6c90d502 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 1dc62e0bc..a0778c086 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, LemmyError> { - unimplemented!() - } } diff --git a/crates/apub/src/activity_queue.rs b/crates/apub/src/activity_queue.rs index f607dafe0..9cdc38864 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 1495c4bbc..cd1c775bd 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 78c4107a1..cd8f32bbc 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 2ea2e1d19..3d16403bc 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 307a8c8c0..ec5c6d943 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; fn private_key(&self) -> Option; - 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, 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, 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), From 4f7dca7c2b9916b52393708d4fc8924e167390dd Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 18 Mar 2021 14:24:29 +0100 Subject: [PATCH 15/19] Fix three federation test cases --- api_tests/src/post.spec.ts | 36 +++++++++++--------- crates/apub/src/activities/receive/post.rs | 11 +++--- crates/apub/src/activities/send/community.rs | 2 +- crates/apub/src/inbox/user_inbox.rs | 17 +++++++-- crates/apub/src/objects/comment.rs | 6 ++-- crates/apub/src/objects/community.rs | 6 ++-- crates/apub/src/objects/mod.rs | 6 ++-- crates/apub/src/objects/post.rs | 8 ++--- crates/apub/src/objects/private_message.rs | 6 ++-- crates/apub/src/objects/user.rs | 6 ++-- 10 files changed, 62 insertions(+), 42 deletions(-) diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 01befa60c..0d1788c62 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -23,6 +23,7 @@ import { banUserFromSite, searchPostLocal, banUserFromCommunity, + followCommunity, } from './shared'; import { PostView, CommunityView } from 'lemmy-js-client'; @@ -169,35 +170,38 @@ test('Sticky a post', async () => { }); test('Lock a post', async () => { + await followCommunity(alpha, true, betaCommunity.community.id); let postRes = await createPost(alpha, betaCommunity.community.id); // Lock the post - let lockedPostRes = await lockPost(alpha, true, postRes.post_view.post); + let searchBeta = await searchPost(beta, postRes.post_view.post); + let betaPost1 = searchBeta.posts[0]; + let lockedPostRes = await lockPost(beta, true, betaPost1.post); expect(lockedPostRes.post_view.post.locked).toBe(true); - // Make sure that post is locked on beta - let searchBeta = await searchPostLocal(beta, postRes.post_view.post); - let betaPost1 = searchBeta.posts[0]; - expect(betaPost1.post.locked).toBe(true); + // Make sure that post is locked on alpha + let searchAlpha = await searchPostLocal(alpha, postRes.post_view.post); + let alphaPost1 = searchAlpha.posts[0]; + expect(alphaPost1.post.locked).toBe(true); // Try to make a new comment there, on alpha - let comment: any = await createComment(alpha, postRes.post_view.post.id); + let comment: any = await createComment(alpha, alphaPost1.post.id); expect(comment['error']).toBe('locked'); // Unlock a post - let unlockedPost = await lockPost(alpha, false, postRes.post_view.post); + let unlockedPost = await lockPost(beta, false, betaPost1.post); expect(unlockedPost.post_view.post.locked).toBe(false); - // Make sure that post is unlocked on beta - let searchBeta2 = await searchPost(beta, postRes.post_view.post); - let betaPost2 = searchBeta2.posts[0]; - expect(betaPost2.community.local).toBe(true); - expect(betaPost2.creator.local).toBe(false); - expect(betaPost2.post.locked).toBe(false); + // Make sure that post is unlocked on alpha + let searchAlpha2 = await searchPostLocal(alpha, postRes.post_view.post); + let alphaPost2 = searchAlpha2.posts[0]; + expect(alphaPost2.community.local).toBe(false); + expect(alphaPost2.creator.local).toBe(true); + expect(alphaPost2.post.locked).toBe(false); - // Try to create a new comment, on beta - let commentBeta = await createComment(beta, betaPost2.post.id); - expect(commentBeta).toBeDefined(); + // Try to create a new comment, on alpha + let commentAlpha = await createComment(alpha, alphaPost1.post.id); + expect(commentAlpha).toBeDefined(); }); test('Delete a post', async () => { diff --git a/crates/apub/src/activities/receive/post.rs b/crates/apub/src/activities/receive/post.rs index b0582e60e..631f9933b 100644 --- a/crates/apub/src/activities/receive/post.rs +++ b/crates/apub/src/activities/receive/post.rs @@ -75,14 +75,17 @@ pub(crate) async fn receive_update_post( // If sticked or locked state was changed, make sure the actor is a mod let stickied = page.ext_one.stickied.context(location_info!())?; let locked = !page.ext_one.comments_enabled.context(location_info!())?; - let mut is_mod_action = false; + let mut mod_action_allowed = false; if stickied != old_post.stickied || locked != old_post.locked { let community = blocking(context.pool(), move |conn| { Community::read(conn, old_post.community_id) }) .await??; - verify_mod_activity(&update, announce, &community, context).await?; - is_mod_action = true; + // Only check mod status if the community is local, otherwise we trust that it was sent correctly. + if community.local { + verify_mod_activity(&update, announce, &community, context).await?; + } + mod_action_allowed = true; } let post = Post::from_apub( @@ -90,7 +93,7 @@ pub(crate) async fn receive_update_post( context, user.actor_id(), request_counter, - is_mod_action, + mod_action_allowed, ) .await?; diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs index c6c90d502..84d2fcedd 100644 --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@ -165,7 +165,7 @@ impl CommunityType for Community { .set_many_contexts(lemmy_context()?) .set_id(generate_activity_id(AnnounceType::Announce)?) .set_to(public()) - .set_many_ccs(vec![self.actor_id()]); + .set_many_ccs(vec![self.followers_url.clone().into_inner()]); send_to_community_followers(announce, self, context).await?; diff --git a/crates/apub/src/inbox/user_inbox.rs b/crates/apub/src/inbox/user_inbox.rs index 3d16403bc..7ddb83368 100644 --- a/crates/apub/src/inbox/user_inbox.rs +++ b/crates/apub/src/inbox/user_inbox.rs @@ -1,5 +1,6 @@ use crate::{ activities::receive::{ + comment::{receive_create_comment, receive_update_comment}, community::{ receive_delete_community, receive_remove_community, @@ -326,6 +327,8 @@ pub async fn receive_announce( } } +/// Receive either a new private message, or a new comment mention. We distinguish them by checking +/// whether the activity is public. async fn receive_create( context: &LemmyContext, activity: AnyBase, @@ -334,9 +337,15 @@ async fn receive_create( ) -> Result<(), LemmyError> { let create = Create::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&create, &expected_domain, true)?; - receive_create_private_message(&context, create, expected_domain, request_counter).await + if verify_is_addressed_to_public(&create).is_ok() { + receive_create_comment(create, context, request_counter).await + } else { + receive_create_private_message(&context, create, expected_domain, request_counter).await + } } +/// Receive either an updated private message, or an updated comment mention. We distinguish +/// them by checking whether the activity is public. async fn receive_update( context: &LemmyContext, activity: AnyBase, @@ -345,7 +354,11 @@ async fn receive_update( ) -> Result<(), LemmyError> { let update = Update::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&update, &expected_domain, true)?; - receive_update_private_message(&context, update, expected_domain, request_counter).await + if verify_is_addressed_to_public(&update).is_ok() { + receive_update_comment(update, context, request_counter).await + } else { + receive_update_private_message(&context, update, expected_domain, request_counter).await + } } async fn receive_delete( diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 43bd86595..63f5d9e2c 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -99,14 +99,14 @@ impl FromApub for Comment { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - is_mod_action: bool, + mod_action_allowed: bool, ) -> Result { let comment: Comment = get_object_from_apub( note, context, expected_domain, request_counter, - is_mod_action, + mod_action_allowed, ) .await?; @@ -135,7 +135,7 @@ impl FromApubToForm for CommentForm { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - _is_mod_action: bool, + _mod_action_allowed: bool, ) -> Result { let creator_actor_id = ¬e .attributed_to() diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index 9ae801b74..73cca5c71 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -107,14 +107,14 @@ impl FromApub for Community { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - is_mod_action: bool, + mod_action_allowed: bool, ) -> Result { let community: Community = get_object_from_apub( group, context, expected_domain, request_counter, - is_mod_action, + mod_action_allowed, ) .await?; @@ -169,7 +169,7 @@ impl FromApubToForm for CommunityForm { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - _is_mod_action: bool, + _mod_action_allowed: bool, ) -> Result { let moderator_uris = fetch_community_mods(context, group, request_counter).await?; let creator_uri = moderator_uris.first().context(location_info!())?; diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 9f5007204..78095e747 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -46,13 +46,13 @@ pub(crate) trait FromApub { /// * `apub` The object to read from /// * `context` LemmyContext which holds DB pool, HTTP client etc /// * `expected_domain` Domain where the object was received from. None in case of mod action. - /// * `is_mod_action` True if the object was sent in a mod activity, ignore `expected_domain` in this case + /// * `mod_action_allowed` True if the object can be a mod activity, ignore `expected_domain` in this case async fn from_apub( apub: &Self::ApubType, context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - is_mod_action: bool, + mod_action_allowed: bool, ) -> Result where Self: Sized; @@ -65,7 +65,7 @@ pub(in crate::objects) trait FromApubToForm { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - is_mod_action: bool, + mod_action_allowed: bool, ) -> Result where Self: Sized; diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 92d542644..14b43c6cb 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -118,14 +118,14 @@ impl FromApub for Post { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - is_mod_action: bool, + mod_action_allowed: bool, ) -> Result { let post: Post = get_object_from_apub( page, context, expected_domain, request_counter, - is_mod_action, + mod_action_allowed, ) .await?; check_object_for_community_or_site_ban(page, post.community_id, context, request_counter) @@ -141,9 +141,9 @@ impl FromApubToForm for PostForm { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - is_mod_action: bool, + mod_action_allowed: bool, ) -> Result { - let ap_id = if is_mod_action { + let ap_id = if mod_action_allowed { let id = page.id_unchecked().context(location_info!())?; check_is_apub_id_valid(id)?; id.to_owned().into() diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 93d13503a..ea9ae3f64 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -77,14 +77,14 @@ impl FromApub for PrivateMessage { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - is_mod_action: bool, + mod_action_allowed: bool, ) -> Result { get_object_from_apub( note, context, expected_domain, request_counter, - is_mod_action, + mod_action_allowed, ) .await } @@ -97,7 +97,7 @@ impl FromApubToForm for PrivateMessageForm { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - _is_mod_action: bool, + _mod_action_allowed: bool, ) -> Result { let creator_actor_id = note .attributed_to() diff --git a/crates/apub/src/objects/user.rs b/crates/apub/src/objects/user.rs index 5b33331b9..b6043d31d 100644 --- a/crates/apub/src/objects/user.rs +++ b/crates/apub/src/objects/user.rs @@ -93,7 +93,7 @@ impl FromApub for User_ { context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, - is_mod_action: bool, + mod_action_allowed: bool, ) -> Result { let user_id = person.id_unchecked().context(location_info!())?.to_owned(); let domain = user_id.domain().context(location_info!())?; @@ -109,7 +109,7 @@ impl FromApub for User_ { context, expected_domain, request_counter, - is_mod_action, + mod_action_allowed, ) .await?; let user = blocking(context.pool(), move |conn| User_::upsert(conn, &user_form)).await??; @@ -125,7 +125,7 @@ impl FromApubToForm for UserForm { _context: &LemmyContext, expected_domain: Url, _request_counter: &mut i32, - _is_mod_action: bool, + _mod_action_allowed: bool, ) -> Result { let avatar = match person.icon() { Some(any_image) => Some( From 05b07b8cbb069ecd1cd5d36724c6f484bd83bdc1 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 19 Mar 2021 18:09:05 +0100 Subject: [PATCH 16/19] Fix conflicts in docker-compose.yml --- docker/federation/docker-compose.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 77382899c..a29e3cf9d 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -29,11 +29,7 @@ services: - ./volumes/pictrs_alpha:/mnt lemmy-alpha-ui: -<<<<<<< HEAD - image: lemmy-ui:test -======= image: dessalines/lemmy-ui:0.10.0-rc.5 ->>>>>>> main environment: - LEMMY_INTERNAL_HOST=lemmy-alpha:8541 - LEMMY_EXTERNAL_HOST=localhost:8541 @@ -62,11 +58,7 @@ services: - ./volumes/postgres_alpha:/var/lib/postgresql/data lemmy-beta-ui: -<<<<<<< HEAD - image: lemmy-ui:test -======= image: dessalines/lemmy-ui:0.10.0-rc.5 ->>>>>>> main environment: - LEMMY_INTERNAL_HOST=lemmy-beta:8551 - LEMMY_EXTERNAL_HOST=localhost:8551 @@ -95,11 +87,7 @@ services: - ./volumes/postgres_beta:/var/lib/postgresql/data lemmy-gamma-ui: -<<<<<<< HEAD - image: lemmy-ui:test -======= image: dessalines/lemmy-ui:0.10.0-rc.5 ->>>>>>> main environment: - LEMMY_INTERNAL_HOST=lemmy-gamma:8561 - LEMMY_EXTERNAL_HOST=localhost:8561 From a5a0d90349744174b07f88e4f7e0abeb0d6443c2 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 22 Mar 2021 13:52:00 +0100 Subject: [PATCH 17/19] Address review comments --- crates/apub/src/activities/receive/post.rs | 2 +- crates/apub/src/fetcher/community.rs | 82 ++++++++++++++++++---- crates/apub/src/objects/community.rs | 53 ++------------ 3 files changed, 73 insertions(+), 64 deletions(-) diff --git a/crates/apub/src/activities/receive/post.rs b/crates/apub/src/activities/receive/post.rs index 2473c2c95..d1c935d56 100644 --- a/crates/apub/src/activities/receive/post.rs +++ b/crates/apub/src/activities/receive/post.rs @@ -76,7 +76,7 @@ pub(crate) async fn receive_update_post( let stickied = page.ext_one.stickied.context(location_info!())?; let locked = !page.ext_one.comments_enabled.context(location_info!())?; let mut mod_action_allowed = false; - if stickied != old_post.stickied || locked != old_post.locked { + if (stickied != old_post.stickied) || (locked != old_post.locked) { let community = blocking(context.pool(), move |conn| { Community::read(conn, old_post.community_id) }) diff --git a/crates/apub/src/fetcher/community.rs b/crates/apub/src/fetcher/community.rs index 2fd5ed255..c27116dc3 100644 --- a/crates/apub/src/fetcher/community.rs +++ b/crates/apub/src/fetcher/community.rs @@ -1,5 +1,10 @@ use crate::{ - fetcher::{fetch::fetch_remote_object, is_deleted, should_refetch_actor}, + fetcher::{ + fetch::fetch_remote_object, + is_deleted, + person::get_or_fetch_and_upsert_person, + should_refetch_actor, + }, inbox::person_inbox::receive_announce, objects::FromApub, GroupExt, @@ -11,8 +16,12 @@ use activitystreams::{ use anyhow::Context; use diesel::result::Error::NotFound; use lemmy_api_structs::blocking; -use lemmy_db_queries::{source::community::Community_, ApubObject}; -use lemmy_db_schema::source::community::Community; +use lemmy_db_queries::{source::community::Community_, ApubObject, Joinable}; +use lemmy_db_schema::{ + source::community::{Community, CommunityModerator, CommunityModeratorForm}, + DbUrl, +}; +use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_utils::{location_info, LemmyError}; use lemmy_websocket::LemmyContext; use log::debug; @@ -54,9 +63,9 @@ async fn fetch_remote_community( apub_id: &Url, context: &LemmyContext, old_community: Option, - recursion_counter: &mut i32, + request_counter: &mut i32, ) -> Result { - let group = fetch_remote_object::(context.client(), apub_id, recursion_counter).await; + let group = fetch_remote_object::(context.client(), apub_id, request_counter).await; if let Some(c) = old_community.to_owned() { if is_deleted(&group) { @@ -71,24 +80,69 @@ async fn fetch_remote_community( } let group = group?; - let community = Community::from_apub( - &group, - context, - apub_id.to_owned(), - recursion_counter, - false, - ) - .await?; + let community = + Community::from_apub(&group, context, apub_id.to_owned(), request_counter, false).await?; + + update_community_mods(&group, &community, context, request_counter).await?; // only fetch outbox for new communities, otherwise this can create an infinite loop if old_community.is_none() { let outbox = group.inner.outbox()?.context(location_info!())?; - fetch_community_outbox(context, outbox, &community, recursion_counter).await? + fetch_community_outbox(context, outbox, &community, request_counter).await? } Ok(community) } +async fn update_community_mods( + group: &GroupExt, + community: &Community, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + let new_moderators = fetch_community_mods(context, group, request_counter).await?; + let community_id = community.id; + let current_moderators = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(&conn, community_id) + }) + .await??; + // Remove old mods from database which arent in the moderators collection anymore + for mod_user in ¤t_moderators { + if !new_moderators.contains(&&mod_user.moderator.actor_id.clone().into()) { + let community_moderator_form = CommunityModeratorForm { + community_id: mod_user.community.id, + person_id: mod_user.moderator.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::leave(conn, &community_moderator_form) + }) + .await??; + } + } + + // Add new mods to database which have been added to moderators collection + for mod_uri in new_moderators { + let mod_user = get_or_fetch_and_upsert_person(&mod_uri, context, request_counter).await?; + let current_mod_uris: Vec = current_moderators + .clone() + .iter() + .map(|c| c.moderator.actor_id.clone()) + .collect(); + if !current_mod_uris.contains(&mod_user.actor_id) { + let community_moderator_form = CommunityModeratorForm { + community_id: community.id, + person_id: mod_user.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::join(conn, &community_moderator_form) + }) + .await??; + } + } + + Ok(()) +} + async fn fetch_community_outbox( context: &LemmyContext, outbox: &Url, diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index ae941a805..a20af20db 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -23,14 +23,11 @@ use activitystreams::{ }; use activitystreams_ext::Ext2; use anyhow::Context; -use lemmy_api_structs::blocking; -use lemmy_db_queries::{DbPool, Joinable}; +use lemmy_db_queries::DbPool; use lemmy_db_schema::{ naive_now, - source::community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm}, - DbUrl, + source::community::{Community, CommunityForm}, }; -use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_utils::{ location_info, utils::{check_slurs, check_slurs_opt, convert_datetime}, @@ -109,56 +106,14 @@ impl FromApub for Community { request_counter: &mut i32, mod_action_allowed: bool, ) -> Result { - let community: Community = get_object_from_apub( + get_object_from_apub( group, context, expected_domain, request_counter, mod_action_allowed, ) - .await?; - - let new_moderators = fetch_community_mods(context, group, request_counter).await?; - let community_id = community.id; - let current_moderators = blocking(context.pool(), move |conn| { - CommunityModeratorView::for_community(&conn, community_id) - }) - .await??; - // Remove old mods from database which arent in the moderators collection anymore - for mod_user in ¤t_moderators { - if !new_moderators.contains(&&mod_user.moderator.actor_id.clone().into()) { - let community_moderator_form = CommunityModeratorForm { - community_id: mod_user.community.id, - person_id: mod_user.moderator.id, - }; - blocking(context.pool(), move |conn| { - CommunityModerator::leave(conn, &community_moderator_form) - }) - .await??; - } - } - - // Add new mods to database which have been added to moderators collection - for mod_uri in new_moderators { - let mod_user = get_or_fetch_and_upsert_person(&mod_uri, context, request_counter).await?; - let current_mod_uris: Vec = current_moderators - .clone() - .iter() - .map(|c| c.moderator.actor_id.clone()) - .collect(); - if !current_mod_uris.contains(&mod_user.actor_id) { - let community_moderator_form = CommunityModeratorForm { - community_id: community.id, - person_id: mod_user.id, - }; - blocking(context.pool(), move |conn| { - CommunityModerator::join(conn, &community_moderator_form) - }) - .await??; - } - } - - Ok(community) + .await } } From d6bd072ea18253aa7d4dd3531e8126db414a5de1 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 22 Mar 2021 15:08:06 +0100 Subject: [PATCH 18/19] Make federation compatible with Lemmy v0.9.9 --- crates/apub/src/objects/comment.rs | 7 +++++- crates/apub/src/objects/community.rs | 37 +++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 536190f03..9ed746586 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -26,6 +26,7 @@ use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::{ source::{ comment::{Comment, CommentForm}, + community::Community, person::Person, post::Post, }, @@ -52,6 +53,9 @@ impl ToApub for Comment { let post_id = self.post_id; let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; + let community_id = post.community_id; + let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; + // Add a vector containing some important info to the "in_reply_to" field // [post_ap_id, Option(parent_comment_ap_id)] let mut in_reply_to_vec = vec![post.ap_id.into_inner()]; @@ -67,7 +71,8 @@ impl ToApub for Comment { .set_many_contexts(lemmy_context()?) .set_id(self.ap_id.to_owned().into_inner()) .set_published(convert_datetime(self.published)) - .set_to(public()) + // NOTE: included community id for compatibility with lemmy v0.9.9 + .set_many_tos(vec![community.actor_id.into_inner(), public()]) .set_many_in_reply_tos(in_reply_to_vec) .set_attributed_to(creator.actor_id.into_inner()); diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index a20af20db..d7e42c4ae 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -23,11 +23,13 @@ use activitystreams::{ }; use activitystreams_ext::Ext2; use anyhow::Context; +use lemmy_api_structs::blocking; use lemmy_db_queries::DbPool; use lemmy_db_schema::{ naive_now, source::community::{Community, CommunityForm}, }; +use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_utils::{ location_info, utils::{check_slurs, check_slurs_opt, convert_datetime}, @@ -40,13 +42,25 @@ use url::Url; impl ToApub for Community { type ApubType = GroupExt; - async fn to_apub(&self, _pool: &DbPool) -> Result { + async fn to_apub(&self, pool: &DbPool) -> Result { + let id = self.id; + let moderators = blocking(pool, move |conn| { + CommunityModeratorView::for_community(&conn, id) + }) + .await??; + let moderators: Vec = moderators + .into_iter() + .map(|m| m.moderator.actor_id.into_inner()) + .collect(); + let mut group = ApObject::new(Group::new()); group .set_many_contexts(lemmy_context()?) .set_id(self.actor_id.to_owned().into()) .set_name(self.title.to_owned()) - .set_published(convert_datetime(self.published)); + .set_published(convert_datetime(self.published)) + // NOTE: included attritubed_to field for compatibility with lemmy v0.9.9 + .set_many_attributed_tos(moderators); if let Some(u) = self.updated.to_owned() { group.set_updated(convert_datetime(u)); @@ -127,9 +141,24 @@ impl FromApubToForm for CommunityForm { _mod_action_allowed: bool, ) -> Result { let moderator_uris = fetch_community_mods(context, group, request_counter).await?; - let creator_uri = moderator_uris.first().context(location_info!())?; + let creator = if let Some(creator_uri) = moderator_uris.first() { + get_or_fetch_and_upsert_person(creator_uri, context, request_counter) + } else { + // NOTE: code for compatibility with lemmy v0.9.9 + let creator_uri = group + .inner + .attributed_to() + .map(|a| a.as_many()) + .flatten() + .map(|a| a.first()) + .flatten() + .map(|a| a.as_xsd_any_uri()) + .flatten() + .context(location_info!())?; + get_or_fetch_and_upsert_person(creator_uri, context, request_counter) + } + .await?; - let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?; let name = group .inner .preferred_username() From c7524d924b968851c379233703697bcfbee74b2f Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 23 Mar 2021 17:03:14 +0100 Subject: [PATCH 19/19] Fix federation tests --- crates/apub/src/inbox/community_inbox.rs | 71 ++++++++++++++++--- crates/apub/src/inbox/mod.rs | 2 +- crates/apub/src/inbox/person_inbox.rs | 41 +++++++++-- .../apub/src/inbox/receive_for_community.rs | 18 ++--- crates/apub/src/inbox/shared_inbox.rs | 12 ++-- crates/apub/src/objects/comment.rs | 15 +++- crates/apub/src/objects/mod.rs | 22 ++---- crates/apub/src/objects/post.rs | 4 +- 8 files changed, 128 insertions(+), 57 deletions(-) diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs index e59201990..c36d4db11 100644 --- a/crates/apub/src/inbox/community_inbox.rs +++ b/crates/apub/src/inbox/community_inbox.rs @@ -134,46 +134,95 @@ pub(crate) async fn community_receive_message( let activity_kind = activity.kind().context(location_info!())?; let do_announce = match activity_kind { CommunityValidTypes::Follow => { - handle_follow(any_base.clone(), person, &to_community, &context).await?; + Box::pin(handle_follow( + any_base.clone(), + person, + &to_community, + &context, + )) + .await?; false } CommunityValidTypes::Undo => { - handle_undo( + Box::pin(handle_undo( context, activity.clone(), actor_url, &to_community, request_counter, - ) + )) .await? } CommunityValidTypes::Create => { - receive_create_for_community(context, any_base.clone(), &actor_url, request_counter).await?; + Box::pin(receive_create_for_community( + context, + any_base.clone(), + &actor_url, + request_counter, + )) + .await?; true } CommunityValidTypes::Update => { - receive_update_for_community(context, any_base.clone(), None, &actor_url, request_counter) - .await?; + Box::pin(receive_update_for_community( + context, + any_base.clone(), + None, + &actor_url, + request_counter, + )) + .await?; true } CommunityValidTypes::Like => { - receive_like_for_community(context, any_base.clone(), &actor_url, request_counter).await?; + Box::pin(receive_like_for_community( + context, + any_base.clone(), + &actor_url, + request_counter, + )) + .await?; true } CommunityValidTypes::Dislike => { - receive_dislike_for_community(context, any_base.clone(), &actor_url, request_counter).await?; + Box::pin(receive_dislike_for_community( + context, + any_base.clone(), + &actor_url, + request_counter, + )) + .await?; true } CommunityValidTypes::Delete => { - receive_delete_for_community(context, any_base.clone(), None, &actor_url).await?; + Box::pin(receive_delete_for_community( + context, + any_base.clone(), + None, + &actor_url, + request_counter, + )) + .await?; true } CommunityValidTypes::Add => { - receive_add_for_community(context, any_base.clone(), None, request_counter).await?; + Box::pin(receive_add_for_community( + context, + any_base.clone(), + None, + request_counter, + )) + .await?; true } CommunityValidTypes::Remove => { - receive_remove_for_community(context, any_base.clone(), None, request_counter).await?; + Box::pin(receive_remove_for_community( + context, + any_base.clone(), + None, + request_counter, + )) + .await?; true } }; diff --git a/crates/apub/src/inbox/mod.rs b/crates/apub/src/inbox/mod.rs index 4fb1732d3..87d6d1824 100644 --- a/crates/apub/src/inbox/mod.rs +++ b/crates/apub/src/inbox/mod.rs @@ -58,7 +58,7 @@ pub(crate) async fn is_activity_already_known( pub(crate) fn get_activity_to_and_cc(activity: &T) -> Vec where - T: AsBase + AsObject + ActorAndObjectRefExt, + T: AsObject, { let mut to_and_cc = vec![]; if let Some(to) = activity.to() { diff --git a/crates/apub/src/inbox/person_inbox.rs b/crates/apub/src/inbox/person_inbox.rs index 1778ea00f..38e4167a7 100644 --- a/crates/apub/src/inbox/person_inbox.rs +++ b/crates/apub/src/inbox/person_inbox.rs @@ -154,19 +154,39 @@ pub(crate) async fn person_receive_message( .await?; } PersonValidTypes::Announce => { - receive_announce(&context, any_base, actor, request_counter).await? + Box::pin(receive_announce(&context, any_base, actor, request_counter)).await? } PersonValidTypes::Create => { - receive_create(&context, any_base, actor_url, request_counter).await? + Box::pin(receive_create( + &context, + any_base, + actor_url, + request_counter, + )) + .await? } PersonValidTypes::Update => { - receive_update(&context, any_base, actor_url, request_counter).await? + Box::pin(receive_update( + &context, + any_base, + actor_url, + request_counter, + )) + .await? } PersonValidTypes::Delete => { - receive_delete(context, any_base, &actor_url, request_counter).await? + Box::pin(receive_delete( + context, + any_base, + &actor_url, + request_counter, + )) + .await? } - PersonValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?, - PersonValidTypes::Remove => receive_remove(context, any_base, &actor_url).await?, + PersonValidTypes::Undo => { + Box::pin(receive_undo(context, any_base, &actor_url, request_counter)).await? + } + PersonValidTypes::Remove => Box::pin(receive_remove(context, any_base, &actor_url)).await?, }; // TODO: would be logical to move websocket notification code here @@ -305,7 +325,14 @@ 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, Some(announce), &inner_id).await + receive_delete_for_community( + context, + inner_activity, + Some(announce), + &inner_id, + request_counter, + ) + .await } Some(Remove) => { receive_remove_for_community(context, inner_activity, Some(announce), request_counter).await diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index b099434a8..2a077e11d 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -35,13 +35,10 @@ use crate::{ objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, person::get_or_fetch_and_upsert_person, }, - 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::{ @@ -122,7 +119,7 @@ pub(in crate::inbox) async fn receive_update_for_community( let update = Update::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&update, &expected_domain, false)?; verify_is_addressed_to_public(&update)?; - verify_modification_actor_instance(&update, &announce, context).await?; + verify_modification_actor_instance(&update, &announce, context, request_counter).await?; let kind = update .object() @@ -197,11 +194,12 @@ pub(in crate::inbox) async fn receive_delete_for_community( activity: AnyBase, announce: Option, expected_domain: &Url, + request_counter: &mut i32, ) -> Result<(), LemmyError> { let delete = Delete::from_any_base(activity)?.context(location_info!())?; verify_activity_domains_valid(&delete, &expected_domain, true)?; verify_is_addressed_to_public(&delete)?; - verify_modification_actor_instance(&delete, &announce, context).await?; + verify_modification_actor_instance(&delete, &announce, context, request_counter).await?; let object = delete .object() @@ -588,6 +586,7 @@ async fn verify_modification_actor_instance( activity: &T, announce: &Option, context: &LemmyContext, + request_counter: &mut i32, ) -> Result<(), LemmyError> where T: ActorAndObjectRef + BaseExt + AsObject, @@ -603,12 +602,9 @@ where .map(|o| o.id()) .flatten() .context(location_info!())?; - 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::Person(p) => p.actor_id(), - Object::PrivateMessage(p) => p.ap_id.into_inner(), + let original_id = match fetch_post_or_comment_by_id(object_id, context, request_counter).await? { + PostOrComment::Post(p) => p.ap_id.into_inner(), + PostOrComment::Comment(c) => c.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/shared_inbox.rs b/crates/apub/src/inbox/shared_inbox.rs index c8076c201..633388a56 100644 --- a/crates/apub/src/inbox/shared_inbox.rs +++ b/crates/apub/src/inbox/shared_inbox.rs @@ -80,13 +80,13 @@ pub async fn shared_inbox( let community_activity = CommunityAcceptedActivities::from_any_base(activity_any_base.clone())? .context(location_info!())?; res = Some( - community_receive_message( + Box::pin(community_receive_message( community_activity, community, actor.as_ref(), &context, request_counter, - ) + )) .await?, ); } else if is_addressed_to_local_person(&to_and_cc, context.pool()).await? { @@ -94,13 +94,13 @@ pub async fn shared_inbox( .context(location_info!())?; // `to_person` is only used for follow activities (which we dont receive here), so no need to pass // it in - person_receive_message( + Box::pin(person_receive_message( person_activity, None, actor.as_ref(), &context, request_counter, - ) + )) .await?; } else if is_addressed_to_community_followers(&to_and_cc, context.pool()) .await? @@ -109,13 +109,13 @@ pub async fn shared_inbox( let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())? .context(location_info!())?; res = Some( - person_receive_message( + Box::pin(person_receive_message( person_activity, None, actor.as_ref(), &context, request_counter, - ) + )) .await?, ); } diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 9ed746586..bd6c1a332 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -165,15 +165,24 @@ impl FromApubToForm for CommentForm { let post_ap_id = in_reply_tos.next().context(location_info!())??; // This post, or the parent comment might not yet exist on this server yet, fetch them. - let post = get_or_fetch_and_insert_post(&post_ap_id, context, request_counter).await?; + let post = Box::pin(get_or_fetch_and_insert_post( + &post_ap_id, + context, + request_counter, + )) + .await?; // The 2nd item, if it exists, is the parent comment apub_id // For deeply nested comments, FromApub automatically gets called recursively let parent_id: Option = match in_reply_tos.next() { Some(parent_comment_uri) => { let parent_comment_ap_id = &parent_comment_uri?; - let parent_comment = - get_or_fetch_and_insert_comment(&parent_comment_ap_id, context, request_counter).await?; + let parent_comment = Box::pin(get_or_fetch_and_insert_comment( + &parent_comment_ap_id, + context, + request_counter, + )) + .await?; Some(parent_comment.id) } diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 800b9dea1..77e9ffbfd 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -1,7 +1,8 @@ use crate::{ check_is_apub_id_valid, fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person}, - inbox::community_inbox::check_community_or_site_ban, + inbox::{community_inbox::check_community_or_site_ban, get_activity_to_and_cc}, + PageExt, }; use activitystreams::{ base::{AsBase, BaseExt, ExtendsExt}, @@ -230,23 +231,12 @@ where check_community_or_site_ban(&person, community_id, context.pool()).await } -pub(in crate::objects) async fn get_to_community( - object: &T, +pub(in crate::objects) async fn get_community_from_to_or_cc( + page: &PageExt, context: &LemmyContext, request_counter: &mut i32, -) -> Result -where - T: ObjectExt, -{ - let community_ids = object - .to() - .context(location_info!())? - .as_many() - .context(location_info!())? - .iter() - .map(|a| a.as_xsd_any_uri().context(location_info!())) - .collect::, anyhow::Error>>()?; - for cid in community_ids { +) -> Result { + for cid in get_activity_to_and_cc(page) { let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await; if community.is_ok() { return community; diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 1e1326385..f532fcc1d 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -6,9 +6,9 @@ use crate::{ check_object_domain, check_object_for_community_or_site_ban, create_tombstone, + get_community_from_to_or_cc, get_object_from_apub, get_source_markdown_value, - get_to_community, set_content_and_source, FromApub, FromApubToForm, @@ -162,7 +162,7 @@ impl FromApubToForm for PostForm { let creator = get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?; - let community = get_to_community(page, context, request_counter).await?; + let community = get_community_from_to_or_cc(page, context, request_counter).await?; let thumbnail_url: Option = match &page.inner.image() { Some(any_image) => Image::from_any_base(