plenum-bot/src/main.rs
2024-12-12 14:07:20 +01:00

742 lines
30 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Dies ist der Plenumsbot vom Chaos Computer Club Berlin. Mehr Infos zum aktuellen Stand des Projektss in der ReadMe.
use std::env;
use std::error::Error;
use std::fmt::Display;
use std::io::IsTerminal;
use std::os::linux::raw::stat;
use std::time::Instant;
use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec};
use cccron_lib::date;
use cccron_lib::email::{self, Email, SimpleEmail};
use cccron_lib::hedgedoc::{self, HedgeDoc};
use cccron_lib::is_dry_run;
use cccron_lib::key_value::{KeyValueStore as KV, KeyValueStore};
use cccron_lib::matrix::{self, MatrixClient};
use cccron_lib::mediawiki::{self, Mediawiki};
use cccron_lib::NYI;
use cccron_lib::{trace_var, trace_var_, verboseln};
use chrono::{DateTime, Local, NaiveDate, Utc};
use clap::{Arg, Command};
use colored::Colorize;
use regex::Regex;
/* ***** Config Spec ***** */
const CONFIG_SPEC: CfgSpec<'static> = CfgSpec {
groups: &[
CfgGroup { name: "config",
description: "(internal values)",
fields: &[
CfgField::Silent { key: "version", default: "1" },
],
},
CfgGroup { name: "state",
description: "varying state info that needs to be kept across invocations",
fields: &[
CfgField::Default { key: "name",
default: "NORMAL",
description: "Named state of the program logic. (NORMAL, ANNOUNCED, REMINDED, LOGGED)",
},
CfgField::Optional { key: "toc", description: "Table of contents / pad summary." },
CfgField::Optional { key: "last-run",
description: "Last run of the program (used to figure out state of previous plenum mails.)",
},
]
},
CfgGroup { name: "date",
description: "specification of relevant dates",
fields: &[
CfgField::Default { key: "spec",
default: "(2,Tue),(4,Tue)",
description: "Days on which the plenum happens.",
},
],
},
hedgedoc::CONFIG,
mediawiki::CONFIG,
email::CONFIG,
matrix::CONFIG,
CfgGroup { name: "text",
description: "Various strings used.",
fields: &[
CfgField::Default { key: "email-greeting",
default: "Hallo liebe Mitreisende,",
description: "\"Hello\"-greeting added at the start of every email.",
},
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.",
},
CfgField::Default { key: "fallback-template",
default: "---\ndate: \"{{datum-iso}}\"\n...\n<!-- bitte oberhalb nichts verändern -->\n\n\
# Plenum vom {{datum}}\n**Beginn 20:XX**\n\n## Themen\n\n[toc]\n\n## Anwesend\nX\n\n\
<!--\nDamit Themen richtig erkannt und verarbeitet werden, bitte folgendes Format \
einhalten: 2+ Rauten, \"TOP \", Nummer, also z.B:\n\n\
## TOP 1: Der Vorstand berichtet\n\n### TOP 1.1: Auf der west.berlin nichts neues\n\n\
-->\n\n\
\n\n**Ende 2X:XX**\n\nNächstes Plenum: Am {{naechstes-plenum}} um 20 Uhr",
description: "Pad template used in case hedgedoc isn't reachable, so that we can write a new pad to the hedgedoc instance that we couldn't reach."
},
],
},
],
};
/* ***** Runtime Configuration (Arguments & Environment Variables) ***** */
/// Gets either today or the date from the environment variable `TODAY` (for
/// testing purposes.)
fn today() -> NaiveDate {
env::var("TODAY")
.map(|v| NaiveDate::parse_from_str(&v, "%F").expect("'TODAY' hat nicht format YYYY-MM-DD"))
.unwrap_or(Local::now().date_naive())
}
/// Gets either the state from the config or overrides it with the state from
/// the environment variable `STATE_OVERRIDE` (for testing purposes.)
///
/// For example, `STATE_OVERRIDE=Waiting` can be used to debug mediawiki.
fn state(config: &KeyValueStore) -> ProgramState {
match env::var("STATE_OVERRIDE") {
Ok(val) => ProgramState::parse(&val),
Err(_e) => ProgramState::parse(&config["state-name"]),
}
}
#[derive(Debug)]
struct Args {
check_mode: bool,
config_file: String,
}
fn parse_args() -> Args {
let matches = Command::new("Plenum-Bot")
.about("CCCB Plenumsbot")
.arg(
Arg::new("check")
.short('c')
.long("check")
.action(clap::ArgAction::SetTrue)
.help("interactively check the config for missing values"),
)
.arg(
Arg::new("config_file")
.short('f')
.long("config-file")
.value_name("FILE")
.action(clap::ArgAction::Set)
.default_value("config.sqlite")
.help("specifies an alternate config file"),
)
.get_matches();
Args {
check_mode: *matches.get_one::<bool>("check").unwrap(),
config_file: matches.get_one::<String>("config_file").unwrap().clone(),
}
}
/* ***** Main ***** */
fn main() -> Result<(), Box<dyn Error>> {
let start = Instant::now();
if std::io::stdout().is_terminal() {
println!(include_str!("chaosknoten.txt"), VERSION = env!("CARGO_PKG_VERSION"));
}
// set up config file access
let args = parse_args();
trace_var!(args);
let config_file = args.config_file.as_str();
verboseln!("Using config file {}.", config_file.cyan());
let config = KV::new(config_file).unwrap();
config_spec::populate_defaults(&CONFIG_SPEC, &config);
// select mode
if args.check_mode {
return config_spec::interactive_check(&CONFIG_SPEC, config);
}
// ensure existence of this is checked early
let _current_pad_id = config
.get("hedgedoc-last-id")
.expect("ID des aktuellen Pads undefiniert. Bitte in der DB eintragen oder generieren.");
trace_var!(_current_pad_id);
// get config
let hedgedoc = HedgeDoc::new(&config["hedgedoc-server-url"], is_dry_run());
trace_var!(hedgedoc);
let email_ = Email::new(
&config["email-server"],
&config["email-user"],
&config["email-password"],
is_dry_run(),
);
let mut email = SimpleEmail::new(
email_,
&config["email-from"],
&config["email-to"],
config.get("email-in-reply-to").ok(),
);
trace_var_!(email);
let wiki = Mediawiki::new(
&config["wiki-server-url"],
&config["wiki-http-user"],
&config["wiki-http-password"],
&config["wiki-api-user"],
&config["wiki-api-secret"],
false, // is_dry_run(), // TODO: Remove this false in order for actually letting dry_run affecting if mediawiki is run
&config["wiki-plenum-page"],
);
trace_var_!(wiki);
// get next plenum days
let today = today();
verboseln!("Heute ist {}", today.to_string().cyan());
let plenum_spec = date::parse_spec(&config["date-spec"])?;
trace_var!(plenum_spec);
let nearest_plenum_days = date::get_matching_dates_around(today, plenum_spec);
trace_var!(nearest_plenum_days);
// figure out where we are
let mut last_state = state(&config);
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();
trace_var!(last_run);
// reset state if this hasn't been run for too long
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, &today, &config, &hedgedoc, &email, &wiki)?;
// reset will have cleared in-reply-to if it existed
let (base, from, to, _) = email.into_parts();
email = SimpleEmail::from_parts(base, from, to, config.get("email-in-reply-to").ok());
}
}
let email = email;
let last_state = last_state;
// figure out where we should be
// deltas has either 2 or 3 days, if 3 then the middle one is == 0 (i.e. today)
let deltas: Vec<i64> = nearest_plenum_days.iter().map(|&d| (d - today).num_days()).collect();
trace_var!(deltas);
// find the relevant one:
let delta = *match last_state {
// In normal state, the past doesn't matter just pick the next occurrence.
ProgramState::Normal | ProgramState::Logged => deltas.last(),
// If announced or reminded, a *recent* past event may be relevant.
ProgramState::Announced | ProgramState::Reminded | ProgramState::Waiting => {
let candidate = deltas.get(deltas.len() - 2);
if candidate.is_some_and(|&i| i >= -7) {
candidate
} else {
deltas.last()
}
},
}
.unwrap(); // always has at least 2 elems
let plenum_day = today.checked_add_signed(chrono::TimeDelta::days(delta)).unwrap();
verboseln!(
"Relevantes Plenum ist am {} ({})",
plenum_day.to_string().cyan(),
relative_date(delta).cyan()
);
let intended_state = if delta > 3 {
ProgramState::Normal // nothing to do 3+ days in advance
} else if delta > 1 {
ProgramState::Announced // 2+ days in advance we want to have it announced
} else if delta >= 0 {
ProgramState::Reminded // up to the day of, we want to send a reminder (or cancel)
} else if delta >= -1 {
ProgramState::Waiting // we will wait a day for the protocol to be cleaned up
} else {
ProgramState::Logged // after that, we want to log it to the list & the wiki
};
verboseln!("Aktueller Zustand: {}", last_state.to_string().cyan());
verboseln!("Soll-Zustand: {}", intended_state.to_string().cyan());
let action: &ST = &TRANSITION_LUT[last_state as usize][intended_state as usize];
verboseln!("Notewendige Aktionen: {}", action.to_string().cyan());
// run action, which is responsible for updating the state as needed
action.get()(delta, &plenum_day, &config, &hedgedoc, &email, &wiki)?;
// shutdown
config.set("state-last-run", &today.to_string())?;
let duration = format!("{:?}", start.elapsed());
config.set("state-eta", &duration).ok();
verboseln!("Der Bot ist fertig und hat für die Ausführung {} gebraucht.", duration);
if config.has_errors() {
Err("There were errors while writing config values.".into())
} else {
Ok(())
}
}
fn generate_new_pad_for_following_date(
config: KV, hedgedoc: HedgeDoc, übernächster_plenumtermin: &str,
überübernächster_plenumtermin: &str, 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(|_| config.get("text-fallback-template").unwrap_or_default()),
Err(_) => config.get("text-fallback-template").unwrap_or_default(),
};
// 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(template_content); // Try regex, if not successful use without regex
match hedgedoc.import_note(Some(&pad_id), template_modified) {
Ok(_) => {
println!("Pad updated successfully with template content.");
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>> {
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\}\}")?;
let result = re_naechstes_plenum.replace_all(&result, überübernächster_plenumtermin);
Ok(result.to_string())
}
fn rotate(future_pad_id: &str, kv: &KV) {
let next_plenum_pad = kv.get("zukünftiges-plenumspad").ok();
if let Some(next_plenum_pad) = next_plenum_pad {
kv.set("aktuelles-plenumspad", &next_plenum_pad)
.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("zukünftiges-plenumspad", future_pad_id)
.expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!");
// Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
}
}
/* ***** formatting helpers ***** */
fn relative_date(ttp: i64) -> String {
if ttp.abs() > 2 {
if ttp.is_negative() {
format!("vor {} Tagen", -ttp)
} else {
format!("in {} Tagen", ttp)
}
} else {
match ttp {
2 => "übermorgen",
1 => "morgen",
0 => "heute",
-1 => "gestern",
-2 => "vorgestern",
_ => unreachable!(),
}
.to_string()
}
}
fn upper_first(s: &str) -> String {
let mut c = s.chars();
match c.next() {
Some(fst) => fst.to_uppercase().collect::<String>() + c.as_str(),
None => String::new(),
}
}
fn topic_count(n: usize, dative: bool) -> String {
match n {
0 => format!("noch keine{} Themen", if dative { "n" } else { "" }),
1 => format!("ein{} Thema", if dative { "em" } else { "" }),
_ => format!("{} Themen", n),
}
}
/* ***** repeating action parts ***** */
fn get_pad_info(config: &KV, hedgedoc: &HedgeDoc) -> (String, String, String, usize) {
let current_pad_id = &config["hedgedoc-last-id"];
let pad_content = hedgedoc.download(current_pad_id).expect("Hedgedoc: Download-Fehler");
let toc = hedgedoc::summarize(pad_content.clone());
verboseln!("Zusammenfassung des aktuellen Plenum-Pads:\n{}", toc.cyan());
let n_topics = toc.lines().count();
verboseln!("(Also {}.)", topic_count(n_topics, false).cyan());
(current_pad_id.to_string(), pad_content, toc, n_topics)
}
/* ***** transition actions ***** */
fn do_announcement(
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
_wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> {
NYI!("trace/verbose annotations");
// fetch current pad contents & summarize
let (current_pad_id, _pad_content, toc, n_topics) = get_pad_info(config, hedgedoc);
// construct email
let subject = format!(
"Plenum {} (am {}): bisher {}",
relative_date(ttp),
plenum_day.format("%d.%m.%Y"),
topic_count(n_topics, false)
);
let line1 = if n_topics == 0 {
"Es sind bisher leider noch keine Themen zusammengekommen. Wenn am Montag immer noch nix ist, dann fällt das Plenum aus.".to_string()
} else {
format!("Die bisherigen Themen für das Plenum sind:\n\n{toc}")
};
let line2 = format!(
"Falls ihr noch Themen ergänzen wollt ist hier der Link zum Pad:\n {}",
hedgedoc.format_url(&current_pad_id)
);
let body = format!("{line1}\n\n{line2}");
// send it
let message_id = send_email(&subject, &body, email, config)?;
// on success, update state (ignore write errors, they'll be checked later)
config.set("email-message-id", &message_id).ok();
config.set("state-name", &ProgramState::Announced.to_string()).ok();
config.set("state-toc", &toc).ok();
Ok(())
}
fn do_reminder(
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
_wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> {
NYI!("trace/verbose annotations");
// fetch current pad contents & summarize
let (current_pad_id, _pad_content, toc, n_topics) = get_pad_info(config, hedgedoc);
let old_toc = config.get("state-toc").unwrap_or_default();
// construct email
let human_date = plenum_day.format("%d.%m.%Y");
let subject = if n_topics == 0 {
format!("Plenum {} (am {}) fällt aus (keine Themen)", relative_date(ttp), human_date)
} else {
format!(
"{} ist Plenum mit {}!",
upper_first(&relative_date(ttp)),
topic_count(n_topics, true)
)
};
let body_prefix = if old_toc == toc {
format!("Die Themen sind gleich geblieben. ")
} else if old_toc.is_empty() {
format!("Es gibt Themen, die aktuelle Liste ist:\n\n{toc}\n\n")
} else {
format!("Es gab nochmal Änderungen, die aktualisierten Themen für das Plenum sind:\n\n{toc}\n\n")
};
let body = if n_topics > 0 {
format!(
"{body_prefix}Die vollen Details findet ihr im Pad:\n {}\n\nBis {} um 20:00!",
hedgedoc.format_url(&current_pad_id),
relative_date(ttp)
)
} else {
NYI!("generate link / pad for next plenum & include in this email");
"Da es immer noch keine Themen gibt fällt das Plenum aus.\n\n\
(Natürlich könnt ihr im Bedarfsfall immer noch kurzfristig ein Treffen einberufen, aber \
bitte kündigt das so früh wie möglich an, damit insbesondere auch Leute mit längeren \
Wegen sich noch darauf einstellen können.)"
.to_string()
};
// send it
let _message_id = send_email(&subject, &body, email, config)?;
// on success, update state (ignore write errors, they'll be checked later)
if n_topics == 0 {
NYI!(
"do we skip ahead to ProgramState::Logged here or do we later add a note to the wiki?"
);
}
config.set("state-name", &ProgramState::Reminded.to_string()).ok();
config.set("state-toc", &toc).ok();
Ok(())
}
#[allow(unused_variables)]
fn do_protocol(
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> {
NYI!("trace/verbose annotations");
let (current_pad_id, pad_content_without_cleanup, toc, n_topics) = get_pad_info(config, hedgedoc);
if !toc.is_empty() {
let human_date = plenum_day.format("%d.%m.%Y");
let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup.clone());
let subject = format!("Protokoll vom Plenum am {human_date}");
let pad_content = pad_content.replace("[toc]", &toc);
let body = format!(
"Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
Das Pad für das nächste Plenum ist zu finden unter <{}/{}>.\nDie Protokolle der letzten Plena findet ihr im wiki unter <{}/index.php?title={}>.\n\n---Protokoll:---\n{}\n-----",
&config["hedgedoc-server-url"],
&config["hedgedoc-next-id"],
&config["wiki-server-url"],
&config["wiki-plenum-page"],
pad_content.clone(),
);
let _message_id = send_email(&subject, &body, email, config)?;
mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?;
config.set("state-name", &ProgramState::Logged.to_string()).ok();
} else {
let human_date = plenum_day.format("%d.%m.%Y");
let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup.clone());
let subject = format!("Protokoll vom ausgefallenem Plenum am {human_date}");
let pad_content = pad_content.replace("[toc]", &toc);
let body = format!(
"Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n---Protokoll:---{}",
&config["hedgedoc-server-url"],
&config["hedgedoc-next-id"],
&config["wiki-server-url"],
&config["wiki-plenum-page"],
pad_content.clone(),
);
let _message_id = send_email(&subject, &body, email, config)?;
mediawiki::pad_ins_wiki(pad_content, wiki, plenum_day)?;
config.set("state-name", &ProgramState::Logged.to_string()).ok();
}
let mut matrix = MatrixClient::new(
&config["matrix-homeserver-url"],
&config["matrix-user-id"],
&config["matrix-access-token"],
"!YduwXBXwKifXYApwKF:catgirl.cloud", //&config["room-id-for-short-messages"],
"!YduwXBXwKifXYApwKF:catgirl.cloud", //&config["room-id-for-long-messages"],
is_dry_run(),
);
// Send the matrix room message
let human_date = plenum_day.format("%d.%m.%Y");
let pad_content = hedgedoc::strip_metadata(pad_content_without_cleanup);
let pad_content = pad_content.replace("[toc]", &toc);
let long_message = format!(
"Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n",
&config["hedgedoc-server-url"],
&config["hedgedoc-next-id"],
&config["wiki-server-url"],
&config["wiki-plenum-page"],
);
let full_long_message = format!(
"{}\n\n{}\n\n{}",
&config["text-email-greeting"], long_message, &config["text-email-signature"]
);
let short_message = format!(
"Das letzte Plenum hatte Anbei das Protokoll vom {human_date}, ab sofort auch im Wiki zu finden.\n\n\
Das Pad für das nächste Plenum ist zu finden unter {}/{}.\nDie Protokolle der letzten Plena findet ihr im wiki unter {}/index.php?title={}.\n\n",
&config["hedgedoc-server-url"],
&config["hedgedoc-next-id"],
&config["wiki-server-url"],
&config["wiki-plenum-page"]
);
let full_short_message = format!(
"{}\n\n{}\n\n{}",
&config["text-email-greeting"], short_message, &config["text-email-signature"]
);
matrix.send_short_and_long_messages_to_two_rooms(&full_short_message, &full_long_message)?;
Ok(())
}
/// General cleanup function. Call as `(999, today, …)` for a complete reset
/// based on today as the base date.
#[allow(unused_must_use)]
fn do_cleanup(
_ttp: i64, _plenum_day: &NaiveDate, config: &KV, _hedgedoc: &HedgeDoc, _email: &SimpleEmail,
_wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> {
NYI!("trace/verbose annotations");
config.delete("state-toc");
config.delete("email-last-message-id");
NYI!("rotate pad links");
NYI!("double-check state for leftovers");
config.set("state-name", &ProgramState::Normal.to_string());
Ok(())
}
/* ***** state machine ***** */
type TransitionFunction = fn(
ttp: i64,
plenum_day: &NaiveDate,
config: &KV,
hedgedoc: &HedgeDoc,
email: &SimpleEmail,
wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>>;
#[rustfmt::skip]
const TRANSITION_LUT: [[ST; 5]; 5] = [
/* vvv IS vvv | >>> SHOULD >>> */
/* NORMAL ANNOUNCED REMINDED WAITING LOGGED */
/* NORMAL */ [ST::Nop, ST::DoAnnouncement, ST::DoReminder, ST::Nop, ST::Nop],
/* ANNOUNCED */ [ST::DoCleanup, ST::Nop, ST::DoReminder, ST::Nop, ST::DoProtocol],
/* REMINDED */ [ST::DoCleanup, ST::DoCleanupThenAnnouncement, ST::Nop, ST::Nop, ST::DoProtocol],
/* WAITING */ [ST::DoCleanup, ST::DoCleanupThenAnnouncement, ST::DoCleanupThenReminder, ST::Nop, ST::DoProtocol],
/* LOGGED */ [ST::DoCleanup, ST::DoCleanupThenAnnouncement, ST::DoCleanupThenReminder, ST::Nop, ST::DoCleanup],
];
#[derive(Debug, Default, Clone, Copy)]
enum ST {
#[default]
Nop,
DoAnnouncement,
DoReminder,
DoProtocol,
DoCleanup,
DoCleanupThenAnnouncement,
DoCleanupThenReminder,
}
impl ST {
fn get(&self) -> TransitionFunction {
match self {
ST::Nop => nop,
ST::DoAnnouncement => do_announcement,
ST::DoReminder => do_reminder,
ST::DoProtocol => do_protocol,
ST::DoCleanup => do_cleanup,
ST::DoCleanupThenAnnouncement => do_clean_announcement,
ST::DoCleanupThenReminder => do_clean_reminder,
}
}
}
impl Display for ST {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
fn nop(
_: i64, _: &NaiveDate, _: &KV, _: &HedgeDoc, _: &SimpleEmail, _: &Mediawiki,
) -> Result<(), Box<dyn Error>> {
Ok(())
}
fn do_clean_announcement(
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> {
do_cleanup(ttp, plenum_day, config, hedgedoc, email, wiki)?;
do_announcement(ttp, plenum_day, config, hedgedoc, email, wiki)
}
fn do_clean_reminder(
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> {
do_cleanup(ttp, plenum_day, config, hedgedoc, email, wiki)?;
do_reminder(ttp, plenum_day, 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 isn't, it obtains a difference, or deviation. The program logic uses
/// > deviations to generate corrective commands to drive the bot from a state
/// > where it is to a state where it isn't, and arriving in a state where it
/// > wasn't, it now is. Consequently, the state where it is, is now the state
/// > that it wasn't, and it follows that the state that it was in, is now the
/// > state that it isn't in.
/// >
/// > In the event that the state that it is in is not the state that it wasn't,
/// > the system has acquired a variation, the variation being the difference
/// > between where the bot is, and where it wasn't. If variation is considered
/// > to be a significant factor, it too may be corrected by the program logic.
/// > However, the bot must also know where it was.
/// >
/// > The program logic works as follows. Because a delay has modified some
/// > of the information the bot has obtained, it is not sure just when it is.
/// > However, it is sure when it isn't, within reason, and it knows when it was.
/// > It now subtracts when it should be from when it wasn't, or vice-versa, and
/// > by differentiating this from the algebraic sum of when it shouldn't be,
/// > and when it was, it is able to obtain the deviation and its variation,
/// > which is called error.
#[derive(Default, Debug)]
enum ProgramState {
/// Normal is the default state, with no actions currently outstanding.
#[default]
Normal,
/// There is an upcoming event, and the first announcement has been sent.
Announced,
/// The reminder / second announcement was sent.
Reminded,
/// We're waiting for the protocol to surely be there & cleaned up.
Waiting,
/// (transient) The protocol was submitted.
Logged,
}
impl ProgramState {
/// Read state from a string. Always succeeds by just assuming the default state.
fn parse(name: &str) -> ProgramState {
match name.to_lowercase().as_str() {
"announced" => ProgramState::Announced,
"reminded" => ProgramState::Reminded,
"waiting" => ProgramState::Waiting,
"logged" => ProgramState::Logged,
_ => ProgramState::Normal,
}
}
}
impl std::fmt::Display for ProgramState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
/* ***** wrappers ***** */
fn send_email(
subject: &str, body: &str, email: &SimpleEmail, config: &KV,
) -> Result<String, Box<dyn Error>> {
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)
}
fn display_logo(eta: &str) {
let ansi_art_pt1 = r#"

 
 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄  Plenum! Ple-e-e-num!
 █ ▂▂▂▂▂▂▂▂▂ █  Plenum ist wichtig für die Revolution!
 █ ▞ ▚ ▌ ▚ █ 
 █ ▌ ▚🬯🮗🮗🮗🮗🮗🮗▚▚▚ █  Dies ist der CCCB Plenumsbot
 █ ▌ ▞🬥🮗🮗🮗🮗🮗🮗▜▞▞▖ █ 
 █ ▚ ▞ ▌ ▞ 🬂🬤▞▚🬭 █  Version {VERSION}
 █ 🬂🬂🬂🬂🬂🬂🬂🬂🬂 ▞▐▐ █ 
 █ 🬔🬈 🬔🬈 🬔🬈 🬴🬗 🬭🬫🬀 █  ETA.:"#;
let ansi_art_pt2 = r#"
 █ 🬣🬖 🬣🬖 🬣🬖 🬲🬘 ▚ █ 
 █▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄█ 
 
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
"#;
let ansi_art = format!("{ansi_art_pt1}{eta}{ansi_art_pt2}");
println!("{}", ansi_art);
}