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