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