butterfly-bot 0.8.0

Butterfly Bot is an opinionated personal-ops AI assistant built for people who want results, not setup overhead.
Documentation
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

use diesel::prelude::*;
use diesel::sql_types::Text;
use diesel::sqlite::SqliteConnection;
use serde_json::Value;

use crate::config::Config;
use crate::error::{ButterflyBotError, Result};

#[derive(QueryableByName)]
struct ConfigRow {
    #[diesel(sql_type = Text)]
    config_json: String,
}

pub fn ensure_parent_dir(path: &str) -> Result<()> {
    let path = Path::new(path);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|e| ButterflyBotError::Runtime(e.to_string()))?;
    }
    Ok(())
}

fn open_conn(db_path: &str) -> Result<SqliteConnection> {
    crate::db::open_sqlcipher_connection_sync(db_path)
}

fn ensure_table(conn: &mut SqliteConnection) -> Result<()> {
    diesel::sql_query(
        "CREATE TABLE IF NOT EXISTS app_config (
            id INTEGER PRIMARY KEY,
            config_json TEXT NOT NULL,
            updated_at INTEGER NOT NULL
        )",
    )
    .execute(conn)
    .map_err(|e| ButterflyBotError::Runtime(e.to_string()))?;
    Ok(())
}

pub fn load_config(db_path: &str) -> Result<Config> {
    ensure_parent_dir(db_path)?;
    let mut conn = open_conn(db_path)?;
    ensure_table(&mut conn)?;

    let row: ConfigRow = diesel::sql_query("SELECT config_json FROM app_config WHERE id = 1")
        .get_result(&mut conn)
        .map_err(|e| ButterflyBotError::Config(e.to_string()))?;

    let value: Value = serde_json::from_str(&row.config_json)
        .map_err(|e| ButterflyBotError::Config(e.to_string()))?;
    let config: Config =
        serde_json::from_value(value).map_err(|e| ButterflyBotError::Config(e.to_string()))?;
    Ok(config)
}

pub fn save_config(db_path: &str, config: &Config) -> Result<()> {
    ensure_parent_dir(db_path)?;
    let mut conn = open_conn(db_path)?;
    ensure_table(&mut conn)?;

    let ts = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|e| ButterflyBotError::Runtime(e.to_string()))?
        .as_secs() as i64;
    let config_json =
        serde_json::to_string(config).map_err(|e| ButterflyBotError::Config(e.to_string()))?;

    diesel::sql_query(
        "INSERT INTO app_config (id, config_json, updated_at)
         VALUES (1, ?1, ?2)
         ON CONFLICT(id) DO UPDATE SET config_json = excluded.config_json, updated_at = excluded.updated_at",
    )
    .bind::<Text, _>(config_json)
    .bind::<diesel::sql_types::BigInt, _>(ts)
    .execute(&mut conn)
    .map_err(|e| ButterflyBotError::Runtime(e.to_string()))?;

    Ok(())
}