diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 1c133be23..09163aef4 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -152,6 +152,7 @@ impl PerformCrud for CreateComment { &local_user_view.person.clone().into(), CreateOrUpdateType::Create, context, + &mut 0, ) .await?; let object = PostOrComment::Comment(Box::new(apub_comment)); diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 4802b1d50..7b2937922 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -91,6 +91,7 @@ impl PerformCrud for EditComment { &local_user_view.person.into(), CreateOrUpdateType::Update, context, + &mut 0, ) .await?; diff --git a/crates/apub/assets/lemmy/activities/create_or_update/create_note.json b/crates/apub/assets/lemmy/activities/create_or_update/create_note.json index 4360ce92a..33ce1cfd9 100644 --- a/crates/apub/assets/lemmy/activities/create_or_update/create_note.json +++ b/crates/apub/assets/lemmy/activities/create_or_update/create_note.json @@ -23,7 +23,13 @@ "http://enterprise.lemmy.ml/c/main", "http://ds9.lemmy.ml/u/lemmy_alpha" ], - "tag": [], + "tag": [ + { + "href": "http://ds9.lemmy.ml/u/lemmy_alpha", + "type": "Mention", + "name": "@lemmy_alpha@ds9.lemmy.ml" + } + ], "type": "Create", "id": "http://ds9.lemmy.ml/activities/create/1e77d67c-44ac-45ed-bf2a-460e21f60236" } \ No newline at end of file diff --git a/crates/apub/assets/lemmy/objects/note.json b/crates/apub/assets/lemmy/objects/note.json index c353d0b2b..269063a76 100644 --- a/crates/apub/assets/lemmy/objects/note.json +++ b/crates/apub/assets/lemmy/objects/note.json @@ -3,6 +3,10 @@ "type": "Note", "attributedTo": "https://enterprise.lemmy.ml/u/picard", "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": [ + "https://enterprise.lemmy.ml/c/tenforward", + "https://enterprise.lemmy.ml/u/picard" + ], "inReplyTo": "https://enterprise.lemmy.ml/post/55143", "content": "

first comment!

\n", "mediaType": "text/html", @@ -10,6 +14,13 @@ "content": "first comment!", "mediaType": "text/markdown" }, + "tag": [ + { + "href": "https://enterprise.lemmy.ml/u/picard", + "type": "Mention", + "name": "@picard@enterprise.lemmy.ml" + } + ], "published": "2021-03-01T13:42:43.966208+00:00", "updated": "2021-03-01T13:43:03.955787+00:00" } diff --git a/crates/apub/assets/smithereen/activities/create_note.json b/crates/apub/assets/smithereen/activities/create_note.json index a30bc9a53..4bd199d4a 100644 --- a/crates/apub/assets/smithereen/activities/create_note.json +++ b/crates/apub/assets/smithereen/activities/create_note.json @@ -16,10 +16,12 @@ "content": "

So does this federate now?

", "inReplyTo": "https://ds9.lemmy.ml/post/1723", "published": "2021-11-09T11:42:35Z", - "tag": { - "type": "Mention", - "href": "https://ds9.lemmy.ml/u/nutomic" - }, + "tag": [ + { + "type": "Mention", + "href": "https://ds9.lemmy.ml/u/nutomic" + } + ], "url": "https://friends.grishka.me/posts/66561", "to": [ "https://www.w3.org/ns/activitystreams#Public" diff --git a/crates/apub/assets/smithereen/objects/note.json b/crates/apub/assets/smithereen/objects/note.json index 24d665b26..0a948ee3d 100644 --- a/crates/apub/assets/smithereen/objects/note.json +++ b/crates/apub/assets/smithereen/objects/note.json @@ -5,10 +5,12 @@ "content": "

So does this federate now?

", "inReplyTo": "https://ds9.lemmy.ml/post/1723", "published": "2021-11-09T11:42:35Z", - "tag": { - "type": "Mention", - "href": "https://ds9.lemmy.ml/u/nutomic" - }, + "tag": [ + { + "type": "Mention", + "href": "https://ds9.lemmy.ml/u/nutomic" + } + ], "url": "https://friends.grishka.me/posts/66561", "to": [ "https://www.w3.org/ns/activitystreams#Public" diff --git a/crates/apub/src/activities/comment/create_or_update.rs b/crates/apub/src/activities/comment/create_or_update.rs index eab2a5771..ad686251b 100644 --- a/crates/apub/src/activities/comment/create_or_update.rs +++ b/crates/apub/src/activities/comment/create_or_update.rs @@ -1,7 +1,7 @@ use crate::{ activities::{ check_community_deleted_or_removed, - comment::{collect_non_local_mentions, get_notif_recipients}, + comment::get_notif_recipients, community::{announce::GetCommunity, send_activity_in_community}, generate_activity_id, verify_activity, @@ -12,7 +12,7 @@ use crate::{ objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson}, protocol::activities::{create_or_update::comment::CreateOrUpdateComment, CreateOrUpdateType}, }; -use activitystreams::public; +use activitystreams::{link::LinkExt, public}; use lemmy_api_common::{blocking, check_post_deleted_or_removed}; use lemmy_apub_lib::{ data::Data, @@ -33,6 +33,7 @@ impl CreateOrUpdateComment { actor: &ApubPerson, kind: CreateOrUpdateType, context: &LemmyContext, + request_counter: &mut i32, ) -> Result<(), LemmyError> { // TODO: might be helpful to add a comment method to retrieve community directly let post_id = comment.post_id; @@ -48,21 +49,34 @@ impl CreateOrUpdateComment { kind.clone(), &context.settings().get_protocol_and_hostname(), )?; - let maa = collect_non_local_mentions(&comment, &community, context).await?; + let note = comment.into_apub(context).await?; let create_or_update = CreateOrUpdateComment { actor: ObjectId::new(actor.actor_id()), to: vec![public()], - object: comment.into_apub(context).await?, - cc: maa.ccs, - tag: maa.tags, + cc: note.cc.clone(), + tag: note.tag.clone(), + object: note, kind, id: id.clone(), unparsed: Default::default(), }; + let tagged_users: Vec> = create_or_update + .tag + .iter() + .map(|t| t.href()) + .flatten() + .map(|t| ObjectId::new(t.clone())) + .collect(); + let mut inboxes = vec![]; + for t in tagged_users { + let person = t.dereference(context, request_counter).await?; + inboxes.push(person.shared_inbox_or_inbox_url()); + } + let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update); - send_activity_in_community(activity, &id, actor, &community, maa.inboxes, context).await + send_activity_in_community(activity, &id, actor, &community, inboxes, context).await } } diff --git a/crates/apub/src/activities/comment/mod.rs b/crates/apub/src/activities/comment/mod.rs index d104db2fe..b42538587 100644 --- a/crates/apub/src/activities/comment/mod.rs +++ b/crates/apub/src/activities/comment/mod.rs @@ -1,32 +1,14 @@ -use activitystreams::{ - base::BaseExt, - link::{LinkExt, Mention}, -}; -use anyhow::anyhow; -use itertools::Itertools; -use log::debug; -use url::Url; - +use crate::objects::person::ApubPerson; use lemmy_api_common::blocking; -use lemmy_apub_lib::{object_id::ObjectId, traits::ActorType}; +use lemmy_apub_lib::object_id::ObjectId; use lemmy_db_schema::{ newtypes::LocalUserId, - source::{comment::Comment, person::Person, post::Post}, + source::{comment::Comment, post::Post}, traits::Crud, - DbPool, -}; -use lemmy_utils::{ - request::{retry, RecvError}, - utils::{scrape_text_for_mentions, MentionData}, - LemmyError, }; +use lemmy_utils::{utils::scrape_text_for_mentions, LemmyError}; use lemmy_websocket::{send::send_local_notifs, LemmyContext}; -use crate::{ - fetcher::webfinger::WebfingerResponse, - objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson}, -}; - pub mod create_or_update; async fn get_notif_recipients( @@ -47,114 +29,3 @@ async fn get_notif_recipients( let mentions = scrape_text_for_mentions(&comment.content); send_local_notifs(mentions, comment, &*actor, &post, true, context).await } - -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: &ApubComment, - community: &ApubCommunity, - context: &LemmyContext, -) -> Result { - let parent_creator = get_comment_parent_creator(context.pool(), comment).await?; - let mut addressed_ccs: Vec = 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.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(&context.settings().hostname)) - .collect::>(); - - for mention in &mentions { - // TODO should it be fetching it every time? - if let Ok(actor_id) = fetch_webfinger_url(mention, context).await { - let actor_id: ObjectId = ObjectId::new(actor_id); - debug!("mention actor_id: {}", actor_id); - addressed_ccs.push(actor_id.to_string().parse()?); - - let mention_person = actor_id.dereference(context, &mut 0).await?; - inboxes.push(mention_person.shared_inbox_or_inbox_url()); - - let mut mention_tag = Mention::new(); - mention_tag - .set_href(actor_id.into()) - .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?? - .into(), - ) -} - -/// 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, - context: &LemmyContext, -) -> Result { - let fetch_url = format!( - "{}://{}/.well-known/webfinger?resource=acct:{}@{}", - context.settings().get_protocol_string(), - mention.domain, - mention.name, - mention.domain - ); - debug!("Fetching webfinger url: {}", &fetch_url); - - let response = retry(|| context.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/lib.rs b/crates/apub/src/lib.rs index ea6bf65be..e400f81e8 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -12,6 +12,7 @@ pub(crate) mod collections; mod context; pub mod fetcher; pub mod http; +pub(crate) mod mentions; pub mod migrations; pub mod objects; pub mod protocol; diff --git a/crates/apub/src/mentions.rs b/crates/apub/src/mentions.rs new file mode 100644 index 000000000..268a1693b --- /dev/null +++ b/crates/apub/src/mentions.rs @@ -0,0 +1,134 @@ +use crate::{ + fetcher::webfinger::WebfingerResponse, + objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson}, +}; +use activitystreams::{ + base::BaseExt, + link::{LinkExt, Mention}, +}; +use anyhow::anyhow; +use lemmy_api_common::blocking; +use lemmy_apub_lib::{object_id::ObjectId, traits::ActorType}; +use lemmy_db_schema::{ + source::{comment::Comment, person::Person, post::Post}, + traits::Crud, + DbPool, +}; +use lemmy_utils::{ + request::{retry, RecvError}, + utils::{scrape_text_for_mentions, MentionData}, + LemmyError, +}; +use lemmy_websocket::LemmyContext; +use log::debug; +use url::Url; + +pub struct MentionsAndAddresses { + pub ccs: 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: &ApubComment, + community_id: ObjectId, + context: &LemmyContext, +) -> Result { + let parent_creator = get_comment_parent_creator(context.pool(), comment).await?; + let mut addressed_ccs: Vec = vec![community_id.into(), parent_creator.actor_id()]; + + // Add the mention tag + let mut parent_creator_tag = Mention::new(); + parent_creator_tag + .set_href(parent_creator.actor_id.clone().into()) + .set_name(format!( + "@{}@{}", + &parent_creator.name, + &parent_creator.actor_id().domain().expect("has domain") + )); + let mut tags = vec![parent_creator_tag]; + + // 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(&context.settings().hostname)) + .collect::>(); + + for mention in &mentions { + // TODO should it be fetching it every time? + if let Ok(actor_id) = fetch_webfinger_url(mention, context).await { + let actor_id: ObjectId = ObjectId::new(actor_id); + debug!("mention actor_id: {}", actor_id); + addressed_ccs.push(actor_id.to_string().parse()?); + + let mut mention_tag = Mention::new(); + mention_tag + .set_href(actor_id.into()) + .set_name(mention.full_name()); + tags.push(mention_tag); + } + } + + Ok(MentionsAndAddresses { + ccs: addressed_ccs, + 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?? + .into(), + ) +} + +/// 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, + context: &LemmyContext, +) -> Result { + let fetch_url = format!( + "{}://{}/.well-known/webfinger?resource=acct:{}@{}", + context.settings().get_protocol_string(), + mention.domain, + mention.name, + mention.domain + ); + debug!("Fetching webfinger url: {}", &fetch_url); + + let response = retry(|| context.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/objects/comment.rs b/crates/apub/src/objects/comment.rs index 83895e8d3..406838181 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -1,6 +1,7 @@ use crate::{ activities::{verify_is_public, verify_person_in_community}, check_is_apub_id_valid, + mentions::collect_non_local_mentions, protocol::{ objects::{ note::{Note, SourceCompat}, @@ -93,6 +94,11 @@ impl ApubObject for ApubComment { 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 in_reply_to = if let Some(comment_id) = self.parent_id { let parent_comment = @@ -101,13 +107,14 @@ impl ApubObject for ApubComment { } else { ObjectId::::new(post.ap_id) }; + let maa = collect_non_local_mentions(&self, ObjectId::new(community.actor_id), context).await?; let note = Note { r#type: NoteType::Note, id: ObjectId::new(self.ap_id.clone()), attributed_to: ObjectId::new(creator.actor_id), to: vec![public()], - cc: vec![], + cc: maa.ccs, content: markdown_to_html(&self.content), media_type: Some(MediaTypeHtml::Html), source: SourceCompat::Lemmy(Source { @@ -117,6 +124,7 @@ impl ApubObject for ApubComment { in_reply_to, published: Some(convert_datetime(self.published)), updated: self.updated.map(convert_datetime), + tag: maa.tags, unparsed: Default::default(), }; diff --git a/crates/apub/src/protocol/activities/create_or_update/mod.rs b/crates/apub/src/protocol/activities/create_or_update/mod.rs index d391e8286..f5db71c20 100644 --- a/crates/apub/src/protocol/activities/create_or_update/mod.rs +++ b/crates/apub/src/protocol/activities/create_or_update/mod.rs @@ -14,7 +14,7 @@ mod tests { #[actix_rt::test] #[serial] - async fn test_parsey_create_or_update() { + async fn test_parse_create_or_update() { test_parse_lemmy_item::( "assets/lemmy/activities/create_or_update/create_page.json", ); diff --git a/crates/apub/src/protocol/mod.rs b/crates/apub/src/protocol/mod.rs index 22df0354d..37a29f8fa 100644 --- a/crates/apub/src/protocol/mod.rs +++ b/crates/apub/src/protocol/mod.rs @@ -30,7 +30,9 @@ pub(crate) mod tests { use serde::{de::DeserializeOwned, Serialize}; use std::collections::HashMap; - pub(crate) fn test_parse_lemmy_item(path: &str) -> T { + pub(crate) fn test_parse_lemmy_item( + path: &str, + ) -> T { let parsed = file_to_json_object::(path); // ensure that no field is ignored when parsing diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs index 784e6f9f8..fdd6ddd96 100644 --- a/crates/apub/src/protocol/objects/note.rs +++ b/crates/apub/src/protocol/objects/note.rs @@ -3,7 +3,7 @@ use crate::{ objects::{comment::ApubComment, person::ApubPerson, post::ApubPost}, protocol::Source, }; -use activitystreams::{object::kind::NoteType, unparsed::Unparsed}; +use activitystreams::{link::Mention, object::kind::NoteType, unparsed::Unparsed}; use anyhow::anyhow; use chrono::{DateTime, FixedOffset}; use lemmy_api_common::blocking; @@ -38,6 +38,8 @@ pub struct Note { pub(crate) in_reply_to: ObjectId, pub(crate) published: Option>, pub(crate) updated: Option>, + #[serde(default)] + pub(crate) tag: Vec, #[serde(flatten)] pub(crate) unparsed: Unparsed, }