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