1 use crate::{request::purge_image_from_pictrs, sensitive::Sensitive, site::FederatedInstances};
3 newtypes::{CommunityId, LocalUserId, PersonId, PostId},
7 email_verification::{EmailVerification, EmailVerificationForm},
8 password_reset_request::PasswordResetRequest,
10 person_block::PersonBlock,
11 post::{Post, PostRead, PostReadForm},
12 registration_application::RegistrationApplication,
16 traits::{Crud, Readable},
21 comment_view::CommentQueryBuilder,
22 structs::{LocalUserSettingsView, LocalUserView},
24 use lemmy_db_views_actor::structs::{
25 CommunityModeratorView,
26 CommunityPersonBanView,
31 email::{send_email, translations::Lang},
33 settings::structs::Settings,
34 utils::generate_random_string,
36 use reqwest_middleware::ClientWithMiddleware;
37 use rosetta_i18n::{Language, LanguageId};
38 use std::str::FromStr;
41 pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
43 F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
46 let pool = pool.clone();
47 let blocking_span = tracing::info_span!("blocking operation");
48 let res = actix_web::web::block(move || {
49 let entered = blocking_span.enter();
50 let conn = pool.get()?;
53 Ok(res) as Result<T, LemmyError>
60 #[tracing::instrument(skip_all)]
61 pub async fn is_mod_or_admin(
64 community_id: CommunityId,
65 ) -> Result<(), LemmyError> {
66 let is_mod_or_admin = blocking(pool, move |conn| {
67 CommunityView::is_mod_or_admin(conn, person_id, community_id)
71 return Err(LemmyError::from_message("not_a_mod_or_admin"));
76 pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
77 if !local_user_view.person.admin {
78 return Err(LemmyError::from_message("not_an_admin"));
83 #[tracing::instrument(skip_all)]
84 pub async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
85 blocking(pool, move |conn| Post::read(conn, post_id))
87 .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_post"))
90 #[tracing::instrument(skip_all)]
91 pub async fn mark_post_as_read(
95 ) -> Result<PostRead, LemmyError> {
96 let post_read_form = PostReadForm { post_id, person_id };
98 blocking(pool, move |conn| {
99 PostRead::mark_as_read(conn, &post_read_form)
102 .map_err(|e| LemmyError::from_error_message(e, "couldnt_mark_post_as_read"))
105 #[tracing::instrument(skip_all)]
106 pub async fn mark_post_as_unread(
110 ) -> Result<usize, LemmyError> {
111 let post_read_form = PostReadForm { post_id, person_id };
113 blocking(pool, move |conn| {
114 PostRead::mark_as_unread(conn, &post_read_form)
117 .map_err(|e| LemmyError::from_error_message(e, "couldnt_mark_post_as_read"))
120 #[tracing::instrument(skip_all)]
121 pub async fn get_local_user_view_from_jwt(
125 ) -> Result<LocalUserView, LemmyError> {
126 let claims = Claims::decode(jwt, &secret.jwt_secret)
127 .map_err(|e| e.with_message("not_logged_in"))?
129 let local_user_id = LocalUserId(claims.sub);
130 let local_user_view =
131 blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
132 // Check for a site ban
133 if local_user_view.person.is_banned() {
134 return Err(LemmyError::from_message("site_ban"));
137 // Check for user deletion
138 if local_user_view.person.deleted {
139 return Err(LemmyError::from_message("deleted"));
142 check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
147 /// Checks if user's token was issued before user's password reset.
148 pub fn check_validator_time(
149 validator_time: &chrono::NaiveDateTime,
151 ) -> Result<(), LemmyError> {
152 let user_validation_time = validator_time.timestamp();
153 if user_validation_time > claims.iat {
154 Err(LemmyError::from_message("not_logged_in"))
160 #[tracing::instrument(skip_all)]
161 pub async fn get_local_user_view_from_jwt_opt(
162 jwt: Option<&Sensitive<String>>,
165 ) -> Result<Option<LocalUserView>, LemmyError> {
167 Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool, secret).await?)),
172 #[tracing::instrument(skip_all)]
173 pub async fn get_local_user_settings_view_from_jwt(
174 jwt: &Sensitive<String>,
177 ) -> Result<LocalUserSettingsView, LemmyError> {
178 let claims = Claims::decode(jwt.as_ref(), &secret.jwt_secret)
179 .map_err(|e| e.with_message("not_logged_in"))?
181 let local_user_id = LocalUserId(claims.sub);
182 let local_user_view = blocking(pool, move |conn| {
183 LocalUserSettingsView::read(conn, local_user_id)
186 // Check for a site ban
187 if local_user_view.person.is_banned() {
188 return Err(LemmyError::from_message("site_ban"));
191 check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
196 #[tracing::instrument(skip_all)]
197 pub async fn get_local_user_settings_view_from_jwt_opt(
198 jwt: Option<&Sensitive<String>>,
201 ) -> Result<Option<LocalUserSettingsView>, LemmyError> {
203 Some(jwt) => Ok(Some(
204 get_local_user_settings_view_from_jwt(jwt, pool, secret).await?,
210 #[tracing::instrument(skip_all)]
211 pub async fn check_community_ban(
213 community_id: CommunityId,
215 ) -> Result<(), LemmyError> {
217 move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
218 if blocking(pool, is_banned).await? {
219 Err(LemmyError::from_message("community_ban"))
225 #[tracing::instrument(skip_all)]
226 pub async fn check_community_deleted_or_removed(
227 community_id: CommunityId,
229 ) -> Result<(), LemmyError> {
230 let community = blocking(pool, move |conn| Community::read(conn, community_id))
232 .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?;
233 if community.deleted || community.removed {
234 Err(LemmyError::from_message("deleted"))
240 pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> {
241 if post.deleted || post.removed {
242 Err(LemmyError::from_message("deleted"))
248 #[tracing::instrument(skip_all)]
249 pub async fn check_person_block(
251 potential_blocker_id: PersonId,
253 ) -> Result<(), LemmyError> {
254 let is_blocked = move |conn: &'_ _| PersonBlock::read(conn, potential_blocker_id, my_id).is_ok();
255 if blocking(pool, is_blocked).await? {
256 Err(LemmyError::from_message("person_block"))
262 #[tracing::instrument(skip_all)]
263 pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
265 let site = blocking(pool, Site::read_local_site).await??;
266 if !site.enable_downvotes {
267 return Err(LemmyError::from_message("downvotes_disabled"));
273 #[tracing::instrument(skip_all)]
274 pub async fn check_private_instance(
275 local_user_view: &Option<LocalUserView>,
277 ) -> Result<(), LemmyError> {
278 if local_user_view.is_none() {
279 let site = blocking(pool, Site::read_local_site).await?;
281 // The site might not be set up yet
282 if let Ok(site) = site {
283 if site.private_instance {
284 return Err(LemmyError::from_message("instance_is_private"));
291 #[tracing::instrument(skip_all)]
292 pub async fn build_federated_instances(
295 ) -> Result<Option<FederatedInstances>, LemmyError> {
296 let federation_config = &settings.federation;
297 let hostname = &settings.hostname;
298 let federation = federation_config.to_owned();
299 if federation.enabled {
300 let distinct_communities = blocking(pool, move |conn| {
301 Community::distinct_federated_communities(conn)
305 let allowed = federation.allowed_instances;
306 let blocked = federation.blocked_instances;
308 let mut linked = distinct_communities
310 .map(|actor_id| Ok(actor_id.host_str().unwrap_or("").to_string()))
311 .collect::<Result<Vec<String>, LemmyError>>()?;
313 if let Some(allowed) = allowed.as_ref() {
314 linked.extend_from_slice(allowed);
317 if let Some(blocked) = blocked.as_ref() {
318 linked.retain(|a| !blocked.contains(a) && !a.eq(hostname));
321 // Sort and remove dupes
322 linked.sort_unstable();
325 Ok(Some(FederatedInstances {
335 /// Checks the password length
336 pub fn password_length_check(pass: &str) -> Result<(), LemmyError> {
337 if !(10..=60).contains(&pass.len()) {
338 Err(LemmyError::from_message("invalid_password"))
344 /// Checks the site description length
345 pub fn site_description_length_check(description: &str) -> Result<(), LemmyError> {
346 if description.len() > 150 {
347 Err(LemmyError::from_message("site_description_length_overflow"))
353 /// Checks for a honeypot. If this field is filled, fail the rest of the function
354 pub fn honeypot_check(honeypot: &Option<String>) -> Result<(), LemmyError> {
355 if honeypot.is_some() {
356 Err(LemmyError::from_message("honeypot_fail"))
362 pub fn send_email_to_user(
363 local_user_view: &LocalUserView,
368 if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
372 if let Some(user_email) = &local_user_view.local_user.email {
376 &local_user_view.person.name,
381 Err(e) => warn!("{}", e),
386 pub async fn send_password_reset_email(
387 user: &LocalUserView,
390 ) -> Result<(), LemmyError> {
391 // Generate a random token
392 let token = generate_random_string();
395 let token2 = token.clone();
396 let local_user_id = user.local_user.id;
397 blocking(pool, move |conn| {
398 PasswordResetRequest::create_token(conn, local_user_id, &token2)
402 let email = &user.local_user.email.to_owned().expect("email");
403 let lang = get_user_lang(user);
404 let subject = &lang.password_reset_subject(&user.person.name);
405 let protocol_and_hostname = settings.get_protocol_and_hostname();
406 let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token);
407 let body = &lang.password_reset_body(reset_link, &user.person.name);
408 send_email(subject, email, &user.person.name, body, settings)
411 /// Send a verification email
412 pub async fn send_verification_email(
413 user: &LocalUserView,
417 ) -> Result<(), LemmyError> {
418 let form = EmailVerificationForm {
419 local_user_id: user.local_user.id,
420 email: new_email.to_string(),
421 verification_token: generate_random_string(),
423 let verify_link = format!(
424 "{}/verify_email/{}",
425 settings.get_protocol_and_hostname(),
426 &form.verification_token
428 blocking(pool, move |conn| EmailVerification::create(conn, &form)).await??;
430 let lang = get_user_lang(user);
431 let subject = lang.verify_email_subject(&settings.hostname);
432 let body = lang.verify_email_body(&settings.hostname, &user.person.name, verify_link);
433 send_email(&subject, new_email, &user.person.name, &body, settings)?;
438 pub fn send_email_verification_success(
439 user: &LocalUserView,
441 ) -> Result<(), LemmyError> {
442 let email = &user.local_user.email.to_owned().expect("email");
443 let lang = get_user_lang(user);
444 let subject = &lang.email_verified_subject(&user.person.actor_id);
445 let body = &lang.email_verified_body();
446 send_email(subject, email, &user.person.name, body, settings)
449 pub fn get_user_lang(user: &LocalUserView) -> Lang {
450 let user_lang = LanguageId::new(user.local_user.lang.clone());
451 Lang::from_language_id(&user_lang).unwrap_or_else(|| {
452 let en = LanguageId::new("en");
453 Lang::from_language_id(&en).expect("default language")
457 pub fn send_application_approved_email(
458 user: &LocalUserView,
460 ) -> Result<(), LemmyError> {
461 let email = &user.local_user.email.to_owned().expect("email");
462 let lang = get_user_lang(user);
463 let subject = lang.registration_approved_subject(&user.person.actor_id);
464 let body = lang.registration_approved_body(&settings.hostname);
465 send_email(&subject, email, &user.person.name, &body, settings)
468 pub async fn check_registration_application(
470 local_user_view: &LocalUserView,
472 ) -> Result<(), LemmyError> {
473 if site.require_application
474 && !local_user_view.local_user.accepted_application
475 && !local_user_view.person.admin
477 // Fetch the registration, see if its denied
478 let local_user_id = local_user_view.local_user.id;
479 let registration = blocking(pool, move |conn| {
480 RegistrationApplication::find_by_local_user_id(conn, local_user_id)
483 if let Some(deny_reason) = registration.deny_reason {
484 let lang = get_user_lang(local_user_view);
485 let registration_denied_message = format!("{}: {}", lang.registration_denied(), &deny_reason);
486 return Err(LemmyError::from_message(®istration_denied_message));
488 return Err(LemmyError::from_message("registration_application_pending"));
494 /// TODO this check should be removed after https://github.com/LemmyNet/lemmy/issues/868 is done.
495 pub async fn check_private_instance_and_federation_enabled(
498 ) -> Result<(), LemmyError> {
499 let site_opt = blocking(pool, Site::read_local_site).await?;
501 if let Ok(site) = site_opt {
502 if site.private_instance && settings.federation.enabled {
503 return Err(LemmyError::from_message(
504 "Cannot have both private instance and federation enabled.",
511 pub async fn purge_image_posts_for_person(
512 banned_person_id: PersonId,
515 client: &ClientWithMiddleware,
516 ) -> Result<(), LemmyError> {
517 let posts = blocking(pool, move |conn: &'_ _| {
518 Post::fetch_pictrs_posts_for_creator(conn, banned_person_id)
522 if let Some(url) = post.url {
523 purge_image_from_pictrs(client, settings, &url).await.ok();
525 if let Some(thumbnail_url) = post.thumbnail_url {
526 purge_image_from_pictrs(client, settings, &thumbnail_url)
532 blocking(pool, move |conn| {
533 Post::remove_pictrs_post_images_and_thumbnails_for_creator(conn, banned_person_id)
540 pub async fn purge_image_posts_for_community(
541 banned_community_id: CommunityId,
544 client: &ClientWithMiddleware,
545 ) -> Result<(), LemmyError> {
546 let posts = blocking(pool, move |conn: &'_ _| {
547 Post::fetch_pictrs_posts_for_community(conn, banned_community_id)
551 if let Some(url) = post.url {
552 purge_image_from_pictrs(client, settings, &url).await.ok();
554 if let Some(thumbnail_url) = post.thumbnail_url {
555 purge_image_from_pictrs(client, settings, &thumbnail_url)
561 blocking(pool, move |conn| {
562 Post::remove_pictrs_post_images_and_thumbnails_for_community(conn, banned_community_id)
569 pub async fn remove_user_data(
570 banned_person_id: PersonId,
573 client: &ClientWithMiddleware,
574 ) -> Result<(), LemmyError> {
576 let person = blocking(pool, move |conn| Person::read(conn, banned_person_id)).await??;
577 if let Some(avatar) = person.avatar {
578 purge_image_from_pictrs(client, settings, &avatar)
582 if let Some(banner) = person.banner {
583 purge_image_from_pictrs(client, settings, &banner)
588 // Update the fields to None
589 blocking(pool, move |conn| {
590 Person::remove_avatar_and_banner(conn, banned_person_id)
595 blocking(pool, move |conn: &'_ _| {
596 Post::update_removed_for_creator(conn, banned_person_id, None, true)
601 purge_image_posts_for_person(banned_person_id, pool, settings, client).await?;
604 // Remove all communities where they're the top mod
605 // for now, remove the communities manually
606 let first_mod_communities = blocking(pool, move |conn: &'_ _| {
607 CommunityModeratorView::get_community_first_mods(conn)
611 // Filter to only this banned users top communities
612 let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
614 .filter(|fmc| fmc.moderator.id == banned_person_id)
617 for first_mod_community in banned_user_first_communities {
618 let community_id = first_mod_community.community.id;
619 blocking(pool, move |conn: &'_ _| {
620 Community::update_removed(conn, community_id, true)
624 // Delete the community images
625 if let Some(icon) = first_mod_community.community.icon {
626 purge_image_from_pictrs(client, settings, &icon).await.ok();
628 if let Some(banner) = first_mod_community.community.banner {
629 purge_image_from_pictrs(client, settings, &banner)
633 // Update the fields to None
634 blocking(pool, move |conn| {
635 Community::remove_avatar_and_banner(conn, community_id)
641 blocking(pool, move |conn: &'_ _| {
642 Comment::update_removed_for_creator(conn, banned_person_id, true)
649 pub async fn remove_user_data_in_community(
650 community_id: CommunityId,
651 banned_person_id: PersonId,
653 ) -> Result<(), LemmyError> {
655 blocking(pool, move |conn| {
656 Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
661 // TODO Diesel doesn't allow updates with joins, so this has to be a loop
662 let comments = blocking(pool, move |conn| {
663 CommentQueryBuilder::create(conn)
664 .creator_id(banned_person_id)
665 .community_id(community_id)
666 .limit(std::i64::MAX)
671 for comment_view in &comments {
672 let comment_id = comment_view.comment.id;
673 blocking(pool, move |conn| {
674 Comment::update_removed(conn, comment_id, true)
682 pub async fn delete_user_account(
686 client: &ClientWithMiddleware,
687 ) -> Result<(), LemmyError> {
688 // Delete their images
689 let person = blocking(pool, move |conn| Person::read(conn, person_id)).await??;
690 if let Some(avatar) = person.avatar {
691 purge_image_from_pictrs(client, settings, &avatar)
695 if let Some(banner) = person.banner {
696 purge_image_from_pictrs(client, settings, &banner)
700 // No need to update avatar and banner, those are handled in Person::delete_account
703 let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id);
704 blocking(pool, permadelete)
706 .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
709 let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id);
710 blocking(pool, permadelete)
712 .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_post"))?;
715 purge_image_posts_for_person(person_id, pool, settings, client).await?;
717 blocking(pool, move |conn| Person::delete_account(conn, person_id)).await??;
722 pub async fn listing_type_with_site_default(
723 listing_type: Option<ListingType>,
725 ) -> Result<ListingType, LemmyError> {
726 Ok(match listing_type {
729 let site = blocking(pool, Site::read_local_site).await??;
730 ListingType::from_str(&site.default_post_listing_type)?