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