840 lines
32 KiB
Rust
840 lines
32 KiB
Rust
// Dies ist der Plenumsbot vom Chaos Computer Club Berlin.
|
||
/*
|
||
Plenumsbot
|
||
Ankündigungsskript
|
||
• Schauen, wann ein Plenum stattfindet
|
||
• Wenn eins in 3 Tagen stattfindet, nächstes Pad kopieren und per Mail schicken
|
||
→ Pad als .md herunterladen
|
||
→ Text dranhängen und per mail an intern@ verschicken
|
||
• Wenn 1 Tag vor dem Plenum immer noch kein TOP im Pad ist
|
||
→ Mail mit Absage verschicken
|
||
Neues-Pad-Erstellen-Skript
|
||
• Schauen, wann Plenum stattfindet
|
||
• Wenn eins im nächsten Monat stattfindet, von dem noch kein Pad erstellt wurde, dann eins hinzufügen
|
||
Pad-ins-Wiki-und-versenden-Skript
|
||
• Skript wird manuell nach dem Plenum ausgelöst
|
||
• Plenumspad wird als .md heruntergeladen
|
||
• Text dranhängen und per Mail an intern@ verschicken
|
||
• Pad in MediaWiki-Format umwandeln
|
||
• Neue Wiki-Seite erstellen und dort das umgewandelte Pad hochladen
|
||
*/
|
||
/* *TO-DO LIST*
|
||
main.rs
|
||
- [ ] Add logic for top_anzahl in main.rs:172
|
||
MediaWiki
|
||
- [ ] Add "get_csrf_token-function" for getting a write token to allow write operations in the wiki (murmeldin)
|
||
- [ ] Add "create_page" function for creating new pages in the wiki that is called on every day after plenum (murmeldin)
|
||
- [ ] Add "modify_plenum_main_page" function for creating new Links on the Plenum main page whenever the create_page function is being called (murmeldin)
|
||
|
||
future improvements:
|
||
- search ADJ_TIMEYWIMEY to find places that need adjusting if the bot might run late
|
||
(that's an incomplete list, but tag things as you notice them…)
|
||
*/
|
||
|
||
use chrono::{Datelike, Local, NaiveDate, Weekday};
|
||
use clap::{Arg, Command};
|
||
use colored::Colorize;
|
||
use regex::Regex;
|
||
use std::borrow::Cow;
|
||
use std::env;
|
||
use std::error::Error;
|
||
|
||
mod key_value;
|
||
use key_value::KeyValueStore as KV;
|
||
mod config_spec;
|
||
use config_spec::{CfgField, CfgGroup, CfgSpec};
|
||
mod template;
|
||
pub mod variables_and_settings;
|
||
|
||
mod email;
|
||
use email::{Email, SimpleEmail};
|
||
mod hedgedoc;
|
||
use hedgedoc::HedgeDoc;
|
||
mod mediawiki;
|
||
use mediawiki::Mediawiki;
|
||
mod date;
|
||
|
||
const FALLBACK_TEMPLATE: &str = variables_and_settings::FALLBACK_TEMPLATE;
|
||
|
||
/* ***** 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: "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,
|
||
// TODO: Matrix, …?
|
||
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.",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
/* ***** Runtime Configuration (Arguments & Environment Variables) ***** */
|
||
|
||
/// Checks environment variable `DRY_RUN` to see if any external operations
|
||
/// should *actually* be done.
|
||
///
|
||
/// If `is_dry_run` returns `true`, just report what you *would* do, and
|
||
/// don't actually e.g. send emails.
|
||
fn is_dry_run() -> bool {
|
||
env::var("DRY_RUN").map(|v| !v.is_empty()).unwrap_or(false)
|
||
}
|
||
|
||
/// Checks environment variable `VERBOSE` to see if status messages should be
|
||
/// printed.
|
||
///
|
||
/// Use `verboseln!` to print stuff only if `VERBOSE` (or `TRACE`) is set.
|
||
fn is_verbose() -> bool {
|
||
env::var("VERBOSE").map(|v| !v.is_empty()).unwrap_or(false)
|
||
}
|
||
/// Like `println!`, but only if `is_verbose` (or `is_trace`) is true (due to
|
||
/// the environment variable `VERBOSE` being set.)
|
||
macro_rules! verboseln {
|
||
($($arg:tt)*) => {
|
||
if is_verbose() || is_trace() {
|
||
println!($($arg)*);
|
||
}
|
||
};
|
||
}
|
||
|
||
/// (A. k. a. "*very* verbose".) Checks environment variable `TRACE` to see if
|
||
/// detailed small-step status messages should be printed.
|
||
///
|
||
/// Use `traceln!` to print stuff only if `TRACE` is set.
|
||
fn is_trace() -> bool {
|
||
env::var("TRACE").map(|v| !v.is_empty()).unwrap_or(false)
|
||
}
|
||
/// Like `println!`, but only if `is_trace` is true (due to the environment
|
||
/// variable `TRACE` being set.)
|
||
macro_rules! traceln {
|
||
($($arg:tt)*) => {
|
||
if is_trace() {
|
||
println!( "{}", format!($($arg)*).yellow() );
|
||
}
|
||
};
|
||
}
|
||
/// `trace_var!( [msg,] var )` prints either `varname = value` or `msg: value`
|
||
/// *if TRACE is set* (else is silent.)
|
||
///
|
||
/// There's an alternative form of `trace_var!( [msg,] var[, true] )` or the
|
||
/// preferred form of `trace_var_!( [msg,] var )` (i.e. just add an underscore
|
||
/// to the name), which will use the "pretty" form.
|
||
macro_rules! trace_var {
|
||
($var:expr, $pretty:expr) => {
|
||
if is_trace() {
|
||
if $pretty {
|
||
println!("{} = {}", stringify!($var).green(), format!("{:#?}", $var).cyan());
|
||
} else {
|
||
println!("{} = {}", stringify!($var).green(), format!("{:?}", $var).cyan());
|
||
}
|
||
}
|
||
};
|
||
($msg:expr, $var:expr, $pretty:expr) => {
|
||
if is_trace() {
|
||
if $pretty {
|
||
println!("{}: {}", $msg.green(), format!("{:#?}", $var).cyan());
|
||
} else {
|
||
println!("{}: {}", $msg.green(), format!("{:?}", $var).cyan());
|
||
}
|
||
}
|
||
};
|
||
($var:expr) => {
|
||
trace_var!($var, false);
|
||
};
|
||
($msg:expr, $var:expr) => {
|
||
trace_var!($msg, $var, false);
|
||
};
|
||
}
|
||
|
||
/// Pretty form of `trace_var!`
|
||
macro_rules! trace_var_ {
|
||
($var:expr) => {
|
||
trace_var!($var, true);
|
||
};
|
||
($msg:expr, $var:expr) => {
|
||
trace_var!($msg, $var, true);
|
||
};
|
||
}
|
||
|
||
/// 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())
|
||
}
|
||
|
||
#[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>> {
|
||
// 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);
|
||
if args.check_mode {
|
||
println!(include_str!("chaosknoten.txt"), VERSION = env!("CARGO_PKG_VERSION"));
|
||
return config_spec::interactive_check(&CONFIG_SPEC, config);
|
||
}
|
||
// 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 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"],
|
||
is_dry_run(),
|
||
);
|
||
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 = 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
|
||
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)?;
|
||
}
|
||
}
|
||
let last_state = last_state;
|
||
// 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();
|
||
// 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];
|
||
trace_var!(action);
|
||
action.get()(delta, &plenum_day, &config, &hedgedoc, &email, &wiki)?;
|
||
|
||
// 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<NaiveDate> = get_all_weekdays(0, Weekday::Tue);
|
||
let zweiter_dienstag: String = all_tuesdays[1].to_string(); // z.B. 2024-07-09
|
||
let vierter_dienstag: String = all_tuesdays[3].to_string(); // z.B. 2024-07-23
|
||
|
||
//Dienstage des nächsten Monats definieren
|
||
let all_tuesdays_next_month: Vec<NaiveDate> = get_all_weekdays(1, Weekday::Tue);
|
||
let zweiter_dienstag_nächster_monat: String = all_tuesdays_next_month[1].to_string();
|
||
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_simple = today.date_naive();
|
||
let yesterday = today_simple.pred_opt().unwrap();
|
||
let in_1_day = today_simple.succ_opt().unwrap();
|
||
let in_2_days = in_1_day.succ_opt().unwrap();
|
||
let in_3_days = in_2_days.succ_opt().unwrap();
|
||
|
||
// 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,
|
||
&zweiter_dienstag
|
||
} else {
|
||
&vierter_dienstag
|
||
};
|
||
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.
|
||
&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);
|
||
let in_3_days_is_plenum: bool = check_if_plenum(nächster_plenumtermin.clone(), in_3_days);
|
||
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 mut message_id: Option<String> = None;
|
||
|
||
// let in_3_days_is_plenum = true;
|
||
//TEMPORÄR ANFANG: BEI PRODUCTION MUSS DAS HIER RAUS
|
||
let top_anzahl: i32 = 0; // Muss noch gecodet werden
|
||
let yesterday_was_plenum = true; // Das ist nur zu Testzwecken, kommt noch weg
|
||
|
||
let auth_result = wiki.get_login_token()?;
|
||
println!("---AUTH RESULT:---\n{}\n-----------", auth_result);
|
||
// TEMPORÄR ENDE
|
||
|
||
// AAAAAAAAAA
|
||
|
||
let pad_content = hedgedoc.download(¤t_pad_id).expect("Fehler beim Download des Pads!");
|
||
let pad_content_without_top_instructions = try_to_remove_top_instructions(pad_content);
|
||
println!("Pad-content geladen!");
|
||
let current_pad_link = hedgedoc.format_url(¤t_pad_id);
|
||
let future_pad_link = hedgedoc.format_url(&future_pad_id);
|
||
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();
|
||
for element in tldr_vec {
|
||
tldr.push_str("\n");
|
||
tldr.push_str(&element)
|
||
}
|
||
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#"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!"#
|
||
); // ADJ_TIMEYWIMEY
|
||
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#"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."#,
|
||
nächster_plenumtermin
|
||
);
|
||
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 {
|
||
println!("In 3 Tagen ist Plenum, deshalb wird eine Erinnerung raus geschickt!");
|
||
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#"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}"#
|
||
);
|
||
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 {
|
||
// This logic breaks on 02/2034, but on every other month it works
|
||
let old_pad_content =
|
||
hedgedoc.download(¤t_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")
|
||
);
|
||
|
||
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");
|
||
// XXX nächstes/übernächstes?
|
||
let message: String = format!(
|
||
r#"Anbei das gestrige Plenumspad. Hier sind die Links zum nächsten:
|
||
{current_pad_link}
|
||
und zum übernächsten Plenum:
|
||
{future_pad_link}
|
||
|
||
TL;DR:
|
||
{tldr}
|
||
|
||
Und hier ist das Protokoll des letzten Plenums:
|
||
|
||
{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 = 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();
|
||
}
|
||
|
||
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)),
|
||
-1 => date.checked_sub_months(chrono::Months::new((-month_offset) as u32)),
|
||
_ => 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();
|
||
while let Some(date) = current_date {
|
||
if date.month() == month && date.weekday() == week_day {
|
||
dates.push(date);
|
||
}
|
||
current_date = date.succ_opt();
|
||
}
|
||
dates
|
||
}
|
||
|
||
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 };
|
||
}
|
||
|
||
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> {
|
||
// 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();
|
||
//println!("{:?}", m);
|
||
tldr
|
||
//tldr_vec.append() = m.iter()
|
||
}
|
||
|
||
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()),
|
||
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
|
||
|
||
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 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"))
|
||
} 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"))
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
/* ***** 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(),
|
||
}
|
||
}
|
||
|
||
/* ***** transition actions ***** */
|
||
|
||
// BBBBBBBBBB
|
||
|
||
fn do_announcement(
|
||
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
|
||
wiki: &Mediawiki,
|
||
) -> Result<(), Box<dyn Error>> {
|
||
// use TTP to adjust text if needed (in {ttp} days)
|
||
todo!()
|
||
}
|
||
|
||
fn do_reminder(
|
||
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
|
||
wiki: &Mediawiki,
|
||
) -> Result<(), Box<dyn Error>> {
|
||
// use TTP to adjust text if needed (tomorrow or today / in {ttp} days)
|
||
todo!()
|
||
}
|
||
|
||
fn do_protocol(
|
||
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
|
||
wiki: &Mediawiki,
|
||
) -> Result<(), Box<dyn Error>> {
|
||
// use TTP to adjust text if needed ({-ttp} days ago)
|
||
todo!()
|
||
}
|
||
|
||
/// General cleanup function. Call as `(999, today, …)` for a complete reset
|
||
/// based on today as the base date.
|
||
fn do_cleanup(
|
||
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
|
||
wiki: &Mediawiki,
|
||
) -> Result<(), Box<dyn Error>> {
|
||
todo!()
|
||
}
|
||
|
||
/* ***** 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] = [
|
||
/* 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,
|
||
}
|
||
}
|
||
}
|
||
|
||
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 {
|
||
let str = match self {
|
||
ProgramState::Normal => "Normal",
|
||
ProgramState::Announced => "Announced",
|
||
ProgramState::Reminded => "Reminded",
|
||
ProgramState::Waiting => "Waiting",
|
||
ProgramState::Logged => "Logged",
|
||
};
|
||
write!(f, "{}", str)
|
||
}
|
||
}
|
||
|
||
/* ***** wrappers ***** */
|
||
|
||
fn send_email(subject: &str, body: &str, email: &SimpleEmail, config: &KV) -> Option<String> {
|
||
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()
|
||
}
|