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