X-Git-Url: http://these/git/?a=blobdiff_plain;f=crates%2Fapi_crud%2Fsrc%2Fsite%2Fcreate.rs;h=34e153c59a0642a4687eaf043351a934571e4567;hb=HEAD;hp=dc6e649d6a096a5816e08c19c8af93ed024d24dc;hpb=db1abff857278c621278e4a398095df5d3351e43;p=lemmy.git diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index dc6e649d..34e153c5 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -1,82 +1,590 @@ -use crate::PerformCrud; -use actix_web::web::Data; +use crate::site::{application_question_check, site_default_post_listing_type_check}; +use activitypub_federation::http_signatures::generate_actor_keypair; +use actix_web::web::{Data, Json}; use lemmy_api_common::{ - blocking, - get_local_user_view_from_jwt, - is_admin, - site::*, - site_description_length_check, + context::LemmyContext, + site::{CreateSite, SiteResponse}, + utils::{ + generate_site_inbox_url, is_admin, local_site_rate_limit_to_rate_limit_config, + local_user_view_from_jwt, sanitize_html, sanitize_html_opt, + }, }; -use lemmy_db_queries::{ - diesel_option_overwrite, - diesel_option_overwrite_to_url, - source::site::Site_, - Crud, +use lemmy_db_schema::{ + newtypes::DbUrl, + source::{ + local_site::{LocalSite, LocalSiteUpdateForm}, + local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, + site::{Site, SiteUpdateForm}, + tagline::Tagline, + }, + traits::Crud, + utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now}, }; -use lemmy_db_schema::source::site::{Site, *}; -use lemmy_db_views::site_view::SiteView; +use lemmy_db_views::structs::SiteView; use lemmy_utils::{ - utils::{check_slurs, check_slurs_opt}, - ApiError, - ConnectionId, - LemmyError, + error::{LemmyError, LemmyErrorType, LemmyResult}, + utils::{ + slurs::{check_slurs, check_slurs_opt}, + validation::{ + build_and_check_regex, check_site_visibility_valid, is_valid_body_field, + site_description_length_check, site_name_length_check, + }, + }, }; -use lemmy_websocket::LemmyContext; +use url::Url; -#[async_trait::async_trait(?Send)] -impl PerformCrud for CreateSite { - type Response = SiteResponse; +#[tracing::instrument(skip(context))] +pub async fn create_site( + data: Json, + context: Data, +) -> Result, LemmyError> { + let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?; + let local_site = LocalSite::read(&mut context.pool()).await?; - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &CreateSite = &self; + // Make sure user is an admin; other types of users should not create site data... + is_admin(&local_user_view)?; - let read_site = move |conn: &'_ _| Site::read_simple(conn); - if blocking(context.pool(), read_site).await?.is_ok() { - return Err(ApiError::err("site_already_exists").into()); - }; + validate_create_payload(&local_site, &data)?; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into(); + let inbox_url = Some(generate_site_inbox_url(&actor_id)?); + let keypair = generate_actor_keypair()?; + let name = sanitize_html(&data.name); + let sidebar = sanitize_html_opt(&data.sidebar); + let description = sanitize_html_opt(&data.description); - check_slurs(&data.name)?; - check_slurs_opt(&data.description)?; + let site_form = SiteUpdateForm { + name: Some(name), + sidebar: diesel_option_overwrite(sidebar), + description: diesel_option_overwrite(description), + icon: diesel_option_overwrite_to_url(&data.icon)?, + banner: diesel_option_overwrite_to_url(&data.banner)?, + actor_id: Some(actor_id), + last_refreshed_at: Some(naive_now()), + inbox_url, + private_key: Some(Some(keypair.private_key)), + public_key: Some(keypair.public_key), + ..Default::default() + }; - // Make sure user is an admin - is_admin(&local_user_view)?; + let site_id = local_site.site_id; - let sidebar = diesel_option_overwrite(&data.sidebar); - let description = diesel_option_overwrite(&data.description); - let icon = diesel_option_overwrite_to_url(&data.icon)?; - let banner = diesel_option_overwrite_to_url(&data.banner)?; + Site::update(&mut context.pool(), site_id, &site_form).await?; + let application_question = sanitize_html_opt(&data.application_question); + let default_theme = sanitize_html_opt(&data.default_theme); + let legal_information = sanitize_html_opt(&data.legal_information); - if let Some(Some(desc)) = &description { - site_description_length_check(desc)?; - } + let local_site_form = LocalSiteUpdateForm { + // Set the site setup to true + site_setup: Some(true), + enable_downvotes: data.enable_downvotes, + enable_federated_downvotes: data.enable_federated_downvotes, + registration_mode: data.registration_mode, + enable_nsfw: data.enable_nsfw, + community_creation_admin_only: data.community_creation_admin_only, + require_email_verification: data.require_email_verification, + application_question: diesel_option_overwrite(application_question), + private_instance: data.private_instance, + default_theme, + default_post_listing_type: data.default_post_listing_type, + legal_information: diesel_option_overwrite(legal_information), + application_email_admins: data.application_email_admins, + hide_modlog_mod_names: data.hide_modlog_mod_names, + updated: Some(Some(naive_now())), + slur_filter_regex: diesel_option_overwrite(data.slur_filter_regex.clone()), + actor_name_max_length: data.actor_name_max_length, + federation_enabled: data.federation_enabled, + captcha_enabled: data.captcha_enabled, + captcha_difficulty: data.captcha_difficulty.clone(), + ..Default::default() + }; - let site_form = SiteForm { - name: data.name.to_owned(), - sidebar, - description, - icon, - banner, - creator_id: local_user_view.person.id, - enable_downvotes: data.enable_downvotes, - open_registration: data.open_registration, - enable_nsfw: data.enable_nsfw, - updated: None, - community_creation_admin_only: Some(data.community_creation_admin_only), - }; + LocalSite::update(&mut context.pool(), &local_site_form).await?; - let create_site = move |conn: &'_ _| Site::create(conn, &site_form); - if blocking(context.pool(), create_site).await?.is_err() { - return Err(ApiError::err("site_already_exists").into()); - } + let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm { + message: data.rate_limit_message, + message_per_second: data.rate_limit_message_per_second, + post: data.rate_limit_post, + post_per_second: data.rate_limit_post_per_second, + register: data.rate_limit_register, + register_per_second: data.rate_limit_register_per_second, + image: data.rate_limit_image, + image_per_second: data.rate_limit_image_per_second, + comment: data.rate_limit_comment, + comment_per_second: data.rate_limit_comment_per_second, + search: data.rate_limit_search, + search_per_second: data.rate_limit_search_per_second, + ..Default::default() + }; + + LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?; + + let site_view = SiteView::read_local(&mut context.pool()).await?; + + let new_taglines = data.taglines.clone(); + let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; + + let rate_limit_config = + local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); + context + .settings_updated_channel() + .send(rate_limit_config) + .await?; + + Ok(Json(SiteResponse { + site_view, + taglines, + })) +} + +fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> { + // Make sure the site hasn't already been set up... + if local_site.site_setup { + Err(LemmyErrorType::SiteAlreadyExists)?; + }; + + // Check that the slur regex compiles, and returns the regex if valid... + // Prioritize using new slur regex from the request; if not provided, use the existing regex. + let slur_regex = build_and_check_regex( + &create_site + .slur_filter_regex + .as_deref() + .or(local_site.slur_filter_regex.as_deref()), + )?; + + site_name_length_check(&create_site.name)?; + check_slurs(&create_site.name, &slur_regex)?; + + if let Some(desc) = &create_site.description { + site_description_length_check(desc)?; + check_slurs_opt(&create_site.description, &slur_regex)?; + } + + site_default_post_listing_type_check(&create_site.default_post_listing_type)?; + + check_site_visibility_valid( + local_site.private_instance, + local_site.federation_enabled, + &create_site.private_instance, + &create_site.federation_enabled, + )?; - let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??; + // Ensure that the sidebar has fewer than the max num characters... + is_valid_body_field(&create_site.sidebar, false)?; - Ok(SiteResponse { site_view }) + application_question_check( + &local_site.application_question, + &create_site.application_question, + create_site + .registration_mode + .unwrap_or(local_site.registration_mode), + ) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + + use crate::site::create::validate_create_payload; + use lemmy_api_common::site::CreateSite; + use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode}; + use lemmy_utils::error::LemmyErrorType; + + #[test] + fn test_validate_invalid_create_payload() { + let invalid_payloads = [ + ( + "CreateSite attempted on set up LocalSite", + LemmyErrorType::SiteAlreadyExists, + &generate_local_site( + true, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite name matches LocalSite slur filter", + LemmyErrorType::Slurs, + &generate_local_site( + false, + Some(String::from("(foo|bar)")), + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("foo site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite name matches new slur filter", + LemmyErrorType::Slurs, + &generate_local_site( + false, + Some(String::from("(foo|bar)")), + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("zeta site_name"), + None::, + None::, + None::, + Some(String::from("(zeta|alpha)")), + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite listing type is Subscribed, which is invalid", + LemmyErrorType::InvalidDefaultPostListingType, + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + Some(ListingType::Subscribed), + None::, + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite is both private and federated", + LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + Some(true), + Some(true), + None::, + None::, + ), + ), + ( + "LocalSite is private, but CreateSite also makes it federated", + LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + Some(true), + None::, + None::, + ), + ), + ( + "CreateSite requires application, but neither it nor LocalSite has an application question", + LemmyErrorType::ApplicationQuestionRequired, + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + Some(RegistrationMode::RequireApplication), + ), + ), + ]; + + invalid_payloads.iter().enumerate().for_each( + |( + idx, + &(reason, ref expected_err, local_site, create_site), + )| { + match validate_create_payload( + local_site, + create_site, + ) { + Ok(_) => { + panic!( + "Got Ok, but validation should have failed with error: {} for reason: {}. invalid_payloads.nth({})", + expected_err, reason, idx + ) + } + Err(error) => { + assert!( + error.error_type.eq(&expected_err.clone()), + "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})", + error.error_type, + expected_err, + reason, + idx + ) + } + } + }, + ); + } + + #[test] + fn test_validate_valid_create_payload() { + let valid_payloads = [ + ( + "No changes between LocalSite and CreateSite", + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite allows clearing and changing values", + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + Some(String::new()), + Some(String::new()), + Some(ListingType::All), + Some(String::new()), + Some(false), + Some(true), + Some(String::new()), + Some(RegistrationMode::Open), + ), + ), + ( + "CreateSite clears existing slur filter regex", + &generate_local_site( + false, + Some(String::from("(foo|bar)")), + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("foo site_name"), + None::, + None::, + None::, + Some(String::new()), + None::, + None::, + None::, + None::, + ), + ), + ( + "LocalSite has application question and CreateSite now requires applications,", + &generate_local_site( + false, + None::, + true, + false, + Some(String::from("question")), + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + Some(RegistrationMode::RequireApplication), + ), + ), + ]; + + valid_payloads + .iter() + .enumerate() + .for_each(|(idx, &(reason, local_site, edit_site))| { + assert!( + validate_create_payload(local_site, edit_site).is_ok(), + "Got Err, but should have got Ok for reason: {}. valid_payloads.nth({})", + reason, + idx + ); + }) + } + + fn generate_local_site( + site_setup: bool, + site_slur_filter_regex: Option, + site_is_private: bool, + site_is_federated: bool, + site_application_question: Option, + site_registration_mode: RegistrationMode, + ) -> LocalSite { + LocalSite { + id: Default::default(), + site_id: Default::default(), + site_setup, + enable_downvotes: false, + enable_federated_downvotes: false, + enable_nsfw: false, + community_creation_admin_only: false, + require_email_verification: false, + application_question: site_application_question, + private_instance: site_is_private, + default_theme: String::new(), + default_post_listing_type: ListingType::All, + legal_information: None, + hide_modlog_mod_names: false, + application_email_admins: false, + slur_filter_regex: site_slur_filter_regex, + actor_name_max_length: 0, + federation_enabled: site_is_federated, + captcha_enabled: false, + captcha_difficulty: String::new(), + published: Default::default(), + updated: None, + registration_mode: site_registration_mode, + reports_email_admins: false, + } + } + + // Allow the test helper function to have too many arguments. + // It's either this or generate the entire struct each time for testing. + #[allow(clippy::too_many_arguments)] + fn generate_create_site( + site_name: String, + site_description: Option, + site_sidebar: Option, + site_listing_type: Option, + site_slur_filter_regex: Option, + site_is_private: Option, + site_is_federated: Option, + site_application_question: Option, + site_registration_mode: Option, + ) -> CreateSite { + CreateSite { + name: site_name, + sidebar: site_sidebar, + description: site_description, + icon: None, + banner: None, + enable_downvotes: None, + enable_federated_downvotes: None, + enable_nsfw: None, + community_creation_admin_only: None, + require_email_verification: None, + application_question: site_application_question, + private_instance: site_is_private, + default_theme: None, + default_post_listing_type: site_listing_type, + legal_information: None, + application_email_admins: None, + hide_modlog_mod_names: None, + discussion_languages: None, + slur_filter_regex: site_slur_filter_regex, + actor_name_max_length: None, + rate_limit_message: None, + rate_limit_message_per_second: None, + rate_limit_post: None, + rate_limit_post_per_second: None, + rate_limit_register: None, + rate_limit_register_per_second: None, + rate_limit_image: None, + rate_limit_image_per_second: None, + rate_limit_comment: None, + rate_limit_comment_per_second: None, + rate_limit_search: None, + rate_limit_search_per_second: None, + federation_enabled: site_is_federated, + federation_debug: None, + captcha_enabled: None, + captcha_difficulty: None, + allowed_instances: None, + blocked_instances: None, + taglines: None, + registration_mode: site_registration_mode, + auth: Default::default(), + } } }