]> Untitled Git - lemmy.git/blob - lemmy_api/src/community.rs
Merge remote-tracking branch 'origin/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   Bannable,
28   Crud,
29   Followable,
30   Joinable,
31   SortType,
32 };
33 use lemmy_structs::{blocking, community::*};
34 use lemmy_utils::{
35   apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
36   location_info,
37   utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
38   APIError,
39   ConnectionId,
40   LemmyError,
41 };
42 use lemmy_websocket::{
43   messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage},
44   LemmyContext,
45   UserOperation,
46 };
47 use std::str::FromStr;
48
49 #[async_trait::async_trait(?Send)]
50 impl Perform for GetCommunity {
51   type Response = GetCommunityResponse;
52
53   async fn perform(
54     &self,
55     context: &Data<LemmyContext>,
56     _websocket_id: Option<ConnectionId>,
57   ) -> Result<GetCommunityResponse, LemmyError> {
58     let data: &GetCommunity = &self;
59     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
60     let user_id = user.map(|u| u.id);
61
62     let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
63     let community = match data.id {
64       Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
65       None => 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     };
74
75     let community_id = community.id;
76     let community_view = match blocking(context.pool(), move |conn| {
77       CommunityView::read(conn, community_id, user_id)
78     })
79     .await?
80     {
81       Ok(community) => community,
82       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
83     };
84
85     let community_id = community.id;
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_actor_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     let community_moderator_form = CommunityModeratorForm {
183       community_id: inserted_community.id,
184       user_id: user.id,
185     };
186
187     let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
188     if blocking(context.pool(), join).await?.is_err() {
189       return Err(APIError::err("community_moderator_already_exists").into());
190     }
191
192     let community_follower_form = CommunityFollowerForm {
193       community_id: inserted_community.id,
194       user_id: user.id,
195       pending: false,
196     };
197
198     let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
199     if blocking(context.pool(), follow).await?.is_err() {
200       return Err(APIError::err("community_follower_already_exists").into());
201     }
202
203     let user_id = user.id;
204     let community_view = blocking(context.pool(), move |conn| {
205       CommunityView::read(conn, inserted_community.id, Some(user_id))
206     })
207     .await??;
208
209     Ok(CommunityResponse { community_view })
210   }
211 }
212
213 #[async_trait::async_trait(?Send)]
214 impl Perform for EditCommunity {
215   type Response = CommunityResponse;
216
217   async fn perform(
218     &self,
219     context: &Data<LemmyContext>,
220     websocket_id: Option<ConnectionId>,
221   ) -> Result<CommunityResponse, LemmyError> {
222     let data: &EditCommunity = &self;
223     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
224
225     check_slurs(&data.title)?;
226     check_slurs_opt(&data.description)?;
227
228     // Verify its a mod (only mods can edit it)
229     let edit_id = data.edit_id;
230     let mods: Vec<i32> = blocking(context.pool(), move |conn| {
231       CommunityModeratorView::for_community(conn, edit_id)
232         .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
233     })
234     .await??;
235     if !mods.contains(&user.id) {
236       return Err(APIError::err("not_a_moderator").into());
237     }
238
239     let edit_id = data.edit_id;
240     let read_community =
241       blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
242
243     let icon = diesel_option_overwrite(&data.icon);
244     let banner = diesel_option_overwrite(&data.banner);
245
246     check_optional_url(&icon)?;
247     check_optional_url(&banner)?;
248
249     let community_form = CommunityForm {
250       name: read_community.name,
251       title: data.title.to_owned(),
252       description: data.description.to_owned(),
253       icon,
254       banner,
255       category_id: data.category_id.to_owned(),
256       creator_id: read_community.creator_id,
257       removed: Some(read_community.removed),
258       deleted: Some(read_community.deleted),
259       nsfw: data.nsfw,
260       updated: Some(naive_now()),
261       actor_id: Some(read_community.actor_id),
262       local: read_community.local,
263       private_key: read_community.private_key,
264       public_key: read_community.public_key,
265       last_refreshed_at: None,
266       published: None,
267     };
268
269     let edit_id = data.edit_id;
270     match blocking(context.pool(), move |conn| {
271       Community::update(conn, edit_id, &community_form)
272     })
273     .await?
274     {
275       Ok(community) => community,
276       Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
277     };
278
279     // TODO there needs to be some kind of an apub update
280     // process for communities and users
281
282     let edit_id = data.edit_id;
283     let user_id = user.id;
284     let community_view = blocking(context.pool(), move |conn| {
285       CommunityView::read(conn, edit_id, Some(user_id))
286     })
287     .await??;
288
289     let res = CommunityResponse { community_view };
290
291     send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
292
293     Ok(res)
294   }
295 }
296
297 #[async_trait::async_trait(?Send)]
298 impl Perform for DeleteCommunity {
299   type Response = CommunityResponse;
300
301   async fn perform(
302     &self,
303     context: &Data<LemmyContext>,
304     websocket_id: Option<ConnectionId>,
305   ) -> Result<CommunityResponse, LemmyError> {
306     let data: &DeleteCommunity = &self;
307     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
308
309     // Verify its the creator (only a creator can delete the community)
310     let edit_id = data.edit_id;
311     let read_community =
312       blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
313     if read_community.creator_id != user.id {
314       return Err(APIError::err("no_community_edit_allowed").into());
315     }
316
317     // Do the delete
318     let edit_id = data.edit_id;
319     let deleted = data.deleted;
320     let updated_community = match blocking(context.pool(), move |conn| {
321       Community::update_deleted(conn, edit_id, deleted)
322     })
323     .await?
324     {
325       Ok(community) => community,
326       Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
327     };
328
329     // Send apub messages
330     if deleted {
331       updated_community.send_delete(context).await?;
332     } else {
333       updated_community.send_undo_delete(context).await?;
334     }
335
336     let edit_id = data.edit_id;
337     let user_id = user.id;
338     let community_view = blocking(context.pool(), move |conn| {
339       CommunityView::read(conn, edit_id, Some(user_id))
340     })
341     .await??;
342
343     let res = CommunityResponse { community_view };
344
345     send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
346
347     Ok(res)
348   }
349 }
350
351 #[async_trait::async_trait(?Send)]
352 impl Perform for RemoveCommunity {
353   type Response = CommunityResponse;
354
355   async fn perform(
356     &self,
357     context: &Data<LemmyContext>,
358     websocket_id: Option<ConnectionId>,
359   ) -> Result<CommunityResponse, LemmyError> {
360     let data: &RemoveCommunity = &self;
361     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
362
363     // Verify its an admin (only an admin can remove a community)
364     is_admin(context.pool(), user.id).await?;
365
366     // Do the remove
367     let edit_id = data.edit_id;
368     let removed = data.removed;
369     let updated_community = match blocking(context.pool(), move |conn| {
370       Community::update_removed(conn, edit_id, removed)
371     })
372     .await?
373     {
374       Ok(community) => community,
375       Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
376     };
377
378     // Mod tables
379     let expires = match data.expires {
380       Some(time) => Some(naive_from_unix(time)),
381       None => None,
382     };
383     let form = ModRemoveCommunityForm {
384       mod_user_id: user.id,
385       community_id: data.edit_id,
386       removed: Some(removed),
387       reason: data.reason.to_owned(),
388       expires,
389     };
390     blocking(context.pool(), move |conn| {
391       ModRemoveCommunity::create(conn, &form)
392     })
393     .await??;
394
395     // Apub messages
396     if removed {
397       updated_community.send_remove(context).await?;
398     } else {
399       updated_community.send_undo_remove(context).await?;
400     }
401
402     let edit_id = data.edit_id;
403     let user_id = user.id;
404     let community_view = blocking(context.pool(), move |conn| {
405       CommunityView::read(conn, edit_id, Some(user_id))
406     })
407     .await??;
408
409     let res = CommunityResponse { community_view };
410
411     send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
412
413     Ok(res)
414   }
415 }
416
417 #[async_trait::async_trait(?Send)]
418 impl Perform for ListCommunities {
419   type Response = ListCommunitiesResponse;
420
421   async fn perform(
422     &self,
423     context: &Data<LemmyContext>,
424     _websocket_id: Option<ConnectionId>,
425   ) -> Result<ListCommunitiesResponse, LemmyError> {
426     let data: &ListCommunities = &self;
427     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
428
429     let user_id = match &user {
430       Some(user) => Some(user.id),
431       None => None,
432     };
433
434     let show_nsfw = match &user {
435       Some(user) => user.show_nsfw,
436       None => false,
437     };
438
439     let sort = SortType::from_str(&data.sort)?;
440
441     let page = data.page;
442     let limit = data.limit;
443     let communities = blocking(context.pool(), move |conn| {
444       CommunityQueryBuilder::create(conn, user_id)
445         .sort(&sort)
446         .show_nsfw(show_nsfw)
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           .for_creator_id(banned_user_id)
599           .for_community_id(community_id)
600           .limit(std::i64::MAX)
601           .list()
602       })
603       .await??;
604
605       for comment in &comments {
606         let comment_id = 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 }