2 activities::{generate_activity_id, send_activity_to_community},
5 create_apub_tombstone_response,
9 get_or_fetch_and_insert_comment,
10 get_or_fetch_and_insert_post,
11 get_or_fetch_and_upsert_user,
19 use activitystreams::{
21 kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
32 object::{kind::NoteType, Note, Tombstone},
36 use actix_web::{body::Body, web, web::Path, HttpResponse};
38 use itertools::Itertools;
40 comment::{Comment, CommentForm},
47 use lemmy_structs::blocking;
50 utils::{convert_datetime, remove_slurs, scrape_text_for_mentions, MentionData},
53 use lemmy_websocket::LemmyContext;
55 use serde::Deserialize;
56 use serde_json::Error;
59 #[derive(Deserialize)]
60 pub struct CommentQuery {
64 /// Return the post json over HTTP.
65 pub async fn get_apub_comment(
66 info: Path<CommentQuery>,
67 context: web::Data<LemmyContext>,
68 ) -> Result<HttpResponse<Body>, LemmyError> {
69 let id = info.comment_id.parse::<i32>()?;
70 let comment = blocking(context.pool(), move |conn| Comment::read(conn, id)).await??;
73 Ok(create_apub_response(
74 &comment.to_apub(context.pool()).await?,
77 Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
81 #[async_trait::async_trait(?Send)]
82 impl ToApub for Comment {
85 async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
86 let mut comment = Note::new();
88 let creator_id = self.creator_id;
89 let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
91 let post_id = self.post_id;
92 let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
94 let community_id = post.community_id;
95 let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
97 // Add a vector containing some important info to the "in_reply_to" field
98 // [post_ap_id, Option(parent_comment_ap_id)]
99 let mut in_reply_to_vec = vec![post.ap_id];
101 if let Some(parent_id) = self.parent_id {
102 let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
104 in_reply_to_vec.push(parent_comment.ap_id);
108 // Not needed when the Post is embedded in a collection (like for community outbox)
109 .set_context(activitystreams::context())
110 .set_id(Url::parse(&self.ap_id)?)
111 .set_published(convert_datetime(self.published))
112 .set_to(community.actor_id)
113 .set_many_in_reply_tos(in_reply_to_vec)
114 .set_content(self.content.to_owned())
115 .set_attributed_to(creator.actor_id);
117 if let Some(u) = self.updated {
118 comment.set_updated(convert_datetime(u));
124 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
125 create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
129 #[async_trait::async_trait(?Send)]
130 impl FromApub for CommentForm {
131 type ApubType = Note;
133 /// Parse an ActivityPub note received from another instance into a Lemmy comment
136 context: &LemmyContext,
137 expected_domain: Option<Url>,
138 ) -> Result<CommentForm, LemmyError> {
139 let creator_actor_id = ¬e
141 .context(location_info!())?
142 .as_single_xsd_any_uri()
143 .context(location_info!())?;
145 let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
147 let mut in_reply_tos = note
150 .context(location_info!())?
152 .context(location_info!())?
154 .map(|i| i.as_xsd_any_uri().context(""));
155 let post_ap_id = in_reply_tos.next().context(location_info!())??;
157 // This post, or the parent comment might not yet exist on this server yet, fetch them.
158 let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
160 // The 2nd item, if it exists, is the parent comment apub_id
161 // For deeply nested comments, FromApub automatically gets called recursively
162 let parent_id: Option<i32> = match in_reply_tos.next() {
163 Some(parent_comment_uri) => {
164 let parent_comment_ap_id = &parent_comment_uri?;
166 get_or_fetch_and_insert_comment(&parent_comment_ap_id, context).await?;
168 Some(parent_comment.id)
174 .context(location_info!())?
175 .as_single_xsd_string()
176 .context(location_info!())?
178 let content_slurs_removed = remove_slurs(&content);
181 creator_id: creator.id,
184 content: content_slurs_removed,
187 published: note.published().map(|u| u.to_owned().naive_local()),
188 updated: note.updated().map(|u| u.to_owned().naive_local()),
190 ap_id: Some(check_actor_domain(note, expected_domain)?),
196 #[async_trait::async_trait(?Send)]
197 impl ApubObjectType for Comment {
198 /// Send out information about a newly created comment, to the followers of the community.
199 async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
200 let note = self.to_apub(context.pool()).await?;
202 let post_id = self.post_id;
203 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
205 let community_id = post.community_id;
206 let community = blocking(context.pool(), move |conn| {
207 Community::read(conn, community_id)
211 let maa = collect_non_local_mentions_and_addresses(&self.content, &community, context).await?;
213 let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
215 .set_context(activitystreams::context())
216 .set_id(generate_activity_id(CreateType::Create)?)
218 .set_many_ccs(maa.addressed_ccs.to_owned())
219 // Set the mention tags
220 .set_many_tags(maa.get_tags()?);
222 send_activity_to_community(&creator, &community, maa.inboxes, create, context).await?;
226 /// Send out information about an edited post, to the followers of the community.
227 async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
228 let note = self.to_apub(context.pool()).await?;
230 let post_id = self.post_id;
231 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
233 let community_id = post.community_id;
234 let community = blocking(context.pool(), move |conn| {
235 Community::read(conn, community_id)
239 let maa = collect_non_local_mentions_and_addresses(&self.content, &community, context).await?;
241 let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
243 .set_context(activitystreams::context())
244 .set_id(generate_activity_id(UpdateType::Update)?)
246 .set_many_ccs(maa.addressed_ccs.to_owned())
247 // Set the mention tags
248 .set_many_tags(maa.get_tags()?);
250 send_activity_to_community(&creator, &community, maa.inboxes, update, context).await?;
254 async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
255 let note = self.to_apub(context.pool()).await?;
257 let post_id = self.post_id;
258 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
260 let community_id = post.community_id;
261 let community = blocking(context.pool(), move |conn| {
262 Community::read(conn, community_id)
266 let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
268 .set_context(activitystreams::context())
269 .set_id(generate_activity_id(DeleteType::Delete)?)
271 .set_many_ccs(vec![community.get_followers_url()?]);
273 send_activity_to_community(
276 vec![community.get_shared_inbox_url()?],
284 async fn send_undo_delete(
287 context: &LemmyContext,
288 ) -> Result<(), LemmyError> {
289 let note = self.to_apub(context.pool()).await?;
291 let post_id = self.post_id;
292 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
294 let community_id = post.community_id;
295 let community = blocking(context.pool(), move |conn| {
296 Community::read(conn, community_id)
300 // Generate a fake delete activity, with the correct object
301 let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
303 .set_context(activitystreams::context())
304 .set_id(generate_activity_id(DeleteType::Delete)?)
306 .set_many_ccs(vec![community.get_followers_url()?]);
308 // Undo that fake activity
309 let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
311 .set_context(activitystreams::context())
312 .set_id(generate_activity_id(UndoType::Undo)?)
314 .set_many_ccs(vec![community.get_followers_url()?]);
316 send_activity_to_community(
319 vec![community.get_shared_inbox_url()?],
327 async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
328 let note = self.to_apub(context.pool()).await?;
330 let post_id = self.post_id;
331 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
333 let community_id = post.community_id;
334 let community = blocking(context.pool(), move |conn| {
335 Community::read(conn, community_id)
339 let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
341 .set_context(activitystreams::context())
342 .set_id(generate_activity_id(RemoveType::Remove)?)
344 .set_many_ccs(vec![community.get_followers_url()?]);
346 send_activity_to_community(
349 vec![community.get_shared_inbox_url()?],
357 async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
358 let note = self.to_apub(context.pool()).await?;
360 let post_id = self.post_id;
361 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
363 let community_id = post.community_id;
364 let community = blocking(context.pool(), move |conn| {
365 Community::read(conn, community_id)
369 // Generate a fake delete activity, with the correct object
370 let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
372 .set_context(activitystreams::context())
373 .set_id(generate_activity_id(RemoveType::Remove)?)
375 .set_many_ccs(vec![community.get_followers_url()?]);
377 // Undo that fake activity
378 let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
380 .set_context(activitystreams::context())
381 .set_id(generate_activity_id(UndoType::Undo)?)
383 .set_many_ccs(vec![community.get_followers_url()?]);
385 send_activity_to_community(
388 vec![community.get_shared_inbox_url()?],
397 #[async_trait::async_trait(?Send)]
398 impl ApubLikeableType for Comment {
399 async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
400 let note = self.to_apub(context.pool()).await?;
402 let post_id = self.post_id;
403 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
405 let community_id = post.community_id;
406 let community = blocking(context.pool(), move |conn| {
407 Community::read(conn, community_id)
411 let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
413 .set_context(activitystreams::context())
414 .set_id(generate_activity_id(LikeType::Like)?)
416 .set_many_ccs(vec![community.get_followers_url()?]);
418 send_activity_to_community(
421 vec![community.get_shared_inbox_url()?],
429 async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
430 let note = self.to_apub(context.pool()).await?;
432 let post_id = self.post_id;
433 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
435 let community_id = post.community_id;
436 let community = blocking(context.pool(), move |conn| {
437 Community::read(conn, community_id)
441 let mut dislike = Dislike::new(creator.actor_id.to_owned(), note.into_any_base()?);
443 .set_context(activitystreams::context())
444 .set_id(generate_activity_id(DislikeType::Dislike)?)
446 .set_many_ccs(vec![community.get_followers_url()?]);
448 send_activity_to_community(
451 vec![community.get_shared_inbox_url()?],
459 async fn send_undo_like(
462 context: &LemmyContext,
463 ) -> Result<(), LemmyError> {
464 let note = self.to_apub(context.pool()).await?;
466 let post_id = self.post_id;
467 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
469 let community_id = post.community_id;
470 let community = blocking(context.pool(), move |conn| {
471 Community::read(conn, community_id)
475 let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
477 .set_context(activitystreams::context())
478 .set_id(generate_activity_id(DislikeType::Dislike)?)
480 .set_many_ccs(vec![community.get_followers_url()?]);
482 // Undo that fake activity
483 let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
485 .set_context(activitystreams::context())
486 .set_id(generate_activity_id(UndoType::Undo)?)
488 .set_many_ccs(vec![community.get_followers_url()?]);
490 send_activity_to_community(
493 vec![community.get_shared_inbox_url()?],
502 struct MentionsAndAddresses {
503 addressed_ccs: Vec<Url>,
508 impl MentionsAndAddresses {
509 fn get_tags(&self) -> Result<Vec<AnyBase>, Error> {
513 .map(|t| t.to_owned().into_any_base())
514 .collect::<Result<Vec<AnyBase>, Error>>()
518 /// This takes a comment, and builds a list of to_addresses, inboxes,
519 /// and mention tags, so they know where to be sent to.
520 /// Addresses are the users / addresses that go in the cc field.
521 async fn collect_non_local_mentions_and_addresses(
523 community: &Community,
524 context: &LemmyContext,
525 ) -> Result<MentionsAndAddresses, LemmyError> {
526 let mut addressed_ccs = vec![community.get_followers_url()?];
528 // Add the mention tag
529 let mut tags = Vec::new();
531 // Get the inboxes for any mentions
532 let mentions = scrape_text_for_mentions(&content)
534 // Filter only the non-local ones
535 .filter(|m| !m.is_local())
536 .collect::<Vec<MentionData>>();
538 let mut mention_inboxes: Vec<Url> = Vec::new();
539 for mention in &mentions {
540 // TODO should it be fetching it every time?
541 if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await {
542 debug!("mention actor_id: {}", actor_id);
543 addressed_ccs.push(actor_id.to_owned().to_string().parse()?);
545 let mention_user = get_or_fetch_and_upsert_user(&actor_id, context).await?;
546 let shared_inbox = mention_user.get_shared_inbox_url()?;
548 mention_inboxes.push(shared_inbox);
549 let mut mention_tag = Mention::new();
550 mention_tag.set_href(actor_id).set_name(mention.full_name());
551 tags.push(mention_tag);
555 let mut inboxes = vec![community.get_shared_inbox_url()?];
556 inboxes.extend(mention_inboxes);
557 inboxes = inboxes.into_iter().unique().collect();
559 Ok(MentionsAndAddresses {