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