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