]> Untitled Git - lemmy.git/blob - src/api/community.rs
c5199fe54b72ce32c26c71a81d9a84fc87baf6f4
[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_db::{
9   comment::Comment,
10   comment_view::CommentQueryBuilder,
11   community::*,
12   community_view::*,
13   diesel_option_overwrite,
14   moderator::*,
15   naive_now,
16   post::Post,
17   site::*,
18   user_view::*,
19   Bannable,
20   Crud,
21   Followable,
22   Joinable,
23   SortType,
24 };
25 use lemmy_structs::{
26   blocking,
27   community::*,
28   websocket::{
29     GetCommunityUsersOnline,
30     JoinCommunityRoom,
31     SendCommunityRoomMessage,
32     UserOperation,
33   },
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
499     let community_id = data.community_id;
500     let user_id = user.id;
501     let mut community_view = blocking(context.pool(), move |conn| {
502       CommunityView::read(conn, community_id, Some(user_id))
503     })
504     .await??;
505
506     // TODO: this needs to return a "pending" state, until Accept is received from the remote server
507     // For now, just assume that remote follows are accepted.
508     // Otherwise, the subscribed will be null
509     if !community.local {
510       community_view.subscribed = Some(data.follow);
511     }
512
513     Ok(CommunityResponse {
514       community: community_view,
515     })
516   }
517 }
518
519 #[async_trait::async_trait(?Send)]
520 impl Perform for GetFollowedCommunities {
521   type Response = GetFollowedCommunitiesResponse;
522
523   async fn perform(
524     &self,
525     context: &Data<LemmyContext>,
526     _websocket_id: Option<ConnectionId>,
527   ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
528     let data: &GetFollowedCommunities = &self;
529     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
530
531     let user_id = user.id;
532     let communities = match blocking(context.pool(), move |conn| {
533       CommunityFollowerView::for_user(conn, user_id)
534     })
535     .await?
536     {
537       Ok(communities) => communities,
538       _ => return Err(APIError::err("system_err_login").into()),
539     };
540
541     // Return the jwt
542     Ok(GetFollowedCommunitiesResponse { communities })
543   }
544 }
545
546 #[async_trait::async_trait(?Send)]
547 impl Perform for BanFromCommunity {
548   type Response = BanFromCommunityResponse;
549
550   async fn perform(
551     &self,
552     context: &Data<LemmyContext>,
553     websocket_id: Option<ConnectionId>,
554   ) -> Result<BanFromCommunityResponse, LemmyError> {
555     let data: &BanFromCommunity = &self;
556     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
557
558     let community_id = data.community_id;
559     let banned_user_id = data.user_id;
560
561     // Verify that only mods or admins can ban
562     is_mod_or_admin(context.pool(), user.id, community_id).await?;
563
564     let community_user_ban_form = CommunityUserBanForm {
565       community_id: data.community_id,
566       user_id: data.user_id,
567     };
568
569     if data.ban {
570       let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
571       if blocking(context.pool(), ban).await?.is_err() {
572         return Err(APIError::err("community_user_already_banned").into());
573       }
574     } else {
575       let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
576       if blocking(context.pool(), unban).await?.is_err() {
577         return Err(APIError::err("community_user_already_banned").into());
578       }
579     }
580
581     // Remove/Restore their data if that's desired
582     if let Some(remove_data) = data.remove_data {
583       // Posts
584       blocking(context.pool(), move |conn: &'_ _| {
585         Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
586       })
587       .await??;
588
589       // Comments
590       // Diesel doesn't allow updates with joins, so this has to be a loop
591       let comments = blocking(context.pool(), move |conn| {
592         CommentQueryBuilder::create(conn)
593           .for_creator_id(banned_user_id)
594           .for_community_id(community_id)
595           .limit(std::i64::MAX)
596           .list()
597       })
598       .await??;
599
600       for comment in &comments {
601         let comment_id = comment.id;
602         blocking(context.pool(), move |conn: &'_ _| {
603           Comment::update_removed(conn, comment_id, remove_data)
604         })
605         .await??;
606       }
607     }
608
609     // Mod tables
610     // TODO eventually do correct expires
611     let expires = match data.expires {
612       Some(time) => Some(naive_from_unix(time)),
613       None => None,
614     };
615
616     let form = ModBanFromCommunityForm {
617       mod_user_id: user.id,
618       other_user_id: data.user_id,
619       community_id: data.community_id,
620       reason: data.reason.to_owned(),
621       banned: Some(data.ban),
622       expires,
623     };
624     blocking(context.pool(), move |conn| {
625       ModBanFromCommunity::create(conn, &form)
626     })
627     .await??;
628
629     let user_id = data.user_id;
630     let user_view = blocking(context.pool(), move |conn| {
631       UserView::get_user_secure(conn, user_id)
632     })
633     .await??;
634
635     let res = BanFromCommunityResponse {
636       user: user_view,
637       banned: data.ban,
638     };
639
640     context.chat_server().do_send(SendCommunityRoomMessage {
641       op: UserOperation::BanFromCommunity,
642       response: res.clone(),
643       community_id,
644       websocket_id,
645     });
646
647     Ok(res)
648   }
649 }
650
651 #[async_trait::async_trait(?Send)]
652 impl Perform for AddModToCommunity {
653   type Response = AddModToCommunityResponse;
654
655   async fn perform(
656     &self,
657     context: &Data<LemmyContext>,
658     websocket_id: Option<ConnectionId>,
659   ) -> Result<AddModToCommunityResponse, LemmyError> {
660     let data: &AddModToCommunity = &self;
661     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
662
663     let community_moderator_form = CommunityModeratorForm {
664       community_id: data.community_id,
665       user_id: data.user_id,
666     };
667
668     let community_id = data.community_id;
669
670     // Verify that only mods or admins can add mod
671     is_mod_or_admin(context.pool(), user.id, community_id).await?;
672
673     if data.added {
674       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
675       if blocking(context.pool(), join).await?.is_err() {
676         return Err(APIError::err("community_moderator_already_exists").into());
677       }
678     } else {
679       let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
680       if blocking(context.pool(), leave).await?.is_err() {
681         return Err(APIError::err("community_moderator_already_exists").into());
682       }
683     }
684
685     // Mod tables
686     let form = ModAddCommunityForm {
687       mod_user_id: user.id,
688       other_user_id: data.user_id,
689       community_id: data.community_id,
690       removed: Some(!data.added),
691     };
692     blocking(context.pool(), move |conn| {
693       ModAddCommunity::create(conn, &form)
694     })
695     .await??;
696
697     let community_id = data.community_id;
698     let moderators = blocking(context.pool(), move |conn| {
699       CommunityModeratorView::for_community(conn, community_id)
700     })
701     .await??;
702
703     let res = AddModToCommunityResponse { moderators };
704
705     context.chat_server().do_send(SendCommunityRoomMessage {
706       op: UserOperation::AddModToCommunity,
707       response: res.clone(),
708       community_id,
709       websocket_id,
710     });
711
712     Ok(res)
713   }
714 }
715
716 #[async_trait::async_trait(?Send)]
717 impl Perform for TransferCommunity {
718   type Response = GetCommunityResponse;
719
720   async fn perform(
721     &self,
722     context: &Data<LemmyContext>,
723     _websocket_id: Option<ConnectionId>,
724   ) -> Result<GetCommunityResponse, LemmyError> {
725     let data: &TransferCommunity = &self;
726     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
727
728     let community_id = data.community_id;
729     let read_community = blocking(context.pool(), move |conn| {
730       Community::read(conn, community_id)
731     })
732     .await??;
733
734     let site_creator_id = blocking(context.pool(), move |conn| {
735       Site::read(conn, 1).map(|s| s.creator_id)
736     })
737     .await??;
738
739     let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
740
741     let creator_index = admins
742       .iter()
743       .position(|r| r.id == site_creator_id)
744       .context(location_info!())?;
745     let creator_user = admins.remove(creator_index);
746     admins.insert(0, creator_user);
747
748     // Make sure user is the creator, or an admin
749     if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
750       return Err(APIError::err("not_an_admin").into());
751     }
752
753     let community_id = data.community_id;
754     let new_creator = data.user_id;
755     let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
756     if blocking(context.pool(), update).await?.is_err() {
757       return Err(APIError::err("couldnt_update_community").into());
758     };
759
760     // You also have to re-do the community_moderator table, reordering it.
761     let community_id = data.community_id;
762     let mut community_mods = blocking(context.pool(), move |conn| {
763       CommunityModeratorView::for_community(conn, community_id)
764     })
765     .await??;
766     let creator_index = community_mods
767       .iter()
768       .position(|r| r.user_id == data.user_id)
769       .context(location_info!())?;
770     let creator_user = community_mods.remove(creator_index);
771     community_mods.insert(0, creator_user);
772
773     let community_id = data.community_id;
774     blocking(context.pool(), move |conn| {
775       CommunityModerator::delete_for_community(conn, community_id)
776     })
777     .await??;
778
779     // TODO: this should probably be a bulk operation
780     for cmod in &community_mods {
781       let community_moderator_form = CommunityModeratorForm {
782         community_id: cmod.community_id,
783         user_id: cmod.user_id,
784       };
785
786       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
787       if blocking(context.pool(), join).await?.is_err() {
788         return Err(APIError::err("community_moderator_already_exists").into());
789       }
790     }
791
792     // Mod tables
793     let form = ModAddCommunityForm {
794       mod_user_id: user.id,
795       other_user_id: data.user_id,
796       community_id: data.community_id,
797       removed: Some(false),
798     };
799     blocking(context.pool(), move |conn| {
800       ModAddCommunity::create(conn, &form)
801     })
802     .await??;
803
804     let community_id = data.community_id;
805     let user_id = user.id;
806     let community_view = match blocking(context.pool(), move |conn| {
807       CommunityView::read(conn, community_id, Some(user_id))
808     })
809     .await?
810     {
811       Ok(community) => community,
812       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
813     };
814
815     let community_id = data.community_id;
816     let moderators = match blocking(context.pool(), move |conn| {
817       CommunityModeratorView::for_community(conn, community_id)
818     })
819     .await?
820     {
821       Ok(moderators) => moderators,
822       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
823     };
824
825     // Return the jwt
826     Ok(GetCommunityResponse {
827       community: community_view,
828       moderators,
829       online: 0,
830     })
831   }
832 }
833
834 pub fn send_community_websocket(
835   res: &CommunityResponse,
836   context: &Data<LemmyContext>,
837   websocket_id: Option<ConnectionId>,
838   op: UserOperation,
839 ) {
840   // Strip out the user id and subscribed when sending to others
841   let mut res_sent = res.clone();
842   res_sent.community.user_id = None;
843   res_sent.community.subscribed = None;
844
845   context.chat_server().do_send(SendCommunityRoomMessage {
846     op,
847     response: res_sent,
848     community_id: res.community.id,
849     websocket_id,
850   });
851 }
852
853 #[async_trait::async_trait(?Send)]
854 impl Perform for CommunityJoin {
855   type Response = CommunityJoinResponse;
856
857   async fn perform(
858     &self,
859     context: &Data<LemmyContext>,
860     websocket_id: Option<ConnectionId>,
861   ) -> Result<CommunityJoinResponse, LemmyError> {
862     let data: &CommunityJoin = &self;
863
864     if let Some(ws_id) = websocket_id {
865       context.chat_server().do_send(JoinCommunityRoom {
866         community_id: data.community_id,
867         id: ws_id,
868       });
869     }
870
871     Ok(CommunityJoinResponse { joined: true })
872   }
873 }