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