]> Untitled Git - lemmy.git/blob - crates/api_crud/src/site/create.rs
Fixing broken SQL migration formatting. (#3800)
[lemmy.git] / crates / api_crud / src / site / create.rs
1 use crate::site::{application_question_check, site_default_post_listing_type_check};
2 use activitypub_federation::http_signatures::generate_actor_keypair;
3 use actix_web::web::{Data, Json};
4 use lemmy_api_common::{
5   context::LemmyContext,
6   site::{CreateSite, SiteResponse},
7   utils::{
8     generate_site_inbox_url,
9     is_admin,
10     local_site_rate_limit_to_rate_limit_config,
11     local_user_view_from_jwt,
12     sanitize_html,
13     sanitize_html_opt,
14   },
15 };
16 use lemmy_db_schema::{
17   newtypes::DbUrl,
18   source::{
19     local_site::{LocalSite, LocalSiteUpdateForm},
20     local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
21     site::{Site, SiteUpdateForm},
22     tagline::Tagline,
23   },
24   traits::Crud,
25   utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now},
26 };
27 use lemmy_db_views::structs::SiteView;
28 use lemmy_utils::{
29   error::{LemmyError, LemmyErrorType, LemmyResult},
30   utils::{
31     slurs::{check_slurs, check_slurs_opt},
32     validation::{
33       build_and_check_regex,
34       check_site_visibility_valid,
35       is_valid_body_field,
36       site_description_length_check,
37       site_name_length_check,
38     },
39   },
40 };
41 use url::Url;
42
43 #[tracing::instrument(skip(context))]
44 pub async fn create_site(
45   data: Json<CreateSite>,
46   context: Data<LemmyContext>,
47 ) -> Result<Json<SiteResponse>, LemmyError> {
48   let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
49   let local_site = LocalSite::read(&mut context.pool()).await?;
50
51   // Make sure user is an admin; other types of users should not create site data...
52   is_admin(&local_user_view)?;
53
54   validate_create_payload(&local_site, &data)?;
55
56   let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
57   let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
58   let keypair = generate_actor_keypair()?;
59   let name = sanitize_html(&data.name);
60   let sidebar = sanitize_html_opt(&data.sidebar);
61   let description = sanitize_html_opt(&data.description);
62
63   let site_form = SiteUpdateForm::builder()
64     .name(Some(name))
65     .sidebar(diesel_option_overwrite(sidebar))
66     .description(diesel_option_overwrite(description))
67     .icon(diesel_option_overwrite_to_url(&data.icon)?)
68     .banner(diesel_option_overwrite_to_url(&data.banner)?)
69     .actor_id(Some(actor_id))
70     .last_refreshed_at(Some(naive_now()))
71     .inbox_url(inbox_url)
72     .private_key(Some(Some(keypair.private_key)))
73     .public_key(Some(keypair.public_key))
74     .build();
75
76   let site_id = local_site.site_id;
77
78   Site::update(&mut context.pool(), site_id, &site_form).await?;
79
80   let application_question = sanitize_html_opt(&data.application_question);
81   let default_theme = sanitize_html_opt(&data.default_theme);
82   let legal_information = sanitize_html_opt(&data.legal_information);
83
84   let local_site_form = LocalSiteUpdateForm::builder()
85     // Set the site setup to true
86     .site_setup(Some(true))
87     .enable_downvotes(data.enable_downvotes)
88     .registration_mode(data.registration_mode)
89     .enable_nsfw(data.enable_nsfw)
90     .community_creation_admin_only(data.community_creation_admin_only)
91     .require_email_verification(data.require_email_verification)
92     .application_question(diesel_option_overwrite(application_question))
93     .private_instance(data.private_instance)
94     .default_theme(default_theme)
95     .default_post_listing_type(data.default_post_listing_type)
96     .legal_information(diesel_option_overwrite(legal_information))
97     .application_email_admins(data.application_email_admins)
98     .hide_modlog_mod_names(data.hide_modlog_mod_names)
99     .updated(Some(Some(naive_now())))
100     .slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
101     .actor_name_max_length(data.actor_name_max_length)
102     .federation_enabled(data.federation_enabled)
103     .captcha_enabled(data.captcha_enabled)
104     .captcha_difficulty(data.captcha_difficulty.clone())
105     .build();
106
107   LocalSite::update(&mut context.pool(), &local_site_form).await?;
108
109   let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
110     .message(data.rate_limit_message)
111     .message_per_second(data.rate_limit_message_per_second)
112     .post(data.rate_limit_post)
113     .post_per_second(data.rate_limit_post_per_second)
114     .register(data.rate_limit_register)
115     .register_per_second(data.rate_limit_register_per_second)
116     .image(data.rate_limit_image)
117     .image_per_second(data.rate_limit_image_per_second)
118     .comment(data.rate_limit_comment)
119     .comment_per_second(data.rate_limit_comment_per_second)
120     .search(data.rate_limit_search)
121     .search_per_second(data.rate_limit_search_per_second)
122     .build();
123
124   LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
125
126   let site_view = SiteView::read_local(&mut context.pool()).await?;
127
128   let new_taglines = data.taglines.clone();
129   let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
130
131   let rate_limit_config =
132     local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
133   context
134     .settings_updated_channel()
135     .send(rate_limit_config)
136     .await?;
137
138   Ok(Json(SiteResponse {
139     site_view,
140     taglines,
141   }))
142 }
143
144 fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> {
145   // Make sure the site hasn't already been set up...
146   if local_site.site_setup {
147     Err(LemmyErrorType::SiteAlreadyExists)?;
148   };
149
150   // Check that the slur regex compiles, and returns the regex if valid...
151   // Prioritize using new slur regex from the request; if not provided, use the existing regex.
152   let slur_regex = build_and_check_regex(
153     &create_site
154       .slur_filter_regex
155       .as_deref()
156       .or(local_site.slur_filter_regex.as_deref()),
157   )?;
158
159   site_name_length_check(&create_site.name)?;
160   check_slurs(&create_site.name, &slur_regex)?;
161
162   if let Some(desc) = &create_site.description {
163     site_description_length_check(desc)?;
164     check_slurs_opt(&create_site.description, &slur_regex)?;
165   }
166
167   site_default_post_listing_type_check(&create_site.default_post_listing_type)?;
168
169   check_site_visibility_valid(
170     local_site.private_instance,
171     local_site.federation_enabled,
172     &create_site.private_instance,
173     &create_site.federation_enabled,
174   )?;
175
176   // Ensure that the sidebar has fewer than the max num characters...
177   is_valid_body_field(&create_site.sidebar, false)?;
178
179   application_question_check(
180     &local_site.application_question,
181     &create_site.application_question,
182     create_site
183       .registration_mode
184       .unwrap_or(local_site.registration_mode),
185   )
186 }
187
188 #[cfg(test)]
189 mod tests {
190   #![allow(clippy::unwrap_used)]
191   #![allow(clippy::indexing_slicing)]
192
193   use crate::site::create::validate_create_payload;
194   use lemmy_api_common::site::CreateSite;
195   use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode};
196   use lemmy_utils::error::LemmyErrorType;
197
198   #[test]
199   fn test_validate_invalid_create_payload() {
200     let invalid_payloads = [
201       (
202         "CreateSite attempted on set up LocalSite",
203         LemmyErrorType::SiteAlreadyExists,
204         &generate_local_site(
205           true,
206           None::<String>,
207           true,
208           false,
209           None::<String>,
210           RegistrationMode::Open,
211         ),
212         &generate_create_site(
213           String::from("site_name"),
214           None::<String>,
215           None::<String>,
216           None::<ListingType>,
217           None::<String>,
218           None::<bool>,
219           None::<bool>,
220           None::<String>,
221           None::<RegistrationMode>,
222         ),
223       ),
224       (
225         "CreateSite name matches LocalSite slur filter",
226         LemmyErrorType::Slurs,
227         &generate_local_site(
228           false,
229           Some(String::from("(foo|bar)")),
230           true,
231           false,
232           None::<String>,
233           RegistrationMode::Open,
234         ),
235         &generate_create_site(
236           String::from("foo site_name"),
237           None::<String>,
238           None::<String>,
239           None::<ListingType>,
240           None::<String>,
241           None::<bool>,
242           None::<bool>,
243           None::<String>,
244           None::<RegistrationMode>,
245         ),
246       ),
247       (
248         "CreateSite name matches new slur filter",
249         LemmyErrorType::Slurs,
250         &generate_local_site(
251           false,
252           Some(String::from("(foo|bar)")),
253           true,
254           false,
255           None::<String>,
256           RegistrationMode::Open,
257         ),
258         &generate_create_site(
259           String::from("zeta site_name"),
260           None::<String>,
261           None::<String>,
262           None::<ListingType>,
263           Some(String::from("(zeta|alpha)")),
264           None::<bool>,
265           None::<bool>,
266           None::<String>,
267           None::<RegistrationMode>,
268         ),
269       ),
270       (
271         "CreateSite listing type is Subscribed, which is invalid",
272         LemmyErrorType::InvalidDefaultPostListingType,
273         &generate_local_site(
274           false,
275           None::<String>,
276           true,
277           false,
278           None::<String>,
279           RegistrationMode::Open,
280         ),
281         &generate_create_site(
282           String::from("site_name"),
283           None::<String>,
284           None::<String>,
285           Some(ListingType::Subscribed),
286           None::<String>,
287           None::<bool>,
288           None::<bool>,
289           None::<String>,
290           None::<RegistrationMode>,
291         ),
292       ),
293       (
294         "CreateSite is both private and federated",
295         LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
296         &generate_local_site(
297           false,
298           None::<String>,
299           true,
300           false,
301           None::<String>,
302           RegistrationMode::Open,
303         ),
304         &generate_create_site(
305           String::from("site_name"),
306           None::<String>,
307           None::<String>,
308           None::<ListingType>,
309           None::<String>,
310           Some(true),
311           Some(true),
312           None::<String>,
313           None::<RegistrationMode>,
314         ),
315       ),
316       (
317         "LocalSite is private, but CreateSite also makes it federated",
318         LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
319         &generate_local_site(
320           false,
321           None::<String>,
322           true,
323           false,
324           None::<String>,
325           RegistrationMode::Open,
326         ),
327         &generate_create_site(
328           String::from("site_name"),
329           None::<String>,
330           None::<String>,
331           None::<ListingType>,
332           None::<String>,
333           None::<bool>,
334           Some(true),
335           None::<String>,
336           None::<RegistrationMode>,
337         ),
338       ),
339       (
340         "CreateSite requires application, but neither it nor LocalSite has an application question",
341         LemmyErrorType::ApplicationQuestionRequired,
342         &generate_local_site(
343           false,
344           None::<String>,
345           true,
346           false,
347           None::<String>,
348           RegistrationMode::Open,
349         ),
350         &generate_create_site(
351           String::from("site_name"),
352           None::<String>,
353           None::<String>,
354           None::<ListingType>,
355           None::<String>,
356           None::<bool>,
357           None::<bool>,
358           None::<String>,
359           Some(RegistrationMode::RequireApplication),
360         ),
361       ),
362     ];
363
364     invalid_payloads.iter().enumerate().for_each(
365       |(
366          idx,
367          &(reason, ref expected_err, local_site, create_site),
368        )| {
369         match validate_create_payload(
370           local_site,
371           create_site,
372         ) {
373           Ok(_) => {
374             panic!(
375               "Got Ok, but validation should have failed with error: {} for reason: {}. invalid_payloads.nth({})",
376               expected_err, reason, idx
377             )
378           }
379           Err(error) => {
380             assert!(
381               error.error_type.eq(&expected_err.clone()),
382               "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
383               error.error_type,
384               expected_err,
385               reason,
386               idx
387             )
388           }
389         }
390       },
391     );
392   }
393
394   #[test]
395   fn test_validate_valid_create_payload() {
396     let valid_payloads = [
397       (
398         "No changes between LocalSite and CreateSite",
399         &generate_local_site(
400           false,
401           None::<String>,
402           true,
403           false,
404           None::<String>,
405           RegistrationMode::Open,
406         ),
407         &generate_create_site(
408           String::from("site_name"),
409           None::<String>,
410           None::<String>,
411           None::<ListingType>,
412           None::<String>,
413           None::<bool>,
414           None::<bool>,
415           None::<String>,
416           None::<RegistrationMode>,
417         ),
418       ),
419       (
420         "CreateSite allows clearing and changing values",
421         &generate_local_site(
422           false,
423           None::<String>,
424           true,
425           false,
426           None::<String>,
427           RegistrationMode::Open,
428         ),
429         &generate_create_site(
430           String::from("site_name"),
431           Some(String::new()),
432           Some(String::new()),
433           Some(ListingType::All),
434           Some(String::new()),
435           Some(false),
436           Some(true),
437           Some(String::new()),
438           Some(RegistrationMode::Open),
439         ),
440       ),
441       (
442         "CreateSite clears existing slur filter regex",
443         &generate_local_site(
444           false,
445           Some(String::from("(foo|bar)")),
446           true,
447           false,
448           None::<String>,
449           RegistrationMode::Open,
450         ),
451         &generate_create_site(
452           String::from("foo site_name"),
453           None::<String>,
454           None::<String>,
455           None::<ListingType>,
456           Some(String::new()),
457           None::<bool>,
458           None::<bool>,
459           None::<String>,
460           None::<RegistrationMode>,
461         ),
462       ),
463       (
464         "LocalSite has application question and CreateSite now requires applications,",
465         &generate_local_site(
466           false,
467           None::<String>,
468           true,
469           false,
470           Some(String::from("question")),
471           RegistrationMode::Open,
472         ),
473         &generate_create_site(
474           String::from("site_name"),
475           None::<String>,
476           None::<String>,
477           None::<ListingType>,
478           None::<String>,
479           None::<bool>,
480           None::<bool>,
481           None::<String>,
482           Some(RegistrationMode::RequireApplication),
483         ),
484       ),
485     ];
486
487     valid_payloads
488       .iter()
489       .enumerate()
490       .for_each(|(idx, &(reason, local_site, edit_site))| {
491         assert!(
492           validate_create_payload(local_site, edit_site).is_ok(),
493           "Got Err, but should have got Ok for reason: {}. valid_payloads.nth({})",
494           reason,
495           idx
496         );
497       })
498   }
499
500   fn generate_local_site(
501     site_setup: bool,
502     site_slur_filter_regex: Option<String>,
503     site_is_private: bool,
504     site_is_federated: bool,
505     site_application_question: Option<String>,
506     site_registration_mode: RegistrationMode,
507   ) -> LocalSite {
508     LocalSite {
509       id: Default::default(),
510       site_id: Default::default(),
511       site_setup,
512       enable_downvotes: false,
513       enable_nsfw: false,
514       community_creation_admin_only: false,
515       require_email_verification: false,
516       application_question: site_application_question,
517       private_instance: site_is_private,
518       default_theme: String::new(),
519       default_post_listing_type: ListingType::All,
520       legal_information: None,
521       hide_modlog_mod_names: false,
522       application_email_admins: false,
523       slur_filter_regex: site_slur_filter_regex,
524       actor_name_max_length: 0,
525       federation_enabled: site_is_federated,
526       captcha_enabled: false,
527       captcha_difficulty: String::new(),
528       published: Default::default(),
529       updated: None,
530       registration_mode: site_registration_mode,
531       reports_email_admins: false,
532     }
533   }
534
535   // Allow the test helper function to have too many arguments.
536   // It's either this or generate the entire struct each time for testing.
537   #[allow(clippy::too_many_arguments)]
538   fn generate_create_site(
539     site_name: String,
540     site_description: Option<String>,
541     site_sidebar: Option<String>,
542     site_listing_type: Option<ListingType>,
543     site_slur_filter_regex: Option<String>,
544     site_is_private: Option<bool>,
545     site_is_federated: Option<bool>,
546     site_application_question: Option<String>,
547     site_registration_mode: Option<RegistrationMode>,
548   ) -> CreateSite {
549     CreateSite {
550       name: site_name,
551       sidebar: site_sidebar,
552       description: site_description,
553       icon: None,
554       banner: None,
555       enable_downvotes: None,
556       enable_nsfw: None,
557       community_creation_admin_only: None,
558       require_email_verification: None,
559       application_question: site_application_question,
560       private_instance: site_is_private,
561       default_theme: None,
562       default_post_listing_type: site_listing_type,
563       legal_information: None,
564       application_email_admins: None,
565       hide_modlog_mod_names: None,
566       discussion_languages: None,
567       slur_filter_regex: site_slur_filter_regex,
568       actor_name_max_length: None,
569       rate_limit_message: None,
570       rate_limit_message_per_second: None,
571       rate_limit_post: None,
572       rate_limit_post_per_second: None,
573       rate_limit_register: None,
574       rate_limit_register_per_second: None,
575       rate_limit_image: None,
576       rate_limit_image_per_second: None,
577       rate_limit_comment: None,
578       rate_limit_comment_per_second: None,
579       rate_limit_search: None,
580       rate_limit_search_per_second: None,
581       federation_enabled: site_is_federated,
582       federation_debug: None,
583       captcha_enabled: None,
584       captcha_difficulty: None,
585       allowed_instances: None,
586       blocked_instances: None,
587       taglines: None,
588       registration_mode: site_registration_mode,
589       auth: Default::default(),
590     }
591   }
592 }