repartee 0.8.0

A modern terminal IRC client built with Ratatui and Tokio
#[allow(dead_code)]
pub mod crypto;
#[allow(dead_code)]
pub mod db;
#[allow(dead_code)]
pub mod query;
pub mod types;
pub mod writer;

pub use types::LogRow;
#[allow(unused_imports)]
pub use types::{ReadMarker, StorageStats, StoredMessage};

use std::sync::{Arc, Mutex};

use rusqlite::Connection;
use tokio::sync::mpsc;

use crate::config::LoggingConfig;
use crate::constants;

/// High-level handle to the storage subsystem.
///
/// Owns the database connection and the background writer task.
/// Created once at startup and shut down when the app exits.
#[allow(dead_code)]
pub struct Storage {
    pub db: Arc<Mutex<Connection>>,
    pub log_tx: mpsc::Sender<LogRow>,
    writer: writer::LogWriterHandle,
    pub encrypt: bool,
}

impl Storage {
    /// Initialize storage from the logging config section.
    ///
    /// Opens (or creates) the `SQLite` database under `~/.repartee/logs/`,
    /// optionally sets up encryption, and spawns the background writer.
    pub fn init(config: &LoggingConfig) -> Result<Self, String> {
        let db_dir = constants::log_dir();
        std::fs::create_dir_all(&db_dir).map_err(|e| format!("failed to create log dir: {e}"))?;

        let db_path = db_dir.join("messages.db");
        let conn = db::open_database_at(
            db_path.to_str().ok_or("invalid log dir path")?,
            config.encrypt,
        )
        .map_err(|e| format!("failed to open log database: {e}"))?;

        let crypto_key = if config.encrypt {
            let hex_key = crypto::load_or_create_key()?;
            Some(crypto::import_key(&hex_key)?)
        } else {
            None
        };

        let has_fts = !config.encrypt;

        // Purge old messages on startup if retention is configured
        if config.retention_days > 0 {
            let removed = db::purge_old_messages(&conn, config.retention_days, has_fts);
            if removed > 0 {
                tracing::info!(
                    "purged {removed} messages older than {} days",
                    config.retention_days
                );
            }
        }

        // Purge old event messages (join/part/quit/nick/kick/mode) on startup
        if config.event_retention_hours > 0 {
            let removed =
                db::purge_old_events(&conn, config.event_retention_hours, has_fts);
            if removed > 0 {
                tracing::info!(
                    "purged {removed} event messages older than {}h",
                    config.event_retention_hours
                );
            }
        }

        let db = Arc::new(Mutex::new(conn));
        let (writer, log_tx) = writer::LogWriterHandle::spawn(Arc::clone(&db), crypto_key);

        Ok(Self {
            db,
            log_tx,
            writer,
            encrypt: config.encrypt,
        })
    }

    /// Drain remaining rows and stop the background writer.
    pub async fn shutdown(self) {
        self.writer.shutdown().await;
    }
}