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