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