From 06457bd1c599e36cc24b0aa67a610338151dc4a5 Mon Sep 17 00:00:00 2001 From: nobody Date: Sat, 24 Aug 2024 01:24:41 +0200 Subject: [PATCH] actions mostly completed (with NYIs) --- src/config_spec.rs | 2 - src/hedgedoc.rs | 15 ++- src/main.rs | 308 +++++++++------------------------------------ 3 files changed, 70 insertions(+), 255 deletions(-) diff --git a/src/config_spec.rs b/src/config_spec.rs index 1cfc817..a4d838f 100644 --- a/src/config_spec.rs +++ b/src/config_spec.rs @@ -57,7 +57,6 @@ use crate::is_dry_run; use crate::key_value::KeyValueStore as KV; -use crate::NYI; use std::error::Error; use std::io::{self, Write}; @@ -218,7 +217,6 @@ impl<'a> CfgField<'a> { /// Ensure all fields with known default values exist. pub fn populate_defaults(spec: &CfgSpec, config: &KV) { for group in spec.groups { - NYI!("asdf"); for field in group.fields { match field { CfgField::Silent { default, .. } => { diff --git a/src/hedgedoc.rs b/src/hedgedoc.rs index a8c8f6d..472f888 100644 --- a/src/hedgedoc.rs +++ b/src/hedgedoc.rs @@ -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::>().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 { // 1. remove HTML comments - let re_comment = Regex::new(r"(?s)").unwrap(); - let pad_content = re_comment.replace_all(&pad_content, "").to_string(); + let pad_content = strip_metadata(pad_content); // 2. accumulate topic lines let re_header = Regex::new(r"^\s*##(#*) TOP ([\d.]+\s*.*?)\s*#*$").unwrap(); let mut result: Vec = Vec::new(); diff --git a/src/main.rs b/src/main.rs index efe6b47..696e3d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,14 +30,11 @@ 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…) */ -#![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 colored::Colorize; use regex::Regex; -use std::borrow::Cow; use std::env; use std::error::Error; use std::fmt::Display; @@ -45,7 +42,6 @@ use std::io::IsTerminal; use cccron_lib::config_spec::{self, CfgField, CfgGroup, CfgSpec}; use cccron_lib::key_value::KeyValueStore as KV; -mod variables_and_settings; use cccron_lib::date; 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::NYI; -const FALLBACK_TEMPLATE: &str = variables_and_settings::FALLBACK_TEMPLATE; - /* ***** Config Spec ***** */ const CONFIG_SPEC: CfgSpec<'static> = CfgSpec { 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.]", description: "Text added at the bottom of every email.", }, + CfgField::Default { key: "fallback-template", + default: "---\ndate: \"{{datum-iso}}\"\n...\n\n\n\ + # Plenum vom {{datum}}\n**Beginn 20:XX**\n\n## Themen\n\n[toc]\n\n## Anwesend\nX\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> { } else { 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 = 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 = 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(¤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 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(); - }; - Ok(()) -} - -fn get_all_weekdays(month_offset: i32, week_day: Weekday) -> Vec { - 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( @@ -568,8 +364,8 @@ fn generate_new_pad_for_following_date( 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(), + .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( @@ -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"()").unwrap(); - let result: Cow = 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 { @@ -662,26 +452,24 @@ fn topic_count(n: usize, dative: bool) -> String { /* ***** 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 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()); let n_topics = toc.lines().count(); 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 ***** */ -// BBBBBBBBBB - fn do_announcement( ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, _wiki: &Mediawiki, ) -> Result<(), Box> { // 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 let subject = format!( "Plenum {} (am {}): bisher {}", @@ -700,7 +488,7 @@ fn do_announcement( ); let body = format!("{line1}\n\n{line2}"); // 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) config.set("email-message-id", &message_id).ok(); config.set("state-name", &ProgramState::Announced.to_string()).ok(); @@ -713,39 +501,42 @@ fn do_reminder( _wiki: &Mediawiki, ) -> Result<(), Box> { // 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(); // construct email - let subject_suffix = if n_topics == 0 { - " fällt aus (keine Themen)".to_string() + 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!(" 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 { 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 { 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 {}", - hedgedoc.format_url(¤t_pad_id) + "{body_prefix}Die vollen Details findet ihr im Pad:\n {}\n\nBis {} um 20:00!", + hedgedoc.format_url(¤t_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 Leute sich darauf einstellen können.)" - .to_string() + (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)?; + 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!( @@ -762,11 +553,26 @@ fn do_protocol( ttp: i64, plenum_day: &NaiveDate, config: &KV, hedgedoc: &HedgeDoc, email: &SimpleEmail, wiki: &Mediawiki, ) -> Result<(), Box> { - // TODO: get pad - // TODO: write to wiki - // TODO: write to email - // TODO: set state as Logged - todo!() + let (current_pad_id, pad_content, toc, n_topics) = get_pad_info(config, hedgedoc); + if toc.len() > 0 { + let human_date = plenum_day.format("%d.%m.%Y"); + let pad_content = hedgedoc::strip_metadata(pad_content); + 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}", + "" + ); + 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