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