]> Untitled Git - lemmy.git/blob - crates/api_common/src/utils.rs
Remove last Option<Vec.. from API. Fixes #2820 (#2822)
[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     Ok(Some(FederatedInstances {
322       linked,
323       allowed,
324       blocked,
325     }))
326   } else {
327     Ok(None)
328   }
329 }
330
331 /// Checks the password length
332 pub fn password_length_check(pass: &str) -> Result<(), LemmyError> {
333   if !(10..=60).contains(&pass.chars().count()) {
334     Err(LemmyError::from_message("invalid_password"))
335   } else {
336     Ok(())
337   }
338 }
339
340 /// Checks the site description length
341 pub fn site_description_length_check(description: &str) -> Result<(), LemmyError> {
342   if description.len() > 150 {
343     Err(LemmyError::from_message("site_description_length_overflow"))
344   } else {
345     Ok(())
346   }
347 }
348
349 /// Checks for a honeypot. If this field is filled, fail the rest of the function
350 pub fn honeypot_check(honeypot: &Option<String>) -> Result<(), LemmyError> {
351   if honeypot.is_some() && honeypot != &Some(String::new()) {
352     Err(LemmyError::from_message("honeypot_fail"))
353   } else {
354     Ok(())
355   }
356 }
357
358 pub fn send_email_to_user(
359   local_user_view: &LocalUserView,
360   subject: &str,
361   body: &str,
362   settings: &Settings,
363 ) {
364   if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
365     return;
366   }
367
368   if let Some(user_email) = &local_user_view.local_user.email {
369     match send_email(
370       subject,
371       user_email,
372       &local_user_view.person.name,
373       body,
374       settings,
375     ) {
376       Ok(_o) => _o,
377       Err(e) => warn!("{}", e),
378     };
379   }
380 }
381
382 pub async fn send_password_reset_email(
383   user: &LocalUserView,
384   pool: &DbPool,
385   settings: &Settings,
386 ) -> Result<(), LemmyError> {
387   // Generate a random token
388   let token = uuid::Uuid::new_v4().to_string();
389
390   // Insert the row
391   let token2 = token.clone();
392   let local_user_id = user.local_user.id;
393   PasswordResetRequest::create_token(pool, local_user_id, &token2).await?;
394
395   let email = &user.local_user.email.clone().expect("email");
396   let lang = get_interface_language(user);
397   let subject = &lang.password_reset_subject(&user.person.name);
398   let protocol_and_hostname = settings.get_protocol_and_hostname();
399   let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token);
400   let body = &lang.password_reset_body(reset_link, &user.person.name);
401   send_email(subject, email, &user.person.name, body, settings)
402 }
403
404 /// Send a verification email
405 pub async fn send_verification_email(
406   user: &LocalUserView,
407   new_email: &str,
408   pool: &DbPool,
409   settings: &Settings,
410 ) -> Result<(), LemmyError> {
411   let form = EmailVerificationForm {
412     local_user_id: user.local_user.id,
413     email: new_email.to_string(),
414     verification_token: uuid::Uuid::new_v4().to_string(),
415   };
416   let verify_link = format!(
417     "{}/verify_email/{}",
418     settings.get_protocol_and_hostname(),
419     &form.verification_token
420   );
421   EmailVerification::create(pool, &form).await?;
422
423   let lang = get_interface_language(user);
424   let subject = lang.verify_email_subject(&settings.hostname);
425   let body = lang.verify_email_body(&settings.hostname, &user.person.name, verify_link);
426   send_email(&subject, new_email, &user.person.name, &body, settings)?;
427
428   Ok(())
429 }
430
431 pub fn send_email_verification_success(
432   user: &LocalUserView,
433   settings: &Settings,
434 ) -> Result<(), LemmyError> {
435   let email = &user.local_user.email.clone().expect("email");
436   let lang = get_interface_language(user);
437   let subject = &lang.email_verified_subject(&user.person.actor_id);
438   let body = &lang.email_verified_body();
439   send_email(subject, email, &user.person.name, body, settings)
440 }
441
442 pub fn get_interface_language(user: &LocalUserView) -> Lang {
443   lang_str_to_lang(&user.local_user.interface_language)
444 }
445
446 pub fn get_interface_language_from_settings(user: &LocalUserView) -> Lang {
447   lang_str_to_lang(&user.local_user.interface_language)
448 }
449
450 fn lang_str_to_lang(lang: &str) -> Lang {
451   let lang_id = LanguageId::new(lang);
452   Lang::from_language_id(&lang_id).unwrap_or_else(|| {
453     let en = LanguageId::new("en");
454     Lang::from_language_id(&en).expect("default language")
455   })
456 }
457
458 pub fn local_site_rate_limit_to_rate_limit_config(
459   local_site_rate_limit: &LocalSiteRateLimit,
460 ) -> RateLimitConfig {
461   let l = local_site_rate_limit;
462   RateLimitConfig {
463     message: l.message,
464     message_per_second: l.message_per_second,
465     post: l.post,
466     post_per_second: l.post_per_second,
467     register: l.register,
468     register_per_second: l.register_per_second,
469     image: l.image,
470     image_per_second: l.image_per_second,
471     comment: l.comment,
472     comment_per_second: l.comment_per_second,
473     search: l.search,
474     search_per_second: l.search_per_second,
475   }
476 }
477
478 pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option<Regex> {
479   build_slur_regex(local_site.slur_filter_regex.as_deref())
480 }
481
482 pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Regex> {
483   local_site
484     .as_ref()
485     .map(local_site_to_slur_regex)
486     .unwrap_or(None)
487 }
488
489 pub fn send_application_approved_email(
490   user: &LocalUserView,
491   settings: &Settings,
492 ) -> Result<(), LemmyError> {
493   let email = &user.local_user.email.clone().expect("email");
494   let lang = get_interface_language(user);
495   let subject = lang.registration_approved_subject(&user.person.actor_id);
496   let body = lang.registration_approved_body(&settings.hostname);
497   send_email(&subject, email, &user.person.name, &body, settings)
498 }
499
500 /// Send a new applicant email notification to all admins
501 pub async fn send_new_applicant_email_to_admins(
502   applicant_username: &str,
503   pool: &DbPool,
504   settings: &Settings,
505 ) -> Result<(), LemmyError> {
506   // Collect the admins with emails
507   let admins = LocalUserView::list_admins_with_emails(pool).await?;
508
509   let applications_link = &format!(
510     "{}/registration_applications",
511     settings.get_protocol_and_hostname(),
512   );
513
514   for admin in &admins {
515     let email = &admin.local_user.email.clone().expect("email");
516     let lang = get_interface_language_from_settings(admin);
517     let subject = lang.new_application_subject(&settings.hostname, applicant_username);
518     let body = lang.new_application_body(applications_link);
519     send_email(&subject, email, &admin.person.name, &body, settings)?;
520   }
521   Ok(())
522 }
523
524 /// Send a report to all admins
525 pub async fn send_new_report_email_to_admins(
526   reporter_username: &str,
527   reported_username: &str,
528   pool: &DbPool,
529   settings: &Settings,
530 ) -> Result<(), LemmyError> {
531   // Collect the admins with emails
532   let admins = LocalUserView::list_admins_with_emails(pool).await?;
533
534   let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),);
535
536   for admin in &admins {
537     let email = &admin.local_user.email.clone().expect("email");
538     let lang = get_interface_language_from_settings(admin);
539     let subject = lang.new_report_subject(&settings.hostname, reporter_username, reported_username);
540     let body = lang.new_report_body(reports_link);
541     send_email(&subject, email, &admin.person.name, &body, settings)?;
542   }
543   Ok(())
544 }
545
546 pub async fn check_registration_application(
547   local_user_view: &LocalUserView,
548   local_site: &LocalSite,
549   pool: &DbPool,
550 ) -> Result<(), LemmyError> {
551   if local_site.registration_mode == RegistrationMode::RequireApplication
552     && !local_user_view.local_user.accepted_application
553     && !local_user_view.person.admin
554   {
555     // Fetch the registration, see if its denied
556     let local_user_id = local_user_view.local_user.id;
557     let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?;
558     if let Some(deny_reason) = registration.deny_reason {
559       let lang = get_interface_language(local_user_view);
560       let registration_denied_message = format!("{}: {}", lang.registration_denied(), &deny_reason);
561       return Err(LemmyError::from_message(&registration_denied_message));
562     } else {
563       return Err(LemmyError::from_message("registration_application_pending"));
564     }
565   }
566   Ok(())
567 }
568
569 pub fn check_private_instance_and_federation_enabled(
570   local_site: &LocalSite,
571 ) -> Result<(), LemmyError> {
572   if local_site.private_instance && local_site.federation_enabled {
573     return Err(LemmyError::from_message(
574       "Cannot have both private instance and federation enabled.",
575     ));
576   }
577   Ok(())
578 }
579
580 pub async fn purge_image_posts_for_person(
581   banned_person_id: PersonId,
582   pool: &DbPool,
583   settings: &Settings,
584   client: &ClientWithMiddleware,
585 ) -> Result<(), LemmyError> {
586   let posts = Post::fetch_pictrs_posts_for_creator(pool, banned_person_id).await?;
587   for post in posts {
588     if let Some(url) = post.url {
589       purge_image_from_pictrs(client, settings, &url).await.ok();
590     }
591     if let Some(thumbnail_url) = post.thumbnail_url {
592       purge_image_from_pictrs(client, settings, &thumbnail_url)
593         .await
594         .ok();
595     }
596   }
597
598   Post::remove_pictrs_post_images_and_thumbnails_for_creator(pool, banned_person_id).await?;
599
600   Ok(())
601 }
602
603 pub async fn purge_image_posts_for_community(
604   banned_community_id: CommunityId,
605   pool: &DbPool,
606   settings: &Settings,
607   client: &ClientWithMiddleware,
608 ) -> Result<(), LemmyError> {
609   let posts = Post::fetch_pictrs_posts_for_community(pool, banned_community_id).await?;
610   for post in posts {
611     if let Some(url) = post.url {
612       purge_image_from_pictrs(client, settings, &url).await.ok();
613     }
614     if let Some(thumbnail_url) = post.thumbnail_url {
615       purge_image_from_pictrs(client, settings, &thumbnail_url)
616         .await
617         .ok();
618     }
619   }
620
621   Post::remove_pictrs_post_images_and_thumbnails_for_community(pool, banned_community_id).await?;
622
623   Ok(())
624 }
625
626 pub async fn remove_user_data(
627   banned_person_id: PersonId,
628   pool: &DbPool,
629   settings: &Settings,
630   client: &ClientWithMiddleware,
631 ) -> Result<(), LemmyError> {
632   // Purge user images
633   let person = Person::read(pool, banned_person_id).await?;
634   if let Some(avatar) = person.avatar {
635     purge_image_from_pictrs(client, settings, &avatar)
636       .await
637       .ok();
638   }
639   if let Some(banner) = person.banner {
640     purge_image_from_pictrs(client, settings, &banner)
641       .await
642       .ok();
643   }
644
645   // Update the fields to None
646   Person::update(
647     pool,
648     banned_person_id,
649     &PersonUpdateForm::builder()
650       .avatar(Some(None))
651       .banner(Some(None))
652       .build(),
653   )
654   .await?;
655
656   // Posts
657   Post::update_removed_for_creator(pool, banned_person_id, None, true).await?;
658
659   // Purge image posts
660   purge_image_posts_for_person(banned_person_id, pool, settings, client).await?;
661
662   // Communities
663   // Remove all communities where they're the top mod
664   // for now, remove the communities manually
665   let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?;
666
667   // Filter to only this banned users top communities
668   let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
669     .into_iter()
670     .filter(|fmc| fmc.moderator.id == banned_person_id)
671     .collect();
672
673   for first_mod_community in banned_user_first_communities {
674     let community_id = first_mod_community.community.id;
675     Community::update(
676       pool,
677       community_id,
678       &CommunityUpdateForm::builder().removed(Some(true)).build(),
679     )
680     .await?;
681
682     // Delete the community images
683     if let Some(icon) = first_mod_community.community.icon {
684       purge_image_from_pictrs(client, settings, &icon).await.ok();
685     }
686     if let Some(banner) = first_mod_community.community.banner {
687       purge_image_from_pictrs(client, settings, &banner)
688         .await
689         .ok();
690     }
691     // Update the fields to None
692     Community::update(
693       pool,
694       community_id,
695       &CommunityUpdateForm::builder()
696         .icon(Some(None))
697         .banner(Some(None))
698         .build(),
699     )
700     .await?;
701   }
702
703   // Comments
704   Comment::update_removed_for_creator(pool, banned_person_id, true).await?;
705
706   Ok(())
707 }
708
709 pub async fn remove_user_data_in_community(
710   community_id: CommunityId,
711   banned_person_id: PersonId,
712   pool: &DbPool,
713 ) -> Result<(), LemmyError> {
714   // Posts
715   Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?;
716
717   // Comments
718   // TODO Diesel doesn't allow updates with joins, so this has to be a loop
719   let comments = CommentQuery::builder()
720     .pool(pool)
721     .creator_id(Some(banned_person_id))
722     .community_id(Some(community_id))
723     .limit(Some(i64::MAX))
724     .build()
725     .list()
726     .await?;
727
728   for comment_view in &comments {
729     let comment_id = comment_view.comment.id;
730     Comment::update(
731       pool,
732       comment_id,
733       &CommentUpdateForm::builder().removed(Some(true)).build(),
734     )
735     .await?;
736   }
737
738   Ok(())
739 }
740
741 pub async fn delete_user_account(
742   person_id: PersonId,
743   pool: &DbPool,
744   settings: &Settings,
745   client: &ClientWithMiddleware,
746 ) -> Result<(), LemmyError> {
747   // Delete their images
748   let person = Person::read(pool, person_id).await?;
749   if let Some(avatar) = person.avatar {
750     purge_image_from_pictrs(client, settings, &avatar)
751       .await
752       .ok();
753   }
754   if let Some(banner) = person.banner {
755     purge_image_from_pictrs(client, settings, &banner)
756       .await
757       .ok();
758   }
759   // No need to update avatar and banner, those are handled in Person::delete_account
760
761   // Comments
762   Comment::permadelete_for_creator(pool, person_id)
763     .await
764     .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
765
766   // Posts
767   Post::permadelete_for_creator(pool, person_id)
768     .await
769     .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_post"))?;
770
771   // Purge image posts
772   purge_image_posts_for_person(person_id, pool, settings, client).await?;
773
774   // Leave communities they mod
775   CommunityModerator::leave_all_communities(pool, person_id).await?;
776
777   Person::delete_account(pool, person_id).await?;
778
779   Ok(())
780 }
781
782 #[cfg(test)]
783 mod tests {
784   use crate::utils::{honeypot_check, password_length_check};
785
786   #[test]
787   #[rustfmt::skip]
788   fn password_length() {
789     assert!(password_length_check("Õ¼¾°3yË,o¸ãtÌÈú|ÇÁÙAøüÒI©·¤(T]/ð>æºWæ[C¤bªWöaÃÎñ·{=û³&§½K/c").is_ok());
790     assert!(password_length_check("1234567890").is_ok());
791     assert!(password_length_check("short").is_err());
792     assert!(password_length_check("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooong").is_err());
793   }
794
795   #[test]
796   fn honeypot() {
797     assert!(honeypot_check(&None).is_ok());
798     assert!(honeypot_check(&Some(String::new())).is_ok());
799     assert!(honeypot_check(&Some("1".to_string())).is_err());
800     assert!(honeypot_check(&Some("message".to_string())).is_err());
801   }
802 }
803
804 pub enum EndpointType {
805   Community,
806   Person,
807   Post,
808   Comment,
809   PrivateMessage,
810 }
811
812 /// Generates an apub endpoint for a given domain, IE xyz.tld
813 pub fn generate_local_apub_endpoint(
814   endpoint_type: EndpointType,
815   name: &str,
816   domain: &str,
817 ) -> Result<DbUrl, ParseError> {
818   let point = match endpoint_type {
819     EndpointType::Community => "c",
820     EndpointType::Person => "u",
821     EndpointType::Post => "post",
822     EndpointType::Comment => "comment",
823     EndpointType::PrivateMessage => "private_message",
824   };
825
826   Ok(Url::parse(&format!("{domain}/{point}/{name}"))?.into())
827 }
828
829 pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
830   Ok(Url::parse(&format!("{actor_id}/followers"))?.into())
831 }
832
833 pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
834   Ok(Url::parse(&format!("{actor_id}/inbox"))?.into())
835 }
836
837 pub fn generate_site_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
838   let mut actor_id: Url = actor_id.clone().into();
839   actor_id.set_path("site_inbox");
840   Ok(actor_id.into())
841 }
842
843 pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
844   let actor_id: Url = actor_id.clone().into();
845   let url = format!(
846     "{}://{}{}/inbox",
847     &actor_id.scheme(),
848     &actor_id.host_str().context(location_info!())?,
849     if let Some(port) = actor_id.port() {
850       format!(":{port}")
851     } else {
852       String::new()
853     },
854   );
855   Ok(Url::parse(&url)?.into())
856 }
857
858 pub fn generate_outbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
859   Ok(Url::parse(&format!("{actor_id}/outbox"))?.into())
860 }
861
862 pub fn generate_featured_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
863   Ok(Url::parse(&format!("{actor_id}/featured"))?.into())
864 }
865
866 pub fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
867   Ok(Url::parse(&format!("{community_id}/moderators"))?.into())
868 }