2024-07-29 15:12:26 +02:00
use crate ::key_value ::KeyValueStore as KV ;
use std ::error ::Error ;
use std ::io ::{ self , Write } ;
2024-07-30 03:57:24 +02:00
/// Text shown for an empty field.
2024-08-02 22:29:22 +02:00
const EMPTY_VALUE : & str = " (empty) " ;
2024-07-30 03:57:24 +02:00
/// Text shown in place of a password.
2024-08-02 22:29:22 +02:00
const HIDDEN_PASSWORD : & str = " ***** " ;
2024-07-30 03:57:24 +02:00
// highlight some of the info
const ANSI_GROUP : & str = " \x1b [1;31m " ;
const ANSI_FIELD : & str = " \x1b [1;33m " ;
2024-08-02 22:29:22 +02:00
const ANSI_NOTICE : & str = " \x1b [33m " ;
2024-07-30 03:57:24 +02:00
const ANSI_RESET : & str = " \x1b [0m " ;
const CONFIG_SPEC : CfgSpec < 'static > = CfgSpec {
2024-07-29 15:12:26 +02:00
groups : & [
CfgGroup { name : " config " ,
description : " (internal values) " ,
fields : & [
CfgField ::Silent { key : " version " , default : " 1 " } ,
] ,
} ,
CfgGroup { name : " hedgedoc " ,
description : " HedgeDoc markdown pad server settings " ,
fields : & [
2024-08-01 18:30:14 +02:00
CfgField ::Default { key : " server-url " , default : " https://md.berlin.ccc.de " , description : " Hedgedoc server storing the pads. " } ,
2024-07-29 15:12:26 +02:00
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). " }
] ,
} ,
2024-08-03 00:11:27 +02:00
CfgGroup { name : " wiki " ,
description : " API Settings for Mediawiki " ,
fields : & [
CfgField ::Default { key : " http-user " , default : " cccb-wiki " , description : " Username that would be needed if you would access the wiki with your web browser " } ,
CfgField ::Password { key : " http-pass " , description : " Password that would be needed if you would access the wiki with your web browser " } ,
CfgField ::Default { key : " api-user " , default : " PlenumBot@PlenumBot-PW1 " , description : " API Username that was generated in the mediawiki by the PlenumBot-Account " } ,
CfgField ::Password { key : " api-secret " , description : " API Secret that was generated in the mediawiki by the PlenumBot-Account. Example: \" PlenumBot-PW1@mgeg89bgit09q8bjblt5tn5ojj2d4gy \" " } ,
CfgField ::Default { key : " server-url " , default : " https://wiki.berlin.ccc.de/ " , description : " Link that is needed for getting the session token " }
] ,
} ,
// TODO: Matrix,
2024-07-29 15:12:26 +02:00
CfgGroup { name : " text " ,
description : " Various strings used. " ,
fields : & [
2024-08-01 18:30:14 +02:00
CfgField ::Default { key : " email-greeting " ,
default : " Hallo liebe Mitreisende, " ,
description : " \" Hello \" -greeting added at the start of every email. "
} ,
2024-07-29 15:12:26 +02:00
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 {
2024-07-30 03:57:24 +02:00
CfgField ::Silent { default , .. } = > {
config . default ( & field . full_key ( group . name ) , default )
2024-07-29 15:12:26 +02:00
} ,
2024-07-30 03:57:24 +02:00
CfgField ::Default { default , .. } = > {
config . default ( & field . full_key ( group . name ) , default )
2024-07-29 15:12:26 +02:00
} ,
2024-08-02 22:29:22 +02:00
_ = > { } ,
2024-07-29 15:12:26 +02:00
}
}
}
}
2024-07-30 03:57:24 +02:00
/// Check all groups / fields and interactively edit as needed.
///
/// Will report if the config is fine or has missing values.
2024-07-29 15:12:26 +02:00
pub fn interactive_check ( config : KV ) -> Result < ( ) , Box < dyn Error > > {
2024-08-02 22:29:22 +02:00
let mut all_valid : bool = true ;
2024-07-29 15:12:26 +02:00
for group in CONFIG_SPEC . groups {
2024-08-02 22:29:22 +02:00
group_header ( group . name , group . description ) ;
2024-07-30 03:57:24 +02:00
let todo = needs_info ( & config , group ) ;
2024-07-30 13:23:51 +02:00
// TODO: add distinction between edit all / edit necessary only
2024-07-30 03:57:24 +02:00
let choices = if ! todo . is_empty ( ) {
2024-08-02 22:29:22 +02:00
println! (
" {}The following fields need adjustment: {}{} " ,
ANSI_NOTICE ,
todo . join ( " , " ) ,
ANSI_RESET
) ;
2024-07-30 03:57:24 +02:00
& [ " [E]dit (default) " , " [L]ist all " , " [S]kip group " ]
} else {
2024-08-02 22:29:22 +02:00
println! (
" {}This group looks fine. OK to [S]kip, unless you want to adjust values here.{} " ,
ANSI_NOTICE , ANSI_RESET
) ;
& [ " [S]kip group (default) " , " [E]dit " , " [L]ist all " ]
2024-07-30 03:57:24 +02:00
} ;
loop {
2024-08-02 22:29:22 +02:00
let choice = prompt_action ( choices ) ;
2024-07-30 03:57:24 +02:00
match choice {
2024-08-02 22:29:22 +02:00
'L' = > {
show_group ( & config , group ) ;
} ,
2024-07-30 03:57:24 +02:00
'E' = > {
for field in group . fields {
2024-08-02 22:29:22 +02:00
check_field ( & config , group . name , field , & mut all_valid ) ? ;
2024-07-30 03:57:24 +02:00
}
break ;
} ,
'S' = > {
2024-08-02 22:29:22 +02:00
if ! todo . is_empty ( ) {
all_valid = false ;
}
2024-07-30 03:57:24 +02:00
break ;
} ,
2024-08-02 22:29:22 +02:00
_ = > {
unreachable! ( ) ;
} , // (prompt already checks)
2024-07-30 03:57:24 +02:00
}
2024-07-29 15:12:26 +02:00
}
}
2024-07-30 03:57:24 +02:00
if all_valid {
Ok ( ( ) )
} else {
Err ( " WARNING: Config is incomplete. Re-run to fix. " . into ( ) )
}
2024-07-29 15:12:26 +02:00
}
2024-08-02 22:29:22 +02:00
fn check_field (
config : & KV , grpname : & str , field : & CfgField , ok : & mut bool ,
) -> Result < ( ) , Box < dyn Error > > {
if field . is_silent ( ) {
return Ok ( ( ) ) ;
}
2024-07-30 03:57:24 +02:00
show_field ( config , grpname , field ) ;
2024-07-29 15:12:26 +02:00
let key = field . full_key ( grpname ) ;
let value = config . get ( & key ) . ok ( ) ;
2024-07-30 03:57:24 +02:00
let mut actions : Vec < & 'static str > = Vec ::new ( ) ;
2024-07-30 13:23:51 +02:00
// TODO: adjust order: empty password should offer input first
// TODO: RandomId should offer generating as [R]egenerate
2024-08-02 22:29:22 +02:00
if value . is_some ( ) | field . is_password ( ) | field . is_optional ( ) {
actions . push ( " [K]eep as-is " ) ;
}
if field . default_description ( ) . is_some ( ) {
actions . push ( " [R]eset to default " ) ;
}
if field . is_password ( ) {
actions . push ( " [R]emove " ) ;
}
actions . push ( " [I]nput new value " ) ;
if ! field . is_password ( ) {
actions . push ( " [L]ong (multiline) input " )
} ;
2024-07-30 13:23:51 +02:00
match prompt_action ( & actions ) {
'K' = > {
// we allow leaving a password empty, but that means config is incomplete
2024-08-02 22:29:22 +02:00
if value . is_none ( ) & ! field . is_optional ( ) {
* ok & = false ;
}
2024-07-30 13:23:51 +02:00
} ,
'R' = > {
match field . default_value ( ) ? {
2024-08-02 22:29:22 +02:00
Some ( value ) = > {
config . set ( & key , & value ) ? ;
} ,
2024-07-30 13:23:51 +02:00
// password again
2024-08-02 22:29:22 +02:00
None = > {
config . delete ( & key ) ? ;
* ok & = false ;
} ,
2024-07-30 13:23:51 +02:00
}
} ,
'I' = > {
let value = if field . is_password ( ) { prompt_password ( ) } else { prompt_single_line ( ) } ;
config . set ( & key , & value ) ? ;
} ,
'L' = > {
let value = prompt_multiline ( ) ;
config . set ( & key , & value ) ? ;
} ,
2024-08-02 22:29:22 +02:00
_ = > {
return Err ( " Wat. " . into ( ) ) ;
} ,
2024-07-29 15:12:26 +02:00
}
Ok ( ( ) )
}
2024-07-30 03:57:24 +02:00
/* * * * * * displaying various kinds of info * * * * * */
2024-08-02 22:29:22 +02:00
fn group_header ( name : & str , description : & str ) {
2024-07-29 15:12:26 +02:00
println! ( " ============================== " ) ;
println! ( " {} {} {} - {} " , ANSI_GROUP , name , ANSI_RESET , description ) ;
println! ( " " ) ;
}
2024-08-02 22:29:22 +02:00
fn show_group ( config : & KV , group : & CfgGroup ) {
2024-07-30 03:57:24 +02:00
for field in group . fields {
2024-08-02 22:29:22 +02:00
show_field ( & config , group . name , field ) ;
2024-07-30 03:57:24 +02:00
}
}
2024-08-02 22:29:22 +02:00
fn show_field ( config : & KV , grpname : & str , field : & CfgField ) {
if field . is_silent ( ) {
return ;
}
2024-07-30 03:57:24 +02:00
let key = field . full_key ( grpname ) ;
2024-08-02 22:29:22 +02:00
println! ( " {} {} {} - {} " , ANSI_FIELD , & key , ANSI_RESET , field . description ( ) ) ;
println! ( " default: {} " , field . default_description ( ) . unwrap_or ( EMPTY_VALUE . to_string ( ) ) ) ;
let value = config
. get ( & key )
. ok ( )
2024-07-30 03:57:24 +02:00
. map ( | s | if field . is_password ( ) { HIDDEN_PASSWORD . to_string ( ) } else { s } )
. unwrap_or ( EMPTY_VALUE . to_string ( ) ) ;
2024-08-02 22:29:22 +02:00
println! ( " current: {} " , value ) ;
2024-07-30 03:57:24 +02:00
}
/* * * * * * basic validation * * * * * */
2024-08-02 22:29:22 +02:00
fn needs_info ( config : & KV , group : & CfgGroup ) -> Vec < String > {
2024-07-30 03:57:24 +02:00
let mut acc = Vec ::new ( ) ;
for field in group . fields {
if field . is_action_required ( config , group . name ) {
2024-08-02 22:29:22 +02:00
acc . push ( field . key ( ) ) ;
2024-07-30 03:57:24 +02:00
}
}
acc
}
/* * * * * * prompting (with actual I / O) * * * * * */
/// Ask for a choice between several options. First option is the default,
/// format options with the first character in brackets like so: `"[E]xample"`.
2024-08-02 22:29:22 +02:00
fn prompt_action ( choices : & [ & str ] ) -> char {
2024-07-30 03:57:24 +02:00
let prompt_message = choices . join ( " , " ) ;
let default_choice = choices [ 0 ] . to_uppercase ( ) . chars ( ) . nth ( 1 ) . unwrap_or_default ( ) ;
loop {
2024-08-02 22:29:22 +02:00
println! ( " {} " , prompt_message ) ;
print! ( " Select: " ) ;
2024-07-30 03:57:24 +02:00
io ::stdout ( ) . flush ( ) . ok ( ) ;
// Read line and take first non-space character
let mut input = String ::new ( ) ;
io ::stdin ( ) . read_line ( & mut input ) . unwrap ( ) ;
let input = input . trim ( ) . to_uppercase ( ) . chars ( ) . next ( ) . unwrap_or ( default_choice ) ;
// Check list of choices for match
2024-08-02 22:29:22 +02:00
if choices
. iter ( )
. any ( | & choice | choice . to_uppercase ( ) . chars ( ) . nth ( 1 ) . unwrap_or_default ( ) = = input )
{
2024-07-30 03:57:24 +02:00
return input ;
} else {
println! ( " Invalid choice. Try again! " ) ;
}
}
}
/// Read a single line of text.
2024-08-02 22:29:22 +02:00
fn prompt_single_line ( ) -> String {
2024-07-29 15:12:26 +02:00
print! ( " New value: " ) ;
io ::stdout ( ) . flush ( ) . ok ( ) ;
let mut input = String ::new ( ) ;
io ::stdin ( ) . read_line ( & mut input ) . unwrap ( ) ;
input . trim ( ) . to_string ( )
}
2024-07-30 03:57:24 +02:00
/// Read multiple lines of text, terminated by a line containing just a '.'.
2024-08-02 22:29:22 +02:00
fn prompt_multiline ( ) -> String {
2024-07-29 15:12:26 +02:00
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
}
2024-07-30 03:57:24 +02:00
/// Read a password without echoing it.
2024-08-02 22:29:22 +02:00
fn prompt_password ( ) -> String {
2024-07-30 13:23:51 +02:00
let pass = rpassword ::prompt_password ( " New password (not shown) : " ) . unwrap ( ) ;
// disabled echo means the newline also isn't shown
println! ( " " ) ;
pass
2024-07-29 15:12:26 +02:00
}
2024-07-30 03:57:24 +02:00
/* * * * * * config structure definition * * * * * */
2024-07-29 15:12:26 +02:00
/// 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.
2024-08-02 22:29:22 +02:00
RandomId {
key : & ' a str ,
generator : fn ( ) -> Result < String , Box < dyn Error > > ,
generator_description : & ' a str ,
description : & ' a str ,
} ,
2024-07-29 15:12:26 +02:00
}
/// 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 > {
2024-07-30 03:57:24 +02:00
/// Silent fields don't get prompted or shown.
2024-07-29 15:12:26 +02:00
fn is_silent ( & self ) -> bool {
2024-08-02 22:29:22 +02:00
matches! ( self , CfgField ::Silent { .. } )
2024-07-29 15:12:26 +02:00
}
2024-07-30 03:57:24 +02:00
/// Password fields will be censored when displayed.
2024-07-29 15:12:26 +02:00
fn is_password ( & self ) -> bool {
2024-08-02 22:29:22 +02:00
matches! ( self , CfgField ::Password { .. } )
2024-07-30 03:57:24 +02:00
}
2024-07-30 13:23:51 +02:00
/// Optional fields are allowed to be (and stay) empty.
fn is_optional ( & self ) -> bool {
2024-08-02 22:29:22 +02:00
matches! ( self , CfgField ::Optional { .. } )
2024-07-30 13:23:51 +02:00
}
2024-07-30 03:57:24 +02:00
/// Reports if the field needs changing or is ok as-is.
///
/// (Because of the default population at boot, Silent and Default must
/// *necessarily* always have a value. Optional is optional. Only
/// Password and RandomId might need actions if they are missing.)
fn is_action_required ( & self , config : & KV , grpname : & str ) -> bool {
2024-08-02 22:29:22 +02:00
if matches! (
self ,
CfgField ::Silent { .. } | CfgField ::Default { .. } | CfgField ::Optional { .. }
) {
2024-07-30 03:57:24 +02:00
false
} else {
config . get ( self . full_key ( grpname ) . as_str ( ) ) . ok ( ) . is_none ( )
2024-07-29 15:12:26 +02:00
}
}
2024-07-30 03:57:24 +02:00
/// Gets `key`, which is the local part of the field name. (`.full_key` is used for the DB.)
2024-07-29 15:12:26 +02:00
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 ,
2024-08-02 22:29:22 +02:00
}
. to_string ( )
2024-07-29 15:12:26 +02:00
}
2024-07-30 03:57:24 +02:00
/// Full field name / key used in the DB (currently `{grpname}-{fieldname}`.)
2024-07-29 15:12:26 +02:00
fn full_key ( & self , grpname : & str ) -> String {
format! ( " {} - {} " , grpname , self . key ( ) )
}
2024-07-30 03:57:24 +02:00
/// Gets `description` or "(none)".
2024-07-29 15:12:26 +02:00
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 ,
2024-08-02 22:29:22 +02:00
}
. to_string ( )
2024-07-29 15:12:26 +02:00
}
2024-07-30 03:57:24 +02:00
/// Gets a description of the default value if one exists.
/// (Either the value itself, or a description of the generator making a default value.)
2024-07-30 13:23:51 +02:00
fn default_description ( & self ) -> Option < String > {
2024-07-29 15:12:26 +02:00
match self {
CfgField ::Silent { default , .. } = > Some ( default . to_string ( ) ) ,
CfgField ::Default { default , .. } = > Some ( default . to_string ( ) ) ,
2024-08-02 22:29:22 +02:00
CfgField ::RandomId { generator_description , .. } = > {
Some ( format! ( " {} {} {} " , '(' , generator_description , ')' ) )
} ,
2024-07-29 15:12:26 +02:00
_ = > None ,
}
}
2024-07-30 13:23:51 +02:00
/// Gets the actual default value, which may involve running a generator.
fn default_value ( & self ) -> Result < Option < String > , Box < dyn Error > > {
match self {
CfgField ::Silent { default , .. } = > Ok ( Some ( default . to_string ( ) ) ) ,
CfgField ::Default { default , .. } = > Ok ( Some ( default . to_string ( ) ) ) ,
CfgField ::RandomId { generator , .. } = > generator ( ) . map ( Some ) ,
_ = > Ok ( None ) ,
}
}
2024-07-29 15:12:26 +02:00
}