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