From 7b8cbbba85e259810ce578d0d4a4387a38feb02f Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 4 Aug 2021 17:13:51 -0400 Subject: [PATCH] Simplify config using macros (#1686) Co-authored-by: Felix Ableitner --- Cargo.lock | 69 ++-------- crates/api/src/local_user.rs | 2 +- crates/api_common/src/lib.rs | 10 +- crates/api_crud/src/post/create.rs | 13 +- crates/api_crud/src/post/update.rs | 13 +- crates/api_crud/src/site/read.rs | 2 +- crates/api_crud/src/user/create.rs | 2 +- crates/apub/src/activities/community/mod.rs | 2 +- crates/apub/src/activity_queue.rs | 6 +- crates/apub/src/http/mod.rs | 2 +- crates/apub/src/http/routes.rs | 4 +- crates/apub/src/lib.rs | 11 +- crates/apub/src/objects/mod.rs | 2 +- crates/apub/src/objects/person.rs | 2 +- crates/apub/src/objects/post.rs | 20 +-- crates/routes/src/images.rs | 30 ++--- crates/routes/src/nodeinfo.rs | 2 +- crates/routes/src/webfinger.rs | 2 +- crates/utils/Cargo.toml | 3 +- crates/utils/src/claims.rs | 6 +- crates/utils/src/email.rs | 4 +- crates/utils/src/lib.rs | 6 +- crates/utils/src/rate_limit/mod.rs | 4 +- crates/utils/src/request.rs | 134 +++++++++----------- crates/utils/src/settings/defaults.rs | 72 ----------- crates/utils/src/settings/mod.rs | 124 ++---------------- crates/utils/src/settings/structs.rs | 121 ++++++++++-------- crates/utils/src/utils.rs | 6 +- src/main.rs | 7 +- 29 files changed, 230 insertions(+), 451 deletions(-) delete mode 100644 crates/utils/src/settings/defaults.rs diff --git a/Cargo.lock b/Cargo.lock index 622c260c6..ee05a46ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1001,15 +1001,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "envy" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" -dependencies = [ - "serde", -] - [[package]] name = "event-listener" version = "2.5.1" @@ -1882,7 +1873,6 @@ dependencies = [ "comrak", "deser-hjson", "diesel", - "envy", "futures", "http", "itertools", @@ -1890,7 +1880,6 @@ dependencies = [ "lazy_static", "lettre", "log", - "merge", "openssl", "percent-encoding", "rand 0.8.4", @@ -1898,6 +1887,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "smart-default", "strum", "strum_macros", "thiserror", @@ -2037,28 +2027,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "merge" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" -dependencies = [ - "merge_derive", - "num-traits", -] - -[[package]] -name = "merge_derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" -dependencies = [ - "proc-macro-error", - "proc-macro2 1.0.27", - "quote 1.0.9", - "syn 1.0.73", -] - [[package]] name = "migrations_internals" version = "1.4.1" @@ -2444,30 +2412,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2 1.0.27", - "quote 1.0.9", - "syn 1.0.73", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2 1.0.27", - "quote 1.0.9", - "version_check", -] - [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -3021,6 +2965,17 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2 1.0.27", + "quote 1.0.9", + "syn 1.0.73", +] + [[package]] name = "socket2" version = "0.4.0" diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index 4b48a0083..54fe6d76b 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -115,7 +115,7 @@ impl Perform for GetCaptcha { context: &Data, _websocket_id: Option, ) -> Result { - let captcha_settings = Settings::get().captcha(); + let captcha_settings = Settings::get().captcha; if !captcha_settings.enabled { return Ok(GetCaptchaResponse { ok: None }); diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 63c0bd530..4e3cd751b 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -198,7 +198,7 @@ pub fn send_email_to_user( let subject = &format!( "{} - {} {}", subject_text, - Settings::get().hostname(), + Settings::get().hostname, local_user_view.person.name, ); let html = &format!( @@ -386,14 +386,14 @@ pub async fn collect_moderated_communities( pub async fn build_federated_instances( pool: &DbPool, ) -> Result, LemmyError> { - if Settings::get().federation().enabled { + if Settings::get().federation.enabled { let distinct_communities = blocking(pool, move |conn| { Community::distinct_federated_communities(conn) }) .await??; - let allowed = Settings::get().get_allowed_instances(); - let blocked = Settings::get().get_blocked_instances(); + let allowed = Settings::get().federation.allowed_instances; + let blocked = Settings::get().federation.blocked_instances; let mut linked = distinct_communities .iter() @@ -405,7 +405,7 @@ pub async fn build_federated_instances( } if let Some(blocked) = blocked.as_ref() { - linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname())); + linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname)); } // Sort and remove dupes diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index bbb720faf..f041a00b1 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -52,8 +52,11 @@ impl PerformCrud for CreatePost { // Fetch Iframely and pictrs cached image let data_url = data.url.as_ref(); - let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = - fetch_iframely_and_pictrs_data(context.client(), data_url).await; + let (iframely_response, pictrs_thumbnail) = + fetch_iframely_and_pictrs_data(context.client(), data_url).await?; + let (embed_title, embed_description, embed_html) = iframely_response + .map(|u| (u.title, u.description, u.html)) + .unwrap_or((None, None, None)); let post_form = PostForm { name: data.name.trim().to_owned(), @@ -62,9 +65,9 @@ impl PerformCrud for CreatePost { community_id: data.community_id, creator_id: local_user_view.person.id, nsfw: data.nsfw, - embed_title: iframely_title, - embed_description: iframely_description, - embed_html: iframely_html, + embed_title, + embed_description, + embed_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), ..PostForm::default() }; diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index dade6f42b..c9fe7e332 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -52,8 +52,11 @@ impl PerformCrud for EditPost { // Fetch Iframely and Pictrs cached image let data_url = data.url.as_ref(); - let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = - fetch_iframely_and_pictrs_data(context.client(), data_url).await; + let (iframely_response, pictrs_thumbnail) = + fetch_iframely_and_pictrs_data(context.client(), data_url).await?; + let (embed_title, embed_description, embed_html) = iframely_response + .map(|u| (u.title, u.description, u.html)) + .unwrap_or((None, None, None)); let post_form = PostForm { creator_id: orig_post.creator_id.to_owned(), @@ -63,9 +66,9 @@ impl PerformCrud for EditPost { body: data.body.to_owned(), nsfw: data.nsfw, updated: Some(naive_now()), - embed_title: iframely_title, - embed_description: iframely_description, - embed_html: iframely_html, + embed_title, + embed_description, + embed_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), ..PostForm::default() }; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index da379e322..8b2df7117 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -28,7 +28,7 @@ impl PerformCrud for GetSite { Ok(site_view) => Some(site_view), // If the site isn't created yet, check the setup Err(_) => { - if let Some(setup) = Settings::get().setup().as_ref() { + if let Some(setup) = Settings::get().setup.as_ref() { let register = Register { username: setup.admin_username.to_owned(), email: setup.admin_email.to_owned(), diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 8047369fa..6f9216b36 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -69,7 +69,7 @@ impl PerformCrud for Register { .await??; // If its not the admin, check the captcha - if !no_admins && Settings::get().captcha().enabled { + if !no_admins && Settings::get().captcha.enabled { let check = context .chat_server() .send(CheckCaptcha { diff --git a/crates/apub/src/activities/community/mod.rs b/crates/apub/src/activities/community/mod.rs index 62b39c6e8..4151471ad 100644 --- a/crates/apub/src/activities/community/mod.rs +++ b/crates/apub/src/activities/community/mod.rs @@ -50,7 +50,7 @@ async fn list_community_follower_inboxes( .iter() .flatten() .unique() - .filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname())) + .filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname)) .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok()) .map(|inbox| inbox.to_owned()) .collect(), diff --git a/crates/apub/src/activity_queue.rs b/crates/apub/src/activity_queue.rs index 3f8a019fd..c7ff0a81d 100644 --- a/crates/apub/src/activity_queue.rs +++ b/crates/apub/src/activity_queue.rs @@ -83,7 +83,7 @@ where .iter() .flatten() .unique() - .filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname())) + .filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname)) .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok()) .map(|inbox| inbox.to_owned()) .collect(); @@ -170,7 +170,7 @@ pub(crate) async fn send_activity_new( where T: Serialize, { - if !Settings::get().federation().enabled || inboxes.is_empty() { + if !Settings::get().federation.enabled || inboxes.is_empty() { return Ok(()); } @@ -230,7 +230,7 @@ where Kind: Serialize, >::Error: From + Send + Sync + 'static, { - if !Settings::get().federation().enabled || inboxes.is_empty() { + if !Settings::get().federation.enabled || inboxes.is_empty() { return Ok(()); } diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index f366cb48c..477345fd4 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -175,7 +175,7 @@ fn assert_activity_not_local(activity: &T) -> Result .domain() .context(location_info!())?; - if activity_domain == Settings::get().hostname() { + if activity_domain == Settings::get().hostname { return Err( anyhow!( "Error: received activity which was sent by local instance: {:?}", diff --git a/crates/apub/src/http/routes.rs b/crates/apub/src/http/routes.rs index 1afc3f1e5..cd6b11486 100644 --- a/crates/apub/src/http/routes.rs +++ b/crates/apub/src/http/routes.rs @@ -25,8 +25,8 @@ static APUB_JSON_CONTENT_TYPE_LONG: &str = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""; pub fn config(cfg: &mut web::ServiceConfig) { - if Settings::get().federation().enabled { - println!("federation enabled, host is {}", Settings::get().hostname()); + if Settings::get().federation.enabled { + println!("federation enabled, host is {}", Settings::get().hostname); let digest_verifier = VerifyDigest::new(Sha256::new()); let header_guard_accept = guard::Any(guard::Header("Accept", APUB_JSON_CONTENT_TYPE)) diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 191230579..60d7c4871 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -75,7 +75,7 @@ pub fn check_is_apub_id_valid(apub_id: &Url, use_strict_allowlist: bool) -> Resu let domain = apub_id.domain().context(location_info!())?.to_string(); let local_instance = settings.get_hostname_without_port()?; - if !settings.federation().enabled { + if !settings.federation.enabled { return if domain == local_instance { Ok(()) } else { @@ -102,18 +102,15 @@ pub fn check_is_apub_id_valid(apub_id: &Url, use_strict_allowlist: bool) -> Resu // TODO: might be good to put the part above in one method, and below in another // (which only gets called in apub::objects) // -> no that doesnt make sense, we still need the code below for blocklist and strict allowlist - if let Some(blocked) = Settings::get().get_blocked_instances() { + if let Some(blocked) = Settings::get().federation.blocked_instances { if blocked.contains(&domain) { return Err(anyhow!("{} is in federation blocklist", domain).into()); } } - if let Some(mut allowed) = Settings::get().get_allowed_instances() { + if let Some(mut allowed) = Settings::get().federation.allowed_instances { // Only check allowlist if this is a community, or strict allowlist is enabled. - let strict_allowlist = Settings::get() - .federation() - .strict_allowlist - .unwrap_or(true); + let strict_allowlist = Settings::get().federation.strict_allowlist; if use_strict_allowlist || strict_allowlist { // need to allow this explicitly because apub receive might contain objects from our local // instance. diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index c4f57c51a..0a29f29a3 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -193,7 +193,7 @@ where let domain = object_id.domain().context(location_info!())?; // if its a local object, return it directly from the database - if Settings::get().hostname() == domain { + if Settings::get().hostname == domain { let object = blocking(context.pool(), move |conn| { To::read_from_apub_id(conn, &object_id.into()) }) diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 29935ec07..cbc80b505 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -109,7 +109,7 @@ impl FromApub for DbPerson { ) -> Result { let person_id = person.id_unchecked().context(location_info!())?.to_owned(); let domain = person_id.domain().context(location_info!())?; - if domain == Settings::get().hostname() { + if domain == Settings::get().hostname { let person = blocking(context.pool(), move |conn| { DbPerson::read_from_apub_id(conn, &person_id.into()) }) diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index 1f2172d16..2fdf8d67b 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -178,12 +178,14 @@ impl FromApub for Post { let community = extract_community(&page.to, context, request_counter).await?; let thumbnail_url: Option = page.image.clone().map(|i| i.url); - let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = - if let Some(url) = &page.url { - fetch_iframely_and_pictrs_data(context.client(), Some(url)).await - } else { - (None, None, None, thumbnail_url) - }; + let (iframely_response, pictrs_thumbnail) = if let Some(url) = &page.url { + fetch_iframely_and_pictrs_data(context.client(), Some(url)).await? + } else { + (None, thumbnail_url) + }; + let (embed_title, embed_description, embed_html) = iframely_response + .map(|u| (u.title, u.description, u.html)) + .unwrap_or((None, None, None)); let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content)); let form = PostForm { @@ -199,9 +201,9 @@ impl FromApub for Post { deleted: None, nsfw: page.sensitive, stickied: page.stickied, - embed_title: iframely_title, - embed_description: iframely_description, - embed_html: iframely_html, + embed_title, + embed_description, + embed_html, thumbnail_url: pictrs_thumbnail.map(|u| u.into()), ap_id: Some(page.id.clone().into()), local: Some(false), diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index ff2b83877..a89bde0f1 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -1,6 +1,7 @@ use actix_web::{body::BodyStream, http::StatusCode, web::Data, *}; +use anyhow::anyhow; use awc::Client; -use lemmy_utils::{claims::Claims, rate_limit::RateLimit, settings::structs::Settings}; +use lemmy_utils::{claims::Claims, rate_limit::RateLimit, settings::structs::Settings, LemmyError}; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -54,10 +55,7 @@ async fn upload( return Ok(HttpResponse::Unauthorized().finish()); }; - let mut client_req = client.request_from( - format!("{}/image", Settings::get().pictrs_url()), - req.head(), - ); + let mut client_req = client.request_from(format!("{}/image", pictrs_url()?), req.head()); if let Some(addr) = req.head().peer_addr { client_req = client_req.insert_header(("X-Forwarded-For", addr.to_string())) @@ -83,17 +81,12 @@ async fn full_res( // If there are no query params, the URL is original let url = if params.format.is_none() && params.thumbnail.is_none() { - format!("{}/image/original/{}", Settings::get().pictrs_url(), name,) + format!("{}/image/original/{}", pictrs_url()?, name,) } else { // Use jpg as a default when none is given let format = params.format.unwrap_or_else(|| "jpg".to_string()); - let mut url = format!( - "{}/image/process.{}?src={}", - Settings::get().pictrs_url(), - format, - name, - ); + let mut url = format!("{}/image/process.{}?src={}", pictrs_url()?, format, name,); if let Some(size) = params.thumbnail { url = format!("{}&thumbnail={}", url, size,); @@ -141,12 +134,7 @@ async fn delete( ) -> Result { let (token, file) = components.into_inner(); - let url = format!( - "{}/image/delete/{}/{}", - Settings::get().pictrs_url(), - &token, - &file - ); + let url = format!("{}/image/delete/{}/{}", pictrs_url()?, &token, &file); let mut client_req = client.request_from(url, req.head()); @@ -162,3 +150,9 @@ async fn delete( Ok(HttpResponse::build(res.status()).body(BodyStream::new(res))) } + +fn pictrs_url() -> Result { + Settings::get() + .pictrs_url + .ok_or_else(|| anyhow!("images_disabled").into()) +} diff --git a/crates/routes/src/nodeinfo.rs b/crates/routes/src/nodeinfo.rs index d06f60928..86d625900 100644 --- a/crates/routes/src/nodeinfo.rs +++ b/crates/routes/src/nodeinfo.rs @@ -31,7 +31,7 @@ async fn node_info(context: web::Data) -> Result( jwt, - &DecodingKey::from_secret(Settings::get().jwt_secret().as_ref()), + &DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()), &v, ) } @@ -30,13 +30,13 @@ impl Claims { pub fn jwt(local_user_id: i32) -> Result { let my_claims = Claims { sub: local_user_id, - iss: Settings::get().hostname(), + iss: Settings::get().hostname, iat: Utc::now().timestamp(), }; encode( &Header::default(), &my_claims, - &EncodingKey::from_secret(Settings::get().jwt_secret().as_ref()), + &EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()), ) } } diff --git a/crates/utils/src/email.rs b/crates/utils/src/email.rs index e2853fa49..3e5307421 100644 --- a/crates/utils/src/email.rs +++ b/crates/utils/src/email.rs @@ -19,8 +19,8 @@ pub fn send_email( to_username: &str, html: &str, ) -> Result<(), String> { - let email_config = Settings::get().email().ok_or("no_email_setup")?; - let domain = Settings::get().hostname(); + let email_config = Settings::get().email.ok_or("no_email_setup")?; + let domain = Settings::get().hostname; let (smtp_server, smtp_port) = { let email_and_port = email_config.smtp_server.split(':').collect::>(); diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index ca544bd49..6bf3237e4 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -2,6 +2,8 @@ extern crate lazy_static; #[macro_use] extern crate strum_macros; +#[macro_use] +extern crate smart_default; pub mod apub; pub mod claims; @@ -90,12 +92,12 @@ impl actix_web::error::ResponseError for LemmyError { lazy_static! { pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!( "^group:([a-z0-9_]{{3,}})@{}$", - Settings::get().hostname() + Settings::get().hostname )) .expect("compile webfinger regex"); pub static ref WEBFINGER_USERNAME_REGEX: Regex = Regex::new(&format!( "^acct:([a-z0-9_]{{3,}})@{}$", - Settings::get().hostname() + Settings::get().hostname )) .expect("compile webfinger regex"); } diff --git a/crates/utils/src/rate_limit/mod.rs b/crates/utils/src/rate_limit/mod.rs index 5fc77d27d..0701be25b 100644 --- a/crates/utils/src/rate_limit/mod.rs +++ b/crates/utils/src/rate_limit/mod.rs @@ -71,7 +71,9 @@ impl RateLimited { { // Does not need to be blocking because the RwLock in settings never held across await points, // and the operation here locks only long enough to clone - let rate_limit: RateLimitConfig = Settings::get().rate_limit(); + let rate_limit: RateLimitConfig = Settings::get() + .rate_limit + .unwrap_or_else(RateLimitConfig::default); // before { diff --git a/crates/utils/src/request.rs b/crates/utils/src/request.rs index f14a6c1ac..c59180c55 100644 --- a/crates/utils/src/request.rs +++ b/crates/utils/src/request.rs @@ -48,26 +48,30 @@ where } #[derive(Deserialize, Debug)] -pub(crate) struct IframelyResponse { - title: Option, - description: Option, +pub struct IframelyResponse { + pub title: Option, + pub description: Option, thumbnail_url: Option, - html: Option, + pub html: Option, } pub(crate) async fn fetch_iframely( client: &Client, url: &Url, ) -> Result { - let fetch_url = format!("{}/oembed?url={}", Settings::get().iframely_url(), url); + if let Some(iframely_url) = Settings::get().iframely_url { + let fetch_url = format!("{}/oembed?url={}", iframely_url, url); - let response = retry(|| client.get(&fetch_url).send()).await?; + let response = retry(|| client.get(&fetch_url).send()).await?; - let res: IframelyResponse = response - .json() - .await - .map_err(|e| RecvError(e.to_string()))?; - Ok(res) + let res: IframelyResponse = response + .json() + .await + .map_err(|e| RecvError(e.to_string()))?; + Ok(res) + } else { + Err(anyhow!("Missing Iframely URL in config.").into()) + } } #[derive(Deserialize, Debug, Clone)] @@ -85,91 +89,73 @@ pub(crate) struct PictrsFile { pub(crate) async fn fetch_pictrs( client: &Client, image_url: &Url, -) -> Result { - is_image_content_type(client, image_url).await?; +) -> Result, LemmyError> { + if let Some(pictrs_url) = Settings::get().pictrs_url { + is_image_content_type(client, image_url).await?; - let fetch_url = format!( - "{}/image/download?url={}", - Settings::get().pictrs_url(), - utf8_percent_encode(image_url.as_str(), NON_ALPHANUMERIC) // TODO this might not be needed - ); + let fetch_url = format!( + "{}/image/download?url={}", + pictrs_url, + utf8_percent_encode(image_url.as_str(), NON_ALPHANUMERIC) // TODO this might not be needed + ); - let response = retry(|| client.get(&fetch_url).send()).await?; + let response = retry(|| client.get(&fetch_url).send()).await?; - let response: PictrsResponse = response - .json() - .await - .map_err(|e| RecvError(e.to_string()))?; + let response: PictrsResponse = response + .json() + .await + .map_err(|e| RecvError(e.to_string()))?; - if response.msg == "ok" { - Ok(response) + if response.msg == "ok" { + Ok(Some(response)) + } else { + Err(anyhow!("{}", &response.msg).into()) + } } else { - Err(anyhow!("{}", &response.msg).into()) + Ok(None) } } pub async fn fetch_iframely_and_pictrs_data( client: &Client, url: Option<&Url>, -) -> (Option, Option, Option, Option) { +) -> Result<(Option, Option), LemmyError> { match &url { Some(url) => { // Fetch iframely data - let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) = - match fetch_iframely(client, url).await { - Ok(res) => (res.title, res.description, res.thumbnail_url, res.html), - Err(e) => { - error!("iframely err: {}", e); - (None, None, None, None) - } - }; + let iframely_res_option = fetch_iframely(client, url).await.ok(); // Fetch pictrs thumbnail - let pictrs_hash = match iframely_thumbnail_url { - Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await { - Ok(res) => Some(res.files[0].file.to_owned()), - Err(e) => { - error!("pictrs err: {}", e); - None - } - }, - // Try to generate a small thumbnail if iframely is not supported - None => match fetch_pictrs(client, url).await { - Ok(res) => Some(res.files[0].file.to_owned()), - Err(e) => { - error!("pictrs err: {}", e); - None - } + let pictrs_hash = match &iframely_res_option { + Some(iframely_res) => match &iframely_res.thumbnail_url { + Some(iframely_thumbnail_url) => fetch_pictrs(client, iframely_thumbnail_url) + .await? + .map(|r| r.files[0].file.to_owned()), + // Try to generate a small thumbnail if iframely is not supported + None => fetch_pictrs(client, url) + .await? + .map(|r| r.files[0].file.to_owned()), }, + None => fetch_pictrs(client, url) + .await? + .map(|r| r.files[0].file.to_owned()), }; // The full urls are necessary for federation - let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash { - let url = Url::parse(&format!( - "{}/pictrs/image/{}", - Settings::get().get_protocol_and_hostname(), - pictrs_hash - )); - match url { - Ok(parsed_url) => Some(parsed_url), - Err(e) => { - // This really shouldn't happen unless the settings or hash are malformed - error!("Unexpected error constructing pictrs thumbnail URL: {}", e); - None - } - } - } else { - None - }; + let pictrs_thumbnail = pictrs_hash + .map(|p| { + Url::parse(&format!( + "{}/pictrs/image/{}", + Settings::get().get_protocol_and_hostname(), + p + )) + .ok() + }) + .flatten(); - ( - iframely_title, - iframely_description, - iframely_html, - pictrs_thumbnail, - ) + Ok((iframely_res_option, pictrs_thumbnail)) } - None => (None, None, None, None), + None => Ok((None, None)), } } diff --git a/crates/utils/src/settings/defaults.rs b/crates/utils/src/settings/defaults.rs deleted file mode 100644 index 1333ebe3d..000000000 --- a/crates/utils/src/settings/defaults.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::settings::{CaptchaConfig, DatabaseConfig, FederationConfig, RateLimitConfig, Settings}; -use std::net::{IpAddr, Ipv4Addr}; - -impl Default for Settings { - fn default() -> Self { - Self { - database: Some(DatabaseConfig::default()), - rate_limit: Some(RateLimitConfig::default()), - federation: Some(FederationConfig::default()), - captcha: Some(CaptchaConfig::default()), - email: None, - setup: None, - hostname: None, - bind: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), - port: Some(8536), - tls_enabled: Some(true), - jwt_secret: Some("changeme".into()), - pictrs_url: Some("http://pictrs:8080".into()), - iframely_url: Some("http://iframely".into()), - additional_slurs: None, - actor_name_max_length: Some(20), - } - } -} - -impl Default for DatabaseConfig { - fn default() -> Self { - Self { - user: Some("lemmy".to_string()), - password: "password".into(), - host: "localhost".into(), - port: Some(5432), - database: Some("lemmy".to_string()), - pool_size: Some(5), - } - } -} - -impl Default for CaptchaConfig { - fn default() -> Self { - Self { - enabled: true, - difficulty: "medium".into(), - } - } -} - -impl Default for FederationConfig { - fn default() -> Self { - Self { - enabled: false, - allowed_instances: None, - blocked_instances: None, - strict_allowlist: Some(true), - } - } -} - -impl Default for RateLimitConfig { - fn default() -> Self { - Self { - message: 180, - message_per_second: 60, - post: 6, - post_per_second: 600, - register: 3, - register_per_second: 3600, - image: 6, - image_per_second: 3600, - } - } -} diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index b247bae93..d4776326f 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -1,22 +1,8 @@ -use crate::{ - location_info, - settings::structs::{ - CaptchaConfig, - DatabaseConfig, - EmailConfig, - FederationConfig, - RateLimitConfig, - Settings, - SetupConfig, - }, - LemmyError, -}; +use crate::{location_info, settings::structs::Settings, LemmyError}; use anyhow::{anyhow, Context}; use deser_hjson::from_str; -use merge::Merge; -use std::{env, fs, io::Error, net::IpAddr, sync::RwLock}; +use std::{env, fs, io::Error, sync::RwLock}; -pub mod defaults; pub mod structs; static CONFIG_FILE: &str = "config/config.hjson"; @@ -28,27 +14,18 @@ lazy_static! { impl Settings { /// Reads config from configuration file. - /// Then values from the environment (with prefix LEMMY) are added to the config. - /// And then default values are merged into config. - /// Defaults are controlled by Default trait implemntation for Settings structs. /// /// Note: The env var `LEMMY_DATABASE_URL` is parsed in /// `lemmy_db_queries/src/lib.rs::get_database_url_from_env()` fn init() -> Result { // Read the config file - let mut custom_config = from_str::(&Self::read_config_file()?)?; + let config = from_str::(&Self::read_config_file()?)?; - // Merge with env vars - custom_config.merge(envy::prefixed("LEMMY_").from_env::()?); - - // Merge with default - custom_config.merge(Settings::default()); - - if custom_config.hostname == Settings::default().hostname { + if config.hostname == "unset" { return Err(anyhow!("Hostname variable is not set!").into()); } - Ok(custom_config) + Ok(config) } /// Returns the config as a struct. @@ -57,14 +34,10 @@ impl Settings { } pub fn get_database_url(&self) -> String { - let conf = self.database(); + let conf = &self.database; format!( "postgres://{}:{}@{}:{}/{}", - conf.user(), - conf.password, - conf.host, - conf.port(), - conf.database(), + conf.user, conf.password, conf.host, conf.port, conf.database, ) } @@ -76,22 +49,10 @@ impl Settings { fs::read_to_string(Self::get_config_location()) } - pub fn get_allowed_instances(&self) -> Option> { - self.federation().allowed_instances - } - - pub fn get_blocked_instances(&self) -> Option> { - self.federation().blocked_instances - } - /// Returns either "http" or "https", depending on tls_enabled setting pub fn get_protocol_string(&self) -> &'static str { - if let Some(tls_enabled) = self.tls_enabled { - if tls_enabled { - "https" - } else { - "http" - } + if self.tls_enabled { + "https" } else { "http" } @@ -100,7 +61,7 @@ impl Settings { /// Returns something like `http://localhost` or `https://lemmy.ml`, /// with the correct protocol and hostname. pub fn get_protocol_and_hostname(&self) -> String { - format!("{}://{}", self.get_protocol_string(), self.hostname()) + format!("{}://{}", self.get_protocol_string(), self.hostname) } /// When running the federation test setup in `api_tests/` or `docker/federation`, the `hostname` @@ -109,7 +70,7 @@ impl Settings { pub fn get_hostname_without_port(&self) -> Result { Ok( self - .hostname() + .hostname .split(':') .collect::>() .first() @@ -131,67 +92,4 @@ impl Settings { Ok(Self::read_config_file()?) } - - pub fn hostname(&self) -> String { - self.hostname.to_owned().expect("No hostname given") - } - pub fn bind(&self) -> IpAddr { - self.bind.expect("return bind address") - } - pub fn port(&self) -> u16 { - self - .port - .unwrap_or_else(|| Settings::default().port.expect("missing port")) - } - pub fn tls_enabled(&self) -> bool { - self.tls_enabled.unwrap_or_else(|| { - Settings::default() - .tls_enabled - .expect("missing tls_enabled") - }) - } - pub fn jwt_secret(&self) -> String { - self - .jwt_secret - .to_owned() - .unwrap_or_else(|| Settings::default().jwt_secret.expect("missing jwt_secret")) - } - pub fn pictrs_url(&self) -> String { - self - .pictrs_url - .to_owned() - .unwrap_or_else(|| Settings::default().pictrs_url.expect("missing pictrs_url")) - } - pub fn iframely_url(&self) -> String { - self.iframely_url.to_owned().unwrap_or_else(|| { - Settings::default() - .iframely_url - .expect("missing iframely_url") - }) - } - pub fn actor_name_max_length(&self) -> usize { - self.actor_name_max_length.unwrap_or_else(|| { - Settings::default() - .actor_name_max_length - .expect("missing actor name length") - }) - } - pub fn database(&self) -> DatabaseConfig { - self.database.to_owned().unwrap_or_default() - } - pub fn rate_limit(&self) -> RateLimitConfig { - self.rate_limit.to_owned().unwrap_or_default() - } - pub fn federation(&self) -> FederationConfig { - self.federation.to_owned().unwrap_or_default() - } - pub fn captcha(&self) -> CaptchaConfig { - self.captcha.to_owned().unwrap_or_default() - } - pub fn email(&self) -> Option { - self.email.to_owned() - } - pub fn setup(&self) -> Option { - self.setup.to_owned() - } } diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 1046bd147..46dde9192 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -1,68 +1,65 @@ -use merge::Merge; use serde::Deserialize; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr}; -#[derive(Debug, Deserialize, Clone, Merge)] +#[derive(Debug, Deserialize, Clone, SmartDefault)] +#[serde(default)] pub struct Settings { - pub(crate) database: Option, - pub(crate) rate_limit: Option, - pub(crate) federation: Option, - pub(crate) hostname: Option, - pub(crate) bind: Option, - pub(crate) port: Option, - pub(crate) tls_enabled: Option, - pub(crate) jwt_secret: Option, - pub(crate) pictrs_url: Option, - pub(crate) iframely_url: Option, - pub(crate) captcha: Option, - pub(crate) email: Option, - pub(crate) setup: Option, - pub(crate) additional_slurs: Option, - pub(crate) actor_name_max_length: Option, + #[serde(default)] + pub database: DatabaseConfig, + #[default(Some(RateLimitConfig::default()))] + pub rate_limit: Option, + #[default(FederationConfig::default())] + pub federation: FederationConfig, + #[default(CaptchaConfig::default())] + pub captcha: CaptchaConfig, + #[default(None)] + pub email: Option, + #[default(None)] + pub setup: Option, + #[default("unset")] + pub hostname: String, + #[default(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))] + pub bind: IpAddr, + #[default(8536)] + pub port: u16, + #[default(true)] + pub tls_enabled: bool, + #[default("changeme")] + pub jwt_secret: String, + #[default(None)] + pub pictrs_url: Option, + #[default(None)] + pub iframely_url: Option, + #[default(None)] + pub additional_slurs: Option, + #[default(20)] + pub actor_name_max_length: usize, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, SmartDefault)] +#[serde(default)] pub struct CaptchaConfig { + #[default(false)] pub enabled: bool, + #[default("medium")] pub difficulty: String, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, SmartDefault)] +#[serde(default)] pub struct DatabaseConfig { - pub(super) user: Option, + #[default("lemmy")] + pub(super) user: String, + #[default("password")] pub password: String, + #[default("localhost")] pub host: String, - pub(super) port: Option, - pub(super) database: Option, - pub(super) pool_size: Option, -} - -impl DatabaseConfig { - pub fn user(&self) -> String { - self - .user - .to_owned() - .unwrap_or_else(|| DatabaseConfig::default().user.expect("missing user")) - } - pub fn port(&self) -> i32 { - self - .port - .unwrap_or_else(|| DatabaseConfig::default().port.expect("missing port")) - } - pub fn database(&self) -> String { - self.database.to_owned().unwrap_or_else(|| { - DatabaseConfig::default() - .database - .expect("missing database") - }) - } - pub fn pool_size(&self) -> u32 { - self.pool_size.unwrap_or_else(|| { - DatabaseConfig::default() - .pool_size - .expect("missing pool_size") - }) - } + #[default(5432)] + pub(super) port: i32, + #[default("lemmy")] + pub(super) database: String, + #[default(5)] + pub pool_size: u32, } #[derive(Debug, Deserialize, Clone)] @@ -74,23 +71,37 @@ pub struct EmailConfig { pub use_tls: bool, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, SmartDefault)] +#[serde(default)] pub struct FederationConfig { + #[default(false)] pub enabled: bool, + #[default(None)] pub allowed_instances: Option>, + #[default(None)] pub blocked_instances: Option>, - pub strict_allowlist: Option, + #[default(true)] + pub strict_allowlist: bool, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, SmartDefault)] +#[serde(default)] pub struct RateLimitConfig { + #[default(180)] pub message: i32, + #[default(60)] pub message_per_second: i32, + #[default(6)] pub post: i32, + #[default(600)] pub post_per_second: i32, + #[default(3)] pub register: i32, + #[default(3600)] pub register_per_second: i32, + #[default(6)] pub image: i32, + #[default(3600)] pub image_per_second: i32, } diff --git a/crates/utils/src/utils.rs b/crates/utils/src/utils.rs index d6e1f25d4..1254aff0a 100644 --- a/crates/utils/src/utils.rs +++ b/crates/utils/src/utils.rs @@ -97,7 +97,7 @@ pub struct MentionData { impl MentionData { pub fn is_local(&self) -> bool { - Settings::get().hostname().eq(&self.domain) + Settings::get().hostname.eq(&self.domain) } pub fn full_name(&self) -> String { format!("@{}@{}", &self.name, &self.domain) @@ -116,7 +116,7 @@ pub fn scrape_text_for_mentions(text: &str) -> Vec { } pub fn is_valid_actor_name(name: &str) -> bool { - name.chars().count() <= Settings::get().actor_name_max_length() + name.chars().count() <= Settings::get().actor_name_max_length && VALID_ACTOR_NAME_REGEX.is_match(name) } @@ -125,7 +125,7 @@ pub fn is_valid_display_name(name: &str) -> bool { !name.starts_with('@') && !name.starts_with('\u{200b}') && name.chars().count() >= 3 - && name.chars().count() <= Settings::get().actor_name_max_length() + && name.chars().count() <= Settings::get().actor_name_max_length } pub fn is_valid_matrix_id(matrix_id: &str) -> bool { diff --git a/src/main.rs b/src/main.rs index 8076fb365..6b4ac3697 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ async fn main() -> Result<(), LemmyError> { }; let manager = ConnectionManager::::new(&db_url); let pool = Pool::builder() - .max_size(settings.database().pool_size()) + .max_size(settings.database.pool_size) .build(manager) .unwrap_or_else(|_| panic!("Error connecting to {}", db_url)); @@ -62,8 +62,7 @@ async fn main() -> Result<(), LemmyError> { println!( "Starting http server at {}:{}", - settings.bind(), - settings.port() + settings.bind, settings.port ); let activity_queue = create_activity_queue(); @@ -97,7 +96,7 @@ async fn main() -> Result<(), LemmyError> { .configure(nodeinfo::config) .configure(webfinger::config) }) - .bind((settings.bind(), settings.port()))? + .bind((settings.bind, settings.port))? .run() .await?;