1 use crate::{captcha_as_wav_base64, Perform};
2 use actix_web::web::Data;
5 use captcha::{gen, Difficulty};
7 use lemmy_api_common::{
9 collect_moderated_communities,
10 get_local_user_view_from_jwt,
12 password_length_check,
15 use lemmy_db_queries::{
16 diesel_option_overwrite,
17 diesel_option_overwrite_to_url,
18 from_opt_str_to_opt_enum,
21 community::Community_,
22 local_user::LocalUser_,
23 password_reset_request::PasswordResetRequest_,
25 person_mention::PersonMention_,
27 private_message::PrivateMessage_,
33 use lemmy_db_schema::{
38 local_user::{LocalUser, LocalUserForm},
40 password_reset_request::*,
42 person_block::{PersonBlock, PersonBlockForm},
45 private_message::PrivateMessage,
50 comment_report_view::CommentReportView,
51 comment_view::CommentQueryBuilder,
52 local_user_view::LocalUserView,
53 post_report_view::PostReportView,
55 use lemmy_db_views_actor::{
56 community_moderator_view::CommunityModeratorView,
57 person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
58 person_view::PersonViewSafe,
64 utils::{generate_random_string, is_valid_display_name, is_valid_matrix_id, naive_from_unix},
69 use lemmy_websocket::{
70 messages::{CaptchaItem, SendAllMessage, SendUserRoomMessage},
75 #[async_trait::async_trait(?Send)]
76 impl Perform for Login {
77 type Response = LoginResponse;
81 context: &Data<LemmyContext>,
82 _websocket_id: Option<ConnectionId>,
83 ) -> Result<LoginResponse, LemmyError> {
84 let data: &Login = self;
86 // Fetch that username / email
87 let username_or_email = data.username_or_email.clone();
88 let local_user_view = blocking(context.pool(), move |conn| {
89 LocalUserView::find_by_email_or_name(conn, &username_or_email)
92 .map_err(|_| ApiError::err("couldnt_find_that_username_or_email"))?;
94 // Verify the password
95 let valid: bool = verify(
97 &local_user_view.local_user.password_encrypted,
101 return Err(ApiError::err("password_incorrect").into());
107 local_user_view.local_user.id.0,
108 &context.secret().jwt_secret,
109 &context.settings().hostname,
115 #[async_trait::async_trait(?Send)]
116 impl Perform for GetCaptcha {
117 type Response = GetCaptchaResponse;
121 context: &Data<LemmyContext>,
122 _websocket_id: Option<ConnectionId>,
123 ) -> Result<Self::Response, LemmyError> {
124 let captcha_settings = context.settings().captcha;
126 if !captcha_settings.enabled {
127 return Ok(GetCaptchaResponse { ok: None });
130 let captcha = match captcha_settings.difficulty.as_str() {
131 "easy" => gen(Difficulty::Easy),
132 "medium" => gen(Difficulty::Medium),
133 "hard" => gen(Difficulty::Hard),
134 _ => gen(Difficulty::Medium),
137 let answer = captcha.chars_as_string();
139 let png = captcha.as_base64().expect("failed to generate captcha");
141 let uuid = uuid::Uuid::new_v4().to_string();
143 let wav = captcha_as_wav_base64(&captcha);
145 let captcha_item = CaptchaItem {
147 uuid: uuid.to_owned(),
148 expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
151 // Stores the captcha item on the queue
152 context.chat_server().do_send(captcha_item);
154 Ok(GetCaptchaResponse {
155 ok: Some(CaptchaResponse { png, wav, uuid }),
160 #[async_trait::async_trait(?Send)]
161 impl Perform for SaveUserSettings {
162 type Response = LoginResponse;
166 context: &Data<LemmyContext>,
167 _websocket_id: Option<ConnectionId>,
168 ) -> Result<LoginResponse, LemmyError> {
169 let data: &SaveUserSettings = self;
170 let local_user_view =
171 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
173 let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
174 let banner = diesel_option_overwrite_to_url(&data.banner)?;
175 let email = diesel_option_overwrite(&data.email);
176 let bio = diesel_option_overwrite(&data.bio);
177 let display_name = diesel_option_overwrite(&data.display_name);
178 let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
179 let bot_account = data.bot_account;
181 if let Some(Some(bio)) = &bio {
182 if bio.chars().count() > 300 {
183 return Err(ApiError::err("bio_length_overflow").into());
187 if let Some(Some(display_name)) = &display_name {
188 if !is_valid_display_name(
190 context.settings().actor_name_max_length,
192 return Err(ApiError::err("invalid_username").into());
196 if let Some(Some(matrix_user_id)) = &matrix_user_id {
197 if !is_valid_matrix_id(matrix_user_id) {
198 return Err(ApiError::err("invalid_matrix_id").into());
202 let local_user_id = local_user_view.local_user.id;
203 let person_id = local_user_view.person.id;
204 let default_listing_type = data.default_listing_type;
205 let default_sort_type = data.default_sort_type;
206 let password_encrypted = local_user_view.local_user.password_encrypted;
208 let person_form = PersonForm {
209 name: local_user_view.person.name,
215 updated: Some(naive_now()),
224 last_refreshed_at: None,
225 shared_inbox_url: None,
230 let person_res = blocking(context.pool(), move |conn| {
231 Person::update(conn, person_id, &person_form)
234 let _updated_person: Person = match person_res {
237 return Err(ApiError::err("user_already_exists").into());
241 let local_user_form = LocalUserForm {
245 show_nsfw: data.show_nsfw,
246 show_bot_accounts: data.show_bot_accounts,
247 show_scores: data.show_scores,
248 theme: data.theme.to_owned(),
250 default_listing_type,
251 lang: data.lang.to_owned(),
252 show_avatars: data.show_avatars,
253 show_read_posts: data.show_read_posts,
254 show_new_post_notifs: data.show_new_post_notifs,
255 send_notifications_to_email: data.send_notifications_to_email,
258 let local_user_res = blocking(context.pool(), move |conn| {
259 LocalUser::update(conn, local_user_id, &local_user_form)
262 let updated_local_user = match local_user_res {
265 let err_type = if e.to_string()
266 == "duplicate key value violates unique constraint \"local_user_email_key\""
268 "email_already_exists"
270 "user_already_exists"
273 return Err(ApiError::err(err_type).into());
280 updated_local_user.id.0,
281 &context.secret().jwt_secret,
282 &context.settings().hostname,
288 #[async_trait::async_trait(?Send)]
289 impl Perform for ChangePassword {
290 type Response = LoginResponse;
294 context: &Data<LemmyContext>,
295 _websocket_id: Option<ConnectionId>,
296 ) -> Result<LoginResponse, LemmyError> {
297 let data: &ChangePassword = self;
298 let local_user_view =
299 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
301 password_length_check(&data.new_password)?;
303 // Make sure passwords match
304 if data.new_password != data.new_password_verify {
305 return Err(ApiError::err("passwords_dont_match").into());
308 // Check the old password
309 let valid: bool = verify(
311 &local_user_view.local_user.password_encrypted,
315 return Err(ApiError::err("password_incorrect").into());
318 let local_user_id = local_user_view.local_user.id;
319 let new_password = data.new_password.to_owned();
320 let updated_local_user = blocking(context.pool(), move |conn| {
321 LocalUser::update_password(conn, local_user_id, &new_password)
328 updated_local_user.id.0,
329 &context.secret().jwt_secret,
330 &context.settings().hostname,
336 #[async_trait::async_trait(?Send)]
337 impl Perform for AddAdmin {
338 type Response = AddAdminResponse;
342 context: &Data<LemmyContext>,
343 websocket_id: Option<ConnectionId>,
344 ) -> Result<AddAdminResponse, LemmyError> {
345 let data: &AddAdmin = self;
346 let local_user_view =
347 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
349 // Make sure user is an admin
350 is_admin(&local_user_view)?;
352 let added = data.added;
353 let added_person_id = data.person_id;
354 let added_admin = match blocking(context.pool(), move |conn| {
355 Person::add_admin(conn, added_person_id, added)
361 return Err(ApiError::err("couldnt_update_user").into());
366 let form = ModAddForm {
367 mod_person_id: local_user_view.person.id,
368 other_person_id: added_admin.id,
369 removed: Some(!data.added),
372 blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
374 let site_creator_id = blocking(context.pool(), move |conn| {
375 Site::read(conn, 1).map(|s| s.creator_id)
379 let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
380 let creator_index = admins
382 .position(|r| r.person.id == site_creator_id)
383 .context(location_info!())?;
384 let creator_person = admins.remove(creator_index);
385 admins.insert(0, creator_person);
387 let res = AddAdminResponse { admins };
389 context.chat_server().do_send(SendAllMessage {
390 op: UserOperation::AddAdmin,
391 response: res.clone(),
399 #[async_trait::async_trait(?Send)]
400 impl Perform for BanPerson {
401 type Response = BanPersonResponse;
405 context: &Data<LemmyContext>,
406 websocket_id: Option<ConnectionId>,
407 ) -> Result<BanPersonResponse, LemmyError> {
408 let data: &BanPerson = self;
409 let local_user_view =
410 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
412 // Make sure user is an admin
413 is_admin(&local_user_view)?;
416 let banned_person_id = data.person_id;
417 let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban);
418 if blocking(context.pool(), ban_person).await?.is_err() {
419 return Err(ApiError::err("couldnt_update_user").into());
422 // Remove their data if that's desired
423 if data.remove_data.unwrap_or(false) {
425 blocking(context.pool(), move |conn: &'_ _| {
426 Post::update_removed_for_creator(conn, banned_person_id, None, true)
431 // Remove all communities where they're the top mod
432 // for now, remove the communities manually
433 let first_mod_communities = blocking(context.pool(), move |conn: &'_ _| {
434 CommunityModeratorView::get_community_first_mods(conn)
438 // Filter to only this banned users top communities
439 let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
441 .filter(|fmc| fmc.moderator.id == banned_person_id)
444 for first_mod_community in banned_user_first_communities {
445 blocking(context.pool(), move |conn: &'_ _| {
446 Community::update_removed(conn, first_mod_community.community.id, true)
452 blocking(context.pool(), move |conn: &'_ _| {
453 Comment::update_removed_for_creator(conn, banned_person_id, true)
459 let expires = data.expires.map(naive_from_unix);
461 let form = ModBanForm {
462 mod_person_id: local_user_view.person.id,
463 other_person_id: data.person_id,
464 reason: data.reason.to_owned(),
465 banned: Some(data.ban),
469 blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
471 let person_id = data.person_id;
472 let person_view = blocking(context.pool(), move |conn| {
473 PersonViewSafe::read(conn, person_id)
477 let res = BanPersonResponse {
482 context.chat_server().do_send(SendAllMessage {
483 op: UserOperation::BanPerson,
484 response: res.clone(),
492 #[async_trait::async_trait(?Send)]
493 impl Perform for BlockPerson {
494 type Response = BlockPersonResponse;
498 context: &Data<LemmyContext>,
499 _websocket_id: Option<ConnectionId>,
500 ) -> Result<BlockPersonResponse, LemmyError> {
501 let data: &BlockPerson = self;
502 let local_user_view =
503 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
505 let target_id = data.person_id;
506 let person_id = local_user_view.person.id;
508 // Don't let a person block themselves
509 if target_id == person_id {
510 return Err(ApiError::err("cant_block_yourself").into());
513 let person_block_form = PersonBlockForm {
519 let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form);
520 if blocking(context.pool(), block).await?.is_err() {
521 return Err(ApiError::err("person_block_already_exists").into());
524 let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form);
525 if blocking(context.pool(), unblock).await?.is_err() {
526 return Err(ApiError::err("person_block_already_exists").into());
530 // TODO does any federated stuff need to be done here?
532 let person_view = blocking(context.pool(), move |conn| {
533 PersonViewSafe::read(conn, target_id)
537 let res = BlockPersonResponse {
546 #[async_trait::async_trait(?Send)]
547 impl Perform for GetReplies {
548 type Response = GetRepliesResponse;
552 context: &Data<LemmyContext>,
553 _websocket_id: Option<ConnectionId>,
554 ) -> Result<GetRepliesResponse, LemmyError> {
555 let data: &GetReplies = self;
556 let local_user_view =
557 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
559 let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
561 let page = data.page;
562 let limit = data.limit;
563 let unread_only = data.unread_only;
564 let person_id = local_user_view.person.id;
565 let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
567 let replies = blocking(context.pool(), move |conn| {
568 CommentQueryBuilder::create(conn)
570 .unread_only(unread_only)
571 .recipient_id(person_id)
572 .show_bot_accounts(show_bot_accounts)
573 .my_person_id(person_id)
580 Ok(GetRepliesResponse { replies })
584 #[async_trait::async_trait(?Send)]
585 impl Perform for GetPersonMentions {
586 type Response = GetPersonMentionsResponse;
590 context: &Data<LemmyContext>,
591 _websocket_id: Option<ConnectionId>,
592 ) -> Result<GetPersonMentionsResponse, LemmyError> {
593 let data: &GetPersonMentions = self;
594 let local_user_view =
595 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
597 let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
599 let page = data.page;
600 let limit = data.limit;
601 let unread_only = data.unread_only;
602 let person_id = local_user_view.person.id;
603 let mentions = blocking(context.pool(), move |conn| {
604 PersonMentionQueryBuilder::create(conn)
605 .recipient_id(person_id)
606 .my_person_id(person_id)
608 .unread_only(unread_only)
615 Ok(GetPersonMentionsResponse { mentions })
619 #[async_trait::async_trait(?Send)]
620 impl Perform for MarkPersonMentionAsRead {
621 type Response = PersonMentionResponse;
625 context: &Data<LemmyContext>,
626 _websocket_id: Option<ConnectionId>,
627 ) -> Result<PersonMentionResponse, LemmyError> {
628 let data: &MarkPersonMentionAsRead = self;
629 let local_user_view =
630 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
632 let person_mention_id = data.person_mention_id;
633 let read_person_mention = blocking(context.pool(), move |conn| {
634 PersonMention::read(conn, person_mention_id)
638 if local_user_view.person.id != read_person_mention.recipient_id {
639 return Err(ApiError::err("couldnt_update_comment").into());
642 let person_mention_id = read_person_mention.id;
643 let read = data.read;
645 move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read);
646 if blocking(context.pool(), update_mention).await?.is_err() {
647 return Err(ApiError::err("couldnt_update_comment").into());
650 let person_mention_id = read_person_mention.id;
651 let person_id = local_user_view.person.id;
652 let person_mention_view = blocking(context.pool(), move |conn| {
653 PersonMentionView::read(conn, person_mention_id, Some(person_id))
657 Ok(PersonMentionResponse {
663 #[async_trait::async_trait(?Send)]
664 impl Perform for MarkAllAsRead {
665 type Response = GetRepliesResponse;
669 context: &Data<LemmyContext>,
670 _websocket_id: Option<ConnectionId>,
671 ) -> Result<GetRepliesResponse, LemmyError> {
672 let data: &MarkAllAsRead = self;
673 let local_user_view =
674 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
676 let person_id = local_user_view.person.id;
677 let replies = blocking(context.pool(), move |conn| {
678 CommentQueryBuilder::create(conn)
679 .my_person_id(person_id)
680 .recipient_id(person_id)
688 // TODO: this should probably be a bulk operation
689 // Not easy to do as a bulk operation,
690 // because recipient_id isn't in the comment table
691 for comment_view in &replies {
692 let reply_id = comment_view.comment.id;
693 let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
694 if blocking(context.pool(), mark_as_read).await?.is_err() {
695 return Err(ApiError::err("couldnt_update_comment").into());
699 // Mark all user mentions as read
700 let update_person_mentions =
701 move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id);
702 if blocking(context.pool(), update_person_mentions)
706 return Err(ApiError::err("couldnt_update_comment").into());
709 // Mark all private_messages as read
710 let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id);
711 if blocking(context.pool(), update_pm).await?.is_err() {
712 return Err(ApiError::err("couldnt_update_private_message").into());
715 Ok(GetRepliesResponse { replies: vec![] })
719 #[async_trait::async_trait(?Send)]
720 impl Perform for PasswordReset {
721 type Response = PasswordResetResponse;
725 context: &Data<LemmyContext>,
726 _websocket_id: Option<ConnectionId>,
727 ) -> Result<PasswordResetResponse, LemmyError> {
728 let data: &PasswordReset = self;
731 let email = data.email.clone();
732 let local_user_view = blocking(context.pool(), move |conn| {
733 LocalUserView::find_by_email(conn, &email)
736 .map_err(|_| ApiError::err("couldnt_find_that_username_or_email"))?;
738 // Generate a random token
739 let token = generate_random_string();
742 let token2 = token.clone();
743 let local_user_id = local_user_view.local_user.id;
744 blocking(context.pool(), move |conn| {
745 PasswordResetRequest::create_token(conn, local_user_id, &token2)
749 // Email the pure token to the user.
750 // TODO no i18n support here.
751 let email = &local_user_view.local_user.email.expect("email");
752 let subject = &format!("Password reset for {}", local_user_view.person.name);
753 let protocol_and_hostname = &context.settings().get_protocol_and_hostname();
754 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);
758 &local_user_view.person.name,
762 .map_err(|e| ApiError::err(&e))?;
764 Ok(PasswordResetResponse {})
768 #[async_trait::async_trait(?Send)]
769 impl Perform for PasswordChange {
770 type Response = LoginResponse;
774 context: &Data<LemmyContext>,
775 _websocket_id: Option<ConnectionId>,
776 ) -> Result<LoginResponse, LemmyError> {
777 let data: &PasswordChange = self;
779 // Fetch the user_id from the token
780 let token = data.token.clone();
781 let local_user_id = blocking(context.pool(), move |conn| {
782 PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id)
786 password_length_check(&data.password)?;
788 // Make sure passwords match
789 if data.password != data.password_verify {
790 return Err(ApiError::err("passwords_dont_match").into());
793 // Update the user with the new password
794 let password = data.password.clone();
795 let updated_local_user = blocking(context.pool(), move |conn| {
796 LocalUser::update_password(conn, local_user_id, &password)
799 .map_err(|_| ApiError::err("couldnt_update_user"))?;
804 updated_local_user.id.0,
805 &context.secret().jwt_secret,
806 &context.settings().hostname,
812 #[async_trait::async_trait(?Send)]
813 impl Perform for GetReportCount {
814 type Response = GetReportCountResponse;
818 context: &Data<LemmyContext>,
819 websocket_id: Option<ConnectionId>,
820 ) -> Result<GetReportCountResponse, LemmyError> {
821 let data: &GetReportCount = self;
822 let local_user_view =
823 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
825 let person_id = local_user_view.person.id;
826 let community_id = data.community;
828 collect_moderated_communities(person_id, community_id, context.pool()).await?;
831 if community_ids.is_empty() {
832 GetReportCountResponse {
838 let ids = community_ids.clone();
839 let comment_reports = blocking(context.pool(), move |conn| {
840 CommentReportView::get_report_count(conn, &ids)
844 let ids = community_ids.clone();
845 let post_reports = blocking(context.pool(), move |conn| {
846 PostReportView::get_report_count(conn, &ids)
850 GetReportCountResponse {
851 community: data.community,
858 context.chat_server().do_send(SendUserRoomMessage {
859 op: UserOperation::GetReportCount,
860 response: res.clone(),
861 local_recipient_id: local_user_view.local_user.id,