diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs index 84e0c849e..a3392f550 100644 --- a/crates/api/src/post.rs +++ b/crates/api/src/post.rs @@ -9,7 +9,10 @@ use lemmy_api_common::{ mark_post_as_read, post::*, }; -use lemmy_apub::{activities::post::update::UpdatePost, ApubLikeableType}; +use lemmy_apub::{ + activities::{post::create_or_update::CreateOrUpdatePost, CreateOrUpdateType}, + 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,7 +143,13 @@ impl Perform for LockPost { blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??; // apub updates - UpdatePost::send(&updated_post, &local_user_view.person, context).await?; + CreateOrUpdatePost::send( + &updated_post, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; // Refetch the post let post_id = data.post_id; @@ -212,7 +221,13 @@ impl Perform for StickyPost { // Apub updates // TODO stickied should pry work like locked for ease of use - UpdatePost::send(&updated_post, &local_user_view.person, context).await?; + CreateOrUpdatePost::send( + &updated_post, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; // Refetch the post let post_id = data.post_id; diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 7923b0836..1b772cd84 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -8,7 +8,12 @@ use lemmy_api_common::{ get_post, send_local_notifs, }; -use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; +use lemmy_apub::{ + activities::{comment::create_or_update::CreateOrUpdateComment, CreateOrUpdateType}, + generate_apub_endpoint, + ApubLikeableType, + EndpointType, +}; use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable}; use lemmy_db_schema::source::comment::*; use lemmy_db_views::comment_view::CommentView; @@ -83,9 +88,13 @@ impl PerformCrud for CreateComment { .await? .map_err(|_| ApiError::err("couldnt_create_comment"))?; - updated_comment - .send_create(&local_user_view.person, context) - .await?; + CreateOrUpdateComment::send( + &updated_comment, + &local_user_view.person, + CreateOrUpdateType::Create, + context, + ) + .await?; // Scan the comment for user mentions, add those rows let post_id = post.id; diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index dbdb1a6de..d0a0e3971 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -7,7 +7,10 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, send_local_notifs, }; -use lemmy_apub::ApubObjectType; +use lemmy_apub::activities::{ + comment::create_or_update::CreateOrUpdateComment, + CreateOrUpdateType, +}; use lemmy_db_queries::{source::comment::Comment_, DeleteableOrRemoveable}; use lemmy_db_schema::source::comment::*; use lemmy_db_views::comment_view::CommentView; @@ -59,9 +62,13 @@ impl PerformCrud for EditComment { .map_err(|_| ApiError::err("couldnt_update_comment"))?; // Send the apub update - updated_comment - .send_update(&local_user_view.person, context) - .await?; + CreateOrUpdateComment::send( + &updated_comment, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; // Do the mentions / recipients let updated_comment_content = updated_comment.content.to_owned(); diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index b2034a367..813c62ce0 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -8,7 +8,7 @@ use lemmy_api_common::{ post::*, }; use lemmy_apub::{ - activities::post::create::CreatePost as CreateApubPost, + activities::{post::create_or_update::CreateOrUpdatePost, CreateOrUpdateType}, generate_apub_endpoint, ApubLikeableType, EndpointType, @@ -87,7 +87,13 @@ impl PerformCrud for CreatePost { .await? .map_err(|_| ApiError::err("couldnt_create_post"))?; - CreateApubPost::send(&updated_post, &local_user_view.person, context).await?; + CreateOrUpdatePost::send( + &updated_post, + &local_user_view.person, + CreateOrUpdateType::Create, + 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 6b2780376..dade6f42b 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::activities::post::update::UpdatePost; +use lemmy_apub::activities::{post::create_or_update::CreateOrUpdatePost, CreateOrUpdateType}; 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,7 +89,13 @@ impl PerformCrud for EditPost { }; // Send apub update - UpdatePost::send(&updated_post, &local_user_view.person, context).await?; + CreateOrUpdatePost::send( + &updated_post, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; let post_id = data.post_id; let mut post_view = blocking(context.pool(), move |conn| { diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 3e3074ad8..1da6466df 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -6,7 +6,14 @@ use lemmy_api_common::{ person::{CreatePrivateMessage, PrivateMessageResponse}, send_email_to_user, }; -use lemmy_apub::{generate_apub_endpoint, ApubObjectType, EndpointType}; +use lemmy_apub::{ + activities::{ + private_message::create_or_update::CreateOrUpdatePrivateMessage, + CreateOrUpdateType, + }, + generate_apub_endpoint, + EndpointType, +}; use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; use lemmy_db_schema::source::private_message::{PrivateMessage, PrivateMessageForm}; use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; @@ -63,9 +70,13 @@ impl PerformCrud for CreatePrivateMessage { .await? .map_err(|_| ApiError::err("couldnt_create_private_message"))?; - updated_private_message - .send_create(&local_user_view.person, context) - .await?; + CreateOrUpdatePrivateMessage::send( + &updated_private_message, + &local_user_view.person, + CreateOrUpdateType::Create, + context, + ) + .await?; let private_message_view = blocking(context.pool(), move |conn| { PrivateMessageView::read(conn, inserted_private_message.id) diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index 33c29d00d..5f5f7b845 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -5,7 +5,10 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, person::{EditPrivateMessage, PrivateMessageResponse}, }; -use lemmy_apub::ApubObjectType; +use lemmy_apub::activities::{ + private_message::create_or_update::CreateOrUpdatePrivateMessage, + CreateOrUpdateType, +}; use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud, DeleteableOrRemoveable}; use lemmy_db_schema::source::private_message::PrivateMessage; use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; @@ -44,9 +47,13 @@ impl PerformCrud for EditPrivateMessage { .map_err(|_| ApiError::err("couldnt_update_private_message"))?; // Send the apub update - updated_private_message - .send_update(&local_user_view.person, context) - .await?; + CreateOrUpdatePrivateMessage::send( + &updated_private_message, + &local_user_view.person, + CreateOrUpdateType::Update, + context, + ) + .await?; let private_message_id = data.private_message_id; let mut private_message_view = blocking(context.pool(), move |conn| { diff --git a/crates/apub/src/activities/comment/create.rs b/crates/apub/src/activities/comment/create.rs deleted file mode 100644 index 0e9472194..000000000 --- a/crates/apub/src/activities/comment/create.rs +++ /dev/null @@ -1,86 +0,0 @@ -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}; -use lemmy_apub_lib::{ - values::PublicUrl, - verify_domains_match_opt, - ActivityCommonFields, - ActivityHandler, -}; -use lemmy_db_schema::source::comment::Comment; -use lemmy_utils::LemmyError; -use lemmy_websocket::{LemmyContext, UserOperationCrud}; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateComment { - to: PublicUrl, - object: NoteExt, - cc: Vec, - #[serde(rename = "type")] - kind: CreateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for CreateComment { - async fn verify( - &self, - 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, - &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) - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let comment = Comment::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - false, - ) - .await?; - let recipients = - get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; - send_websocket_message( - comment.id, - recipients, - UserOperationCrud::CreateComment, - context, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/comment/create_or_update.rs b/crates/apub/src/activities/comment/create_or_update.rs new file mode 100644 index 000000000..4cafafbaa --- /dev/null +++ b/crates/apub/src/activities/comment/create_or_update.rs @@ -0,0 +1,131 @@ +use crate::{ + activities::{ + comment::{collect_non_local_mentions, get_notif_recipients, send_websocket_message}, + community::announce::AnnouncableActivities, + extract_community, + generate_activity_id, + verify_activity, + verify_person_in_community, + CreateOrUpdateType, + }, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, + objects::{comment::Note, FromApub, ToApub}, + ActorType, +}; +use activitystreams::link::Mention; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{ + values::PublicUrl, + verify_domains_match, + ActivityCommonFields, + ActivityHandler, +}; +use lemmy_db_queries::Crud; +use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateOrUpdateComment { + to: PublicUrl, + object: Note, + cc: Vec, + tag: Vec, + #[serde(rename = "type")] + kind: CreateOrUpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +impl CreateOrUpdateComment { + pub async fn send( + comment: &Comment, + actor: &Person, + kind: CreateOrUpdateType, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + // TODO: might be helpful to add a comment method to retrieve community directly + let post_id = comment.post_id; + let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + let community_id = post.community_id; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + let id = generate_activity_id(kind.clone())?; + let maa = collect_non_local_mentions(comment, &community, context).await?; + + let create_or_update = CreateOrUpdateComment { + to: PublicUrl::Public, + object: comment.to_apub(context.pool()).await?, + cc: maa.ccs, + tag: maa.tags, + kind, + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + + let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update); + send_to_community_new(activity, &id, actor, &community, maa.inboxes, context).await + } +} + +#[async_trait::async_trait(?Send)] +impl ActivityHandler for CreateOrUpdateComment { + async fn verify( + &self, + 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, + &community.actor_id(), + context, + request_counter, + ) + .await?; + verify_domains_match(&self.common.actor, &self.object.id)?; + // TODO: should add a check that the correct community is in cc (probably needs changes to + // comment deserialization) + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let comment = Comment::from_apub( + &self.object, + context, + self.common.actor.clone(), + request_counter, + false, + ) + .await?; + let recipients = + get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; + let notif_type = match self.kind { + CreateOrUpdateType::Create => UserOperationCrud::CreateComment, + CreateOrUpdateType::Update => UserOperationCrud::EditComment, + }; + send_websocket_message(comment.id, recipients, notif_type, context).await + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub/src/activities/comment/mod.rs b/crates/apub/src/activities/comment/mod.rs index 292d57bf4..e7499718c 100644 --- a/crates/apub/src/activities/comment/mod.rs +++ b/crates/apub/src/activities/comment/mod.rs @@ -1,18 +1,30 @@ -use crate::fetcher::person::get_or_fetch_and_upsert_person; -use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs}; -use lemmy_db_queries::Crud; +use crate::{fetcher::person::get_or_fetch_and_upsert_person, ActorType}; +use activitystreams::{ + base::BaseExt, + link::{LinkExt, Mention}, +}; +use anyhow::anyhow; +use itertools::Itertools; +use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs, WebFingerResponse}; +use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::{ - source::{comment::Comment, post::Post}, + source::{comment::Comment, community::Community, person::Person, post::Post}, CommentId, LocalUserId, }; use lemmy_db_views::comment_view::CommentView; -use lemmy_utils::{utils::scrape_text_for_mentions, LemmyError}; +use lemmy_utils::{ + request::{retry, RecvError}, + settings::structs::Settings, + utils::{scrape_text_for_mentions, MentionData}, + LemmyError, +}; use lemmy_websocket::{messages::SendComment, LemmyContext}; +use log::debug; +use reqwest::Client; use url::Url; -pub mod create; -pub mod update; +pub mod create_or_update; async fn get_notif_recipients( actor: &Url, @@ -63,3 +75,104 @@ pub(crate) async fn send_websocket_message< Ok(()) } + +pub struct MentionsAndAddresses { + pub ccs: Vec, + pub inboxes: Vec, + pub tags: Vec, +} + +/// This takes a comment, and builds a list of to_addresses, inboxes, +/// and mention tags, so they know where to be sent to. +/// Addresses are the persons / addresses that go in the cc field. +pub async fn collect_non_local_mentions( + comment: &Comment, + community: &Community, + context: &LemmyContext, +) -> Result { + let parent_creator = get_comment_parent_creator(context.pool(), comment).await?; + let mut addressed_ccs = vec![community.actor_id(), parent_creator.actor_id()]; + // Note: dont include community inbox here, as we send to it separately with `send_to_community()` + let mut inboxes = vec![parent_creator.get_shared_inbox_or_inbox_url()]; + + // Add the mention tag + let mut tags = Vec::new(); + + // Get the person IDs for any mentions + let mentions = scrape_text_for_mentions(&comment.content) + .into_iter() + // Filter only the non-local ones + .filter(|m| !m.is_local()) + .collect::>(); + + for mention in &mentions { + // TODO should it be fetching it every time? + if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await { + debug!("mention actor_id: {}", actor_id); + addressed_ccs.push(actor_id.to_owned().to_string().parse()?); + + let mention_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?; + inboxes.push(mention_person.get_shared_inbox_or_inbox_url()); + + let mut mention_tag = Mention::new(); + mention_tag.set_href(actor_id).set_name(mention.full_name()); + tags.push(mention_tag); + } + } + + let inboxes = inboxes.into_iter().unique().collect(); + + Ok(MentionsAndAddresses { + ccs: addressed_ccs, + inboxes, + tags, + }) +} + +/// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a +/// top-level comment, the creator of the post, otherwise the creator of the parent comment. +async fn get_comment_parent_creator( + pool: &DbPool, + comment: &Comment, +) -> Result { + let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id { + let parent_comment = + blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??; + parent_comment.creator_id + } else { + let parent_post_id = comment.post_id; + let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; + parent_post.creator_id + }; + Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??) +} + +/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`, +/// using webfinger. +async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result { + let fetch_url = format!( + "{}://{}/.well-known/webfinger?resource=acct:{}@{}", + Settings::get().get_protocol_string(), + mention.domain, + mention.name, + mention.domain + ); + debug!("Fetching webfinger url: {}", &fetch_url); + + let response = retry(|| client.get(&fetch_url).send()).await?; + + let res: WebFingerResponse = response + .json() + .await + .map_err(|e| RecvError(e.to_string()))?; + + let link = res + .links + .iter() + .find(|l| l.type_.eq(&Some("application/activity+json".to_string()))) + .ok_or_else(|| anyhow!("No application/activity+json link found."))?; + link + .href + .to_owned() + .ok_or_else(|| anyhow!("No href found.").into()) +} diff --git a/crates/apub/src/activities/comment/remove.rs b/crates/apub/src/activities/comment/remove.rs deleted file mode 100644 index 5702f9fa7..000000000 --- a/crates/apub/src/activities/comment/remove.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{ - activities::{comment::send_websocket_message, verify_mod_action}, - check_is_apub_id_valid, - fetcher::objects::get_or_fetch_and_insert_comment, -}; -use activitystreams::activity::kind::RemoveType; -use lemmy_api_common::blocking; -use lemmy_apub_lib::{ - values::PublicUrl, - verify_domains_match, - ActivityCommonFields, - ActivityHandlerNew, -}; -use lemmy_db_queries::source::comment::Comment_; -use lemmy_db_schema::source::comment::Comment; -use lemmy_utils::LemmyError; -use lemmy_websocket::{LemmyContext, UserOperationCrud}; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoveComment { - to: PublicUrl, - pub(in crate::activities::comment) object: Url, - cc: [Url; 1], - #[serde(rename = "type")] - kind: RemoveType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandlerNew for RemoveComment { - async fn verify(&self, context: &LemmyContext, _: &mut i32) -> Result<(), LemmyError> { - verify_domains_match(&self.common.actor, self.common.id_unchecked())?; - check_is_apub_id_valid(&self.common.actor, false)?; - verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let comment = get_or_fetch_and_insert_comment(&self.object, context, request_counter).await?; - - let removed_comment = blocking(context.pool(), move |conn| { - Comment::update_removed(conn, comment.id, true) - }) - .await??; - - send_websocket_message( - removed_comment.id, - vec![], - UserOperationCrud::EditComment, - context, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/comment/undo_remove.rs b/crates/apub/src/activities/comment/undo_remove.rs deleted file mode 100644 index 07aa67119..000000000 --- a/crates/apub/src/activities/comment/undo_remove.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{ - activities::{ - comment::{remove::RemoveComment, send_websocket_message}, - verify_mod_action, - }, - check_is_apub_id_valid, - fetcher::objects::get_or_fetch_and_insert_comment, -}; -use activitystreams::activity::kind::UndoType; -use lemmy_api_common::blocking; -use lemmy_apub_lib::{ - values::PublicUrl, - verify_domains_match, - ActivityCommonFields, - ActivityHandlerNew, -}; -use lemmy_db_queries::source::comment::Comment_; -use lemmy_db_schema::source::comment::Comment; -use lemmy_utils::LemmyError; -use lemmy_websocket::{LemmyContext, UserOperationCrud}; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UndoRemoveComment { - to: PublicUrl, - object: RemoveComment, - cc: [Url; 1], - #[serde(rename = "type")] - kind: UndoType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandlerNew for UndoRemoveComment { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_domains_match(&self.common.actor, self.common.id_unchecked())?; - check_is_apub_id_valid(&self.common.actor, false)?; - verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?; - self.object.verify(context, request_counter).await - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let comment = - get_or_fetch_and_insert_comment(&self.object.object, context, request_counter).await?; - - let removed_comment = blocking(context.pool(), move |conn| { - Comment::update_removed(conn, comment.id, false) - }) - .await??; - - send_websocket_message( - removed_comment.id, - vec![], - UserOperationCrud::EditComment, - context, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/comment/update.rs b/crates/apub/src/activities/comment/update.rs deleted file mode 100644 index c0d014813..000000000 --- a/crates/apub/src/activities/comment/update.rs +++ /dev/null @@ -1,85 +0,0 @@ -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}; -use lemmy_apub_lib::{ - values::PublicUrl, - verify_domains_match_opt, - ActivityCommonFields, - ActivityHandler, -}; -use lemmy_db_schema::source::comment::Comment; -use lemmy_utils::LemmyError; -use lemmy_websocket::{LemmyContext, UserOperationCrud}; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateComment { - to: PublicUrl, - object: NoteExt, - cc: Vec, - #[serde(rename = "type")] - kind: UpdateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for UpdateComment { - async fn verify( - &self, - 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, - &community.actor_id(), - context, - request_counter, - ) - .await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let comment = Comment::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - false, - ) - .await?; - - let recipients = - get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?; - send_websocket_message( - comment.id, - recipients, - UserOperationCrud::EditComment, - context, - ) - .await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index bc72d80fe..b72cd1a5c 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -1,6 +1,6 @@ use crate::{ activities::{ - comment::{create::CreateComment, update::UpdateComment}, + comment::create_or_update::CreateOrUpdateComment, community::{ add_mod::AddMod, block_user::BlockUserFromCommunity, @@ -12,7 +12,7 @@ use crate::{ undo_delete::UndoDeletePostCommentOrCommunity, }, generate_activity_id, - post::{create::CreatePost, update::UpdatePost}, + post::create_or_update::CreateOrUpdatePost, removal::{ remove::RemovePostCommentCommunityOrMod, undo_remove::UndoRemovePostCommentOrCommunity, @@ -44,10 +44,8 @@ use url::Url; #[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)] #[serde(untagged)] pub enum AnnouncableActivities { - CreateComment(CreateComment), - UpdateComment(UpdateComment), - CreatePost(CreatePost), - UpdatePost(UpdatePost), + CreateOrUpdateComment(CreateOrUpdateComment), + CreateOrUpdatePost(Box), LikePostOrComment(LikePostOrComment), DislikePostOrComment(DislikePostOrComment), UndoLikePostOrComment(UndoLikePostOrComment), @@ -87,7 +85,7 @@ impl AnnounceActivity { kind: AnnounceType::Announce, common: ActivityCommonFields { context: lemmy_context(), - id: generate_activity_id(AnnounceType::Announce)?, + id: generate_activity_id(&AnnounceType::Announce)?, actor: community.actor_id(), unparsed: Default::default(), }, diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index af133167f..f4c535ee6 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -15,6 +15,8 @@ use lemmy_db_schema::{ use lemmy_db_views_actor::community_view::CommunityView; use lemmy_utils::{settings::structs::Settings, LemmyError}; use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; +use strum_macros::ToString; use url::{ParseError, Url}; use uuid::Uuid; @@ -28,6 +30,13 @@ pub mod removal; pub mod send; pub mod voting; +#[derive(Clone, Debug, ToString, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum CreateOrUpdateType { + Create, + Update, +} + /// Checks that the specified Url actually identifies a Person (by fetching it), and that the person /// doesn't have a site ban. async fn verify_person( @@ -61,7 +70,7 @@ pub(crate) async fn extract_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( +pub(crate) async fn verify_person_in_community( person_id: &Url, community_id: &Url, context: &LemmyContext, diff --git a/crates/apub/src/activities/post/create.rs b/crates/apub/src/activities/post/create_or_update.rs similarity index 51% rename from crates/apub/src/activities/post/create.rs rename to crates/apub/src/activities/post/create_or_update.rs index 3dd8ce554..4af720753 100644 --- a/crates/apub/src/activities/post/create.rs +++ b/crates/apub/src/activities/post/create_or_update.rs @@ -5,7 +5,9 @@ use crate::{ generate_activity_id, post::send_websocket_message, verify_activity, + verify_mod_action, verify_person_in_community, + CreateOrUpdateType, }, activity_queue::send_to_community_new, extensions::context::lemmy_context, @@ -13,7 +15,6 @@ use crate::{ objects::{post::Page, FromApub, ToApub}, ActorType, }; -use activitystreams::activity::kind::CreateType; use anyhow::anyhow; use lemmy_api_common::blocking; use lemmy_apub_lib::{ @@ -31,29 +32,35 @@ use url::Url; #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] -pub struct CreatePost { +pub struct CreateOrUpdatePost { to: PublicUrl, object: Page, cc: [Url; 1], - r#type: CreateType, + #[serde(rename = "type")] + kind: CreateOrUpdateType, #[serde(flatten)] common: ActivityCommonFields, } -impl CreatePost { - pub async fn send(post: &Post, actor: &Person, context: &LemmyContext) -> Result<(), LemmyError> { +impl CreateOrUpdatePost { + pub async fn send( + post: &Post, + actor: &Person, + kind: CreateOrUpdateType, + 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 { + let id = generate_activity_id(kind.clone())?; + let create_or_update = CreateOrUpdatePost { to: PublicUrl::Public, object: post.to_apub(context.pool()).await?, cc: [community.actor_id()], - r#type: Default::default(), + kind, common: ActivityCommonFields { context: lemmy_context(), id: id.clone(), @@ -62,33 +69,45 @@ impl CreatePost { }, }; - let activity = AnnouncableActivities::CreatePost(create); + let activity = AnnouncableActivities::CreateOrUpdatePost(Box::new(create_or_update)); send_to_community_new(activity, &id, actor, &community, vec![], context).await } } #[async_trait::async_trait(?Send)] -impl ActivityHandler for CreatePost { +impl ActivityHandler for CreateOrUpdatePost { async fn verify( &self, 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, 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()); + let community = extract_community(&self.cc, context, request_counter).await?; + let community_id = community.actor_id(); + verify_person_in_community(&self.common.actor, &community_id, context, request_counter).await?; + match self.kind { + CreateOrUpdateType::Create => { + 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()); + } + } + CreateOrUpdateType::Update => { + let is_mod_action = self.object.is_mod_action(context.pool()).await?; + if is_mod_action { + verify_mod_action(&self.common.actor, community_id, context).await?; + } else { + verify_domains_match(&self.common.actor, &self.object.id)?; + verify_urls_match(&self.common.actor, &self.object.attributed_to)?; + } + } } self.object.verify(context, request_counter).await?; Ok(()) @@ -110,7 +129,11 @@ impl ActivityHandler for CreatePost { ) .await?; - send_websocket_message(post.id, UserOperationCrud::CreatePost, context).await + let notif_type = match self.kind { + CreateOrUpdateType::Create => UserOperationCrud::CreatePost, + CreateOrUpdateType::Update => UserOperationCrud::EditPost, + }; + send_websocket_message(post.id, notif_type, context).await } fn common(&self) -> &ActivityCommonFields { diff --git a/crates/apub/src/activities/post/mod.rs b/crates/apub/src/activities/post/mod.rs index a46620402..b60348ed0 100644 --- a/crates/apub/src/activities/post/mod.rs +++ b/crates/apub/src/activities/post/mod.rs @@ -4,8 +4,7 @@ use lemmy_db_views::post_view::PostView; use lemmy_utils::LemmyError; use lemmy_websocket::{messages::SendPost, LemmyContext}; -pub mod create; -pub mod update; +pub mod create_or_update; pub(crate) async fn send_websocket_message< OP: ToString + Send + lemmy_websocket::OperationType + 'static, diff --git a/crates/apub/src/activities/post/update.rs b/crates/apub/src/activities/post/update.rs deleted file mode 100644 index f5cd07b9f..000000000 --- a/crates/apub/src/activities/post/update.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::{ - activities::{ - community::announce::AnnouncableActivities, - generate_activity_id, - post::send_websocket_message, - verify_activity, - verify_mod_action, - verify_person_in_community, - }, - activity_queue::send_to_community_new, - extensions::context::lemmy_context, - fetcher::community::get_or_fetch_and_upsert_community, - objects::{post::Page, FromApub, ToApub}, - ActorType, -}; -use activitystreams::activity::kind::UpdateType; -use lemmy_api_common::blocking; -use lemmy_apub_lib::{values::PublicUrl, verify_urls_match, ActivityCommonFields, ActivityHandler}; -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; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdatePost { - to: PublicUrl, - 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(), - 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( - &self, - 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())?; - 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)?; - } - self.object.verify(context, request_counter).await?; - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let post = Post::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - // TODO: we already check here if the mod action is valid, can remove that check param - true, - ) - .await?; - - send_websocket_message(post.id, UserOperationCrud::EditPost, context).await - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/private_message/create.rs b/crates/apub/src/activities/private_message/create.rs deleted file mode 100644 index 5e6db4ea8..000000000 --- a/crates/apub/src/activities/private_message/create.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{ - activities::{private_message::send_websocket_message, verify_activity, verify_person}, - objects::FromApub, - NoteExt, -}; -use activitystreams::{activity::kind::CreateType, base::BaseExt}; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler}; -use lemmy_db_schema::source::private_message::PrivateMessage; -use lemmy_utils::LemmyError; -use lemmy_websocket::{LemmyContext, UserOperationCrud}; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreatePrivateMessage { - to: Url, - object: NoteExt, - #[serde(rename = "type")] - kind: CreateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for CreatePrivateMessage { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person(&self.common.actor, context, request_counter).await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let private_message = PrivateMessage::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - false, - ) - .await?; - - send_websocket_message( - private_message.id, - UserOperationCrud::CreatePrivateMessage, - context, - ) - .await?; - - Ok(()) - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/private_message/create_or_update.rs b/crates/apub/src/activities/private_message/create_or_update.rs new file mode 100644 index 000000000..05f3c98f2 --- /dev/null +++ b/crates/apub/src/activities/private_message/create_or_update.rs @@ -0,0 +1,107 @@ +use crate::{ + activities::{ + generate_activity_id, + private_message::send_websocket_message, + verify_activity, + verify_person, + CreateOrUpdateType, + }, + activity_queue::send_activity_new, + extensions::context::lemmy_context, + objects::{private_message::Note, FromApub, ToApub}, + ActorType, +}; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandler}; +use lemmy_db_queries::Crud; +use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage}; +use lemmy_utils::LemmyError; +use lemmy_websocket::{LemmyContext, UserOperationCrud}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateOrUpdatePrivateMessage { + to: Url, + object: Note, + #[serde(rename = "type")] + kind: CreateOrUpdateType, + #[serde(flatten)] + common: ActivityCommonFields, +} + +impl CreateOrUpdatePrivateMessage { + pub async fn send( + private_message: &PrivateMessage, + actor: &Person, + kind: CreateOrUpdateType, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + let recipient_id = private_message.recipient_id; + let recipient = + blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??; + + let id = generate_activity_id(kind.clone())?; + let create_or_update = CreateOrUpdatePrivateMessage { + to: recipient.actor_id(), + object: private_message.to_apub(context.pool()).await?, + kind, + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + send_activity_new( + context, + &create_or_update, + &id, + actor, + vec![recipient.get_shared_inbox_or_inbox_url()], + true, + ) + .await + } +} +#[async_trait::async_trait(?Send)] +impl ActivityHandler for CreateOrUpdatePrivateMessage { + async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_activity(self.common())?; + verify_person(&self.common.actor, context, request_counter).await?; + verify_domains_match(&self.common.actor, &self.object.id)?; + self.object.verify(context, request_counter).await?; + Ok(()) + } + + async fn receive( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let private_message = PrivateMessage::from_apub( + &self.object, + context, + self.common.actor.clone(), + request_counter, + false, + ) + .await?; + + let notif_type = match self.kind { + CreateOrUpdateType::Create => UserOperationCrud::CreatePrivateMessage, + CreateOrUpdateType::Update => UserOperationCrud::EditPrivateMessage, + }; + send_websocket_message(private_message.id, notif_type, context).await?; + + Ok(()) + } + + fn common(&self) -> &ActivityCommonFields { + &self.common + } +} diff --git a/crates/apub/src/activities/private_message/mod.rs b/crates/apub/src/activities/private_message/mod.rs index beb28b299..0c0557286 100644 --- a/crates/apub/src/activities/private_message/mod.rs +++ b/crates/apub/src/activities/private_message/mod.rs @@ -4,10 +4,9 @@ use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::Priva use lemmy_utils::LemmyError; use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud}; -pub mod create; +pub mod create_or_update; pub mod delete; pub mod undo_delete; -pub mod update; async fn send_websocket_message( private_message_id: PrivateMessageId, diff --git a/crates/apub/src/activities/private_message/update.rs b/crates/apub/src/activities/private_message/update.rs deleted file mode 100644 index 72ac0749a..000000000 --- a/crates/apub/src/activities/private_message/update.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{ - activities::{private_message::send_websocket_message, verify_activity, verify_person}, - objects::FromApub, - NoteExt, -}; -use activitystreams::{activity::kind::UpdateType, base::BaseExt}; -use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler}; -use lemmy_db_schema::source::private_message::PrivateMessage; -use lemmy_utils::LemmyError; -use lemmy_websocket::{LemmyContext, UserOperationCrud}; -use url::Url; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdatePrivateMessage { - to: Url, - object: NoteExt, - #[serde(rename = "type")] - kind: UpdateType, - #[serde(flatten)] - common: ActivityCommonFields, -} - -#[async_trait::async_trait(?Send)] -impl ActivityHandler for UpdatePrivateMessage { - async fn verify( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - verify_activity(self.common())?; - verify_person(&self.common.actor, context, request_counter).await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; - Ok(()) - } - - async fn receive( - &self, - context: &LemmyContext, - request_counter: &mut i32, - ) -> Result<(), LemmyError> { - let private_message = PrivateMessage::from_apub( - &self.object, - context, - self.common.actor.clone(), - request_counter, - false, - ) - .await?; - - send_websocket_message( - private_message.id, - UserOperationCrud::EditPrivateMessage, - context, - ) - .await?; - - Ok(()) - } - - fn common(&self) -> &ActivityCommonFields { - &self.common - } -} diff --git a/crates/apub/src/activities/send/comment.rs b/crates/apub/src/activities/send/comment.rs index 47fc2a3e0..619d59b1b 100644 --- a/crates/apub/src/activities/send/comment.rs +++ b/crates/apub/src/activities/send/comment.rs @@ -1,112 +1,45 @@ use crate::{ activities::generate_activity_id, - activity_queue::{send_comment_mentions, send_to_community}, + activity_queue::send_to_community, extensions::context::lemmy_context, - fetcher::person::get_or_fetch_and_upsert_person, - 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, }, - base::AnyBase, - link::Mention, prelude::*, public, }; -use anyhow::anyhow; -use itertools::Itertools; -use lemmy_api_common::{blocking, WebFingerResponse}; -use lemmy_db_queries::{Crud, DbPool}; +use lemmy_api_common::blocking; +use lemmy_db_queries::Crud; use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post}; -use lemmy_utils::{ - request::{retry, RecvError}, - settings::structs::Settings, - utils::{scrape_text_for_mentions, MentionData}, - LemmyError, -}; +use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; -use log::debug; -use reqwest::Client; -use serde_json::Error; -use url::Url; #[async_trait::async_trait(?Send)] impl ApubObjectType for Comment { - /// Send out information about a newly created comment, to the followers of the community and - /// mentioned persons. - async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let note = self.to_apub(context.pool()).await?; - - let post_id = self.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - let community_id = post.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let maa = collect_non_local_mentions(self, &community, context).await?; - - let mut create = Create::new( - creator.actor_id.to_owned().into_inner(), - note.into_any_base()?, - ); - create - .set_many_contexts(lemmy_context()) - .set_id(generate_activity_id(CreateType::Create)?) - .set_to(public()) - .set_many_ccs(maa.ccs.to_owned()) - // Set the mention tags - .set_many_tags(maa.get_tags()?); - - send_to_community(create.clone(), creator, &community, None, context).await?; - send_comment_mentions(creator, maa.inboxes, create, 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 and mentioned - /// persons. - async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let note = self.to_apub(context.pool()).await?; - - let post_id = self.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - let community_id = post.community_id; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let maa = collect_non_local_mentions(self, &community, context).await?; - - let mut update = Update::new( - creator.actor_id.to_owned().into_inner(), - note.into_any_base()?, - ); - update - .set_many_contexts(lemmy_context()) - .set_id(generate_activity_id(UpdateType::Update)?) - .set_to(public()) - .set_many_ccs(maa.ccs.to_owned()) - // Set the mention tags - .set_many_tags(maa.get_tags()?); - - send_to_community(update.clone(), creator, &community, None, context).await?; - send_comment_mentions(creator, maa.inboxes, update, 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> { @@ -327,114 +260,3 @@ impl ApubLikeableType for Comment { Ok(()) } } - -struct MentionsAndAddresses { - ccs: Vec, - inboxes: Vec, - tags: Vec, -} - -impl MentionsAndAddresses { - fn get_tags(&self) -> Result, Error> { - self - .tags - .iter() - .map(|t| t.to_owned().into_any_base()) - .collect::, Error>>() - } -} - -/// This takes a comment, and builds a list of to_addresses, inboxes, -/// and mention tags, so they know where to be sent to. -/// Addresses are the persons / addresses that go in the cc field. -async fn collect_non_local_mentions( - comment: &Comment, - community: &Community, - context: &LemmyContext, -) -> Result { - let parent_creator = get_comment_parent_creator(context.pool(), comment).await?; - let mut addressed_ccs = vec![community.actor_id(), parent_creator.actor_id()]; - // Note: dont include community inbox here, as we send to it separately with `send_to_community()` - let mut inboxes = vec![parent_creator.get_shared_inbox_or_inbox_url()]; - - // Add the mention tag - let mut tags = Vec::new(); - - // Get the person IDs for any mentions - let mentions = scrape_text_for_mentions(&comment.content) - .into_iter() - // Filter only the non-local ones - .filter(|m| !m.is_local()) - .collect::>(); - - for mention in &mentions { - // TODO should it be fetching it every time? - if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await { - debug!("mention actor_id: {}", actor_id); - addressed_ccs.push(actor_id.to_owned().to_string().parse()?); - - let mention_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?; - inboxes.push(mention_person.get_shared_inbox_or_inbox_url()); - - let mut mention_tag = Mention::new(); - mention_tag.set_href(actor_id).set_name(mention.full_name()); - tags.push(mention_tag); - } - } - - let inboxes = inboxes.into_iter().unique().collect(); - - Ok(MentionsAndAddresses { - ccs: addressed_ccs, - inboxes, - tags, - }) -} - -/// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a -/// top-level comment, the creator of the post, otherwise the creator of the parent comment. -async fn get_comment_parent_creator( - pool: &DbPool, - comment: &Comment, -) -> Result { - let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id { - let parent_comment = - blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??; - parent_comment.creator_id - } else { - let parent_post_id = comment.post_id; - let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; - parent_post.creator_id - }; - Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??) -} - -/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`, -/// using webfinger. -async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result { - let fetch_url = format!( - "{}://{}/.well-known/webfinger?resource=acct:{}@{}", - Settings::get().get_protocol_string(), - mention.domain, - mention.name, - mention.domain - ); - debug!("Fetching webfinger url: {}", &fetch_url); - - let response = retry(|| client.get(&fetch_url).send()).await?; - - let res: WebFingerResponse = response - .json() - .await - .map_err(|e| RecvError(e.to_string()))?; - - let link = res - .links - .iter() - .find(|l| l.type_.eq(&Some("application/activity+json".to_string()))) - .ok_or_else(|| anyhow!("No application/activity+json link found."))?; - link - .href - .to_owned() - .ok_or_else(|| anyhow!("No href found.").into()) -} diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs index fbc61fc05..852410624 100644 --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@ -54,7 +54,7 @@ impl ActorType for Community { self.local } fn actor_id(&self) -> Url { - self.actor_id.to_owned().into_inner() + self.actor_id.to_owned().into() } fn name(&self) -> String { self.name.clone() @@ -78,7 +78,7 @@ impl ActorType for Community { #[async_trait::async_trait(?Send)] impl CommunityType for Community { fn followers_url(&self) -> Url { - self.followers_url.clone().into_inner() + self.followers_url.clone().into() } /// As a local community, accept the follow request from a remote person. diff --git a/crates/apub/src/activities/send/private_message.rs b/crates/apub/src/activities/send/private_message.rs index 67d0afcd3..7461926c1 100644 --- a/crates/apub/src/activities/send/private_message.rs +++ b/crates/apub/src/activities/send/private_message.rs @@ -2,19 +2,17 @@ use crate::{ activities::generate_activity_id, activity_queue::send_activity_single_dest, extensions::context::lemmy_context, - objects::ToApub, ActorType, ApubObjectType, }; use activitystreams::{ activity::{ - kind::{CreateType, DeleteType, UndoType, UpdateType}, - Create, + kind::{DeleteType, UndoType}, Delete, Undo, - Update, }, - prelude::*, + base::{BaseExt, ExtendsExt}, + object::ObjectExt, }; use lemmy_api_common::blocking; use lemmy_db_queries::Crud; @@ -24,47 +22,20 @@ use lemmy_websocket::LemmyContext; #[async_trait::async_trait(?Send)] impl ApubObjectType for PrivateMessage { - /// Send out information about a newly created private message - async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let note = self.to_apub(context.pool()).await?; - - let recipient_id = self.recipient_id; - let recipient = - blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??; - - let mut create = Create::new( - creator.actor_id.to_owned().into_inner(), - note.into_any_base()?, - ); - - create - .set_many_contexts(lemmy_context()) - .set_id(generate_activity_id(CreateType::Create)?) - .set_to(recipient.actor_id()); - - send_activity_single_dest(create, creator, recipient.inbox_url.into(), context).await?; - Ok(()) + async fn send_create( + &self, + _creator: &Person, + _context: &LemmyContext, + ) -> Result<(), LemmyError> { + unimplemented!() } - /// Send out information about an edited private message, to the followers of the community. - async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> { - let note = self.to_apub(context.pool()).await?; - - let recipient_id = self.recipient_id; - let recipient = - blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??; - - let mut update = Update::new( - creator.actor_id.to_owned().into_inner(), - note.into_any_base()?, - ); - update - .set_many_contexts(lemmy_context()) - .set_id(generate_activity_id(UpdateType::Update)?) - .set_to(recipient.actor_id()); - - send_activity_single_dest(update, creator, recipient.inbox_url.into(), 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/activity_queue.rs b/crates/apub/src/activity_queue.rs index f4f8ce90a..3f8a019fd 100644 --- a/crates/apub/src/activity_queue.rs +++ b/crates/apub/src/activity_queue.rs @@ -138,40 +138,6 @@ where Ok(()) } -/// Sends notification to any persons mentioned in a comment -/// -/// * `creator` person who created the comment -/// * `mentions` list of inboxes of persons which are mentioned in the comment -/// * `activity` either a `Create/Note` or `Update/Note` -pub(crate) async fn send_comment_mentions( - creator: &Person, - mentions: Vec, - activity: T, - context: &LemmyContext, -) -> Result<(), LemmyError> -where - T: AsObject + Extends + Debug + BaseExt, - Kind: Serialize, - >::Error: From + Send + Sync + 'static, -{ - debug!( - "Sending mentions activity {:?} to {:?}", - &activity.id_unchecked(), - &mentions - ); - let mentions = mentions - .iter() - .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok()) - .map(|i| i.to_owned()) - .collect(); - send_activity_internal( - context, activity, creator, mentions, false, // Don't create a new DB row - false, - ) - .await?; - Ok(()) -} - pub(crate) async fn send_to_community_new( activity: AnnouncableActivities, activity_id: &Url, diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs index fd94f6499..a06b99d63 100644 --- a/crates/apub/src/fetcher/objects.rs +++ b/crates/apub/src/fetcher/objects.rs @@ -1,7 +1,6 @@ use crate::{ fetcher::fetch::fetch_remote_object, - objects::{post::Page, FromApub}, - NoteExt, + objects::{comment::Note, post::Page, FromApub}, PostOrComment, }; use anyhow::anyhow; @@ -73,7 +72,7 @@ pub async fn get_or_fetch_and_insert_comment( comment_ap_id ); let comment = - fetch_remote_object::(context.client(), comment_ap_id, recursion_counter).await?; + fetch_remote_object::(context.client(), comment_ap_id, recursion_counter).await?; let comment = Comment::from_apub( &comment, context, diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 69076c151..647d057a6 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -6,9 +6,8 @@ use crate::{ is_deleted, }, find_object_by_id, - objects::{post::Page, FromApub}, + objects::{comment::Note, post::Page, FromApub}, GroupExt, - NoteExt, Object, PersonExt, }; @@ -46,7 +45,7 @@ enum SearchAcceptedObjects { Person(Box), Group(Box), Page(Box), - Comment(Box), + Comment(Box), } /// Attempt to parse the query as URL, and fetch an ActivityPub object from it. diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 6b264fad6..587c6cfb8 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -155,7 +155,7 @@ pub(crate) async fn get_apub_community_moderators( let moderators: Vec = moderators .into_iter() - .map(|m| m.moderator.actor_id.into_inner()) + .map(|m| m.moderator.actor_id.into()) .collect(); let mut collection = OrderedCollection::new(); collection diff --git a/crates/apub/src/http/inbox_enums.rs b/crates/apub/src/http/inbox_enums.rs index 0f31f534e..f8ee0cb21 100644 --- a/crates/apub/src/http/inbox_enums.rs +++ b/crates/apub/src/http/inbox_enums.rs @@ -1,5 +1,5 @@ use crate::activities::{ - comment::{create::CreateComment, update::UpdateComment}, + comment::create_or_update::CreateOrUpdateComment, community::{ add_mod::AddMod, announce::AnnounceActivity, @@ -9,12 +9,11 @@ use crate::activities::{ }, deletion::{delete::DeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity}, following::{accept::AcceptFollowCommunity, follow::FollowCommunity, undo::UndoFollowCommunity}, - post::{create::CreatePost, update::UpdatePost}, + post::create_or_update::CreateOrUpdatePost, private_message::{ - create::CreatePrivateMessage, + create_or_update::CreateOrUpdatePrivateMessage, delete::DeletePrivateMessage, undo_delete::UndoDeletePrivateMessage, - update::UpdatePrivateMessage, }, removal::{ remove::RemovePostCommentCommunityOrMod, @@ -36,8 +35,7 @@ use serde::{Deserialize, Serialize}; #[serde(untagged)] pub enum PersonInboxActivities { AcceptFollowCommunity(AcceptFollowCommunity), - CreatePrivateMessage(CreatePrivateMessage), - UpdatePrivateMessage(UpdatePrivateMessage), + CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage), DeletePrivateMessage(DeletePrivateMessage), UndoDeletePrivateMessage(UndoDeletePrivateMessage), AnnounceActivity(Box), @@ -48,10 +46,8 @@ pub enum PersonInboxActivities { pub enum GroupInboxActivities { FollowCommunity(FollowCommunity), UndoFollowCommunity(UndoFollowCommunity), - CreateComment(CreateComment), - UpdateComment(UpdateComment), - CreatePost(CreatePost), - UpdatePost(UpdatePost), + CreateOrUpdateComment(CreateOrUpdateComment), + CreateOrUpdatePost(Box), LikePostOrComment(LikePostOrComment), DislikePostOrComment(DislikePostOrComment), UndoLikePostOrComment(UndoLikePostOrComment), @@ -72,10 +68,8 @@ pub enum SharedInboxActivities { // received by group FollowCommunity(FollowCommunity), UndoFollowCommunity(UndoFollowCommunity), - CreateComment(CreateComment), - UpdateComment(UpdateComment), - CreatePost(CreatePost), - UpdatePost(UpdatePost), + CreateOrUpdateComment(CreateOrUpdateComment), + CreateOrUpdatePost(Box), LikePostOrComment(LikePostOrComment), DislikePostOrComment(DislikePostOrComment), UndoDislikePostOrComment(UndoDislikePostOrComment), @@ -92,8 +86,7 @@ pub enum SharedInboxActivities { AcceptFollowCommunity(AcceptFollowCommunity), // Note, pm activities need to be at the end, otherwise comments will end up here. We can probably // avoid this problem by replacing createpm.object with our own struct, instead of NoteExt. - CreatePrivateMessage(CreatePrivateMessage), - UpdatePrivateMessage(UpdatePrivateMessage), + CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage), DeletePrivateMessage(DeletePrivateMessage), UndoDeletePrivateMessage(UndoDeletePrivateMessage), AnnounceActivity(Box), diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 94da33cc6..16773aae5 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -20,7 +20,7 @@ use activitystreams::{ activity::Follow, actor, base::AnyBase, - object::{ApObject, AsObject, Note, ObjectExt}, + object::{ApObject, AsObject, ObjectExt}, }; use activitystreams_ext::Ext2; use anyhow::{anyhow, Context}; @@ -53,7 +53,6 @@ pub type GroupExt = type PersonExt = Ext2>>, PersonExtension, PublicKeyExtension>; pub type SiteExt = actor::ApActor>; -pub type NoteExt = ApObject; #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)] pub enum UserTypes { @@ -314,7 +313,7 @@ pub fn generate_inbox_url(actor_id: &DbUrl) -> Result { } pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result { - let actor_id = actor_id.clone().into_inner(); + let actor_id: Url = actor_id.clone().into(); let url = format!( "{}://{}{}/inbox", &actor_id.scheme(), diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 8501ea2a7..c97ea994c 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,29 +1,24 @@ use crate::{ + activities::verify_person_in_community, extensions::context::lemmy_context, fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, - get_community_from_to_or_cc, - objects::{ - check_object_domain, - check_object_for_community_or_site_ban, - create_tombstone, - get_object_from_apub, - get_or_fetch_and_upsert_person, - get_source_markdown_value, - set_content_and_source, - FromApub, - FromApubToForm, - ToApub, - }, - NoteExt, + objects::{create_tombstone, get_or_fetch_and_upsert_person, FromApub, Source, ToApub}, + ActorType, }; use activitystreams::{ - object::{kind::NoteType, ApObject, Note, Tombstone}, - prelude::*, - public, + base::AnyBase, + object::{kind::NoteType, Tombstone}, + primitives::OneOrMany, + unparsed::Unparsed, }; use anyhow::{anyhow, Context}; +use chrono::{DateTime, FixedOffset}; use lemmy_api_common::blocking; -use lemmy_db_queries::{Crud, DbPool}; +use lemmy_apub_lib::{ + values::{MediaTypeHtml, MediaTypeMarkdown, PublicUrl}, + verify_domains_match, +}; +use lemmy_db_queries::{ApubObject, Crud, DbPool}; use lemmy_db_schema::{ source::{ comment::{Comment, CommentForm}, @@ -39,24 +34,103 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; use url::Url; +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Note { + #[serde(rename = "@context")] + context: OneOrMany, + r#type: NoteType, + pub(crate) id: Url, + pub(crate) attributed_to: Url, + /// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain + /// the community ID, as it would be incompatible with Pleroma (and we can get the community from + /// the post in [`in_reply_to`]). + to: PublicUrl, + content: String, + media_type: MediaTypeHtml, + source: Source, + in_reply_to: Vec, + published: DateTime, + updated: Option>, + #[serde(flatten)] + unparsed: Unparsed, +} + +impl Note { + async fn get_parents( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(Post, Option), LemmyError> { + // This post, or the parent comment might not yet exist on this server yet, fetch them. + let post_id = self.in_reply_to.get(0).context(location_info!())?; + let post = Box::pin(get_or_fetch_and_insert_post( + post_id, + context, + request_counter, + )) + .await?; + + // The 2nd item, if it exists, is the parent comment apub_id + // Nested comments will automatically get fetched recursively + let parent_id: Option = match self.in_reply_to.get(1) { + Some(parent_comment_uri) => { + let parent_comment = Box::pin(get_or_fetch_and_insert_comment( + parent_comment_uri, + context, + request_counter, + )) + .await?; + + Some(parent_comment.id) + } + None => None, + }; + + Ok((post, parent_id)) + } + + pub(crate) async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let (post, _parent_comment_id) = self.get_parents(context, request_counter).await?; + let community_id = post.community_id; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + if post.locked { + return Err(anyhow!("Post is locked").into()); + } + verify_domains_match(&self.attributed_to, &self.id)?; + verify_person_in_community( + &self.attributed_to, + &community.actor_id(), + context, + request_counter, + ) + .await?; + Ok(()) + } +} + #[async_trait::async_trait(?Send)] impl ToApub for Comment { - type ApubType = NoteExt; - - async fn to_apub(&self, pool: &DbPool) -> Result { - let mut comment = ApObject::new(Note::new()); + type ApubType = Note; + 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 post_id = self.post_id; let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; - let community_id = post.community_id; - let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; - // Add a vector containing some important info to the "in_reply_to" field // [post_ap_id, Option(parent_comment_ap_id)] let mut in_reply_to_vec = vec![post.ap_id.into_inner()]; @@ -67,23 +141,25 @@ impl ToApub for Comment { in_reply_to_vec.push(parent_comment.ap_id.into_inner()); } - comment - // Not needed when the Post is embedded in a collection (like for community outbox) - .set_many_contexts(lemmy_context()) - .set_id(self.ap_id.to_owned().into_inner()) - .set_published(convert_datetime(self.published)) - // NOTE: included community id for compatibility with lemmy v0.9.9 - .set_many_tos(vec![community.actor_id.into_inner(), public()]) - .set_many_in_reply_tos(in_reply_to_vec) - .set_attributed_to(creator.actor_id.into_inner()); - - set_content_and_source(&mut comment, &self.content)?; - - if let Some(u) = self.updated { - comment.set_updated(convert_datetime(u)); - } + let note = Note { + context: lemmy_context(), + r#type: NoteType::Note, + id: self.ap_id.to_owned().into_inner(), + attributed_to: creator.actor_id.into_inner(), + to: PublicUrl::Public, + content: self.content.clone(), + media_type: MediaTypeHtml::Html, + source: Source { + content: self.content.clone(), + media_type: MediaTypeMarkdown::Markdown, + }, + in_reply_to: in_reply_to_vec, + published: convert_datetime(self.published), + updated: self.updated.map(convert_datetime), + unparsed: Default::default(), + }; - Ok(comment) + Ok(note) } fn to_tombstone(&self) -> Result { @@ -98,108 +174,38 @@ impl ToApub for Comment { #[async_trait::async_trait(?Send)] impl FromApub for Comment { - type ApubType = NoteExt; + type ApubType = Note; /// Converts a `Note` to `Comment`. /// /// If the parent community, post and comment(s) are not known locally, these are also fetched. async fn from_apub( - note: &NoteExt, + note: &Note, context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - mod_action_allowed: bool, - ) -> Result { - let comment: Comment = get_object_from_apub( - note, - context, - expected_domain, - request_counter, - mod_action_allowed, - ) - .await?; - - let post_id = comment.post_id; - let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - check_object_for_community_or_site_ban(note, post.community_id, context, request_counter) - .await?; - Ok(comment) - } -} - -#[async_trait::async_trait(?Send)] -impl FromApubToForm for CommentForm { - async fn from_apub( - note: &NoteExt, - 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(note, context, request_counter).await?; - let ap_id = Some(check_object_domain(note, expected_domain, community.local)?); - let creator_actor_id = ¬e - .attributed_to() - .context(location_info!())? - .as_single_xsd_any_uri() - .context(location_info!())?; - + ) -> Result { let creator = - get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?; - - let mut in_reply_tos = note - .in_reply_to() - .as_ref() - .context(location_info!())? - .as_many() - .context(location_info!())? - .iter() - .map(|i| i.as_xsd_any_uri().context("")); - let post_ap_id = in_reply_tos.next().context(location_info!())??; + get_or_fetch_and_upsert_person(¬e.attributed_to, context, request_counter).await?; + let (post, parent_comment_id) = note.get_parents(context, request_counter).await?; - // This post, or the parent comment might not yet exist on this server yet, fetch them. - let post = Box::pin(get_or_fetch_and_insert_post( - post_ap_id, - context, - request_counter, - )) - .await?; - if post.locked { - return Err(anyhow!("Post is locked").into()); - } - - // The 2nd item, if it exists, is the parent comment apub_id - // For deeply nested comments, FromApub automatically gets called recursively - let parent_id: Option = match in_reply_tos.next() { - Some(parent_comment_uri) => { - let parent_comment_ap_id = &parent_comment_uri?; - let parent_comment = Box::pin(get_or_fetch_and_insert_comment( - parent_comment_ap_id, - context, - request_counter, - )) - .await?; + let content = ¬e.source.content; + let content_slurs_removed = remove_slurs(content); - Some(parent_comment.id) - } - None => None, - }; - - let content = get_source_markdown_value(note)?.context(location_info!())?; - let content_slurs_removed = remove_slurs(&content); - - Ok(CommentForm { + let form = CommentForm { creator_id: creator.id, post_id: post.id, - parent_id, + parent_id: parent_comment_id, content: content_slurs_removed, removed: None, read: None, - published: note.published().map(|u| u.to_owned().naive_local()), - updated: note.updated().map(|u| u.to_owned().naive_local()), + published: Some(note.published.naive_local()), + updated: note.updated.map(|u| u.to_owned().naive_local()), deleted: None, - ap_id, + ap_id: Some(note.id.clone().into()), local: Some(false), - }) + }; + Ok(blocking(context.pool(), move |conn| Comment::upsert(conn, &form)).await??) } } diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 7191a4a1b..c4f57c51a 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -1,8 +1,4 @@ -use crate::{ - check_community_or_site_ban, - check_is_apub_id_valid, - fetcher::person::get_or_fetch_and_upsert_person, -}; +use crate::{check_is_apub_id_valid, fetcher::person::get_or_fetch_and_upsert_person}; use activitystreams::{ base::{AsBase, BaseExt, ExtendsExt}, markers::Base, @@ -14,7 +10,7 @@ use chrono::NaiveDateTime; use lemmy_api_common::blocking; use lemmy_apub_lib::values::MediaTypeMarkdown; use lemmy_db_queries::{ApubObject, Crud, DbPool}; -use lemmy_db_schema::{CommunityId, DbUrl}; +use lemmy_db_schema::DbUrl; use lemmy_utils::{ location_info, settings::structs::Settings, @@ -219,21 +215,3 @@ where Ok(to) } } - -pub(in crate::objects) async fn check_object_for_community_or_site_ban( - object: &T, - community_id: CommunityId, - context: &LemmyContext, - request_counter: &mut i32, -) -> Result<(), LemmyError> -where - T: ObjectExt, -{ - let person_id = object - .attributed_to() - .context(location_info!())? - .as_single_xsd_any_uri() - .context(location_info!())?; - 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 -} diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index a7a6cfe93..1f2172d16 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -1,8 +1,9 @@ use crate::{ - activities::extract_community, + activities::{extract_community, verify_person_in_community}, extensions::context::lemmy_context, fetcher::person::get_or_fetch_and_upsert_person, objects::{create_tombstone, FromApub, Source, ToApub}, + ActorType, }; use activitystreams::{ base::AnyBase, @@ -35,9 +36,10 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Page { #[serde(rename = "@context")] @@ -57,8 +59,6 @@ pub struct Page { pub(crate) stickied: Option, published: DateTime, updated: Option>, - - // unparsed fields #[serde(flatten)] unparsed: Unparsed, } @@ -92,11 +92,20 @@ impl Page { pub(crate) async fn verify( &self, - _context: &LemmyContext, - _request_counter: &mut i32, + context: &LemmyContext, + request_counter: &mut i32, ) -> Result<(), LemmyError> { + let community = extract_community(&self.to, context, request_counter).await?; + check_slurs(&self.name)?; verify_domains_match(&self.attributed_to, &self.id)?; + verify_person_in_community( + &self.attributed_to, + &community.actor_id(), + context, + request_counter, + ) + .await?; Ok(()) } } diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 624123ecf..2b284a2da 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -1,60 +1,93 @@ use crate::{ extensions::context::lemmy_context, fetcher::person::get_or_fetch_and_upsert_person, - objects::{ - check_object_domain, - create_tombstone, - get_object_from_apub, - get_source_markdown_value, - set_content_and_source, - FromApub, - FromApubToForm, - ToApub, - }, - NoteExt, + objects::{create_tombstone, FromApub, Source, ToApub}, }; use activitystreams::{ - object::{kind::NoteType, ApObject, Note, Tombstone}, - prelude::*, + base::AnyBase, + object::{kind::NoteType, Tombstone}, + primitives::OneOrMany, + unparsed::Unparsed, }; -use anyhow::Context; +use anyhow::anyhow; +use chrono::{DateTime, FixedOffset}; use lemmy_api_common::blocking; -use lemmy_db_queries::{Crud, DbPool}; +use lemmy_apub_lib::{ + values::{MediaTypeHtml, MediaTypeMarkdown}, + verify_domains_match, +}; +use lemmy_db_queries::{ApubObject, Crud, DbPool}; use lemmy_db_schema::source::{ person::Person, private_message::{PrivateMessage, PrivateMessageForm}, }; -use lemmy_utils::{location_info, utils::convert_datetime, LemmyError}; +use lemmy_utils::{utils::convert_datetime, LemmyError}; use lemmy_websocket::LemmyContext; +use serde::{Deserialize, Serialize}; use url::Url; +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Note { + #[serde(rename = "@context")] + context: OneOrMany, + r#type: NoteType, + pub(crate) id: Url, + pub(crate) attributed_to: Url, + to: Url, + content: String, + media_type: MediaTypeHtml, + source: Source, + published: DateTime, + updated: Option>, + #[serde(flatten)] + unparsed: Unparsed, +} + +impl Note { + pub(crate) async fn verify( + &self, + context: &LemmyContext, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_domains_match(&self.attributed_to, &self.id)?; + let person = + get_or_fetch_and_upsert_person(&self.attributed_to, context, request_counter).await?; + if person.banned { + return Err(anyhow!("Person is banned from site").into()); + } + Ok(()) + } +} + #[async_trait::async_trait(?Send)] impl ToApub for PrivateMessage { - type ApubType = NoteExt; - - async fn to_apub(&self, pool: &DbPool) -> Result { - let mut private_message = ApObject::new(Note::new()); + type ApubType = Note; + 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 recipient_id = self.recipient_id; let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??; - private_message - .set_many_contexts(lemmy_context()) - .set_id(self.ap_id.to_owned().into_inner()) - .set_published(convert_datetime(self.published)) - .set_to(recipient.actor_id.into_inner()) - .set_attributed_to(creator.actor_id.into_inner()); - - set_content_and_source(&mut private_message, &self.content)?; - - if let Some(u) = self.updated { - private_message.set_updated(convert_datetime(u)); - } - - Ok(private_message) + let note = Note { + context: lemmy_context(), + r#type: NoteType::Note, + id: self.ap_id.clone().into(), + attributed_to: creator.actor_id.into_inner(), + to: recipient.actor_id.into(), + content: self.content.clone(), + media_type: MediaTypeHtml::Html, + source: Source { + content: self.content.clone(), + media_type: MediaTypeMarkdown::Markdown, + }, + published: convert_datetime(self.published), + updated: self.updated.map(convert_datetime), + unparsed: Default::default(), + }; + Ok(note) } fn to_tombstone(&self) -> Result { @@ -69,66 +102,35 @@ impl ToApub for PrivateMessage { #[async_trait::async_trait(?Send)] impl FromApub for PrivateMessage { - type ApubType = NoteExt; + type ApubType = Note; async fn from_apub( - note: &NoteExt, + note: &Note, context: &LemmyContext, - expected_domain: Url, - request_counter: &mut i32, - mod_action_allowed: bool, - ) -> Result { - get_object_from_apub( - note, - context, - expected_domain, - request_counter, - mod_action_allowed, - ) - .await - } -} - -#[async_trait::async_trait(?Send)] -impl FromApubToForm for PrivateMessageForm { - async fn from_apub( - note: &NoteExt, - context: &LemmyContext, - expected_domain: Url, + _expected_domain: Url, request_counter: &mut i32, _mod_action_allowed: bool, - ) -> Result { - let creator_actor_id = note - .attributed_to() - .context(location_info!())? - .clone() - .single_xsd_any_uri() - .context(location_info!())?; - + ) -> Result { let creator = - get_or_fetch_and_upsert_person(&creator_actor_id, context, request_counter).await?; - let recipient_actor_id = note - .to() - .context(location_info!())? - .clone() - .single_xsd_any_uri() - .context(location_info!())?; - let recipient = - get_or_fetch_and_upsert_person(&recipient_actor_id, context, request_counter).await?; - let ap_id = Some(check_object_domain(note, expected_domain, false)?); - - let content = get_source_markdown_value(note)?.context(location_info!())?; + get_or_fetch_and_upsert_person(¬e.attributed_to, context, request_counter).await?; + let recipient = get_or_fetch_and_upsert_person(¬e.to, context, request_counter).await?; - Ok(PrivateMessageForm { + let form = PrivateMessageForm { creator_id: creator.id, recipient_id: recipient.id, - content, - published: note.published().map(|u| u.to_owned().naive_local()), - updated: note.updated().map(|u| u.to_owned().naive_local()), + content: note.source.content.clone(), + published: Some(note.published.naive_local()), + updated: note.updated.map(|u| u.to_owned().naive_local()), deleted: None, read: None, - ap_id, + ap_id: Some(note.id.clone().into()), local: Some(false), - }) + }; + Ok( + blocking(context.pool(), move |conn| { + PrivateMessage::upsert(conn, &form) + }) + .await??, + ) } } diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 4efa983fe..3c8abaf79 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -92,6 +92,7 @@ where } impl DbUrl { + // TODO: remove this method and just use into() pub fn into_inner(self) -> Url { self.0 } @@ -99,7 +100,7 @@ impl DbUrl { impl Display for DbUrl { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.to_owned().into_inner().fmt(f) + self.to_owned().0.fmt(f) } }