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