Clean up key/value store, add convenience wrapper.
This commit is contained in:
		
							parent
							
								
									08878169bc
								
							
						
					
					
						commit
						19be226f11
					
				
					 2 changed files with 116 additions and 79 deletions
				
			
		
							
								
								
									
										139
									
								
								src/key_value.rs
									
										
									
									
									
								
							
							
						
						
									
										139
									
								
								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<bool>, | ||||
| } | ||||
| 
 | ||||
| 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<Self> { | ||||
|         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<Option<String>> { | ||||
|     /// Get value at `key`.  (Use `.unwrap_or(default)` & friends to work with the Result.)
 | ||||
|     pub fn get(&self, key: &str) -> Result<String> { | ||||
|         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(()) | ||||
| } | ||||
| 
 | ||||
| */ | ||||
							
								
								
									
										56
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										56
									
								
								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<dyn Error>> { | ||||
|     // 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<Option<NaiveDate>> = 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<String, Box<dyn std | |||
|         .text() | ||||
|         .await?; | ||||
|     //println!("{}", pad_content);
 | ||||
|     Ok(pad_content) 
 | ||||
|     Ok(pad_content) | ||||
| } | ||||
| 
 | ||||
| fn get_tuesdays(month_offset: u32) -> Vec<Option<NaiveDate>>{ | ||||
|  | @ -256,7 +260,7 @@ fn mail_versenden(inhalt: String, betreff: String) -> std::result::Result<String | |||
|     let message_id: String = Uuid::new_v4().to_string() + &String::from("@berlin.ccc.de"); | ||||
| 
 | ||||
|     let mail_to: &str = if TESTING_MODE == EMailOperationStates::Test { "Marek Krug <mail@marekkrug.de>"} else {"CCCB Intern <intern@berlin.ccc.de>"}; | ||||
|     
 | ||||
| 
 | ||||
|     let email = Message::builder() | ||||
|         // Set the sender's name and email address
 | ||||
|         .from("Plenum Bot <plenum-bot@berlin.ccc.de>".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
 | ||||
|     
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 nobody
						nobody