actions mostly completed (with NYIs)

This commit is contained in:
nobody 2024-08-24 01:24:41 +02:00 committed by murmeldin
parent 92d87193a8
commit 06457bd1c5
3 changed files with 70 additions and 255 deletions

View file

@ -57,7 +57,6 @@
use crate::is_dry_run; use crate::is_dry_run;
use crate::key_value::KeyValueStore as KV; use crate::key_value::KeyValueStore as KV;
use crate::NYI;
use std::error::Error; use std::error::Error;
use std::io::{self, Write}; use std::io::{self, Write};
@ -218,7 +217,6 @@ impl<'a> CfgField<'a> {
/// Ensure all fields with known default values exist. /// Ensure all fields with known default values exist.
pub fn populate_defaults(spec: &CfgSpec, config: &KV) { pub fn populate_defaults(spec: &CfgSpec, config: &KV) {
for group in spec.groups { for group in spec.groups {
NYI!("asdf");
for field in group.fields { for field in group.fields {
match field { match field {
CfgField::Silent { default, .. } => { CfgField::Silent { default, .. } => {

View file

@ -97,10 +97,21 @@ impl HedgeDoc {
} }
} }
pub fn extract_metadata(pad_content: String) -> String {
let re_yaml = Regex::new(r"(?s)---\s*(.*?)\s*(?:\.\.\.|---)").unwrap();
re_yaml.captures_iter(&pad_content).map(|c| c[1].to_string()).collect::<Vec<_>>().join("\n")
}
pub fn strip_metadata(pad_content: String) -> String {
let re_yaml = Regex::new(r"(?s)---\s*.*?\s*(?:\.\.\.|---)").unwrap();
let pad_content = re_yaml.replace_all(&pad_content, "").to_string();
let re_comment = Regex::new(r"(?s)<!--.*?-->").unwrap();
re_comment.replace_all(&pad_content, "").to_string()
}
pub fn summarize(pad_content: String) -> String { pub fn summarize(pad_content: String) -> String {
// 1. remove HTML comments // 1. remove HTML comments
let re_comment = Regex::new(r"(?s)<!--.*?-->").unwrap(); let pad_content = strip_metadata(pad_content);
let pad_content = re_comment.replace_all(&pad_content, "").to_string();
// 2. accumulate topic lines // 2. accumulate topic lines
let re_header = Regex::new(r"^\s*##(#*) TOP ([\d.]+\s*.*?)\s*#*$").unwrap(); let re_header = Regex::new(r"^\s*##(#*) TOP ([\d.]+\s*.*?)\s*#*$").unwrap();
let mut result: Vec<String> = Vec::new(); let mut result: Vec<String> = Vec::new();

View file

@ -30,14 +30,11 @@ future improvements:
- search ADJ_TIMEYWIMEY to find places that need adjusting if the bot might run late - 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) (that's an incomplete list, but tag things as you notice them)
*/ */
#![allow(dead_code)] // ≈100 warnings for yet-to-be-used stuff…
#![allow(unused_macros)]
use chrono::{Datelike, Local, NaiveDate, Weekday}; use chrono::{Local, NaiveDate};
use clap::{Arg, Command}; use clap::{Arg, Command};
use colored::Colorize; use colored::Colorize;
use regex::Regex; use regex::Regex;
use std::borrow::Cow;
use std::env; use std::env;
use std::error::Error; use std::error::Error;
use std::fmt::Display; use std::fmt::Display;
@ -45,7 +42,6 @@ use std::io::IsTerminal;
use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec}; use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec};
use cccron_lib::key_value::KeyValueStore as KV; use cccron_lib::key_value::KeyValueStore as KV;
mod variables_and_settings;
use cccron_lib::date; use cccron_lib::date;
use cccron_lib::email::{self, Email, SimpleEmail}; use cccron_lib::email::{self, Email, SimpleEmail};
@ -54,8 +50,6 @@ use cccron_lib::is_dry_run;
use cccron_lib::mediawiki::{self, Mediawiki}; use cccron_lib::mediawiki::{self, Mediawiki};
use cccron_lib::NYI; use cccron_lib::NYI;
const FALLBACK_TEMPLATE: &str = variables_and_settings::FALLBACK_TEMPLATE;
/* ***** Config Spec ***** */ /* ***** Config Spec ***** */
const CONFIG_SPEC: CfgSpec<'static> = CfgSpec { const CONFIG_SPEC: CfgSpec<'static> = CfgSpec {
groups: &[ groups: &[
@ -102,6 +96,16 @@ const CONFIG_SPEC: CfgSpec<'static> = CfgSpec {
default: "\n\n[Diese Nachricht wurde automatisiert vom Plenumsbot erstellt und ist daher ohne Unterschrift gültig.]", 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.", 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."
},
], ],
}, },
], ],
@ -345,214 +349,6 @@ fn main() -> Result<(), Box<dyn Error>> {
} else { } else {
return Ok(()); return Ok(());
} }
/* ** dead code beyond this point ** */
let _ = (); // dummy line so linter doesn't highlight the entire following block
#[allow(unused_variables)]
#[rustfmt::skip]
{
// 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: String;
// 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(&current_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(&current_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 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!");
// 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();
};
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( fn generate_new_pad_for_following_date(
@ -568,8 +364,8 @@ fn generate_new_pad_for_following_date(
let template_content: String = match config.get("hedgedoc-template-name") { let template_content: String = match config.get("hedgedoc-template-name") {
Ok(content) => hedgedoc Ok(content) => hedgedoc
.download(&content.clone()) .download(&content.clone())
.unwrap_or_else(|_| FALLBACK_TEMPLATE.to_string()), .unwrap_or_else(|_| config.get("text-fallback-template").unwrap_or_default()),
Err(_) => FALLBACK_TEMPLATE.to_string(), Err(_) => config.get("text-fallback-template").unwrap_or_default(),
}; };
// XXX you don't just use the template as-is… // XXX you don't just use the template as-is…
let template_modified: String = replace_placeholders( let template_modified: String = replace_placeholders(
@ -616,12 +412,6 @@ fn rotate(future_pad_id: &str, kv: &KV) {
} }
} }
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 ***** */ /* ***** formatting helpers ***** */
fn relative_date(ttp: i64) -> String { fn relative_date(ttp: i64) -> String {
@ -662,26 +452,24 @@ fn topic_count(n: usize, dative: bool) -> String {
/* ***** repeating action parts ***** */ /* ***** repeating action parts ***** */
fn get_pad_info(config: &KV, hedgedoc: &HedgeDoc) -> (String, String, usize) { fn get_pad_info(config: &KV, hedgedoc: &HedgeDoc) -> (String, String, String, usize) {
let current_pad_id = &config["hedgedoc-last-id"]; let current_pad_id = &config["hedgedoc-last-id"];
let pad_content = hedgedoc.download(current_pad_id).expect("Hedgedoc: Download-Fehler"); let pad_content = hedgedoc.download(current_pad_id).expect("Hedgedoc: Download-Fehler");
let toc = hedgedoc::summarize(pad_content); let toc = hedgedoc::summarize(pad_content.clone());
verboseln!("Zusammenfassung des aktuellen Plenum-Pads:\n{}", toc.cyan()); verboseln!("Zusammenfassung des aktuellen Plenum-Pads:\n{}", toc.cyan());
let n_topics = toc.lines().count(); let n_topics = toc.lines().count();
verboseln!("(Also {}.)", topic_count(n_topics, false).cyan()); verboseln!("(Also {}.)", topic_count(n_topics, false).cyan());
(current_pad_id.to_string(), toc, n_topics) (current_pad_id.to_string(), pad_content, toc, n_topics)
} }
/* ***** transition actions ***** */ /* ***** transition actions ***** */
// BBBBBBBBBB
fn do_announcement( fn do_announcement(
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
_wiki: &Mediawiki, _wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// fetch current pad contents & summarize // fetch current pad contents & summarize
let (current_pad_id, toc, n_topics) = get_pad_info(config, hedgedoc); let (current_pad_id, _pad_content, toc, n_topics) = get_pad_info(config, hedgedoc);
// construct email // construct email
let subject = format!( let subject = format!(
"Plenum {} (am {}): bisher {}", "Plenum {} (am {}): bisher {}",
@ -700,7 +488,7 @@ fn do_announcement(
); );
let body = format!("{line1}\n\n{line2}"); let body = format!("{line1}\n\n{line2}");
// send it // send it
let message_id = send_email(&subject, &body, &email, &config)?; let message_id = send_email(&subject, &body, email, config)?;
// on success, update state (ignore write errors, they'll be checked later) // on success, update state (ignore write errors, they'll be checked later)
config.set("email-message-id", &message_id).ok(); config.set("email-message-id", &message_id).ok();
config.set("state-name", &ProgramState::Announced.to_string()).ok(); config.set("state-name", &ProgramState::Announced.to_string()).ok();
@ -713,39 +501,42 @@ fn do_reminder(
_wiki: &Mediawiki, _wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// fetch current pad contents & summarize // fetch current pad contents & summarize
let (current_pad_id, toc, n_topics) = get_pad_info(config, hedgedoc); let (current_pad_id, _pad_content, toc, n_topics) = get_pad_info(config, hedgedoc);
let old_toc = config.get("state-toc").unwrap_or_default(); let old_toc = config.get("state-toc").unwrap_or_default();
// construct email // construct email
let subject_suffix = if n_topics == 0 { let human_date = plenum_day.format("%d.%m.%Y");
" fällt aus (keine Themen)".to_string() let subject = if n_topics == 0 {
format!("Plenum {} (am {}) fällt aus (keine Themen)", relative_date(ttp), human_date)
} else { } else {
format!(" findet mit {} statt", topic_count(n_topics, true)) format!(
"{} ist Plenum mit {}!",
upper_first(&relative_date(ttp)),
topic_count(n_topics, true)
)
}; };
let subject = format!(
"Plenum {} (am {}) {}",
relative_date(ttp),
plenum_day.format("%d.%m.%Y"),
subject_suffix
);
let body_prefix = if old_toc == toc { let body_prefix = if old_toc == toc {
format!("Die Themen sind gleich geblieben. ") format!("Die Themen sind gleich geblieben. ")
} else if toc.is_empty() {
format!("Es gibt Themen, die aktuelle Liste ist:\n\n{toc}\n\n")
} else { } else {
format!("Es gab nochmal Änderungen, die aktualisierten Themen für das Plenum sind:\n\n{toc}\n\n") format!("Es gab nochmal Änderungen, die aktualisierten Themen für das Plenum sind:\n\n{toc}\n\n")
}; };
let body = if n_topics > 0 { let body = if n_topics > 0 {
format!( format!(
"{body_prefix}Die vollen Details findet ihr im Pad:\n {}", "{body_prefix}Die vollen Details findet ihr im Pad:\n {}\n\nBis {} um 20:00!",
hedgedoc.format_url(&current_pad_id) hedgedoc.format_url(&current_pad_id),
relative_date(ttp)
) )
} else { } else {
NYI!("generate link / pad for next plenum & include in this email"); 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\ "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 \ (Natürlich könnt ihr im Bedarfsfall immer noch kurzfristig ein Treffen einberufen, aber \
kündigt das so früh wie möglich an, damit Leute sich darauf einstellen können.)" bitte kündigt das so früh wie möglich an, damit insbesondere auch Leute mit längeren \
.to_string() Wegen sich noch darauf einstellen können.)"
.to_string()
}; };
// send it // send it
let _message_id = send_email(&subject, &body, &email, &config)?; let _message_id = send_email(&subject, &body, email, config)?;
// on success, update state (ignore write errors, they'll be checked later) // on success, update state (ignore write errors, they'll be checked later)
if n_topics == 0 { if n_topics == 0 {
NYI!( NYI!(
@ -762,11 +553,26 @@ fn do_protocol(
ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail,
wiki: &Mediawiki, wiki: &Mediawiki,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// TODO: get pad let (current_pad_id, pad_content, toc, n_topics) = get_pad_info(config, hedgedoc);
// TODO: write to wiki if toc.len() > 0 {
// TODO: write to email let human_date = plenum_day.format("%d.%m.%Y");
// TODO: set state as Logged let pad_content = hedgedoc::strip_metadata(pad_content);
todo!() let subject = format!("Protokoll vom Plenum am {human_date}");
NYI!("link for next plenum");
NYI!("replace [toc] with actual table of contents");
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 {}.\n\n-----\n\n{pad_content}",
"<FIXME>"
);
let _message_id = send_email(&subject, &body, email, config)?;
NYI!("convert to mediawiki");
NYI!("add to wiki");
config.set("state-name", &ProgramState::Logged.to_string()).ok();
} else {
NYI!("What do we do in the no topics / no plenum case?");
}
Ok(())
} }
/// General cleanup function. Call as `(999, today, …)` for a complete reset /// General cleanup function. Call as `(999, today, …)` for a complete reset