use crate::key_value::KeyValueStore as KV; use std::error::Error; use std::io::{self, Write}; const CONFIG_SPEC: CfgSpec<'_> = CfgSpec { groups: &[ CfgGroup { name: "config", description: "(internal values)", fields: &[ CfgField::Silent { key: "version", default: "1" }, ], }, CfgGroup { name: "hedgedoc", description: "HedgeDoc markdown pad server settings", fields: &[ CfgField::Default { key: "server-url", default: "https://md.berlin.ccc.de/", description: "Hedgedoc server storing the pads." }, CfgField::Default { key: "template-name", default: "plenum-template", description: "Name of the pad containing the template to use." }, // TODO: make these generators? CfgField::Optional { key: "last-id", description: "ID of last plenum's pad." }, CfgField::Optional { key: "next-id", description: "ID of next plenum's pad." } ], }, CfgGroup { name: "email", description: "Sending emails.", fields: &[ CfgField::Default { key: "server", default: "mail.berlin.ccc.de", description: "SMTP server used for sending emails." }, CfgField::Default { key: "user", default: "plenum-bot@berlin.ccc.de", description: "User name used for authenticating with the mail server." }, CfgField::Password { key: "password", description: "Password for authenticating with the mail server." }, CfgField::Default { key: "sender", default: "Plenumsbot ", description: "Email address to use for \"From:\"." }, CfgField::Default { key: "to", default: "CCCB Intern ", description: "Recipient of the emails sent." }, CfgField::Optional { key: "last-message-id", description: "Message-Id of last initial announcement to send In-Reply-To (if applicable)." } ], }, // TODO: Matrix, Wiki CfgGroup { name: "text", description: "Various strings used.", fields: &[ CfgField::Default { key: "email-signature", default: "\n\n[Diese Nachricht wurde automatisiert vom Plenumsbot erstellt und ist daher ohne Unterschrift gültig.]", description: "Text added at the bottom of every email." }, ], }, ], }; /// Ensure all fields with known default values exist. pub fn populate_defaults(config: &KV) { for group in CONFIG_SPEC.groups { for field in group.fields { match field { CfgField::Silent { key, default, .. } => { config.default(&format!("{}-{}", group.name, key), default) }, CfgField::Default { key, default, .. } => { config.default(&format!("{}-{}", group.name, key), default) }, _ => {} } } } } const ANSI_GROUP: &str = "\x1b[1;31m"; const ANSI_FIELD: &str = "\x1b[1;33m"; const ANSI_RESET: &str = "\x1b[0m"; pub fn interactive_check(config: KV) -> Result<(), Box> { for group in CONFIG_SPEC.groups { group_header( group.name, group.description ); // TODO: skip category option? for field in group.fields { check_field( &config, group.name, field )?; } } Ok(()) } fn check_field( config: &KV, grpname: &str, field: &CfgField ) -> Result<(), Box> { if field.is_silent() { return Ok(()) } let key = field.full_key(grpname); println!( "{}{}{} - {}", ANSI_FIELD, &key, ANSI_RESET, field.description() ); println!( " default: {}", field.default().unwrap_or("(empty)".to_string())); let value = config.get(&key).ok(); if field.is_password() { match value { Some(_) => println!(" current: *****"), None => println!(" current: (empty)"), }; //todo!() // TODO: [K]eep as-is (if exists), [R]emove, [I]nput } else { println!( " current: {}", value.unwrap_or("(empty)".to_string()) ); // TODO: [K]eep as-is, [R]eset to default, [I]nput, [L]ong (multiline) input } // Action - Ok(()) } fn group_header( name: &str, description: &str ) { println!("=============================="); println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description); println!(""); } fn prompt_single_line( ) -> String { print!("New value: "); io::stdout().flush().ok(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); input.trim().to_string() } fn prompt_multiline( ) -> String { println!("Enter new value: (end with '.' on a new line)"); let mut acc = String::new(); loop { let mut line = String::new(); io::stdin().read_line(&mut line).unwrap(); if line.trim() == "." { break; } acc.push_str(&line); } acc } fn prompt_password( ) -> String { rpassword::prompt_password("New password (not shown) : ").unwrap() } /// Describes a field in the configuration. enum CfgField<'a> { /// Silently writes a default to the DB if absent, never prompts for changes. Silent { key: &'a str, default: &'a str }, /// Writes a default, asks for changes. Default { key: &'a str, default: &'a str, description: &'a str }, /// No default, asks for value, but can stay empty. Optional { key: &'a str, description: &'a str }, /// Empty by default, required, will prompt without echo. Password { key: &'a str, description: &'a str }, /// Empty by default, can be user-provided, or will be generated randomly. RandomId { key: &'a str, generator: fn() -> String, generator_description: &'a str, description: &'a str }, } /// A group of related config fields. The final key of an inner value will be /// `{group.name}-{key}`. struct CfgGroup<'a> { name: &'a str, description: &'a str, fields: &'a [CfgField<'a>], } /// A description of the entire config, as a collection of named groups. /// /// Used both to populate defaults and to interactively adjust the configuration. struct CfgSpec<'a> { groups: &'a [CfgGroup<'a>], } impl<'a> CfgField<'a> { fn is_silent(&self) -> bool { match self { CfgField::Silent { .. } => true, _ => false } } fn is_password(&self) -> bool { match self { CfgField::Password { .. } => true, _ => false } } fn key(&self) -> String { match self { CfgField::Silent { key, .. } => key, CfgField::Default { key, .. } => key, CfgField::Optional { key, .. } => key, CfgField::Password { key, .. } => key, CfgField::RandomId { key, .. } => key, }.to_string() } fn full_key(&self, grpname: &str) -> String { format!("{}-{}", grpname, self.key()) } fn description(&self) -> String { match self { CfgField::Silent { .. } => "(none)", CfgField::Default { description, .. } => description, CfgField::Optional { description, .. } => description, CfgField::Password { description, .. } => description, CfgField::RandomId { description, .. } => description, }.to_string() } fn default(&self) -> Option { match self { CfgField::Silent { default, .. } => Some(default.to_string()), CfgField::Default { default, .. } => Some(default.to_string()), CfgField::RandomId { generator_description, .. } => Some(format!("{}{}{}",'(',generator_description,')')), _ => None, } } }