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