1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
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;

/// Provide custom error with links for when application panics (unrecoverable error).
#[macro_use]
mod git_panic;

// TODO: Write logs to file, this requires multi-threading safety or using an external crate
/// An instance of the `Logger`.
static LOGGER: Logger = Logger;
/// The log collector and handler for most printed messages in terminal.
struct Logger;

impl log::Log for Logger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        let enable = if !cfg!(debug_assertions) {
            // Only in release mode
            // Do the filters below unless it is a Warning, Error (or Debug)
            metadata.level() == Level::Warn
                || metadata.level() == Level::Error
                || metadata.level() == Level::Debug
                // Hide part of the Rocket messages
                || !(metadata.target() == "launch_" || metadata.target() == "_")
        } else {
            // Don't apply additional filters in debug build
            true
        };

        // All messages need to be Trace or lower
        metadata.level() <= Level::Trace
            // Don't display serde xml parsing messages (to many)
            && metadata.target() != "serde_xml_rs::de"
            // Don't display hyper networking message (not useful in most cases)
            && !metadata.target().starts_with("hyper::")
            // If release mode filter on
            && 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()
            );
        }
        // TODO: log to file
        // TODO: add timestamps to log file
    }

    fn flush(&self) {}
}

/// The available command line parameters.
#[derive(StructOpt, Debug)]
#[structopt(
    name = "df_storyteller",
    about = "Parse Dwarf Fortress Legends and have fun with the stories."
)]
struct Opts {
    /// Activates debug mode
    #[structopt(short, long)]
    debug: bool,

    /// Activates quiet mode, no log message in std out
    #[structopt(short, long)]
    quiet: bool,

    /// Verbose mode (-v, -vv)
    #[structopt(short, long, parse(from_occurrences))]
    verbose: u8,

    /// All available subcommand
    #[structopt(subcommand)]
    cmd: Commands,
}

/// All available subcommands in DF Storyteller.
#[derive(StructOpt, Debug)]
enum Commands {
    /// A Guide for setting up DF Storyteller.
    /// This is also available on [our website](https://guide.dfstoryteller.com/).
    Guide {
        // No other options
    },
    /// Parse and save the Legends files
    Import {
        /// Select what world to load from DB
        #[structopt(short, long)]
        world: u32,

        /// Legends Files to process.
        /// Give one of the legend files, other files will be found automatically
        #[structopt(name = "FILE", parse(from_os_str))]
        file: PathBuf,

        /// (TODO) Show a list of unknown tags in files
        #[structopt(short, long)]
        show_unknown: bool,
    },
    /// Start API server
    Start {
        /// Select what world to load from DB
        #[structopt(short, long)]
        world: u32,
    },
    /// List all the saved world
    List {
        /// View next page of worlds. Current page limit = 20.
        /// If the list is to long it will be broken up into pages.
        /// This allows you to get the other pages.
        #[structopt(short, long, default_value = "0")]
        page: u32,
    },
    /// (TODO) Export a saved world
    Export {
        /// Select what world to load from DB
        #[structopt(short, long)]
        world: u32,

        /// Output the parsed legends to file
        #[structopt(name = "OUTPUT FILE", parse(from_os_str))]
        output: PathBuf,

        /// Format of output file. Default is Json
        #[structopt(subcommand)]
        format: Option<ExportFormats>,
    },
    /// Export OpenAPI JSON file.
    /// This file can be used together with other documentation viewers like
    /// RapiDoc to view the API documentation.
    Docs {
        /// Path to where to write the `openapi.json` file, can be a file or directory
        #[structopt(name = "OUTPUT FILE", parse(from_os_str))]
        output: PathBuf,
    },
    /// Create/overwrite persistent settings/config file.
    /// If no file is found it will create the default config file.
    /// If a file is found it will read the file (if valid) and write it again,
    /// this will ensure it is a valid and nicely formatted config file.
    Config {
        // No other options
    },
    /// Setup the database
    Database {
        /// Set up a Postgres database
        #[structopt(long)]
        postgres: bool,

        /// The username that be used to create new roles
        /// By default "postgres" is used
        #[structopt(short = "u", long)]
        db_user: Option<String>,

        /// The port used for the database connection
        /// Default is `5432`
        #[structopt(short = "p", long)]
        db_port: Option<u16>,

        /// DANGER: This will DELETE the existing `df_storyteller` database.
        /// This deletes all stored data in the database.
        /// Once deleted it will create a new database.
        #[structopt(long)]
        drop_db: bool,
    },
}

/// The available file formats for exporting, used by the [`Export` subcommand](Commands::Export).
#[derive(StructOpt, Debug)]
enum ExportFormats {
    Json,
    Xml,
}

/// Start of the DF Storyteller CLI application.
pub fn main() -> io::Result<()> {
    setup_panic!();
    let opts = Opts::from_args();
    // Setup logger and log level
    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 {
        // TODO change this to logging to file one, but can not use static var. (unsafe)
        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);

    // Read config file
    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 {
        // df_st_updater::create_keypair();
        // df_st_updater::create_signed_message();
    }

    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."
            );
            // This is allowed because we do not want to prevent people from enjoying it without
            // an internet connection.
        }
        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()
            );
        }
    }

    // Check what subcommand we want to run
    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");
                        // Run migrations and create all the tables
                        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");
                        // Run migrations and create all the tables
                        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: _,
        } => {
            // TODO it should automatically detect if the extension is xml
            // to export it in xml 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() {
                // TODO: To lowercase?
                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);

                // Get password
                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);
                // Reload config to use new user (less privileged)
                let config = get_config(&config_path);
                df_st_db::recreate_database(&config, drop_db);
                // Run migrations and create all the tables
                df_st_db::run_migrations(&config);
            } else {
                // Run migrations and create all the tables
                df_st_db::run_migrations(&config);
            }
        }
    }
    Ok(())
}