]> Untitled Git - lemmy.git/blob - crates/api/src/local_user.rs
User / community blocking. Fixes #426 (#1604)
[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   get_local_user_view_from_jwt,
11   is_admin,
12   password_length_check,
13   person::*,
14 };
15 use lemmy_db_queries::{
16   diesel_option_overwrite,
17   diesel_option_overwrite_to_url,
18   from_opt_str_to_opt_enum,
19   source::{
20     comment::Comment_,
21     community::Community_,
22     local_user::LocalUser_,
23     password_reset_request::PasswordResetRequest_,
24     person::Person_,
25     person_mention::PersonMention_,
26     post::Post_,
27     private_message::PrivateMessage_,
28   },
29   Blockable,
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_block::{PersonBlock, PersonBlockForm},
43     person_mention::*,
44     post::Post,
45     private_message::PrivateMessage,
46     site::*,
47   },
48 };
49 use lemmy_db_views::{
50   comment_report_view::CommentReportView,
51   comment_view::CommentQueryBuilder,
52   local_user_view::LocalUserView,
53   post_report_view::PostReportView,
54 };
55 use lemmy_db_views_actor::{
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 BlockPerson {
476   type Response = BlockPersonResponse;
477
478   async fn perform(
479     &self,
480     context: &Data<LemmyContext>,
481     _websocket_id: Option<ConnectionId>,
482   ) -> Result<BlockPersonResponse, LemmyError> {
483     let data: &BlockPerson = self;
484     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
485
486     let target_id = data.person_id;
487     let person_id = local_user_view.person.id;
488
489     // Don't let a person block themselves
490     if target_id == person_id {
491       return Err(ApiError::err("cant_block_yourself").into());
492     }
493
494     let person_block_form = PersonBlockForm {
495       person_id,
496       target_id,
497     };
498
499     if data.block {
500       let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form);
501       if blocking(context.pool(), block).await?.is_err() {
502         return Err(ApiError::err("person_block_already_exists").into());
503       }
504     } else {
505       let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form);
506       if blocking(context.pool(), unblock).await?.is_err() {
507         return Err(ApiError::err("person_block_already_exists").into());
508       }
509     }
510
511     // TODO does any federated stuff need to be done here?
512
513     let person_view = blocking(context.pool(), move |conn| {
514       PersonViewSafe::read(conn, target_id)
515     })
516     .await??;
517
518     let res = BlockPersonResponse {
519       person_view,
520       blocked: data.block,
521     };
522
523     Ok(res)
524   }
525 }
526
527 #[async_trait::async_trait(?Send)]
528 impl Perform for GetReplies {
529   type Response = GetRepliesResponse;
530
531   async fn perform(
532     &self,
533     context: &Data<LemmyContext>,
534     _websocket_id: Option<ConnectionId>,
535   ) -> Result<GetRepliesResponse, LemmyError> {
536     let data: &GetReplies = self;
537     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
538
539     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
540
541     let page = data.page;
542     let limit = data.limit;
543     let unread_only = data.unread_only;
544     let person_id = local_user_view.person.id;
545     let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
546
547     let replies = blocking(context.pool(), move |conn| {
548       CommentQueryBuilder::create(conn)
549         .sort(sort)
550         .unread_only(unread_only)
551         .recipient_id(person_id)
552         .show_bot_accounts(show_bot_accounts)
553         .my_person_id(person_id)
554         .page(page)
555         .limit(limit)
556         .list()
557     })
558     .await??;
559
560     Ok(GetRepliesResponse { replies })
561   }
562 }
563
564 #[async_trait::async_trait(?Send)]
565 impl Perform for GetPersonMentions {
566   type Response = GetPersonMentionsResponse;
567
568   async fn perform(
569     &self,
570     context: &Data<LemmyContext>,
571     _websocket_id: Option<ConnectionId>,
572   ) -> Result<GetPersonMentionsResponse, LemmyError> {
573     let data: &GetPersonMentions = self;
574     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
575
576     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
577
578     let page = data.page;
579     let limit = data.limit;
580     let unread_only = data.unread_only;
581     let person_id = local_user_view.person.id;
582     let mentions = blocking(context.pool(), move |conn| {
583       PersonMentionQueryBuilder::create(conn)
584         .recipient_id(person_id)
585         .my_person_id(person_id)
586         .sort(sort)
587         .unread_only(unread_only)
588         .page(page)
589         .limit(limit)
590         .list()
591     })
592     .await??;
593
594     Ok(GetPersonMentionsResponse { mentions })
595   }
596 }
597
598 #[async_trait::async_trait(?Send)]
599 impl Perform for MarkPersonMentionAsRead {
600   type Response = PersonMentionResponse;
601
602   async fn perform(
603     &self,
604     context: &Data<LemmyContext>,
605     _websocket_id: Option<ConnectionId>,
606   ) -> Result<PersonMentionResponse, LemmyError> {
607     let data: &MarkPersonMentionAsRead = self;
608     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
609
610     let person_mention_id = data.person_mention_id;
611     let read_person_mention = blocking(context.pool(), move |conn| {
612       PersonMention::read(conn, person_mention_id)
613     })
614     .await??;
615
616     if local_user_view.person.id != read_person_mention.recipient_id {
617       return Err(ApiError::err("couldnt_update_comment").into());
618     }
619
620     let person_mention_id = read_person_mention.id;
621     let read = data.read;
622     let update_mention =
623       move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read);
624     if blocking(context.pool(), update_mention).await?.is_err() {
625       return Err(ApiError::err("couldnt_update_comment").into());
626     };
627
628     let person_mention_id = read_person_mention.id;
629     let person_id = local_user_view.person.id;
630     let person_mention_view = blocking(context.pool(), move |conn| {
631       PersonMentionView::read(conn, person_mention_id, Some(person_id))
632     })
633     .await??;
634
635     Ok(PersonMentionResponse {
636       person_mention_view,
637     })
638   }
639 }
640
641 #[async_trait::async_trait(?Send)]
642 impl Perform for MarkAllAsRead {
643   type Response = GetRepliesResponse;
644
645   async fn perform(
646     &self,
647     context: &Data<LemmyContext>,
648     _websocket_id: Option<ConnectionId>,
649   ) -> Result<GetRepliesResponse, LemmyError> {
650     let data: &MarkAllAsRead = self;
651     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
652
653     let person_id = local_user_view.person.id;
654     let replies = blocking(context.pool(), move |conn| {
655       CommentQueryBuilder::create(conn)
656         .my_person_id(person_id)
657         .recipient_id(person_id)
658         .unread_only(true)
659         .page(1)
660         .limit(999)
661         .list()
662     })
663     .await??;
664
665     // TODO: this should probably be a bulk operation
666     // Not easy to do as a bulk operation,
667     // because recipient_id isn't in the comment table
668     for comment_view in &replies {
669       let reply_id = comment_view.comment.id;
670       let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
671       if blocking(context.pool(), mark_as_read).await?.is_err() {
672         return Err(ApiError::err("couldnt_update_comment").into());
673       }
674     }
675
676     // Mark all user mentions as read
677     let update_person_mentions =
678       move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id);
679     if blocking(context.pool(), update_person_mentions)
680       .await?
681       .is_err()
682     {
683       return Err(ApiError::err("couldnt_update_comment").into());
684     }
685
686     // Mark all private_messages as read
687     let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id);
688     if blocking(context.pool(), update_pm).await?.is_err() {
689       return Err(ApiError::err("couldnt_update_private_message").into());
690     }
691
692     Ok(GetRepliesResponse { replies: vec![] })
693   }
694 }
695
696 #[async_trait::async_trait(?Send)]
697 impl Perform for PasswordReset {
698   type Response = PasswordResetResponse;
699
700   async fn perform(
701     &self,
702     context: &Data<LemmyContext>,
703     _websocket_id: Option<ConnectionId>,
704   ) -> Result<PasswordResetResponse, LemmyError> {
705     let data: &PasswordReset = self;
706
707     // Fetch that email
708     let email = data.email.clone();
709     let local_user_view = blocking(context.pool(), move |conn| {
710       LocalUserView::find_by_email(conn, &email)
711     })
712     .await?
713     .map_err(|_| ApiError::err("couldnt_find_that_username_or_email"))?;
714
715     // Generate a random token
716     let token = generate_random_string();
717
718     // Insert the row
719     let token2 = token.clone();
720     let local_user_id = local_user_view.local_user.id;
721     blocking(context.pool(), move |conn| {
722       PasswordResetRequest::create_token(conn, local_user_id, &token2)
723     })
724     .await??;
725
726     // Email the pure token to the user.
727     // TODO no i18n support here.
728     let email = &local_user_view.local_user.email.expect("email");
729     let subject = &format!("Password reset for {}", local_user_view.person.name);
730     let hostname = &Settings::get().get_protocol_and_hostname();
731     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);
732     send_email(subject, email, &local_user_view.person.name, html)
733       .map_err(|e| ApiError::err(&e))?;
734
735     Ok(PasswordResetResponse {})
736   }
737 }
738
739 #[async_trait::async_trait(?Send)]
740 impl Perform for PasswordChange {
741   type Response = LoginResponse;
742
743   async fn perform(
744     &self,
745     context: &Data<LemmyContext>,
746     _websocket_id: Option<ConnectionId>,
747   ) -> Result<LoginResponse, LemmyError> {
748     let data: &PasswordChange = self;
749
750     // Fetch the user_id from the token
751     let token = data.token.clone();
752     let local_user_id = blocking(context.pool(), move |conn| {
753       PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id)
754     })
755     .await??;
756
757     password_length_check(&data.password)?;
758
759     // Make sure passwords match
760     if data.password != data.password_verify {
761       return Err(ApiError::err("passwords_dont_match").into());
762     }
763
764     // Update the user with the new password
765     let password = data.password.clone();
766     let updated_local_user = blocking(context.pool(), move |conn| {
767       LocalUser::update_password(conn, local_user_id, &password)
768     })
769     .await?
770     .map_err(|_| ApiError::err("couldnt_update_user"))?;
771
772     // Return the jwt
773     Ok(LoginResponse {
774       jwt: Claims::jwt(updated_local_user.id.0)?,
775     })
776   }
777 }
778
779 #[async_trait::async_trait(?Send)]
780 impl Perform for GetReportCount {
781   type Response = GetReportCountResponse;
782
783   async fn perform(
784     &self,
785     context: &Data<LemmyContext>,
786     websocket_id: Option<ConnectionId>,
787   ) -> Result<GetReportCountResponse, LemmyError> {
788     let data: &GetReportCount = self;
789     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
790
791     let person_id = local_user_view.person.id;
792     let community_id = data.community;
793     let community_ids =
794       collect_moderated_communities(person_id, community_id, context.pool()).await?;
795
796     let res = {
797       if community_ids.is_empty() {
798         GetReportCountResponse {
799           community: None,
800           comment_reports: 0,
801           post_reports: 0,
802         }
803       } else {
804         let ids = community_ids.clone();
805         let comment_reports = blocking(context.pool(), move |conn| {
806           CommentReportView::get_report_count(conn, &ids)
807         })
808         .await??;
809
810         let ids = community_ids.clone();
811         let post_reports = blocking(context.pool(), move |conn| {
812           PostReportView::get_report_count(conn, &ids)
813         })
814         .await??;
815
816         GetReportCountResponse {
817           community: data.community,
818           comment_reports,
819           post_reports,
820         }
821       }
822     };
823
824     context.chat_server().do_send(SendUserRoomMessage {
825       op: UserOperation::GetReportCount,
826       response: res.clone(),
827       local_recipient_id: local_user_view.local_user.id,
828       websocket_id,
829     });
830
831     Ok(res)
832   }
833 }