Simplify config using macros (#1686)

Co-authored-by: Felix Ableitner <me@nutomic.com>
This commit is contained in:
Dessalines 2021-08-04 17:13:51 -04:00 committed by GitHub
parent b8d7f00d58
commit 7b8cbbba85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 230 additions and 451 deletions

69
Cargo.lock generated
View File

@ -1001,15 +1001,6 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "envy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.1" version = "2.5.1"
@ -1882,7 +1873,6 @@ dependencies = [
"comrak", "comrak",
"deser-hjson", "deser-hjson",
"diesel", "diesel",
"envy",
"futures", "futures",
"http", "http",
"itertools", "itertools",
@ -1890,7 +1880,6 @@ dependencies = [
"lazy_static", "lazy_static",
"lettre", "lettre",
"log", "log",
"merge",
"openssl", "openssl",
"percent-encoding", "percent-encoding",
"rand 0.8.4", "rand 0.8.4",
@ -1898,6 +1887,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"smart-default",
"strum", "strum",
"strum_macros", "strum_macros",
"thiserror", "thiserror",
@ -2037,28 +2027,6 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "migrations_internals" name = "migrations_internals"
version = "1.4.1" version = "1.4.1"
@ -2444,30 +2412,6 @@ dependencies = [
"vcpkg", "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]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.19" version = "0.5.19"
@ -3021,6 +2965,17 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 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]] [[package]]
name = "socket2" name = "socket2"
version = "0.4.0" version = "0.4.0"

View File

@ -115,7 +115,7 @@ impl Perform for GetCaptcha {
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>, _websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError> { ) -> Result<Self::Response, LemmyError> {
let captcha_settings = Settings::get().captcha(); let captcha_settings = Settings::get().captcha;
if !captcha_settings.enabled { if !captcha_settings.enabled {
return Ok(GetCaptchaResponse { ok: None }); return Ok(GetCaptchaResponse { ok: None });

View File

@ -198,7 +198,7 @@ pub fn send_email_to_user(
let subject = &format!( let subject = &format!(
"{} - {} {}", "{} - {} {}",
subject_text, subject_text,
Settings::get().hostname(), Settings::get().hostname,
local_user_view.person.name, local_user_view.person.name,
); );
let html = &format!( let html = &format!(
@ -386,14 +386,14 @@ pub async fn collect_moderated_communities(
pub async fn build_federated_instances( pub async fn build_federated_instances(
pool: &DbPool, pool: &DbPool,
) -> Result<Option<FederatedInstances>, LemmyError> { ) -> Result<Option<FederatedInstances>, LemmyError> {
if Settings::get().federation().enabled { if Settings::get().federation.enabled {
let distinct_communities = blocking(pool, move |conn| { let distinct_communities = blocking(pool, move |conn| {
Community::distinct_federated_communities(conn) Community::distinct_federated_communities(conn)
}) })
.await??; .await??;
let allowed = Settings::get().get_allowed_instances(); let allowed = Settings::get().federation.allowed_instances;
let blocked = Settings::get().get_blocked_instances(); let blocked = Settings::get().federation.blocked_instances;
let mut linked = distinct_communities let mut linked = distinct_communities
.iter() .iter()
@ -405,7 +405,7 @@ pub async fn build_federated_instances(
} }
if let Some(blocked) = blocked.as_ref() { 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 // Sort and remove dupes

View File

@ -52,8 +52,11 @@ impl PerformCrud for CreatePost {
// Fetch Iframely and pictrs cached image // Fetch Iframely and pictrs cached image
let data_url = data.url.as_ref(); let data_url = data.url.as_ref();
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_response, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(context.client(), data_url).await; 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 { let post_form = PostForm {
name: data.name.trim().to_owned(), name: data.name.trim().to_owned(),
@ -62,9 +65,9 @@ impl PerformCrud for CreatePost {
community_id: data.community_id, community_id: data.community_id,
creator_id: local_user_view.person.id, creator_id: local_user_view.person.id,
nsfw: data.nsfw, nsfw: data.nsfw,
embed_title: iframely_title, embed_title,
embed_description: iframely_description, embed_description,
embed_html: iframely_html, embed_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()), thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
..PostForm::default() ..PostForm::default()
}; };

View File

@ -52,8 +52,11 @@ impl PerformCrud for EditPost {
// Fetch Iframely and Pictrs cached image // Fetch Iframely and Pictrs cached image
let data_url = data.url.as_ref(); let data_url = data.url.as_ref();
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_response, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(context.client(), data_url).await; 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 { let post_form = PostForm {
creator_id: orig_post.creator_id.to_owned(), creator_id: orig_post.creator_id.to_owned(),
@ -63,9 +66,9 @@ impl PerformCrud for EditPost {
body: data.body.to_owned(), body: data.body.to_owned(),
nsfw: data.nsfw, nsfw: data.nsfw,
updated: Some(naive_now()), updated: Some(naive_now()),
embed_title: iframely_title, embed_title,
embed_description: iframely_description, embed_description,
embed_html: iframely_html, embed_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()), thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
..PostForm::default() ..PostForm::default()
}; };

View File

@ -28,7 +28,7 @@ impl PerformCrud for GetSite {
Ok(site_view) => Some(site_view), Ok(site_view) => Some(site_view),
// If the site isn't created yet, check the setup // If the site isn't created yet, check the setup
Err(_) => { Err(_) => {
if let Some(setup) = Settings::get().setup().as_ref() { if let Some(setup) = Settings::get().setup.as_ref() {
let register = Register { let register = Register {
username: setup.admin_username.to_owned(), username: setup.admin_username.to_owned(),
email: setup.admin_email.to_owned(), email: setup.admin_email.to_owned(),

View File

@ -69,7 +69,7 @@ impl PerformCrud for Register {
.await??; .await??;
// If its not the admin, check the captcha // 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 let check = context
.chat_server() .chat_server()
.send(CheckCaptcha { .send(CheckCaptcha {

View File

@ -50,7 +50,7 @@ async fn list_community_follower_inboxes(
.iter() .iter()
.flatten() .flatten()
.unique() .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()) .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok())
.map(|inbox| inbox.to_owned()) .map(|inbox| inbox.to_owned())
.collect(), .collect(),

View File

@ -83,7 +83,7 @@ where
.iter() .iter()
.flatten() .flatten()
.unique() .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()) .filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok())
.map(|inbox| inbox.to_owned()) .map(|inbox| inbox.to_owned())
.collect(); .collect();
@ -170,7 +170,7 @@ pub(crate) async fn send_activity_new<T>(
where where
T: Serialize, T: Serialize,
{ {
if !Settings::get().federation().enabled || inboxes.is_empty() { if !Settings::get().federation.enabled || inboxes.is_empty() {
return Ok(()); return Ok(());
} }
@ -230,7 +230,7 @@ where
Kind: Serialize, Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static, <T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{ {
if !Settings::get().federation().enabled || inboxes.is_empty() { if !Settings::get().federation.enabled || inboxes.is_empty() {
return Ok(()); return Ok(());
} }

View File

@ -175,7 +175,7 @@ fn assert_activity_not_local<T: Debug + ActivityHandler>(activity: &T) -> Result
.domain() .domain()
.context(location_info!())?; .context(location_info!())?;
if activity_domain == Settings::get().hostname() { if activity_domain == Settings::get().hostname {
return Err( return Err(
anyhow!( anyhow!(
"Error: received activity which was sent by local instance: {:?}", "Error: received activity which was sent by local instance: {:?}",

View File

@ -25,8 +25,8 @@ static APUB_JSON_CONTENT_TYPE_LONG: &str =
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""; "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
if Settings::get().federation().enabled { if Settings::get().federation.enabled {
println!("federation enabled, host is {}", Settings::get().hostname()); println!("federation enabled, host is {}", Settings::get().hostname);
let digest_verifier = VerifyDigest::new(Sha256::new()); let digest_verifier = VerifyDigest::new(Sha256::new());
let header_guard_accept = guard::Any(guard::Header("Accept", APUB_JSON_CONTENT_TYPE)) let header_guard_accept = guard::Any(guard::Header("Accept", APUB_JSON_CONTENT_TYPE))

View File

@ -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 domain = apub_id.domain().context(location_info!())?.to_string();
let local_instance = settings.get_hostname_without_port()?; let local_instance = settings.get_hostname_without_port()?;
if !settings.federation().enabled { if !settings.federation.enabled {
return if domain == local_instance { return if domain == local_instance {
Ok(()) Ok(())
} else { } 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 // TODO: might be good to put the part above in one method, and below in another
// (which only gets called in apub::objects) // (which only gets called in apub::objects)
// -> no that doesnt make sense, we still need the code below for blocklist and strict allowlist // -> 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) { if blocked.contains(&domain) {
return Err(anyhow!("{} is in federation blocklist", domain).into()); 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. // Only check allowlist if this is a community, or strict allowlist is enabled.
let strict_allowlist = Settings::get() let strict_allowlist = Settings::get().federation.strict_allowlist;
.federation()
.strict_allowlist
.unwrap_or(true);
if use_strict_allowlist || strict_allowlist { if use_strict_allowlist || strict_allowlist {
// need to allow this explicitly because apub receive might contain objects from our local // need to allow this explicitly because apub receive might contain objects from our local
// instance. // instance.

View File

@ -193,7 +193,7 @@ where
let domain = object_id.domain().context(location_info!())?; let domain = object_id.domain().context(location_info!())?;
// if its a local object, return it directly from the database // 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| { let object = blocking(context.pool(), move |conn| {
To::read_from_apub_id(conn, &object_id.into()) To::read_from_apub_id(conn, &object_id.into())
}) })

View File

@ -109,7 +109,7 @@ impl FromApub for DbPerson {
) -> Result<DbPerson, LemmyError> { ) -> Result<DbPerson, LemmyError> {
let person_id = person.id_unchecked().context(location_info!())?.to_owned(); let person_id = person.id_unchecked().context(location_info!())?.to_owned();
let domain = person_id.domain().context(location_info!())?; 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| { let person = blocking(context.pool(), move |conn| {
DbPerson::read_from_apub_id(conn, &person_id.into()) DbPerson::read_from_apub_id(conn, &person_id.into())
}) })

View File

@ -178,12 +178,14 @@ impl FromApub for Post {
let community = extract_community(&page.to, context, request_counter).await?; let community = extract_community(&page.to, context, request_counter).await?;
let thumbnail_url: Option<Url> = page.image.clone().map(|i| i.url); let thumbnail_url: Option<Url> = page.image.clone().map(|i| i.url);
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_response, pictrs_thumbnail) = if let Some(url) = &page.url {
if let Some(url) = &page.url { fetch_iframely_and_pictrs_data(context.client(), Some(url)).await?
fetch_iframely_and_pictrs_data(context.client(), Some(url)).await } else {
} else { (None, thumbnail_url)
(None, None, 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 body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content));
let form = PostForm { let form = PostForm {
@ -199,9 +201,9 @@ impl FromApub for Post {
deleted: None, deleted: None,
nsfw: page.sensitive, nsfw: page.sensitive,
stickied: page.stickied, stickied: page.stickied,
embed_title: iframely_title, embed_title,
embed_description: iframely_description, embed_description,
embed_html: iframely_html, embed_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()), thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(page.id.clone().into()), ap_id: Some(page.id.clone().into()),
local: Some(false), local: Some(false),

View File

@ -1,6 +1,7 @@
use actix_web::{body::BodyStream, http::StatusCode, web::Data, *}; use actix_web::{body::BodyStream, http::StatusCode, web::Data, *};
use anyhow::anyhow;
use awc::Client; 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 serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
@ -54,10 +55,7 @@ async fn upload(
return Ok(HttpResponse::Unauthorized().finish()); return Ok(HttpResponse::Unauthorized().finish());
}; };
let mut client_req = client.request_from( let mut client_req = client.request_from(format!("{}/image", pictrs_url()?), req.head());
format!("{}/image", Settings::get().pictrs_url()),
req.head(),
);
if let Some(addr) = req.head().peer_addr { if let Some(addr) = req.head().peer_addr {
client_req = client_req.insert_header(("X-Forwarded-For", addr.to_string())) 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 // If there are no query params, the URL is original
let url = if params.format.is_none() && params.thumbnail.is_none() { 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 { } else {
// Use jpg as a default when none is given // Use jpg as a default when none is given
let format = params.format.unwrap_or_else(|| "jpg".to_string()); let format = params.format.unwrap_or_else(|| "jpg".to_string());
let mut url = format!( let mut url = format!("{}/image/process.{}?src={}", pictrs_url()?, format, name,);
"{}/image/process.{}?src={}",
Settings::get().pictrs_url(),
format,
name,
);
if let Some(size) = params.thumbnail { if let Some(size) = params.thumbnail {
url = format!("{}&thumbnail={}", url, size,); url = format!("{}&thumbnail={}", url, size,);
@ -141,12 +134,7 @@ async fn delete(
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let (token, file) = components.into_inner(); let (token, file) = components.into_inner();
let url = format!( let url = format!("{}/image/delete/{}/{}", pictrs_url()?, &token, &file);
"{}/image/delete/{}/{}",
Settings::get().pictrs_url(),
&token,
&file
);
let mut client_req = client.request_from(url, req.head()); 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))) Ok(HttpResponse::build(res.status()).body(BodyStream::new(res)))
} }
fn pictrs_url() -> Result<String, LemmyError> {
Settings::get()
.pictrs_url
.ok_or_else(|| anyhow!("images_disabled").into())
}

View File

@ -31,7 +31,7 @@ async fn node_info(context: web::Data<LemmyContext>) -> Result<HttpResponse, Err
.await? .await?
.map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?; .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?;
let protocols = if Settings::get().federation().enabled { let protocols = if Settings::get().federation.enabled {
vec!["activitypub".to_string()] vec!["activitypub".to_string()]
} else { } else {
vec![] vec![]

View File

@ -18,7 +18,7 @@ struct Params {
} }
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
if Settings::get().federation().enabled { if Settings::get().federation.enabled {
cfg.route( cfg.route(
".well-known/webfinger", ".well-known/webfinger",
web::get().to(get_webfinger_response), web::get().to(get_webfinger_response),

View File

@ -35,5 +35,4 @@ diesel = "1.4.7"
http = "0.2.4" http = "0.2.4"
jsonwebtoken = "7.2.0" jsonwebtoken = "7.2.0"
deser-hjson = "1.0.1" deser-hjson = "1.0.1"
merge = "0.1.0" smart-default = "0.6.0"
envy = "0.4.2"

View File

@ -22,7 +22,7 @@ impl Claims {
}; };
decode::<Claims>( decode::<Claims>(
jwt, jwt,
&DecodingKey::from_secret(Settings::get().jwt_secret().as_ref()), &DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
&v, &v,
) )
} }
@ -30,13 +30,13 @@ impl Claims {
pub fn jwt(local_user_id: i32) -> Result<Jwt, jsonwebtoken::errors::Error> { pub fn jwt(local_user_id: i32) -> Result<Jwt, jsonwebtoken::errors::Error> {
let my_claims = Claims { let my_claims = Claims {
sub: local_user_id, sub: local_user_id,
iss: Settings::get().hostname(), iss: Settings::get().hostname,
iat: Utc::now().timestamp(), iat: Utc::now().timestamp(),
}; };
encode( encode(
&Header::default(), &Header::default(),
&my_claims, &my_claims,
&EncodingKey::from_secret(Settings::get().jwt_secret().as_ref()), &EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
) )
} }
} }

View File

@ -19,8 +19,8 @@ pub fn send_email(
to_username: &str, to_username: &str,
html: &str, html: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let email_config = Settings::get().email().ok_or("no_email_setup")?; let email_config = Settings::get().email.ok_or("no_email_setup")?;
let domain = Settings::get().hostname(); let domain = Settings::get().hostname;
let (smtp_server, smtp_port) = { let (smtp_server, smtp_port) = {
let email_and_port = email_config.smtp_server.split(':').collect::<Vec<&str>>(); let email_and_port = email_config.smtp_server.split(':').collect::<Vec<&str>>();

View File

@ -2,6 +2,8 @@
extern crate lazy_static; extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate strum_macros; extern crate strum_macros;
#[macro_use]
extern crate smart_default;
pub mod apub; pub mod apub;
pub mod claims; pub mod claims;
@ -90,12 +92,12 @@ impl actix_web::error::ResponseError for LemmyError {
lazy_static! { lazy_static! {
pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!( pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
"^group:([a-z0-9_]{{3,}})@{}$", "^group:([a-z0-9_]{{3,}})@{}$",
Settings::get().hostname() Settings::get().hostname
)) ))
.expect("compile webfinger regex"); .expect("compile webfinger regex");
pub static ref WEBFINGER_USERNAME_REGEX: Regex = Regex::new(&format!( pub static ref WEBFINGER_USERNAME_REGEX: Regex = Regex::new(&format!(
"^acct:([a-z0-9_]{{3,}})@{}$", "^acct:([a-z0-9_]{{3,}})@{}$",
Settings::get().hostname() Settings::get().hostname
)) ))
.expect("compile webfinger regex"); .expect("compile webfinger regex");
} }

View File

@ -71,7 +71,9 @@ impl RateLimited {
{ {
// Does not need to be blocking because the RwLock in settings never held across await points, // 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 // 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 // before
{ {

View File

@ -48,26 +48,30 @@ where
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub(crate) struct IframelyResponse { pub struct IframelyResponse {
title: Option<String>, pub title: Option<String>,
description: Option<String>, pub description: Option<String>,
thumbnail_url: Option<Url>, thumbnail_url: Option<Url>,
html: Option<String>, pub html: Option<String>,
} }
pub(crate) async fn fetch_iframely( pub(crate) async fn fetch_iframely(
client: &Client, client: &Client,
url: &Url, url: &Url,
) -> Result<IframelyResponse, LemmyError> { ) -> Result<IframelyResponse, LemmyError> {
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 let res: IframelyResponse = response
.json() .json()
.await .await
.map_err(|e| RecvError(e.to_string()))?; .map_err(|e| RecvError(e.to_string()))?;
Ok(res) Ok(res)
} else {
Err(anyhow!("Missing Iframely URL in config.").into())
}
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
@ -85,91 +89,73 @@ pub(crate) struct PictrsFile {
pub(crate) async fn fetch_pictrs( pub(crate) async fn fetch_pictrs(
client: &Client, client: &Client,
image_url: &Url, image_url: &Url,
) -> Result<PictrsResponse, LemmyError> { ) -> Result<Option<PictrsResponse>, LemmyError> {
is_image_content_type(client, image_url).await?; if let Some(pictrs_url) = Settings::get().pictrs_url {
is_image_content_type(client, image_url).await?;
let fetch_url = format!( let fetch_url = format!(
"{}/image/download?url={}", "{}/image/download?url={}",
Settings::get().pictrs_url(), pictrs_url,
utf8_percent_encode(image_url.as_str(), NON_ALPHANUMERIC) // TODO this might not be needed 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 let response: PictrsResponse = response
.json() .json()
.await .await
.map_err(|e| RecvError(e.to_string()))?; .map_err(|e| RecvError(e.to_string()))?;
if response.msg == "ok" { if response.msg == "ok" {
Ok(response) Ok(Some(response))
} else {
Err(anyhow!("{}", &response.msg).into())
}
} else { } else {
Err(anyhow!("{}", &response.msg).into()) Ok(None)
} }
} }
pub async fn fetch_iframely_and_pictrs_data( pub async fn fetch_iframely_and_pictrs_data(
client: &Client, client: &Client,
url: Option<&Url>, url: Option<&Url>,
) -> (Option<String>, Option<String>, Option<String>, Option<Url>) { ) -> Result<(Option<IframelyResponse>, Option<Url>), LemmyError> {
match &url { match &url {
Some(url) => { Some(url) => {
// Fetch iframely data // Fetch iframely data
let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) = let iframely_res_option = fetch_iframely(client, url).await.ok();
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)
}
};
// Fetch pictrs thumbnail // Fetch pictrs thumbnail
let pictrs_hash = match iframely_thumbnail_url { let pictrs_hash = match &iframely_res_option {
Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await { Some(iframely_res) => match &iframely_res.thumbnail_url {
Ok(res) => Some(res.files[0].file.to_owned()), Some(iframely_thumbnail_url) => fetch_pictrs(client, iframely_thumbnail_url)
Err(e) => { .await?
error!("pictrs err: {}", e); .map(|r| r.files[0].file.to_owned()),
None // Try to generate a small thumbnail if iframely is not supported
} None => fetch_pictrs(client, url)
}, .await?
// Try to generate a small thumbnail if iframely is not supported .map(|r| r.files[0].file.to_owned()),
None => match fetch_pictrs(client, url).await {
Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => {
error!("pictrs err: {}", e);
None
}
}, },
None => fetch_pictrs(client, url)
.await?
.map(|r| r.files[0].file.to_owned()),
}; };
// The full urls are necessary for federation // The full urls are necessary for federation
let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash { let pictrs_thumbnail = pictrs_hash
let url = Url::parse(&format!( .map(|p| {
"{}/pictrs/image/{}", Url::parse(&format!(
Settings::get().get_protocol_and_hostname(), "{}/pictrs/image/{}",
pictrs_hash Settings::get().get_protocol_and_hostname(),
)); p
match url { ))
Ok(parsed_url) => Some(parsed_url), .ok()
Err(e) => { })
// This really shouldn't happen unless the settings or hash are malformed .flatten();
error!("Unexpected error constructing pictrs thumbnail URL: {}", e);
None
}
}
} else {
None
};
( Ok((iframely_res_option, pictrs_thumbnail))
iframely_title,
iframely_description,
iframely_html,
pictrs_thumbnail,
)
} }
None => (None, None, None, None), None => Ok((None, None)),
} }
} }

View File

@ -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,
}
}
}

View File

@ -1,22 +1,8 @@
use crate::{ use crate::{location_info, settings::structs::Settings, LemmyError};
location_info,
settings::structs::{
CaptchaConfig,
DatabaseConfig,
EmailConfig,
FederationConfig,
RateLimitConfig,
Settings,
SetupConfig,
},
LemmyError,
};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use deser_hjson::from_str; use deser_hjson::from_str;
use merge::Merge; use std::{env, fs, io::Error, sync::RwLock};
use std::{env, fs, io::Error, net::IpAddr, sync::RwLock};
pub mod defaults;
pub mod structs; pub mod structs;
static CONFIG_FILE: &str = "config/config.hjson"; static CONFIG_FILE: &str = "config/config.hjson";
@ -28,27 +14,18 @@ lazy_static! {
impl Settings { impl Settings {
/// Reads config from configuration file. /// 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 /// Note: The env var `LEMMY_DATABASE_URL` is parsed in
/// `lemmy_db_queries/src/lib.rs::get_database_url_from_env()` /// `lemmy_db_queries/src/lib.rs::get_database_url_from_env()`
fn init() -> Result<Self, LemmyError> { fn init() -> Result<Self, LemmyError> {
// Read the config file // Read the config file
let mut custom_config = from_str::<Settings>(&Self::read_config_file()?)?; let config = from_str::<Settings>(&Self::read_config_file()?)?;
// Merge with env vars if config.hostname == "unset" {
custom_config.merge(envy::prefixed("LEMMY_").from_env::<Settings>()?);
// Merge with default
custom_config.merge(Settings::default());
if custom_config.hostname == Settings::default().hostname {
return Err(anyhow!("Hostname variable is not set!").into()); return Err(anyhow!("Hostname variable is not set!").into());
} }
Ok(custom_config) Ok(config)
} }
/// Returns the config as a struct. /// Returns the config as a struct.
@ -57,14 +34,10 @@ impl Settings {
} }
pub fn get_database_url(&self) -> String { pub fn get_database_url(&self) -> String {
let conf = self.database(); let conf = &self.database;
format!( format!(
"postgres://{}:{}@{}:{}/{}", "postgres://{}:{}@{}:{}/{}",
conf.user(), conf.user, conf.password, conf.host, conf.port, conf.database,
conf.password,
conf.host,
conf.port(),
conf.database(),
) )
} }
@ -76,22 +49,10 @@ impl Settings {
fs::read_to_string(Self::get_config_location()) fs::read_to_string(Self::get_config_location())
} }
pub fn get_allowed_instances(&self) -> Option<Vec<String>> {
self.federation().allowed_instances
}
pub fn get_blocked_instances(&self) -> Option<Vec<String>> {
self.federation().blocked_instances
}
/// Returns either "http" or "https", depending on tls_enabled setting /// Returns either "http" or "https", depending on tls_enabled setting
pub fn get_protocol_string(&self) -> &'static str { pub fn get_protocol_string(&self) -> &'static str {
if let Some(tls_enabled) = self.tls_enabled { if self.tls_enabled {
if tls_enabled { "https"
"https"
} else {
"http"
}
} else { } else {
"http" "http"
} }
@ -100,7 +61,7 @@ impl Settings {
/// Returns something like `http://localhost` or `https://lemmy.ml`, /// Returns something like `http://localhost` or `https://lemmy.ml`,
/// with the correct protocol and hostname. /// with the correct protocol and hostname.
pub fn get_protocol_and_hostname(&self) -> String { 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` /// 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<String, anyhow::Error> { pub fn get_hostname_without_port(&self) -> Result<String, anyhow::Error> {
Ok( Ok(
self self
.hostname() .hostname
.split(':') .split(':')
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.first() .first()
@ -131,67 +92,4 @@ impl Settings {
Ok(Self::read_config_file()?) 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<EmailConfig> {
self.email.to_owned()
}
pub fn setup(&self) -> Option<SetupConfig> {
self.setup.to_owned()
}
} }

View File

@ -1,68 +1,65 @@
use merge::Merge;
use serde::Deserialize; 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 struct Settings {
pub(crate) database: Option<DatabaseConfig>, #[serde(default)]
pub(crate) rate_limit: Option<RateLimitConfig>, pub database: DatabaseConfig,
pub(crate) federation: Option<FederationConfig>, #[default(Some(RateLimitConfig::default()))]
pub(crate) hostname: Option<String>, pub rate_limit: Option<RateLimitConfig>,
pub(crate) bind: Option<IpAddr>, #[default(FederationConfig::default())]
pub(crate) port: Option<u16>, pub federation: FederationConfig,
pub(crate) tls_enabled: Option<bool>, #[default(CaptchaConfig::default())]
pub(crate) jwt_secret: Option<String>, pub captcha: CaptchaConfig,
pub(crate) pictrs_url: Option<String>, #[default(None)]
pub(crate) iframely_url: Option<String>, pub email: Option<EmailConfig>,
pub(crate) captcha: Option<CaptchaConfig>, #[default(None)]
pub(crate) email: Option<EmailConfig>, pub setup: Option<SetupConfig>,
pub(crate) setup: Option<SetupConfig>, #[default("unset")]
pub(crate) additional_slurs: Option<String>, pub hostname: String,
pub(crate) actor_name_max_length: Option<usize>, #[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<String>,
#[default(None)]
pub iframely_url: Option<String>,
#[default(None)]
pub additional_slurs: Option<String>,
#[default(20)]
pub actor_name_max_length: usize,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone, SmartDefault)]
#[serde(default)]
pub struct CaptchaConfig { pub struct CaptchaConfig {
#[default(false)]
pub enabled: bool, pub enabled: bool,
#[default("medium")]
pub difficulty: String, pub difficulty: String,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone, SmartDefault)]
#[serde(default)]
pub struct DatabaseConfig { pub struct DatabaseConfig {
pub(super) user: Option<String>, #[default("lemmy")]
pub(super) user: String,
#[default("password")]
pub password: String, pub password: String,
#[default("localhost")]
pub host: String, pub host: String,
pub(super) port: Option<i32>, #[default(5432)]
pub(super) database: Option<String>, pub(super) port: i32,
pub(super) pool_size: Option<u32>, #[default("lemmy")]
} pub(super) database: String,
#[default(5)]
impl DatabaseConfig { pub pool_size: u32,
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")
})
}
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@ -74,23 +71,37 @@ pub struct EmailConfig {
pub use_tls: bool, pub use_tls: bool,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone, SmartDefault)]
#[serde(default)]
pub struct FederationConfig { pub struct FederationConfig {
#[default(false)]
pub enabled: bool, pub enabled: bool,
#[default(None)]
pub allowed_instances: Option<Vec<String>>, pub allowed_instances: Option<Vec<String>>,
#[default(None)]
pub blocked_instances: Option<Vec<String>>, pub blocked_instances: Option<Vec<String>>,
pub strict_allowlist: Option<bool>, #[default(true)]
pub strict_allowlist: bool,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone, SmartDefault)]
#[serde(default)]
pub struct RateLimitConfig { pub struct RateLimitConfig {
#[default(180)]
pub message: i32, pub message: i32,
#[default(60)]
pub message_per_second: i32, pub message_per_second: i32,
#[default(6)]
pub post: i32, pub post: i32,
#[default(600)]
pub post_per_second: i32, pub post_per_second: i32,
#[default(3)]
pub register: i32, pub register: i32,
#[default(3600)]
pub register_per_second: i32, pub register_per_second: i32,
#[default(6)]
pub image: i32, pub image: i32,
#[default(3600)]
pub image_per_second: i32, pub image_per_second: i32,
} }

View File

@ -97,7 +97,7 @@ pub struct MentionData {
impl MentionData { impl MentionData {
pub fn is_local(&self) -> bool { pub fn is_local(&self) -> bool {
Settings::get().hostname().eq(&self.domain) Settings::get().hostname.eq(&self.domain)
} }
pub fn full_name(&self) -> String { pub fn full_name(&self) -> String {
format!("@{}@{}", &self.name, &self.domain) format!("@{}@{}", &self.name, &self.domain)
@ -116,7 +116,7 @@ pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
} }
pub fn is_valid_actor_name(name: &str) -> bool { 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) && 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('@')
&& !name.starts_with('\u{200b}') && !name.starts_with('\u{200b}')
&& name.chars().count() >= 3 && 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 { pub fn is_valid_matrix_id(matrix_id: &str) -> bool {

View File

@ -38,7 +38,7 @@ async fn main() -> Result<(), LemmyError> {
}; };
let manager = ConnectionManager::<PgConnection>::new(&db_url); let manager = ConnectionManager::<PgConnection>::new(&db_url);
let pool = Pool::builder() let pool = Pool::builder()
.max_size(settings.database().pool_size()) .max_size(settings.database.pool_size)
.build(manager) .build(manager)
.unwrap_or_else(|_| panic!("Error connecting to {}", db_url)); .unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
@ -62,8 +62,7 @@ async fn main() -> Result<(), LemmyError> {
println!( println!(
"Starting http server at {}:{}", "Starting http server at {}:{}",
settings.bind(), settings.bind, settings.port
settings.port()
); );
let activity_queue = create_activity_queue(); let activity_queue = create_activity_queue();
@ -97,7 +96,7 @@ async fn main() -> Result<(), LemmyError> {
.configure(nodeinfo::config) .configure(nodeinfo::config)
.configure(webfinger::config) .configure(webfinger::config)
}) })
.bind((settings.bind(), settings.port()))? .bind((settings.bind, settings.port))?
.run() .run()
.await?; .await?;