]> Untitled Git - lemmy.git/blob - crates/apub/src/inbox/receive_for_community.rs
Refactor activitypub code
[lemmy.git] / crates / apub / src / inbox / receive_for_community.rs
1 use crate::{
2   activities::receive::{
3     comment::{
4       receive_create_comment,
5       receive_delete_comment,
6       receive_dislike_comment,
7       receive_like_comment,
8       receive_remove_comment,
9       receive_update_comment,
10     },
11     comment_undo::{
12       receive_undo_delete_comment,
13       receive_undo_dislike_comment,
14       receive_undo_like_comment,
15       receive_undo_remove_comment,
16     },
17     post::{
18       receive_create_post,
19       receive_delete_post,
20       receive_dislike_post,
21       receive_like_post,
22       receive_remove_post,
23       receive_update_post,
24     },
25     post_undo::{
26       receive_undo_delete_post,
27       receive_undo_dislike_post,
28       receive_undo_like_post,
29       receive_undo_remove_post,
30     },
31     receive_unhandled_activity,
32     verify_activity_domains_valid,
33   },
34   fetcher::{
35     objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
36     user::get_or_fetch_and_upsert_user,
37   },
38   find_object_by_id,
39   find_post_or_comment_by_id,
40   generate_moderators_url,
41   inbox::verify_is_addressed_to_public,
42   ActorType,
43   CommunityType,
44   Object,
45   PostOrComment,
46 };
47 use activitystreams::{
48   activity::{
49     ActorAndObjectRef,
50     Add,
51     Announce,
52     Create,
53     Delete,
54     Dislike,
55     Like,
56     OptTargetRef,
57     Remove,
58     Undo,
59     Update,
60   },
61   base::AnyBase,
62   object::AsObject,
63   prelude::*,
64 };
65 use anyhow::{anyhow, Context};
66 use diesel::result::Error::NotFound;
67 use lemmy_api_structs::blocking;
68 use lemmy_db_queries::{source::community::CommunityModerator_, ApubObject, Crud, Joinable};
69 use lemmy_db_schema::{
70   source::{
71     community::{Community, CommunityModerator, CommunityModeratorForm},
72     site::Site,
73     user::User_,
74   },
75   DbUrl,
76 };
77 use lemmy_db_views_actor::community_view::CommunityView;
78 use lemmy_utils::{location_info, LemmyError};
79 use lemmy_websocket::LemmyContext;
80 use strum_macros::EnumString;
81 use url::Url;
82
83 #[derive(EnumString)]
84 enum PageOrNote {
85   Page,
86   Note,
87 }
88
89 /// This file is for post/comment activities received by the community, and for post/comment
90 ///       activities announced by the community and received by the user.
91
92 /// A post or comment being created
93 pub(in crate::inbox) async fn receive_create_for_community(
94   context: &LemmyContext,
95   activity: AnyBase,
96   expected_domain: &Url,
97   request_counter: &mut i32,
98 ) -> Result<(), LemmyError> {
99   let create = Create::from_any_base(activity)?.context(location_info!())?;
100   verify_activity_domains_valid(&create, &expected_domain, true)?;
101   verify_is_addressed_to_public(&create)?;
102
103   let kind = create
104     .object()
105     .as_single_kind_str()
106     .and_then(|s| s.parse().ok());
107   match kind {
108     Some(PageOrNote::Page) => receive_create_post(create, context, request_counter).await,
109     Some(PageOrNote::Note) => receive_create_comment(create, context, request_counter).await,
110     _ => receive_unhandled_activity(create),
111   }
112 }
113
114 /// A post or comment being edited
115 pub(in crate::inbox) async fn receive_update_for_community(
116   context: &LemmyContext,
117   activity: AnyBase,
118   announce: Option<Announce>,
119   expected_domain: &Url,
120   request_counter: &mut i32,
121 ) -> Result<(), LemmyError> {
122   let update = Update::from_any_base(activity)?.context(location_info!())?;
123   verify_activity_domains_valid(&update, &expected_domain, false)?;
124   verify_is_addressed_to_public(&update)?;
125   verify_modification_actor_instance(&update, &announce, context).await?;
126
127   let kind = update
128     .object()
129     .as_single_kind_str()
130     .and_then(|s| s.parse().ok());
131   match kind {
132     Some(PageOrNote::Page) => receive_update_post(update, announce, context, request_counter).await,
133     Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await,
134     _ => receive_unhandled_activity(update),
135   }
136 }
137
138 /// A post or comment being upvoted
139 pub(in crate::inbox) async fn receive_like_for_community(
140   context: &LemmyContext,
141   activity: AnyBase,
142   expected_domain: &Url,
143   request_counter: &mut i32,
144 ) -> Result<(), LemmyError> {
145   let like = Like::from_any_base(activity)?.context(location_info!())?;
146   verify_activity_domains_valid(&like, &expected_domain, false)?;
147   verify_is_addressed_to_public(&like)?;
148
149   let object_id = like
150     .object()
151     .as_single_xsd_any_uri()
152     .context(location_info!())?;
153   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
154     PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await,
155     PostOrComment::Comment(comment) => {
156       receive_like_comment(like, *comment, context, request_counter).await
157     }
158   }
159 }
160
161 /// A post or comment being downvoted
162 pub(in crate::inbox) async fn receive_dislike_for_community(
163   context: &LemmyContext,
164   activity: AnyBase,
165   expected_domain: &Url,
166   request_counter: &mut i32,
167 ) -> Result<(), LemmyError> {
168   let enable_downvotes = blocking(context.pool(), move |conn| {
169     Site::read(conn, 1).map(|s| s.enable_downvotes)
170   })
171   .await??;
172   if !enable_downvotes {
173     return Ok(());
174   }
175
176   let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
177   verify_activity_domains_valid(&dislike, &expected_domain, false)?;
178   verify_is_addressed_to_public(&dislike)?;
179
180   let object_id = dislike
181     .object()
182     .as_single_xsd_any_uri()
183     .context(location_info!())?;
184   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
185     PostOrComment::Post(post) => {
186       receive_dislike_post(dislike, *post, context, request_counter).await
187     }
188     PostOrComment::Comment(comment) => {
189       receive_dislike_comment(dislike, *comment, context, request_counter).await
190     }
191   }
192 }
193
194 /// A post or comment being deleted by its creator
195 pub(in crate::inbox) async fn receive_delete_for_community(
196   context: &LemmyContext,
197   activity: AnyBase,
198   announce: Option<Announce>,
199   expected_domain: &Url,
200 ) -> Result<(), LemmyError> {
201   let delete = Delete::from_any_base(activity)?.context(location_info!())?;
202   verify_activity_domains_valid(&delete, &expected_domain, true)?;
203   verify_is_addressed_to_public(&delete)?;
204   verify_modification_actor_instance(&delete, &announce, context).await?;
205
206   let object = delete
207     .object()
208     .to_owned()
209     .single_xsd_any_uri()
210     .context(location_info!())?;
211
212   match find_post_or_comment_by_id(context, object).await {
213     Ok(PostOrComment::Post(p)) => receive_delete_post(context, *p).await,
214     Ok(PostOrComment::Comment(c)) => receive_delete_comment(context, *c).await,
215     // if we dont have the object, no need to do anything
216     Err(_) => Ok(()),
217   }
218 }
219
220 /// A post or comment being removed by a mod/admin
221 pub(in crate::inbox) async fn receive_remove_for_community(
222   context: &LemmyContext,
223   remove_any_base: AnyBase,
224   announce: Option<Announce>,
225   request_counter: &mut i32,
226 ) -> Result<(), LemmyError> {
227   let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?;
228   let community = extract_community_from_cc(&remove, context).await?;
229
230   verify_mod_activity(&remove, announce, &community, context).await?;
231   verify_is_addressed_to_public(&remove)?;
232
233   if remove.target().is_some() {
234     let remove_mod = remove
235       .object()
236       .as_single_xsd_any_uri()
237       .context(location_info!())?;
238     let remove_mod = get_or_fetch_and_upsert_user(&remove_mod, context, request_counter).await?;
239     let form = CommunityModeratorForm {
240       community_id: community.id,
241       user_id: remove_mod.id,
242     };
243     blocking(context.pool(), move |conn| {
244       CommunityModerator::leave(conn, &form)
245     })
246     .await??;
247     community.send_announce(remove_any_base, context).await?;
248     // TODO: send websocket notification about removed mod
249     Ok(())
250   }
251   // Remove a post or comment
252   else {
253     let object = remove
254       .object()
255       .to_owned()
256       .single_xsd_any_uri()
257       .context(location_info!())?;
258
259     match find_post_or_comment_by_id(context, object).await {
260       Ok(PostOrComment::Post(p)) => receive_remove_post(context, *p).await,
261       Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, *c).await,
262       // if we dont have the object, no need to do anything
263       Err(_) => Ok(()),
264     }
265   }
266 }
267
268 #[derive(EnumString)]
269 enum UndoableActivities {
270   Delete,
271   Remove,
272   Like,
273   Dislike,
274 }
275
276 /// A post/comment action being reverted (either a delete, remove, upvote or downvote)
277 pub(in crate::inbox) async fn receive_undo_for_community(
278   context: &LemmyContext,
279   activity: AnyBase,
280   announce: Option<Announce>,
281   expected_domain: &Url,
282   request_counter: &mut i32,
283 ) -> Result<(), LemmyError> {
284   let undo = Undo::from_any_base(activity)?.context(location_info!())?;
285   verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
286   verify_is_addressed_to_public(&undo)?;
287
288   use UndoableActivities::*;
289   match undo
290     .object()
291     .as_single_kind_str()
292     .and_then(|s| s.parse().ok())
293   {
294     Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
295     Some(Remove) => {
296       receive_undo_remove_for_community(context, undo, announce, expected_domain).await
297     }
298     Some(Like) => {
299       receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
300     }
301     Some(Dislike) => {
302       receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
303     }
304     _ => receive_unhandled_activity(undo),
305   }
306 }
307
308 /// A post or comment deletion being reverted
309 pub(in crate::inbox) async fn receive_undo_delete_for_community(
310   context: &LemmyContext,
311   undo: Undo,
312   expected_domain: &Url,
313 ) -> Result<(), LemmyError> {
314   let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
315     .context(location_info!())?;
316   verify_activity_domains_valid(&delete, &expected_domain, true)?;
317   verify_is_addressed_to_public(&delete)?;
318
319   let object = delete
320     .object()
321     .to_owned()
322     .single_xsd_any_uri()
323     .context(location_info!())?;
324   match find_post_or_comment_by_id(context, object).await {
325     Ok(PostOrComment::Post(p)) => receive_undo_delete_post(context, *p).await,
326     Ok(PostOrComment::Comment(c)) => receive_undo_delete_comment(context, *c).await,
327     // if we dont have the object, no need to do anything
328     Err(_) => Ok(()),
329   }
330 }
331
332 /// A post or comment removal being reverted
333 pub(in crate::inbox) async fn receive_undo_remove_for_community(
334   context: &LemmyContext,
335   undo: Undo,
336   announce: Option<Announce>,
337   expected_domain: &Url,
338 ) -> Result<(), LemmyError> {
339   let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
340     .context(location_info!())?;
341   verify_activity_domains_valid(&remove, &expected_domain, false)?;
342   verify_is_addressed_to_public(&remove)?;
343   verify_undo_remove_actor_instance(&undo, &remove, &announce, context).await?;
344
345   let object = remove
346     .object()
347     .to_owned()
348     .single_xsd_any_uri()
349     .context(location_info!())?;
350   match find_post_or_comment_by_id(context, object).await {
351     Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, *p).await,
352     Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, *c).await,
353     // if we dont have the object, no need to do anything
354     Err(_) => Ok(()),
355   }
356 }
357
358 /// A post or comment upvote being reverted
359 pub(in crate::inbox) async fn receive_undo_like_for_community(
360   context: &LemmyContext,
361   undo: Undo,
362   expected_domain: &Url,
363   request_counter: &mut i32,
364 ) -> Result<(), LemmyError> {
365   let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
366     .context(location_info!())?;
367   verify_activity_domains_valid(&like, &expected_domain, false)?;
368   verify_is_addressed_to_public(&like)?;
369
370   let object_id = like
371     .object()
372     .as_single_xsd_any_uri()
373     .context(location_info!())?;
374   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
375     PostOrComment::Post(post) => {
376       receive_undo_like_post(&like, *post, context, request_counter).await
377     }
378     PostOrComment::Comment(comment) => {
379       receive_undo_like_comment(&like, *comment, context, request_counter).await
380     }
381   }
382 }
383
384 /// Add a new mod to the community (can only be done by an existing mod).
385 pub(in crate::inbox) async fn receive_add_for_community(
386   context: &LemmyContext,
387   add_any_base: AnyBase,
388   announce: Option<Announce>,
389   request_counter: &mut i32,
390 ) -> Result<(), LemmyError> {
391   let add = Add::from_any_base(add_any_base.to_owned())?.context(location_info!())?;
392   let community = extract_community_from_cc(&add, context).await?;
393
394   verify_mod_activity(&add, announce, &community, context).await?;
395   verify_is_addressed_to_public(&add)?;
396   verify_add_remove_moderator_target(&add, &community)?;
397
398   let new_mod = add
399     .object()
400     .as_single_xsd_any_uri()
401     .context(location_info!())?;
402   let new_mod = get_or_fetch_and_upsert_user(&new_mod, context, request_counter).await?;
403
404   // If we had to refetch the community while parsing the activity, then the new mod has already
405   // been added. Skip it here as it would result in a duplicate key error.
406   let new_mod_id = new_mod.id;
407   let moderated_communities = blocking(context.pool(), move |conn| {
408     CommunityModerator::get_user_moderated_communities(conn, new_mod_id)
409   })
410   .await??;
411   if !moderated_communities.contains(&community.id) {
412     let form = CommunityModeratorForm {
413       community_id: community.id,
414       user_id: new_mod.id,
415     };
416     blocking(context.pool(), move |conn| {
417       CommunityModerator::join(conn, &form)
418     })
419     .await??;
420   }
421   if community.local {
422     community.send_announce(add_any_base, context).await?;
423   }
424   // TODO: send websocket notification about added mod
425   Ok(())
426 }
427
428 /// A post or comment downvote being reverted
429 pub(in crate::inbox) async fn receive_undo_dislike_for_community(
430   context: &LemmyContext,
431   undo: Undo,
432   expected_domain: &Url,
433   request_counter: &mut i32,
434 ) -> Result<(), LemmyError> {
435   let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
436     .context(location_info!())?;
437   verify_activity_domains_valid(&dislike, &expected_domain, false)?;
438   verify_is_addressed_to_public(&dislike)?;
439
440   let object_id = dislike
441     .object()
442     .as_single_xsd_any_uri()
443     .context(location_info!())?;
444   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
445     PostOrComment::Post(post) => {
446       receive_undo_dislike_post(&dislike, *post, context, request_counter).await
447     }
448     PostOrComment::Comment(comment) => {
449       receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
450     }
451   }
452 }
453
454 async fn fetch_post_or_comment_by_id(
455   apub_id: &Url,
456   context: &LemmyContext,
457   request_counter: &mut i32,
458 ) -> Result<PostOrComment, LemmyError> {
459   if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await {
460     return Ok(PostOrComment::Post(Box::new(post)));
461   }
462
463   if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
464     return Ok(PostOrComment::Comment(Box::new(comment)));
465   }
466
467   Err(NotFound.into())
468 }
469
470 /// Searches the activity's cc field for a Community ID, and returns the community.
471 async fn extract_community_from_cc<T, Kind>(
472   activity: &T,
473   context: &LemmyContext,
474 ) -> Result<Community, LemmyError>
475 where
476   T: AsObject<Kind>,
477 {
478   let cc = activity
479     .cc()
480     .map(|c| c.as_many())
481     .flatten()
482     .context(location_info!())?;
483   let community_id = cc
484     .first()
485     .map(|c| c.as_xsd_any_uri())
486     .flatten()
487     .context(location_info!())?;
488   let community_id: DbUrl = community_id.to_owned().into();
489   let community = blocking(&context.pool(), move |conn| {
490     Community::read_from_apub_id(&conn, &community_id)
491   })
492   .await??;
493   Ok(community)
494 }
495
496 /// Checks that a moderation activity was sent by a user who is listed as mod for the community.
497 /// This is only used in the case of remote mods, as local mod actions don't go through the
498 /// community inbox.
499 ///
500 /// This method should only be used for activities received by the community, not for activities
501 /// used by community followers.
502 async fn verify_actor_is_community_mod<T, Kind>(
503   activity: &T,
504   community: &Community,
505   context: &LemmyContext,
506 ) -> Result<(), LemmyError>
507 where
508   T: ActorAndObjectRef + BaseExt<Kind>,
509 {
510   let actor = activity
511     .actor()?
512     .as_single_xsd_any_uri()
513     .context(location_info!())?
514     .to_owned();
515   let actor = blocking(&context.pool(), move |conn| {
516     User_::read_from_apub_id(&conn, &actor.into())
517   })
518   .await??;
519
520   // Note: this will also return true for admins in addition to mods, but as we dont know about
521   //       remote admins, it doesnt make any difference.
522   let community_id = community.id;
523   let actor_id = actor.id;
524   let is_mod_or_admin = blocking(context.pool(), move |conn| {
525     CommunityView::is_mod_or_admin(conn, actor_id, community_id)
526   })
527   .await?;
528   if !is_mod_or_admin {
529     return Err(anyhow!("Not a mod").into());
530   }
531
532   Ok(())
533 }
534
535 /// This method behaves differently, depending if it is called via community inbox (activity
536 /// received by community from a remote user), or via user inbox (activity received by user from
537 /// community). We distinguish the cases by checking if the activity is wrapper in an announce
538 /// (only true when sent from user to community).
539 ///
540 /// In the first case, we check that the actor is listed as community mod. In the second case, we
541 /// only check that the announce comes from the same domain as the activity. We trust the
542 /// community's instance to have validated the inner activity correctly. We can't do this validation
543 /// here, because we don't know who the instance admins are. Plus this allows for compatibility with
544 /// software that uses different rules for mod actions.
545 pub(crate) async fn verify_mod_activity<T, Kind>(
546   mod_action: &T,
547   announce: Option<Announce>,
548   community: &Community,
549   context: &LemmyContext,
550 ) -> Result<(), LemmyError>
551 where
552   T: ActorAndObjectRef + BaseExt<Kind>,
553 {
554   match announce {
555     None => verify_actor_is_community_mod(mod_action, community, context).await?,
556     Some(a) => verify_activity_domains_valid(&a, &community.actor_id.to_owned().into(), false)?,
557   }
558
559   Ok(())
560 }
561
562 /// For Add/Remove community moderator activities, check that the target field actually contains
563 /// /c/community/moderators. Any different values are unsupported.
564 fn verify_add_remove_moderator_target<T, Kind>(
565   activity: &T,
566   community: &Community,
567 ) -> Result<(), LemmyError>
568 where
569   T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
570 {
571   let target = activity
572     .target()
573     .map(|t| t.as_single_xsd_any_uri())
574     .flatten()
575     .context(location_info!())?;
576   if target != &generate_moderators_url(&community.actor_id)?.into_inner() {
577     return Err(anyhow!("Unkown target url").into());
578   }
579   Ok(())
580 }
581
582 /// For activities like Update, Delete or Remove, check that the actor is from the same instance
583 /// as the original object itself (or is a remote mod).
584 ///
585 /// Note: This is only needed for mod actions. Normal user actions (edit post, undo vote etc) are
586 ///       already verified with `expected_domain`, so this serves as an additional check.
587 async fn verify_modification_actor_instance<T, Kind>(
588   activity: &T,
589   announce: &Option<Announce>,
590   context: &LemmyContext,
591 ) -> Result<(), LemmyError>
592 where
593   T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
594 {
595   let actor_id = activity
596     .actor()?
597     .to_owned()
598     .single_xsd_any_uri()
599     .context(location_info!())?;
600   let object_id = activity
601     .object()
602     .as_one()
603     .map(|o| o.id())
604     .flatten()
605     .context(location_info!())?;
606   let original_id = match find_object_by_id(context, object_id.to_owned()).await? {
607     Object::Post(p) => p.ap_id.into_inner(),
608     Object::Comment(c) => c.ap_id.into_inner(),
609     Object::Community(c) => c.actor_id(),
610     Object::User(u) => u.actor_id(),
611     Object::PrivateMessage(p) => p.ap_id.into_inner(),
612   };
613   if actor_id.domain() != original_id.domain() {
614     let community = extract_community_from_cc(activity, context).await?;
615     verify_mod_activity(activity, announce.to_owned(), &community, context).await?;
616   }
617
618   Ok(())
619 }
620
621 pub(crate) async fn verify_undo_remove_actor_instance<T, Kind>(
622   undo: &Undo,
623   inner: &T,
624   announce: &Option<Announce>,
625   context: &LemmyContext,
626 ) -> Result<(), LemmyError>
627 where
628   T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
629 {
630   if announce.is_none() {
631     let community = extract_community_from_cc(undo, context).await?;
632     verify_mod_activity(undo, announce.to_owned(), &community, context).await?;
633     verify_mod_activity(inner, announce.to_owned(), &community, context).await?;
634   }
635
636   Ok(())
637 }