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