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,
18 receive_remote_mod_delete_community,
19 receive_remote_mod_undo_delete_community,
20 receive_remote_mod_update_community,
31 receive_undo_delete_post,
32 receive_undo_dislike_post,
33 receive_undo_like_post,
34 receive_undo_remove_post,
36 receive_unhandled_activity,
37 verify_activity_domains_valid,
39 inbox::verify_is_addressed_to_public,
41 use activitystreams::{
60 use anyhow::{anyhow, Context};
61 use diesel::result::Error::NotFound;
62 use lemmy_api_common::blocking;
65 objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
66 person::get_or_fetch_and_upsert_person,
69 find_post_or_comment_by_id,
70 generate_moderators_url,
76 use lemmy_db_queries::{
77 source::community::CommunityModerator_,
84 use lemmy_db_schema::{
89 CommunityFollowerForm,
91 CommunityModeratorForm,
93 CommunityPersonBanForm,
100 use lemmy_db_views_actor::community_view::CommunityView;
101 use lemmy_utils::{location_info, LemmyError};
102 use lemmy_websocket::LemmyContext;
103 use strum_macros::EnumString;
106 #[derive(EnumString)]
112 #[derive(EnumString)]
120 /// This file is for post/comment activities received by the community, and for post/comment
121 /// activities announced by the community and received by the person.
123 /// A post or comment being created
124 pub(in crate::inbox) async fn receive_create_for_community(
125 context: &LemmyContext,
127 expected_domain: &Url,
128 request_counter: &mut i32,
129 ) -> Result<(), LemmyError> {
130 let create = Create::from_any_base(activity)?.context(location_info!())?;
131 verify_activity_domains_valid(&create, &expected_domain, true)?;
132 verify_is_addressed_to_public(&create)?;
136 .as_single_kind_str()
137 .and_then(|s| s.parse().ok());
139 Some(ObjectTypes::Page) => receive_create_post(create, context, request_counter).await,
140 Some(ObjectTypes::Note) => receive_create_comment(create, context, request_counter).await,
141 _ => receive_unhandled_activity(create),
145 /// A post or comment being edited
146 pub(in crate::inbox) async fn receive_update_for_community(
147 context: &LemmyContext,
149 announce: Option<Announce>,
150 expected_domain: &Url,
151 request_counter: &mut i32,
152 ) -> Result<(), LemmyError> {
153 let update = Update::from_any_base(activity.to_owned())?.context(location_info!())?;
154 verify_activity_domains_valid(&update, &expected_domain, false)?;
155 verify_is_addressed_to_public(&update)?;
156 verify_modification_actor_instance(&update, &announce, context, request_counter).await?;
160 .as_single_kind_str()
161 .and_then(|s| s.parse().ok());
163 Some(ObjectTypes::Page) => {
164 receive_update_post(update, announce, context, request_counter).await
166 Some(ObjectTypes::Note) => receive_update_comment(update, context, request_counter).await,
167 Some(ObjectTypes::Group) => {
168 receive_remote_mod_update_community(update, context, request_counter).await
170 _ => receive_unhandled_activity(update),
174 /// A post or comment being upvoted
175 pub(in crate::inbox) async fn receive_like_for_community(
176 context: &LemmyContext,
178 expected_domain: &Url,
179 request_counter: &mut i32,
180 ) -> Result<(), LemmyError> {
181 let like = Like::from_any_base(activity)?.context(location_info!())?;
182 verify_activity_domains_valid(&like, &expected_domain, false)?;
183 verify_is_addressed_to_public(&like)?;
187 .as_single_xsd_any_uri()
188 .context(location_info!())?;
189 match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
190 PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await,
191 PostOrComment::Comment(comment) => {
192 receive_like_comment(like, *comment, context, request_counter).await
197 /// A post or comment being downvoted
198 pub(in crate::inbox) async fn receive_dislike_for_community(
199 context: &LemmyContext,
201 expected_domain: &Url,
202 request_counter: &mut i32,
203 ) -> Result<(), LemmyError> {
204 let enable_downvotes = blocking(context.pool(), move |conn| {
205 Site::read(conn, 1).map(|s| s.enable_downvotes)
208 if !enable_downvotes {
212 let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
213 verify_activity_domains_valid(&dislike, &expected_domain, false)?;
214 verify_is_addressed_to_public(&dislike)?;
216 let object_id = dislike
218 .as_single_xsd_any_uri()
219 .context(location_info!())?;
220 match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
221 PostOrComment::Post(post) => {
222 receive_dislike_post(dislike, *post, context, request_counter).await
224 PostOrComment::Comment(comment) => {
225 receive_dislike_comment(dislike, *comment, context, request_counter).await
230 /// A post or comment being deleted by its creator
231 pub(in crate::inbox) async fn receive_delete_for_community(
232 context: &LemmyContext,
234 announce: Option<Announce>,
235 expected_domain: &Url,
236 request_counter: &mut i32,
237 ) -> Result<(), LemmyError> {
238 let delete = Delete::from_any_base(activity)?.context(location_info!())?;
239 // TODO: skip this check if action is done by remote mod
240 verify_is_addressed_to_public(&delete)?;
241 verify_modification_actor_instance(&delete, &announce, context, request_counter).await?;
246 .single_xsd_any_uri()
247 .context(location_info!())?;
249 match find_object_by_id(context, object).await {
250 Ok(Object::Post(p)) => {
251 verify_activity_domains_valid(&delete, &expected_domain, true)?;
252 receive_delete_post(context, *p).await
254 Ok(Object::Comment(c)) => {
255 verify_activity_domains_valid(&delete, &expected_domain, true)?;
256 receive_delete_comment(context, *c).await
258 Ok(Object::Community(c)) => {
259 receive_remote_mod_delete_community(delete, *c, context, request_counter).await
261 // if we dont have the object or dont support its deletion, no need to do anything
266 /// A post or comment being removed by a mod/admin
267 pub(in crate::inbox) async fn receive_remove_for_community(
268 context: &LemmyContext,
269 remove_any_base: AnyBase,
270 announce: Option<Announce>,
271 request_counter: &mut i32,
272 ) -> Result<(), LemmyError> {
273 let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?;
274 let community = extract_community_from_cc(&remove, context).await?;
276 verify_mod_activity(&remove, announce, &community, context).await?;
277 verify_is_addressed_to_public(&remove)?;
279 if remove.target().is_some() {
280 let remove_mod = remove
282 .as_single_xsd_any_uri()
283 .context(location_info!())?;
284 let remove_mod = get_or_fetch_and_upsert_person(&remove_mod, context, request_counter).await?;
285 let form = CommunityModeratorForm {
286 community_id: community.id,
287 person_id: remove_mod.id,
289 blocking(context.pool(), move |conn| {
290 CommunityModerator::leave(conn, &form)
296 remove.object().clone().single_xsd_any_uri(),
300 // TODO: send websocket notification about removed mod
303 // Remove a post or comment
308 .single_xsd_any_uri()
309 .context(location_info!())?;
311 match find_post_or_comment_by_id(context, object).await {
312 Ok(PostOrComment::Post(p)) => receive_remove_post(context, *p).await,
313 Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, *c).await,
314 // if we dont have the object, no need to do anything
320 #[derive(EnumString)]
321 enum UndoableActivities {
329 /// A post/comment action being reverted (either a delete, remove, upvote or downvote)
330 pub(in crate::inbox) async fn receive_undo_for_community(
331 context: &LemmyContext,
333 announce: Option<Announce>,
334 expected_domain: &Url,
335 request_counter: &mut i32,
336 ) -> Result<(), LemmyError> {
337 let undo = Undo::from_any_base(activity)?.context(location_info!())?;
338 verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
339 verify_is_addressed_to_public(&undo)?;
341 use UndoableActivities::*;
344 .as_single_kind_str()
345 .and_then(|s| s.parse().ok())
348 receive_undo_delete_for_community(context, undo, expected_domain, request_counter).await
351 receive_undo_remove_for_community(context, undo, announce, expected_domain).await
354 receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
357 receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
360 receive_undo_block_user_for_community(
369 _ => receive_unhandled_activity(undo),
373 /// A post, comment or community deletion being reverted
374 pub(in crate::inbox) async fn receive_undo_delete_for_community(
375 context: &LemmyContext,
377 expected_domain: &Url,
378 request_counter: &mut i32,
379 ) -> Result<(), LemmyError> {
380 let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
381 .context(location_info!())?;
382 verify_is_addressed_to_public(&delete)?;
387 .single_xsd_any_uri()
388 .context(location_info!())?;
389 match find_object_by_id(context, object).await {
390 Ok(Object::Post(p)) => {
391 verify_activity_domains_valid(&delete, &expected_domain, true)?;
392 receive_undo_delete_post(context, *p).await
394 Ok(Object::Comment(c)) => {
395 verify_activity_domains_valid(&delete, &expected_domain, true)?;
396 receive_undo_delete_comment(context, *c).await
398 Ok(Object::Community(c)) => {
399 verify_actor_is_community_mod(&undo, &c, context).await?;
400 receive_remote_mod_undo_delete_community(undo, *c, context, request_counter).await
402 // if we dont have the object or dont support its deletion, no need to do anything
407 /// A post or comment removal being reverted
408 pub(in crate::inbox) async fn receive_undo_remove_for_community(
409 context: &LemmyContext,
411 announce: Option<Announce>,
412 expected_domain: &Url,
413 ) -> Result<(), LemmyError> {
414 let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
415 .context(location_info!())?;
416 verify_activity_domains_valid(&remove, &expected_domain, false)?;
417 verify_is_addressed_to_public(&remove)?;
418 verify_undo_remove_actor_instance(&undo, &remove, &announce, context).await?;
423 .single_xsd_any_uri()
424 .context(location_info!())?;
425 match find_post_or_comment_by_id(context, object).await {
426 Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, *p).await,
427 Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, *c).await,
428 // if we dont have the object, no need to do anything
433 /// A post or comment upvote being reverted
434 pub(in crate::inbox) async fn receive_undo_like_for_community(
435 context: &LemmyContext,
437 expected_domain: &Url,
438 request_counter: &mut i32,
439 ) -> Result<(), LemmyError> {
440 let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
441 .context(location_info!())?;
442 verify_activity_domains_valid(&like, &expected_domain, false)?;
443 verify_is_addressed_to_public(&like)?;
447 .as_single_xsd_any_uri()
448 .context(location_info!())?;
449 match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
450 PostOrComment::Post(post) => {
451 receive_undo_like_post(&like, *post, context, request_counter).await
453 PostOrComment::Comment(comment) => {
454 receive_undo_like_comment(&like, *comment, context, request_counter).await
459 /// Add a new mod to the community (can only be done by an existing mod).
460 pub(in crate::inbox) async fn receive_add_for_community(
461 context: &LemmyContext,
462 add_any_base: AnyBase,
463 announce: Option<Announce>,
464 request_counter: &mut i32,
465 ) -> Result<(), LemmyError> {
466 let add = Add::from_any_base(add_any_base.to_owned())?.context(location_info!())?;
467 let community = extract_community_from_cc(&add, context).await?;
469 verify_mod_activity(&add, announce, &community, context).await?;
470 verify_is_addressed_to_public(&add)?;
471 verify_add_remove_moderator_target(&add, &community)?;
475 .as_single_xsd_any_uri()
476 .context(location_info!())?;
477 let new_mod = get_or_fetch_and_upsert_person(&new_mod, context, request_counter).await?;
479 // If we had to refetch the community while parsing the activity, then the new mod has already
480 // been added. Skip it here as it would result in a duplicate key error.
481 let new_mod_id = new_mod.id;
482 let moderated_communities = blocking(context.pool(), move |conn| {
483 CommunityModerator::get_person_moderated_communities(conn, new_mod_id)
486 if !moderated_communities.contains(&community.id) {
487 let form = CommunityModeratorForm {
488 community_id: community.id,
489 person_id: new_mod.id,
491 blocking(context.pool(), move |conn| {
492 CommunityModerator::join(conn, &form)
500 add.object().clone().single_xsd_any_uri(),
505 // TODO: send websocket notification about added mod
509 /// A post or comment downvote being reverted
510 pub(in crate::inbox) async fn receive_undo_dislike_for_community(
511 context: &LemmyContext,
513 expected_domain: &Url,
514 request_counter: &mut i32,
515 ) -> Result<(), LemmyError> {
516 let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
517 .context(location_info!())?;
518 verify_activity_domains_valid(&dislike, &expected_domain, false)?;
519 verify_is_addressed_to_public(&dislike)?;
521 let object_id = dislike
523 .as_single_xsd_any_uri()
524 .context(location_info!())?;
525 match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
526 PostOrComment::Post(post) => {
527 receive_undo_dislike_post(&dislike, *post, context, request_counter).await
529 PostOrComment::Comment(comment) => {
530 receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
535 pub(crate) async fn receive_block_user_for_community(
536 context: &LemmyContext,
537 block_any_base: AnyBase,
538 announce: Option<Announce>,
539 request_counter: &mut i32,
540 ) -> Result<(), LemmyError> {
541 let block = Block::from_any_base(block_any_base.to_owned())?.context(location_info!())?;
542 let community = extract_community_from_cc(&block, context).await?;
544 verify_mod_activity(&block, announce, &community, context).await?;
545 verify_is_addressed_to_public(&block)?;
547 let blocked_user = block
549 .as_single_xsd_any_uri()
550 .context(location_info!())?;
552 get_or_fetch_and_upsert_person(&blocked_user, context, request_counter).await?;
554 let community_user_ban_form = CommunityPersonBanForm {
555 community_id: community.id,
556 person_id: blocked_user.id,
559 blocking(context.pool(), move |conn: &'_ _| {
560 CommunityPersonBan::ban(conn, &community_user_ban_form)
564 // Also unsubscribe them from the community, if they are subscribed
565 let community_follower_form = CommunityFollowerForm {
566 community_id: community.id,
567 person_id: blocked_user.id,
570 blocking(context.pool(), move |conn: &'_ _| {
571 CommunityFollower::unfollow(conn, &community_follower_form)
579 pub(crate) async fn receive_undo_block_user_for_community(
580 context: &LemmyContext,
582 announce: Option<Announce>,
583 expected_domain: &Url,
584 request_counter: &mut i32,
585 ) -> Result<(), LemmyError> {
586 let object = undo.object().clone().one().context(location_info!())?;
587 let block = Block::from_any_base(object)?.context(location_info!())?;
588 let community = extract_community_from_cc(&block, context).await?;
590 verify_activity_domains_valid(&block, &expected_domain, false)?;
591 verify_is_addressed_to_public(&block)?;
592 verify_undo_remove_actor_instance(&undo, &block, &announce, context).await?;
594 let blocked_user = block
596 .as_single_xsd_any_uri()
597 .context(location_info!())?;
599 get_or_fetch_and_upsert_person(&blocked_user, context, request_counter).await?;
601 let community_user_ban_form = CommunityPersonBanForm {
602 community_id: community.id,
603 person_id: blocked_user.id,
606 blocking(context.pool(), move |conn: &'_ _| {
607 CommunityPersonBan::unban(conn, &community_user_ban_form)
614 async fn fetch_post_or_comment_by_id(
616 context: &LemmyContext,
617 request_counter: &mut i32,
618 ) -> Result<PostOrComment, LemmyError> {
619 if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await {
620 return Ok(PostOrComment::Post(Box::new(post)));
623 if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
624 return Ok(PostOrComment::Comment(Box::new(comment)));
630 /// Searches the activity's cc field for a Community ID, and returns the community.
631 async fn extract_community_from_cc<T, Kind>(
633 context: &LemmyContext,
634 ) -> Result<Community, LemmyError>
640 .map(|c| c.as_many())
642 .context(location_info!())?;
643 let community_id = cc
645 .map(|c| c.as_xsd_any_uri())
647 .context(location_info!())?;
648 let community_id: DbUrl = community_id.to_owned().into();
649 let community = blocking(&context.pool(), move |conn| {
650 Community::read_from_apub_id(&conn, &community_id)
656 /// Checks that a moderation activity was sent by a user who is listed as mod for the community.
657 /// This is only used in the case of remote mods, as local mod actions don't go through the
660 /// This method should only be used for activities received by the community, not for activities
661 /// used by community followers.
662 pub(crate) async fn verify_actor_is_community_mod<T, Kind>(
664 community: &Community,
665 context: &LemmyContext,
666 ) -> Result<(), LemmyError>
668 T: ActorAndObjectRef + BaseExt<Kind>,
672 .as_single_xsd_any_uri()
673 .context(location_info!())?
675 let actor = blocking(&context.pool(), move |conn| {
676 Person::read_from_apub_id(&conn, &actor.into())
680 // Note: this will also return true for admins in addition to mods, but as we dont know about
681 // remote admins, it doesnt make any difference.
682 let community_id = community.id;
683 let actor_id = actor.id;
684 let is_mod_or_admin = blocking(context.pool(), move |conn| {
685 CommunityView::is_mod_or_admin(conn, actor_id, community_id)
688 if !is_mod_or_admin {
689 return Err(anyhow!("Not a mod").into());
695 /// This method behaves differently, depending if it is called via community inbox (activity
696 /// received by community from a remote user), or via user inbox (activity received by user from
697 /// community). We distinguish the cases by checking if the activity is wrapper in an announce
698 /// (only true when sent from user to community).
700 /// In the first case, we check that the actor is listed as community mod. In the second case, we
701 /// only check that the announce comes from the same domain as the activity. We trust the
702 /// community's instance to have validated the inner activity correctly. We can't do this validation
703 /// here, because we don't know who the instance admins are. Plus this allows for compatibility with
704 /// software that uses different rules for mod actions.
705 pub(crate) async fn verify_mod_activity<T, Kind>(
707 announce: Option<Announce>,
708 community: &Community,
709 context: &LemmyContext,
710 ) -> Result<(), LemmyError>
712 T: ActorAndObjectRef + BaseExt<Kind>,
715 None => verify_actor_is_community_mod(mod_action, community, context).await?,
716 Some(a) => verify_activity_domains_valid(&a, &community.actor_id.to_owned().into(), false)?,
722 /// For Add/Remove community moderator activities, check that the target field actually contains
723 /// /c/community/moderators. Any different values are unsupported.
724 fn verify_add_remove_moderator_target<T, Kind>(
726 community: &Community,
727 ) -> Result<(), LemmyError>
729 T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
731 let target = activity
733 .map(|t| t.as_single_xsd_any_uri())
735 .context(location_info!())?;
736 if target != &generate_moderators_url(&community.actor_id)?.into_inner() {
737 return Err(anyhow!("Unkown target url").into());
742 /// For activities like Update, Delete or Remove, check that the actor is from the same instance
743 /// as the original object itself (or is a remote mod).
745 /// Note: This is only needed for mod actions. Normal user actions (edit post, undo vote etc) are
746 /// already verified with `expected_domain`, so this serves as an additional check.
747 async fn verify_modification_actor_instance<T, Kind>(
749 announce: &Option<Announce>,
750 context: &LemmyContext,
751 request_counter: &mut i32,
752 ) -> Result<(), LemmyError>
754 T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
756 let actor_id = activity
759 .single_xsd_any_uri()
760 .context(location_info!())?;
761 let object_id = activity
766 .context(location_info!())?;
767 let original_id = match fetch_post_or_comment_by_id(object_id, context, request_counter).await {
768 Ok(PostOrComment::Post(p)) => p.ap_id.into_inner(),
769 Ok(PostOrComment::Comment(c)) => c.ap_id.into_inner(),
771 // We can also receive Update activity from remote mod for local activity
772 let object_id = object_id.to_owned().into();
773 blocking(context.pool(), move |conn| {
774 Community::read_from_apub_id(conn, &object_id)
780 if actor_id.domain() != original_id.domain() {
781 let community = extract_community_from_cc(activity, context).await?;
782 verify_mod_activity(activity, announce.to_owned(), &community, context).await?;
788 pub(crate) async fn verify_undo_remove_actor_instance<T, Kind>(
791 announce: &Option<Announce>,
792 context: &LemmyContext,
793 ) -> Result<(), LemmyError>
795 T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
797 if announce.is_none() {
798 let community = extract_community_from_cc(undo, context).await?;
799 verify_mod_activity(undo, announce.to_owned(), &community, context).await?;
800 verify_mod_activity(inner, announce.to_owned(), &community, context).await?;