]> Untitled Git - lemmy.git/blob - crates/api/src/local_user.rs
When banning a user, remove communities they've created (#1700)
[lemmy.git] / crates / api / src / local_user.rs
1 use crate::{captcha_as_wav_base64, Perform};
2 use actix_web::web::Data;
3 use anyhow::Context;
4 use bcrypt::verify;
5 use captcha::{gen, Difficulty};
6 use chrono::Duration;
7 use lemmy_api_common::{
8   blocking,
9   collect_moderated_communities,
10   community::{GetFollowedCommunities, GetFollowedCommunitiesResponse},
11   get_local_user_view_from_jwt,
12   is_admin,
13   password_length_check,
14   person::*,
15 };
16 use lemmy_db_queries::{
17   diesel_option_overwrite,
18   diesel_option_overwrite_to_url,
19   from_opt_str_to_opt_enum,
20   source::{
21     comment::Comment_,
22     community::Community_,
23     local_user::LocalUser_,
24     password_reset_request::PasswordResetRequest_,
25     person::Person_,
26     person_mention::PersonMention_,
27     post::Post_,
28     private_message::PrivateMessage_,
29   },
30   Crud,
31   SortType,
32 };
33 use lemmy_db_schema::{
34   naive_now,
35   source::{
36     comment::Comment,
37     community::Community,
38     local_user::{LocalUser, LocalUserForm},
39     moderator::*,
40     password_reset_request::*,
41     person::*,
42     person_mention::*,
43     post::Post,
44     private_message::PrivateMessage,
45     site::*,
46   },
47 };
48 use lemmy_db_views::{
49   comment_report_view::CommentReportView,
50   comment_view::CommentQueryBuilder,
51   local_user_view::LocalUserView,
52   post_report_view::PostReportView,
53 };
54 use lemmy_db_views_actor::{
55   community_follower_view::CommunityFollowerView,
56   community_moderator_view::CommunityModeratorView,
57   person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
58   person_view::PersonViewSafe,
59 };
60 use lemmy_utils::{
61   claims::Claims,
62   email::send_email,
63   location_info,
64   settings::structs::Settings,
65   utils::{generate_random_string, is_valid_display_name, is_valid_matrix_id, naive_from_unix},
66   ApiError,
67   ConnectionId,
68   LemmyError,
69 };
70 use lemmy_websocket::{
71   messages::{CaptchaItem, SendAllMessage, SendUserRoomMessage},
72   LemmyContext,
73   UserOperation,
74 };
75
76 #[async_trait::async_trait(?Send)]
77 impl Perform for Login {
78   type Response = LoginResponse;
79
80   async fn perform(
81     &self,
82     context: &Data<LemmyContext>,
83     _websocket_id: Option<ConnectionId>,
84   ) -> Result<LoginResponse, LemmyError> {
85     let data: &Login = self;
86
87     // Fetch that username / email
88     let username_or_email = data.username_or_email.clone();
89     let local_user_view = blocking(context.pool(), move |conn| {
90       LocalUserView::find_by_email_or_name(conn, &username_or_email)
91     })
92     .await?
93     .map_err(|_| ApiError::err("couldnt_find_that_username_or_email"))?;
94
95     // Verify the password
96     let valid: bool = verify(
97       &data.password,
98       &local_user_view.local_user.password_encrypted,
99     )
100     .unwrap_or(false);
101     if !valid {
102       return Err(ApiError::err("password_incorrect").into());
103     }
104
105     // Return the jwt
106     Ok(LoginResponse {
107       jwt: Claims::jwt(local_user_view.local_user.id.0)?,
108     })
109   }
110 }
111
112 #[async_trait::async_trait(?Send)]
113 impl Perform for GetCaptcha {
114   type Response = GetCaptchaResponse;
115
116   async fn perform(
117     &self,
118     context: &Data<LemmyContext>,
119     _websocket_id: Option<ConnectionId>,
120   ) -> Result<Self::Response, LemmyError> {
121     let captcha_settings = Settings::get().captcha;
122
123     if !captcha_settings.enabled {
124       return Ok(GetCaptchaResponse { ok: None });
125     }
126
127     let captcha = match captcha_settings.difficulty.as_str() {
128       "easy" => gen(Difficulty::Easy),
129       "medium" => gen(Difficulty::Medium),
130       "hard" => gen(Difficulty::Hard),
131       _ => gen(Difficulty::Medium),
132     };
133
134     let answer = captcha.chars_as_string();
135
136     let png = captcha.as_base64().expect("failed to generate captcha");
137
138     let uuid = uuid::Uuid::new_v4().to_string();
139
140     let wav = captcha_as_wav_base64(&captcha);
141
142     let captcha_item = CaptchaItem {
143       answer,
144       uuid: uuid.to_owned(),
145       expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
146     };
147
148     // Stores the captcha item on the queue
149     context.chat_server().do_send(captcha_item);
150
151     Ok(GetCaptchaResponse {
152       ok: Some(CaptchaResponse { png, wav, uuid }),
153     })
154   }
155 }
156
157 #[async_trait::async_trait(?Send)]
158 impl Perform for SaveUserSettings {
159   type Response = LoginResponse;
160
161   async fn perform(
162     &self,
163     context: &Data<LemmyContext>,
164     _websocket_id: Option<ConnectionId>,
165   ) -> Result<LoginResponse, LemmyError> {
166     let data: &SaveUserSettings = self;
167     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
168
169     let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
170     let banner = diesel_option_overwrite_to_url(&data.banner)?;
171     let email = diesel_option_overwrite(&data.email);
172     let bio = diesel_option_overwrite(&data.bio);
173     let display_name = diesel_option_overwrite(&data.display_name);
174     let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
175     let bot_account = data.bot_account;
176
177     if let Some(Some(bio)) = &bio {
178       if bio.chars().count() > 300 {
179         return Err(ApiError::err("bio_length_overflow").into());
180       }
181     }
182
183     if let Some(Some(display_name)) = &display_name {
184       if !is_valid_display_name(display_name.trim()) {
185         return Err(ApiError::err("invalid_username").into());
186       }
187     }
188
189     if let Some(Some(matrix_user_id)) = &matrix_user_id {
190       if !is_valid_matrix_id(matrix_user_id) {
191         return Err(ApiError::err("invalid_matrix_id").into());
192       }
193     }
194
195     let local_user_id = local_user_view.local_user.id;
196     let person_id = local_user_view.person.id;
197     let default_listing_type = data.default_listing_type;
198     let default_sort_type = data.default_sort_type;
199     let password_encrypted = local_user_view.local_user.password_encrypted;
200
201     let person_form = PersonForm {
202       name: local_user_view.person.name,
203       avatar,
204       banner,
205       inbox_url: None,
206       display_name,
207       published: None,
208       updated: Some(naive_now()),
209       banned: None,
210       deleted: None,
211       actor_id: None,
212       bio,
213       local: None,
214       admin: None,
215       private_key: None,
216       public_key: None,
217       last_refreshed_at: None,
218       shared_inbox_url: None,
219       matrix_user_id,
220       bot_account,
221     };
222
223     let person_res = blocking(context.pool(), move |conn| {
224       Person::update(conn, person_id, &person_form)
225     })
226     .await?;
227     let _updated_person: Person = match person_res {
228       Ok(p) => p,
229       Err(_) => {
230         return Err(ApiError::err("user_already_exists").into());
231       }
232     };
233
234     let local_user_form = LocalUserForm {
235       person_id,
236       email,
237       password_encrypted,
238       show_nsfw: data.show_nsfw,
239       show_bot_accounts: data.show_bot_accounts,
240       show_scores: data.show_scores,
241       theme: data.theme.to_owned(),
242       default_sort_type,
243       default_listing_type,
244       lang: data.lang.to_owned(),
245       show_avatars: data.show_avatars,
246       show_read_posts: data.show_read_posts,
247       show_new_post_notifs: data.show_new_post_notifs,
248       send_notifications_to_email: data.send_notifications_to_email,
249     };
250
251     let local_user_res = blocking(context.pool(), move |conn| {
252       LocalUser::update(conn, local_user_id, &local_user_form)
253     })
254     .await?;
255     let updated_local_user = match local_user_res {
256       Ok(u) => u,
257       Err(e) => {
258         let err_type = if e.to_string()
259           == "duplicate key value violates unique constraint \"local_user_email_key\""
260         {
261           "email_already_exists"
262         } else {
263           "user_already_exists"
264         };
265
266         return Err(ApiError::err(err_type).into());
267       }
268     };
269
270     // Return the jwt
271     Ok(LoginResponse {
272       jwt: Claims::jwt(updated_local_user.id.0)?,
273     })
274   }
275 }
276
277 #[async_trait::async_trait(?Send)]
278 impl Perform for ChangePassword {
279   type Response = LoginResponse;
280
281   async fn perform(
282     &self,
283     context: &Data<LemmyContext>,
284     _websocket_id: Option<ConnectionId>,
285   ) -> Result<LoginResponse, LemmyError> {
286     let data: &ChangePassword = self;
287     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
288
289     password_length_check(&data.new_password)?;
290
291     // Make sure passwords match
292     if data.new_password != data.new_password_verify {
293       return Err(ApiError::err("passwords_dont_match").into());
294     }
295
296     // Check the old password
297     let valid: bool = verify(
298       &data.old_password,
299       &local_user_view.local_user.password_encrypted,
300     )
301     .unwrap_or(false);
302     if !valid {
303       return Err(ApiError::err("password_incorrect").into());
304     }
305
306     let local_user_id = local_user_view.local_user.id;
307     let new_password = data.new_password.to_owned();
308     let updated_local_user = blocking(context.pool(), move |conn| {
309       LocalUser::update_password(conn, local_user_id, &new_password)
310     })
311     .await??;
312
313     // Return the jwt
314     Ok(LoginResponse {
315       jwt: Claims::jwt(updated_local_user.id.0)?,
316     })
317   }
318 }
319
320 #[async_trait::async_trait(?Send)]
321 impl Perform for AddAdmin {
322   type Response = AddAdminResponse;
323
324   async fn perform(
325     &self,
326     context: &Data<LemmyContext>,
327     websocket_id: Option<ConnectionId>,
328   ) -> Result<AddAdminResponse, LemmyError> {
329     let data: &AddAdmin = self;
330     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
331
332     // Make sure user is an admin
333     is_admin(&local_user_view)?;
334
335     let added = data.added;
336     let added_person_id = data.person_id;
337     let added_admin = match blocking(context.pool(), move |conn| {
338       Person::add_admin(conn, added_person_id, added)
339     })
340     .await?
341     {
342       Ok(a) => a,
343       Err(_) => {
344         return Err(ApiError::err("couldnt_update_user").into());
345       }
346     };
347
348     // Mod tables
349     let form = ModAddForm {
350       mod_person_id: local_user_view.person.id,
351       other_person_id: added_admin.id,
352       removed: Some(!data.added),
353     };
354
355     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
356
357     let site_creator_id = blocking(context.pool(), move |conn| {
358       Site::read(conn, 1).map(|s| s.creator_id)
359     })
360     .await??;
361
362     let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
363     let creator_index = admins
364       .iter()
365       .position(|r| r.person.id == site_creator_id)
366       .context(location_info!())?;
367     let creator_person = admins.remove(creator_index);
368     admins.insert(0, creator_person);
369
370     let res = AddAdminResponse { admins };
371
372     context.chat_server().do_send(SendAllMessage {
373       op: UserOperation::AddAdmin,
374       response: res.clone(),
375       websocket_id,
376     });
377
378     Ok(res)
379   }
380 }
381
382 #[async_trait::async_trait(?Send)]
383 impl Perform for BanPerson {
384   type Response = BanPersonResponse;
385
386   async fn perform(
387     &self,
388     context: &Data<LemmyContext>,
389     websocket_id: Option<ConnectionId>,
390   ) -> Result<BanPersonResponse, LemmyError> {
391     let data: &BanPerson = self;
392     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
393
394     // Make sure user is an admin
395     is_admin(&local_user_view)?;
396
397     let ban = data.ban;
398     let banned_person_id = data.person_id;
399     let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban);
400     if blocking(context.pool(), ban_person).await?.is_err() {
401       return Err(ApiError::err("couldnt_update_user").into());
402     }
403
404     // Remove their data if that's desired
405     if data.remove_data.unwrap_or(false) {
406       // Posts
407       blocking(context.pool(), move |conn: &'_ _| {
408         Post::update_removed_for_creator(conn, banned_person_id, None, true)
409       })
410       .await??;
411
412       // Communities
413       // Remove all communities where they're the top mod
414       // for now, remove the communities manually
415       let first_mod_communities = blocking(context.pool(), move |conn: &'_ _| {
416         CommunityModeratorView::get_community_first_mods(conn)
417       })
418       .await??;
419
420       // Filter to only this banned users top communities
421       let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
422         .into_iter()
423         .filter(|fmc| fmc.moderator.id == banned_person_id)
424         .collect();
425
426       for first_mod_community in banned_user_first_communities {
427         blocking(context.pool(), move |conn: &'_ _| {
428           Community::update_removed(conn, first_mod_community.community.id, true)
429         })
430         .await??;
431       }
432
433       // Comments
434       blocking(context.pool(), move |conn: &'_ _| {
435         Comment::update_removed_for_creator(conn, banned_person_id, true)
436       })
437       .await??;
438     }
439
440     // Mod tables
441     let expires = data.expires.map(naive_from_unix);
442
443     let form = ModBanForm {
444       mod_person_id: local_user_view.person.id,
445       other_person_id: data.person_id,
446       reason: data.reason.to_owned(),
447       banned: Some(data.ban),
448       expires,
449     };
450
451     blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
452
453     let person_id = data.person_id;
454     let person_view = blocking(context.pool(), move |conn| {
455       PersonViewSafe::read(conn, person_id)
456     })
457     .await??;
458
459     let res = BanPersonResponse {
460       person_view,
461       banned: data.ban,
462     };
463
464     context.chat_server().do_send(SendAllMessage {
465       op: UserOperation::BanPerson,
466       response: res.clone(),
467       websocket_id,
468     });
469
470     Ok(res)
471   }
472 }
473
474 #[async_trait::async_trait(?Send)]
475 impl Perform for GetReplies {
476   type Response = GetRepliesResponse;
477
478   async fn perform(
479     &self,
480     context: &Data<LemmyContext>,
481     _websocket_id: Option<ConnectionId>,
482   ) -> Result<GetRepliesResponse, LemmyError> {
483     let data: &GetReplies = self;
484     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
485
486     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
487
488     let page = data.page;
489     let limit = data.limit;
490     let unread_only = data.unread_only;
491     let person_id = local_user_view.person.id;
492     let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
493
494     let replies = blocking(context.pool(), move |conn| {
495       CommentQueryBuilder::create(conn)
496         .sort(sort)
497         .unread_only(unread_only)
498         .recipient_id(person_id)
499         .show_bot_accounts(show_bot_accounts)
500         .my_person_id(person_id)
501         .page(page)
502         .limit(limit)
503         .list()
504     })
505     .await??;
506
507     Ok(GetRepliesResponse { replies })
508   }
509 }
510
511 #[async_trait::async_trait(?Send)]
512 impl Perform for GetPersonMentions {
513   type Response = GetPersonMentionsResponse;
514
515   async fn perform(
516     &self,
517     context: &Data<LemmyContext>,
518     _websocket_id: Option<ConnectionId>,
519   ) -> Result<GetPersonMentionsResponse, LemmyError> {
520     let data: &GetPersonMentions = self;
521     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
522
523     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
524
525     let page = data.page;
526     let limit = data.limit;
527     let unread_only = data.unread_only;
528     let person_id = local_user_view.person.id;
529     let mentions = blocking(context.pool(), move |conn| {
530       PersonMentionQueryBuilder::create(conn)
531         .recipient_id(person_id)
532         .my_person_id(person_id)
533         .sort(sort)
534         .unread_only(unread_only)
535         .page(page)
536         .limit(limit)
537         .list()
538     })
539     .await??;
540
541     Ok(GetPersonMentionsResponse { mentions })
542   }
543 }
544
545 #[async_trait::async_trait(?Send)]
546 impl Perform for MarkPersonMentionAsRead {
547   type Response = PersonMentionResponse;
548
549   async fn perform(
550     &self,
551     context: &Data<LemmyContext>,
552     _websocket_id: Option<ConnectionId>,
553   ) -> Result<PersonMentionResponse, LemmyError> {
554     let data: &MarkPersonMentionAsRead = self;
555     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
556
557     let person_mention_id = data.person_mention_id;
558     let read_person_mention = blocking(context.pool(), move |conn| {
559       PersonMention::read(conn, person_mention_id)
560     })
561     .await??;
562
563     if local_user_view.person.id != read_person_mention.recipient_id {
564       return Err(ApiError::err("couldnt_update_comment").into());
565     }
566
567     let person_mention_id = read_person_mention.id;
568     let read = data.read;
569     let update_mention =
570       move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read);
571     if blocking(context.pool(), update_mention).await?.is_err() {
572       return Err(ApiError::err("couldnt_update_comment").into());
573     };
574
575     let person_mention_id = read_person_mention.id;
576     let person_id = local_user_view.person.id;
577     let person_mention_view = blocking(context.pool(), move |conn| {
578       PersonMentionView::read(conn, person_mention_id, Some(person_id))
579     })
580     .await??;
581
582     Ok(PersonMentionResponse {
583       person_mention_view,
584     })
585   }
586 }
587
588 #[async_trait::async_trait(?Send)]
589 impl Perform for MarkAllAsRead {
590   type Response = GetRepliesResponse;
591
592   async fn perform(
593     &self,
594     context: &Data<LemmyContext>,
595     _websocket_id: Option<ConnectionId>,
596   ) -> Result<GetRepliesResponse, LemmyError> {
597     let data: &MarkAllAsRead = self;
598     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
599
600     let person_id = local_user_view.person.id;
601     let replies = blocking(context.pool(), move |conn| {
602       CommentQueryBuilder::create(conn)
603         .my_person_id(person_id)
604         .recipient_id(person_id)
605         .unread_only(true)
606         .page(1)
607         .limit(999)
608         .list()
609     })
610     .await??;
611
612     // TODO: this should probably be a bulk operation
613     // Not easy to do as a bulk operation,
614     // because recipient_id isn't in the comment table
615     for comment_view in &replies {
616       let reply_id = comment_view.comment.id;
617       let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
618       if blocking(context.pool(), mark_as_read).await?.is_err() {
619         return Err(ApiError::err("couldnt_update_comment").into());
620       }
621     }
622
623     // Mark all user mentions as read
624     let update_person_mentions =
625       move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id);
626     if blocking(context.pool(), update_person_mentions)
627       .await?
628       .is_err()
629     {
630       return Err(ApiError::err("couldnt_update_comment").into());
631     }
632
633     // Mark all private_messages as read
634     let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id);
635     if blocking(context.pool(), update_pm).await?.is_err() {
636       return Err(ApiError::err("couldnt_update_private_message").into());
637     }
638
639     Ok(GetRepliesResponse { replies: vec![] })
640   }
641 }
642
643 #[async_trait::async_trait(?Send)]
644 impl Perform for PasswordReset {
645   type Response = PasswordResetResponse;
646
647   async fn perform(
648     &self,
649     context: &Data<LemmyContext>,
650     _websocket_id: Option<ConnectionId>,
651   ) -> Result<PasswordResetResponse, LemmyError> {
652     let data: &PasswordReset = self;
653
654     // Fetch that email
655     let email = data.email.clone();
656     let local_user_view = blocking(context.pool(), move |conn| {
657       LocalUserView::find_by_email(conn, &email)
658     })
659     .await?
660     .map_err(|_| ApiError::err("couldnt_find_that_username_or_email"))?;
661
662     // Generate a random token
663     let token = generate_random_string();
664
665     // Insert the row
666     let token2 = token.clone();
667     let local_user_id = local_user_view.local_user.id;
668     blocking(context.pool(), move |conn| {
669       PasswordResetRequest::create_token(conn, local_user_id, &token2)
670     })
671     .await??;
672
673     // Email the pure token to the user.
674     // TODO no i18n support here.
675     let email = &local_user_view.local_user.email.expect("email");
676     let subject = &format!("Password reset for {}", local_user_view.person.name);
677     let hostname = &Settings::get().get_protocol_and_hostname();
678     let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", local_user_view.person.name, hostname, &token);
679     send_email(subject, email, &local_user_view.person.name, html)
680       .map_err(|e| ApiError::err(&e))?;
681
682     Ok(PasswordResetResponse {})
683   }
684 }
685
686 #[async_trait::async_trait(?Send)]
687 impl Perform for PasswordChange {
688   type Response = LoginResponse;
689
690   async fn perform(
691     &self,
692     context: &Data<LemmyContext>,
693     _websocket_id: Option<ConnectionId>,
694   ) -> Result<LoginResponse, LemmyError> {
695     let data: &PasswordChange = self;
696
697     // Fetch the user_id from the token
698     let token = data.token.clone();
699     let local_user_id = blocking(context.pool(), move |conn| {
700       PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id)
701     })
702     .await??;
703
704     password_length_check(&data.password)?;
705
706     // Make sure passwords match
707     if data.password != data.password_verify {
708       return Err(ApiError::err("passwords_dont_match").into());
709     }
710
711     // Update the user with the new password
712     let password = data.password.clone();
713     let updated_local_user = blocking(context.pool(), move |conn| {
714       LocalUser::update_password(conn, local_user_id, &password)
715     })
716     .await?
717     .map_err(|_| ApiError::err("couldnt_update_user"))?;
718
719     // Return the jwt
720     Ok(LoginResponse {
721       jwt: Claims::jwt(updated_local_user.id.0)?,
722     })
723   }
724 }
725
726 #[async_trait::async_trait(?Send)]
727 impl Perform for GetReportCount {
728   type Response = GetReportCountResponse;
729
730   async fn perform(
731     &self,
732     context: &Data<LemmyContext>,
733     websocket_id: Option<ConnectionId>,
734   ) -> Result<GetReportCountResponse, LemmyError> {
735     let data: &GetReportCount = self;
736     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
737
738     let person_id = local_user_view.person.id;
739     let community_id = data.community;
740     let community_ids =
741       collect_moderated_communities(person_id, community_id, context.pool()).await?;
742
743     let res = {
744       if community_ids.is_empty() {
745         GetReportCountResponse {
746           community: None,
747           comment_reports: 0,
748           post_reports: 0,
749         }
750       } else {
751         let ids = community_ids.clone();
752         let comment_reports = blocking(context.pool(), move |conn| {
753           CommentReportView::get_report_count(conn, &ids)
754         })
755         .await??;
756
757         let ids = community_ids.clone();
758         let post_reports = blocking(context.pool(), move |conn| {
759           PostReportView::get_report_count(conn, &ids)
760         })
761         .await??;
762
763         GetReportCountResponse {
764           community: data.community,
765           comment_reports,
766           post_reports,
767         }
768       }
769     };
770
771     context.chat_server().do_send(SendUserRoomMessage {
772       op: UserOperation::GetReportCount,
773       response: res.clone(),
774       local_recipient_id: local_user_view.local_user.id,
775       websocket_id,
776     });
777
778     Ok(res)
779   }
780 }
781
782 #[async_trait::async_trait(?Send)]
783 impl Perform for GetFollowedCommunities {
784   type Response = GetFollowedCommunitiesResponse;
785
786   async fn perform(
787     &self,
788     context: &Data<LemmyContext>,
789     _websocket_id: Option<ConnectionId>,
790   ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
791     let data: &GetFollowedCommunities = self;
792     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
793
794     let person_id = local_user_view.person.id;
795     let communities = blocking(context.pool(), move |conn| {
796       CommunityFollowerView::for_person(conn, person_id)
797     })
798     .await?
799     .map_err(|_| ApiError::err("system_err_login"))?;
800
801     // Return the jwt
802     Ok(GetFollowedCommunitiesResponse { communities })
803   }
804 }