diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs index 4a55b7e0..84e0c849 100644 --- a/crates/api/src/post.rs +++ b/crates/api/src/post.rs @@ -9,7 +9,7 @@ use lemmy_api_common::{ mark_post_as_read, post::*, }; -use lemmy_apub::{ApubLikeableType, ApubObjectType}; +use lemmy_apub::{activities::post::update::UpdatePost, ApubLikeableType}; use lemmy_db_queries::{source::post::Post_, Crud, Likeable, Saveable}; use lemmy_db_schema::source::{moderator::*, post::*}; use lemmy_db_views::post_view::PostView; @@ -140,9 +140,7 @@ impl Perform for LockPost { blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??; // apub updates - updated_post - .send_update(&local_user_view.person, context) - .await?; + UpdatePost::send(&updated_post, &local_user_view.person, context).await?; // Refetch the post let post_id = data.post_id; @@ -214,9 +212,7 @@ impl Perform for StickyPost { // Apub updates // TODO stickied should pry work like locked for ease of use - updated_post - .send_update(&local_user_view.person, context) - .await?; + UpdatePost::send(&updated_post, &local_user_view.person, context).await?; // Refetch the post let post_id = data.post_id; diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index dc99f3ec..89f71d93 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -7,7 +7,7 @@ use lemmy_api_common::{ mark_post_as_read, post::*, }; -use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; +use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, EndpointType}; use lemmy_db_queries::{source::post::Post_, Crud, Likeable}; use lemmy_db_schema::source::post::*; use lemmy_db_views::post_view::PostView; @@ -82,9 +82,12 @@ impl PerformCrud for CreatePost { .await? .map_err(|_| ApiError::err("couldnt_create_post"))?; - updated_post - .send_create(&local_user_view.person, context) - .await?; + lemmy_apub::activities::post::create::CreatePost::send( + &updated_post, + &local_user_view.person, + context, + ) + .await?; // They like their own post by default let person_id = local_user_view.person.id; diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index 5166f212..6b278037 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -1,7 +1,7 @@ use crate::PerformCrud; use actix_web::web::Data; use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*}; -use lemmy_apub::ApubObjectType; +use lemmy_apub::activities::post::update::UpdatePost; use lemmy_db_queries::{source::post::Post_, Crud, DeleteableOrRemoveable}; use lemmy_db_schema::{naive_now, source::post::*}; use lemmy_db_views::post_view::PostView; @@ -89,9 +89,7 @@ impl PerformCrud for EditPost { }; // Send apub update - updated_post - .send_update(&local_user_view.person, context) - .await?; + UpdatePost::send(&updated_post, &local_user_view.person, context).await?; let post_id = data.post_id; let mut post_view = blocking(context.pool(), move |conn| { diff --git a/crates/apub/src/activities/comment/create.rs b/crates/apub/src/activities/comment/create.rs index 84dab107..b3855335 100644 --- a/crates/apub/src/activities/comment/create.rs +++ b/crates/apub/src/activities/comment/create.rs @@ -1,10 +1,12 @@ use crate::{ activities::{ comment::{get_notif_recipients, send_websocket_message}, + extract_community, verify_activity, verify_person_in_community, }, objects::FromApub, + ActorType, NoteExt, }; use activitystreams::{activity::kind::CreateType, base::BaseExt}; @@ -33,8 +35,16 @@ impl ActivityHandler for CreateComment { context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { + let community = extract_community(&self.cc, context, request_counter).await?; + verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community( + &self.common.actor, + &community.actor_id(), + context, + request_counter, + ) + .await?; verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; // TODO: should add a check that the correct community is in cc (probably needs changes to // comment deserialization) diff --git a/crates/apub/src/activities/comment/update.rs b/crates/apub/src/activities/comment/update.rs index 142656f5..5e785229 100644 --- a/crates/apub/src/activities/comment/update.rs +++ b/crates/apub/src/activities/comment/update.rs @@ -1,10 +1,12 @@ use crate::{ activities::{ comment::{get_notif_recipients, send_websocket_message}, + extract_community, verify_activity, verify_person_in_community, }, objects::FromApub, + ActorType, NoteExt, }; use activitystreams::{activity::kind::UpdateType, base::BaseExt}; @@ -33,8 +35,16 @@ impl ActivityHandler for UpdateComment { context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { + let community = extract_community(&self.cc, context, request_counter).await?; + verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community( + &self.common.actor, + &community.actor_id(), + context, + request_counter, + ) + .await?; verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; Ok(()) } diff --git a/crates/apub/src/activities/community/add_mod.rs b/crates/apub/src/activities/community/add_mod.rs index 2785856c..fd22b978 100644 --- a/crates/apub/src/activities/community/add_mod.rs +++ b/crates/apub/src/activities/community/add_mod.rs @@ -38,7 +38,7 @@ impl ActivityHandler for AddMod { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?; Ok(()) diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index 301ccc64..0bdb8bc0 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -4,12 +4,14 @@ use crate::{ community::{ add_mod::AddMod, block_user::BlockUserFromCommunity, + list_community_follower_inboxes, undo_block_user::UndoBlockUserFromCommunity, }, deletion::{ delete::DeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity, }, + generate_activity_id, post::{create::CreatePost, update::UpdatePost}, removal::{ remove::RemovePostCommentCommunityOrMod, @@ -24,11 +26,16 @@ use crate::{ undo_like::UndoLikePostOrComment, }, }, + activity_queue::send_activity_new, + extensions::context::lemmy_context, http::is_activity_already_known, insert_activity, + ActorType, + CommunityType, }; use activitystreams::activity::kind::AnnounceType; use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_schema::source::community::Community; use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use serde::{Deserialize, Serialize}; @@ -66,6 +73,38 @@ pub struct AnnounceActivity { common: ActivityCommonFields, } +impl AnnounceActivity { + pub async fn send( + object: AnnouncableActivities, + community: &Community, + additional_inboxes: Vec, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + let announce = AnnounceActivity { + to: PublicUrl::Public, + object, + cc: vec![community.followers_url()], + kind: AnnounceType::Announce, + common: ActivityCommonFields { + context: lemmy_context()?.into(), + id: generate_activity_id(AnnounceType::Announce)?, + actor: community.actor_id(), + unparsed: Default::default(), + }, + }; + let inboxes = list_community_follower_inboxes(community, additional_inboxes, context).await?; + send_activity_new( + context, + &announce, + &announce.common.id, + community, + inboxes, + false, + ) + .await + } +} + #[async_trait::async_trait(?Send)] impl ActivityHandler for AnnounceActivity { async fn verify( diff --git a/crates/apub/src/activities/community/block_user.rs b/crates/apub/src/activities/community/block_user.rs index c20652b0..1c5f0eaf 100644 --- a/crates/apub/src/activities/community/block_user.rs +++ b/crates/apub/src/activities/community/block_user.rs @@ -36,7 +36,7 @@ impl ActivityHandler for BlockUserFromCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; Ok(()) } diff --git a/crates/apub/src/activities/community/mod.rs b/crates/apub/src/activities/community/mod.rs index 81152d92..62b39c6e 100644 --- a/crates/apub/src/activities/community/mod.rs +++ b/crates/apub/src/activities/community/mod.rs @@ -1,8 +1,11 @@ +use crate::{check_is_apub_id_valid, CommunityType}; +use itertools::Itertools; use lemmy_api_common::{blocking, community::CommunityResponse}; -use lemmy_db_schema::CommunityId; +use lemmy_db_schema::{source::community::Community, CommunityId}; use lemmy_db_views_actor::community_view::CommunityView; -use lemmy_utils::LemmyError; +use lemmy_utils::{settings::structs::Settings, LemmyError}; use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext}; +use url::Url; pub mod add_mod; pub mod announce; @@ -33,3 +36,23 @@ pub(crate) async fn send_websocket_message< Ok(()) } + +async fn list_community_follower_inboxes( + community: &Community, + additional_inboxes: Vec, + context: &LemmyContext, +) -> Result, LemmyError> { + Ok( + vec![ + community.get_follower_inboxes(context.pool()).await?, + additional_inboxes, + ] + .iter() + .flatten() + .unique() + .filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname())) + .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok()) + .map(|inbox| inbox.to_owned()) + .collect(), + ) +} diff --git a/crates/apub/src/activities/community/undo_block_user.rs b/crates/apub/src/activities/community/undo_block_user.rs index 025c498a..d44fe266 100644 --- a/crates/apub/src/activities/community/undo_block_user.rs +++ b/crates/apub/src/activities/community/undo_block_user.rs @@ -36,7 +36,7 @@ impl ActivityHandler for UndoBlockUserFromCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; self.object.verify(context, request_counter).await?; Ok(()) diff --git a/crates/apub/src/activities/community/update.rs b/crates/apub/src/activities/community/update.rs index cfee29da..ce185496 100644 --- a/crates/apub/src/activities/community/update.rs +++ b/crates/apub/src/activities/community/update.rs @@ -39,7 +39,7 @@ impl ActivityHandler for UpdateCommunity { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; Ok(()) } diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index f0f4185a..f7e7fe5c 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -64,7 +64,8 @@ impl ActivityHandler for DeletePostCommentOrCommunity { } // deleting a post or comment else { - verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter) + .await?; let object_creator = get_post_or_comment_actor_id(&self.object, context, request_counter).await?; verify_urls_match(&self.common.actor, &object_creator)?; @@ -83,7 +84,7 @@ impl ActivityHandler for DeletePostCommentOrCommunity { if let Ok(community) = object_community { if community.local { // repeat these checks just to be sure - verify_person_in_community(&self.common().actor, &self.cc, context, request_counter) + verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter) .await?; verify_mod_action(&self.common.actor, self.object.clone(), context).await?; let mod_ = diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index 1de5ca58..94e44d7a 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -54,7 +54,8 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity { } // restoring a post or comment else { - verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter) + .await?; verify_urls_match(&self.common.actor, &self.object.common().actor)?; } Ok(()) @@ -71,7 +72,7 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity { if let Ok(community) = object_community { if community.local { // repeat these checks just to be sure - verify_person_in_community(&self.common().actor, &self.cc, context, request_counter) + verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter) .await?; verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?; let mod_ = diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index d9f3dcdd..af133167 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -13,9 +13,10 @@ use lemmy_db_schema::{ DbUrl, }; use lemmy_db_views_actor::community_view::CommunityView; -use lemmy_utils::LemmyError; +use lemmy_utils::{settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; -use url::Url; +use url::{ParseError, Url}; +use uuid::Uuid; pub mod comment; pub mod community; @@ -41,27 +42,34 @@ async fn verify_person( Ok(()) } -/// Fetches the person and community to verify their type, then checks if person is banned from site -/// or community. -async fn verify_person_in_community( - person_id: &Url, +pub(crate) async fn extract_community( cc: &[Url], context: &LemmyContext, request_counter: &mut i32, ) -> Result { - let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?; let mut cc_iter = cc.iter(); - let community: Community = loop { + loop { if let Some(cid) = cc_iter.next() { if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await { - break c; + break Ok(c); } } else { return Err(anyhow!("No community found in cc").into()); } - }; - check_community_or_site_ban(&person, community.id, context.pool()).await?; - Ok(community) + } +} + +/// Fetches the person and community to verify their type, then checks if person is banned from site +/// or community. +async fn verify_person_in_community( + person_id: &Url, + community_id: &Url, + context: &LemmyContext, + request_counter: &mut i32, +) -> Result<(), LemmyError> { + let community = get_or_fetch_and_upsert_community(community_id, context, request_counter).await?; + 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 } /// Simply check that the url actually refers to a valid group. @@ -80,13 +88,16 @@ fn verify_activity(common: &ActivityCommonFields) -> Result<(), LemmyError> { Ok(()) } -async fn verify_mod_action( +/// Verify that the actor is a community mod. This check is only run if the community is local, +/// because in case of remote communities, admins can also perform mod actions. As admin status +/// is not federated, we cant verify their actions remotely. +pub(crate) async fn verify_mod_action( actor_id: &Url, - activity_cc: Url, + community: Url, context: &LemmyContext, ) -> Result<(), LemmyError> { let community = blocking(context.pool(), move |conn| { - Community::read_from_apub_id(conn, &activity_cc.into()) + Community::read_from_apub_id(conn, &community.into()) }) .await??; @@ -120,3 +131,18 @@ fn verify_add_remove_moderator_target(target: &Url, community: Url) -> Result<() } Ok(()) } + +/// Generate a unique ID for an activity, in the format: +/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36` +fn generate_activity_id(kind: T) -> Result +where + T: ToString, +{ + let id = format!( + "{}/activities/{}/{}", + Settings::get().get_protocol_and_hostname(), + kind.to_string().to_lowercase(), + Uuid::new_v4() + ); + Url::parse(&id) +} diff --git a/crates/apub/src/activities/post/create.rs b/crates/apub/src/activities/post/create.rs index a2ccf4ce..909a6148 100644 --- a/crates/apub/src/activities/post/create.rs +++ b/crates/apub/src/activities/post/create.rs @@ -1,13 +1,30 @@ use crate::{ - activities::{post::send_websocket_message, verify_activity, verify_person_in_community}, + activities::{ + community::announce::AnnouncableActivities, + extract_community, + generate_activity_id, + post::send_websocket_message, + verify_activity, + verify_person_in_community, + }, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, fetcher::person::get_or_fetch_and_upsert_person, - objects::FromApub, + objects::{post::Page, FromApub, ToApub}, ActorType, - PageExt, }; -use activitystreams::{activity::kind::CreateType, base::BaseExt}; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_db_schema::source::post::Post; +use activitystreams::activity::kind::CreateType; +use anyhow::anyhow; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + verify_domains_match, + verify_urls_match, + ActivityCommonFields, + ActivityHandler, + PublicUrl, +}; +use lemmy_db_queries::Crud; +use lemmy_db_schema::source::{community::Community, person::Person, post::Post}; use lemmy_utils::LemmyError; use lemmy_websocket::{LemmyContext, UserOperationCrud}; use url::Url; @@ -16,14 +33,40 @@ use url::Url; #[serde(rename_all = "camelCase")] pub struct CreatePost { to: PublicUrl, - object: PageExt, - cc: Vec, - #[serde(rename = "type")] - kind: CreateType, + object: Page, + cc: [Url; 1], + r#type: CreateType, #[serde(flatten)] common: ActivityCommonFields, } +impl CreatePost { + pub async fn send(post: &Post, actor: &Person, context: &LemmyContext) -> Result<(), LemmyError> { + let community_id = post.community_id; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + let id = generate_activity_id(CreateType::Create)?; + let create = CreatePost { + to: PublicUrl::Public, + object: post.to_apub(context.pool()).await?, + cc: [community.actor_id()], + r#type: Default::default(), + common: ActivityCommonFields { + context: lemmy_context()?.into(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + + let activity = AnnouncableActivities::CreatePost(create); + send_to_community_new(activity, &id, actor, &community, vec![], context).await + } +} + #[async_trait::async_trait(?Send)] impl ActivityHandler for CreatePost { async fn verify( @@ -31,9 +74,23 @@ impl ActivityHandler for CreatePost { context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { + let community = extract_community(&self.cc, context, request_counter).await?; + let community_id = &community.actor_id(); + verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + verify_person_in_community(&self.common.actor, community_id, context, request_counter).await?; + verify_domains_match(&self.common.actor, &self.object.id)?; + verify_urls_match(&self.common.actor, &self.object.attributed_to)?; + // Check that the post isnt locked or stickied, as that isnt possible for newly created posts. + // However, when fetching a remote post we generate a new create activity with the current + // locked/stickied value, so this check may fail. So only check if its a local community, + // because then we will definitely receive all create and update activities separately. + let is_stickied_or_locked = + self.object.stickied == Some(true) || self.object.comments_enabled == Some(false); + if community.local && is_stickied_or_locked { + return Err(anyhow!("New post cannot be stickied or locked").into()); + } + self.object.verify(context, request_counter).await?; Ok(()) } diff --git a/crates/apub/src/activities/post/update.rs b/crates/apub/src/activities/post/update.rs index 13456dcd..a8cec9e7 100644 --- a/crates/apub/src/activities/post/update.rs +++ b/crates/apub/src/activities/post/update.rs @@ -1,24 +1,24 @@ use crate::{ activities::{ + community::announce::AnnouncableActivities, + generate_activity_id, post::send_websocket_message, verify_activity, verify_mod_action, verify_person_in_community, }, - objects::{FromApub, FromApubToForm}, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, + fetcher::community::get_or_fetch_and_upsert_community, + objects::{post::Page, FromApub, ToApub}, ActorType, - PageExt, }; -use activitystreams::{activity::kind::UpdateType, base::BaseExt}; -use anyhow::Context; +use activitystreams::activity::kind::UpdateType; use lemmy_api_common::blocking; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl}; -use lemmy_db_queries::ApubObject; -use lemmy_db_schema::{ - source::post::{Post, PostForm}, - DbUrl, -}; -use lemmy_utils::{location_info, LemmyError}; +use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl}; +use lemmy_db_queries::Crud; +use lemmy_db_schema::source::{community::Community, person::Person, post::Post}; +use lemmy_utils::LemmyError; use lemmy_websocket::{LemmyContext, UserOperationCrud}; use url::Url; @@ -26,14 +26,39 @@ use url::Url; #[serde(rename_all = "camelCase")] pub struct UpdatePost { to: PublicUrl, - object: PageExt, - cc: Vec, - #[serde(rename = "type")] - kind: UpdateType, + object: Page, + cc: [Url; 1], + r#type: UpdateType, #[serde(flatten)] common: ActivityCommonFields, } +impl UpdatePost { + pub async fn send(post: &Post, actor: &Person, context: &LemmyContext) -> Result<(), LemmyError> { + let community_id = post.community_id; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + let id = generate_activity_id(UpdateType::Update)?; + let update = UpdatePost { + to: PublicUrl::Public, + object: post.to_apub(context.pool()).await?, + cc: [community.actor_id()], + r#type: Default::default(), + common: ActivityCommonFields { + context: lemmy_context()?.into(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + let activity = AnnouncableActivities::UpdatePost(update); + send_to_community_new(activity, &id, actor, &community, vec![], context).await + } +} + #[async_trait::async_trait(?Send)] impl ActivityHandler for UpdatePost { async fn verify( @@ -41,34 +66,19 @@ impl ActivityHandler for UpdatePost { context: &LemmyContext, request_counter: &mut i32, ) -> Result<(), LemmyError> { + let community_id = get_or_fetch_and_upsert_community(&self.cc[0], context, request_counter) + .await? + .actor_id(); + let is_mod_action = self.object.is_mod_action(context.pool()).await?; + verify_activity(self.common())?; - let community = - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; - - let temp_post = PostForm::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - true, - ) - .await?; - let post_id: DbUrl = temp_post.ap_id.context(location_info!())?; - let old_post = blocking(context.pool(), move |conn| { - Post::read_from_apub_id(conn, &post_id) - }) - .await??; - let stickied = temp_post.stickied.context(location_info!())?; - let locked = temp_post.locked.context(location_info!())?; - // community mod changed locked/sticky status - if (stickied != old_post.stickied) || (locked != old_post.locked) { - verify_mod_action(&self.common.actor, community.actor_id(), context).await?; + verify_person_in_community(&self.common.actor, &community_id, context, request_counter).await?; + if is_mod_action { + verify_mod_action(&self.common.actor, community_id, context).await?; + } else { + verify_urls_match(&self.common.actor, &self.object.attributed_to)?; } - // user edited their own post - else { - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - } - + self.object.verify(context, request_counter).await?; Ok(()) } diff --git a/crates/apub/src/activities/removal/remove.rs b/crates/apub/src/activities/removal/remove.rs index a30f2332..7e62ab7b 100644 --- a/crates/apub/src/activities/removal/remove.rs +++ b/crates/apub/src/activities/removal/remove.rs @@ -64,13 +64,13 @@ impl ActivityHandler for RemovePostCommentCommunityOrMod { } // removing community mod else if let Some(target) = &self.target { - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; verify_add_remove_moderator_target(target, self.cc[0].clone())?; } // removing a post or comment else { - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; } Ok(()) diff --git a/crates/apub/src/activities/removal/undo_remove.rs b/crates/apub/src/activities/removal/undo_remove.rs index 997d527d..ca77a31d 100644 --- a/crates/apub/src/activities/removal/undo_remove.rs +++ b/crates/apub/src/activities/removal/undo_remove.rs @@ -52,7 +52,7 @@ impl ActivityHandler for UndoRemovePostCommentOrCommunity { } // removing a post or comment else { - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; } self.object.verify(context, request_counter).await?; diff --git a/crates/apub/src/activities/send/comment.rs b/crates/apub/src/activities/send/comment.rs index b93f9e1c..be24dd07 100644 --- a/crates/apub/src/activities/send/comment.rs +++ b/crates/apub/src/activities/send/comment.rs @@ -1,5 +1,5 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::{send_comment_mentions, send_to_community}, extensions::context::lemmy_context, fetcher::person::get_or_fetch_and_upsert_person, diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs index bf751479..96cb058a 100644 --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@ -1,5 +1,5 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers}, check_is_apub_id_valid, extensions::context::lemmy_context, diff --git a/crates/apub/src/activities/send/mod.rs b/crates/apub/src/activities/send/mod.rs index 10dd8a26..65135bdd 100644 --- a/crates/apub/src/activities/send/mod.rs +++ b/crates/apub/src/activities/send/mod.rs @@ -1,24 +1,5 @@ -use lemmy_utils::settings::structs::Settings; -use url::{ParseError, Url}; -use uuid::Uuid; - pub(crate) mod comment; pub(crate) mod community; pub(crate) mod person; pub(crate) mod post; pub(crate) mod private_message; - -/// Generate a unique ID for an activity, in the format: -/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36` -fn generate_activity_id(kind: T) -> Result -where - T: ToString, -{ - let id = format!( - "{}/activities/{}/{}", - Settings::get().get_protocol_and_hostname(), - kind.to_string().to_lowercase(), - Uuid::new_v4() - ); - Url::parse(&id) -} diff --git a/crates/apub/src/activities/send/person.rs b/crates/apub/src/activities/send/person.rs index b1fc0cd2..a5e79251 100644 --- a/crates/apub/src/activities/send/person.rs +++ b/crates/apub/src/activities/send/person.rs @@ -1,5 +1,5 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::send_activity_single_dest, extensions::context::lemmy_context, ActorType, diff --git a/crates/apub/src/activities/send/post.rs b/crates/apub/src/activities/send/post.rs index c51d6f2d..db96621b 100644 --- a/crates/apub/src/activities/send/post.rs +++ b/crates/apub/src/activities/send/post.rs @@ -1,22 +1,19 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::send_to_community, extensions::context::lemmy_context, - objects::ToApub, ActorType, ApubLikeableType, ApubObjectType, }; use activitystreams::{ activity::{ - kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType}, - Create, + kind::{DeleteType, DislikeType, LikeType, RemoveType, UndoType}, Delete, Dislike, Like, Remove, Undo, - Update, }, prelude::*, public, @@ -29,52 +26,20 @@ use lemmy_websocket::LemmyContext; #[async_trait::async_trait(?Send)] impl ApubObjectType for Post { - /// Send out information about a newly created post, to the followers of the community. - async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let page = self.to_apub(context.pool()).await?; - - let community_id = self.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut create = Create::new( - creator.actor_id.to_owned().into_inner(), - page.into_any_base()?, - ); - create - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(CreateType::Create)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(create, creator, &community, None, context).await?; - Ok(()) + async fn send_create( + &self, + _creator: &Person, + _context: &LemmyContext, + ) -> Result<(), LemmyError> { + unimplemented!() } - /// Send out information about an edited post, to the followers of the community. - async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let page = self.to_apub(context.pool()).await?; - - let community_id = self.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let mut update = Update::new( - creator.actor_id.to_owned().into_inner(), - page.into_any_base()?, - ); - update - .set_many_contexts(lemmy_context()?) - .set_id(generate_activity_id(UpdateType::Update)?) - .set_to(public()) - .set_many_ccs(vec![community.actor_id()]); - - send_to_community(update, creator, &community, None, context).await?; - Ok(()) + async fn send_update( + &self, + _creator: &Person, + _context: &LemmyContext, + ) -> Result<(), LemmyError> { + unimplemented!() } async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { diff --git a/crates/apub/src/activities/send/private_message.rs b/crates/apub/src/activities/send/private_message.rs index e5a30585..d208b25d 100644 --- a/crates/apub/src/activities/send/private_message.rs +++ b/crates/apub/src/activities/send/private_message.rs @@ -1,5 +1,5 @@ use crate::{ - activities::send::generate_activity_id, + activities::generate_activity_id, activity_queue::send_activity_single_dest, extensions::context::lemmy_context, objects::ToApub, diff --git a/crates/apub/src/activities/voting/dislike.rs b/crates/apub/src/activities/voting/dislike.rs index 18d72f39..b34b2d10 100644 --- a/crates/apub/src/activities/voting/dislike.rs +++ b/crates/apub/src/activities/voting/dislike.rs @@ -29,7 +29,7 @@ impl ActivityHandler for DislikePostOrComment { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; Ok(()) } diff --git a/crates/apub/src/activities/voting/like.rs b/crates/apub/src/activities/voting/like.rs index ca899d3d..50a4d44d 100644 --- a/crates/apub/src/activities/voting/like.rs +++ b/crates/apub/src/activities/voting/like.rs @@ -29,7 +29,7 @@ impl ActivityHandler for LikePostOrComment { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; Ok(()) } diff --git a/crates/apub/src/activities/voting/undo_dislike.rs b/crates/apub/src/activities/voting/undo_dislike.rs index 11871e79..5ba3b47f 100644 --- a/crates/apub/src/activities/voting/undo_dislike.rs +++ b/crates/apub/src/activities/voting/undo_dislike.rs @@ -29,7 +29,7 @@ impl ActivityHandler for UndoDislikePostOrComment { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_urls_match(&self.common.actor, &self.object.common().actor)?; self.object.verify(context, request_counter).await?; Ok(()) diff --git a/crates/apub/src/activities/voting/undo_like.rs b/crates/apub/src/activities/voting/undo_like.rs index 07c3c470..2de03f4b 100644 --- a/crates/apub/src/activities/voting/undo_like.rs +++ b/crates/apub/src/activities/voting/undo_like.rs @@ -29,7 +29,7 @@ impl ActivityHandler for UndoLikePostOrComment { request_counter: &mut i32, ) -> Result<(), LemmyError> { verify_activity(self.common())?; - verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?; + verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?; verify_urls_match(&self.common.actor, &self.object.common().actor)?; self.object.verify(context, request_counter).await?; Ok(()) diff --git a/crates/apub/src/activity_queue.rs b/crates/apub/src/activity_queue.rs index 22b88d14..f4f8ce90 100644 --- a/crates/apub/src/activity_queue.rs +++ b/crates/apub/src/activity_queue.rs @@ -1,4 +1,5 @@ use crate::{ + activities::community::announce::{AnnouncableActivities, AnnounceActivity}, check_is_apub_id_valid, extensions::signatures::sign_and_send, insert_activity, @@ -24,7 +25,7 @@ use itertools::Itertools; use lemmy_db_schema::source::{community::Community, person::Person}; use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; -use log::{debug, warn}; +use log::{debug, info, warn}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin}; @@ -171,6 +172,80 @@ where Ok(()) } +pub(crate) async fn send_to_community_new( + activity: AnnouncableActivities, + activity_id: &Url, + actor: &dyn ActorType, + community: &Community, + additional_inboxes: Vec, + context: &LemmyContext, +) -> Result<(), LemmyError> { + // if this is a local community, we need to do an announce from the community instead + if community.local { + insert_activity(activity_id, activity.clone(), true, false, context.pool()).await?; + AnnounceActivity::send(activity, community, additional_inboxes, context).await?; + } else { + let mut inboxes = additional_inboxes; + inboxes.push(community.get_shared_inbox_or_inbox_url()); + send_activity_new(context, &activity, activity_id, actor, inboxes, false).await?; + } + + Ok(()) +} + +pub(crate) async fn send_activity_new( + context: &LemmyContext, + activity: &T, + activity_id: &Url, + actor: &dyn ActorType, + inboxes: Vec, + sensitive: bool, +) -> Result<(), LemmyError> +where + T: Serialize, +{ + if !Settings::get().federation().enabled || inboxes.is_empty() { + return Ok(()); + } + + info!("Sending activity {}", activity_id.to_string()); + + // Don't send anything to ourselves + // TODO: this should be a debug assert + let hostname = Settings::get().get_hostname_without_port()?; + let inboxes: Vec<&Url> = inboxes + .iter() + .filter(|i| i.domain().expect("valid inbox url") != hostname) + .collect(); + + let serialised_activity = serde_json::to_string(&activity)?; + + insert_activity( + activity_id, + serialised_activity.clone(), + true, + sensitive, + context.pool(), + ) + .await?; + + for i in inboxes { + let message = SendActivityTask { + activity: serialised_activity.to_owned(), + inbox: i.to_owned(), + actor_id: actor.actor_id(), + private_key: actor.private_key().context(location_info!())?, + }; + if env::var("LEMMY_TEST_SEND_SYNC").is_ok() { + do_send(message, &Client::default()).await?; + } else { + context.activity_queue.queue::(message)?; + } + } + + Ok(()) +} + /// Create new `SendActivityTasks`, which will deliver the given activity to inboxes, as well as /// handling signing and retrying failed deliveres. /// diff --git a/crates/apub/src/extensions/mod.rs b/crates/apub/src/extensions/mod.rs index 19e37894..781e89e6 100644 --- a/crates/apub/src/extensions/mod.rs +++ b/crates/apub/src/extensions/mod.rs @@ -1,5 +1,4 @@ pub mod context; pub(crate) mod group_extension; -pub(crate) mod page_extension; pub(crate) mod person_extension; pub mod signatures; diff --git a/crates/apub/src/extensions/page_extension.rs b/crates/apub/src/extensions/page_extension.rs deleted file mode 100644 index 752fa2b4..00000000 --- a/crates/apub/src/extensions/page_extension.rs +++ /dev/null @@ -1,36 +0,0 @@ -use activitystreams::unparsed::UnparsedMutExt; -use activitystreams_ext::UnparsedExtension; -use serde::{Deserialize, Serialize}; - -/// Activitystreams extension to allow (de)serializing additional Post fields -/// `comemnts_enabled` (called 'locked' in Lemmy), -/// `sensitive` (called 'nsfw') and `stickied`. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PageExtension { - pub comments_enabled: Option, - pub sensitive: Option, - pub stickied: Option, -} - -impl UnparsedExtension for PageExtension -where - U: UnparsedMutExt, -{ - type Error = serde_json::Error; - - fn try_from_unparsed(unparsed_mut: &mut U) -> Result { - Ok(PageExtension { - comments_enabled: unparsed_mut.remove("commentsEnabled")?, - sensitive: unparsed_mut.remove("sensitive")?, - stickied: unparsed_mut.remove("stickied")?, - }) - } - - fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { - unparsed_mut.insert("commentsEnabled", self.comments_enabled)?; - unparsed_mut.insert("sensitive", self.sensitive)?; - unparsed_mut.insert("stickied", self.stickied)?; - Ok(()) - } -} diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs index af8a59f7..fd94f649 100644 --- a/crates/apub/src/fetcher/objects.rs +++ b/crates/apub/src/fetcher/objects.rs @@ -1,8 +1,7 @@ use crate::{ fetcher::fetch::fetch_remote_object, - objects::FromApub, + objects::{post::Page, FromApub}, NoteExt, - PageExt, PostOrComment, }; use anyhow::anyhow; @@ -35,7 +34,7 @@ pub async fn get_or_fetch_and_insert_post( Err(NotFound {}) => { debug!("Fetching and creating remote post: {}", post_ap_id); let page = - fetch_remote_object::(context.client(), post_ap_id, recursion_counter).await?; + fetch_remote_object::(context.client(), post_ap_id, recursion_counter).await?; let post = Post::from_apub( &page, context, diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 5a09fd43..69076c15 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -6,11 +6,10 @@ use crate::{ is_deleted, }, find_object_by_id, - objects::FromApub, + objects::{post::Page, FromApub}, GroupExt, NoteExt, Object, - PageExt, PersonExt, }; use activitystreams::base::BaseExt; @@ -46,7 +45,7 @@ use url::Url; enum SearchAcceptedObjects { Person(Box), Group(Box), - Page(Box), + Page(Box), Comment(Box), } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 1b1e3d37..94da33cc 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -11,7 +11,6 @@ pub mod objects; use crate::{ extensions::{ group_extension::GroupExtension, - page_extension::PageExtension, person_extension::PersonExtension, signatures::{PublicKey, PublicKeyExtension}, }, @@ -21,9 +20,9 @@ use activitystreams::{ activity::Follow, actor, base::AnyBase, - object::{ApObject, AsObject, Note, ObjectExt, Page}, + object::{ApObject, AsObject, Note, ObjectExt}, }; -use activitystreams_ext::{Ext1, Ext2}; +use activitystreams_ext::Ext2; use anyhow::{anyhow, Context}; use diesel::NotFound; use lemmy_api_common::blocking; @@ -54,8 +53,6 @@ pub type GroupExt = type PersonExt = Ext2>>, PersonExtension, PublicKeyExtension>; pub type SiteExt = actor::ApActor>; -/// Activitystreams type for post -pub type PageExt = Ext1, PageExtension>; pub type NoteExt = ApObject; #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)] diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 19bfe8aa..ace6b913 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,30 +1,23 @@ use crate::{ - check_is_apub_id_valid, - extensions::{context::lemmy_context, page_extension::PageExtension}, + activities::extract_community, + extensions::context::lemmy_context, 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_object_from_apub, - get_source_markdown_value, - set_content_and_source, - FromApub, - FromApubToForm, - ToApub, - }, - PageExt, + objects::{create_tombstone, FromApub, ToApub}, }; use activitystreams::{ - object::{kind::PageType, ApObject, Image, Page, Tombstone}, - prelude::*, + base::AnyBase, + object::{ + kind::{ImageType, PageType}, + Tombstone, + }, + primitives::OneOrMany, public, + unparsed::Unparsed, }; -use activitystreams_ext::Ext1; -use anyhow::Context; +use chrono::{DateTime, FixedOffset}; use lemmy_api_common::blocking; -use lemmy_db_queries::{Crud, DbPool}; +use lemmy_apub_lib::verify_domains_match; +use lemmy_db_queries::{ApubObject, Crud, DbPool}; use lemmy_db_schema::{ self, source::{ @@ -34,9 +27,8 @@ use lemmy_db_schema::{ }, }; use lemmy_utils::{ - location_info, request::fetch_iframely_and_pictrs_data, - utils::{check_slurs, convert_datetime, remove_slurs}, + utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs}, LemmyError, }; use lemmy_websocket::LemmyContext; @@ -44,56 +36,44 @@ use url::Url; #[async_trait::async_trait(?Send)] impl ToApub for Post { - type ApubType = PageExt; + type ApubType = Page; // Turn a Lemmy post into an ActivityPub page that can be sent out over the network. - async fn to_apub(&self, pool: &DbPool) -> Result { - let mut page = ApObject::new(Page::new()); - + async fn to_apub(&self, pool: &DbPool) -> Result { let creator_id = self.creator_id; let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??; - let community_id = self.community_id; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; - page - // Not needed when the Post is embedded in a collection (like for community outbox) - // TODO: need to set proper context defining sensitive/commentsEnabled fields - // https://git.asonix.dog/Aardwolf/activitystreams/issues/5 - .set_many_contexts(lemmy_context()?) - .set_id(self.ap_id.to_owned().into_inner()) - .set_name(self.name.to_owned()) - // `summary` field for compatibility with lemmy v0.9.9 and older, - // TODO: remove this after some time - .set_summary(self.name.to_owned()) - .set_published(convert_datetime(self.published)) - .set_many_tos(vec![community.actor_id.into_inner(), public()]) - .set_attributed_to(creator.actor_id.into_inner()); + let source = self.body.clone().map(|body| Source { + content: body, + media_type: MediaTypeMarkdown::Markdown, + }); + let image = self.thumbnail_url.clone().map(|thumb| ImageObject { + content: ImageType::Image, + url: thumb.into(), + }); - if let Some(body) = &self.body { - set_content_and_source(&mut page, body)?; - } - - if let Some(url) = &self.url { - page.set_url::(url.to_owned().into()); - } - - if let Some(thumbnail_url) = &self.thumbnail_url { - let mut image = Image::new(); - image.set_url::(thumbnail_url.to_owned().into()); - page.set_image(image.into_any_base()?); - } - - if let Some(u) = self.updated { - page.set_updated(convert_datetime(u)); - } - - let ext = PageExtension { + let page = Page { + context: lemmy_context()?.into(), + r#type: PageType::Page, + id: self.ap_id.clone().into(), + attributed_to: creator.actor_id.into(), + to: [community.actor_id.into(), public()], + name: self.name.clone(), + content: self.body.as_ref().map(|b| markdown_to_html(b)), + media_type: MediaTypeHtml::Markdown, + source, + url: self.url.clone().map(|u| u.into()), + image, comments_enabled: Some(!self.locked), sensitive: Some(self.nsfw), stickied: Some(self.stickied), + published: convert_datetime(self.published), + updated: self.updated.map(convert_datetime), + unparsed: Default::default(), }; - Ok(Ext1::new(page, ext)) + Ok(page) } fn to_tombstone(&self) -> Result { @@ -106,138 +86,133 @@ impl ToApub for Post { } } -#[async_trait::async_trait(?Send)] -impl FromApub for Post { - type ApubType = PageExt; +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub enum MediaTypeMarkdown { + #[serde(rename = "text/markdown")] + Markdown, +} - /// Converts a `PageExt` to `PostForm`. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub enum MediaTypeHtml { + #[serde(rename = "text/html")] + Markdown, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Source { + content: String, + media_type: MediaTypeMarkdown, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageObject { + content: ImageType, + url: Url, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Page { + #[serde(rename = "@context")] + context: OneOrMany, + r#type: PageType, + pub(crate) id: Url, + pub(crate) attributed_to: Url, + to: [Url; 2], + name: String, + content: Option, + media_type: MediaTypeHtml, + source: Option, + url: Option, + image: Option, + pub(crate) comments_enabled: Option, + sensitive: Option, + pub(crate) stickied: Option, + published: DateTime, + updated: Option>, + + // unparsed fields + #[serde(flatten)] + unparsed: Unparsed, +} + +impl Page { + /// Only mods can change the post's stickied/locked status. So if either of these is changed from + /// the current value, it is a mod action and needs to be verified as such. /// - /// If the post's community or creator are not known locally, these are also fetched. - async fn from_apub( - page: &PageExt, - context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - mod_action_allowed: bool, - ) -> Result { - let post: Post = get_object_from_apub( - page, - context, - expected_domain, - request_counter, - mod_action_allowed, - ) + /// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]]. + pub(crate) async fn is_mod_action(&self, pool: &DbPool) -> Result { + let post_id = self.id.clone(); + let old_post = blocking(pool, move |conn| { + Post::read_from_apub_id(conn, &post_id.into()) + }) .await?; - check_object_for_community_or_site_ban(page, post.community_id, context, request_counter) - .await?; - Ok(post) + + let is_mod_action = if let Ok(old_post) = old_post { + self.stickied != Some(old_post.stickied) || self.comments_enabled != Some(!old_post.locked) + } else { + false + }; + Ok(is_mod_action) + } + + pub(crate) async fn verify( + &self, + _context: &LemmyContext, + _request_counter: &mut i32, + ) -> Result<(), LemmyError> { + check_slurs(&self.name)?; + verify_domains_match(&self.attributed_to, &self.id)?; + Ok(()) } } #[async_trait::async_trait(?Send)] -impl FromApubToForm for PostForm { +impl FromApub for Post { + type ApubType = Page; + async fn from_apub( - page: &PageExt, + page: &Page, context: &LemmyContext, - expected_domain: Url, + _expected_domain: Url, request_counter: &mut i32, - mod_action_allowed: bool, - ) -> Result { - let community = get_community_from_to_or_cc(page, context, request_counter).await?; - let ap_id = if mod_action_allowed { - let id = page.id_unchecked().context(location_info!())?; - check_is_apub_id_valid(id, community.local)?; - id.to_owned().into() - } else { - check_object_domain(page, expected_domain, community.local)? - }; - let ext = &page.ext_one; - let creator_actor_id = page - .inner - .attributed_to() - .as_ref() - .context(location_info!())? - .as_single_xsd_any_uri() - .context(location_info!())?; - + _mod_action_allowed: bool, + ) -> Result { let creator = - get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?; - - let thumbnail_url: Option = match &page.inner.image() { - Some(any_image) => Image::from_any_base( - any_image - .to_owned() - .as_one() - .context(location_info!())? - .to_owned(), - )? - .context(location_info!())? - .url() - .context(location_info!())? - .as_single_xsd_any_uri() - .map(|url| url.to_owned()), - None => None, - }; - let url = page - .inner - .url() - .map(|u| u.as_single_xsd_any_uri()) - .flatten() - .map(|u| u.to_owned()); + get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?; + let community = extract_community(&page.to, context, request_counter).await?; + let thumbnail_url: Option = page.image.clone().map(|i| i.url); let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = - if let Some(url) = &url { + if let Some(url) = &page.url { fetch_iframely_and_pictrs_data(context.client(), Some(url)).await } else { (None, None, None, thumbnail_url) }; - let name = page - .inner - .name() - // 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()) - .context(location_info!())? - .as_single_xsd_string() - .context(location_info!())? - .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 { - name, - url: url.map(|u| u.into()), + let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content)); + let form = PostForm { + name: page.name.clone(), + url: page.url.clone().map(|u| u.into()), body: body_slurs_removed, creator_id: creator.id, community_id: community.id, removed: None, - locked: ext.comments_enabled.map(|e| !e), - published: page - .inner - .published() - .as_ref() - .map(|u| u.to_owned().naive_local()), - updated: page - .inner - .updated() - .as_ref() - .map(|u| u.to_owned().naive_local()), + locked: page.comments_enabled.map(|e| !e), + published: Some(page.published.naive_local()), + updated: page.updated.map(|u| u.naive_local()), deleted: None, - nsfw: ext.sensitive, - stickied: ext.stickied, + nsfw: page.sensitive, + stickied: page.stickied, embed_title: iframely_title, embed_description: iframely_description, embed_html: iframely_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ap_id: Some(ap_id), + ap_id: Some(page.id.clone().into()), local: Some(false), - }) + }; + Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??) } } diff --git a/crates/apub_lib/src/lib.rs b/crates/apub_lib/src/lib.rs index 66bba9f4..81564572 100644 --- a/crates/apub_lib/src/lib.rs +++ b/crates/apub_lib/src/lib.rs @@ -20,7 +20,7 @@ pub enum PublicUrl { pub struct ActivityCommonFields { #[serde(rename = "@context")] pub context: OneOrMany, - id: Url, + pub id: Url, pub actor: Url, // unparsed fields