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