]> Untitled Git - lemmy.git/blob - crates/apub/src/inbox/receive_for_community.rs
438a8b3d850ac1c7f0308069f24adc353d25ad6c
[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_post_or_comment_by_id,
39   inbox::is_addressed_to_public,
40   PostOrComment,
41 };
42 use activitystreams::{
43   activity::{
44     ActorAndObjectRef,
45     Add,
46     Create,
47     Delete,
48     Dislike,
49     Like,
50     OptTargetRef,
51     Remove,
52     Undo,
53     Update,
54   },
55   base::AnyBase,
56   prelude::*,
57 };
58 use anyhow::{anyhow, Context};
59 use diesel::result::Error::NotFound;
60 use lemmy_api_structs::blocking;
61 use lemmy_db_queries::{ApubObject, Crud, Joinable};
62 use lemmy_db_schema::{
63   source::{
64     community::{Community, CommunityModerator, CommunityModeratorForm},
65     site::Site,
66     user::User_,
67   },
68   DbUrl,
69 };
70 use lemmy_db_views_actor::community_view::CommunityView;
71 use lemmy_utils::{location_info, LemmyError};
72 use lemmy_websocket::LemmyContext;
73 use strum_macros::EnumString;
74 use url::Url;
75
76 #[derive(EnumString)]
77 enum PageOrNote {
78   Page,
79   Note,
80 }
81
82 /// This file is for post/comment activities received by the community, and for post/comment
83 ///       activities announced by the community and received by the user.
84
85 /// A post or comment being created
86 pub(in crate::inbox) async fn receive_create_for_community(
87   context: &LemmyContext,
88   activity: AnyBase,
89   expected_domain: &Url,
90   request_counter: &mut i32,
91 ) -> Result<(), LemmyError> {
92   let create = Create::from_any_base(activity)?.context(location_info!())?;
93   verify_activity_domains_valid(&create, &expected_domain, true)?;
94   is_addressed_to_public(&create)?;
95
96   let kind = create
97     .object()
98     .as_single_kind_str()
99     .and_then(|s| s.parse().ok());
100   match kind {
101     Some(PageOrNote::Page) => receive_create_post(create, context, request_counter).await,
102     Some(PageOrNote::Note) => receive_create_comment(create, context, request_counter).await,
103     _ => receive_unhandled_activity(create),
104   }
105 }
106
107 /// A post or comment being edited
108 pub(in crate::inbox) async fn receive_update_for_community(
109   context: &LemmyContext,
110   activity: AnyBase,
111   expected_domain: &Url,
112   request_counter: &mut i32,
113 ) -> Result<(), LemmyError> {
114   let update = Update::from_any_base(activity)?.context(location_info!())?;
115   verify_activity_domains_valid(&update, &expected_domain, true)?;
116   is_addressed_to_public(&update)?;
117
118   let kind = update
119     .object()
120     .as_single_kind_str()
121     .and_then(|s| s.parse().ok());
122   match kind {
123     Some(PageOrNote::Page) => receive_update_post(update, context, request_counter).await,
124     Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await,
125     _ => receive_unhandled_activity(update),
126   }
127 }
128
129 /// A post or comment being upvoted
130 pub(in crate::inbox) async fn receive_like_for_community(
131   context: &LemmyContext,
132   activity: AnyBase,
133   expected_domain: &Url,
134   request_counter: &mut i32,
135 ) -> Result<(), LemmyError> {
136   let like = Like::from_any_base(activity)?.context(location_info!())?;
137   verify_activity_domains_valid(&like, &expected_domain, false)?;
138   is_addressed_to_public(&like)?;
139
140   let object_id = like
141     .object()
142     .as_single_xsd_any_uri()
143     .context(location_info!())?;
144   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
145     PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await,
146     PostOrComment::Comment(comment) => {
147       receive_like_comment(like, *comment, context, request_counter).await
148     }
149   }
150 }
151
152 /// A post or comment being downvoted
153 pub(in crate::inbox) async fn receive_dislike_for_community(
154   context: &LemmyContext,
155   activity: AnyBase,
156   expected_domain: &Url,
157   request_counter: &mut i32,
158 ) -> Result<(), LemmyError> {
159   let enable_downvotes = blocking(context.pool(), move |conn| {
160     Site::read(conn, 1).map(|s| s.enable_downvotes)
161   })
162   .await??;
163   if !enable_downvotes {
164     return Ok(());
165   }
166
167   let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
168   verify_activity_domains_valid(&dislike, &expected_domain, false)?;
169   is_addressed_to_public(&dislike)?;
170
171   let object_id = dislike
172     .object()
173     .as_single_xsd_any_uri()
174     .context(location_info!())?;
175   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
176     PostOrComment::Post(post) => {
177       receive_dislike_post(dislike, *post, context, request_counter).await
178     }
179     PostOrComment::Comment(comment) => {
180       receive_dislike_comment(dislike, *comment, context, request_counter).await
181     }
182   }
183 }
184
185 /// A post or comment being deleted by its creator
186 pub(in crate::inbox) async fn receive_delete_for_community(
187   context: &LemmyContext,
188   activity: AnyBase,
189   expected_domain: &Url,
190 ) -> Result<(), LemmyError> {
191   let delete = Delete::from_any_base(activity)?.context(location_info!())?;
192   verify_activity_domains_valid(&delete, &expected_domain, true)?;
193   is_addressed_to_public(&delete)?;
194
195   let object = delete
196     .object()
197     .to_owned()
198     .single_xsd_any_uri()
199     .context(location_info!())?;
200
201   match find_post_or_comment_by_id(context, object).await {
202     Ok(PostOrComment::Post(p)) => receive_delete_post(context, *p).await,
203     Ok(PostOrComment::Comment(c)) => receive_delete_comment(context, *c).await,
204     // if we dont have the object, no need to do anything
205     Err(_) => Ok(()),
206   }
207 }
208
209 /// A post or comment being removed by a mod/admin
210 pub(in crate::inbox) async fn receive_remove_for_community(
211   context: &LemmyContext,
212   activity: AnyBase,
213   expected_domain: &Url,
214   request_counter: &mut i32,
215 ) -> Result<(), LemmyError> {
216   let remove = Remove::from_any_base(activity)?.context(location_info!())?;
217   verify_activity_domains_valid(&remove, &expected_domain, false)?;
218   is_addressed_to_public(&remove)?;
219
220   // Remove a moderator from community
221   if remove.target().is_some() {
222     let community = verify_actor_is_community_mod(&remove, context).await?;
223
224     let remove_mod = remove
225       .object()
226       .as_single_xsd_any_uri()
227       .context(location_info!())?;
228     let remove_mod = get_or_fetch_and_upsert_user(&remove_mod, context, request_counter).await?;
229     let form = CommunityModeratorForm {
230       community_id: community.id,
231       user_id: remove_mod.id,
232     };
233     blocking(context.pool(), move |conn| {
234       CommunityModerator::leave(conn, &form)
235     })
236     .await??;
237     Ok(())
238   }
239   // Remove a post or comment
240   else {
241     let cc = remove
242       .cc()
243       .map(|c| c.as_many())
244       .flatten()
245       .context(location_info!())?;
246     let community_id = cc
247       .first()
248       .map(|c| c.as_xsd_any_uri())
249       .flatten()
250       .context(location_info!())?;
251
252     let object = remove
253       .object()
254       .to_owned()
255       .single_xsd_any_uri()
256       .context(location_info!())?;
257
258     // Ensure that remove activity comes from the same domain as the community
259     remove.id(community_id.domain().context(location_info!())?)?;
260
261     match find_post_or_comment_by_id(context, object).await {
262       Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await,
263       Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await,
264       // if we dont have the object, no need to do anything
265       Err(_) => Ok(()),
266     }
267   }
268 }
269
270 #[derive(EnumString)]
271 enum UndoableActivities {
272   Delete,
273   Remove,
274   Like,
275   Dislike,
276 }
277
278 /// A post/comment action being reverted (either a delete, remove, upvote or downvote)
279 pub(in crate::inbox) async fn receive_undo_for_community(
280   context: &LemmyContext,
281   activity: AnyBase,
282   expected_domain: &Url,
283   request_counter: &mut i32,
284 ) -> Result<(), LemmyError> {
285   let undo = Undo::from_any_base(activity)?.context(location_info!())?;
286   verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
287   is_addressed_to_public(&undo)?;
288
289   use UndoableActivities::*;
290   match undo
291     .object()
292     .as_single_kind_str()
293     .and_then(|s| s.parse().ok())
294   {
295     Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
296     Some(Remove) => receive_undo_remove_for_community(context, undo, expected_domain).await,
297     Some(Like) => {
298       receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
299     }
300     Some(Dislike) => {
301       receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
302     }
303     _ => receive_unhandled_activity(undo),
304   }
305 }
306
307 /// A post or comment deletion being reverted
308 pub(in crate::inbox) async fn receive_undo_delete_for_community(
309   context: &LemmyContext,
310   undo: Undo,
311   expected_domain: &Url,
312 ) -> Result<(), LemmyError> {
313   let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
314     .context(location_info!())?;
315   verify_activity_domains_valid(&delete, &expected_domain, true)?;
316   is_addressed_to_public(&delete)?;
317
318   let object = delete
319     .object()
320     .to_owned()
321     .single_xsd_any_uri()
322     .context(location_info!())?;
323   match find_post_or_comment_by_id(context, object).await {
324     Ok(PostOrComment::Post(p)) => receive_undo_delete_post(context, *p).await,
325     Ok(PostOrComment::Comment(c)) => receive_undo_delete_comment(context, *c).await,
326     // if we dont have the object, no need to do anything
327     Err(_) => Ok(()),
328   }
329 }
330
331 /// A post or comment removal being reverted
332 pub(in crate::inbox) async fn receive_undo_remove_for_community(
333   context: &LemmyContext,
334   undo: Undo,
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   is_addressed_to_public(&remove)?;
341
342   let object = remove
343     .object()
344     .to_owned()
345     .single_xsd_any_uri()
346     .context(location_info!())?;
347   match find_post_or_comment_by_id(context, object).await {
348     Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, *p).await,
349     Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, *c).await,
350     // if we dont have the object, no need to do anything
351     Err(_) => Ok(()),
352   }
353 }
354
355 /// A post or comment upvote being reverted
356 pub(in crate::inbox) async fn receive_undo_like_for_community(
357   context: &LemmyContext,
358   undo: Undo,
359   expected_domain: &Url,
360   request_counter: &mut i32,
361 ) -> Result<(), LemmyError> {
362   let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
363     .context(location_info!())?;
364   verify_activity_domains_valid(&like, &expected_domain, false)?;
365   is_addressed_to_public(&like)?;
366
367   let object_id = like
368     .object()
369     .as_single_xsd_any_uri()
370     .context(location_info!())?;
371   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
372     PostOrComment::Post(post) => {
373       receive_undo_like_post(&like, *post, context, request_counter).await
374     }
375     PostOrComment::Comment(comment) => {
376       receive_undo_like_comment(&like, *comment, context, request_counter).await
377     }
378   }
379 }
380
381 /// Add a new mod to the community (can only be done by an existing mod).
382 pub(in crate::inbox) async fn receive_add_for_community(
383   context: &LemmyContext,
384   activity: AnyBase,
385   expected_domain: &Url,
386   request_counter: &mut i32,
387 ) -> Result<(), LemmyError> {
388   let add = Add::from_any_base(activity)?.context(location_info!())?;
389   verify_activity_domains_valid(&add, &expected_domain, false)?;
390   is_addressed_to_public(&add)?;
391   let community = verify_actor_is_community_mod(&add, context).await?;
392
393   let new_mod = add
394     .object()
395     .as_single_xsd_any_uri()
396     .context(location_info!())?;
397   let new_mod = get_or_fetch_and_upsert_user(&new_mod, context, request_counter).await?;
398   let form = CommunityModeratorForm {
399     community_id: community.id,
400     user_id: new_mod.id,
401   };
402   blocking(context.pool(), move |conn| {
403     CommunityModerator::join(conn, &form)
404   })
405   .await??;
406   Ok(())
407 }
408
409 /// A post or comment downvote being reverted
410 pub(in crate::inbox) async fn receive_undo_dislike_for_community(
411   context: &LemmyContext,
412   undo: Undo,
413   expected_domain: &Url,
414   request_counter: &mut i32,
415 ) -> Result<(), LemmyError> {
416   let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
417     .context(location_info!())?;
418   verify_activity_domains_valid(&dislike, &expected_domain, false)?;
419   is_addressed_to_public(&dislike)?;
420
421   let object_id = dislike
422     .object()
423     .as_single_xsd_any_uri()
424     .context(location_info!())?;
425   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
426     PostOrComment::Post(post) => {
427       receive_undo_dislike_post(&dislike, *post, context, request_counter).await
428     }
429     PostOrComment::Comment(comment) => {
430       receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
431     }
432   }
433 }
434
435 async fn fetch_post_or_comment_by_id(
436   apub_id: &Url,
437   context: &LemmyContext,
438   request_counter: &mut i32,
439 ) -> Result<PostOrComment, LemmyError> {
440   if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await {
441     return Ok(PostOrComment::Post(Box::new(post)));
442   }
443
444   if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
445     return Ok(PostOrComment::Comment(Box::new(comment)));
446   }
447
448   Err(NotFound.into())
449 }
450
451 async fn verify_actor_is_community_mod<T, Kind>(
452   activity: &T,
453   context: &LemmyContext,
454 ) -> Result<Community, LemmyError>
455 where
456   T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
457 {
458   // should be the moderators collection of a local community
459   let target = activity
460     .target()
461     .map(|t| t.as_single_xsd_string())
462     .flatten()
463     .context(location_info!())?;
464   // TODO: very hacky, we should probably store the moderators url in db
465   let community_id: DbUrl = Url::parse(&target.to_string().replace("/moderators", ""))?.into();
466   let community = blocking(&context.pool(), move |conn| {
467     Community::read_from_apub_id(&conn, &community_id)
468   })
469   .await??;
470
471   let actor = activity
472     .actor()?
473     .as_single_xsd_any_uri()
474     .context(location_info!())?
475     .to_owned();
476   let actor = blocking(&context.pool(), move |conn| {
477     User_::read_from_apub_id(&conn, &actor.into())
478   })
479   .await??;
480
481   // Note: this will also return true for admins in addition to mods, but as we dont know about
482   //       remote admins, it doesnt make any difference.
483   let community_id = community.id;
484   let actor_id = actor.id;
485   let is_mod_or_admin = blocking(context.pool(), move |conn| {
486     CommunityView::is_mod_or_admin(conn, actor_id, community_id)
487   })
488   .await?;
489   if !is_mod_or_admin {
490     return Err(anyhow!("Not a mod").into());
491   }
492
493   // TODO: the function name doesnt make sense if we return the community
494   Ok(community)
495 }