385 lines
13 KiB
Rust
385 lines
13 KiB
Rust
use crate::search::TokenizerKind as SearchTokenizer;
|
|
use crate::signups::Strategy as SignupStrategy;
|
|
use crate::smtp::{SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
|
use rocket::config::Limits;
|
|
use rocket::Config as RocketConfig;
|
|
use std::collections::HashSet;
|
|
use std::env::{self, var};
|
|
|
|
#[cfg(not(test))]
|
|
const DB_NAME: &str = "plume";
|
|
#[cfg(test)]
|
|
const DB_NAME: &str = "plume_tests";
|
|
|
|
pub struct Config {
|
|
pub base_url: String,
|
|
pub database_url: String,
|
|
pub db_name: &'static str,
|
|
pub db_max_size: Option<u32>,
|
|
pub db_min_idle: Option<u32>,
|
|
pub signup: SignupStrategy,
|
|
pub search_index: String,
|
|
pub search_tokenizers: SearchTokenizerConfig,
|
|
pub rocket: Result<RocketConfig, InvalidRocketConfig>,
|
|
pub logo: LogoConfig,
|
|
pub default_theme: String,
|
|
pub media_directory: String,
|
|
pub mail: Option<MailConfig>,
|
|
pub ldap: Option<LdapConfig>,
|
|
pub proxy: Option<ProxyConfig>,
|
|
}
|
|
impl Config {
|
|
pub fn proxy(&self) -> Option<&reqwest::Proxy> {
|
|
self.proxy.as_ref().map(|p| &p.proxy)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum InvalidRocketConfig {
|
|
Env,
|
|
Address,
|
|
SecretKey,
|
|
}
|
|
|
|
fn get_rocket_config() -> Result<RocketConfig, InvalidRocketConfig> {
|
|
let mut c = RocketConfig::active().map_err(|_| InvalidRocketConfig::Env)?;
|
|
|
|
let address = var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned());
|
|
let port = var("ROCKET_PORT")
|
|
.ok()
|
|
.map(|s| s.parse::<u16>().unwrap())
|
|
.unwrap_or(7878);
|
|
let secret_key = var("ROCKET_SECRET_KEY").map_err(|_| InvalidRocketConfig::SecretKey)?;
|
|
let form_size = var("FORM_SIZE")
|
|
.unwrap_or_else(|_| "128".to_owned())
|
|
.parse::<u64>()
|
|
.unwrap();
|
|
let activity_size = var("ACTIVITY_SIZE")
|
|
.unwrap_or_else(|_| "1024".to_owned())
|
|
.parse::<u64>()
|
|
.unwrap();
|
|
|
|
c.set_address(address)
|
|
.map_err(|_| InvalidRocketConfig::Address)?;
|
|
c.set_port(port);
|
|
c.set_secret_key(secret_key)
|
|
.map_err(|_| InvalidRocketConfig::SecretKey)?;
|
|
|
|
c.set_limits(
|
|
Limits::new()
|
|
.limit("forms", form_size * 1024)
|
|
.limit("json", activity_size * 1024),
|
|
);
|
|
|
|
Ok(c)
|
|
}
|
|
|
|
pub struct LogoConfig {
|
|
pub main: String,
|
|
pub favicon: String,
|
|
pub other: Vec<Icon>, //url, size, type
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct Icon {
|
|
pub src: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub sizes: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[serde(rename = "type")]
|
|
pub image_type: Option<String>,
|
|
}
|
|
|
|
impl Icon {
|
|
pub fn with_prefix(&self, prefix: &str) -> Icon {
|
|
Icon {
|
|
src: format!("{}/{}", prefix, self.src),
|
|
sizes: self.sizes.clone(),
|
|
image_type: self.image_type.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for LogoConfig {
|
|
fn default() -> Self {
|
|
let to_icon = |(src, sizes, image_type): &(&str, Option<&str>, Option<&str>)| Icon {
|
|
src: str::to_owned(src),
|
|
sizes: sizes.map(str::to_owned),
|
|
image_type: image_type.map(str::to_owned),
|
|
};
|
|
let icons = [
|
|
(
|
|
"icons/trwnh/feather/plumeFeather48.png",
|
|
Some("48x48"),
|
|
Some("image/png"),
|
|
),
|
|
(
|
|
"icons/trwnh/feather/plumeFeather72.png",
|
|
Some("72x72"),
|
|
Some("image/png"),
|
|
),
|
|
(
|
|
"icons/trwnh/feather/plumeFeather96.png",
|
|
Some("96x96"),
|
|
Some("image/png"),
|
|
),
|
|
(
|
|
"icons/trwnh/feather/plumeFeather144.png",
|
|
Some("144x144"),
|
|
Some("image/png"),
|
|
),
|
|
(
|
|
"icons/trwnh/feather/plumeFeather160.png",
|
|
Some("160x160"),
|
|
Some("image/png"),
|
|
),
|
|
(
|
|
"icons/trwnh/feather/plumeFeather192.png",
|
|
Some("192x192"),
|
|
Some("image/png"),
|
|
),
|
|
(
|
|
"icons/trwnh/feather/plumeFeather256.png",
|
|
Some("256x256"),
|
|
Some("image/png"),
|
|
),
|
|
(
|
|
"icons/trwnh/feather/plumeFeather512.png",
|
|
Some("512x512"),
|
|
Some("image/png"),
|
|
),
|
|
("icons/trwnh/feather/plumeFeather.svg", None, None),
|
|
]
|
|
.iter()
|
|
.map(to_icon)
|
|
.collect();
|
|
|
|
let custom_main = var("PLUME_LOGO").ok();
|
|
let custom_favicon = var("PLUME_LOGO_FAVICON")
|
|
.ok()
|
|
.or_else(|| custom_main.clone());
|
|
let other = if let Some(main) = custom_main.clone() {
|
|
let ext = |path: &str| match path.rsplit_once('.').map(|x| x.1) {
|
|
Some("png") => Some("image/png".to_owned()),
|
|
Some("jpg") | Some("jpeg") => Some("image/jpeg".to_owned()),
|
|
Some("svg") => Some("image/svg+xml".to_owned()),
|
|
Some("webp") => Some("image/webp".to_owned()),
|
|
_ => None,
|
|
};
|
|
let mut custom_icons = env::vars()
|
|
.filter_map(|(var, val)| {
|
|
var.strip_prefix("PLUME_LOGO_")
|
|
.map(|size| (size.to_owned(), val))
|
|
})
|
|
.filter_map(|(var, val)| var.parse::<u64>().ok().map(|var| (var, val)))
|
|
.map(|(dim, src)| Icon {
|
|
image_type: ext(&src),
|
|
src,
|
|
sizes: Some(format!("{}x{}", dim, dim)),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
custom_icons.push(Icon {
|
|
image_type: ext(&main),
|
|
src: main,
|
|
sizes: None,
|
|
});
|
|
custom_icons
|
|
} else {
|
|
icons
|
|
};
|
|
|
|
LogoConfig {
|
|
main: custom_main
|
|
.unwrap_or_else(|| "icons/trwnh/feather/plumeFeather256.png".to_owned()),
|
|
favicon: custom_favicon.unwrap_or_else(|| {
|
|
"icons/trwnh/feather-filled/plumeFeatherFilled64.png".to_owned()
|
|
}),
|
|
other,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct SearchTokenizerConfig {
|
|
pub tag_tokenizer: SearchTokenizer,
|
|
pub content_tokenizer: SearchTokenizer,
|
|
pub property_tokenizer: SearchTokenizer,
|
|
}
|
|
|
|
impl SearchTokenizerConfig {
|
|
pub fn init() -> Self {
|
|
use SearchTokenizer::*;
|
|
|
|
match var("SEARCH_LANG").ok().as_deref() {
|
|
Some("ja") => {
|
|
#[cfg(not(feature = "search-lindera"))]
|
|
panic!("You need build Plume with search-lindera feature, or execute it with SEARCH_TAG_TOKENIZER=ngram and SEARCH_CONTENT_TOKENIZER=ngram to enable Japanese search feature");
|
|
#[cfg(feature = "search-lindera")]
|
|
Self {
|
|
tag_tokenizer: Self::determine_tokenizer("SEARCH_TAG_TOKENIZER", Lindera),
|
|
content_tokenizer: Self::determine_tokenizer(
|
|
"SEARCH_CONTENT_TOKENIZER",
|
|
Lindera,
|
|
),
|
|
property_tokenizer: Ngram,
|
|
}
|
|
}
|
|
_ => Self {
|
|
tag_tokenizer: Self::determine_tokenizer("SEARCH_TAG_TOKENIZER", Whitespace),
|
|
content_tokenizer: Self::determine_tokenizer("SEARCH_CONTENT_TOKENIZER", Simple),
|
|
property_tokenizer: Ngram,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn determine_tokenizer(var_name: &str, default: SearchTokenizer) -> SearchTokenizer {
|
|
use SearchTokenizer::*;
|
|
|
|
match var(var_name).ok().as_deref() {
|
|
Some("simple") => Simple,
|
|
Some("ngram") => Ngram,
|
|
Some("whitespace") => Whitespace,
|
|
Some("lindera") => {
|
|
#[cfg(not(feature = "search-lindera"))]
|
|
panic!("You need build Plume with search-lindera feature to use Lindera tokenizer");
|
|
#[cfg(feature = "search-lindera")]
|
|
Lindera
|
|
}
|
|
_ => default,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct MailConfig {
|
|
pub server: String,
|
|
pub port: u16,
|
|
pub helo_name: String,
|
|
pub username: String,
|
|
pub password: String,
|
|
}
|
|
|
|
fn get_mail_config() -> Option<MailConfig> {
|
|
Some(MailConfig {
|
|
server: env::var("MAIL_SERVER").ok()?,
|
|
port: env::var("MAIL_PORT").map_or(SUBMISSIONS_PORT, |port| match port.as_str() {
|
|
"smtp" => SMTP_PORT,
|
|
"submissions" => SUBMISSIONS_PORT,
|
|
"submission" => SUBMISSION_PORT,
|
|
number => number
|
|
.parse()
|
|
.expect(r#"MAIL_PORT must be "smtp", "submissions", "submission" or an integer."#),
|
|
}),
|
|
helo_name: env::var("MAIL_HELO_NAME").unwrap_or_else(|_| "localhost".to_owned()),
|
|
username: env::var("MAIL_USER").ok()?,
|
|
password: env::var("MAIL_PASSWORD").ok()?,
|
|
})
|
|
}
|
|
|
|
pub struct LdapConfig {
|
|
pub addr: String,
|
|
pub base_dn: String,
|
|
pub tls: bool,
|
|
pub user_name_attr: String,
|
|
pub mail_attr: String,
|
|
}
|
|
|
|
fn get_ldap_config() -> Option<LdapConfig> {
|
|
let addr = var("LDAP_ADDR").ok();
|
|
let base_dn = var("LDAP_BASE_DN").ok();
|
|
match (addr, base_dn) {
|
|
(Some(addr), Some(base_dn)) => {
|
|
let tls = var("LDAP_TLS").unwrap_or_else(|_| "false".to_owned());
|
|
let tls = match tls.as_ref() {
|
|
"1" | "true" | "TRUE" => true,
|
|
"0" | "false" | "FALSE" => false,
|
|
_ => panic!("Invalid LDAP configuration : tls"),
|
|
};
|
|
let user_name_attr = var("LDAP_USER_NAME_ATTR").unwrap_or_else(|_| "cn".to_owned());
|
|
let mail_attr = var("LDAP_USER_MAIL_ATTR").unwrap_or_else(|_| "mail".to_owned());
|
|
Some(LdapConfig {
|
|
addr,
|
|
base_dn,
|
|
tls,
|
|
user_name_attr,
|
|
mail_attr,
|
|
})
|
|
}
|
|
(None, None) => None,
|
|
(_, _) => {
|
|
panic!("Invalid LDAP configuration : both LDAP_ADDR and LDAP_BASE_DN must be set")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ProxyConfig {
|
|
pub url: reqwest::Url,
|
|
pub only_domains: Option<HashSet<String>>,
|
|
pub proxy: reqwest::Proxy,
|
|
}
|
|
|
|
fn get_proxy_config() -> Option<ProxyConfig> {
|
|
let url: reqwest::Url = var("PROXY_URL").ok()?.parse().expect("Invalid PROXY_URL");
|
|
let proxy_url = url.clone();
|
|
let only_domains: Option<HashSet<String>> = var("PROXY_DOMAINS")
|
|
.ok()
|
|
.map(|ods| ods.split(',').map(str::to_owned).collect());
|
|
let proxy = if let Some(ref only_domains) = only_domains {
|
|
let only_domains = only_domains.clone();
|
|
reqwest::Proxy::custom(move |url| {
|
|
if let Some(domain) = url.domain() {
|
|
if only_domains.contains(domain)
|
|
|| only_domains
|
|
.iter()
|
|
.any(|target| domain.ends_with(&format!(".{}", target)))
|
|
{
|
|
Some(proxy_url.clone())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
} else {
|
|
reqwest::Proxy::all(proxy_url).expect("Invalid PROXY_URL")
|
|
};
|
|
Some(ProxyConfig {
|
|
url,
|
|
only_domains,
|
|
proxy,
|
|
})
|
|
}
|
|
|
|
lazy_static! {
|
|
pub static ref CONFIG: Config = Config {
|
|
base_url: var("BASE_URL").unwrap_or_else(|_| format!(
|
|
"127.0.0.1:{}",
|
|
var("ROCKET_PORT").unwrap_or_else(|_| "7878".to_owned())
|
|
)),
|
|
db_name: DB_NAME,
|
|
db_max_size: var("DB_MAX_SIZE").map_or(None, |s| Some(
|
|
s.parse::<u32>()
|
|
.expect("Couldn't parse DB_MAX_SIZE into u32")
|
|
)),
|
|
db_min_idle: var("DB_MIN_IDLE").map_or(None, |s| Some(
|
|
s.parse::<u32>()
|
|
.expect("Couldn't parse DB_MIN_IDLE into u32")
|
|
)),
|
|
signup: var("SIGNUP").map_or(SignupStrategy::default(), |s| s.parse().unwrap()),
|
|
#[cfg(feature = "postgres")]
|
|
database_url: var("DATABASE_URL")
|
|
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME)),
|
|
#[cfg(feature = "sqlite")]
|
|
database_url: var("DATABASE_URL").unwrap_or_else(|_| format!("{}.sqlite", DB_NAME)),
|
|
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
|
|
search_tokenizers: SearchTokenizerConfig::init(),
|
|
rocket: get_rocket_config(),
|
|
logo: LogoConfig::default(),
|
|
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
|
|
media_directory: var("MEDIA_UPLOAD_DIRECTORY")
|
|
.unwrap_or_else(|_| "static/media".to_owned()),
|
|
mail: get_mail_config(),
|
|
ldap: get_ldap_config(),
|
|
proxy: get_proxy_config(),
|
|
};
|
|
}
|