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 person::get_or_fetch_and_upsert_person,
38 find_post_or_comment_by_id,
39 generate_moderators_url,
40 inbox::verify_is_addressed_to_public,
44 use activitystreams::{
62 use anyhow::{anyhow, Context};
63 use diesel::result::Error::NotFound;
64 use lemmy_api_common::blocking;
65 use lemmy_db_queries::{source::community::CommunityModerator_, ApubObject, Crud, Joinable};
66 use lemmy_db_schema::{
68 community::{Community, CommunityModerator, CommunityModeratorForm},
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;
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.
89 /// A post or comment being created
90 pub(in crate::inbox) async fn receive_create_for_community(
91 context: &LemmyContext,
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)?;
102 .as_single_kind_str()
103 .and_then(|s| s.parse().ok());
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),
111 /// A post or comment being edited
112 pub(in crate::inbox) async fn receive_update_for_community(
113 context: &LemmyContext,
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?;
126 .as_single_kind_str()
127 .and_then(|s| s.parse().ok());
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),
135 /// A post or comment being upvoted
136 pub(in crate::inbox) async fn receive_like_for_community(
137 context: &LemmyContext,
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)?;
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
158 /// A post or comment being downvoted
159 pub(in crate::inbox) async fn receive_dislike_for_community(
160 context: &LemmyContext,
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)
169 if !enable_downvotes {
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)?;
177 let object_id = dislike
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
185 PostOrComment::Comment(comment) => {
186 receive_dislike_comment(dislike, *comment, context, request_counter).await
191 /// A post or comment being deleted by its creator
192 pub(in crate::inbox) async fn receive_delete_for_community(
193 context: &LemmyContext,
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?;
207 .single_xsd_any_uri()
208 .context(location_info!())?;
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
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?;
228 verify_mod_activity(&remove, announce, &community, context).await?;
229 verify_is_addressed_to_public(&remove)?;
231 if remove.target().is_some() {
232 let remove_mod = remove
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,
241 blocking(context.pool(), move |conn| {
242 CommunityModerator::leave(conn, &form)
245 community.send_announce(remove_any_base, context).await?;
246 // TODO: send websocket notification about removed mod
249 // Remove a post or comment
254 .single_xsd_any_uri()
255 .context(location_info!())?;
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
266 #[derive(EnumString)]
267 enum UndoableActivities {
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,
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)?;
286 use UndoableActivities::*;
289 .as_single_kind_str()
290 .and_then(|s| s.parse().ok())
292 Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
294 receive_undo_remove_for_community(context, undo, announce, expected_domain).await
297 receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
300 receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
302 _ => receive_unhandled_activity(undo),
306 /// A post or comment deletion being reverted
307 pub(in crate::inbox) async fn receive_undo_delete_for_community(
308 context: &LemmyContext,
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)?;
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
330 /// A post or comment removal being reverted
331 pub(in crate::inbox) async fn receive_undo_remove_for_community(
332 context: &LemmyContext,
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?;
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
356 /// A post or comment upvote being reverted
357 pub(in crate::inbox) async fn receive_undo_like_for_community(
358 context: &LemmyContext,
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)?;
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
376 PostOrComment::Comment(comment) => {
377 receive_undo_like_comment(&like, *comment, context, request_counter).await
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?;
392 verify_mod_activity(&add, announce, &community, context).await?;
393 verify_is_addressed_to_public(&add)?;
394 verify_add_remove_moderator_target(&add, &community)?;
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?;
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)
409 if !moderated_communities.contains(&community.id) {
410 let form = CommunityModeratorForm {
411 community_id: community.id,
412 person_id: new_mod.id,
414 blocking(context.pool(), move |conn| {
415 CommunityModerator::join(conn, &form)
420 community.send_announce(add_any_base, context).await?;
422 // TODO: send websocket notification about added mod
426 /// A post or comment downvote being reverted
427 pub(in crate::inbox) async fn receive_undo_dislike_for_community(
428 context: &LemmyContext,
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)?;
438 let object_id = dislike
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
446 PostOrComment::Comment(comment) => {
447 receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
452 async fn fetch_post_or_comment_by_id(
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)));
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)));
468 /// Searches the activity's cc field for a Community ID, and returns the community.
469 async fn extract_community_from_cc<T, Kind>(
471 context: &LemmyContext,
472 ) -> Result<Community, LemmyError>
478 .map(|c| c.as_many())
480 .context(location_info!())?;
481 let community_id = cc
483 .map(|c| c.as_xsd_any_uri())
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)
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
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>(
502 community: &Community,
503 context: &LemmyContext,
504 ) -> Result<(), LemmyError>
506 T: ActorAndObjectRef + BaseExt<Kind>,
510 .as_single_xsd_any_uri()
511 .context(location_info!())?
513 let actor = blocking(&context.pool(), move |conn| {
514 Person::read_from_apub_id(&conn, &actor.into())
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)
526 if !is_mod_or_admin {
527 return Err(anyhow!("Not a mod").into());
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).
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>(
545 announce: Option<Announce>,
546 community: &Community,
547 context: &LemmyContext,
548 ) -> Result<(), LemmyError>
550 T: ActorAndObjectRef + BaseExt<Kind>,
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)?,
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>(
564 community: &Community,
565 ) -> Result<(), LemmyError>
567 T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
569 let target = activity
571 .map(|t| t.as_single_xsd_any_uri())
573 .context(location_info!())?;
574 if target != &generate_moderators_url(&community.actor_id)?.into_inner() {
575 return Err(anyhow!("Unkown target url").into());
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).
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>(
587 announce: &Option<Announce>,
588 context: &LemmyContext,
589 request_counter: &mut i32,
590 ) -> Result<(), LemmyError>
592 T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
594 let actor_id = activity
597 .single_xsd_any_uri()
598 .context(location_info!())?;
599 let object_id = activity
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(),
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?;
617 pub(crate) async fn verify_undo_remove_actor_instance<T, Kind>(
620 announce: &Option<Announce>,
621 context: &LemmyContext,
622 ) -> Result<(), LemmyError>
624 T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
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?;