From: Felix Ableitner Date: Fri, 19 Mar 2021 16:11:34 +0000 (+0100) Subject: Merge branch 'main' into federated-moderation X-Git-Url: http://these/git/%7Biframely.thumbnail_url%7D?a=commitdiff_plain;h=4f54108a9cebc11486584adfbccf01de0f069f1e;p=lemmy.git Merge branch 'main' into federated-moderation --- 4f54108a9cebc11486584adfbccf01de0f069f1e diff --cc api_tests/src/post.spec.ts index 0d1788c6,a79ee198..db5f3ede --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@@ -20,10 -20,9 +20,10 @@@ import getPost, unfollowRemotes, searchForUser, - banUserFromSite, + banPersonFromSite, searchPostLocal, - banUserFromCommunity, + followCommunity, + banPersonFromCommunity, } from './shared'; import { PostView, CommunityView } from 'lemmy-js-client'; diff --cc crates/api/src/community.rs index 0bb64f37,f6ecbf12..f7e8e23c --- a/crates/api/src/community.rs +++ b/crates/api/src/community.rs @@@ -36,7 -34,8 +36,8 @@@ use lemmy_db_queries:: }; use lemmy_db_schema::{ naive_now, - source::{comment::Comment, community::*, moderator::*, post::Post, site::*, user::User_}, - source::{comment::Comment, community::*, moderator::*, post::Post, site::*}, ++ source::{comment::Comment, community::*, moderator::*, person::Person, post::Post, site::*}, + PersonId, }; use lemmy_db_views::comment_view::CommentQueryBuilder; use lemmy_db_views_actor::{ @@@ -699,18 -705,18 +707,18 @@@ impl Perform for AddModToCommunity websocket_id: Option, ) -> Result { let data: &AddModToCommunity = &self; - let user = get_user_from_jwt(&data.auth, context.pool()).await?; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - let community_moderator_form = CommunityModeratorForm { - community_id: data.community_id, - person_id: data.person_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?; + is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?; + // Update in local database + let community_moderator_form = CommunityModeratorForm { + community_id: data.community_id, - user_id: data.user_id, ++ person_id: data.person_id, + }; if data.added { let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); if blocking(context.pool(), join).await?.is_err() { @@@ -735,26 -741,6 +743,28 @@@ }) .await??; + // Send to federated instances - let updated_mod_id = data.user_id; ++ let updated_mod_id = data.person_id; + let updated_mod = blocking(context.pool(), move |conn| { - User_::read(conn, updated_mod_id) ++ Person::read(conn, updated_mod_id) + }) + .await??; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + if data.added { - community.send_add_mod(&user, updated_mod, context).await?; ++ community ++ .send_add_mod(&local_user_view.person, updated_mod, context) ++ .await?; + } else { + community - .send_remove_mod(&user, updated_mod, context) ++ .send_remove_mod(&local_user_view.person, updated_mod, 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) diff --cc crates/apub/src/activities/receive/comment.rs index 07f25eec,9ab14dab..95b51d64 --- a/crates/apub/src/activities/receive/comment.rs +++ b/crates/apub/src/activities/receive/comment.rs @@@ -1,6 -1,6 +1,6 @@@ - use crate::{activities::receive::get_actor_as_user, objects::FromApub, ActorType, NoteExt}; + use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, NoteExt}; use activitystreams::{ - activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update}, + activity::{ActorAndObjectRefExt, Create, Dislike, Like, Update}, base::ExtendsExt, }; use anyhow::Context; @@@ -23,7 -23,7 +23,8 @@@ pub(crate) async fn receive_create_comm 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, false).await?; - let comment = Comment::from_apub(¬e, context, person.actor_id(), request_counter).await?; ++ let comment = ++ Comment::from_apub(¬e, context, person.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??; @@@ -64,9 -71,9 +72,10 @@@ pub(crate) async fn receive_update_comm ) -> Result<(), LemmyError> { let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)? .context(location_info!())?; - let user = get_actor_as_user(&update, context, request_counter).await?; + let person = get_actor_as_person(&update, context, request_counter).await?; - let comment = Comment::from_apub(¬e, context, user.actor_id(), request_counter, false).await?; - let comment = Comment::from_apub(¬e, context, person.actor_id(), request_counter).await?; ++ let comment = ++ Comment::from_apub(¬e, context, person.actor_id(), request_counter, false).await?; let comment_id = comment.id; let post_id = comment.post_id; diff --cc crates/apub/src/activities/receive/post.rs index 631f9933,528b1276..2473c2c9 --- a/crates/apub/src/activities/receive/post.rs +++ b/crates/apub/src/activities/receive/post.rs @@@ -1,12 -1,6 +1,12 @@@ -use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, PageExt}; +use crate::{ - activities::receive::get_actor_as_user, ++ activities::receive::get_actor_as_person, + inbox::receive_for_community::verify_mod_activity, + objects::FromApub, + ActorType, + PageExt, +}; use activitystreams::{ - activity::{Create, Dislike, Like, Remove, Update}, + activity::{Announce, Create, Dislike, Like, Update}, prelude::*, }; use anyhow::Context; @@@ -32,7 -20,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, false).await?; - let post = Post::from_apub(&page, context, person.actor_id(), request_counter).await?; ++ let post = Post::from_apub(&page, context, person.actor_id(), request_counter, false).await?; // Refetch the view let post_id = post.id; @@@ -62,40 -49,7 +62,40 @@@ 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 = Post::from_apub(&page, context, person.actor_id(), request_counter).await?; + 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!())?; + 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??; + // 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( + &page, + context, - user.actor_id(), ++ person.actor_id(), + request_counter, + mod_action_allowed, + ) + .await?; let post_id = post.id; // Refetch the view diff --cc crates/apub/src/activities/send/community.rs index 84d2fced,af77a473..80f0a42c --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@@ -1,12 -1,11 +1,13 @@@ 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, + fetcher::person::get_or_fetch_and_upsert_person, + generate_moderators_url, + insert_activity, ActorType, + CommunityType, }; use activitystreams::{ activity::{ @@@ -29,9 -26,9 +30,9 @@@ use anyhow::Context use itertools::Itertools; use lemmy_api_structs::blocking; use lemmy_db_queries::DbPool; - use lemmy_db_schema::source::{community::Community, user::User_}; -use lemmy_db_schema::source::community::Community; ++use lemmy_db_schema::source::{community::Community, person::Person}; use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; - use lemmy_utils::{location_info, LemmyError}; + use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; use url::Url; @@@ -57,11 -54,24 +58,11 @@@ 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. + /// As a local community, accept the follow request from a remote person. async fn send_accept_follow( &self, follow: Follow, @@@ -192,46 -211,4 +202,46 @@@ Ok(inboxes) } + + async fn send_add_mod( + &self, - actor: &User_, - added_mod: User_, ++ actor: &Person, ++ added_mod: Person, + 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()); + + send_to_community(add, actor, self, context).await?; + Ok(()) + } + + async fn send_remove_mod( + &self, - actor: &User_, - removed_mod: User_, ++ actor: &Person, ++ removed_mod: Person, + 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()); + + send_to_community(remove, &actor, self, context).await?; + Ok(()) + } } diff --cc crates/apub/src/activities/send/person.rs index a0778c08,40cc1422..9560c2fb --- a/crates/apub/src/activities/send/person.rs +++ b/crates/apub/src/activities/send/person.rs @@@ -15,10 -14,10 +15,10 @@@ use activitystreams:: 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_, + person::Person, }; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; @@@ -48,11 -47,8 +48,11 @@@ impl ActorType for Person .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. ++impl UserType for Person { + /// As a given local person, send out a follow request to a remote community. async fn send_follow( &self, follow_actor_id: &Url, diff --cc crates/apub/src/fetcher/community.rs index 4ae98be6,12821bbe..2fd5ed25 --- a/crates/apub/src/fetcher/community.rs +++ b/crates/apub/src/fetcher/community.rs @@@ -1,6 -1,11 +1,6 @@@ use crate::{ - fetcher::{ - fetch::fetch_remote_object, - get_or_fetch_and_upsert_person, - is_deleted, - should_refetch_actor, - }, + fetcher::{fetch::fetch_remote_object, is_deleted, should_refetch_actor}, - inbox::user_inbox::receive_announce, + inbox::person_inbox::receive_announce, objects::FromApub, GroupExt, }; diff --cc crates/apub/src/fetcher/person.rs index 7f998ac7,792e6370..3788163b --- a/crates/apub/src/fetcher/person.rs +++ b/crates/apub/src/fetcher/person.rs @@@ -46,18 -46,12 +46,18 @@@ pub(crate) async fn get_or_fetch_and_up return Ok(u); } - let user = User_::from_apub( - let person = - Person::from_apub(&person?, context, apub_id.to_owned(), recursion_counter).await?; ++ let person = Person::from_apub( + &person?, + context, + apub_id.to_owned(), + recursion_counter, + false, + ) + .await?; - let user_id = user.id; + let person_id = person.id; blocking(context.pool(), move |conn| { - User_::mark_as_updated(conn, user_id) + Person::mark_as_updated(conn, person_id) }) .await??; @@@ -69,16 -63,10 +69,16 @@@ let person = fetch_remote_object::(context.client(), apub_id, recursion_counter).await?; - let user = User_::from_apub( - let person = - Person::from_apub(&person, context, apub_id.to_owned(), recursion_counter).await?; ++ let person = Person::from_apub( + &person, + context, + apub_id.to_owned(), + recursion_counter, + false, + ) + .await?; - Ok(user) + Ok(person) } Err(e) => Err(e.into()), } diff --cc crates/apub/src/http/person.rs index dcb73e3b,89d678cf..d523d641 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@@ -22,9 -22,9 +22,9 @@@ pub struct PersonQuery user_name: String, } - /// Return the ActivityPub json representation of a local user over HTTP. - pub(crate) async fn get_apub_user_http( - info: web::Path, + /// Return the ActivityPub json representation of a local person over HTTP. -pub async fn get_apub_person_http( ++pub(crate) async fn get_apub_person_http( + info: web::Path, context: web::Data, ) -> Result, LemmyError> { let user_name = info.into_inner().user_name; @@@ -43,15 -43,15 +43,15 @@@ } } - pub(crate) async fn get_apub_user_outbox( - info: web::Path, -pub async fn get_apub_person_outbox( ++pub(crate) async fn get_apub_person_outbox( + info: web::Path, context: web::Data, ) -> Result, LemmyError> { - let user = blocking(context.pool(), move |conn| { - User_::read_from_name(&conn, &info.user_name) + let person = blocking(context.pool(), move |conn| { + Person::find_by_name(&conn, &info.user_name) }) .await??; - // TODO: populate the user outbox + // TODO: populate the person outbox let mut collection = OrderedCollection::new(); collection .set_many_items(Vec::::new()) @@@ -61,18 -61,18 +61,18 @@@ Ok(create_apub_response(&collection)) } - pub(crate) async fn get_apub_user_inbox( - info: web::Path, -pub async fn get_apub_person_inbox( ++pub(crate) async fn get_apub_person_inbox( + info: web::Path, context: web::Data, ) -> Result, LemmyError> { - let user = blocking(context.pool(), move |conn| { - User_::read_from_name(&conn, &info.user_name) + let person = blocking(context.pool(), move |conn| { + Person::find_by_name(&conn, &info.user_name) }) .await??; let mut collection = OrderedCollection::new(); collection - .set_id(user.inbox_url.into()) - .set_id(format!("{}/inbox", person.actor_id.into_inner()).parse()?) ++ .set_id(person.inbox_url.into()) .set_many_contexts(lemmy_context()?); Ok(create_apub_response(&collection)) } diff --cc crates/apub/src/inbox/mod.rs index ea884183,5a3b9d3d..4fb1732d --- a/crates/apub/src/inbox/mod.rs +++ b/crates/apub/src/inbox/mod.rs @@@ -26,9 -26,9 +26,9 @@@ use std::fmt::Debug use url::Url; pub mod community_inbox; + pub mod person_inbox; -mod receive_for_community; +pub(crate) mod receive_for_community; pub mod shared_inbox; - pub mod user_inbox; pub(crate) fn get_activity_id(activity: &T, creator_uri: &Url) -> Result where diff --cc crates/apub/src/inbox/person_inbox.rs index 7ddb8336,14d76f0c..1778ea00 --- a/crates/apub/src/inbox/person_inbox.rs +++ b/crates/apub/src/inbox/person_inbox.rs @@@ -25,9 -25,9 +25,9 @@@ use crate:: inbox_verify_http_signature, is_activity_already_known, is_addressed_to_community_followers, - is_addressed_to_local_user, + is_addressed_to_local_person, - is_addressed_to_public, receive_for_community::{ + receive_add_for_community, receive_create_for_community, receive_delete_for_community, receive_dislike_for_community, @@@ -153,20 -152,20 +153,20 @@@ pub(crate) async fn person_receive_mess ) .await?; } - UserValidTypes::Announce => { + PersonValidTypes::Announce => { receive_announce(&context, any_base, actor, request_counter).await? } - UserValidTypes::Create => { + PersonValidTypes::Create => { receive_create(&context, any_base, actor_url, request_counter).await? } - UserValidTypes::Update => { + PersonValidTypes::Update => { receive_update(&context, any_base, actor_url, request_counter).await? } - UserValidTypes::Delete => { + PersonValidTypes::Delete => { 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(context, any_base, &actor_url).await?, + PersonValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?, - PersonValidTypes::Remove => receive_remove_community(&context, any_base, &actor_url).await?, ++ PersonValidTypes::Remove => receive_remove(context, any_base, &actor_url).await?, }; // TODO: would be logical to move websocket notification code here diff --cc crates/apub/src/inbox/receive_for_community.rs index cd8f32bb,3c5c2303..b099434a --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@@ -31,50 -31,21 +31,50 @@@ 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, ++ person::get_or_fetch_and_upsert_person, + }, + find_object_by_id, find_post_or_comment_by_id, - inbox::is_addressed_to_public, + generate_moderators_url, + inbox::verify_is_addressed_to_public, + ActorType, + CommunityType, + Object, PostOrComment, }; use activitystreams::{ - activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, + activity::{ + ActorAndObjectRef, + Add, + Announce, + Create, + Delete, + Dislike, + Like, + OptTargetRef, + Remove, + Undo, + Update, + }, base::AnyBase, + object::AsObject, 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::{source::community::CommunityModerator_, ApubObject, Crud, Joinable}; +use lemmy_db_schema::{ + source::{ + community::{Community, CommunityModerator, CommunityModeratorForm}, ++ person::Person, + 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; @@@ -220,48 -187,38 +220,48 @@@ pub(in crate::inbox) async fn receive_d /// 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)?.context(location_info!())?; - verify_activity_domains_valid(&remove, &expected_domain, false)?; - is_addressed_to_public(&remove)?; - - let cc = remove - .cc() - .map(|c| c.as_many()) - .flatten() - .context(location_info!())?; - let community_id = cc - .first() - .map(|c| c.as_xsd_any_uri()) - .flatten() - .context(location_info!())?; - - let object = remove - .object() - .to_owned() - .single_xsd_any_uri() - .context(location_info!())?; - - // Ensure that remove activity comes from the same domain as the community - remove.id(community_id.domain().context(location_info!())?)?; - - match find_post_or_comment_by_id(context, object).await { - Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await, - Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await, - // if we dont have the object, no need to do anything - Err(_) => Ok(()), + 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)?; + + if remove.target().is_some() { + 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 remove_mod = get_or_fetch_and_upsert_person(&remove_mod, context, request_counter).await?; + let form = CommunityModeratorForm { + community_id: community.id, - user_id: remove_mod.id, ++ person_id: remove_mod.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::leave(conn, &form) + }) + .await??; + community.send_announce(remove_any_base, context).await?; + // TODO: send websocket notification about removed mod + Ok(()) + } + // Remove a post or comment + else { + 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, *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(()), + } } } @@@ -381,50 -333,6 +381,50 @@@ pub(in crate::inbox) async fn receive_u } } +/// 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, + add_any_base: AnyBase, + announce: Option, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + 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_add_remove_moderator_target(&add, &community)?; + + 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 new_mod = get_or_fetch_and_upsert_person(&new_mod, context, request_counter).await?; + + // 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) ++ CommunityModerator::get_person_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, ++ person_id: new_mod.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::join(conn, &form) + }) + .await??; + } + if community.local { + community.send_announce(add_any_base, context).await?; + } + // TODO: send websocket notification about added mod + Ok(()) +} + /// A post or comment downvote being reverted pub(in crate::inbox) async fn receive_undo_dislike_for_community( context: &LemmyContext, @@@ -466,172 -374,3 +466,172 @@@ 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, +) -> Result +where + T: AsObject, +{ + let cc = activity + .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 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) +} + +/// 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, + context: &LemmyContext, +) -> Result<(), LemmyError> +where + T: ActorAndObjectRef + BaseExt, +{ + 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()) ++ Person::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()); + } + + 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, + community: &Community, + context: &LemmyContext, +) -> Result<(), LemmyError> +where + T: ActorAndObjectRef + BaseExt, +{ + 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, +) -> 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(()) +} + +/// 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 +/// already verified with `expected_domain`, so this serves as an additional check. +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_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::Person(p) => p.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?; + verify_mod_activity(activity, announce.to_owned(), &community, context).await?; + } + + 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 --cc crates/apub/src/lib.rs index ec5c6d94,38b039ca..74d4cbef --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@@ -17,7 -17,7 +17,7 @@@ use crate::extensions:: }; use activitystreams::{ activity::Follow, -- actor::{ApActor, Group, Person}, ++ actor, base::AnyBase, object::{ApObject, Note, Page}, }; @@@ -31,9 -31,9 +31,9 @@@ use lemmy_db_schema:: activity::Activity, comment::Comment, community::Community, - person::Person as DbPerson, ++ person::{Person as DbPerson, Person}, post::Post, private_message::PrivateMessage, - user::User_, }, DbUrl, }; @@@ -44,9 -44,9 +44,9 @@@ use std::net::IpAddr use url::{ParseError, Url}; /// Activitystreams type for community --type GroupExt = Ext2>, GroupExtension, PublicKeyExtension>; - /// Activitystreams type for user - type PersonExt = Ext1>, PublicKeyExtension>; ++type GroupExt = Ext2>, GroupExtension, PublicKeyExtension>; + /// Activitystreams type for person -type PersonExt = Ext1>, PublicKeyExtension>; ++type PersonExt = Ext1>, PublicKeyExtension>; /// Activitystreams type for post type PageExt = Ext1, PageExtension>; type NoteExt = ApObject; @@@ -175,58 -221,9 +189,58 @@@ 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_, ++ actor: &Person, ++ added_mod: Person, + context: &LemmyContext, + ) -> Result<(), LemmyError>; + async fn send_remove_mod( + &self, - actor: &User_, - removed_mod: User_, ++ actor: &Person, ++ removed_mod: Person, + 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, + Person, Post, Comment, PrivateMessage, @@@ -336,13 -329,12 +350,13 @@@ pub(crate) async fn find_post_or_commen Err(NotFound.into()) } +#[derive(Debug)] pub(crate) enum Object { - Comment(Comment), - Post(Post), - Community(Community), - User(User_), - PrivateMessage(PrivateMessage), + Comment(Box), + Post(Box), + Community(Box), + Person(Box), + PrivateMessage(Box), } pub(crate) async fn find_object_by_id( diff --cc crates/apub/src/objects/community.rs index 73cca5c7,93693813..ae941a80 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@@ -1,7 -1,6 +1,7 @@@ use crate::{ extensions::{context::lemmy_context, group_extensions::GroupExtension}, - fetcher::{community::fetch_community_mods, user::get_or_fetch_and_upsert_user}, - fetcher::person::get_or_fetch_and_upsert_person, ++ fetcher::{community::fetch_community_mods, person::get_or_fetch_and_upsert_person}, + generate_moderators_url, objects::{ check_object_domain, create_tombstone, @@@ -107,58 -120,8 +107,58 @@@ impl FromApub for Community context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, + mod_action_allowed: bool, ) -> 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, + 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, - user_id: mod_user.moderator.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_user(&mod_uri, context, request_counter).await?; ++ 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, - user_id: mod_user.id, ++ person_id: mod_user.id, + }; + blocking(context.pool(), move |conn| { + CommunityModerator::join(conn, &community_moderator_form) + }) + .await??; + } + } + + Ok(community) } } @@@ -169,12 -132,18 +169,12 @@@ impl FromApubToForm for Commu context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, + _mod_action_allowed: bool, ) -> 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 = 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?; + let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?; let name = group .inner .preferred_username() diff --cc crates/apub/src/objects/person.rs index b6043d31,de45aedd..87227dd1 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@@ -93,27 -93,23 +93,30 @@@ impl FromApub for DbPerson context: &LemmyContext, expected_domain: Url, request_counter: &mut i32, + mod_action_allowed: bool, - ) -> Result { - let user_id = person.id_unchecked().context(location_info!())?.to_owned(); - let domain = user_id.domain().context(location_info!())?; + ) -> Result { + let person_id = person.id_unchecked().context(location_info!())?.to_owned(); + let domain = person_id.domain().context(location_info!())?; if domain == Settings::get().hostname() { - let user = blocking(context.pool(), move |conn| { - User_::read_from_apub_id(conn, &user_id.into()) + let person = blocking(context.pool(), move |conn| { + DbPerson::read_from_apub_id(conn, &person_id.into()) }) .await??; - Ok(user) + Ok(person) } else { - let user_form = UserForm::from_apub( - let person_form = - PersonForm::from_apub(person, context, expected_domain, request_counter).await?; ++ let person_form = PersonForm::from_apub( + person, + context, + expected_domain, + request_counter, + mod_action_allowed, + ) + .await?; - let user = blocking(context.pool(), move |conn| User_::upsert(conn, &user_form)).await??; - Ok(user) + let person = blocking(context.pool(), move |conn| { + DbPerson::upsert(conn, &person_form) + }) + .await??; + Ok(person) } } } diff --cc crates/apub/src/objects/post.rs index 14b43c6c,776946cb..1e132638 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@@ -1,7 -1,6 +1,7 @@@ use crate::{ + check_is_apub_id_valid, extensions::{context::lemmy_context, page_extension::PageExtension}, - fetcher::user::get_or_fetch_and_upsert_user, + fetcher::person::get_or_fetch_and_upsert_person, objects::{ check_object_domain, check_object_for_community_or_site_ban, diff --cc crates/apub/src/routes.rs index 2ec8c26d,37fdd66f..fecf333f --- a/crates/apub/src/routes.rs +++ b/crates/apub/src/routes.rs @@@ -54,13 -57,12 +58,16 @@@ pub fn config(cfg: &mut web::ServiceCon "/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)) + .route("/u/{user_name}", web::get().to(get_apub_person_http)) + .route( + "/u/{user_name}/outbox", + web::get().to(get_apub_person_outbox), + ) + .route("/u/{user_name}/inbox", web::get().to(get_apub_person_inbox)) .route("/post/{post_id}", web::get().to(get_apub_post)) .route("/comment/{comment_id}", web::get().to(get_apub_comment)) .route("/activities/{type_}/{id}", web::get().to(get_activity)), diff --cc docker/federation/docker-compose.yml index 142c4fa4,a29e3cf9..77382899 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@@ -29,7 -29,7 +29,11 @@@ 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 @@@ -58,7 -58,7 +62,11 @@@ - ./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 @@@ -87,7 -87,7 +95,11 @@@ - ./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