// Send apub messages
if deleted {
- updated_community.send_delete(context).await?;
+ updated_community
+ .send_delete(local_user_view.person.to_owned(), context)
+ .await?;
} else {
- updated_community.send_undo_delete(context).await?;
+ updated_community
+ .send_undo_delete(local_user_view.person.to_owned(), context)
+ .await?;
}
let community_id = data.community_id;
community::{CommunityResponse, EditCommunity},
get_local_user_view_from_jwt,
};
+use lemmy_apub::CommunityType;
use lemmy_db_queries::{diesel_option_overwrite_to_url, Crud};
use lemmy_db_schema::{
naive_now,
};
let community_id = data.community_id;
- match blocking(context.pool(), move |conn| {
+ let updated_community = blocking(context.pool(), move |conn| {
Community::update(conn, community_id, &community_form)
})
.await?
- {
- Ok(community) => community,
- Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
- };
+ .map_err(|_| ApiError::err("couldnt_update_community"))?;
- // TODO there needs to be some kind of an apub update
- // process for communities and users
+ updated_community
+ .send_update(local_user_view.person.to_owned(), context)
+ .await?;
let community_id = data.community_id;
let person_id = local_user_view.person.id;
fetcher::{get_or_fetch_and_upsert_actor, person::get_or_fetch_and_upsert_person},
generate_moderators_url,
insert_activity,
+ objects::ToApub,
ActorType,
CommunityType,
};
LikeType,
RemoveType,
UndoType,
+ UpdateType,
},
Accept,
ActorAndObjectRefExt,
OptTargetRefExt,
Remove,
Undo,
+ Update,
},
base::{AnyBase, BaseExt, ExtendsExt},
object::ObjectExt,
Ok(())
}
- /// If the creator of a community deletes the community, send this to all followers.
- async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError> {
- let mut delete = Delete::new(self.actor_id(), self.actor_id());
- delete
- .set_many_contexts(lemmy_context()?)
- .set_id(generate_activity_id(DeleteType::Delete)?)
- .set_to(public())
- .set_many_ccs(vec![self.followers_url()]);
+ /// If a remote community is updated by a local mod, send the updated info to the community's
+ /// instance.
+ async fn send_update(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError> {
+ if self.local {
+ // Do nothing, other instances will automatically refetch the community
+ } else {
+ let mut update = Update::new(
+ mod_.actor_id(),
+ self.to_apub(context.pool()).await?.into_any_base()?,
+ );
+ update
+ .set_many_contexts(lemmy_context()?)
+ .set_id(generate_activity_id(UpdateType::Update)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.actor_id()]);
+ send_to_community(update, &mod_, self, None, context).await?;
+ }
+ Ok(())
+ }
- send_to_community_followers(delete, self, None, context).await?;
+ /// If the creator of a community deletes the community, send this to all followers.
+ ///
+ /// We need to handle deletion by a remote mod separately.
+ async fn send_delete(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError> {
+ // Local mod, send directly from community to followers
+ if self.local {
+ let mut delete = Delete::new(self.actor_id(), self.actor_id());
+ delete
+ .set_many_contexts(lemmy_context()?)
+ .set_id(generate_activity_id(DeleteType::Delete)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.followers_url()]);
+
+ send_to_community_followers(delete, self, None, context).await?;
+ }
+ // Remote mod, send from mod to community
+ else {
+ let mut delete = Delete::new(mod_.actor_id(), self.actor_id());
+ delete
+ .set_many_contexts(lemmy_context()?)
+ .set_id(generate_activity_id(DeleteType::Delete)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.actor_id()]);
+
+ send_to_community(delete, &mod_, self, None, context).await?;
+ }
Ok(())
}
/// If the creator of a community reverts the deletion of a community, send this to all followers.
- async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError> {
- let mut delete = Delete::new(self.actor_id(), self.actor_id());
- delete
- .set_many_contexts(lemmy_context()?)
- .set_id(generate_activity_id(DeleteType::Delete)?)
- .set_to(public())
- .set_many_ccs(vec![self.followers_url()]);
-
- let mut undo = Undo::new(self.actor_id(), delete.into_any_base()?);
- undo
- .set_many_contexts(lemmy_context()?)
- .set_id(generate_activity_id(UndoType::Undo)?)
- .set_to(public())
- .set_many_ccs(vec![self.followers_url()]);
-
- send_to_community_followers(undo, self, None, context).await?;
+ ///
+ /// We need to handle undelete by a remote mod separately.
+ async fn send_undo_delete(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError> {
+ // Local mod, send directly from community to followers
+ if self.local {
+ let mut delete = Delete::new(self.actor_id(), self.actor_id());
+ delete
+ .set_many_contexts(lemmy_context()?)
+ .set_id(generate_activity_id(DeleteType::Delete)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.followers_url()]);
+
+ let mut undo = Undo::new(self.actor_id(), delete.into_any_base()?);
+ undo
+ .set_many_contexts(lemmy_context()?)
+ .set_id(generate_activity_id(UndoType::Undo)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.followers_url()]);
+
+ send_to_community_followers(undo, self, None, context).await?;
+ }
+ // Remote mod, send from mod to community
+ else {
+ let mut delete = Delete::new(mod_.actor_id(), self.actor_id());
+ delete
+ .set_many_contexts(lemmy_context()?)
+ .set_id(generate_activity_id(DeleteType::Delete)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.actor_id()]);
+
+ let mut undo = Undo::new(mod_.actor_id(), delete.into_any_base()?);
+ undo
+ .set_many_contexts(lemmy_context()?)
+ .set_id(generate_activity_id(UndoType::Undo)?)
+ .set_to(public())
+ .set_many_ccs(vec![self.actor_id()]);
+
+ send_to_community(undo, &mod_, self, None, context).await?;
+ }
Ok(())
}
pub mod fetcher;
pub mod objects;
-use crate::extensions::{
- group_extension::GroupExtension,
- page_extension::PageExtension,
- person_extension::PersonExtension,
- signatures::{PublicKey, PublicKeyExtension},
+use crate::{
+ extensions::{
+ group_extension::GroupExtension,
+ page_extension::PageExtension,
+ person_extension::PersonExtension,
+ signatures::{PublicKey, PublicKeyExtension},
+ },
+ fetcher::community::get_or_fetch_and_upsert_community,
};
use activitystreams::{
activity::Follow,
use url::{ParseError, Url};
/// Activitystreams type for community
-type GroupExt = Ext2<actor::ApActor<ApObject<actor::Group>>, GroupExtension, PublicKeyExtension>;
+pub type GroupExt =
+ Ext2<actor::ApActor<ApObject<actor::Group>>, GroupExtension, PublicKeyExtension>;
/// Activitystreams type for person
type PersonExt = Ext2<actor::ApActor<ApObject<actor::Person>>, PersonExtension, PublicKeyExtension>;
/// Activitystreams type for post
/// Outbox URL is not generally used by Lemmy, so it can be generated on the fly (but only for
/// local actors).
fn get_outbox_url(&self) -> Result<Url, LemmyError> {
+ /* TODO
if !self.is_local() {
return Err(anyhow!("get_outbox_url() called for remote actor").into());
}
+ */
Ok(Url::parse(&format!("{}/outbox", &self.actor_id()))?)
}
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_update(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
+ async fn send_delete(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
+ async fn send_undo_delete(&self, mod_: Person, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
}
#[derive(Debug)]
-pub(crate) enum Object {
+pub enum Object {
Comment(Box<Comment>),
Post(Box<Post>),
Community(Box<Community>),
PrivateMessage(Box<PrivateMessage>),
}
-pub(crate) async fn find_object_by_id(
- context: &LemmyContext,
- apub_id: Url,
-) -> Result<Object, LemmyError> {
+pub async fn find_object_by_id(context: &LemmyContext, apub_id: Url) -> Result<Object, LemmyError> {
let ap_id = apub_id.clone();
if let Ok(pc) = find_post_or_comment_by_id(context, ap_id.to_owned()).await {
return Ok(match pc {
}
to_and_cc
}
+
+pub async fn get_community_from_to_or_cc<T, Kind>(
+ activity: &T,
+ context: &LemmyContext,
+ request_counter: &mut i32,
+) -> Result<Community, LemmyError>
+where
+ T: AsObject<Kind>,
+{
+ for cid in get_activity_to_and_cc(activity) {
+ let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await;
+ if community.is_ok() {
+ return community;
+ }
+ }
+ Err(NotFound.into())
+}
use crate::{
check_community_or_site_ban,
check_is_apub_id_valid,
- fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
- get_activity_to_and_cc,
- PageExt,
+ fetcher::person::get_or_fetch_and_upsert_person,
};
use activitystreams::{
base::{AsBase, BaseExt, ExtendsExt},
};
use anyhow::{anyhow, Context};
use chrono::NaiveDateTime;
-use diesel::result::Error::NotFound;
use lemmy_api_common::blocking;
use lemmy_db_queries::{ApubObject, Crud, DbPool};
-use lemmy_db_schema::{source::community::Community, CommunityId, DbUrl};
+use lemmy_db_schema::{CommunityId, DbUrl};
use lemmy_utils::{
location_info,
settings::structs::Settings,
}
#[async_trait::async_trait(?Send)]
-pub(in crate::objects) trait FromApubToForm<ApubType> {
+pub trait FromApubToForm<ApubType> {
async fn from_apub(
apub: &ApubType,
context: &LemmyContext,
/// Converts an ActivityPub object (eg `Note`) to a database object (eg `Comment`). If an object
/// with the same ActivityPub ID already exists in the database, it is returned directly. Otherwise
/// the apub object is parsed, inserted and returned.
-pub(in crate::objects) async fn get_object_from_apub<From, Kind, To, ToForm, IdType>(
+pub async fn get_object_from_apub<From, Kind, To, ToForm, IdType>(
from: &From,
context: &LemmyContext,
expected_domain: Url,
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
check_community_or_site_ban(&person, community_id, context.pool()).await
}
-
-pub(in crate::objects) async fn get_community_from_to_or_cc(
- page: &PageExt,
- context: &LemmyContext,
- request_counter: &mut i32,
-) -> Result<Community, LemmyError> {
- 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;
- }
- }
- Err(NotFound.into())
-}
check_is_apub_id_valid,
extensions::{context::lemmy_context, page_extension::PageExtension},
fetcher::person::get_or_fetch_and_upsert_person,
+ get_community_from_to_or_cc,
objects::{
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,
set_content_and_source,
+use crate::{
+ activities::receive::get_actor_as_person,
+ inbox::receive_for_community::verify_actor_is_community_mod,
+};
+use activitystreams::{
+ activity::{ActorAndObjectRefExt, Delete, Undo, Update},
+ base::ExtendsExt,
+};
+use anyhow::{anyhow, Context};
use lemmy_api_common::{blocking, community::CommunityResponse};
-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::LemmyError;
+use lemmy_apub::{
+ get_community_from_to_or_cc,
+ objects::FromApubToForm,
+ ActorType,
+ CommunityType,
+ GroupExt,
+};
+use lemmy_db_queries::{source::community::Community_, Crud};
+use lemmy_db_schema::source::{
+ community::{Community, CommunityForm},
+ person::Person,
+};
+use lemmy_db_views_actor::{
+ community_moderator_view::CommunityModeratorView,
+ community_view::CommunityView,
+};
+use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperationCrud};
+/// This activity is received from a remote community mod, and updates the description or other
+/// fields of a local community.
+pub(crate) async fn receive_remote_mod_update_community(
+ update: Update,
+ context: &LemmyContext,
+ request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+ let community = get_community_from_to_or_cc(&update, context, request_counter).await?;
+ verify_actor_is_community_mod(&update, &community, context).await?;
+ let group = GroupExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
+ .context(location_info!())?;
+ let updated_community = CommunityForm::from_apub(
+ &group,
+ context,
+ community.actor_id(),
+ request_counter,
+ false,
+ )
+ .await?;
+ let cf = CommunityForm {
+ name: updated_community.name,
+ title: updated_community.title,
+ description: updated_community.description,
+ nsfw: updated_community.nsfw,
+ // TODO: icon and banner would be hosted on the other instance, ideally we would copy it to ours
+ icon: updated_community.icon,
+ banner: updated_community.banner,
+ ..CommunityForm::default()
+ };
+ blocking(context.pool(), move |conn| {
+ Community::update(conn, community.id, &cf)
+ })
+ .await??;
+
+ Ok(())
+}
+
+pub(crate) async fn receive_remote_mod_delete_community(
+ delete: Delete,
+ community: Community,
+ context: &LemmyContext,
+ request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+ verify_actor_is_community_mod(&delete, &community, context).await?;
+ let actor = get_actor_as_person(&delete, context, request_counter).await?;
+ verify_is_remote_community_creator(&actor, &community, context).await?;
+ let community_id = community.id;
+ blocking(context.pool(), move |conn| {
+ Community::update_deleted(conn, community_id, true)
+ })
+ .await??;
+ community.send_delete(actor, context).await
+}
+
pub(crate) async fn receive_delete_community(
context: &LemmyContext,
community: Community,
Ok(())
}
+pub(crate) async fn receive_remote_mod_undo_delete_community(
+ undo: Undo,
+ community: Community,
+ context: &LemmyContext,
+ request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+ verify_actor_is_community_mod(&undo, &community, context).await?;
+ let actor = get_actor_as_person(&undo, context, request_counter).await?;
+ verify_is_remote_community_creator(&actor, &community, context).await?;
+ let community_id = community.id;
+ blocking(context.pool(), move |conn| {
+ Community::update_deleted(conn, community_id, false)
+ })
+ .await??;
+ community.send_undo_delete(actor, context).await
+}
+
pub(crate) async fn receive_undo_delete_community(
context: &LemmyContext,
community: Community,
Ok(())
}
+
+/// Checks if the remote user is creator of the local community. This can only happen if a community
+/// is created by a local user, and then transferred to a remote user.
+async fn verify_is_remote_community_creator(
+ user: &Person,
+ community: &Community,
+ context: &LemmyContext,
+) -> Result<(), LemmyError> {
+ let community_id = community.id;
+ let community_mods = blocking(context.pool(), move |conn| {
+ CommunityModeratorView::for_community(conn, community_id)
+ })
+ .await??;
+
+ if user.id != community_mods[0].moderator.id {
+ Err(anyhow!("Actor is not community creator").into())
+ } else {
+ Ok(())
+ }
+}
receive_undo_like_comment,
receive_undo_remove_comment,
},
+ community::{
+ receive_remote_mod_delete_community,
+ receive_remote_mod_undo_delete_community,
+ receive_remote_mod_update_community,
+ },
post::{
receive_create_post,
receive_delete_post,
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,
+ ActorType,
CommunityType,
+ Object,
PostOrComment,
};
use lemmy_db_queries::{
Note,
}
+#[derive(EnumString)]
+enum ObjectTypes {
+ Page,
+ Note,
+ Group,
+ Person,
+}
+
/// This file is for post/comment activities received by the community, and for post/comment
/// activities announced by the community and received by the person.
.as_single_kind_str()
.and_then(|s| s.parse().ok());
match kind {
- Some(PageOrNote::Page) => receive_create_post(create, context, request_counter).await,
- Some(PageOrNote::Note) => receive_create_comment(create, context, request_counter).await,
+ Some(ObjectTypes::Page) => receive_create_post(create, context, request_counter).await,
+ Some(ObjectTypes::Note) => receive_create_comment(create, context, request_counter).await,
_ => receive_unhandled_activity(create),
}
}
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
- let update = Update::from_any_base(activity)?.context(location_info!())?;
+ let update = Update::from_any_base(activity.to_owned())?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, false)?;
verify_is_addressed_to_public(&update)?;
verify_modification_actor_instance(&update, &announce, context, request_counter).await?;
.as_single_kind_str()
.and_then(|s| s.parse().ok());
match kind {
- Some(PageOrNote::Page) => receive_update_post(update, announce, context, request_counter).await,
- Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await,
+ Some(ObjectTypes::Page) => {
+ receive_update_post(update, announce, context, request_counter).await
+ }
+ Some(ObjectTypes::Note) => receive_update_comment(update, context, request_counter).await,
+ Some(ObjectTypes::Group) => {
+ receive_remote_mod_update_community(update, context, request_counter).await
+ }
_ => receive_unhandled_activity(update),
}
}
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let delete = Delete::from_any_base(activity)?.context(location_info!())?;
- verify_activity_domains_valid(&delete, &expected_domain, true)?;
+ // TODO: skip this check if action is done by remote mod
verify_is_addressed_to_public(&delete)?;
verify_modification_actor_instance(&delete, &announce, context, request_counter).await?;
.single_xsd_any_uri()
.context(location_info!())?;
- match find_post_or_comment_by_id(context, object).await {
- Ok(PostOrComment::Post(p)) => receive_delete_post(context, *p).await,
- Ok(PostOrComment::Comment(c)) => receive_delete_comment(context, *c).await,
- // if we dont have the object, no need to do anything
- Err(_) => Ok(()),
+ match find_object_by_id(context, object).await {
+ Ok(Object::Post(p)) => {
+ verify_activity_domains_valid(&delete, &expected_domain, true)?;
+ receive_delete_post(context, *p).await
+ }
+ Ok(Object::Comment(c)) => {
+ verify_activity_domains_valid(&delete, &expected_domain, true)?;
+ receive_delete_comment(context, *c).await
+ }
+ Ok(Object::Community(c)) => {
+ receive_remote_mod_delete_community(delete, *c, context, request_counter).await
+ }
+ // if we dont have the object or dont support its deletion, no need to do anything
+ _ => Ok(()),
}
}
.as_single_kind_str()
.and_then(|s| s.parse().ok())
{
- Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
+ Some(Delete) => {
+ receive_undo_delete_for_community(context, undo, expected_domain, request_counter).await
+ }
Some(Remove) => {
receive_undo_remove_for_community(context, undo, announce, expected_domain).await
}
}
}
-/// A post or comment deletion being reverted
+/// A post, comment or community deletion being reverted
pub(in crate::inbox) async fn receive_undo_delete_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
+ request_counter: &mut i32,
) -> Result<(), LemmyError> {
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)?;
verify_is_addressed_to_public(&delete)?;
let object = delete
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
- match find_post_or_comment_by_id(context, object).await {
- Ok(PostOrComment::Post(p)) => receive_undo_delete_post(context, *p).await,
- Ok(PostOrComment::Comment(c)) => receive_undo_delete_comment(context, *c).await,
- // if we dont have the object, no need to do anything
- Err(_) => Ok(()),
+ match find_object_by_id(context, object).await {
+ Ok(Object::Post(p)) => {
+ verify_activity_domains_valid(&delete, &expected_domain, true)?;
+ receive_undo_delete_post(context, *p).await
+ }
+ Ok(Object::Comment(c)) => {
+ verify_activity_domains_valid(&delete, &expected_domain, true)?;
+ receive_undo_delete_comment(context, *c).await
+ }
+ Ok(Object::Community(c)) => {
+ verify_actor_is_community_mod(&undo, &c, context).await?;
+ receive_remote_mod_undo_delete_community(undo, *c, context, request_counter).await
+ }
+ // if we dont have the object or dont support its deletion, no need to do anything
+ _ => Ok(()),
}
}
///
/// 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<T, Kind>(
+pub(crate) async fn verify_actor_is_community_mod<T, Kind>(
activity: &T,
community: &Community,
context: &LemmyContext,
.map(|o| o.id())
.flatten()
.context(location_info!())?;
- 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(),
+ let original_id = match fetch_post_or_comment_by_id(object_id, context, request_counter).await {
+ Ok(PostOrComment::Post(p)) => p.ap_id.into_inner(),
+ Ok(PostOrComment::Comment(c)) => c.ap_id.into_inner(),
+ Err(_) => {
+ // We can also receive Update activity from remote mod for local activity
+ let object_id = object_id.to_owned().into();
+ blocking(context.pool(), move |conn| {
+ Community::read_from_apub_id(conn, &object_id)
+ })
+ .await??
+ .actor_id()
+ }
};
if actor_id.domain() != original_id.domain() {
let community = extract_community_from_cc(activity, context).await?;