diff --git a/src/main.rs b/src/main.rs index 26165f2..86e94ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -179,14 +179,22 @@ fn main() -> Result<(), Box> { is_dry_run(), ); // figure out where we are - let last_state = ProgramState::parse(&config["state-name"]); + let mut last_state = ProgramState::parse(&config["state-name"]); let last_run = config.get("state-last-run").unwrap_or_default(); let last_run = NaiveDate::parse_from_str(&last_run, "%Y-%m-%d").unwrap_or_default(); // figure out where we should be - let plenum_spec = date::parse_spec(&config["date-spec"])?; let today = Local::now().date_naive(); + if (today - last_run).num_days() > 10 { + if !matches!(last_state, ProgramState::Normal) { + eprintln!("WARNING: last run was a long time ago, resetting state."); + last_state = ProgramState::Normal; + do_cleanup(999, &config, &hedgedoc, &email, &wiki)?; + } + } + let last_state = last_state; + let plenum_spec = date::parse_spec(&config["date-spec"])?; let nearest_plenum_days = date::get_matching_dates_around(today, plenum_spec); - // deltas has either 2 or 3 days, if 3 then the middle one is == 0 + // deltas has either 2 or 3 days, if 3 then the middle one is == 0 (i.e. today) let deltas: Vec = nearest_plenum_days.iter().map(|&d| (d - today).num_days()).collect(); // find the relevant one: let delta = *match last_state { @@ -215,20 +223,16 @@ fn main() -> Result<(), Box> { ProgramState::Logged // after that, we want to log it to the list & the wiki }; - println!("aktueller Zustand: {}", last_state); - println!("letzter Durchlauf: {}", last_run); - println!("relevante Tage: {:#?}", nearest_plenum_days); - println!("Abweichungen: {:#?}", deltas); - println!("Geschätzter Termin (Δ): {}", delta); - println!("Gewollter Zustand: {}", intended_state); + let action: TransitionFunction = TRANSITION_LUT[last_state.index()][intended_state.index()]; + action(delta, &config, &hedgedoc, &email, &wiki)?; - // TODO: state-matrix: was macht sinn? - // Normal Announced Reminded Waiting Logged - // Normal nop announce reminder(!) nop log - // Announced log nop reminder nop log - // Reminded log log;announce nop nop log - // Waiting log log;announce log;reminder(!) nop log - // Logged nop announce reminder(!) nop nop + // TODO: cleanup / write new state + + if config.has_errors() { + return Err("There were errors while writing config values.".into()); + } else { + return Ok(()); + } // Dienstage diesen Monat let all_tuesdays: Vec = get_all_weekdays(0, Weekday::Tue); @@ -299,8 +303,6 @@ fn main() -> Result<(), Box> { println!("Pad-content geladen!"); let current_pad_link = hedgedoc.format_url(¤t_pad_id); let future_pad_link = hedgedoc.format_url(&future_pad_id); - let email_greeting = &config["text-email-greeting"]; - let email_signature = &config["text-email-signature"]; 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); @@ -313,37 +315,29 @@ fn main() -> Result<(), Box> { // Mail an alle senden, findet statt let betreff = "Morgen ist Plenum!".to_string(); // ADJ_TIMEYWIMEY let message: String = format!( - r#"{email_greeting} - -Es gibt Themen, deshalb wird das morgige Plenum statt finden. Anbei das Plenumspad: + r#"Es gibt Themen, deshalb wird das morgige Plenum statt finden. Anbei das Plenumspad: {current_pad_link} Und hier ein TL;DR von den aktuellen Themen: {tldr} -Bis morgen, 20 Uhr! - -{email_signature}"# +Bis morgen, 20 Uhr!"# ); // ADJ_TIMEYWIMEY - message_id = email.send_email(betreff, message).ok() + message_id = send_email(&betreff, &message, &email, &config); // .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} - -Es gibt keine Themen, deshalb wird das morgige Plenum leider nicht statt finden. + r#"Es gibt keine Themen, deshalb wird das morgige Plenum leider nicht statt finden. Hier ist der Link zum Pad vom nächsten Plenum, das am {} statt finden wird: {future_pad_link} -Bis zum nächsten Plenum. - -{email_signature}"#, +Bis zum nächsten Plenum."#, nächster_plenumtermin ); - message_id = email.send_email(betreff, message).ok() + message_id = send_email(&betreff, &message, &email, &config); // .expect("Plenum wird abgesagt. Mail wurde versucht zu senden, konnte aber nicht gesendet werden!")) } } else if in_3_days_is_plenum { @@ -352,16 +346,12 @@ Bis zum nächsten Plenum. // 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} - -Es sind bisher leider keine Themen zusammengekommen. Wenn es bis Sonntag Abend keine Themen gibt, wird das Plenum voraussichtlich nicht statt finden. + r#"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}"# + {current_pad_link}"# ); - message_id = email.send_email(betreff, message).ok() + message_id = send_email(&betreff, &message, &email, &config); // .expect("Noch nicht genug Themen. Mail wurde versucht zu senden, konnte aber nicht gesendet werden!")) } } else if yesterday_was_plenum { @@ -381,9 +371,7 @@ Hier ist der Link zum Pad, wo ihr noch Themen eintragen könnt: let tldr = tldr_vec.join("\n"); // XXX nächstes/übernächstes? let message: String = format!( - r#"{email_greeting} - -Anbei das gestrige Plenumspad. Hier sind die Links zum nächsten: + r#"Anbei das gestrige Plenumspad. Hier sind die Links zum nächsten: {current_pad_link} und zum übernächsten Plenum: {future_pad_link} @@ -393,26 +381,25 @@ TL;DR: Und hier ist das Protokoll des letzten Plenums: -{old_pad_content_without_top_instructions} - -{email_signature}"# +{old_pad_content_without_top_instructions}"# ); let betreff: String = format!("Plenumsprotokoll vom {}: Es gab {} TOPs", nächster_plenumtermin, top_anzahl); // XXX option x expect - message_id = email.send_email(betreff, message).ok(); + message_id = send_email(&betreff, &message, &email, &config); // .expect("Mail mit Plenumsprotokoll wurde versucht zu senden, konnte aber nicht gesendet werden!")); mediawiki::pad_ins_wiki(old_pad_content_without_top_instructions); } println!("message id: {:?}", message_id); println!("[ENDE]\n{}", "Aktueller Zustand der DB:".bold()); - config.dump_redacting(&["email-password", "wiki-http-password", "wiki-api-secret", "matrix-password"]).ok(); - - if config.has_errors() { - Err("There were errors.".into()) - } else { - Ok(()) - } + config + .dump_redacting(&[ + "email-password", + "wiki-http-password", + "wiki-api-secret", + "matrix-password", + ]) + .ok(); } fn get_all_weekdays(month_offset: i32, week_day: Weekday) -> Vec { @@ -525,12 +512,82 @@ fn try_to_remove_top_instructions(pad_content: String) -> String { result.to_string() // Wenn es nicht geklappt hat, wird einfach das Pad mit dem Kommentar zurückgegeben } -/// State machine of the announcement logic, ensuring we can deal with inconsistent runs -/// (e.g. multiple runs in a day, or skipping days). The states are: +/* ***** transition actions ***** */ + +fn do_announcement( + ttp: i64, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, wiki: &Mediawiki, +) -> Result<(), Box> { + // use TTP to adjust text if needed (in {ttp} days) + todo!() +} + +fn do_reminder( + ttp: i64, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, wiki: &Mediawiki, +) -> Result<(), Box> { + // use TTP to adjust text if needed (tomorrow or today / in {ttp} days) + todo!() +} + +fn do_protocol( + ttp: i64, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, wiki: &Mediawiki, +) -> Result<(), Box> { + // use TTP to adjust text if needed ({-ttp} days ago) + todo!() +} + +fn do_cleanup( + ttp: i64, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, wiki: &Mediawiki, +) -> Result<(), Box> { + todo!() +} + +/* ***** state machine ***** */ + +type TransitionFunction = fn( + ttp: i64, + config: &KV, + hedgedoc: &HedgeDoc, + email: &SimpleEmail, + wiki: &Mediawiki, +) -> Result<(), Box>; + +#[rustfmt::skip] +const TRANSITION_LUT: [[TransitionFunction; 5]; 5] = [ + /* NORMAL ANNOUNCED REMINDED WAITING LOGGED */ + /* NORMAL */ [nop, do_announcement, do_reminder, nop, nop], + /* ANNOUNCED */ [do_cleanup, nop, do_reminder, nop, do_protocol], + /* REMINDED */ [do_cleanup, do_clean_announcement, nop, nop, do_protocol], + /* WAITING */ [do_cleanup, do_clean_announcement, do_clean_reminder, nop, do_protocol], + /* LOGGED */ [do_cleanup, do_clean_announcement, do_clean_reminder, nop, do_cleanup], +]; + +fn nop(_: i64, _: &KV, _: &HedgeDoc, _: &SimpleEmail, _: &Mediawiki) -> Result<(), Box> { + Ok(()) +} + +fn do_clean_announcement( + ttp: i64, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, wiki: &Mediawiki, +) -> Result<(), Box> { + do_cleanup(ttp, config, hedgedoc, email, wiki)?; + do_announcement(ttp, config, hedgedoc, email, wiki) +} + +fn do_clean_reminder( + ttp: i64, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, wiki: &Mediawiki, +) -> Result<(), Box> { + do_cleanup(ttp, config, hedgedoc, email, wiki)?; + do_reminder(ttp, config, hedgedoc, email, wiki) +} + +/// State machine type for the announcement logic, ensuring we can deal with +/// inconsistent runs (e.g. multiple runs in a day, or skipping days). The +/// states are: /// /// - Normal (default) – in between dates, with nothing to do /// - Announced – the first announcement was sent /// - Reminded – the reminder was sent +/// - Waiting – just a day of delay (after it makes sense to send a reminder) +/// - Logged – protocol was written to wiki & mailing list /// /// The bot knows in which state it is at all times. It knows this because it /// knows in which state it isn't. By comparing where it is with where it @@ -574,10 +631,20 @@ impl ProgramState { "announced" => ProgramState::Announced, "reminded" => ProgramState::Reminded, "waiting" => ProgramState::Waiting, - // "logged" also becomes Normal + "logged" => ProgramState::Logged, _ => ProgramState::Normal, } } + + fn index(&self) -> usize { + match self { + ProgramState::Normal => 0, + ProgramState::Announced => 1, + ProgramState::Reminded => 2, + ProgramState::Waiting => 3, + ProgramState::Logged => 4, + } + } } impl std::fmt::Display for ProgramState { @@ -592,3 +659,14 @@ impl std::fmt::Display for ProgramState { write!(f, "{}", str) } } + +/* ***** wrappers ***** */ + +fn send_email(subject: &str, body: &str, email: &SimpleEmail, config: &KV) -> Option { + let full_subject = format!("[PleB] {}", subject); + let full_body = format!( + "{}\n\n{}\n\n{}", + &config["text-email-greeting"], body, &config["text-email-signature"] + ); + email.send_email(full_subject, full_body).ok() +}