468 lines
15 KiB
Rust
468 lines
15 KiB
Rust
//! `config update v1` subcommand
|
|
//! <https://firefish.dev/firefish/firefish/-/issues/10947>
|
|
|
|
use crate::{
|
|
command::config::{read_file_as_string, ReadError},
|
|
config::{client, server, Revision, CLIENT_CONFIG_PATH, SERVER_CONFIG_PATH},
|
|
};
|
|
use color_print::cprintln;
|
|
use sqlx::{postgres::PgConnectOptions, ConnectOptions};
|
|
use std::{
|
|
collections::HashMap,
|
|
fs,
|
|
io::{self, Write},
|
|
path::Path,
|
|
};
|
|
use url::Url;
|
|
use yaml_rust::{Yaml, YamlLoader};
|
|
|
|
/// Errors that can happen in `config update v1` subcommand
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub(crate) enum Error {
|
|
#[error("failed to parse the old config file (.config/default.yml)")]
|
|
ReadYaml(#[from] ReadYamlConfigError),
|
|
#[error(transparent)]
|
|
WriteToml(#[from] WriteTomlConfigError),
|
|
#[error("failed to read the meta table")]
|
|
ReadMeta(#[from] sqlx::Error),
|
|
#[error("invalid config ({0})")]
|
|
InvalidConfig(&'static str),
|
|
#[error("failed to parse server URL")]
|
|
InvalidUrl(#[from] url::ParseError),
|
|
}
|
|
|
|
/// Errors that can happen while reading `.config/default.yml`
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub(crate) enum ReadYamlConfigError {
|
|
#[error(transparent)]
|
|
ReadFile(#[from] ReadError),
|
|
#[error(transparent)]
|
|
Yaml(#[from] yaml_rust::ScanError),
|
|
#[error("invalid config ({0})")]
|
|
InvalidConfig(String),
|
|
}
|
|
|
|
/// Errors that can happen while writing `config/{server,client}.toml`
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub(crate) enum WriteTomlConfigError {
|
|
#[error("failed to serialize the new server config into TOML format")]
|
|
ServerSer(#[source] toml::ser::Error),
|
|
#[error("failed to serialize the new client config into TOML format")]
|
|
ClientSer(#[source] toml::ser::Error),
|
|
#[error("failed to create {}", SERVER_CONFIG_PATH)]
|
|
ServerWrite(#[source] io::Error),
|
|
#[error("failed to create {}", CLIENT_CONFIG_PATH)]
|
|
ClientWrite(#[source] io::Error),
|
|
#[error("failed to create the config directory")]
|
|
Mkdir(#[source] io::Error),
|
|
}
|
|
|
|
fn read_default_yml() -> Result<HashMap<String, Yaml>, ReadYamlConfigError> {
|
|
let content = YamlLoader::load_from_str(&read_file_as_string(".config/default.yml")?)?;
|
|
|
|
if content.is_empty() {
|
|
return Err(ReadYamlConfigError::InvalidConfig(
|
|
"file is empty".to_string(),
|
|
));
|
|
}
|
|
if content.len() > 2 || content[0].is_array() {
|
|
return Err(ReadYamlConfigError::InvalidConfig(
|
|
"top-level should not be an array".to_string(),
|
|
));
|
|
}
|
|
|
|
let content = content[0]
|
|
.clone()
|
|
.into_hash()
|
|
.ok_or(ReadYamlConfigError::InvalidConfig(
|
|
"invalid format".to_string(),
|
|
))?;
|
|
|
|
let mut res = HashMap::new();
|
|
|
|
for (key, val) in content {
|
|
let Some(key) = key.as_str() else {
|
|
return Err(ReadYamlConfigError::InvalidConfig(format!(
|
|
"non-string key: {:?}",
|
|
key
|
|
)));
|
|
};
|
|
res.insert(key.to_owned(), val);
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
#[derive(sqlx::FromRow)]
|
|
#[sqlx(rename_all = "camelCase")]
|
|
struct Meta {
|
|
name: Option<String>,
|
|
description: Option<String>,
|
|
maintainer_name: Option<String>,
|
|
maintainer_email: Option<String>,
|
|
disable_registration: bool,
|
|
disable_local_timeline: bool,
|
|
disable_global_timeline: bool,
|
|
banner_url: Option<String>,
|
|
error_image_url: Option<String>,
|
|
icon_url: Option<String>,
|
|
cache_remote_files: bool,
|
|
enable_recaptcha: bool,
|
|
recaptcha_site_key: Option<String>,
|
|
recaptcha_secret_key: Option<String>,
|
|
local_drive_capacity_mb: i32,
|
|
remote_drive_capacity_mb: i32,
|
|
summaly_proxy: Option<String>,
|
|
enable_email: bool,
|
|
email: Option<String>,
|
|
smtp_secure: bool,
|
|
smtp_host: Option<String>,
|
|
smtp_port: Option<i32>,
|
|
smtp_user: Option<String>,
|
|
smtp_pass: Option<String>,
|
|
enable_service_worker: bool,
|
|
sw_public_key: Option<String>,
|
|
sw_private_key: Option<String>,
|
|
pinned_users: Vec<String>,
|
|
tos_url: Option<String>,
|
|
repository_url: String,
|
|
feedback_url: Option<String>,
|
|
use_object_storage: bool,
|
|
object_storage_bucket: Option<String>,
|
|
object_storage_prefix: Option<String>,
|
|
object_storage_base_url: Option<String>,
|
|
object_storage_endpoint: Option<String>,
|
|
object_storage_region: Option<String>,
|
|
object_storage_access_key: Option<String>,
|
|
object_storage_secret_key: Option<String>,
|
|
object_storage_port: Option<i32>,
|
|
object_storage_use_ssl: bool,
|
|
object_storage_set_public_read: bool,
|
|
object_storage_s3_force_path_style: bool,
|
|
object_storage_use_proxy: bool,
|
|
proxy_account_id: Option<String>,
|
|
enable_hcaptcha: bool,
|
|
hcaptcha_site_key: Option<String>,
|
|
hcaptcha_secret_key: Option<String>,
|
|
background_image_url: Option<String>,
|
|
logo_image_url: Option<String>,
|
|
pinned_clip_id: Option<String>,
|
|
allowed_hosts: Vec<String>,
|
|
secure_mode: bool,
|
|
private_mode: bool,
|
|
deepl_auth_key: Option<String>,
|
|
deepl_is_pro: bool,
|
|
email_required_for_signup: bool,
|
|
theme_color: Option<String>,
|
|
default_light_theme: Option<String>,
|
|
default_dark_theme: Option<String>,
|
|
enable_ip_logging: bool,
|
|
enable_active_email_validation: bool,
|
|
custom_motd: Vec<String>,
|
|
custom_splash_icons: Vec<String>,
|
|
disable_recommended_timeline: bool,
|
|
recommended_instances: Vec<String>,
|
|
enable_guest_timeline: bool,
|
|
default_reaction: String,
|
|
libre_translate_api_url: Option<String>,
|
|
libre_translate_api_key: Option<String>,
|
|
enable_server_machine_stats: bool,
|
|
enable_identicon_generation: bool,
|
|
donation_link: Option<String>,
|
|
mark_local_files_nsfw_by_default: bool,
|
|
antenna_limit: i32,
|
|
// TODO:
|
|
// more_urls: jsonb
|
|
// experimental_features: jsonb
|
|
}
|
|
|
|
async fn read_meta_table(
|
|
host: &str,
|
|
port: u16,
|
|
username: &str,
|
|
password: &str,
|
|
database: &str,
|
|
) -> Result<Meta, sqlx::Error> {
|
|
let mut conn = PgConnectOptions::new()
|
|
.host(host)
|
|
.port(port)
|
|
.username(username)
|
|
.password(password)
|
|
.database(database)
|
|
.connect()
|
|
.await?;
|
|
|
|
let meta: Meta = sqlx::query_as(r#"SELECT * FROM "meta""#)
|
|
.fetch_one(&mut conn)
|
|
.await?;
|
|
|
|
Ok(meta)
|
|
}
|
|
|
|
async fn read_old_config() -> Result<(HashMap<String, Yaml>, Meta), Error> {
|
|
let default_yml = read_default_yml()?;
|
|
let db = default_yml
|
|
.get("db")
|
|
.and_then(|db| db.as_hash())
|
|
.ok_or(Error::InvalidConfig("`db` is missing"))?;
|
|
|
|
let meta = read_meta_table(
|
|
db.get(&Yaml::String("host".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("db.host"))?,
|
|
db.get(&Yaml::String("port".to_string()))
|
|
.and_then(|v| v.as_i64())
|
|
.ok_or(Error::InvalidConfig("db.port"))? as u16,
|
|
db.get(&Yaml::String("user".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("db.user"))?,
|
|
db.get(&Yaml::String("pass".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("db.pass"))?,
|
|
db.get(&Yaml::String("db".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("db.db"))?,
|
|
)
|
|
.await?;
|
|
|
|
Ok((default_yml, meta))
|
|
}
|
|
|
|
fn create_new_server_config(
|
|
default_yml: HashMap<String, Yaml>,
|
|
meta: &Meta,
|
|
) -> Result<server::Config, Error> {
|
|
let db = default_yml
|
|
.get("db")
|
|
.and_then(|db| db.as_hash())
|
|
.ok_or(Error::InvalidConfig("`db` is missing"))?;
|
|
|
|
let redis = default_yml
|
|
.get("redis")
|
|
.and_then(|redis| redis.as_hash())
|
|
.ok_or(Error::InvalidConfig("`redis` is missing"))?;
|
|
|
|
let server_url = default_yml
|
|
.get("url")
|
|
.and_then(|url| url.as_str())
|
|
.ok_or(Error::InvalidConfig("`url` is missing"))?;
|
|
|
|
let parsed_server_url = Url::parse(server_url)?;
|
|
let protocol = parsed_server_url.scheme().to_owned();
|
|
|
|
if protocol != "https" && protocol != "http" {
|
|
return Err(Error::InvalidConfig(
|
|
"server URL must start with http:// or https://",
|
|
));
|
|
}
|
|
|
|
let hostname = parsed_server_url.host_str().ok_or(Error::InvalidConfig(
|
|
"hostname is missing in the server url",
|
|
))?;
|
|
let host = match parsed_server_url.port() {
|
|
Some(port) => format!("{}:{}", hostname, port),
|
|
None => hostname.to_owned(),
|
|
};
|
|
|
|
let repository_url = match meta.repository_url.as_ref() {
|
|
"https://codeberg.org/calckey/calckey"
|
|
| "https://codeberg.org/firefish/firefish"
|
|
| "https://git.joinfirefish.org/firefish/firefish"
|
|
| "https://firefish.dev/firefish/firefish" => None,
|
|
url => Some(url.to_owned()),
|
|
};
|
|
|
|
let mut server_config = server::Config {
|
|
config_revision: Revision::V1,
|
|
info: Some(server::Info {
|
|
name: meta.name.to_owned(),
|
|
description: meta.description.to_owned(),
|
|
maintainer_name: meta.maintainer_name.to_owned(),
|
|
contact_info: meta.maintainer_email.to_owned(),
|
|
open_registrations: !meta.disable_registration,
|
|
repository_url,
|
|
}),
|
|
timelines: Some(server::Timelines {
|
|
local: !meta.disable_local_timeline,
|
|
global: !meta.disable_global_timeline,
|
|
recommended: !meta.disable_recommended_timeline,
|
|
guest: meta.enable_guest_timeline,
|
|
}),
|
|
network: server::Network {
|
|
protocol: match protocol.as_str() {
|
|
"http" => Some(server::HttpProtocol::Http),
|
|
_ => None,
|
|
},
|
|
host,
|
|
port: default_yml
|
|
.get("port")
|
|
.and_then(|v| v.as_i64())
|
|
.ok_or(Error::InvalidConfig("port"))? as u16,
|
|
},
|
|
database: server::Database {
|
|
host: db
|
|
.get(&Yaml::String("host".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("db.host"))?
|
|
.to_string(),
|
|
port: db
|
|
.get(&Yaml::String("port".to_string()))
|
|
.and_then(|v| v.as_i64())
|
|
.ok_or(Error::InvalidConfig("db.port"))? as u16,
|
|
user: db
|
|
.get(&Yaml::String("user".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("db.user"))?
|
|
.to_string(),
|
|
password: db
|
|
.get(&Yaml::String("pass".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("db.pass"))?
|
|
.to_string(),
|
|
name: db
|
|
.get(&Yaml::String("db".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("db.db"))?
|
|
.to_string(),
|
|
},
|
|
cache_server: server::CacheServer {
|
|
host: redis
|
|
.get(&Yaml::String("host".to_string()))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or(Error::InvalidConfig("redis.host"))?
|
|
.to_string(),
|
|
port: redis
|
|
.get(&Yaml::String("port".to_string()))
|
|
.and_then(|v| v.as_i64())
|
|
.ok_or(Error::InvalidConfig("redis.port"))? as u16,
|
|
user: match redis.get(&Yaml::String("user".to_string())) {
|
|
Some(user) => Some(
|
|
user.as_str()
|
|
.ok_or(Error::InvalidConfig("redis.user"))?
|
|
.to_string(),
|
|
),
|
|
None => None,
|
|
},
|
|
password: match redis.get(&Yaml::String("pass".to_string())) {
|
|
Some(pass) => Some(
|
|
pass.as_str()
|
|
.ok_or(Error::InvalidConfig("redis.pass"))?
|
|
.to_string(),
|
|
),
|
|
None => None,
|
|
},
|
|
index: match redis.get(&Yaml::String("db".to_string())) {
|
|
Some(db) => Some(db.as_i64().ok_or(Error::InvalidConfig("redis.db"))? as u8),
|
|
None => None,
|
|
},
|
|
prefix: match redis.get(&Yaml::String("prefix".to_string())) {
|
|
Some(prefix) => Some(
|
|
prefix
|
|
.as_str()
|
|
.ok_or(Error::InvalidConfig("redis.prefix"))?
|
|
.to_string(),
|
|
),
|
|
None => None,
|
|
},
|
|
},
|
|
id: None,
|
|
};
|
|
|
|
if let Some(id) = default_yml.get("cuid") {
|
|
let id = id.as_hash().ok_or(Error::InvalidConfig("cuid"))?;
|
|
server_config.id = Some(server::Id {
|
|
length: match id.get(&Yaml::String("length".to_string())) {
|
|
Some(length) => {
|
|
Some(length.as_i64().ok_or(Error::InvalidConfig("cuid.length"))? as u8)
|
|
}
|
|
None => None,
|
|
},
|
|
fingerprint: match id.get(&Yaml::String("fingerprint".to_string())) {
|
|
Some(fingerprint) => Some(
|
|
fingerprint
|
|
.as_str()
|
|
.ok_or(Error::InvalidConfig("cuid.fingerprint"))?
|
|
.to_string(),
|
|
),
|
|
None => None,
|
|
},
|
|
});
|
|
}
|
|
|
|
Ok(server_config)
|
|
}
|
|
|
|
fn create_new_client_config(meta: Meta) -> Result<client::Config, Error> {
|
|
let mut config = client::Config {
|
|
config_revision: Revision::V1,
|
|
theme: None,
|
|
image: None,
|
|
pinned_links: None,
|
|
};
|
|
|
|
if meta.theme_color.is_some()
|
|
|| meta.default_light_theme.is_some()
|
|
|| meta.default_dark_theme.is_some()
|
|
{
|
|
config.theme = Some(client::Theme {
|
|
light: meta.default_light_theme,
|
|
dark: meta.default_dark_theme,
|
|
color: meta.theme_color,
|
|
});
|
|
}
|
|
|
|
if meta.logo_image_url.is_some()
|
|
|| meta.icon_url.is_some()
|
|
|| meta.banner_url.is_some()
|
|
|| meta.background_image_url.is_some()
|
|
|| (meta.error_image_url.is_some()
|
|
&& meta.error_image_url.as_ref().unwrap() != "/static-assets/badges/error.webp")
|
|
{
|
|
config.image = Some(client::Image {
|
|
logo: meta.logo_image_url,
|
|
icon: meta.icon_url,
|
|
banner: meta.banner_url,
|
|
background: meta.background_image_url,
|
|
error: meta.error_image_url,
|
|
});
|
|
}
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
pub(super) async fn run() -> Result<(), Error> {
|
|
println!("Updating the config to revision 1...");
|
|
|
|
let (default_yml, meta) = read_old_config().await?;
|
|
|
|
let server_config = create_new_server_config(default_yml, &meta)?;
|
|
let client_config = create_new_client_config(meta)?;
|
|
|
|
if !Path::new("config").exists() {
|
|
fs::create_dir("config").map_err(WriteTomlConfigError::Mkdir)?;
|
|
}
|
|
|
|
let mut server_toml =
|
|
fs::File::create(SERVER_CONFIG_PATH).map_err(WriteTomlConfigError::ServerWrite)?;
|
|
server_toml
|
|
.write(
|
|
toml::to_string_pretty(&server_config)
|
|
.map_err(WriteTomlConfigError::ServerSer)?
|
|
.as_bytes(),
|
|
)
|
|
.map_err(WriteTomlConfigError::ServerWrite)?;
|
|
cprintln!("<bold>{}</> has been created!", SERVER_CONFIG_PATH);
|
|
|
|
let mut client_toml =
|
|
fs::File::create(CLIENT_CONFIG_PATH).map_err(WriteTomlConfigError::ClientWrite)?;
|
|
client_toml
|
|
.write(
|
|
toml::to_string_pretty(&client_config)
|
|
.map_err(WriteTomlConfigError::ClientSer)?
|
|
.as_bytes(),
|
|
)
|
|
.map_err(WriteTomlConfigError::ClientWrite)?;
|
|
cprintln!("<bold>{}</> has been created!", CLIENT_CONFIG_PATH);
|
|
|
|
Ok(())
|
|
}
|