]> Untitled Git - lemmy.git/blob - lemmy_api/src/community.rs
Remove logging to find bug (yerbamate.ml/LemmyNet/lemmy/pulls/146)
[lemmy.git] / lemmy_api / src / community.rs
1 use crate::{
2   check_optional_url,
3   get_user_from_jwt,
4   get_user_from_jwt_opt,
5   is_admin,
6   is_mod_or_admin,
7   Perform,
8 };
9 use actix_web::web::Data;
10 use anyhow::Context;
11 use lemmy_apub::ActorType;
12 use lemmy_db::{
13   comment::Comment,
14   comment_view::CommentQueryBuilder,
15   community::*,
16   community_view::*,
17   diesel_option_overwrite,
18   moderator::*,
19   naive_now,
20   post::Post,
21   site::*,
22   user_view::*,
23   ApubObject,
24   Bannable,
25   Crud,
26   Followable,
27   Joinable,
28   SortType,
29 };
30 use lemmy_structs::{blocking, community::*};
31 use lemmy_utils::{
32   apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
33   location_info,
34   utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
35   APIError,
36   ConnectionId,
37   LemmyError,
38 };
39 use lemmy_websocket::{
40   messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage},
41   LemmyContext,
42   UserOperation,
43 };
44 use std::str::FromStr;
45
46 #[async_trait::async_trait(?Send)]
47 impl Perform for GetCommunity {
48   type Response = GetCommunityResponse;
49
50   async fn perform(
51     &self,
52     context: &Data<LemmyContext>,
53     _websocket_id: Option<ConnectionId>,
54   ) -> Result<GetCommunityResponse, LemmyError> {
55     let data: &GetCommunity = &self;
56     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
57     let user_id = user.map(|u| u.id);
58
59     let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
60     let community = match data.id {
61       Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
62       None => match blocking(context.pool(), move |conn| {
63         Community::read_from_name(conn, &name)
64       })
65       .await?
66       {
67         Ok(community) => community,
68         Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
69       },
70     };
71
72     let community_id = community.id;
73     let community_view = match blocking(context.pool(), move |conn| {
74       CommunityView::read(conn, community_id, user_id)
75     })
76     .await?
77     {
78       Ok(community) => community,
79       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
80     };
81
82     let community_id = community.id;
83     let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
84       CommunityModeratorView::for_community(conn, community_id)
85     })
86     .await?
87     {
88       Ok(moderators) => moderators,
89       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
90     };
91
92     let online = context
93       .chat_server()
94       .send(GetCommunityUsersOnline { community_id })
95       .await
96       .unwrap_or(1);
97
98     let res = GetCommunityResponse {
99       community: community_view,
100       moderators,
101       online,
102     };
103
104     // Return the jwt
105     Ok(res)
106   }
107 }
108
109 #[async_trait::async_trait(?Send)]
110 impl Perform for CreateCommunity {
111   type Response = CommunityResponse;
112
113   async fn perform(
114     &self,
115     context: &Data<LemmyContext>,
116     _websocket_id: Option<ConnectionId>,
117   ) -> Result<CommunityResponse, LemmyError> {
118     let data: &CreateCommunity = &self;
119     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
120
121     check_slurs(&data.name)?;
122     check_slurs(&data.title)?;
123     check_slurs_opt(&data.description)?;
124
125     if !is_valid_community_name(&data.name) {
126       return Err(APIError::err("invalid_community_name").into());
127     }
128
129     // Double check for duplicate community actor_ids
130     let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
131     let actor_id_cloned = actor_id.to_owned();
132     let community_dupe = blocking(context.pool(), move |conn| {
133       Community::read_from_apub_id(conn, &actor_id_cloned)
134     })
135     .await?;
136     if community_dupe.is_ok() {
137       return Err(APIError::err("community_already_exists").into());
138     }
139
140     // Check to make sure the icon and banners are urls
141     let icon = diesel_option_overwrite(&data.icon);
142     let banner = diesel_option_overwrite(&data.banner);
143
144     check_optional_url(&icon)?;
145     check_optional_url(&banner)?;
146
147     // When you create a community, make sure the user becomes a moderator and a follower
148     let keypair = generate_actor_keypair()?;
149
150     let community_form = CommunityForm {
151       name: data.name.to_owned(),
152       title: data.title.to_owned(),
153       description: data.description.to_owned(),
154       icon,
155       banner,
156       category_id: data.category_id,
157       creator_id: user.id,
158       removed: None,
159       deleted: None,
160       nsfw: data.nsfw,
161       updated: None,
162       actor_id: Some(actor_id),
163       local: true,
164       private_key: Some(keypair.private_key),
165       public_key: Some(keypair.public_key),
166       last_refreshed_at: None,
167       published: None,
168     };
169
170     let inserted_community = match blocking(context.pool(), move |conn| {
171       Community::create(conn, &community_form)
172     })
173     .await?
174     {
175       Ok(community) => community,
176       Err(_e) => return Err(APIError::err("community_already_exists").into()),
177     };
178
179     let community_moderator_form = CommunityModeratorForm {
180       community_id: inserted_community.id,
181       user_id: user.id,
182     };
183
184     let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
185     if blocking(context.pool(), join).await?.is_err() {
186       return Err(APIError::err("community_moderator_already_exists").into());
187     }
188
189     let community_follower_form = CommunityFollowerForm {
190       community_id: inserted_community.id,
191       user_id: user.id,
192       pending: false,
193     };
194
195     let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
196     if blocking(context.pool(), follow).await?.is_err() {
197       return Err(APIError::err("community_follower_already_exists").into());
198     }
199
200     let user_id = user.id;
201     let community_view = blocking(context.pool(), move |conn| {
202       CommunityView::read(conn, inserted_community.id, Some(user_id))
203     })
204     .await??;
205
206     Ok(CommunityResponse {
207       community: community_view,
208     })
209   }
210 }
211
212 #[async_trait::async_trait(?Send)]
213 impl Perform for EditCommunity {
214   type Response = CommunityResponse;
215
216   async fn perform(
217     &self,
218     context: &Data<LemmyContext>,
219     websocket_id: Option<ConnectionId>,
220   ) -> Result<CommunityResponse, LemmyError> {
221     let data: &EditCommunity = &self;
222     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
223
224     check_slurs(&data.title)?;
225     check_slurs_opt(&data.description)?;
226
227     // Verify its a mod (only mods can edit it)
228     let edit_id = data.edit_id;
229     let mods: Vec<i32> = blocking(context.pool(), move |conn| {
230       CommunityModeratorView::for_community(conn, edit_id)
231         .map(|v| v.into_iter().map(|m| m.user_id).collect())
232     })
233     .await??;
234     if !mods.contains(&user.id) {
235       return Err(APIError::err("not_a_moderator").into());
236     }
237
238     let edit_id = data.edit_id;
239     let read_community =
240       blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
241
242     let icon = diesel_option_overwrite(&data.icon);
243     let banner = diesel_option_overwrite(&data.banner);
244
245     check_optional_url(&icon)?;
246     check_optional_url(&banner)?;
247
248     let community_form = CommunityForm {
249       name: read_community.name,
250       title: data.title.to_owned(),
251       description: data.description.to_owned(),
252       icon,
253       banner,
254       category_id: data.category_id.to_owned(),
255       creator_id: read_community.creator_id,
256       removed: Some(read_community.removed),
257       deleted: Some(read_community.deleted),
258       nsfw: data.nsfw,
259       updated: Some(naive_now()),
260       actor_id: Some(read_community.actor_id),
261       local: read_community.local,
262       private_key: read_community.private_key,
263       public_key: read_community.public_key,
264       last_refreshed_at: None,
265       published: None,
266     };
267
268     let edit_id = data.edit_id;
269     match blocking(context.pool(), move |conn| {
270       Community::update(conn, edit_id, &community_form)
271     })
272     .await?
273     {
274       Ok(community) => community,
275       Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
276     };
277
278     // TODO there needs to be some kind of an apub update
279     // process for communities and users
280
281     let edit_id = data.edit_id;
282     let user_id = user.id;
283     let community_view = blocking(context.pool(), move |conn| {
284       CommunityView::read(conn, edit_id, Some(user_id))
285     })
286     .await??;
287
288     let res = CommunityResponse {
289       community: community_view,
290     };
291
292     send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
293
294     Ok(res)
295   }
296 }
297
298 #[async_trait::async_trait(?Send)]
299 impl Perform for DeleteCommunity {
300   type Response = CommunityResponse;
301
302   async fn perform(
303     &self,
304     context: &Data<LemmyContext>,
305     websocket_id: Option<ConnectionId>,
306   ) -> Result<CommunityResponse, LemmyError> {
307     let data: &DeleteCommunity = &self;
308     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
309
310     // Verify its the creator (only a creator can delete the community)
311     let edit_id = data.edit_id;
312     let read_community =
313       blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
314     if read_community.creator_id != user.id {
315       return Err(APIError::err("no_community_edit_allowed").into());
316     }
317
318     // Do the delete
319     let edit_id = data.edit_id;
320     let deleted = data.deleted;
321     let updated_community = match blocking(context.pool(), move |conn| {
322       Community::update_deleted(conn, edit_id, deleted)
323     })
324     .await?
325     {
326       Ok(community) => community,
327       Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
328     };
329
330     // Send apub messages
331     if deleted {
332       updated_community.send_delete(context).await?;
333     } else {
334       updated_community.send_undo_delete(context).await?;
335     }
336
337     let edit_id = data.edit_id;
338     let user_id = user.id;
339     let community_view = blocking(context.pool(), move |conn| {
340       CommunityView::read(conn, edit_id, Some(user_id))
341     })
342     .await??;
343
344     let res = CommunityResponse {
345       community: community_view,
346     };
347
348     send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
349
350     Ok(res)
351   }
352 }
353
354 #[async_trait::async_trait(?Send)]
355 impl Perform for RemoveCommunity {
356   type Response = CommunityResponse;
357
358   async fn perform(
359     &self,
360     context: &Data<LemmyContext>,
361     websocket_id: Option<ConnectionId>,
362   ) -> Result<CommunityResponse, LemmyError> {
363     let data: &RemoveCommunity = &self;
364     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
365
366     // Verify its an admin (only an admin can remove a community)
367     is_admin(context.pool(), user.id).await?;
368
369     // Do the remove
370     let edit_id = data.edit_id;
371     let removed = data.removed;
372     let updated_community = match blocking(context.pool(), move |conn| {
373       Community::update_removed(conn, edit_id, removed)
374     })
375     .await?
376     {
377       Ok(community) => community,
378       Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
379     };
380
381     // Mod tables
382     let expires = match data.expires {
383       Some(time) => Some(naive_from_unix(time)),
384       None => None,
385     };
386     let form = ModRemoveCommunityForm {
387       mod_user_id: user.id,
388       community_id: data.edit_id,
389       removed: Some(removed),
390       reason: data.reason.to_owned(),
391       expires,
392     };
393     blocking(context.pool(), move |conn| {
394       ModRemoveCommunity::create(conn, &form)
395     })
396     .await??;
397
398     // Apub messages
399     if removed {
400       updated_community.send_remove(context).await?;
401     } else {
402       updated_community.send_undo_remove(context).await?;
403     }
404
405     let edit_id = data.edit_id;
406     let user_id = user.id;
407     let community_view = blocking(context.pool(), move |conn| {
408       CommunityView::read(conn, edit_id, Some(user_id))
409     })
410     .await??;
411
412     let res = CommunityResponse {
413       community: community_view,
414     };
415
416     send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
417
418     Ok(res)
419   }
420 }
421
422 #[async_trait::async_trait(?Send)]
423 impl Perform for ListCommunities {
424   type Response = ListCommunitiesResponse;
425
426   async fn perform(
427     &self,
428     context: &Data<LemmyContext>,
429     _websocket_id: Option<ConnectionId>,
430   ) -> Result<ListCommunitiesResponse, LemmyError> {
431     let data: &ListCommunities = &self;
432     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
433
434     let user_id = match &user {
435       Some(user) => Some(user.id),
436       None => None,
437     };
438
439     let show_nsfw = match &user {
440       Some(user) => user.show_nsfw,
441       None => false,
442     };
443
444     let sort = SortType::from_str(&data.sort)?;
445
446     let page = data.page;
447     let limit = data.limit;
448     let communities = blocking(context.pool(), move |conn| {
449       CommunityQueryBuilder::create(conn)
450         .sort(&sort)
451         .for_user(user_id)
452         .show_nsfw(show_nsfw)
453         .page(page)
454         .limit(limit)
455         .list()
456     })
457     .await??;
458
459     // Return the jwt
460     Ok(ListCommunitiesResponse { communities })
461   }
462 }
463
464 #[async_trait::async_trait(?Send)]
465 impl Perform for FollowCommunity {
466   type Response = CommunityResponse;
467
468   async fn perform(
469     &self,
470     context: &Data<LemmyContext>,
471     _websocket_id: Option<ConnectionId>,
472   ) -> Result<CommunityResponse, LemmyError> {
473     let data: &FollowCommunity = &self;
474     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
475
476     let community_id = data.community_id;
477     let community = blocking(context.pool(), move |conn| {
478       Community::read(conn, community_id)
479     })
480     .await??;
481     let community_follower_form = CommunityFollowerForm {
482       community_id: data.community_id,
483       user_id: user.id,
484       pending: false,
485     };
486
487     if community.local {
488       if data.follow {
489         let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
490         if blocking(context.pool(), follow).await?.is_err() {
491           return Err(APIError::err("community_follower_already_exists").into());
492         }
493       } else {
494         let unfollow =
495           move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
496         if blocking(context.pool(), unfollow).await?.is_err() {
497           return Err(APIError::err("community_follower_already_exists").into());
498         }
499       }
500     } else if data.follow {
501       // Dont actually add to the community followers here, because you need
502       // to wait for the accept
503       user.send_follow(&community.actor_id()?, context).await?;
504     } else {
505       user.send_unfollow(&community.actor_id()?, context).await?;
506       let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
507       if blocking(context.pool(), unfollow).await?.is_err() {
508         return Err(APIError::err("community_follower_already_exists").into());
509       }
510     }
511
512     let community_id = data.community_id;
513     let user_id = user.id;
514     let mut community_view = blocking(context.pool(), move |conn| {
515       CommunityView::read(conn, community_id, Some(user_id))
516     })
517     .await??;
518
519     // TODO: this needs to return a "pending" state, until Accept is received from the remote server
520     // For now, just assume that remote follows are accepted.
521     // Otherwise, the subscribed will be null
522     if !community.local {
523       community_view.subscribed = Some(data.follow);
524     }
525
526     Ok(CommunityResponse {
527       community: community_view,
528     })
529   }
530 }
531
532 #[async_trait::async_trait(?Send)]
533 impl Perform for GetFollowedCommunities {
534   type Response = GetFollowedCommunitiesResponse;
535
536   async fn perform(
537     &self,
538     context: &Data<LemmyContext>,
539     _websocket_id: Option<ConnectionId>,
540   ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
541     let data: &GetFollowedCommunities = &self;
542     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
543
544     let user_id = user.id;
545     let communities = match blocking(context.pool(), move |conn| {
546       CommunityFollowerView::for_user(conn, user_id)
547     })
548     .await?
549     {
550       Ok(communities) => communities,
551       _ => return Err(APIError::err("system_err_login").into()),
552     };
553
554     // Return the jwt
555     Ok(GetFollowedCommunitiesResponse { communities })
556   }
557 }
558
559 #[async_trait::async_trait(?Send)]
560 impl Perform for BanFromCommunity {
561   type Response = BanFromCommunityResponse;
562
563   async fn perform(
564     &self,
565     context: &Data<LemmyContext>,
566     websocket_id: Option<ConnectionId>,
567   ) -> Result<BanFromCommunityResponse, LemmyError> {
568     let data: &BanFromCommunity = &self;
569     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
570
571     let community_id = data.community_id;
572     let banned_user_id = data.user_id;
573
574     // Verify that only mods or admins can ban
575     is_mod_or_admin(context.pool(), user.id, community_id).await?;
576
577     let community_user_ban_form = CommunityUserBanForm {
578       community_id: data.community_id,
579       user_id: data.user_id,
580     };
581
582     if data.ban {
583       let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
584       if blocking(context.pool(), ban).await?.is_err() {
585         return Err(APIError::err("community_user_already_banned").into());
586       }
587     } else {
588       let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
589       if blocking(context.pool(), unban).await?.is_err() {
590         return Err(APIError::err("community_user_already_banned").into());
591       }
592     }
593
594     // Remove/Restore their data if that's desired
595     if let Some(remove_data) = data.remove_data {
596       // Posts
597       blocking(context.pool(), move |conn: &'_ _| {
598         Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
599       })
600       .await??;
601
602       // Comments
603       // Diesel doesn't allow updates with joins, so this has to be a loop
604       let comments = blocking(context.pool(), move |conn| {
605         CommentQueryBuilder::create(conn)
606           .for_creator_id(banned_user_id)
607           .for_community_id(community_id)
608           .limit(std::i64::MAX)
609           .list()
610       })
611       .await??;
612
613       for comment in &comments {
614         let comment_id = comment.id;
615         blocking(context.pool(), move |conn: &'_ _| {
616           Comment::update_removed(conn, comment_id, remove_data)
617         })
618         .await??;
619       }
620     }
621
622     // Mod tables
623     // TODO eventually do correct expires
624     let expires = match data.expires {
625       Some(time) => Some(naive_from_unix(time)),
626       None => None,
627     };
628
629     let form = ModBanFromCommunityForm {
630       mod_user_id: user.id,
631       other_user_id: data.user_id,
632       community_id: data.community_id,
633       reason: data.reason.to_owned(),
634       banned: Some(data.ban),
635       expires,
636     };
637     blocking(context.pool(), move |conn| {
638       ModBanFromCommunity::create(conn, &form)
639     })
640     .await??;
641
642     let user_id = data.user_id;
643     let user_view = blocking(context.pool(), move |conn| {
644       UserView::get_user_secure(conn, user_id)
645     })
646     .await??;
647
648     let res = BanFromCommunityResponse {
649       user: user_view,
650       banned: data.ban,
651     };
652
653     context.chat_server().do_send(SendCommunityRoomMessage {
654       op: UserOperation::BanFromCommunity,
655       response: res.clone(),
656       community_id,
657       websocket_id,
658     });
659
660     Ok(res)
661   }
662 }
663
664 #[async_trait::async_trait(?Send)]
665 impl Perform for AddModToCommunity {
666   type Response = AddModToCommunityResponse;
667
668   async fn perform(
669     &self,
670     context: &Data<LemmyContext>,
671     websocket_id: Option<ConnectionId>,
672   ) -> Result<AddModToCommunityResponse, LemmyError> {
673     let data: &AddModToCommunity = &self;
674     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
675
676     let community_moderator_form = CommunityModeratorForm {
677       community_id: data.community_id,
678       user_id: data.user_id,
679     };
680
681     let community_id = data.community_id;
682
683     // Verify that only mods or admins can add mod
684     is_mod_or_admin(context.pool(), user.id, community_id).await?;
685
686     if data.added {
687       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
688       if blocking(context.pool(), join).await?.is_err() {
689         return Err(APIError::err("community_moderator_already_exists").into());
690       }
691     } else {
692       let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
693       if blocking(context.pool(), leave).await?.is_err() {
694         return Err(APIError::err("community_moderator_already_exists").into());
695       }
696     }
697
698     // Mod tables
699     let form = ModAddCommunityForm {
700       mod_user_id: user.id,
701       other_user_id: data.user_id,
702       community_id: data.community_id,
703       removed: Some(!data.added),
704     };
705     blocking(context.pool(), move |conn| {
706       ModAddCommunity::create(conn, &form)
707     })
708     .await??;
709
710     let community_id = data.community_id;
711     let moderators = blocking(context.pool(), move |conn| {
712       CommunityModeratorView::for_community(conn, community_id)
713     })
714     .await??;
715
716     let res = AddModToCommunityResponse { moderators };
717
718     context.chat_server().do_send(SendCommunityRoomMessage {
719       op: UserOperation::AddModToCommunity,
720       response: res.clone(),
721       community_id,
722       websocket_id,
723     });
724
725     Ok(res)
726   }
727 }
728
729 #[async_trait::async_trait(?Send)]
730 impl Perform for TransferCommunity {
731   type Response = GetCommunityResponse;
732
733   async fn perform(
734     &self,
735     context: &Data<LemmyContext>,
736     _websocket_id: Option<ConnectionId>,
737   ) -> Result<GetCommunityResponse, LemmyError> {
738     let data: &TransferCommunity = &self;
739     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
740
741     let community_id = data.community_id;
742     let read_community = blocking(context.pool(), move |conn| {
743       Community::read(conn, community_id)
744     })
745     .await??;
746
747     let site_creator_id = blocking(context.pool(), move |conn| {
748       Site::read(conn, 1).map(|s| s.creator_id)
749     })
750     .await??;
751
752     let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
753
754     let creator_index = admins
755       .iter()
756       .position(|r| r.id == site_creator_id)
757       .context(location_info!())?;
758     let creator_user = admins.remove(creator_index);
759     admins.insert(0, creator_user);
760
761     // Make sure user is the creator, or an admin
762     if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
763       return Err(APIError::err("not_an_admin").into());
764     }
765
766     let community_id = data.community_id;
767     let new_creator = data.user_id;
768     let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
769     if blocking(context.pool(), update).await?.is_err() {
770       return Err(APIError::err("couldnt_update_community").into());
771     };
772
773     // You also have to re-do the community_moderator table, reordering it.
774     let community_id = data.community_id;
775     let mut community_mods = blocking(context.pool(), move |conn| {
776       CommunityModeratorView::for_community(conn, community_id)
777     })
778     .await??;
779     let creator_index = community_mods
780       .iter()
781       .position(|r| r.user_id == data.user_id)
782       .context(location_info!())?;
783     let creator_user = community_mods.remove(creator_index);
784     community_mods.insert(0, creator_user);
785
786     let community_id = data.community_id;
787     blocking(context.pool(), move |conn| {
788       CommunityModerator::delete_for_community(conn, community_id)
789     })
790     .await??;
791
792     // TODO: this should probably be a bulk operation
793     for cmod in &community_mods {
794       let community_moderator_form = CommunityModeratorForm {
795         community_id: cmod.community_id,
796         user_id: cmod.user_id,
797       };
798
799       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
800       if blocking(context.pool(), join).await?.is_err() {
801         return Err(APIError::err("community_moderator_already_exists").into());
802       }
803     }
804
805     // Mod tables
806     let form = ModAddCommunityForm {
807       mod_user_id: user.id,
808       other_user_id: data.user_id,
809       community_id: data.community_id,
810       removed: Some(false),
811     };
812     blocking(context.pool(), move |conn| {
813       ModAddCommunity::create(conn, &form)
814     })
815     .await??;
816
817     let community_id = data.community_id;
818     let user_id = user.id;
819     let community_view = match blocking(context.pool(), move |conn| {
820       CommunityView::read(conn, community_id, Some(user_id))
821     })
822     .await?
823     {
824       Ok(community) => community,
825       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
826     };
827
828     let community_id = data.community_id;
829     let moderators = match blocking(context.pool(), move |conn| {
830       CommunityModeratorView::for_community(conn, community_id)
831     })
832     .await?
833     {
834       Ok(moderators) => moderators,
835       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
836     };
837
838     // Return the jwt
839     Ok(GetCommunityResponse {
840       community: community_view,
841       moderators,
842       online: 0,
843     })
844   }
845 }
846
847 fn send_community_websocket(
848   res: &CommunityResponse,
849   context: &Data<LemmyContext>,
850   websocket_id: Option<ConnectionId>,
851   op: UserOperation,
852 ) {
853   // Strip out the user id and subscribed when sending to others
854   let mut res_sent = res.clone();
855   res_sent.community.user_id = None;
856   res_sent.community.subscribed = None;
857
858   context.chat_server().do_send(SendCommunityRoomMessage {
859     op,
860     response: res_sent,
861     community_id: res.community.id,
862     websocket_id,
863   });
864 }
865
866 #[async_trait::async_trait(?Send)]
867 impl Perform for CommunityJoin {
868   type Response = CommunityJoinResponse;
869
870   async fn perform(
871     &self,
872     context: &Data<LemmyContext>,
873     websocket_id: Option<ConnectionId>,
874   ) -> Result<CommunityJoinResponse, LemmyError> {
875     let data: &CommunityJoin = &self;
876
877     if let Some(ws_id) = websocket_id {
878       context.chat_server().do_send(JoinCommunityRoom {
879         community_id: data.community_id,
880         id: ws_id,
881       });
882     }
883
884     Ok(CommunityJoinResponse { joined: true })
885   }
886 }
887
888 #[async_trait::async_trait(?Send)]
889 impl Perform for ModJoin {
890   type Response = ModJoinResponse;
891
892   async fn perform(
893     &self,
894     context: &Data<LemmyContext>,
895     websocket_id: Option<ConnectionId>,
896   ) -> Result<ModJoinResponse, LemmyError> {
897     let data: &ModJoin = &self;
898
899     if let Some(ws_id) = websocket_id {
900       context.chat_server().do_send(JoinModRoom {
901         community_id: data.community_id,
902         id: ws_id,
903       });
904     }
905
906     Ok(ModJoinResponse { joined: true })
907   }
908 }