]> Untitled Git - lemmy.git/blob - lemmy_apub/src/comment.rs
Move websocket code into workspace (#107)
[lemmy.git] / lemmy_apub / src / comment.rs
1 use crate::{
2   activities::{generate_activity_id, send_activity_to_community},
3   check_actor_domain,
4   create_apub_response,
5   create_apub_tombstone_response,
6   create_tombstone,
7   fetch_webfinger_url,
8   fetcher::{
9     get_or_fetch_and_insert_comment,
10     get_or_fetch_and_insert_post,
11     get_or_fetch_and_upsert_user,
12   },
13   ActorType,
14   ApubLikeableType,
15   ApubObjectType,
16   FromApub,
17   ToApub,
18 };
19 use activitystreams::{
20   activity::{
21     kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
22     Create,
23     Delete,
24     Dislike,
25     Like,
26     Remove,
27     Undo,
28     Update,
29   },
30   base::AnyBase,
31   link::Mention,
32   object::{kind::NoteType, Note, Tombstone},
33   prelude::*,
34   public,
35 };
36 use actix_web::{body::Body, web, web::Path, HttpResponse};
37 use anyhow::Context;
38 use itertools::Itertools;
39 use lemmy_db::{
40   comment::{Comment, CommentForm},
41   community::Community,
42   post::Post,
43   user::User_,
44   Crud,
45   DbPool,
46 };
47 use lemmy_structs::blocking;
48 use lemmy_utils::{
49   location_info,
50   utils::{convert_datetime, remove_slurs, scrape_text_for_mentions, MentionData},
51   LemmyError,
52 };
53 use lemmy_websocket::LemmyContext;
54 use log::debug;
55 use serde::Deserialize;
56 use serde_json::Error;
57 use url::Url;
58
59 #[derive(Deserialize)]
60 pub struct CommentQuery {
61   comment_id: String,
62 }
63
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??;
71
72   if !comment.deleted {
73     Ok(create_apub_response(
74       &comment.to_apub(context.pool()).await?,
75     ))
76   } else {
77     Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
78   }
79 }
80
81 #[async_trait::async_trait(?Send)]
82 impl ToApub for Comment {
83   type Response = Note;
84
85   async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
86     let mut comment = Note::new();
87
88     let creator_id = self.creator_id;
89     let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
90
91     let post_id = self.post_id;
92     let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
93
94     let community_id = post.community_id;
95     let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
96
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];
100
101     if let Some(parent_id) = self.parent_id {
102       let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
103
104       in_reply_to_vec.push(parent_comment.ap_id);
105     }
106
107     comment
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);
116
117     if let Some(u) = self.updated {
118       comment.set_updated(convert_datetime(u));
119     }
120
121     Ok(comment)
122   }
123
124   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
125     create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
126   }
127 }
128
129 #[async_trait::async_trait(?Send)]
130 impl FromApub for CommentForm {
131   type ApubType = Note;
132
133   /// Parse an ActivityPub note received from another instance into a Lemmy comment
134   async fn from_apub(
135     note: &Note,
136     context: &LemmyContext,
137     expected_domain: Option<Url>,
138   ) -> Result<CommentForm, LemmyError> {
139     let creator_actor_id = &note
140       .attributed_to()
141       .context(location_info!())?
142       .as_single_xsd_any_uri()
143       .context(location_info!())?;
144
145     let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
146
147     let mut in_reply_tos = note
148       .in_reply_to()
149       .as_ref()
150       .context(location_info!())?
151       .as_many()
152       .context(location_info!())?
153       .iter()
154       .map(|i| i.as_xsd_any_uri().context(""));
155     let post_ap_id = in_reply_tos.next().context(location_info!())??;
156
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?;
159
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?;
165         let parent_comment =
166           get_or_fetch_and_insert_comment(&parent_comment_ap_id, context).await?;
167
168         Some(parent_comment.id)
169       }
170       None => None,
171     };
172     let content = note
173       .content()
174       .context(location_info!())?
175       .as_single_xsd_string()
176       .context(location_info!())?
177       .to_string();
178     let content_slurs_removed = remove_slurs(&content);
179
180     Ok(CommentForm {
181       creator_id: creator.id,
182       post_id: post.id,
183       parent_id,
184       content: content_slurs_removed,
185       removed: None,
186       read: None,
187       published: note.published().map(|u| u.to_owned().naive_local()),
188       updated: note.updated().map(|u| u.to_owned().naive_local()),
189       deleted: None,
190       ap_id: Some(check_actor_domain(note, expected_domain)?),
191       local: false,
192     })
193   }
194 }
195
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?;
201
202     let post_id = self.post_id;
203     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
204
205     let community_id = post.community_id;
206     let community = blocking(context.pool(), move |conn| {
207       Community::read(conn, community_id)
208     })
209     .await??;
210
211     let maa = collect_non_local_mentions_and_addresses(&self.content, &community, context).await?;
212
213     let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
214     create
215       .set_context(activitystreams::context())
216       .set_id(generate_activity_id(CreateType::Create)?)
217       .set_to(public())
218       .set_many_ccs(maa.addressed_ccs.to_owned())
219       // Set the mention tags
220       .set_many_tags(maa.get_tags()?);
221
222     send_activity_to_community(&creator, &community, maa.inboxes, create, context).await?;
223     Ok(())
224   }
225
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?;
229
230     let post_id = self.post_id;
231     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
232
233     let community_id = post.community_id;
234     let community = blocking(context.pool(), move |conn| {
235       Community::read(conn, community_id)
236     })
237     .await??;
238
239     let maa = collect_non_local_mentions_and_addresses(&self.content, &community, context).await?;
240
241     let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
242     update
243       .set_context(activitystreams::context())
244       .set_id(generate_activity_id(UpdateType::Update)?)
245       .set_to(public())
246       .set_many_ccs(maa.addressed_ccs.to_owned())
247       // Set the mention tags
248       .set_many_tags(maa.get_tags()?);
249
250     send_activity_to_community(&creator, &community, maa.inboxes, update, context).await?;
251     Ok(())
252   }
253
254   async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
255     let note = self.to_apub(context.pool()).await?;
256
257     let post_id = self.post_id;
258     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
259
260     let community_id = post.community_id;
261     let community = blocking(context.pool(), move |conn| {
262       Community::read(conn, community_id)
263     })
264     .await??;
265
266     let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
267     delete
268       .set_context(activitystreams::context())
269       .set_id(generate_activity_id(DeleteType::Delete)?)
270       .set_to(public())
271       .set_many_ccs(vec![community.get_followers_url()?]);
272
273     send_activity_to_community(
274       &creator,
275       &community,
276       vec![community.get_shared_inbox_url()?],
277       delete,
278       context,
279     )
280     .await?;
281     Ok(())
282   }
283
284   async fn send_undo_delete(
285     &self,
286     creator: &User_,
287     context: &LemmyContext,
288   ) -> Result<(), LemmyError> {
289     let note = self.to_apub(context.pool()).await?;
290
291     let post_id = self.post_id;
292     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
293
294     let community_id = post.community_id;
295     let community = blocking(context.pool(), move |conn| {
296       Community::read(conn, community_id)
297     })
298     .await??;
299
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()?);
302     delete
303       .set_context(activitystreams::context())
304       .set_id(generate_activity_id(DeleteType::Delete)?)
305       .set_to(public())
306       .set_many_ccs(vec![community.get_followers_url()?]);
307
308     // Undo that fake activity
309     let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
310     undo
311       .set_context(activitystreams::context())
312       .set_id(generate_activity_id(UndoType::Undo)?)
313       .set_to(public())
314       .set_many_ccs(vec![community.get_followers_url()?]);
315
316     send_activity_to_community(
317       &creator,
318       &community,
319       vec![community.get_shared_inbox_url()?],
320       undo,
321       context,
322     )
323     .await?;
324     Ok(())
325   }
326
327   async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
328     let note = self.to_apub(context.pool()).await?;
329
330     let post_id = self.post_id;
331     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
332
333     let community_id = post.community_id;
334     let community = blocking(context.pool(), move |conn| {
335       Community::read(conn, community_id)
336     })
337     .await??;
338
339     let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
340     remove
341       .set_context(activitystreams::context())
342       .set_id(generate_activity_id(RemoveType::Remove)?)
343       .set_to(public())
344       .set_many_ccs(vec![community.get_followers_url()?]);
345
346     send_activity_to_community(
347       &mod_,
348       &community,
349       vec![community.get_shared_inbox_url()?],
350       remove,
351       context,
352     )
353     .await?;
354     Ok(())
355   }
356
357   async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
358     let note = self.to_apub(context.pool()).await?;
359
360     let post_id = self.post_id;
361     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
362
363     let community_id = post.community_id;
364     let community = blocking(context.pool(), move |conn| {
365       Community::read(conn, community_id)
366     })
367     .await??;
368
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()?);
371     remove
372       .set_context(activitystreams::context())
373       .set_id(generate_activity_id(RemoveType::Remove)?)
374       .set_to(public())
375       .set_many_ccs(vec![community.get_followers_url()?]);
376
377     // Undo that fake activity
378     let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
379     undo
380       .set_context(activitystreams::context())
381       .set_id(generate_activity_id(UndoType::Undo)?)
382       .set_to(public())
383       .set_many_ccs(vec![community.get_followers_url()?]);
384
385     send_activity_to_community(
386       &mod_,
387       &community,
388       vec![community.get_shared_inbox_url()?],
389       undo,
390       context,
391     )
392     .await?;
393     Ok(())
394   }
395 }
396
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?;
401
402     let post_id = self.post_id;
403     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
404
405     let community_id = post.community_id;
406     let community = blocking(context.pool(), move |conn| {
407       Community::read(conn, community_id)
408     })
409     .await??;
410
411     let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
412     like
413       .set_context(activitystreams::context())
414       .set_id(generate_activity_id(LikeType::Like)?)
415       .set_to(public())
416       .set_many_ccs(vec![community.get_followers_url()?]);
417
418     send_activity_to_community(
419       &creator,
420       &community,
421       vec![community.get_shared_inbox_url()?],
422       like,
423       context,
424     )
425     .await?;
426     Ok(())
427   }
428
429   async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
430     let note = self.to_apub(context.pool()).await?;
431
432     let post_id = self.post_id;
433     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
434
435     let community_id = post.community_id;
436     let community = blocking(context.pool(), move |conn| {
437       Community::read(conn, community_id)
438     })
439     .await??;
440
441     let mut dislike = Dislike::new(creator.actor_id.to_owned(), note.into_any_base()?);
442     dislike
443       .set_context(activitystreams::context())
444       .set_id(generate_activity_id(DislikeType::Dislike)?)
445       .set_to(public())
446       .set_many_ccs(vec![community.get_followers_url()?]);
447
448     send_activity_to_community(
449       &creator,
450       &community,
451       vec![community.get_shared_inbox_url()?],
452       dislike,
453       context,
454     )
455     .await?;
456     Ok(())
457   }
458
459   async fn send_undo_like(
460     &self,
461     creator: &User_,
462     context: &LemmyContext,
463   ) -> Result<(), LemmyError> {
464     let note = self.to_apub(context.pool()).await?;
465
466     let post_id = self.post_id;
467     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
468
469     let community_id = post.community_id;
470     let community = blocking(context.pool(), move |conn| {
471       Community::read(conn, community_id)
472     })
473     .await??;
474
475     let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
476     like
477       .set_context(activitystreams::context())
478       .set_id(generate_activity_id(DislikeType::Dislike)?)
479       .set_to(public())
480       .set_many_ccs(vec![community.get_followers_url()?]);
481
482     // Undo that fake activity
483     let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
484     undo
485       .set_context(activitystreams::context())
486       .set_id(generate_activity_id(UndoType::Undo)?)
487       .set_to(public())
488       .set_many_ccs(vec![community.get_followers_url()?]);
489
490     send_activity_to_community(
491       &creator,
492       &community,
493       vec![community.get_shared_inbox_url()?],
494       undo,
495       context,
496     )
497     .await?;
498     Ok(())
499   }
500 }
501
502 struct MentionsAndAddresses {
503   addressed_ccs: Vec<Url>,
504   inboxes: Vec<Url>,
505   tags: Vec<Mention>,
506 }
507
508 impl MentionsAndAddresses {
509   fn get_tags(&self) -> Result<Vec<AnyBase>, Error> {
510     self
511       .tags
512       .iter()
513       .map(|t| t.to_owned().into_any_base())
514       .collect::<Result<Vec<AnyBase>, Error>>()
515   }
516 }
517
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(
522   content: &str,
523   community: &Community,
524   context: &LemmyContext,
525 ) -> Result<MentionsAndAddresses, LemmyError> {
526   let mut addressed_ccs = vec![community.get_followers_url()?];
527
528   // Add the mention tag
529   let mut tags = Vec::new();
530
531   // Get the inboxes for any mentions
532   let mentions = scrape_text_for_mentions(&content)
533     .into_iter()
534     // Filter only the non-local ones
535     .filter(|m| !m.is_local())
536     .collect::<Vec<MentionData>>();
537
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()?);
544
545       let mention_user = get_or_fetch_and_upsert_user(&actor_id, context).await?;
546       let shared_inbox = mention_user.get_shared_inbox_url()?;
547
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);
552     }
553   }
554
555   let mut inboxes = vec![community.get_shared_inbox_url()?];
556   inboxes.extend(mention_inboxes);
557   inboxes = inboxes.into_iter().unique().collect();
558
559   Ok(MentionsAndAddresses {
560     addressed_ccs,
561     inboxes,
562     tags,
563   })
564 }