From 19be226f11e0eea0d74d43454b2f0908c26e8e33 Mon Sep 17 00:00:00 2001 From: nobody Date: Sat, 27 Jul 2024 15:16:44 +0200 Subject: [PATCH] Clean up key/value store, add convenience wrapper. --- src/key_value.rs | 139 +++++++++++++++++++++++++++++------------------ src/main.rs | 56 ++++++++++--------- 2 files changed, 116 insertions(+), 79 deletions(-) diff --git a/src/key_value.rs b/src/key_value.rs index 0347edd..6b00af6 100644 --- a/src/key_value.rs +++ b/src/key_value.rs @@ -1,12 +1,23 @@ -use rusqlite::{params, Connection, Result}; -// use std::collections::HashMap; +use rusqlite::{Connection, Result, params}; +use std::ops::Index; +use std::cell::Cell; +use std::collections::HashSet; +/// Simple SQLite-backed key/value store. +/// +/// All failing writes will log a message, any errors will set `has_errors`. +/// +/// There's `Index` support for convenience (`&kvstore["foo"]`), +/// but **it will leak data.** +/// (That's perfectly fine when used for getting data once for the duration of +/// the program, just use `.get()` for stuff that changes.) pub struct KeyValueStore { conn: Connection, + has_write_errors: Cell, } impl KeyValueStore { - // Initialize the database and create the table if it doesn't exist + /// Open or create a DB with a `kv_store` table storing all tuples. pub fn new(db_path: &str) -> Result { let conn = Connection::open(db_path)?; conn.execute( @@ -16,73 +27,95 @@ impl KeyValueStore { )", [], )?; - Ok(KeyValueStore { conn }) + Ok(Self { conn, has_write_errors: Cell::new(false), }) } - // Insert or update a key-value pair - pub fn set(&self, key: &str, value: &str) -> Result<()> { - self.conn.execute( - "INSERT INTO kv_store (key, value) VALUES (?1, ?2) - ON CONFLICT(key) DO UPDATE SET value = excluded.value", - params![key, value], - )?; - Ok(()) + /// Report if any write errors occurred. + pub fn has_errors(&self) -> bool { + self.has_write_errors.get() } - pub fn default(&self, key: &str, value: &str) { - self.conn.execute( - "INSERT INTO kv_store (key, value) VALUES (?1, ?2) - ON CONFLICT(key) DO NOTHING", - params![key, value], - ).ok(); - } - - // Retrieve a value by key - pub fn get(&self, key: &str) -> Result> { + /// Get value at `key`. (Use `.unwrap_or(default)` & friends to work with the Result.) + pub fn get(&self, key: &str) -> Result { let mut stmt = self.conn.prepare("SELECT value FROM kv_store WHERE key = ?1")?; let mut rows = stmt.query(params![key])?; if let Some(row) = rows.next()? { - let value: String = row.get(0)?; - Ok(Some(value)) + let result = row.get(0)?; + Ok(result) } else { - Ok(None) + Err(rusqlite::Error::QueryReturnedNoRows) } } - // Delete a key-value pair + /// If `key` is absent from the DB, write `key`/`value` pair (else leave unchanged). + /// + /// Will panic on error. (The idea is to call this a bunch of times at boot, + /// to populate the DB if missing / newly created, and not use it after.) + pub fn default(&self, key: &str, value: &str) { + self.conn.execute( + "INSERT INTO kv_store (key, value) VALUES (?1, ?2) + ON CONFLICT(key) DO NOTHING", + params![key, value], + ).expect(&format!("Failed to write default at key: {}", key)); + } + + /// Write a `key`/`value` pair to the DB. + /// + /// Errors log the failed write and set `has_errors`. + pub fn set(&self, key: &str, value: &str) -> Result<()> { + match self.conn.execute( + "INSERT INTO kv_store (key, value) VALUES (?1, ?2) + ON CONFLICT(key) DO UPDATE SET value = excluded.value", + params![key, value], + ) { + Ok(_) => Ok(()), + Err(e) => { + eprintln!("Failed DB write: ({}, {})", key, value); + self.has_write_errors.set(true); + Err(e) + } + } + } + + /// Delete the key-value pair associated with `key`. + /// + /// Errors log the failed write and set `has_errors`. pub fn delete(&self, key: &str) -> Result<()> { - self.conn.execute("DELETE FROM kv_store WHERE key = ?1", params![key])?; + match self.conn.execute("DELETE FROM kv_store WHERE key = ?1", params![key]) { + Ok(_) => Ok(()), + Err(e) => { + eprintln!("Failed DB write: ({}, NULL)", key); + self.has_write_errors.set(true); + Err(e) + } + } + } + + /// Dump the current DB contents to stderr. + /// + /// For all keys listed in `redacted_keys`, no value is printed, only the presence is reported. + pub fn dump_redacting(&self, redacted_keys: &[&str]) -> Result<()> { + let exclude_set: HashSet<&str> = redacted_keys.iter().cloned().collect(); + let mut stmt = self.conn.prepare("SELECT key, value FROM kv_store")?; + let mut rows = stmt.query(params![])?; + while let Some(row) = rows.next()? { + let key: String = row.get(0)?; + if exclude_set.contains(key.as_str()) { + eprintln!("{} = REDACTED", key); + } else { + let value: String = row.get(1)?; + eprintln!("{} = {}", key, value); + } + } Ok(()) } } +impl Index<&str> for KeyValueStore { + type Output = str; -/* -fn main() -> Result<()> { - let kv_store = KeyValueStore::new("kv_store.db")?; - - // Set some key-value pairs - kv_store.set("last_week_pad", "pad_id_123")?; - kv_store.set("email_id", "email_id_456")?; - - // Retrieve and print values - if let Some(value) = kv_store.get("last_week_pad")? { - println!("Last week's pad ID: {}", value); - } else { - println!("Key not found"); + fn index(&self, key: &str) -> &Self::Output { + Box::leak(Box::new(self.get(key).unwrap())).as_str() } - - if let Some(value) = kv_store.get("email_id")? { - println!("Email ID: {}", value); - } else { - println!("Key not found"); - } - - // Delete a key-value pair - kv_store.delete("last_week_pad")?; - - Ok(()) } - -*/ \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f07f6bc..ddcd8b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ mod create_new_pads; pub mod variables_and_settings; use chrono::{Datelike, Local, NaiveDate, Weekday}; -use regex::{Regex}; +use regex::Regex; use uuid::Uuid; use reqwest::Client; use std::error::Error; @@ -41,7 +41,6 @@ use pandoc; // 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 @@ -56,20 +55,25 @@ const PLENUM_TEMPLATE_URL: &str = variables_and_settings::PLENUM_TEMPLATE_URL; const FALLBACK_TEMPLATE: &str = variables_and_settings::FALLBACK_TEMPLATE; const TESTING_MODE: EMailOperationStates = EMailOperationStates::Test; - - fn kv_defaults (kv: &KV) { - kv.default("template-url", "https://md.berlin.ccc.de/plenum-template"); + kv.default("hedgedoc-server-url","https://md.berlin.ccc.de/"); + kv.default("hedgedoc-template-name","plenum-template"); + // hedgedoc-last-id, hedgedoc-next-id have no defaults + kv.default("email-server","mail.berlin.ccc.de"); + kv.default("email-user","plenum-bot@berlin.ccc.de"); + kv.default("email-name","Plenumsbot"); + // add email-pass and email-to manually + // email-last-message-id has no default 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 - +async fn main() -> Result<(), Box> { // 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"))); + let hedgedoc_server = &config["hedgedoc-server-url"]; + println!("[START]\nAktueller Zustand der DB:"); + config.dump_redacting(&["email-pass","matrix-pass"]).ok(); // Dienstage diesen Monat let all_tuesdays: Vec> = get_tuesdays(0); @@ -110,21 +114,21 @@ async fn main() { // 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 pad_id = &config["aktuelles-plenumspad"]; + let current_pad_link: String = format!("{}{}", hedgedoc_server, pad_id); + let future_pad_id = &config["zukünftiges-plenumspad"]; + let future_pad_link: String = format!("{}{}", hedgedoc_server, 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 { @@ -183,10 +187,10 @@ async fn main() { 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"))); - - + println!("[ENDE]\nAktueller Zustand der DB:"); + config.dump_redacting(&["email-pass","matrix-pass"]).ok(); + if config.has_errors() { Err("There were errors.".into()) } else { Ok(()) } } @@ -197,7 +201,7 @@ async fn download_and_return_pad(pad_link: String) -> Result Vec>{ @@ -256,7 +260,7 @@ fn mail_versenden(inhalt: String, betreff: String) -> std::result::Result"} else {"CCCB Intern "}; - + let email = Message::builder() // Set the sender's name and email address .from("Plenum Bot ".parse().unwrap()) @@ -320,7 +324,7 @@ fn replace_placeholders(template: &str, übernächster_plenumtermin: String, üb } fn rotate (future_pad_id: &str, kv: &KV) { - let next_plenum_pad = kv.get("zukünftiges-plenumspad").unwrap_or_else(|error | None); + 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 { @@ -339,7 +343,7 @@ fn try_to_remove_top_instructions (pad_content: String) -> String { fn pad_ins_wiki(old_pad_content: String) { //Convert Markdown into Mediawiki - + let pandoc_parsed = old_pad_content; // MUSS GEÄNDERT WERDEN - + }