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