]> Untitled Git - lemmy.git/blob - crates/api_common/src/utils.rs
Adding diesel enums for SortType and ListingType (#2808)
[lemmy.git] / crates / api_common / src / utils.rs
1 use crate::{request::purge_image_from_pictrs, sensitive::Sensitive, site::FederatedInstances};
2 use anyhow::Context;
3 use chrono::NaiveDateTime;
4 use lemmy_db_schema::{
5   impls::person::is_banned,
6   newtypes::{CommunityId, DbUrl, LocalUserId, PersonId, PostId},
7   source::{
8     comment::{Comment, CommentUpdateForm},
9     community::{Community, CommunityModerator, CommunityUpdateForm},
10     email_verification::{EmailVerification, EmailVerificationForm},
11     instance::Instance,
12     local_site::LocalSite,
13     local_site_rate_limit::LocalSiteRateLimit,
14     password_reset_request::PasswordResetRequest,
15     person::{Person, PersonUpdateForm},
16     person_block::PersonBlock,
17     post::{Post, PostRead, PostReadForm},
18     registration_application::RegistrationApplication,
19     secret::Secret,
20   },
21   traits::{Crud, Readable},
22   utils::DbPool,
23   RegistrationMode,
24 };
25 use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView};
26 use lemmy_db_views_actor::structs::{
27   CommunityModeratorView,
28   CommunityPersonBanView,
29   CommunityView,
30   PersonView,
31 };
32 use lemmy_utils::{
33   claims::Claims,
34   email::{send_email, translations::Lang},
35   error::LemmyError,
36   location_info,
37   rate_limit::RateLimitConfig,
38   settings::structs::Settings,
39   utils::slurs::build_slur_regex,
40 };
41 use regex::Regex;
42 use reqwest_middleware::ClientWithMiddleware;
43 use rosetta_i18n::{Language, LanguageId};
44 use tracing::warn;
45 use url::{ParseError, Url};
46
47 #[tracing::instrument(skip_all)]
48 pub async fn is_mod_or_admin(
49   pool: &DbPool,
50   person_id: PersonId,
51   community_id: CommunityId,
52 ) -> Result<(), LemmyError> {
53   let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person_id, community_id).await?;
54   if !is_mod_or_admin {
55     return Err(LemmyError::from_message("not_a_mod_or_admin"));
56   }
57   Ok(())
58 }
59
60 #[tracing::instrument(skip_all)]
61 pub async fn is_mod_or_admin_opt(
62   pool: &DbPool,
63   local_user_view: Option<&LocalUserView>,
64   community_id: Option<CommunityId>,
65 ) -> Result<(), LemmyError> {
66   if let Some(local_user_view) = local_user_view {
67     if let Some(community_id) = community_id {
68       is_mod_or_admin(pool, local_user_view.person.id, community_id).await
69     } else {
70       is_admin(local_user_view)
71     }
72   } else {
73     Err(LemmyError::from_message("not_a_mod_or_admin"))
74   }
75 }
76
77 pub async fn is_top_admin(pool: &DbPool, person_id: PersonId) -> Result<(), LemmyError> {
78   let admins = PersonView::admins(pool).await?;
79   let top_admin = admins
80     .first()
81     .ok_or_else(|| LemmyError::from_message("no admins"))?;
82
83   if top_admin.person.id != person_id {
84     return Err(LemmyError::from_message("not_top_admin"));
85   }
86   Ok(())
87 }
88
89 pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
90   if !local_user_view.person.admin {
91     return Err(LemmyError::from_message("not_an_admin"));
92   }
93   Ok(())
94 }
95
96 pub fn is_top_mod(
97   local_user_view: &LocalUserView,
98   community_mods: &[CommunityModeratorView],
99 ) -> Result<(), LemmyError> {
100   if local_user_view.person.id
101     != community_mods
102       .first()
103       .map(|cm| cm.moderator.id)
104       .unwrap_or(PersonId(0))
105   {
106     return Err(LemmyError::from_message("not_top_mod"));
107   }
108   Ok(())
109 }
110
111 #[tracing::instrument(skip_all)]
112 pub async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
113   Post::read(pool, post_id)
114     .await
115     .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_post"))
116 }
117
118 #[tracing::instrument(skip_all)]
119 pub async fn mark_post_as_read(
120   person_id: PersonId,
121   post_id: PostId,
122   pool: &DbPool,
123 ) -> Result<PostRead, LemmyError> {
124   let post_read_form = PostReadForm { post_id, person_id };
125
126   PostRead::mark_as_read(pool, &post_read_form)
127     .await
128     .map_err(|e| LemmyError::from_error_message(e, "couldnt_mark_post_as_read"))
129 }
130
131 #[tracing::instrument(skip_all)]
132 pub async fn mark_post_as_unread(
133   person_id: PersonId,
134   post_id: PostId,
135   pool: &DbPool,
136 ) -> Result<usize, LemmyError> {
137   let post_read_form = PostReadForm { post_id, person_id };
138
139   PostRead::mark_as_unread(pool, &post_read_form)
140     .await
141     .map_err(|e| LemmyError::from_error_message(e, "couldnt_mark_post_as_read"))
142 }
143
144 // TODO: this should simply take LemmyContext as param
145 #[tracing::instrument(skip_all)]
146 pub async fn get_local_user_view_from_jwt(
147   jwt: &str,
148   pool: &DbPool,
149   secret: &Secret,
150 ) -> Result<LocalUserView, LemmyError> {
151   let claims = Claims::decode(jwt, &secret.jwt_secret)
152     .map_err(|e| e.with_message("not_logged_in"))?
153     .claims;
154   let local_user_id = LocalUserId(claims.sub);
155   let local_user_view = LocalUserView::read(pool, local_user_id).await?;
156   check_user_valid(
157     local_user_view.person.banned,
158     local_user_view.person.ban_expires,
159     local_user_view.person.deleted,
160   )?;
161
162   check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
163
164   Ok(local_user_view)
165 }
166
167 /// Checks if user's token was issued before user's password reset.
168 pub fn check_validator_time(
169   validator_time: &NaiveDateTime,
170   claims: &Claims,
171 ) -> Result<(), LemmyError> {
172   let user_validation_time = validator_time.timestamp();
173   if user_validation_time > claims.iat {
174     Err(LemmyError::from_message("not_logged_in"))
175   } else {
176     Ok(())
177   }
178 }
179
180 #[tracing::instrument(skip_all)]
181 pub async fn get_local_user_view_from_jwt_opt(
182   jwt: Option<&Sensitive<String>>,
183   pool: &DbPool,
184   secret: &Secret,
185 ) -> Result<Option<LocalUserView>, LemmyError> {
186   match jwt {
187     Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool, secret).await?)),
188     None => Ok(None),
189   }
190 }
191
192 #[tracing::instrument(skip_all)]
193 pub async fn get_local_user_settings_view_from_jwt_opt(
194   jwt: Option<&Sensitive<String>>,
195   pool: &DbPool,
196   secret: &Secret,
197 ) -> Result<Option<LocalUserView>, LemmyError> {
198   match jwt {
199     Some(jwt) => {
200       let claims = Claims::decode(jwt.as_ref(), &secret.jwt_secret)
201         .map_err(|e| e.with_message("not_logged_in"))?
202         .claims;
203       let local_user_id = LocalUserId(claims.sub);
204       let local_user_view = LocalUserView::read(pool, local_user_id).await?;
205       check_user_valid(
206         local_user_view.person.banned,
207         local_user_view.person.ban_expires,
208         local_user_view.person.deleted,
209       )?;
210
211       check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
212
213       Ok(Some(local_user_view))
214     }
215     None => Ok(None),
216   }
217 }
218 pub fn check_user_valid(
219   banned: bool,
220   ban_expires: Option<NaiveDateTime>,
221   deleted: bool,
222 ) -> Result<(), LemmyError> {
223   // Check for a site ban
224   if is_banned(banned, ban_expires) {
225     return Err(LemmyError::from_message("site_ban"));
226   }
227
228   // check for account deletion
229   if deleted {
230     return Err(LemmyError::from_message("deleted"));
231   }
232
233   Ok(())
234 }
235
236 #[tracing::instrument(skip_all)]
237 pub async fn check_community_ban(
238   person_id: PersonId,
239   community_id: CommunityId,
240   pool: &DbPool,
241 ) -> Result<(), LemmyError> {
242   let is_banned = CommunityPersonBanView::get(pool, person_id, community_id)
243     .await
244     .is_ok();
245   if is_banned {
246     Err(LemmyError::from_message("community_ban"))
247   } else {
248     Ok(())
249   }
250 }
251
252 #[tracing::instrument(skip_all)]
253 pub async fn check_community_deleted_or_removed(
254   community_id: CommunityId,
255   pool: &DbPool,
256 ) -> Result<(), LemmyError> {
257   let community = Community::read(pool, community_id)
258     .await
259     .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?;
260   if community.deleted || community.removed {
261     Err(LemmyError::from_message("deleted"))
262   } else {
263     Ok(())
264   }
265 }
266
267 pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> {
268   if post.deleted || post.removed {
269     Err(LemmyError::from_message("deleted"))
270   } else {
271     Ok(())
272   }
273 }
274
275 #[tracing::instrument(skip_all)]
276 pub async fn check_person_block(
277   my_id: PersonId,
278   potential_blocker_id: PersonId,
279   pool: &DbPool,
280 ) -> Result<(), LemmyError> {
281   let is_blocked = PersonBlock::read(pool, potential_blocker_id, my_id)
282     .await
283     .is_ok();
284   if is_blocked {
285     Err(LemmyError::from_message("person_block"))
286   } else {
287     Ok(())
288   }
289 }
290
291 #[tracing::instrument(skip_all)]
292 pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> Result<(), LemmyError> {
293   if score == -1 && !local_site.enable_downvotes {
294     return Err(LemmyError::from_message("downvotes_disabled"));
295   }
296   Ok(())
297 }
298
299 #[tracing::instrument(skip_all)]
300 pub fn check_private_instance(
301   local_user_view: &Option<LocalUserView>,
302   local_site: &LocalSite,
303 ) -> Result<(), LemmyError> {
304   if local_user_view.is_none() && local_site.private_instance {
305     return Err(LemmyError::from_message("instance_is_private"));
306   }
307   Ok(())
308 }
309
310 #[tracing::instrument(skip_all)]
311 pub async fn build_federated_instances(
312   local_site: &LocalSite,
313   pool: &DbPool,
314 ) -> Result<Option<FederatedInstances>, LemmyError> {
315   if local_site.federation_enabled {
316     // TODO I hate that this requires 3 queries
317     let linked = Instance::linked(pool).await?;
318     let allowed = Instance::allowlist(pool).await?;
319     let blocked = Instance::blocklist(pool).await?;
320
321     // These can return empty vectors, so convert them to options
322     let allowed = (!allowed.is_empty()).then_some(allowed);
323     let blocked = (!blocked.is_empty()).then_some(blocked);
324
325     Ok(Some(FederatedInstances {
326       linked,
327       allowed,
328       blocked,
329     }))
330   } else {
331     Ok(None)
332   }
333 }
334
335 /// Checks the password length
336 pub fn password_length_check(pass: &str) -> Result<(), LemmyError> {
337   if !(10..=60).contains(&pass.chars().count()) {
338     Err(LemmyError::from_message("invalid_password"))
339   } else {
340     Ok(())
341   }
342 }
343
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"))
348   } else {
349     Ok(())
350   }
351 }
352
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() && honeypot != &Some(String::new()) {
356     Err(LemmyError::from_message("honeypot_fail"))
357   } else {
358     Ok(())
359   }
360 }
361
362 pub fn send_email_to_user(
363   local_user_view: &LocalUserView,
364   subject: &str,
365   body: &str,
366   settings: &Settings,
367 ) {
368   if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
369     return;
370   }
371
372   if let Some(user_email) = &local_user_view.local_user.email {
373     match send_email(
374       subject,
375       user_email,
376       &local_user_view.person.name,
377       body,
378       settings,
379     ) {
380       Ok(_o) => _o,
381       Err(e) => warn!("{}", e),
382     };
383   }
384 }
385
386 pub async fn send_password_reset_email(
387   user: &LocalUserView,
388   pool: &DbPool,
389   settings: &Settings,
390 ) -> Result<(), LemmyError> {
391   // Generate a random token
392   let token = uuid::Uuid::new_v4().to_string();
393
394   // Insert the row
395   let token2 = token.clone();
396   let local_user_id = user.local_user.id;
397   PasswordResetRequest::create_token(pool, local_user_id, &token2).await?;
398
399   let email = &user.local_user.email.clone().expect("email");
400   let lang = get_interface_language(user);
401   let subject = &lang.password_reset_subject(&user.person.name);
402   let protocol_and_hostname = settings.get_protocol_and_hostname();
403   let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token);
404   let body = &lang.password_reset_body(reset_link, &user.person.name);
405   send_email(subject, email, &user.person.name, body, settings)
406 }
407
408 /// Send a verification email
409 pub async fn send_verification_email(
410   user: &LocalUserView,
411   new_email: &str,
412   pool: &DbPool,
413   settings: &Settings,
414 ) -> Result<(), LemmyError> {
415   let form = EmailVerificationForm {
416     local_user_id: user.local_user.id,
417     email: new_email.to_string(),
418     verification_token: uuid::Uuid::new_v4().to_string(),
419   };
420   let verify_link = format!(
421     "{}/verify_email/{}",
422     settings.get_protocol_and_hostname(),
423     &form.verification_token
424   );
425   EmailVerification::create(pool, &form).await?;
426
427   let lang = get_interface_language(user);
428   let subject = lang.verify_email_subject(&settings.hostname);
429   let body = lang.verify_email_body(&settings.hostname, &user.person.name, verify_link);
430   send_email(&subject, new_email, &user.person.name, &body, settings)?;
431
432   Ok(())
433 }
434
435 pub fn send_email_verification_success(
436   user: &LocalUserView,
437   settings: &Settings,
438 ) -> Result<(), LemmyError> {
439   let email = &user.local_user.email.clone().expect("email");
440   let lang = get_interface_language(user);
441   let subject = &lang.email_verified_subject(&user.person.actor_id);
442   let body = &lang.email_verified_body();
443   send_email(subject, email, &user.person.name, body, settings)
444 }
445
446 pub fn get_interface_language(user: &LocalUserView) -> Lang {
447   lang_str_to_lang(&user.local_user.interface_language)
448 }
449
450 pub fn get_interface_language_from_settings(user: &LocalUserView) -> Lang {
451   lang_str_to_lang(&user.local_user.interface_language)
452 }
453
454 fn lang_str_to_lang(lang: &str) -> Lang {
455   let lang_id = LanguageId::new(lang);
456   Lang::from_language_id(&lang_id).unwrap_or_else(|| {
457     let en = LanguageId::new("en");
458     Lang::from_language_id(&en).expect("default language")
459   })
460 }
461
462 pub fn local_site_rate_limit_to_rate_limit_config(
463   local_site_rate_limit: &LocalSiteRateLimit,
464 ) -> RateLimitConfig {
465   let l = local_site_rate_limit;
466   RateLimitConfig {
467     message: l.message,
468     message_per_second: l.message_per_second,
469     post: l.post,
470     post_per_second: l.post_per_second,
471     register: l.register,
472     register_per_second: l.register_per_second,
473     image: l.image,
474     image_per_second: l.image_per_second,
475     comment: l.comment,
476     comment_per_second: l.comment_per_second,
477     search: l.search,
478     search_per_second: l.search_per_second,
479   }
480 }
481
482 pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option<Regex> {
483   build_slur_regex(local_site.slur_filter_regex.as_deref())
484 }
485
486 pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Regex> {
487   local_site
488     .as_ref()
489     .map(local_site_to_slur_regex)
490     .unwrap_or(None)
491 }
492
493 pub fn send_application_approved_email(
494   user: &LocalUserView,
495   settings: &Settings,
496 ) -> Result<(), LemmyError> {
497   let email = &user.local_user.email.clone().expect("email");
498   let lang = get_interface_language(user);
499   let subject = lang.registration_approved_subject(&user.person.actor_id);
500   let body = lang.registration_approved_body(&settings.hostname);
501   send_email(&subject, email, &user.person.name, &body, settings)
502 }
503
504 /// Send a new applicant email notification to all admins
505 pub async fn send_new_applicant_email_to_admins(
506   applicant_username: &str,
507   pool: &DbPool,
508   settings: &Settings,
509 ) -> Result<(), LemmyError> {
510   // Collect the admins with emails
511   let admins = LocalUserView::list_admins_with_emails(pool).await?;
512
513   let applications_link = &format!(
514     "{}/registration_applications",
515     settings.get_protocol_and_hostname(),
516   );
517
518   for admin in &admins {
519     let email = &admin.local_user.email.clone().expect("email");
520     let lang = get_interface_language_from_settings(admin);
521     let subject = lang.new_application_subject(&settings.hostname, applicant_username);
522     let body = lang.new_application_body(applications_link);
523     send_email(&subject, email, &admin.person.name, &body, settings)?;
524   }
525   Ok(())
526 }
527
528 /// Send a report to all admins
529 pub async fn send_new_report_email_to_admins(
530   reporter_username: &str,
531   reported_username: &str,
532   pool: &DbPool,
533   settings: &Settings,
534 ) -> Result<(), LemmyError> {
535   // Collect the admins with emails
536   let admins = LocalUserView::list_admins_with_emails(pool).await?;
537
538   let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),);
539
540   for admin in &admins {
541     let email = &admin.local_user.email.clone().expect("email");
542     let lang = get_interface_language_from_settings(admin);
543     let subject = lang.new_report_subject(&settings.hostname, reporter_username, reported_username);
544     let body = lang.new_report_body(reports_link);
545     send_email(&subject, email, &admin.person.name, &body, settings)?;
546   }
547   Ok(())
548 }
549
550 pub async fn check_registration_application(
551   local_user_view: &LocalUserView,
552   local_site: &LocalSite,
553   pool: &DbPool,
554 ) -> Result<(), LemmyError> {
555   if local_site.registration_mode == RegistrationMode::RequireApplication
556     && !local_user_view.local_user.accepted_application
557     && !local_user_view.person.admin
558   {
559     // Fetch the registration, see if its denied
560     let local_user_id = local_user_view.local_user.id;
561     let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?;
562     if let Some(deny_reason) = registration.deny_reason {
563       let lang = get_interface_language(local_user_view);
564       let registration_denied_message = format!("{}: {}", lang.registration_denied(), &deny_reason);
565       return Err(LemmyError::from_message(&registration_denied_message));
566     } else {
567       return Err(LemmyError::from_message("registration_application_pending"));
568     }
569   }
570   Ok(())
571 }
572
573 pub fn check_private_instance_and_federation_enabled(
574   local_site: &LocalSite,
575 ) -> Result<(), LemmyError> {
576   if local_site.private_instance && local_site.federation_enabled {
577     return Err(LemmyError::from_message(
578       "Cannot have both private instance and federation enabled.",
579     ));
580   }
581   Ok(())
582 }
583
584 pub async fn purge_image_posts_for_person(
585   banned_person_id: PersonId,
586   pool: &DbPool,
587   settings: &Settings,
588   client: &ClientWithMiddleware,
589 ) -> Result<(), LemmyError> {
590   let posts = Post::fetch_pictrs_posts_for_creator(pool, banned_person_id).await?;
591   for post in posts {
592     if let Some(url) = post.url {
593       purge_image_from_pictrs(client, settings, &url).await.ok();
594     }
595     if let Some(thumbnail_url) = post.thumbnail_url {
596       purge_image_from_pictrs(client, settings, &thumbnail_url)
597         .await
598         .ok();
599     }
600   }
601
602   Post::remove_pictrs_post_images_and_thumbnails_for_creator(pool, banned_person_id).await?;
603
604   Ok(())
605 }
606
607 pub async fn purge_image_posts_for_community(
608   banned_community_id: CommunityId,
609   pool: &DbPool,
610   settings: &Settings,
611   client: &ClientWithMiddleware,
612 ) -> Result<(), LemmyError> {
613   let posts = Post::fetch_pictrs_posts_for_community(pool, banned_community_id).await?;
614   for post in posts {
615     if let Some(url) = post.url {
616       purge_image_from_pictrs(client, settings, &url).await.ok();
617     }
618     if let Some(thumbnail_url) = post.thumbnail_url {
619       purge_image_from_pictrs(client, settings, &thumbnail_url)
620         .await
621         .ok();
622     }
623   }
624
625   Post::remove_pictrs_post_images_and_thumbnails_for_community(pool, banned_community_id).await?;
626
627   Ok(())
628 }
629
630 pub async fn remove_user_data(
631   banned_person_id: PersonId,
632   pool: &DbPool,
633   settings: &Settings,
634   client: &ClientWithMiddleware,
635 ) -> Result<(), LemmyError> {
636   // Purge user images
637   let person = Person::read(pool, banned_person_id).await?;
638   if let Some(avatar) = person.avatar {
639     purge_image_from_pictrs(client, settings, &avatar)
640       .await
641       .ok();
642   }
643   if let Some(banner) = person.banner {
644     purge_image_from_pictrs(client, settings, &banner)
645       .await
646       .ok();
647   }
648
649   // Update the fields to None
650   Person::update(
651     pool,
652     banned_person_id,
653     &PersonUpdateForm::builder()
654       .avatar(Some(None))
655       .banner(Some(None))
656       .build(),
657   )
658   .await?;
659
660   // Posts
661   Post::update_removed_for_creator(pool, banned_person_id, None, true).await?;
662
663   // Purge image posts
664   purge_image_posts_for_person(banned_person_id, pool, settings, client).await?;
665
666   // Communities
667   // Remove all communities where they're the top mod
668   // for now, remove the communities manually
669   let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?;
670
671   // Filter to only this banned users top communities
672   let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
673     .into_iter()
674     .filter(|fmc| fmc.moderator.id == banned_person_id)
675     .collect();
676
677   for first_mod_community in banned_user_first_communities {
678     let community_id = first_mod_community.community.id;
679     Community::update(
680       pool,
681       community_id,
682       &CommunityUpdateForm::builder().removed(Some(true)).build(),
683     )
684     .await?;
685
686     // Delete the community images
687     if let Some(icon) = first_mod_community.community.icon {
688       purge_image_from_pictrs(client, settings, &icon).await.ok();
689     }
690     if let Some(banner) = first_mod_community.community.banner {
691       purge_image_from_pictrs(client, settings, &banner)
692         .await
693         .ok();
694     }
695     // Update the fields to None
696     Community::update(
697       pool,
698       community_id,
699       &CommunityUpdateForm::builder()
700         .icon(Some(None))
701         .banner(Some(None))
702         .build(),
703     )
704     .await?;
705   }
706
707   // Comments
708   Comment::update_removed_for_creator(pool, banned_person_id, true).await?;
709
710   Ok(())
711 }
712
713 pub async fn remove_user_data_in_community(
714   community_id: CommunityId,
715   banned_person_id: PersonId,
716   pool: &DbPool,
717 ) -> Result<(), LemmyError> {
718   // Posts
719   Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?;
720
721   // Comments
722   // TODO Diesel doesn't allow updates with joins, so this has to be a loop
723   let comments = CommentQuery::builder()
724     .pool(pool)
725     .creator_id(Some(banned_person_id))
726     .community_id(Some(community_id))
727     .limit(Some(i64::MAX))
728     .build()
729     .list()
730     .await?;
731
732   for comment_view in &comments {
733     let comment_id = comment_view.comment.id;
734     Comment::update(
735       pool,
736       comment_id,
737       &CommentUpdateForm::builder().removed(Some(true)).build(),
738     )
739     .await?;
740   }
741
742   Ok(())
743 }
744
745 pub async fn delete_user_account(
746   person_id: PersonId,
747   pool: &DbPool,
748   settings: &Settings,
749   client: &ClientWithMiddleware,
750 ) -> Result<(), LemmyError> {
751   // Delete their images
752   let person = Person::read(pool, person_id).await?;
753   if let Some(avatar) = person.avatar {
754     purge_image_from_pictrs(client, settings, &avatar)
755       .await
756       .ok();
757   }
758   if let Some(banner) = person.banner {
759     purge_image_from_pictrs(client, settings, &banner)
760       .await
761       .ok();
762   }
763   // No need to update avatar and banner, those are handled in Person::delete_account
764
765   // Comments
766   Comment::permadelete_for_creator(pool, person_id)
767     .await
768     .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
769
770   // Posts
771   Post::permadelete_for_creator(pool, person_id)
772     .await
773     .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_post"))?;
774
775   // Purge image posts
776   purge_image_posts_for_person(person_id, pool, settings, client).await?;
777
778   // Leave communities they mod
779   CommunityModerator::leave_all_communities(pool, person_id).await?;
780
781   Person::delete_account(pool, person_id).await?;
782
783   Ok(())
784 }
785
786 #[cfg(test)]
787 mod tests {
788   use crate::utils::{honeypot_check, password_length_check};
789
790   #[test]
791   #[rustfmt::skip]
792   fn password_length() {
793     assert!(password_length_check("Õ¼¾°3yË,o¸ãtÌÈú|ÇÁÙAøüÒI©·¤(T]/ð>æºWæ[C¤bªWöaÃÎñ·{=û³&§½K/c").is_ok());
794     assert!(password_length_check("1234567890").is_ok());
795     assert!(password_length_check("short").is_err());
796     assert!(password_length_check("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooong").is_err());
797   }
798
799   #[test]
800   fn honeypot() {
801     assert!(honeypot_check(&None).is_ok());
802     assert!(honeypot_check(&Some(String::new())).is_ok());
803     assert!(honeypot_check(&Some("1".to_string())).is_err());
804     assert!(honeypot_check(&Some("message".to_string())).is_err());
805   }
806 }
807
808 pub enum EndpointType {
809   Community,
810   Person,
811   Post,
812   Comment,
813   PrivateMessage,
814 }
815
816 /// Generates an apub endpoint for a given domain, IE xyz.tld
817 pub fn generate_local_apub_endpoint(
818   endpoint_type: EndpointType,
819   name: &str,
820   domain: &str,
821 ) -> Result<DbUrl, ParseError> {
822   let point = match endpoint_type {
823     EndpointType::Community => "c",
824     EndpointType::Person => "u",
825     EndpointType::Post => "post",
826     EndpointType::Comment => "comment",
827     EndpointType::PrivateMessage => "private_message",
828   };
829
830   Ok(Url::parse(&format!("{domain}/{point}/{name}"))?.into())
831 }
832
833 pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
834   Ok(Url::parse(&format!("{actor_id}/followers"))?.into())
835 }
836
837 pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
838   Ok(Url::parse(&format!("{actor_id}/inbox"))?.into())
839 }
840
841 pub fn generate_site_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
842   let mut actor_id: Url = actor_id.clone().into();
843   actor_id.set_path("site_inbox");
844   Ok(actor_id.into())
845 }
846
847 pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
848   let actor_id: Url = actor_id.clone().into();
849   let url = format!(
850     "{}://{}{}/inbox",
851     &actor_id.scheme(),
852     &actor_id.host_str().context(location_info!())?,
853     if let Some(port) = actor_id.port() {
854       format!(":{port}")
855     } else {
856       String::new()
857     },
858   );
859   Ok(Url::parse(&url)?.into())
860 }
861
862 pub fn generate_outbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
863   Ok(Url::parse(&format!("{actor_id}/outbox"))?.into())
864 }
865
866 pub fn generate_featured_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
867   Ok(Url::parse(&format!("{actor_id}/featured"))?.into())
868 }
869
870 pub fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
871   Ok(Url::parse(&format!("{community_id}/moderators"))?.into())
872 }