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