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,
38 find_post_or_comment_by_id,
39 inbox::is_addressed_to_public,
43 use activitystreams::{
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::{
65 community::{Community, CommunityModerator, CommunityModeratorForm},
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;
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.
86 /// A post or comment being created
87 pub(in crate::inbox) async fn receive_create_for_community(
88 context: &LemmyContext,
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)?;
100 .and_then(|s| s.parse().ok());
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),
108 /// A post or comment being edited
109 pub(in crate::inbox) async fn receive_update_for_community(
110 context: &LemmyContext,
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)?;
121 .as_single_kind_str()
122 .and_then(|s| s.parse().ok());
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),
130 /// A post or comment being upvoted
131 pub(in crate::inbox) async fn receive_like_for_community(
132 context: &LemmyContext,
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)?;
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
153 /// A post or comment being downvoted
154 pub(in crate::inbox) async fn receive_dislike_for_community(
155 context: &LemmyContext,
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)
164 if !enable_downvotes {
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)?;
172 let object_id = dislike
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
180 PostOrComment::Comment(comment) => {
181 receive_dislike_comment(dislike, *comment, context, request_counter).await
186 /// A post or comment being deleted by its creator
187 pub(in crate::inbox) async fn receive_delete_for_community(
188 context: &LemmyContext,
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)?;
199 .single_xsd_any_uri()
200 .context(location_info!())?;
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
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,
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)?;
221 // Remove a moderator from community
222 if remove.target().is_some() {
223 let community = verify_actor_is_community_mod(&remove, context).await?;
225 let remove_mod = remove
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,
234 blocking(context.pool(), move |conn| {
235 CommunityModerator::leave(conn, &form)
238 community.send_announce(activity, context).await?;
239 // TODO: send websocket notification about removed mod
242 // Remove a post or comment
246 .map(|c| c.as_many())
248 .context(location_info!())?;
249 let community_id = cc
251 .map(|c| c.as_xsd_any_uri())
253 .context(location_info!())?;
258 .single_xsd_any_uri()
259 .context(location_info!())?;
261 // Ensure that remove activity comes from the same domain as the community
262 remove.id(community_id.domain().context(location_info!())?)?;
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
273 #[derive(EnumString)]
274 enum UndoableActivities {
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,
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)?;
292 use UndoableActivities::*;
295 .as_single_kind_str()
296 .and_then(|s| s.parse().ok())
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,
301 receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
304 receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
306 _ => receive_unhandled_activity(undo),
310 /// A post or comment deletion being reverted
311 pub(in crate::inbox) async fn receive_undo_delete_for_community(
312 context: &LemmyContext,
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)?;
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
334 /// A post or comment removal being reverted
335 pub(in crate::inbox) async fn receive_undo_remove_for_community(
336 context: &LemmyContext,
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)?;
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 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,
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?;
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?;
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)
409 if moderated_communities.contains(&community.id) {
410 let form = CommunityModeratorForm {
411 community_id: community.id,
414 blocking(context.pool(), move |conn| {
415 CommunityModerator::join(conn, &form)
420 community.send_announce(activity, 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 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 async fn verify_actor_is_community_mod<T, Kind>(
470 context: &LemmyContext,
471 ) -> Result<Community, LemmyError>
473 T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
475 // should be the moderators collection of a local community
476 let target = activity
478 .map(|t| t.as_single_xsd_any_uri())
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)
490 .as_single_xsd_any_uri()
491 .context(location_info!())?
493 let actor = blocking(&context.pool(), move |conn| {
494 User_::read_from_apub_id(&conn, &actor.into())
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)
506 if !is_mod_or_admin {
507 return Err(anyhow!("Not a mod").into());
510 // TODO: the function name doesnt make sense if we return the community