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