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