]> Untitled Git - lemmy.git/blob - crates/apub/src/activities/send/comment.rs
Move most code into crates/ subfolder
[lemmy.git] / crates / apub / src / activities / send / comment.rs
1 use crate::{
2   activities::send::generate_activity_id,
3   activity_queue::{send_comment_mentions, send_to_community},
4   extensions::context::lemmy_context,
5   fetcher::user::get_or_fetch_and_upsert_user,
6   objects::ToApub,
7   ActorType,
8   ApubLikeableType,
9   ApubObjectType,
10 };
11 use activitystreams::{
12   activity::{
13     kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
14     Create,
15     Delete,
16     Dislike,
17     Like,
18     Remove,
19     Undo,
20     Update,
21   },
22   base::AnyBase,
23   link::Mention,
24   prelude::*,
25   public,
26 };
27 use anyhow::anyhow;
28 use itertools::Itertools;
29 use lemmy_db_queries::{Crud, DbPool};
30 use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post, user::User_};
31 use lemmy_structs::{blocking, WebFingerResponse};
32 use lemmy_utils::{
33   request::{retry, RecvError},
34   settings::Settings,
35   utils::{scrape_text_for_mentions, MentionData},
36   LemmyError,
37 };
38 use lemmy_websocket::LemmyContext;
39 use log::debug;
40 use reqwest::Client;
41 use serde_json::Error;
42 use url::Url;
43
44 #[async_trait::async_trait(?Send)]
45 impl ApubObjectType for Comment {
46   /// Send out information about a newly created comment, to the followers of the community and
47   /// mentioned users.
48   async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
49     let note = self.to_apub(context.pool()).await?;
50
51     let post_id = self.post_id;
52     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
53
54     let community_id = post.community_id;
55     let community = blocking(context.pool(), move |conn| {
56       Community::read(conn, community_id)
57     })
58     .await??;
59
60     let maa = collect_non_local_mentions(&self, &community, context).await?;
61
62     let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
63     create
64       .set_many_contexts(lemmy_context()?)
65       .set_id(generate_activity_id(CreateType::Create)?)
66       .set_to(public())
67       .set_many_ccs(maa.ccs.to_owned())
68       // Set the mention tags
69       .set_many_tags(maa.get_tags()?);
70
71     send_to_community(create.clone(), &creator, &community, context).await?;
72     send_comment_mentions(&creator, maa.inboxes, create, context).await?;
73     Ok(())
74   }
75
76   /// Send out information about an edited post, to the followers of the community and mentioned
77   /// users.
78   async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
79     let note = self.to_apub(context.pool()).await?;
80
81     let post_id = self.post_id;
82     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
83
84     let community_id = post.community_id;
85     let community = blocking(context.pool(), move |conn| {
86       Community::read(conn, community_id)
87     })
88     .await??;
89
90     let maa = collect_non_local_mentions(&self, &community, context).await?;
91
92     let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
93     update
94       .set_many_contexts(lemmy_context()?)
95       .set_id(generate_activity_id(UpdateType::Update)?)
96       .set_to(public())
97       .set_many_ccs(maa.ccs.to_owned())
98       // Set the mention tags
99       .set_many_tags(maa.get_tags()?);
100
101     send_to_community(update.clone(), &creator, &community, context).await?;
102     send_comment_mentions(&creator, maa.inboxes, update, context).await?;
103     Ok(())
104   }
105
106   async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
107     let post_id = self.post_id;
108     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
109
110     let community_id = post.community_id;
111     let community = blocking(context.pool(), move |conn| {
112       Community::read(conn, community_id)
113     })
114     .await??;
115
116     let mut delete = Delete::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
117     delete
118       .set_many_contexts(lemmy_context()?)
119       .set_id(generate_activity_id(DeleteType::Delete)?)
120       .set_to(public())
121       .set_many_ccs(vec![community.actor_id()?]);
122
123     send_to_community(delete, &creator, &community, context).await?;
124     Ok(())
125   }
126
127   async fn send_undo_delete(
128     &self,
129     creator: &User_,
130     context: &LemmyContext,
131   ) -> Result<(), LemmyError> {
132     let post_id = self.post_id;
133     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
134
135     let community_id = post.community_id;
136     let community = blocking(context.pool(), move |conn| {
137       Community::read(conn, community_id)
138     })
139     .await??;
140
141     // Generate a fake delete activity, with the correct object
142     let mut delete = Delete::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
143     delete
144       .set_many_contexts(lemmy_context()?)
145       .set_id(generate_activity_id(DeleteType::Delete)?)
146       .set_to(public())
147       .set_many_ccs(vec![community.actor_id()?]);
148
149     // Undo that fake activity
150     let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
151     undo
152       .set_many_contexts(lemmy_context()?)
153       .set_id(generate_activity_id(UndoType::Undo)?)
154       .set_to(public())
155       .set_many_ccs(vec![community.actor_id()?]);
156
157     send_to_community(undo, &creator, &community, context).await?;
158     Ok(())
159   }
160
161   async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
162     let post_id = self.post_id;
163     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
164
165     let community_id = post.community_id;
166     let community = blocking(context.pool(), move |conn| {
167       Community::read(conn, community_id)
168     })
169     .await??;
170
171     let mut remove = Remove::new(mod_.actor_id.to_owned(), Url::parse(&self.ap_id)?);
172     remove
173       .set_many_contexts(lemmy_context()?)
174       .set_id(generate_activity_id(RemoveType::Remove)?)
175       .set_to(public())
176       .set_many_ccs(vec![community.actor_id()?]);
177
178     send_to_community(remove, &mod_, &community, context).await?;
179     Ok(())
180   }
181
182   async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
183     let post_id = self.post_id;
184     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
185
186     let community_id = post.community_id;
187     let community = blocking(context.pool(), move |conn| {
188       Community::read(conn, community_id)
189     })
190     .await??;
191
192     // Generate a fake delete activity, with the correct object
193     let mut remove = Remove::new(mod_.actor_id.to_owned(), Url::parse(&self.ap_id)?);
194     remove
195       .set_many_contexts(lemmy_context()?)
196       .set_id(generate_activity_id(RemoveType::Remove)?)
197       .set_to(public())
198       .set_many_ccs(vec![community.actor_id()?]);
199
200     // Undo that fake activity
201     let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
202     undo
203       .set_many_contexts(lemmy_context()?)
204       .set_id(generate_activity_id(UndoType::Undo)?)
205       .set_to(public())
206       .set_many_ccs(vec![community.actor_id()?]);
207
208     send_to_community(undo, &mod_, &community, context).await?;
209     Ok(())
210   }
211 }
212
213 #[async_trait::async_trait(?Send)]
214 impl ApubLikeableType for Comment {
215   async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
216     let post_id = self.post_id;
217     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
218
219     let community_id = post.community_id;
220     let community = blocking(context.pool(), move |conn| {
221       Community::read(conn, community_id)
222     })
223     .await??;
224
225     let mut like = Like::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
226     like
227       .set_many_contexts(lemmy_context()?)
228       .set_id(generate_activity_id(LikeType::Like)?)
229       .set_to(public())
230       .set_many_ccs(vec![community.actor_id()?]);
231
232     send_to_community(like, &creator, &community, context).await?;
233     Ok(())
234   }
235
236   async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
237     let post_id = self.post_id;
238     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
239
240     let community_id = post.community_id;
241     let community = blocking(context.pool(), move |conn| {
242       Community::read(conn, community_id)
243     })
244     .await??;
245
246     let mut dislike = Dislike::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
247     dislike
248       .set_many_contexts(lemmy_context()?)
249       .set_id(generate_activity_id(DislikeType::Dislike)?)
250       .set_to(public())
251       .set_many_ccs(vec![community.actor_id()?]);
252
253     send_to_community(dislike, &creator, &community, context).await?;
254     Ok(())
255   }
256
257   async fn send_undo_like(
258     &self,
259     creator: &User_,
260     context: &LemmyContext,
261   ) -> Result<(), LemmyError> {
262     let post_id = self.post_id;
263     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
264
265     let community_id = post.community_id;
266     let community = blocking(context.pool(), move |conn| {
267       Community::read(conn, community_id)
268     })
269     .await??;
270
271     let mut like = Like::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
272     like
273       .set_many_contexts(lemmy_context()?)
274       .set_id(generate_activity_id(DislikeType::Dislike)?)
275       .set_to(public())
276       .set_many_ccs(vec![community.actor_id()?]);
277
278     // Undo that fake activity
279     let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
280     undo
281       .set_many_contexts(lemmy_context()?)
282       .set_id(generate_activity_id(UndoType::Undo)?)
283       .set_to(public())
284       .set_many_ccs(vec![community.actor_id()?]);
285
286     send_to_community(undo, &creator, &community, context).await?;
287     Ok(())
288   }
289 }
290
291 struct MentionsAndAddresses {
292   ccs: Vec<Url>,
293   inboxes: Vec<Url>,
294   tags: Vec<Mention>,
295 }
296
297 impl MentionsAndAddresses {
298   fn get_tags(&self) -> Result<Vec<AnyBase>, Error> {
299     self
300       .tags
301       .iter()
302       .map(|t| t.to_owned().into_any_base())
303       .collect::<Result<Vec<AnyBase>, Error>>()
304   }
305 }
306
307 /// This takes a comment, and builds a list of to_addresses, inboxes,
308 /// and mention tags, so they know where to be sent to.
309 /// Addresses are the users / addresses that go in the cc field.
310 async fn collect_non_local_mentions(
311   comment: &Comment,
312   community: &Community,
313   context: &LemmyContext,
314 ) -> Result<MentionsAndAddresses, LemmyError> {
315   let parent_creator = get_comment_parent_creator(context.pool(), comment).await?;
316   let mut addressed_ccs = vec![community.actor_id()?, parent_creator.actor_id()?];
317   // Note: dont include community inbox here, as we send to it separately with `send_to_community()`
318   let mut inboxes = vec![parent_creator.get_shared_inbox_url()?];
319
320   // Add the mention tag
321   let mut tags = Vec::new();
322
323   // Get the user IDs for any mentions
324   let mentions = scrape_text_for_mentions(&comment.content)
325     .into_iter()
326     // Filter only the non-local ones
327     .filter(|m| !m.is_local())
328     .collect::<Vec<MentionData>>();
329
330   for mention in &mentions {
331     // TODO should it be fetching it every time?
332     if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await {
333       debug!("mention actor_id: {}", actor_id);
334       addressed_ccs.push(actor_id.to_owned().to_string().parse()?);
335
336       let mention_user = get_or_fetch_and_upsert_user(&actor_id, context, &mut 0).await?;
337       inboxes.push(mention_user.get_shared_inbox_url()?);
338
339       let mut mention_tag = Mention::new();
340       mention_tag.set_href(actor_id).set_name(mention.full_name());
341       tags.push(mention_tag);
342     }
343   }
344
345   let inboxes = inboxes.into_iter().unique().collect();
346
347   Ok(MentionsAndAddresses {
348     ccs: addressed_ccs,
349     inboxes,
350     tags,
351   })
352 }
353
354 /// Returns the apub ID of the user this comment is responding to. Meaning, in case this is a
355 /// top-level comment, the creator of the post, otherwise the creator of the parent comment.
356 async fn get_comment_parent_creator(pool: &DbPool, comment: &Comment) -> Result<User_, LemmyError> {
357   let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id {
358     let parent_comment =
359       blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??;
360     parent_comment.creator_id
361   } else {
362     let parent_post_id = comment.post_id;
363     let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
364     parent_post.creator_id
365   };
366   Ok(blocking(pool, move |conn| User_::read(conn, parent_creator_id)).await??)
367 }
368
369 /// Turns a user id like `@name@example.com` into an apub ID, like `https://example.com/user/name`,
370 /// using webfinger.
371 async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result<Url, LemmyError> {
372   let fetch_url = format!(
373     "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
374     Settings::get().get_protocol_string(),
375     mention.domain,
376     mention.name,
377     mention.domain
378   );
379   debug!("Fetching webfinger url: {}", &fetch_url);
380
381   let response = retry(|| client.get(&fetch_url).send()).await?;
382
383   let res: WebFingerResponse = response
384     .json()
385     .await
386     .map_err(|e| RecvError(e.to_string()))?;
387
388   let link = res
389     .links
390     .iter()
391     .find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
392     .ok_or_else(|| anyhow!("No application/activity+json link found."))?;
393   link
394     .href
395     .to_owned()
396     .map(|u| Url::parse(&u))
397     .transpose()?
398     .ok_or_else(|| anyhow!("No href found.").into())
399 }