4 receive_create_comment,
5 receive_delete_comment,
6 receive_dislike_comment,
8 receive_remove_comment,
9 receive_update_comment,
12 receive_undo_delete_comment,
13 receive_undo_dislike_comment,
14 receive_undo_like_comment,
15 receive_undo_remove_comment,
26 receive_undo_delete_post,
27 receive_undo_dislike_post,
28 receive_undo_like_post,
29 receive_undo_remove_post,
31 receive_unhandled_activity,
32 verify_activity_domains_valid,
35 objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
36 user::get_or_fetch_and_upsert_user,
39 find_post_or_comment_by_id,
40 generate_moderators_url,
41 inbox::verify_is_addressed_to_public,
47 use activitystreams::{
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::{
71 community::{Community, CommunityModerator, CommunityModeratorForm},
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;
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.
92 /// A post or comment being created
93 pub(in crate::inbox) async fn receive_create_for_community(
94 context: &LemmyContext,
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)?;
105 .as_single_kind_str()
106 .and_then(|s| s.parse().ok());
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),
114 /// A post or comment being edited
115 pub(in crate::inbox) async fn receive_update_for_community(
116 context: &LemmyContext,
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?;
129 .as_single_kind_str()
130 .and_then(|s| s.parse().ok());
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),
138 /// A post or comment being upvoted
139 pub(in crate::inbox) async fn receive_like_for_community(
140 context: &LemmyContext,
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)?;
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
161 /// A post or comment being downvoted
162 pub(in crate::inbox) async fn receive_dislike_for_community(
163 context: &LemmyContext,
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)
172 if !enable_downvotes {
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)?;
180 let object_id = dislike
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
188 PostOrComment::Comment(comment) => {
189 receive_dislike_comment(dislike, *comment, context, request_counter).await
194 /// A post or comment being deleted by its creator
195 pub(in crate::inbox) async fn receive_delete_for_community(
196 context: &LemmyContext,
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?;
209 .single_xsd_any_uri()
210 .context(location_info!())?;
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
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?;
230 verify_mod_activity(&remove, announce, &community, context).await?;
231 verify_is_addressed_to_public(&remove)?;
233 if remove.target().is_some() {
234 let remove_mod = remove
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,
243 blocking(context.pool(), move |conn| {
244 CommunityModerator::leave(conn, &form)
247 community.send_announce(remove_any_base, context).await?;
248 // TODO: send websocket notification about removed mod
251 // Remove a post or comment
256 .single_xsd_any_uri()
257 .context(location_info!())?;
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
268 #[derive(EnumString)]
269 enum UndoableActivities {
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,
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)?;
288 use UndoableActivities::*;
291 .as_single_kind_str()
292 .and_then(|s| s.parse().ok())
294 Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
296 receive_undo_remove_for_community(context, undo, announce, expected_domain).await
299 receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
302 receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
304 _ => receive_unhandled_activity(undo),
308 /// A post or comment deletion being reverted
309 pub(in crate::inbox) async fn receive_undo_delete_for_community(
310 context: &LemmyContext,
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)?;
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
332 /// A post or comment removal being reverted
333 pub(in crate::inbox) async fn receive_undo_remove_for_community(
334 context: &LemmyContext,
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?;
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
358 /// A post or comment upvote being reverted
359 pub(in crate::inbox) async fn receive_undo_like_for_community(
360 context: &LemmyContext,
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)?;
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
378 PostOrComment::Comment(comment) => {
379 receive_undo_like_comment(&like, *comment, context, request_counter).await
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?;
394 verify_mod_activity(&add, announce, &community, context).await?;
395 verify_is_addressed_to_public(&add)?;
396 verify_add_remove_moderator_target(&add, &community)?;
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?;
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)
411 if !moderated_communities.contains(&community.id) {
412 let form = CommunityModeratorForm {
413 community_id: community.id,
416 blocking(context.pool(), move |conn| {
417 CommunityModerator::join(conn, &form)
422 community.send_announce(add_any_base, context).await?;
424 // TODO: send websocket notification about added mod
428 /// A post or comment downvote being reverted
429 pub(in crate::inbox) async fn receive_undo_dislike_for_community(
430 context: &LemmyContext,
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)?;
440 let object_id = dislike
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
448 PostOrComment::Comment(comment) => {
449 receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
454 async fn fetch_post_or_comment_by_id(
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)));
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)));
470 /// Searches the activity's cc field for a Community ID, and returns the community.
471 async fn extract_community_from_cc<T, Kind>(
473 context: &LemmyContext,
474 ) -> Result<Community, LemmyError>
480 .map(|c| c.as_many())
482 .context(location_info!())?;
483 let community_id = cc
485 .map(|c| c.as_xsd_any_uri())
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)
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
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>(
504 community: &Community,
505 context: &LemmyContext,
506 ) -> Result<(), LemmyError>
508 T: ActorAndObjectRef + BaseExt<Kind>,
512 .as_single_xsd_any_uri()
513 .context(location_info!())?
515 let actor = blocking(&context.pool(), move |conn| {
516 User_::read_from_apub_id(&conn, &actor.into())
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)
528 if !is_mod_or_admin {
529 return Err(anyhow!("Not a mod").into());
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).
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>(
547 announce: Option<Announce>,
548 community: &Community,
549 context: &LemmyContext,
550 ) -> Result<(), LemmyError>
552 T: ActorAndObjectRef + BaseExt<Kind>,
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)?,
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>(
566 community: &Community,
567 ) -> Result<(), LemmyError>
569 T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
571 let target = activity
573 .map(|t| t.as_single_xsd_any_uri())
575 .context(location_info!())?;
576 if target != &generate_moderators_url(&community.actor_id)?.into_inner() {
577 return Err(anyhow!("Unkown target url").into());
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).
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>(
589 announce: &Option<Announce>,
590 context: &LemmyContext,
591 ) -> Result<(), LemmyError>
593 T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
595 let actor_id = activity
598 .single_xsd_any_uri()
599 .context(location_info!())?;
600 let object_id = activity
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(),
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?;
621 pub(crate) async fn verify_undo_remove_actor_instance<T, Kind>(
624 announce: &Option<Announce>,
625 context: &LemmyContext,
626 ) -> Result<(), LemmyError>
628 T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
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?;