]> Untitled Git - lemmy.git/blob - crates/api/src/community.rs
c0aea60224dd61f15e79e38edc639ce4e783688b
[lemmy.git] / crates / api / src / community.rs
1 use crate::Perform;
2 use actix_web::web::Data;
3 use anyhow::Context;
4 use lemmy_api_common::{
5   blocking,
6   check_community_ban,
7   check_community_deleted_or_removed,
8   community::*,
9   get_local_user_view_from_jwt,
10   is_mod_or_admin,
11 };
12 use lemmy_apub::{
13   objects::{community::ApubCommunity, person::ApubPerson},
14   protocol::activities::{
15     community::{
16       add_mod::AddMod,
17       block_user::BlockUserFromCommunity,
18       remove_mod::RemoveMod,
19       undo_block_user::UndoBlockUserFromCommunity,
20     },
21     following::{follow::FollowCommunity as FollowCommunityApub, undo_follow::UndoFollowCommunity},
22   },
23 };
24 use lemmy_db_schema::{
25   source::{
26     comment::Comment,
27     community::{
28       Community,
29       CommunityFollower,
30       CommunityFollowerForm,
31       CommunityModerator,
32       CommunityModeratorForm,
33       CommunityPersonBan,
34       CommunityPersonBanForm,
35     },
36     community_block::{CommunityBlock, CommunityBlockForm},
37     moderator::{
38       ModAddCommunity,
39       ModAddCommunityForm,
40       ModBanFromCommunity,
41       ModBanFromCommunityForm,
42       ModTransferCommunity,
43       ModTransferCommunityForm,
44     },
45     person::Person,
46     post::Post,
47     site::Site,
48   },
49   traits::{Bannable, Blockable, Crud, Followable, Joinable},
50 };
51 use lemmy_db_views::comment_view::CommentQueryBuilder;
52 use lemmy_db_views_actor::{
53   community_moderator_view::CommunityModeratorView,
54   community_view::CommunityView,
55   person_view::PersonViewSafe,
56 };
57 use lemmy_utils::{location_info, utils::naive_from_unix, ConnectionId, LemmyError};
58 use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
59
60 #[async_trait::async_trait(?Send)]
61 impl Perform for FollowCommunity {
62   type Response = CommunityResponse;
63
64   #[tracing::instrument(skip(context, _websocket_id))]
65   async fn perform(
66     &self,
67     context: &Data<LemmyContext>,
68     _websocket_id: Option<ConnectionId>,
69   ) -> Result<CommunityResponse, LemmyError> {
70     let data: &FollowCommunity = self;
71     let local_user_view =
72       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
73
74     let community_id = data.community_id;
75     let community: ApubCommunity = blocking(context.pool(), move |conn| {
76       Community::read(conn, community_id)
77     })
78     .await??
79     .into();
80     let community_follower_form = CommunityFollowerForm {
81       community_id: data.community_id,
82       person_id: local_user_view.person.id,
83       pending: false,
84     };
85
86     if community.local {
87       if data.follow {
88         check_community_ban(local_user_view.person.id, community_id, context.pool()).await?;
89         check_community_deleted_or_removed(community_id, context.pool()).await?;
90
91         let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
92         blocking(context.pool(), follow)
93           .await?
94           .map_err(LemmyError::from)
95           .map_err(|e| e.with_message("community_follower_already_exists"))?;
96       } else {
97         let unfollow =
98           move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
99         blocking(context.pool(), unfollow)
100           .await?
101           .map_err(LemmyError::from)
102           .map_err(|e| e.with_message("community_follower_already_exists"))?;
103       }
104     } else if data.follow {
105       // Dont actually add to the community followers here, because you need
106       // to wait for the accept
107       FollowCommunityApub::send(&local_user_view.person.clone().into(), &community, context)
108         .await?;
109     } else {
110       UndoFollowCommunity::send(&local_user_view.person.clone().into(), &community, context)
111         .await?;
112       let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
113       blocking(context.pool(), unfollow)
114         .await?
115         .map_err(LemmyError::from)
116         .map_err(|e| e.with_message("community_follower_already_exists"))?;
117     }
118
119     let community_id = data.community_id;
120     let person_id = local_user_view.person.id;
121     let mut community_view = blocking(context.pool(), move |conn| {
122       CommunityView::read(conn, community_id, Some(person_id))
123     })
124     .await??;
125
126     // TODO: this needs to return a "pending" state, until Accept is received from the remote server
127     // For now, just assume that remote follows are accepted.
128     // Otherwise, the subscribed will be null
129     if !community.local {
130       community_view.subscribed = data.follow;
131     }
132
133     Ok(CommunityResponse { community_view })
134   }
135 }
136
137 #[async_trait::async_trait(?Send)]
138 impl Perform for BlockCommunity {
139   type Response = BlockCommunityResponse;
140
141   #[tracing::instrument(skip(context, _websocket_id))]
142   async fn perform(
143     &self,
144     context: &Data<LemmyContext>,
145     _websocket_id: Option<ConnectionId>,
146   ) -> Result<BlockCommunityResponse, LemmyError> {
147     let data: &BlockCommunity = self;
148     let local_user_view =
149       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
150
151     let community_id = data.community_id;
152     let person_id = local_user_view.person.id;
153     let community_block_form = CommunityBlockForm {
154       person_id,
155       community_id,
156     };
157
158     if data.block {
159       let block = move |conn: &'_ _| CommunityBlock::block(conn, &community_block_form);
160       blocking(context.pool(), block)
161         .await?
162         .map_err(LemmyError::from)
163         .map_err(|e| e.with_message("community_block_already_exists"))?;
164
165       // Also, unfollow the community, and send a federated unfollow
166       let community_follower_form = CommunityFollowerForm {
167         community_id: data.community_id,
168         person_id,
169         pending: false,
170       };
171       blocking(context.pool(), move |conn: &'_ _| {
172         CommunityFollower::unfollow(conn, &community_follower_form)
173       })
174       .await?
175       .ok();
176       let community = blocking(context.pool(), move |conn| {
177         Community::read(conn, community_id)
178       })
179       .await??;
180       UndoFollowCommunity::send(&local_user_view.person.into(), &community.into(), context).await?;
181     } else {
182       let unblock = move |conn: &'_ _| CommunityBlock::unblock(conn, &community_block_form);
183       blocking(context.pool(), unblock)
184         .await?
185         .map_err(LemmyError::from)
186         .map_err(|e| e.with_message("community_block_already_exists"))?;
187     }
188
189     let community_view = blocking(context.pool(), move |conn| {
190       CommunityView::read(conn, community_id, Some(person_id))
191     })
192     .await??;
193
194     Ok(BlockCommunityResponse {
195       blocked: data.block,
196       community_view,
197     })
198   }
199 }
200
201 #[async_trait::async_trait(?Send)]
202 impl Perform for BanFromCommunity {
203   type Response = BanFromCommunityResponse;
204
205   #[tracing::instrument(skip(context, websocket_id))]
206   async fn perform(
207     &self,
208     context: &Data<LemmyContext>,
209     websocket_id: Option<ConnectionId>,
210   ) -> Result<BanFromCommunityResponse, LemmyError> {
211     let data: &BanFromCommunity = self;
212     let local_user_view =
213       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
214
215     let community_id = data.community_id;
216     let banned_person_id = data.person_id;
217
218     // Verify that only mods or admins can ban
219     is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
220
221     let community_user_ban_form = CommunityPersonBanForm {
222       community_id: data.community_id,
223       person_id: data.person_id,
224     };
225
226     let community: ApubCommunity = blocking(context.pool(), move |conn: &'_ _| {
227       Community::read(conn, community_id)
228     })
229     .await??
230     .into();
231     let banned_person: ApubPerson = blocking(context.pool(), move |conn: &'_ _| {
232       Person::read(conn, banned_person_id)
233     })
234     .await??
235     .into();
236
237     if data.ban {
238       let ban = move |conn: &'_ _| CommunityPersonBan::ban(conn, &community_user_ban_form);
239       blocking(context.pool(), ban)
240         .await?
241         .map_err(LemmyError::from)
242         .map_err(|e| e.with_message("community_user_already_banned"))?;
243
244       // Also unsubscribe them from the community, if they are subscribed
245       let community_follower_form = CommunityFollowerForm {
246         community_id: data.community_id,
247         person_id: banned_person_id,
248         pending: false,
249       };
250       blocking(context.pool(), move |conn: &'_ _| {
251         CommunityFollower::unfollow(conn, &community_follower_form)
252       })
253       .await?
254       .ok();
255
256       BlockUserFromCommunity::send(
257         &community,
258         &banned_person,
259         &local_user_view.person.clone().into(),
260         context,
261       )
262       .await?;
263     } else {
264       let unban = move |conn: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form);
265       blocking(context.pool(), unban)
266         .await?
267         .map_err(LemmyError::from)
268         .map_err(|e| e.with_message("community_user_already_banned"))?;
269       UndoBlockUserFromCommunity::send(
270         &community,
271         &banned_person,
272         &local_user_view.person.clone().into(),
273         context,
274       )
275       .await?;
276     }
277
278     // Remove/Restore their data if that's desired
279     if data.remove_data.unwrap_or(false) {
280       // Posts
281       blocking(context.pool(), move |conn: &'_ _| {
282         Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
283       })
284       .await??;
285
286       // Comments
287       // TODO Diesel doesn't allow updates with joins, so this has to be a loop
288       let comments = blocking(context.pool(), move |conn| {
289         CommentQueryBuilder::create(conn)
290           .creator_id(banned_person_id)
291           .community_id(community_id)
292           .limit(std::i64::MAX)
293           .list()
294       })
295       .await??;
296
297       for comment_view in &comments {
298         let comment_id = comment_view.comment.id;
299         blocking(context.pool(), move |conn: &'_ _| {
300           Comment::update_removed(conn, comment_id, true)
301         })
302         .await??;
303       }
304     }
305
306     // Mod tables
307     // TODO eventually do correct expires
308     let expires = data.expires.map(naive_from_unix);
309
310     let form = ModBanFromCommunityForm {
311       mod_person_id: local_user_view.person.id,
312       other_person_id: data.person_id,
313       community_id: data.community_id,
314       reason: data.reason.to_owned(),
315       banned: Some(data.ban),
316       expires,
317     };
318     blocking(context.pool(), move |conn| {
319       ModBanFromCommunity::create(conn, &form)
320     })
321     .await??;
322
323     let person_id = data.person_id;
324     let person_view = blocking(context.pool(), move |conn| {
325       PersonViewSafe::read(conn, person_id)
326     })
327     .await??;
328
329     let res = BanFromCommunityResponse {
330       person_view,
331       banned: data.ban,
332     };
333
334     context.chat_server().do_send(SendCommunityRoomMessage {
335       op: UserOperation::BanFromCommunity,
336       response: res.clone(),
337       community_id,
338       websocket_id,
339     });
340
341     Ok(res)
342   }
343 }
344
345 #[async_trait::async_trait(?Send)]
346 impl Perform for AddModToCommunity {
347   type Response = AddModToCommunityResponse;
348
349   #[tracing::instrument(skip(context, websocket_id))]
350   async fn perform(
351     &self,
352     context: &Data<LemmyContext>,
353     websocket_id: Option<ConnectionId>,
354   ) -> Result<AddModToCommunityResponse, LemmyError> {
355     let data: &AddModToCommunity = self;
356     let local_user_view =
357       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
358
359     let community_id = data.community_id;
360
361     // Verify that only mods or admins can add mod
362     is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
363
364     // Update in local database
365     let community_moderator_form = CommunityModeratorForm {
366       community_id: data.community_id,
367       person_id: data.person_id,
368     };
369     if data.added {
370       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
371       blocking(context.pool(), join)
372         .await?
373         .map_err(LemmyError::from)
374         .map_err(|e| e.with_message("community_moderator_already_exists"))?;
375     } else {
376       let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
377       blocking(context.pool(), leave)
378         .await?
379         .map_err(LemmyError::from)
380         .map_err(|e| e.with_message("community_moderator_already_exists"))?;
381     }
382
383     // Mod tables
384     let form = ModAddCommunityForm {
385       mod_person_id: local_user_view.person.id,
386       other_person_id: data.person_id,
387       community_id: data.community_id,
388       removed: Some(!data.added),
389     };
390     blocking(context.pool(), move |conn| {
391       ModAddCommunity::create(conn, &form)
392     })
393     .await??;
394
395     // Send to federated instances
396     let updated_mod_id = data.person_id;
397     let updated_mod: ApubPerson = blocking(context.pool(), move |conn| {
398       Person::read(conn, updated_mod_id)
399     })
400     .await??
401     .into();
402     let community: ApubCommunity = blocking(context.pool(), move |conn| {
403       Community::read(conn, community_id)
404     })
405     .await??
406     .into();
407     if data.added {
408       AddMod::send(
409         &community,
410         &updated_mod,
411         &local_user_view.person.into(),
412         context,
413       )
414       .await?;
415     } else {
416       RemoveMod::send(
417         &community,
418         &updated_mod,
419         &local_user_view.person.into(),
420         context,
421       )
422       .await?;
423     }
424
425     // Note: in case a remote mod is added, this returns the old moderators list, it will only get
426     //       updated once we receive an activity from the community (like `Announce/Add/Moderator`)
427     let community_id = data.community_id;
428     let moderators = blocking(context.pool(), move |conn| {
429       CommunityModeratorView::for_community(conn, community_id)
430     })
431     .await??;
432
433     let res = AddModToCommunityResponse { moderators };
434     context.chat_server().do_send(SendCommunityRoomMessage {
435       op: UserOperation::AddModToCommunity,
436       response: res.clone(),
437       community_id,
438       websocket_id,
439     });
440     Ok(res)
441   }
442 }
443
444 // TODO: we dont do anything for federation here, it should be updated the next time the community
445 //       gets fetched. i hope we can get rid of the community creator role soon.
446 #[async_trait::async_trait(?Send)]
447 impl Perform for TransferCommunity {
448   type Response = GetCommunityResponse;
449
450   #[tracing::instrument(skip(context, _websocket_id))]
451   async fn perform(
452     &self,
453     context: &Data<LemmyContext>,
454     _websocket_id: Option<ConnectionId>,
455   ) -> Result<GetCommunityResponse, LemmyError> {
456     let data: &TransferCommunity = self;
457     let local_user_view =
458       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
459
460     let site_creator_id = blocking(context.pool(), move |conn| {
461       Site::read(conn, 1).map(|s| s.creator_id)
462     })
463     .await??;
464
465     let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
466
467     // Making sure the site creator, if an admin, is at the top
468     let creator_index = admins
469       .iter()
470       .position(|r| r.person.id == site_creator_id)
471       .context(location_info!())?;
472     let creator_person = admins.remove(creator_index);
473     admins.insert(0, creator_person);
474
475     // Fetch the community mods
476     let community_id = data.community_id;
477     let mut community_mods = blocking(context.pool(), move |conn| {
478       CommunityModeratorView::for_community(conn, community_id)
479     })
480     .await??;
481
482     // Make sure transferrer is either the top community mod, or an admin
483     if local_user_view.person.id != community_mods[0].moderator.id
484       && !admins
485         .iter()
486         .map(|a| a.person.id)
487         .any(|x| x == local_user_view.person.id)
488     {
489       return Err(LemmyError::from_message("not_an_admin"));
490     }
491
492     // You have to re-do the community_moderator table, reordering it.
493     // Add the transferee to the top
494     let creator_index = community_mods
495       .iter()
496       .position(|r| r.moderator.id == data.person_id)
497       .context(location_info!())?;
498     let creator_person = community_mods.remove(creator_index);
499     community_mods.insert(0, creator_person);
500
501     // Delete all the mods
502     let community_id = data.community_id;
503     blocking(context.pool(), move |conn| {
504       CommunityModerator::delete_for_community(conn, community_id)
505     })
506     .await??;
507
508     // TODO: this should probably be a bulk operation
509     // Re-add the mods, in the new order
510     for cmod in &community_mods {
511       let community_moderator_form = CommunityModeratorForm {
512         community_id: cmod.community.id,
513         person_id: cmod.moderator.id,
514       };
515
516       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
517       blocking(context.pool(), join)
518         .await?
519         .map_err(LemmyError::from)
520         .map_err(|e| e.with_message("community_moderator_already_exists"))?;
521     }
522
523     // Mod tables
524     let form = ModTransferCommunityForm {
525       mod_person_id: local_user_view.person.id,
526       other_person_id: data.person_id,
527       community_id: data.community_id,
528       removed: Some(false),
529     };
530     blocking(context.pool(), move |conn| {
531       ModTransferCommunity::create(conn, &form)
532     })
533     .await??;
534
535     let community_id = data.community_id;
536     let person_id = local_user_view.person.id;
537     let community_view = blocking(context.pool(), move |conn| {
538       CommunityView::read(conn, community_id, Some(person_id))
539     })
540     .await?
541     .map_err(LemmyError::from)
542     .map_err(|e| e.with_message("couldnt_find_community"))?;
543
544     let community_id = data.community_id;
545     let moderators = blocking(context.pool(), move |conn| {
546       CommunityModeratorView::for_community(conn, community_id)
547     })
548     .await?
549     .map_err(LemmyError::from)
550     .map_err(|e| e.with_message("couldnt_find_community"))?;
551
552     // Return the jwt
553     Ok(GetCommunityResponse {
554       community_view,
555       moderators,
556       online: 0,
557     })
558   }
559 }