use colored::*;
use df_st_core::config::get_config;
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use log::{Level, LevelFilter, Metadata, Record};
use std::io;
use std::path::PathBuf;
use structopt::StructOpt;
#[macro_use]
mod git_panic;
static LOGGER: Logger = Logger;
struct Logger;
impl log::Log for Logger {
fn enabled(&self, metadata: &Metadata) -> bool {
let enable = if !cfg!(debug_assertions) {
metadata.level() == Level::Warn
|| metadata.level() == Level::Error
|| metadata.level() == Level::Debug
|| !(metadata.target() == "launch_" || metadata.target() == "_")
} else {
true
};
metadata.level() <= Level::Trace
&& metadata.target() != "serde_xml_rs::de"
&& !metadata.target().starts_with("hyper::")
&& enable
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
println!(
"{:<5}:{} - {}",
match record.level() {
Level::Error => "ERROR".bright_red(),
Level::Warn => "WARN".bright_yellow(),
Level::Info => "INFO".bright_blue(),
Level::Debug => "DEBUG".bright_green(),
Level::Trace => "TRACE".bright_magenta(),
},
record.target(),
record.args()
);
}
}
fn flush(&self) {}
}
#[derive(StructOpt, Debug)]
#[structopt(
name = "df_storyteller",
about = "Parse Dwarf Fortress Legends and have fun with the stories."
)]
struct Opts {
#[structopt(short, long)]
debug: bool,
#[structopt(short, long)]
quiet: bool,
#[structopt(short, long, parse(from_occurrences))]
verbose: u8,
#[structopt(subcommand)]
cmd: Commands,
}
#[derive(StructOpt, Debug)]
enum Commands {
Guide {
},
Import {
#[structopt(short, long)]
world: u32,
#[structopt(name = "FILE", parse(from_os_str))]
file: PathBuf,
#[structopt(short, long)]
show_unknown: bool,
},
Start {
#[structopt(short, long)]
world: u32,
},
List {
#[structopt(short, long, default_value = "0")]
page: u32,
},
Export {
#[structopt(short, long)]
world: u32,
#[structopt(name = "OUTPUT FILE", parse(from_os_str))]
output: PathBuf,
#[structopt(subcommand)]
format: Option<ExportFormats>,
},
Docs {
#[structopt(name = "OUTPUT FILE", parse(from_os_str))]
output: PathBuf,
},
Config {
},
Database {
#[structopt(long)]
postgres: bool,
#[structopt(short = "u", long)]
db_user: Option<String>,
#[structopt(short = "p", long)]
db_port: Option<u16>,
#[structopt(long)]
drop_db: bool,
},
}
#[derive(StructOpt, Debug)]
enum ExportFormats {
Json,
Xml,
}
pub fn main() -> io::Result<()> {
setup_panic!();
let opts = Opts::from_args();
log::set_logger(&LOGGER).unwrap();
if opts.debug {
log::set_max_level(LevelFilter::Debug);
if opts.verbose >= 2 {
log::set_max_level(LevelFilter::Trace);
}
} else if opts.quiet {
log::set_max_level(LevelFilter::Off);
} else {
match opts.verbose {
0 => log::set_max_level(LevelFilter::Info),
1 => log::set_max_level(LevelFilter::Debug),
_ => log::set_max_level(LevelFilter::Trace),
}
}
trace!("Command arguments: {:#?}", opts);
let config_path = PathBuf::from("./df_storyteller-config.json");
let config = get_config(&config_path);
trace!("Config loaded: {:#?}", config);
#[cfg(debug_assertions)]
if opts.debug {
}
let status = futures::executor::block_on(df_st_updater::check_version()).unwrap();
match status {
df_st_updater::VersionStatus::Latest => {
info!("DF Storyteller is up-to-date.");
}
df_st_updater::VersionStatus::UpdateAvailable => {
info!(
"-----------------------------------------------\n\
There is a new update available for DF Storyteller!\n\
Please check https://dfstoryteller.com/ for the latest version.\n\
-----------------------------------------------"
);
}
df_st_updater::VersionStatus::UpdateRequired => {
error!(
"This version of DF Storyteller is outdated.\n\
This version is marked as unsafe. Updating is required!"
);
info!(
"Please download a new version at\n\
https://dfstoryteller.com/ You are blocked from using an unsafe versions.\n\
Closing application now!"
);
panic!("This version is unsafe. Please update.");
}
df_st_updater::VersionStatus::TamperProof => {
error!(
"Something is off. We are detecting some tampering with update verification.\n\
We do not allow tampering in this application. If you did nothing weird, \
please open an issue: {}",
df_st_core::git_issue::link_to_issue_page()
);
error!(
"Tamper prevention in place. Please update to the latest version.\n\
Closing application now!"
);
panic!("Tamper prevention in place. Please update to the latest version.");
}
df_st_updater::VersionStatus::CouldNotCheck => {
warn!("DF Storyteller could not check for updates.");
info!(
"Please check for updates yourself to make sure you are not \
vulnerable to attacks."
);
}
df_st_updater::VersionStatus::Unknown => {
warn!(
"DF Storyteller encountered an unknown status from update server.\n\
This might be a bug, please open an issue: {}",
df_st_core::git_issue::link_to_issue_page()
);
}
}
match opts.cmd {
Commands::Guide {} => {
df_st_guide::start_guide_server();
}
Commands::Import {
world,
file,
show_unknown: _,
} => {
debug!("Checking database connection");
if let Some(service) = &config.database.service {
if service == "sqlite" {
if !df_st_db::check_db_has_tables(&config) {
info!("Found a database without tables. Running migrations");
df_st_db::run_migrations(&config);
}
} else if !df_st_db::check_db_has_tables(&config) {
panic!("Found a database without tables.");
}
}
if !file.is_file() && !file.is_dir() {
error!(
"The path provided is not a file or directory: {}",
file.to_string_lossy()
);
} else {
df_st_parser::parse_and_store_xml(&file, &config, world as i32);
df_st_parser::parse_world_map_images(&file, &config, world as i32);
df_st_parser::parse_site_map_images(&file, &config, world as i32);
info!(
"Done parsing all files. You can now use the command below to view you world."
);
if cfg!(windows) {
info!("Run: `./df_storyteller.exe start -w {}`", world);
} else {
info!("Run: `./df_storyteller start -w {}`", world);
}
}
}
Commands::Start { world } => {
debug!("Checking database connection");
if let Some(service) = &config.database.service {
if service == "sqlite" {
if !df_st_db::check_db_has_tables(&config) {
info!("Found a database without tables. Running migrations");
df_st_db::run_migrations(&config);
warn!(
"You have not imported any worlds so all responses will be empty.\n\
It is best if you first use the `import` command to import a world."
);
}
} else if !df_st_db::check_db_has_tables(&config) {
panic!("Found a database without tables.");
}
}
df_st_api::start_server(&config, world);
}
Commands::List { page } => {
use df_st_db::DBObject;
let pool =
df_st_db::establish_connection(&config).expect("Failed to connect to database.");
let conn = pool.get().expect("Couldn't get db connection from pool.");
let per_page_limit = 10;
let world_list = df_st_db::DFWorldInfo::get_list_from_db(
&*conn,
df_st_db::id_filter![],
df_st_db::string_filter![],
page,
per_page_limit,
Some(df_st_db::OrderTypes::Asc),
Some("id".to_owned()),
None,
true,
)
.expect("Could not get the list of worlds from that database.");
let mut list_string = String::new();
let mut message = String::new();
if world_list.is_empty() {
if page == 0 {
message = "No worlds found.".to_owned();
} else {
message = "No more worlds found on this page.".to_owned();
}
}
if world_list.len() >= per_page_limit as usize {
message = format!("More world might be on next page, use `-p {}`.", page + 1);
}
for world in world_list {
let year = match world.year {
Some(x) => x.to_string(),
None => "<unknown>".to_owned(),
};
let region_number = match world.region_number {
Some(x) => x.to_string(),
None => "<unknown>".to_owned(),
};
list_string = format!(
"{}{:>3}: {} ({}), Year: {} Region: {}\n",
list_string,
world.id,
world.name.unwrap_or_else(|| "<no-name>".to_owned()),
world
.alternative_name
.unwrap_or_else(|| "<no-name>".to_owned()),
year,
region_number
);
}
println!("List of worlds in database:\n{}{}", list_string, message);
}
Commands::Export {
world: _,
output: _,
format: _,
} => {
println!("The Export subcommand is not yet implemented. We are still working on this.");
}
Commands::Docs { mut output } => {
if let Some(extension) = output.extension() {
if extension != "json" {
warn!(
"The openapi file can only be exported in json format. \
Replacing extension with '.json'."
);
output.set_extension("json");
}
}
if output.is_dir() {
output.set_file_name("openapi.json");
}
let output_name = output.to_str().unwrap_or("openapi.json");
info!("Write documentation to '{}' file.", output_name);
let openapi = df_st_api::get_openapi_spec();
df_st_api::write_json_file(output, &openapi).ok();
}
Commands::Config {} => {
df_st_core::config::store_config_to_file(config, None);
info!("Created/replaced config file: `df_storyteller-config.json`.");
}
Commands::Database {
postgres,
db_user,
db_port,
drop_db,
} => {
if postgres {
println!(
"The Database subcommand is still experimental. \
Tel us know if something does not seem right."
);
let user = db_user.unwrap_or_else(|| "postgres".to_owned());
let port = db_port.unwrap_or(5432);
println!("Password for {}: ", user);
let mut password = String::new();
io::stdin().read_line(&mut password)?;
password = password.trim().to_owned();
let mut privileged_config = config;
privileged_config.database.service = Some("postgres".to_owned());
privileged_config.database.config = df_st_core::config::DBURLConfig {
user: Some(user),
password,
host: Some("localhost".to_owned()),
port: Some(port),
database: Some("df_storyteller".to_owned()),
..Default::default()
};
df_st_db::create_user(&privileged_config);
let config = get_config(&config_path);
df_st_db::recreate_database(&config, drop_db);
df_st_db::run_migrations(&config);
} else {
df_st_db::run_migrations(&config);
}
}
}
Ok(())
}