mit cargo fmt alles formatiert

This commit is contained in:
murmeldin 2024-08-02 22:29:22 +02:00 committed by murmeldin
parent c026a38239
commit c8031dfc01
5 changed files with 256 additions and 154 deletions

View file

@ -3,14 +3,14 @@ use std::error::Error;
use std::io::{self, Write};
/// Text shown for an empty field.
const EMPTY_VALUE : &str = "(empty)";
const EMPTY_VALUE: &str = "(empty)";
/// Text shown in place of a password.
const HIDDEN_PASSWORD : &str = "*****";
const HIDDEN_PASSWORD: &str = "*****";
// highlight some of the info
const ANSI_GROUP: &str = "\x1b[1;31m";
const ANSI_FIELD: &str = "\x1b[1;33m";
const ANSI_NOTICE: &str= "\x1b[33m";
const ANSI_NOTICE: &str = "\x1b[33m";
const ANSI_RESET: &str = "\x1b[0m";
const CONFIG_SPEC: CfgSpec<'static> = CfgSpec {
@ -70,7 +70,7 @@ pub fn populate_defaults(config: &KV) {
CfgField::Default { default, .. } => {
config.default(&field.full_key(group.name), default)
},
_ => {}
_ => {},
}
}
}
@ -80,33 +80,47 @@ pub fn populate_defaults(config: &KV) {
///
/// Will report if the config is fine or has missing values.
pub fn interactive_check(config: KV) -> Result<(), Box<dyn Error>> {
let mut all_valid : bool = true;
let mut all_valid: bool = true;
for group in CONFIG_SPEC.groups {
group_header( group.name, group.description );
group_header(group.name, group.description);
let todo = needs_info(&config, group);
// TODO: add distinction between edit all / edit necessary only
let choices = if !todo.is_empty() {
println!( "{}The following fields need adjustment: {}{}", ANSI_NOTICE, todo.join(", "), ANSI_RESET );
println!(
"{}The following fields need adjustment: {}{}",
ANSI_NOTICE,
todo.join(", "),
ANSI_RESET
);
&["[E]dit (default)", "[L]ist all", "[S]kip group"]
} else {
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", ]
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"]
};
loop {
let choice = prompt_action( choices );
let choice = prompt_action(choices);
match choice {
'L' => { show_group(&config, group); },
'L' => {
show_group(&config, group);
},
'E' => {
for field in group.fields {
check_field( &config, group.name, field, &mut all_valid )?;
check_field(&config, group.name, field, &mut all_valid)?;
}
break;
},
'S' => {
if !todo.is_empty() { all_valid = false; }
if !todo.is_empty() {
all_valid = false;
}
break;
},
_ => { unreachable!(); }, // (prompt already checks)
_ => {
unreachable!();
}, // (prompt already checks)
}
}
}
@ -117,30 +131,49 @@ pub fn interactive_check(config: KV) -> Result<(), Box<dyn Error>> {
}
}
fn check_field( config: &KV, grpname: &str, field: &CfgField, ok: &mut bool ) -> Result<(), Box<dyn Error>> {
if field.is_silent() { return Ok(()) }
fn check_field(
config: &KV, grpname: &str, field: &CfgField, ok: &mut bool,
) -> Result<(), Box<dyn Error>> {
if field.is_silent() {
return Ok(());
}
show_field(config, grpname, field);
let key = field.full_key(grpname);
let value = config.get(&key).ok();
let mut actions: Vec<&'static str> = Vec::new();
// TODO: adjust order: empty password should offer input first
// TODO: RandomId should offer generating as [R]egenerate
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" ) };
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")
};
match prompt_action(&actions) {
'K' => {
// we allow leaving a password empty, but that means config is incomplete
if value.is_none() & !field.is_optional() { *ok &= false; }
if value.is_none() & !field.is_optional() {
*ok &= false;
}
},
'R' => {
match field.default_value()? {
Some(value) => { config.set(&key, &value)?; },
Some(value) => {
config.set(&key, &value)?;
},
// password again
None => { config.delete(&key)?; *ok &= false; },
None => {
config.delete(&key)?;
*ok &= false;
},
}
},
'I' => {
@ -151,43 +184,49 @@ fn check_field( config: &KV, grpname: &str, field: &CfgField, ok: &mut bool ) ->
let value = prompt_multiline();
config.set(&key, &value)?;
},
_ => { return Err("Wat.".into()); }
_ => {
return Err("Wat.".into());
},
}
Ok(())
}
/* ***** displaying various kinds of info ***** */
fn group_header( name: &str, description: &str ) {
fn group_header(name: &str, description: &str) {
println!("==============================");
println!("{}{}{} - {}", ANSI_GROUP, name, ANSI_RESET, description);
println!("");
}
fn show_group( config: &KV, group: &CfgGroup ) {
fn show_group(config: &KV, group: &CfgGroup) {
for field in group.fields {
show_field( &config, group.name, field );
show_field(&config, group.name, field);
}
}
fn show_field( config: &KV, grpname: &str, field: &CfgField ) {
if field.is_silent() { return }
fn show_field(config: &KV, grpname: &str, field: &CfgField) {
if field.is_silent() {
return;
}
let key = field.full_key(grpname);
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()
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()
.map(|s| if field.is_password() { HIDDEN_PASSWORD.to_string() } else { s })
.unwrap_or(EMPTY_VALUE.to_string());
println!( " current: {}", value );
println!(" current: {}", value);
}
/* ***** basic validation ***** */
fn needs_info( config: &KV, group: &CfgGroup ) -> Vec<String> {
fn needs_info(config: &KV, group: &CfgGroup) -> Vec<String> {
let mut acc = Vec::new();
for field in group.fields {
if field.is_action_required(config, group.name) {
acc.push( field.key() );
acc.push(field.key());
}
}
acc
@ -197,12 +236,12 @@ fn needs_info( config: &KV, group: &CfgGroup ) -> Vec<String> {
/// Ask for a choice between several options. First option is the default,
/// format options with the first character in brackets like so: `"[E]xample"`.
fn prompt_action( choices: &[&str] ) -> char {
fn prompt_action(choices: &[&str]) -> char {
let prompt_message = choices.join(", ");
let default_choice = choices[0].to_uppercase().chars().nth(1).unwrap_or_default();
loop {
println!( "{}", prompt_message );
print!( "Select: " );
println!("{}", prompt_message);
print!("Select: ");
io::stdout().flush().ok();
// Read line and take first non-space character
@ -211,7 +250,10 @@ fn prompt_action( choices: &[&str] ) -> char {
let input = input.trim().to_uppercase().chars().next().unwrap_or(default_choice);
// Check list of choices for match
if choices.iter().any(|&choice| choice.to_uppercase().chars().nth(1).unwrap_or_default() == input) {
if choices
.iter()
.any(|&choice| choice.to_uppercase().chars().nth(1).unwrap_or_default() == input)
{
return input;
} else {
println!("Invalid choice. Try again!");
@ -220,7 +262,7 @@ fn prompt_action( choices: &[&str] ) -> char {
}
/// Read a single line of text.
fn prompt_single_line( ) -> String {
fn prompt_single_line() -> String {
print!("New value: ");
io::stdout().flush().ok();
let mut input = String::new();
@ -229,7 +271,7 @@ fn prompt_single_line( ) -> String {
}
/// Read multiple lines of text, terminated by a line containing just a '.'.
fn prompt_multiline( ) -> String {
fn prompt_multiline() -> String {
println!("Enter new value: (end with '.' on a new line)");
let mut acc = String::new();
loop {
@ -244,7 +286,7 @@ fn prompt_multiline( ) -> String {
}
/// Read a password without echoing it.
fn prompt_password( ) -> String {
fn prompt_password() -> String {
let pass = rpassword::prompt_password("New password (not shown) : ").unwrap();
// disabled echo means the newline also isn't shown
println!("");
@ -264,7 +306,12 @@ enum CfgField<'a> {
/// 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() -> Result<String,Box<dyn Error>>, generator_description: &'a str, description: &'a str },
RandomId {
key: &'a str,
generator: fn() -> Result<String, Box<dyn Error>>,
generator_description: &'a str,
description: &'a str,
},
}
/// A group of related config fields. The final key of an inner value will be
@ -285,17 +332,17 @@ struct CfgSpec<'a> {
impl<'a> CfgField<'a> {
/// Silent fields don't get prompted or shown.
fn is_silent(&self) -> bool {
matches!( self, CfgField::Silent { .. } )
matches!(self, CfgField::Silent { .. })
}
/// Password fields will be censored when displayed.
fn is_password(&self) -> bool {
matches!( self, CfgField::Password { .. } )
matches!(self, CfgField::Password { .. })
}
/// Optional fields are allowed to be (and stay) empty.
fn is_optional(&self) -> bool {
matches!( self, CfgField::Optional { .. } )
matches!(self, CfgField::Optional { .. })
}
/// Reports if the field needs changing or is ok as-is.
@ -304,7 +351,10 @@ impl<'a> CfgField<'a> {
/// *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 {
if matches!( self, CfgField::Silent { .. } | CfgField::Default { .. } | CfgField::Optional { .. } ) {
if matches!(
self,
CfgField::Silent { .. } | CfgField::Default { .. } | CfgField::Optional { .. }
) {
false
} else {
config.get(self.full_key(grpname).as_str()).ok().is_none()
@ -319,7 +369,8 @@ impl<'a> CfgField<'a> {
CfgField::Optional { key, .. } => key,
CfgField::Password { key, .. } => key,
CfgField::RandomId { key, .. } => key,
}.to_string()
}
.to_string()
}
/// Full field name / key used in the DB (currently `{grpname}-{fieldname}`.)
@ -335,7 +386,8 @@ impl<'a> CfgField<'a> {
CfgField::Optional { description, .. } => description,
CfgField::Password { description, .. } => description,
CfgField::RandomId { description, .. } => description,
}.to_string()
}
.to_string()
}
/// Gets a description of the default value if one exists.
@ -344,7 +396,9 @@ impl<'a> CfgField<'a> {
match self {
CfgField::Silent { default, .. } => Some(default.to_string()),
CfgField::Default { default, .. } => Some(default.to_string()),
CfgField::RandomId { generator_description, .. } => Some(format!("{}{}{}",'(',generator_description,')')),
CfgField::RandomId { generator_description, .. } => {
Some(format!("{}{}{}", '(', generator_description, ')'))
},
_ => None,
}
}

View file

@ -1,63 +1,61 @@
use reqwest::blocking::Response;
use reqwest::blocking::Client;
use reqwest::blocking::Response;
use std::error::Error;
// TODO: implement dry-run logic
pub struct HedgeDoc {
server_url : String,
is_dry_run : bool,
client : Client,
server_url: String,
is_dry_run: bool,
client: Client,
}
impl HedgeDoc {
pub fn new( server_url: &str, is_dry_run: bool ) -> Self {
pub fn new(server_url: &str, is_dry_run: bool) -> Self {
Self { server_url: server_url.to_string(), is_dry_run, client: Client::new() }
}
pub fn format_url( &self, pad_name: &str ) -> String {
format!( "{}/{}", self.server_url, pad_name )
pub fn format_url(&self, pad_name: &str) -> String {
format!("{}/{}", self.server_url, pad_name)
}
fn format_action( &self, pad_name: &str, verb: &str ) -> String {
format!( "{}/{}/{}", self.server_url, pad_name, verb )
fn format_action(&self, pad_name: &str, verb: &str) -> String {
format!("{}/{}/{}", self.server_url, pad_name, verb)
}
fn do_request(&self, url : &str ) -> Result<Response, Box<dyn Error>> {
Ok(self.client.get( url ).send().unwrap())
fn do_request(&self, url: &str) -> Result<Response, Box<dyn Error>> {
Ok(self.client.get(url).send().unwrap())
}
fn get_id_from_response( &self, res : Response ) -> String {
res.url().to_string().trim_start_matches( &format!( "{}/", self.server_url ) ).to_string()
fn get_id_from_response(&self, res: Response) -> String {
res.url().to_string().trim_start_matches(&format!("{}/", self.server_url)).to_string()
}
pub fn download( &self, pad_name: &str ) -> Result<String, Box<dyn Error>> {
Ok(self.do_request( &self.format_action(pad_name, "download"))?.text()?)
pub fn download(&self, pad_name: &str) -> Result<String, Box<dyn Error>> {
Ok(self.do_request(&self.format_action(pad_name, "download"))?.text()?)
}
pub fn create_pad( &self ) -> Result<String, Box<dyn Error>> {
let res = self.do_request( &format!( "{}/new", self.server_url ) ).unwrap();
pub fn create_pad(&self) -> Result<String, Box<dyn Error>> {
let res = self.do_request(&format!("{}/new", self.server_url)).unwrap();
if res.status().is_success() {
Ok(self.get_id_from_response(res))
} else {
Err( format!("Failed to create pad {}", res.status()).into() )
Err(format!("Failed to create pad {}", res.status()).into())
}
}
pub fn import_note( &self, id: Option<&str>, content: String ) -> Result<String, Box<dyn Error>> {
pub fn import_note(&self, id: Option<&str>, content: String) -> Result<String, Box<dyn Error>> {
let url = match id {
Some(id) => self.format_url( &format!( "new/{id}" ) ),
Some(id) => self.format_url(&format!("new/{id}")),
None => self.format_url("new"),
};
let res = self.client.post(&url)
.header( "Content-Type", "text/markdown" )
.body(content)
.send()?;
let res =
self.client.post(&url).header("Content-Type", "text/markdown").body(content).send()?;
if res.status().is_success() {
Ok(self.get_id_from_response(res))
} else {
Err( format!("Failed to import note: {}", res.status()).into() )
Err(format!("Failed to import note: {}", res.status()).into())
}
}
}

View file

@ -32,12 +32,11 @@ mod mediawiki;
mod config_check;
pub mod variables_and_settings;
use std::borrow::Cow;
use std::error::Error;
use std::env;
use std::error::Error;
use std::fs::File;
use std::io::Read;
// use std::future::Future;
@ -47,9 +46,9 @@ use clap::{Arg, Command};
use regex::Regex;
use uuid::Uuid;
use lettre::{Message, SmtpTransport, Transport};
use lettre::message::{header, SinglePart};
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
const FALLBACK_TEMPLATE: &str = variables_and_settings::FALLBACK_TEMPLATE;
@ -86,7 +85,8 @@ fn parse_args() -> Args {
.action(clap::ArgAction::Set)
.default_value("config.sqlite")
.help("specifies an alternate config file"),
).get_matches();
)
.get_matches();
Args {
check_mode: *matches.get_one::<bool>("check").unwrap(),
config_file: matches.get_one::<String>("config_file").unwrap().clone(),
@ -102,12 +102,12 @@ fn main() -> Result<(), Box<dyn Error>> {
let config = KV::new(config_file).unwrap();
config_check::populate_defaults(&config);
if args.check_mode {
return config_check::interactive_check( config );
return config_check::interactive_check(config);
}
// config
let hedgedoc = HedgeDoc::new( &config["hedgedoc-server-url"], is_dry_run() );
let hedgedoc = HedgeDoc::new(&config["hedgedoc-server-url"], is_dry_run());
println!("[START]\nAktueller Zustand der DB:");
config.dump_redacting(&["email-password","wiki-password","matrix-password"]).ok();
config.dump_redacting(&["email-password", "wiki-password", "matrix-password"]).ok();
// Dienstage diesen Monat
let all_tuesdays: Vec<NaiveDate> = get_all_weekdays(0, Weekday::Tue);
@ -120,7 +120,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let vierter_dienstag_nächster_monat: String = all_tuesdays_next_month[3].to_string();
// Daten, die später benutzt werden, definieren
let today= Local::now();
let today = Local::now();
let today_simple = today.date_naive();
let yesterday = today_simple.pred_opt().unwrap();
let in_1_day = today_simple.succ_opt().unwrap();
@ -129,23 +129,25 @@ fn main() -> Result<(), Box<dyn Error>> {
// Nächste Plena nachschauen:
let nächster_plenumtermin: &String = if all_tuesdays[1] >= yesterday { // Für das Pad rumschicken am nächsten Tag wird das Datum einen Tag nach Hinten gesetzt,
let nächster_plenumtermin: &String = if all_tuesdays[1] >= yesterday {
// Für das Pad rumschicken am nächsten Tag wird das Datum einen Tag nach Hinten gesetzt,
&zweiter_dienstag
} else {
} else {
&vierter_dienstag
};
let übernächster_plenumtermin = if all_tuesdays[1] >= yesterday { // hier das Gleiche.
let übernächster_plenumtermin = if all_tuesdays[1] >= yesterday {
// hier das Gleiche.
&vierter_dienstag
} else {
&zweiter_dienstag_nächster_monat
};
let überübernächster_plenumtermin = if all_tuesdays[1] >= yesterday { // hier das Gleiche.
let überübernächster_plenumtermin = if all_tuesdays[1] >= yesterday {
// hier das Gleiche.
&zweiter_dienstag_nächster_monat
} else {
&vierter_dienstag_nächster_monat
};
// Der Code muss nur für vor dem 2. und vor dem 4. Dienstag gebaut werden, weil im nächsten Monat der Code frühestens 7 Tage vor dem Plenum wieder passt.
let in_1_day_is_plenum: bool = check_if_plenum(nächster_plenumtermin.clone(), in_1_day);
@ -153,8 +155,12 @@ fn main() -> Result<(), Box<dyn Error>> {
let yesterday_was_plenum: bool = check_if_plenum(nächster_plenumtermin.clone(), yesterday);
// Pad-Links aus der Datenbank laden:
let current_pad_id = config.get("hedgedoc-last-id").expect("ID des aktuellen Pads undefiniert. Bitte in der DB eintragen oder generieren.");
let future_pad_id = config.get("hedgedoc-next-id").expect("ID des nächsten Pads undefiniert. Bitte in der DB eintragen oder generieren.");
let current_pad_id = config
.get("hedgedoc-last-id")
.expect("ID des aktuellen Pads undefiniert. Bitte in der DB eintragen oder generieren.");
let future_pad_id = config
.get("hedgedoc-next-id")
.expect("ID des nächsten Pads undefiniert. Bitte in der DB eintragen oder generieren.");
let mut message_id: Option<String> = None;
@ -172,7 +178,7 @@ fn main() -> Result<(), Box<dyn Error>> {
if in_1_day_is_plenum {
println!("In 1 Tag ist Plenum, deshalb wird eine Erinnerung raus geschickt!");
let tldr_vec = create_tldr(&pad_content_without_top_instructions);
let mut tldr = String::new();
let mut tldr = String::new();
for element in tldr_vec {
tldr.push_str("\n");
tldr.push_str(&element)
@ -180,7 +186,8 @@ fn main() -> Result<(), Box<dyn Error>> {
if number_of_tops(&pad_content_without_top_instructions) != 0 {
// Mail an alle senden, findet statt
let betreff = "Morgen ist Plenum!".to_string(); // ADJ_TIMEYWIMEY
let message: String = format!(r#"{email_greeting}
let message: String = format!(
r#"{email_greeting}
Es gibt Themen, deshalb wird das morgige Plenum statt finden. Anbei das Plenumspad:
{current_pad_link}
@ -190,14 +197,16 @@ Und hier ein TL;DR von den aktuellen Themen:
Bis morgen, 20 Uhr!
{email_signature}"#); // ADJ_TIMEYWIMEY
{email_signature}"#
); // ADJ_TIMEYWIMEY
println!("---E-Mail:---\n{}\n-----------", message);
// XXX option x expect
message_id = Some(mail_versenden(&config, betreff, message).expect("Plenum findet statt. Mail wurde versucht zu senden, konnte aber nicht gesendet werden!"))
} else {
// Mail an alle senden und absagen
let betreff = format!("Plenum am {} fällt mangels Themen aus", nächster_plenumtermin);
let message: String = format!(r#"{email_greeting}
let message: String = format!(
r#"{email_greeting}
Es gibt keine Themen, deshalb wird das morgige Plenum leider nicht statt finden.
@ -206,7 +215,9 @@ Hier ist der Link zum Pad vom nächsten Plenum, das am {} statt finden wird:
Bis zum nächsten Plenum.
{email_signature}"#, nächster_plenumtermin);
{email_signature}"#,
nächster_plenumtermin
);
println!("---E-Mail:---\n{}\n-----------", message);
// XXX option x expect
message_id = Some(mail_versenden(&config, betreff, message).expect("Plenum wird abgesagt. Mail wurde versucht zu senden, konnte aber nicht gesendet werden!"))
@ -216,29 +227,38 @@ Bis zum nächsten Plenum.
if number_of_tops(&pad_content_without_top_instructions) == 0 {
// Mail an alle senden und sagen, dass es noch keine Themen gibt
let betreff = format!("Plenum vom {}: Bisher noch keine Themen", nächster_plenumtermin);
let message: String = format!(r#"{email_greeting}
let message: String = format!(
r#"{email_greeting}
Es sind bisher leider keine Themen zusammengekommen. Wenn es bis Sonntag Abend keine Themen gibt, wird das Plenum voraussichtlich nicht statt finden.
Hier ist der Link zum Pad, wo ihr noch Themen eintragen könnt:
{current_pad_link}
{email_signature}"#);
{email_signature}"#
);
println!("---E-Mail:---\n{}\n-----------", message);
// XXX option x expect
message_id = Some(mail_versenden(&config, betreff, message).expect("Noch nicht genug Themen. Mail wurde versucht zu senden, konnte aber nicht gesendet werden!"))
}
} else if yesterday_was_plenum {
// This logic breaks on 02/2034, but on every other month it works
let old_pad_content = hedgedoc.download(&current_pad_id).expect("Fehler beim Hedgedoc-Pad-Download!");
let old_pad_content =
hedgedoc.download(&current_pad_id).expect("Fehler beim Hedgedoc-Pad-Download!");
// MUSS WIEDER REIN NACH DEM TESTEN: generate_new_pad_for_following_date(übernächster_plenumtermin, überübernächster_plenumtermin, &config).await.expect("Fehler! Plenumspad konnte nicht generiert werden!");
println!("DATENBANK: aktuelles-plenumspad: {:?} und zukünftiges plenumspad: {:?}", &config.get("hedgedoc-last-id"), &config.get("hedgedoc-next-id"));
println!(
"DATENBANK: aktuelles-plenumspad: {:?} und zukünftiges plenumspad: {:?}",
&config.get("hedgedoc-last-id"),
&config.get("hedgedoc-next-id")
);
let old_pad_content_without_top_instructions = try_to_remove_top_instructions(old_pad_content);
let old_pad_content_without_top_instructions =
try_to_remove_top_instructions(old_pad_content);
let tldr_vec = create_tldr(&old_pad_content_without_top_instructions);
let tldr = tldr_vec.join("\n");
let tldr = tldr_vec.join("\n");
// XXX nächstes/übernächstes?
let message: String = format!(r#"{email_greeting}
let message: String = format!(
r#"{email_greeting}
Anbei das gestrige Plenumspad. Hier sind die Links zum nächsten:
{current_pad_link}
@ -252,8 +272,10 @@ Und hier ist das Protokoll des letzten Plenums:
{old_pad_content_without_top_instructions}
{email_signature}"#);
let betreff: String = format!("Plenumsprotokoll vom {}: Es gab {} TOPs", nächster_plenumtermin, top_anzahl);
{email_signature}"#
);
let betreff: String =
format!("Plenumsprotokoll vom {}: Es gab {} TOPs", nächster_plenumtermin, top_anzahl);
println!("---E-Mail:---\n{}\n-----------", message);
// XXX option x expect
message_id = Some(mail_versenden(&config, betreff, message).expect("Mail mit Plenumsprotokoll wurde versucht zu senden, konnte aber nicht gesendet werden!"));
@ -262,19 +284,24 @@ Und hier ist das Protokoll des letzten Plenums:
println!("message id: {:?}", message_id);
println!("[ENDE]\nAktueller Zustand der DB:");
config.dump_redacting(&["email-password","matrix-password","wiki-password"]).ok();
if config.has_errors() { Err("There were errors.".into()) } else { Ok(()) }
config.dump_redacting(&["email-password", "matrix-password", "wiki-password"]).ok();
if config.has_errors() {
Err("There were errors.".into())
} else {
Ok(())
}
}
fn get_all_weekdays(month_offset: i32, week_day: Weekday) -> Vec<NaiveDate>{
fn get_all_weekdays(month_offset: i32, week_day: Weekday) -> Vec<NaiveDate> {
let date = Local::now().date_naive();
let date = match month_offset.signum() {
0 => Some(date),
1 => date.checked_add_months(chrono::Months::new(month_offset as u32)),
0 => Some(date),
1 => date.checked_add_months(chrono::Months::new(month_offset as u32)),
-1 => date.checked_sub_months(chrono::Months::new((-month_offset) as u32)),
_ => unreachable!(),
}.expect("(very) invalid month offset");
_ => unreachable!(),
}
.expect("(very) invalid month offset");
let month = date.month();
let mut current_date = NaiveDate::from_ymd_opt(date.year(), date.month(), 1);
let mut dates = Vec::new();
@ -290,30 +317,28 @@ fn get_all_weekdays(month_offset: i32, week_day: Weekday) -> Vec<NaiveDate>{
fn check_if_plenum(infrage_kommendes_plenum: String, date_to_check: NaiveDate) -> bool {
// Überprüfen, ob an dem Datum Plenum ist
let date_to_check = date_to_check.to_string();
return if infrage_kommendes_plenum == date_to_check {
true
} else {
false
}
return if infrage_kommendes_plenum == date_to_check { true } else { false };
}
fn number_of_tops (pad_content: &String) -> i32 {
fn number_of_tops(pad_content: &String) -> i32 {
// Logik: Wenn irgendwo TOP 2, top2, Top2 oder TOP2 steht, dann gibt es Themen
let re = Regex::new(r"^##+ *([Tt][Oo][Pp] +\d[.\d]*.*)$").unwrap();
let m = re.find(&pad_content);
m.iter().len() as i32
}
fn create_tldr (pad_content: &String) -> Vec<&str> {
fn create_tldr(pad_content: &String) -> Vec<&str> {
// Logik: Wenn irgendwo TOP 2, top2, Top2 oder TOP2 steht, dann gibt es Themen
let re_top = Regex::new(r"^##+ *([Tt][Oo][Pp] +\d[.\d]*.*)$").unwrap();
let tldr: Vec<&str> = re_top.find_iter(&pad_content).map(|m|m.as_str()).collect();
let tldr: Vec<&str> = re_top.find_iter(&pad_content).map(|m| m.as_str()).collect();
//println!("{:?}", m);
tldr
//tldr_vec.append() = m.iter()
}
fn mail_versenden(config: &KV, betreff: String, inhalt: String) -> std::result::Result<String, Box<dyn std::error::Error>> {
fn mail_versenden(
config: &KV, betreff: String, inhalt: String,
) -> std::result::Result<String, Box<dyn std::error::Error>> {
// Define the email
let message_id: String = Uuid::new_v4().to_string() + &String::from("@berlin.ccc.de");
@ -328,59 +353,74 @@ fn mail_versenden(config: &KV, betreff: String, inhalt: String) -> std::result::
.in_reply_to("19de3985-05b4-42f3-9a6a-e941479be2ed@berlin.ccc.de".to_string())
// Set the subject of the email
.subject(betreff)
.singlepart(SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(inhalt))
.singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(inhalt))
.unwrap();
if !is_dry_run() {
// Set up the SMTP client
let creds = Credentials::new(config["email-user"].to_string(), config["email-password"].to_string());
let creds = Credentials::new(
config["email-user"].to_string(),
config["email-password"].to_string(),
);
// Open a remote connection to gmail
let mailer = SmtpTransport::starttls_relay(&config["email-server"])?
.credentials(creds)
.build();
let mailer =
SmtpTransport::starttls_relay(&config["email-server"])?.credentials(creds).build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => eprintln!("Could not send email: {:?}", e), }
Err(e) => eprintln!("Could not send email: {:?}", e),
}
Ok(message_id)
} else {
println!( "[DRY RUN - NOT] sending email\n(raw message:)\n{}", std::str::from_utf8(&email.formatted()).unwrap_or("((UTF-8 error))") );
println!(
"[DRY RUN - NOT] sending email\n(raw message:)\n{}",
std::str::from_utf8(&email.formatted()).unwrap_or("((UTF-8 error))")
);
Ok("dummy-message-id@localhost".to_string())
}
}
fn generate_new_pad_for_following_date(config : KV, hedgedoc : HedgeDoc, übernächster_plenumtermin: &String, überübernächster_plenumtermin: &String, kv: &KV) -> Result<(), Box<dyn Error>> {
match hedgedoc.create_pad( ) {
fn generate_new_pad_for_following_date(
config: KV, hedgedoc: HedgeDoc, übernächster_plenumtermin: &String,
überübernächster_plenumtermin: &String, kv: &KV,
) -> Result<(), Box<dyn Error>> {
match hedgedoc.create_pad() {
Err(e) => println!("Failed to create pad: {}", e),
Ok(pad_id) => {
println!("Pad created successfully with ID: {}", pad_id);
// Get the most recent plenum template and replace the placeholders:
let template_content: String = match config.get("hedgedoc-template-name") {
Ok(content) => hedgedoc.download(&content.clone()).unwrap_or_else(|_| FALLBACK_TEMPLATE.to_string()),
Ok(content) => hedgedoc
.download(&content.clone())
.unwrap_or_else(|_| FALLBACK_TEMPLATE.to_string()),
Err(_) => FALLBACK_TEMPLATE.to_string(),
};
// XXX you don't just use the template as-is…
let template_modified: String = replace_placeholders(&template_content, übernächster_plenumtermin, überübernächster_plenumtermin).unwrap_or_else(|error |template_content); // Try regex, if not successful use without regex
let template_modified: String = replace_placeholders(
&template_content,
übernächster_plenumtermin,
überübernächster_plenumtermin,
)
.unwrap_or_else(|error| template_content); // Try regex, if not successful use without regex
match hedgedoc.import_note( Some(&pad_id), template_modified ) {
match hedgedoc.import_note(Some(&pad_id), template_modified) {
Ok(_) => {
println!("Pad updated successfully with template content.");
rotate (&pad_id, kv);
rotate(&pad_id, kv);
},
Err(e) => println!("Failed to update pad: {}", e),
}
}
},
}
Ok(())
}
fn replace_placeholders(template: &str, übernächster_plenumtermin: &str, überübernächster_plenumtermin: &str) -> Result<String, Box<dyn Error>> {
fn replace_placeholders(
template: &str, übernächster_plenumtermin: &str, überübernächster_plenumtermin: &str,
) -> Result<String, Box<dyn Error>> {
let re_datum = Regex::new(r"\{\{Datum\}\}")?;
let result = re_datum.replace_all(template, übernächster_plenumtermin);
let re_naechstes_plenum = Regex::new(r"\{\{naechstes-plenum\}\}")?;
@ -388,18 +428,23 @@ fn replace_placeholders(template: &str, übernächster_plenumtermin: &str, über
Ok(result.to_string())
}
fn rotate (future_pad_id: &str, kv: &KV) {
fn rotate(future_pad_id: &str, kv: &KV) {
let next_plenum_pad = kv.get("zukünftiges-plenumspad").ok();
if next_plenum_pad == None {
kv.set("zukünftiges-plenumspad", &future_pad_id).expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!"); // Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
kv.set("zukünftiges-plenumspad", &future_pad_id)
.expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!");
// Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
} else {
kv.set("aktuelles-plenumspad", &next_plenum_pad.unwrap()).expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!"); // Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
kv.set("zukünftiges-plenumspad", &future_pad_id).expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!"); // Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
kv.set("aktuelles-plenumspad", &next_plenum_pad.unwrap())
.expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!"); // Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
kv.set("zukünftiges-plenumspad", &future_pad_id)
.expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!");
// Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
}
}
fn try_to_remove_top_instructions (pad_content: String) -> String {
fn try_to_remove_top_instructions(pad_content: String) -> String {
let re_top_instructions: Regex = Regex::new(r"(<!--(?:.||\n)*-->)").unwrap();
let result: Cow<str> = re_top_instructions.replace_all(&pad_content, "---");
result.to_string() // Wenn es nicht geklappt hat, wird einfach das Pad mit dem Kommentar zurückgegeben
}
}

View file

@ -6,18 +6,20 @@ pub fn pad_ins_wiki(old_pad_content: String) {
convert_markdown_to_mediawiki_and_save_as_txt(old_pad_content);
// Textdatei wieder einlesen
let mut file = File::open("pandoc-output.txt").expect("Fehler beim öffnen der MediaWiki-Textdatei!");
let mut file =
File::open("pandoc-output.txt").expect("Fehler beim öffnen der MediaWiki-Textdatei!");
let mut contents = String::new();
file.read_to_string(&mut contents).expect("Fehler beim auslesen der MediaWiki-Textdatei!");
// Passwörter aus Datenbank lesen (ToBeDone)
let plenum_bot_user = String::from("PlenumBot@PlenumBot-PW1");
let plenum_bot_pw = String::from("**OLD_API_PW_REMOVED**");
let login_token = login_to_mediawiki(plenum_bot_user.clone(), plenum_bot_pw.clone()).expect("Fehler beim Einloggen!");
let login_token = login_to_mediawiki(plenum_bot_user.clone(), plenum_bot_pw.clone())
.expect("Fehler beim Einloggen!");
println!("plenum_bot_user: {plenum_bot_user}, plenum_bot_pw: {plenum_bot_pw}, login_token: {login_token}")
}
fn convert_markdown_to_mediawiki_and_save_as_txt (old_pad_content: String) {
fn convert_markdown_to_mediawiki_and_save_as_txt(old_pad_content: String) {
//Convert Markdown into Mediawiki
// Vanilla pandoc Befehl: pandoc --from markdown --to mediawiki --no-highlight
let mut p = pandoc::new();
@ -25,10 +27,13 @@ fn convert_markdown_to_mediawiki_and_save_as_txt (old_pad_content: String) {
p.set_input_format(pandoc::InputFormat::Markdown, vec![]);
p.set_output(pandoc::OutputKind::File("./pandoc-output.txt".parse().unwrap()));
p.set_output_format(pandoc::OutputFormat::MediaWiki, vec![]);
p.execute().expect("Fehler beim Umwandeln des und speichern des Pads in eine mediawiki-Textdatei");
p.execute()
.expect("Fehler beim Umwandeln des und speichern des Pads in eine mediawiki-Textdatei");
}
fn login_to_mediawiki (plenum_bot_user: String, plenum_bot_pw: String) -> Result<String, Box<dyn Error>> {
fn login_to_mediawiki(
plenum_bot_user: String, plenum_bot_pw: String,
) -> Result<String, Box<dyn Error>> {
//let mut map = HashMap::new();
//map.insert("logintoken", "result");
let username = "cccb-wiki";
@ -50,7 +55,7 @@ fn login_to_mediawiki (plenum_bot_user: String, plenum_bot_pw: String) -> Result
let html_source = resp.text()?;
//let login_token: String = map.get("logintoken").unwrap().to_string().clone();
println!("---HTML:---\n{}\n-----------", html_source);
*/
Ok(String::from("unimplemented"))
}
}

View file

@ -40,4 +40,4 @@ Nächstes Plenum: Am 2024-08-13 um 20 Uhr
---"#;
pub const PLENUM_TEMPLATE_URL: &str = "https://md.berlin.ccc.de/plenum-template/download";
pub const HEDGEDOC_SERVER_URL: &str = "https://md.berlin.ccc.de";
pub const HEDGEDOC_SERVER_URL: &str = "https://md.berlin.ccc.de";