]> Untitled Git - lemmy.git/blob - crates/apub/src/inbox/receive_for_community.rs
Allow adding remote users as community mods (ref #1061)
[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   ActorType,
41   PostOrComment,
42 };
43 use activitystreams::{
44   activity::{
45     ActorAndObjectRef,
46     Add,
47     Create,
48     Delete,
49     Dislike,
50     Like,
51     OptTargetRef,
52     Remove,
53     Undo,
54     Update,
55   },
56   base::AnyBase,
57   prelude::*,
58 };
59 use anyhow::{anyhow, Context};
60 use diesel::result::Error::NotFound;
61 use lemmy_api_structs::blocking;
62 use lemmy_db_queries::{source::community::CommunityModerator_, ApubObject, Crud, Joinable};
63 use lemmy_db_schema::{
64   source::{
65     community::{Community, CommunityModerator, CommunityModeratorForm},
66     site::Site,
67     user::User_,
68   },
69   DbUrl,
70 };
71 use lemmy_db_views_actor::community_view::CommunityView;
72 use lemmy_utils::{location_info, LemmyError};
73 use lemmy_websocket::LemmyContext;
74 use strum_macros::EnumString;
75 use url::Url;
76
77 #[derive(EnumString)]
78 enum PageOrNote {
79   Page,
80   Note,
81 }
82
83 /// This file is for post/comment activities received by the community, and for post/comment
84 ///       activities announced by the community and received by the user.
85
86 /// A post or comment being created
87 pub(in crate::inbox) async fn receive_create_for_community(
88   context: &LemmyContext,
89   activity: AnyBase,
90   expected_domain: &Url,
91   request_counter: &mut i32,
92 ) -> Result<(), LemmyError> {
93   let create = Create::from_any_base(activity)?.context(location_info!())?;
94   verify_activity_domains_valid(&create, &expected_domain, true)?;
95   is_addressed_to_public(&create)?;
96
97   let kind = create
98     .object()
99     .as_single_kind_str()
100     .and_then(|s| s.parse().ok());
101   match kind {
102     Some(PageOrNote::Page) => receive_create_post(create, context, request_counter).await,
103     Some(PageOrNote::Note) => receive_create_comment(create, context, request_counter).await,
104     _ => receive_unhandled_activity(create),
105   }
106 }
107
108 /// A post or comment being edited
109 pub(in crate::inbox) async fn receive_update_for_community(
110   context: &LemmyContext,
111   activity: AnyBase,
112   expected_domain: &Url,
113   request_counter: &mut i32,
114 ) -> Result<(), LemmyError> {
115   let update = Update::from_any_base(activity)?.context(location_info!())?;
116   verify_activity_domains_valid(&update, &expected_domain, true)?;
117   is_addressed_to_public(&update)?;
118
119   let kind = update
120     .object()
121     .as_single_kind_str()
122     .and_then(|s| s.parse().ok());
123   match kind {
124     Some(PageOrNote::Page) => receive_update_post(update, context, request_counter).await,
125     Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await,
126     _ => receive_unhandled_activity(update),
127   }
128 }
129
130 /// A post or comment being upvoted
131 pub(in crate::inbox) async fn receive_like_for_community(
132   context: &LemmyContext,
133   activity: AnyBase,
134   expected_domain: &Url,
135   request_counter: &mut i32,
136 ) -> Result<(), LemmyError> {
137   let like = Like::from_any_base(activity)?.context(location_info!())?;
138   verify_activity_domains_valid(&like, &expected_domain, false)?;
139   is_addressed_to_public(&like)?;
140
141   let object_id = like
142     .object()
143     .as_single_xsd_any_uri()
144     .context(location_info!())?;
145   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
146     PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await,
147     PostOrComment::Comment(comment) => {
148       receive_like_comment(like, *comment, context, request_counter).await
149     }
150   }
151 }
152
153 /// A post or comment being downvoted
154 pub(in crate::inbox) async fn receive_dislike_for_community(
155   context: &LemmyContext,
156   activity: AnyBase,
157   expected_domain: &Url,
158   request_counter: &mut i32,
159 ) -> Result<(), LemmyError> {
160   let enable_downvotes = blocking(context.pool(), move |conn| {
161     Site::read(conn, 1).map(|s| s.enable_downvotes)
162   })
163   .await??;
164   if !enable_downvotes {
165     return Ok(());
166   }
167
168   let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
169   verify_activity_domains_valid(&dislike, &expected_domain, false)?;
170   is_addressed_to_public(&dislike)?;
171
172   let object_id = dislike
173     .object()
174     .as_single_xsd_any_uri()
175     .context(location_info!())?;
176   match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
177     PostOrComment::Post(post) => {
178       receive_dislike_post(dislike, *post, context, request_counter).await
179     }
180     PostOrComment::Comment(comment) => {
181       receive_dislike_comment(dislike, *comment, context, request_counter).await
182     }
183   }
184 }
185
186 /// A post or comment being deleted by its creator
187 pub(in crate::inbox) async fn receive_delete_for_community(
188   context: &LemmyContext,
189   activity: AnyBase,
190   expected_domain: &Url,
191 ) -> Result<(), LemmyError> {
192   let delete = Delete::from_any_base(activity)?.context(location_info!())?;
193   verify_activity_domains_valid(&delete, &expected_domain, true)?;
194   is_addressed_to_public(&delete)?;
195
196   let object = delete
197     .object()
198     .to_owned()
199     .single_xsd_any_uri()
200     .context(location_info!())?;
201
202   match find_post_or_comment_by_id(context, object).await {
203     Ok(PostOrComment::Post(p)) => receive_delete_post(context, *p).await,
204     Ok(PostOrComment::Comment(c)) => receive_delete_comment(context, *c).await,
205     // if we dont have the object, no need to do anything
206     Err(_) => Ok(()),
207   }
208 }
209
210 /// A post or comment being removed by a mod/admin
211 pub(in crate::inbox) async fn receive_remove_for_community(
212   context: &LemmyContext,
213   activity: AnyBase,
214   expected_domain: &Url,
215   request_counter: &mut i32,
216 ) -> Result<(), LemmyError> {
217   let remove = Remove::from_any_base(activity.to_owned())?.context(location_info!())?;
218   verify_activity_domains_valid(&remove, &expected_domain, false)?;
219   is_addressed_to_public(&remove)?;
220
221   // Remove a moderator from community
222   if remove.target().is_some() {
223     let community = verify_actor_is_community_mod(&remove, context).await?;
224
225     let remove_mod = remove
226       .object()
227       .as_single_xsd_any_uri()
228       .context(location_info!())?;
229     let remove_mod = get_or_fetch_and_upsert_user(&remove_mod, context, request_counter).await?;
230     let form = CommunityModeratorForm {
231       community_id: community.id,
232       user_id: remove_mod.id,
233     };
234     blocking(context.pool(), move |conn| {
235       CommunityModerator::leave(conn, &form)
236     })
237     .await??;
238     community.send_announce(activity, context).await?;
239     // TODO: send websocket notification about removed mod
240     Ok(())
241   }
242   // Remove a post or comment
243   else {
244     let cc = remove
245       .cc()
246       .map(|c| c.as_many())
247       .flatten()
248       .context(location_info!())?;
249     let community_id = cc
250       .first()
251       .map(|c| c.as_xsd_any_uri())
252       .flatten()
253       .context(location_info!())?;
254
255     let object = remove
256       .object()
257       .to_owned()
258       .single_xsd_any_uri()
259       .context(location_info!())?;
260
261     // Ensure that remove activity comes from the same domain as the community
262     remove.id(community_id.domain().context(location_info!())?)?;
263
264     match find_post_or_comment_by_id(context, object).await {
265       Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await,
266       Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await,
267       // if we dont have the object, no need to do anything
268       Err(_) => Ok(()),
269     }
270   }
271 }
272
273 #[derive(EnumString)]
274 enum UndoableActivities {
275   Delete,
276   Remove,
277   Like,
278   Dislike,
279 }
280
281 /// A post/comment action being reverted (either a delete, remove, upvote or downvote)
282 pub(in crate::inbox) async fn receive_undo_for_community(
283   context: &LemmyContext,
284   activity: AnyBase,
285   expected_domain: &Url,
286   request_counter: &mut i32,
287 ) -> Result<(), LemmyError> {
288   let undo = Undo::from_any_base(activity)?.context(location_info!())?;
289   verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
290   is_addressed_to_public(&undo)?;
291
292   use UndoableActivities::*;
293   match undo
294     .object()
295     .as_single_kind_str()
296     .and_then(|s| s.parse().ok())
297   {
298     Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
299     Some(Remove) => receive_undo_remove_for_community(context, undo, expected_domain).await,
300     Some(Like) => {
301       receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
302     }
303     Some(Dislike) => {
304       receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
305     }
306     _ => receive_unhandled_activity(undo),
307   }
308 }
309
310 /// A post or comment deletion being reverted
311 pub(in crate::inbox) async fn receive_undo_delete_for_community(
312   context: &LemmyContext,
313   undo: Undo,
314   expected_domain: &Url,
315 ) -> Result<(), LemmyError> {
316   let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
317     .context(location_info!())?;
318   verify_activity_domains_valid(&delete, &expected_domain, true)?;
319   is_addressed_to_public(&delete)?;
320
321   let object = delete
322     .object()
323     .to_owned()
324     .single_xsd_any_uri()
325     .context(location_info!())?;
326   match find_post_or_comment_by_id(context, object).await {
327     Ok(PostOrComment::Post(p)) => receive_undo_delete_post(context, *p).await,
328     Ok(PostOrComment::Comment(c)) => receive_undo_delete_comment(context, *c).await,
329     // if we dont have the object, no need to do anything
330     Err(_) => Ok(()),
331   }
332 }
333
334 /// A post or comment removal being reverted
335 pub(in crate::inbox) async fn receive_undo_remove_for_community(
336   context: &LemmyContext,
337   undo: Undo,
338   expected_domain: &Url,
339 ) -> Result<(), LemmyError> {
340   let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
341     .context(location_info!())?;
342   verify_activity_domains_valid(&remove, &expected_domain, false)?;
343   is_addressed_to_public(&remove)?;
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   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   activity: AnyBase,
388   expected_domain: &Url,
389   request_counter: &mut i32,
390 ) -> Result<(), LemmyError> {
391   let add = Add::from_any_base(activity.to_owned())?.context(location_info!())?;
392   verify_activity_domains_valid(&add, &expected_domain, false)?;
393   is_addressed_to_public(&add)?;
394   let community = verify_actor_is_community_mod(&add, context).await?;
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_user(&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_user_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       user_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(activity, 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   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 async fn verify_actor_is_community_mod<T, Kind>(
469   activity: &T,
470   context: &LemmyContext,
471 ) -> Result<Community, LemmyError>
472 where
473   T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
474 {
475   // should be the moderators collection of a local community
476   let target = activity
477     .target()
478     .map(|t| t.as_single_xsd_any_uri())
479     .flatten()
480     .context(location_info!())?;
481   // TODO: very hacky, we should probably store the moderators url in db
482   let community_id: DbUrl = Url::parse(&target.to_string().replace("/moderators", ""))?.into();
483   let community = blocking(&context.pool(), move |conn| {
484     Community::read_from_apub_id(&conn, &community_id)
485   })
486   .await??;
487
488   let actor = activity
489     .actor()?
490     .as_single_xsd_any_uri()
491     .context(location_info!())?
492     .to_owned();
493   let actor = blocking(&context.pool(), move |conn| {
494     User_::read_from_apub_id(&conn, &actor.into())
495   })
496   .await??;
497
498   // Note: this will also return true for admins in addition to mods, but as we dont know about
499   //       remote admins, it doesnt make any difference.
500   let community_id = community.id;
501   let actor_id = actor.id;
502   let is_mod_or_admin = blocking(context.pool(), move |conn| {
503     CommunityView::is_mod_or_admin(conn, actor_id, community_id)
504   })
505   .await?;
506   if !is_mod_or_admin {
507     return Err(anyhow!("Not a mod").into());
508   }
509
510   // TODO: the function name doesnt make sense if we return the community
511   Ok(community)
512 }