209 lines
7.7 KiB
Rust
209 lines
7.7 KiB
Rust
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 <plenum-bot@berlin.ccc.de>", description: "Email address to use for \"From:\"." },
|
|
CfgField::Default { key: "to", default: "CCCB Intern <intern@berlin.ccc.de>", 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<dyn Error>> {
|
|
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<dyn Error>> {
|
|
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<String> {
|
|
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,
|
|
}
|
|
}
|
|
}
|