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