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