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