Clean up key/value store, add convenience wrapper.
This commit is contained in:
parent
08878169bc
commit
19be226f11
135
src/key_value.rs
135
src/key_value.rs
|
@ -1,12 +1,23 @@
|
||||||
use rusqlite::{params, Connection, Result};
|
use rusqlite::{Connection, Result, params};
|
||||||
// use std::collections::HashMap;
|
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 {
|
pub struct KeyValueStore {
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
|
has_write_errors: Cell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyValueStore {
|
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> {
|
pub fn new(db_path: &str) -> Result<Self> {
|
||||||
let conn = Connection::open(db_path)?;
|
let conn = Connection::open(db_path)?;
|
||||||
conn.execute(
|
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
|
/// Report if any write errors occurred.
|
||||||
pub fn set(&self, key: &str, value: &str) -> Result<()> {
|
pub fn has_errors(&self) -> bool {
|
||||||
self.conn.execute(
|
self.has_write_errors.get()
|
||||||
"INSERT INTO kv_store (key, value) VALUES (?1, ?2)
|
|
||||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
||||||
params![key, value],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 result = row.get(0)?;
|
||||||
|
Ok(result)
|
||||||
|
} else {
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
pub fn default(&self, key: &str, value: &str) {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT INTO kv_store (key, value) VALUES (?1, ?2)
|
"INSERT INTO kv_store (key, value) VALUES (?1, ?2)
|
||||||
ON CONFLICT(key) DO NOTHING",
|
ON CONFLICT(key) DO NOTHING",
|
||||||
params![key, value],
|
params![key, value],
|
||||||
).ok();
|
).expect(&format!("Failed to write default at key: {}", key));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve a value by key
|
/// Write a `key`/`value` pair to the DB.
|
||||||
pub fn get(&self, key: &str) -> Result<Option<String>> {
|
///
|
||||||
let mut stmt = self.conn.prepare("SELECT value FROM kv_store WHERE key = ?1")?;
|
/// Errors log the failed write and set `has_errors`.
|
||||||
let mut rows = stmt.query(params![key])?;
|
pub fn set(&self, key: &str, value: &str) -> Result<()> {
|
||||||
|
match self.conn.execute(
|
||||||
if let Some(row) = rows.next()? {
|
"INSERT INTO kv_store (key, value) VALUES (?1, ?2)
|
||||||
let value: String = row.get(0)?;
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||||
Ok(Some(value))
|
params![key, value],
|
||||||
} else {
|
) {
|
||||||
Ok(None)
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed DB write: ({}, {})", key, value);
|
||||||
|
self.has_write_errors.set(true);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a key-value pair
|
/// Delete the key-value pair associated with `key`.
|
||||||
|
///
|
||||||
|
/// Errors log the failed write and set `has_errors`.
|
||||||
pub fn delete(&self, key: &str) -> Result<()> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Index<&str> for KeyValueStore {
|
||||||
|
type Output = str;
|
||||||
|
|
||||||
/*
|
fn index(&self, key: &str) -> &Self::Output {
|
||||||
fn main() -> Result<()> {
|
Box::leak(Box::new(self.get(key).unwrap())).as_str()
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
38
src/main.rs
38
src/main.rs
|
@ -28,7 +28,7 @@ mod create_new_pads;
|
||||||
pub mod variables_and_settings;
|
pub mod variables_and_settings;
|
||||||
|
|
||||||
use chrono::{Datelike, Local, NaiveDate, Weekday};
|
use chrono::{Datelike, Local, NaiveDate, Weekday};
|
||||||
use regex::{Regex};
|
use regex::Regex;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
@ -41,7 +41,6 @@ use pandoc;
|
||||||
// MAIL START
|
// MAIL START
|
||||||
use lettre::{Message, SmtpTransport, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
use lettre::message::{header, SinglePart};
|
use lettre::message::{header, SinglePart};
|
||||||
use lettre::message::header::MessageId;
|
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
// MAIL END
|
// 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 FALLBACK_TEMPLATE: &str = variables_and_settings::FALLBACK_TEMPLATE;
|
||||||
const TESTING_MODE: EMailOperationStates = EMailOperationStates::Test;
|
const TESTING_MODE: EMailOperationStates = EMailOperationStates::Test;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn kv_defaults (kv: &KV) {
|
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.]");
|
kv.default("email-signature", "\n\n[Diese Nachricht wurde automatisiert vom Plenumsbot erstellt und ist daher ohne Unterschrift gültig.]");
|
||||||
}
|
}
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
// BEGIN ANKÜNDIGUNGSSCRIPT
|
|
||||||
|
|
||||||
// config
|
// config
|
||||||
let config = KV::new("plenum_config.sqlite").unwrap();
|
let config = KV::new("plenum_config.sqlite").unwrap();
|
||||||
kv_defaults(&config);
|
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
|
// Dienstage diesen Monat
|
||||||
let all_tuesdays: Vec<Option<NaiveDate>> = get_tuesdays(0);
|
let all_tuesdays: Vec<Option<NaiveDate>> = get_tuesdays(0);
|
||||||
|
@ -116,10 +120,10 @@ async fn main() {
|
||||||
let yesterday_was_plenum: bool = check_if_plenum(nächster_plenumtermin.clone(), yesterday);
|
let yesterday_was_plenum: bool = check_if_plenum(nächster_plenumtermin.clone(), yesterday);
|
||||||
|
|
||||||
// Pad-Links aus der Datenbank laden:
|
// Pad-Links aus der Datenbank laden:
|
||||||
let pad_id: String = config.get("aktuelles-plenumspad").unwrap().unwrap();
|
let pad_id = &config["aktuelles-plenumspad"];
|
||||||
let current_pad_link: String = format!("https://md.berlin.ccc.de/{}", pad_id);
|
let current_pad_link: String = format!("{}{}", hedgedoc_server, 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_id = &config["zukünftiges-plenumspad"];
|
||||||
let future_pad_link: String = format!("https://md.berlin.ccc.de/{}", future_pad_id);
|
let future_pad_link: String = format!("{}{}", hedgedoc_server, future_pad_id);
|
||||||
|
|
||||||
let mut message_id: String = String::from("FEHLER"); // message id initialisieren
|
let mut message_id: String = String::from("FEHLER"); // message id initialisieren
|
||||||
|
|
||||||
|
@ -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!");
|
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);
|
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(()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -320,7 +324,7 @@ fn replace_placeholders(template: &str, übernächster_plenumtermin: String, üb
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rotate (future_pad_id: &str, kv: &KV) {
|
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 {
|
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"))
|
kv.set("zukünftiges-plenumspad", &future_pad_id).expect("Fehler beim Beschreiben der Datenbank mit neuem Plenumslink!"); // Beispiel: aktuelles-plenumspad: Ok(Some("eCH24zXGS9S8Stg5xI3aRg"))
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue