// 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 */ // Import other .rs files as modules mod key_value; use std::borrow::Cow; use key_value::KeyValueStore as KV; mod create_new_pads; pub mod variables_and_settings; use chrono::{Datelike, Local, NaiveDate, Weekday}; use regex::{Regex}; use uuid::Uuid; use reqwest::Client; use std::error::Error; // use std::future::Future; use pandoc; // use std::process::Command; // use headers::ContentType; // MAIL START use lettre::{Message, SmtpTransport, Transport}; use lettre::message::{header, SinglePart}; use lettre::message::header::MessageId; use lettre::transport::smtp::authentication::Credentials; // MAIL END const HEDGEDOC_SERVER_URL: &str = variables_and_settings::HEDGEDOC_SERVER_URL; const PLENUM_TEMPLATE_URL: &str = variables_and_settings::PLENUM_TEMPLATE_URL; const FALLBACK_TEMPLATE: &str = variables_and_settings::FALLBACK_TEMPLATE; const TESTING_MODE: bool = false; fn kv_defaults (kv: &KV) { kv.default("template-url", "https://md.berlin.ccc.de/plenum-template"); kv.default("email-signature", "\n\n[Diese Nachricht wurde automatisiert vom Plenumsbot erstellt und ist daher ohne Unterschrift gültig.]"); } #[tokio::main] async fn main() { // BEGIN ANKÜNDIGUNGSSCRIPT // config let config = KV::new("plenum_config.sqlite").unwrap(); kv_defaults(&config); println!("[BEGINNING] Aktuelle Situation der Datenbank: \"current_pad_link\": https://md.berlin.ccc.de/{}, \"zukünftiges-plenumspad\" https://md.berlin.ccc.de/{}", config.get("aktuelles-plenumspad").unwrap().unwrap(), config.get("zukünftiges-plenumspad").unwrap_or_else(|error|Option::from(String::from("error"))).unwrap_or(String::from("error"))); // Dienstage diesen Monat let all_tuesdays: Vec> = get_tuesdays(0); let zweiter_dienstag: String = all_tuesdays[1].unwrap().to_string(); // z.B. 2024-07-09 let vierter_dienstag: String = all_tuesdays[3].unwrap().to_string(); // z.B. 2024-07-23 //Dienstage des nächsten Monats definieren let all_tuesdays_next_month: Vec> = get_tuesdays(1); let zweiter_dienstag_nächster_monat: String = all_tuesdays_next_month[1].unwrap().to_string(); let vierter_dienstag_nächster_monat: String = all_tuesdays_next_month[3].unwrap().to_string(); // Daten, die später benutzt werden, definieren let today= Local::now(); let today_simple = NaiveDate::from_ymd_opt(today.year(), today.month(), today.day()).unwrap(); let in_1_day: NaiveDate = NaiveDate::from_ymd_opt(today.year(), today.month(), today.day()).unwrap().succ_opt().unwrap(); let in_2_days: NaiveDate = in_1_day.succ_opt().unwrap(); let in_3_days: NaiveDate = in_2_days.succ_opt().unwrap(); let yesterday: NaiveDate = NaiveDate::from_ymd_opt(today.year(), today.month(), today.day()).unwrap().pred_opt().unwrap(); // Nächste Plena nachschauen: let nächster_plenumtermin: String = if all_tuesdays[1].unwrap() >= today_simple.pred_opt().unwrap() { // Für das Pad rumschicken am nächsten Tag wird das Datum einen Tag nach Hinten gesetzt, zweiter_dienstag.clone() } else { vierter_dienstag.clone() }; let übernächster_plenumtermin = if all_tuesdays[1].unwrap() >= today_simple.pred_opt().unwrap() { // hier das Gleiche. vierter_dienstag.clone() } else { zweiter_dienstag_nächster_monat.clone() }; let überübernächster_plenumtermin = if all_tuesdays[1].unwrap() >= today_simple.pred_opt().unwrap() { // hier das Gleiche. zweiter_dienstag_nächster_monat.clone() } 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 pad_id: String = config.get("aktuelles-plenumspad").unwrap().unwrap(); let current_pad_link: String = format!("https://md.berlin.ccc.de/{}", pad_id); let future_pad_id:String = config.get("zukünftiges-plenumspad").unwrap_or_else(|error|Option::from(String::from("error"))).unwrap_or(String::from("error")); let future_pad_link: String = format!("https://md.berlin.ccc.de/{}", future_pad_id); let mut message_id: String = String::from("FEHLER"); // message id initialisieren // let in_3_days_is_plenum = true; let top_anzahl: i32 = 0; // Muss noch gecodet werden let in_1_day_is_plenum = true; if in_1_day_is_plenum { println!("In 1 Tag ist Plenum, deshalb wird eine Erinnerung raus geschickt!"); let pad_content = download_and_return_pad(format!("{}/download", current_pad_link.clone())).await.expect("Fehler beim Download des Pads!"); let pad_content_without_top_instructions = try_to_remove_top_instructions(pad_content); 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) } println!("Pad-content geladen!"); if number_of_tops(&pad_content_without_top_instructions) != 0 { // Mail an alle senden, findet statt let message: String = format!("Hallo liebe Mitreisenden,\nEs gibt Themen, deshalb wird das morgige Plenum statt finden.\nAnbei das Plenumspad:\n{current_pad_link}\nUnd hier ein TL;DR von den aktuellen Themen:{tldr}\n\nBis morgen, 20 Uhr!"); println!("---E-Mail:---\n{}\n-----------", message); let betreff = format!("Plenum vom {} findet statt", nächster_plenumtermin); message_id = mail_versenden(message, betreff).expect("Plenum findet statt. Mail wurde versucht zu senden, konnte aber nicht gesendet werden!") } else { // Mail an alle senden und absagen let message: String = format!("Hallo liebe Mitreisenden,\nEs sind keine Themen zusammengekommen, deshalb wird das morgige Plenum leider nicht statt finden.\nHier ist der Link zum Pad vom nächsten Plenum, das am {} statt finden wird: {future_pad_link} und hier der morgige Padlink, der nicht mehr gebraucht wird: {current_pad_link}\nBis zum nächsten Plenum.\n\n[Diese Nachricht wurde automatisiert vom Plenumsbot erstellt und ist daher ohne Unterschrift gültig.]", nächster_plenumtermin); println!("---E-Mail:---\n{}\n-----------", message); let betreff = format!("Plenum vom {} muss leider ausfallen", nächster_plenumtermin); message_id = mail_versenden(message, betreff).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!"); let pad_content = download_and_return_pad(current_pad_link.clone()).await.expect("Fehler beim Download des Pads!"); let pad_content_without_top_instructions = try_to_remove_top_instructions(pad_content); println!("Pad-content geladen!"); if number_of_tops(&pad_content_without_top_instructions) == 0 { // Mail an alle senden und sagen, dass es noch keine Themen gibt let message: String = format!("Hallo liebe Mitreisenden,\nEs sind leider bisher keine Themen zusammengekommen. Wenn bis Sonntag Abend keine Themen zusammenkommen, wird das Plenum voraussichtlich nicht statt finden.\nHier ist der Link zum Pad, wo ihr noch Themen einragen könnt: {}\n\n[Diese Nachricht wurde automatisiert vom Plenumsbot erstellt und ist daher ohne Unterschrift gültig.]", current_pad_link.clone()); let betreff = format!("Plenum vom {}:Bisher noch keine Plenumsthemen", nächster_plenumtermin); println!("---E-Mail:---\n{}\n-----------", message); message_id = mail_versenden(message, betreff).expect("Noch nicht genug Themen. Mail wurde versucht zu senden, konnte aber nicht gesendet werden!") } println!("message id: {}", message_id) } else if yesterday_was_plenum { // This logic breaks on 02/2034, but on every other month it works let old_pad_content = download_and_return_pad(format!("{}/download", current_pad_link.clone())).await.expect("Fehler beim Download des Pads!"); 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("aktuelles-plenumspad"), &config.get("zukünftiges-plenumspad")); 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 mut tldr = String::new(); for element in tldr_vec { tldr.push_str("\n"); tldr.push_str(&element) } let message: String = format!("Hallo liebe Mitreisenden,\nAnbei das gestrige Plenumspad.\nHier sind die Links zum nächsten: {}\nund zum übernächsten Plenum:\nTL;DR:{} {}\nUnd hier ist das Protokoll des letzten Plenums:\n{}\n[Diese Nachricht wurde automatisiert vom Plenumsbot erstellt und ist daher ohne Unterschrift gültig.]", current_pad_link, future_pad_link, tldr, old_pad_content_without_top_instructions.clone()); let betreff: String = format!("Plenumsprotokoll vom {}: Es gab {} TOPs", nächster_plenumtermin, top_anzahl); println!("---E-Mail:---\n{}\n-----------", message); message_id = mail_versenden(message, betreff).expect("Mail mit Plenumsprotokoll wurde versucht zu senden, konnte aber nicht gesendet werden!"); pad_ins_wiki(old_pad_content_without_top_instructions); } println!("[END] Aktuelle Situation der Datenbank: \"current_pad_link\": https://md.berlin.ccc.de/{}, \"zukünftiges-plenumspad\" https://md.berlin.ccc.de/{}", config.get("aktuelles-plenumspad").unwrap().unwrap(), config.get("zukünftiges-plenumspad").unwrap_or_else(|error|Option::from(String::from("error"))).unwrap_or(String::from("error"))); } async fn download_and_return_pad(pad_link: String) -> Result> { //https://md.berlin.ccc.de/OlizTqsSQEeqil0OZmo-Qw/download let pad_content: String = reqwest::get(pad_link) .await? .text() .await?; //println!("{}", pad_content); Ok(pad_content) } fn get_tuesdays(month_offset: u32) -> Vec>{ // Jeden Donnerstag im Monat finden: let today = Local::now(); // jetzige Zeit abrufen (z.B. 2024-07-17 15:36:31.660997700 +02:00) let current_month = today.month(); // diesen Monat als Zahl (z.B. 7) let current_year = today.year(); // dieses Jahr als Zahl (z.B. 2024) let first_day: Option = NaiveDate::from_ymd_opt(current_year, current_month+month_offset, 1); // Erster Tag des Monats (z.B. 2024-07-01) let last_day: Option = NaiveDate::from_ymd_opt(current_year, current_month+1+month_offset, 1).unwrap().pred_opt(); //Letzter Tag des Monats(z.B. 2024-07-31) // - Vektor mit allen Tagen im Monat erstellen let mut days_in_month_vec: Vec> = Vec::new(); let mut all_tuesdays: Vec> = Vec::new(); let mut current_day: Option = first_day; while current_day <= last_day { days_in_month_vec.push(current_day); current_day = current_day.unwrap().succ_opt() } // - Alle bis auf Donnerstage raus for element in days_in_month_vec { match element.unwrap().weekday() { Weekday::Tue => all_tuesdays.push(element), _ => {} // Kein Dienstag, } } all_tuesdays } 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 mail_versenden(inhalt: String, betreff: String) -> std::result::Result> { // Define the email let message_id: String = Uuid::new_v4().to_string() + &String::from("@berlin.ccc.de"); let mail_to: &str = if TESTING_MODE { "Marek Krug "} else {"CCCB Intern "}; let email = Message::builder() // Set the sender's name and email address .from("Plenum Bot ".parse().unwrap()) // Set the recipient's name and email address .to(mail_to.parse().unwrap()) .message_id(Option::from(message_id.clone())) .in_reply_to("19de3985-05b4-42f3-9a6a-e941479be2ed@berlin.ccc.de".to_string()) // Set the subject of the email .subject(betreff) .singlepart(SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .body(inhalt)) .unwrap(); // Set up the SMTP client let creds = Credentials::new("plenum-bot@berlin.ccc.de".to_string(), "m9yBn16pUtbC".to_string()); // Open a remote connection to gmail let mailer = SmtpTransport::starttls_relay("mail.berlin.ccc.de")? .credentials(creds) .build(); // Send the email match mailer.send(&email) { Ok(_) => println!("Email sent successfully!"), Err(e) => eprintln!("Could not send email: {:?}", e), } Ok(message_id) } async fn generate_new_pad_for_following_date(übernächster_plenumtermin: String, überübernächster_plenumtermin: String, kv: &KV) -> Result<(), Box> { let client = Client::new(); match create_new_pads::create_pad(&client, HEDGEDOC_SERVER_URL).await { Ok(pad_url) => { println!("Pad created successfully at URL: {}", pad_url); // Get the most recent plenum template and replace the placeholders: let template_from_pad = download_and_return_pad(PLENUM_TEMPLATE_URL.to_string()).await; // Download Pad let template_content: String = template_from_pad.unwrap_or_else(|error| FALLBACK_TEMPLATE.to_string()); // If Download wasn't successful, use offline Template 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 let future_pad_id: &str = pad_url.trim_start_matches(&format!("{}/", HEDGEDOC_SERVER_URL)); match create_new_pads::import_note(&client, template_modified, Some(future_pad_id), HEDGEDOC_SERVER_URL).await { Ok(_) => { println!("Pad updated successfully with template content."); rotate (future_pad_id, kv); }, Err(e) => println!("Failed to update pad: {}", e), } } Err(e) => println!("Failed to create pad: {}", e), } Ok(()) } fn replace_placeholders(template: &str, übernächster_plenumtermin: String, überübernächster_plenumtermin: String) -> Result> { 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").unwrap_or_else(|error | None); 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"()").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 } fn pad_ins_wiki(old_pad_content: String) { //Convert Markdown into Mediawiki let pandoc_parsed = old_pad_content; // MUSS GEÄNDERT WERDEN }