From 43ad99bbe8701f5a25620ac60e65f3615a270a18 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Sat, 31 Jul 2021 16:57:37 +0200 Subject: [PATCH] Rewrite apub comment (de)serialization using structs (ref #1657) --- crates/api_crud/src/comment/create.rs | 11 +- crates/api_crud/src/comment/update.rs | 6 +- crates/apub/src/activities/comment/create.rs | 59 +++- crates/apub/src/activities/comment/mod.rs | 124 ++++++++- crates/apub/src/activities/comment/remove.rs | 64 ----- .../src/activities/comment/undo_remove.rs | 73 ----- crates/apub/src/activities/comment/update.rs | 59 +++- crates/apub/src/activities/mod.rs | 2 +- crates/apub/src/activities/send/comment.rs | 212 ++------------ crates/apub/src/activity_queue.rs | 34 --- crates/apub/src/fetcher/objects.rs | 5 +- crates/apub/src/fetcher/search.rs | 5 +- crates/apub/src/objects/comment.rs | 258 +++++++++--------- crates/apub/src/objects/mod.rs | 26 +- crates/apub/src/objects/post.rs | 21 +- 15 files changed, 401 insertions(+), 558 deletions(-) delete mode 100644 crates/apub/src/activities/comment/remove.rs delete mode 100644 crates/apub/src/activities/comment/undo_remove.rs diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 7923b0836..4418c14e2 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::CreateComment as CreateApubComment, + 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,7 @@ impl PerformCrud for CreateComment { .await? .map_err(|_| ApiError::err("couldnt_create_comment"))?; - updated_comment - .send_create(&local_user_view.person, context) - .await?; + CreateApubComment::send(&updated_comment, &local_user_view.person, 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..4bd35884d 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -7,7 +7,7 @@ use lemmy_api_common::{ get_local_user_view_from_jwt, send_local_notifs, }; -use lemmy_apub::ApubObjectType; +use lemmy_apub::activities::comment::update::UpdateComment; use lemmy_db_queries::{source::comment::Comment_, DeleteableOrRemoveable}; use lemmy_db_schema::source::comment::*; use lemmy_db_views::comment_view::CommentView; @@ -59,9 +59,7 @@ 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?; + UpdateComment::send(&updated_comment, &local_user_view.person, context).await?; // Do the mentions / recipients let updated_comment_content = updated_comment.content.to_owned(); diff --git a/crates/apub/src/activities/comment/create.rs b/crates/apub/src/activities/comment/create.rs index 0e9472194..6c79dec48 100644 --- a/crates/apub/src/activities/comment/create.rs +++ b/crates/apub/src/activities/comment/create.rs @@ -1,22 +1,27 @@ use crate::{ activities::{ - comment::{get_notif_recipients, send_websocket_message}, + 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, }, - objects::FromApub, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, + objects::{comment::Note, FromApub, ToApub}, ActorType, - NoteExt, }; -use activitystreams::{activity::kind::CreateType, base::BaseExt}; +use activitystreams::{activity::kind::CreateType, link::Mention}; +use lemmy_api_common::blocking; use lemmy_apub_lib::{ values::PublicUrl, - verify_domains_match_opt, + verify_domains_match, ActivityCommonFields, ActivityHandler, }; -use lemmy_db_schema::source::comment::Comment; +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 url::Url; @@ -25,14 +30,51 @@ use url::Url; #[serde(rename_all = "camelCase")] pub struct CreateComment { to: PublicUrl, - object: NoteExt, + object: Note, cc: Vec, + tag: Vec, #[serde(rename = "type")] kind: CreateType, #[serde(flatten)] common: ActivityCommonFields, } +impl CreateComment { + pub async fn send( + comment: &Comment, + actor: &Person, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + // TODO: would 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(CreateType::Create)?; + let maa = collect_non_local_mentions(comment, &community, context).await?; + + let create = CreateComment { + to: PublicUrl::Public, + object: comment.to_apub(context.pool()).await?, + cc: maa.ccs, + tag: maa.tags, + kind: Default::default(), + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + + let activity = AnnouncableActivities::CreateComment(create); + send_to_community_new(activity, &id, actor, &community, maa.inboxes, context).await + } +} + #[async_trait::async_trait(?Send)] impl ActivityHandler for CreateComment { async fn verify( @@ -50,9 +92,10 @@ impl ActivityHandler for CreateComment { request_counter, ) .await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + 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(()) } diff --git a/crates/apub/src/activities/comment/mod.rs b/crates/apub/src/activities/comment/mod.rs index 292d57bf4..1bf94ed6c 100644 --- a/crates/apub/src/activities/comment/mod.rs +++ b/crates/apub/src/activities/comment/mod.rs @@ -1,14 +1,27 @@ -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; @@ -63,3 +76,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 index c0d014813..8dd44b804 100644 --- a/crates/apub/src/activities/comment/update.rs +++ b/crates/apub/src/activities/comment/update.rs @@ -1,22 +1,27 @@ use crate::{ activities::{ - comment::{get_notif_recipients, send_websocket_message}, + 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, }, - objects::FromApub, + activity_queue::send_to_community_new, + extensions::context::lemmy_context, + objects::{comment::Note, FromApub, ToApub}, ActorType, - NoteExt, }; -use activitystreams::{activity::kind::UpdateType, base::BaseExt}; +use activitystreams::{activity::kind::UpdateType, link::Mention}; +use lemmy_api_common::blocking; use lemmy_apub_lib::{ values::PublicUrl, - verify_domains_match_opt, + verify_domains_match, ActivityCommonFields, ActivityHandler, }; -use lemmy_db_schema::source::comment::Comment; +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 url::Url; @@ -25,14 +30,51 @@ use url::Url; #[serde(rename_all = "camelCase")] pub struct UpdateComment { to: PublicUrl, - object: NoteExt, + object: Note, cc: Vec, + tag: Vec, #[serde(rename = "type")] kind: UpdateType, #[serde(flatten)] common: ActivityCommonFields, } +impl UpdateComment { + pub async fn send( + comment: &Comment, + actor: &Person, + context: &LemmyContext, + ) -> Result<(), LemmyError> { + // TODO: would 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(UpdateType::Update)?; + let maa = collect_non_local_mentions(comment, &community, context).await?; + + let update = UpdateComment { + to: PublicUrl::Public, + object: comment.to_apub(context.pool()).await?, + cc: maa.ccs, + tag: maa.tags, + kind: Default::default(), + common: ActivityCommonFields { + context: lemmy_context(), + id: id.clone(), + actor: actor.actor_id(), + unparsed: Default::default(), + }, + }; + + let activity = AnnouncableActivities::UpdateComment(update); + send_to_community_new(activity, &id, actor, &community, maa.inboxes, context).await + } +} + #[async_trait::async_trait(?Send)] impl ActivityHandler for UpdateComment { async fn verify( @@ -50,7 +92,8 @@ impl ActivityHandler for UpdateComment { request_counter, ) .await?; - verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?; + verify_domains_match(&self.common.actor, &self.object.id)?; + self.object.verify(context, request_counter).await?; Ok(()) } diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index af133167f..d8eaa0e77 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -61,7 +61,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/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/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/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()); + 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(), + }; - set_content_and_source(&mut comment, &self.content)?; - - if let Some(u) = self.updated { - comment.set_updated(convert_datetime(u)); - } - - 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?; + 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?; - 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!())??; + let content = ¬e.source.content; + let content_slurs_removed = remove_slurs(content); - // 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?; - - 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(()) } }