mollysocket 1.7.1

MollySocket allows getting signal notifications via UnifiedPush.
use eyre::Result;
use rusqlite::{self, Row};
use std::{
    sync::{Arc, Mutex},
    time::{Duration, SystemTime, UNIX_EPOCH},
};

use crate::config;

mod migrations;

pub struct MollySocketDb {
    db: Arc<Mutex<rusqlite::Connection>>,
}

#[derive(Debug)]
pub struct Connection {
    pub uuid: String,
    pub device_id: u32,
    pub password: String,
    pub endpoint: String,
    pub forbidden: bool,
    pub last_registration: OptTime,
}

impl Connection {
    pub fn new(uuid: String, device_id: u32, password: String, endpoint: String) -> Self {
        Connection {
            uuid,
            device_id,
            password,
            endpoint,
            forbidden: false,
            last_registration: OptTime::from(SystemTime::now()),
        }
    }
}

#[derive(Debug)]
pub struct OptTime(pub Option<SystemTime>);

impl From<&OptTime> for i64 {
    fn from(i: &OptTime) -> i64 {
        let instant = match i.0 {
            Some(instant) => instant,
            None => return 0,
        };
        match instant.duration_since(UNIX_EPOCH) {
            Ok(duration) => duration.as_secs() as i64,
            Err(_) => 0,
        }
    }
}

impl From<i64> for OptTime {
    fn from(i: i64) -> OptTime {
        if i == 0 {
            return OptTime(None);
        }
        let duration = Duration::from_secs(i as u64);
        OptTime(UNIX_EPOCH.checked_add(duration))
    }
}

impl From<SystemTime> for OptTime {
    fn from(t: SystemTime) -> Self {
        OptTime(Some(t))
    }
}

impl Connection {
    fn map(row: &Row) -> Result<Connection> {
        Ok(Connection {
            uuid: row.get(0)?,
            device_id: row.get(1)?,
            password: row.get(2)?,
            endpoint: row.get(3)?,
            forbidden: row.get(4)?,
            last_registration: OptTime::from(row.get::<usize, i64>(5)?),
        })
    }
}

impl MollySocketDb {
    pub fn new() -> Result<MollySocketDb> {
        let db = rusqlite::Connection::open(config::get_db())?;
        db.execute_batch(
            "
CREATE TABLE IF NOT EXISTS connections(
    uuid TEXT UNIQUE ON CONFLICT REPLACE,
    device_id INTEGER,
    password TEXT,
    endpoint TEXT,
    forbidden BOOLEAN NOT NULL CHECK (forbidden IN (0, 1)),
    last_registration INTEGER
)
            ",
        )?;
        Ok(MollySocketDb {
            db: Arc::new(Mutex::new(db)),
        })
    }

    pub fn add(&self, co: &Connection) -> Result<()> {
        self.db.lock().unwrap().execute(
            "INSERT INTO connections(uuid, device_id, password, endpoint, forbidden, last_registration)
            VALUES (?, ?, ?, ?, ?, ?);",
            [&co.uuid, &co.device_id.to_string(), &co.password, &co.endpoint, &String::from(if co.forbidden { "1" } else { "0" }), &i64::from(&co.last_registration).to_string()]
        )?;
        Ok(())
    }

    pub fn update_last_registration(&self, uuid: &str) -> Result<()> {
        let now = OptTime::from(SystemTime::now());
        self.db.lock().unwrap().execute(
            "UPDATE connections
            SET last_registration = ?
            WHERE uuid = ?;",
            [&i64::from(&now).to_string(), uuid],
        )?;
        Ok(())
    }

    pub fn list(&self) -> Result<Vec<Connection>> {
        self.db
            .lock()
            .unwrap()
            .prepare("SELECT * FROM connections;")?
            .query_and_then([], Connection::map)?
            .collect::<Result<Vec<Connection>>>()
    }

    pub fn get(&self, uuid: &str) -> Result<Connection> {
        self.db
            .lock()
            .unwrap()
            .prepare("SELECT * FROM connections WHERE uuid=?1 LIMIT 1")?
            .query_and_then([uuid], Connection::map)?
            .next()
            .ok_or(rusqlite::Error::QueryReturnedNoRows)?
    }

    pub fn rm(&self, uuid: &str) -> Result<()> {
        self.db
            .lock()
            .unwrap()
            .execute("DELETE FROM connections WHERE uuid=?1;", [&uuid])?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_db() {
        config::load_config(None);
        let db = MollySocketDb::new().unwrap();
        let uuid = "0d2ff653-3d88-43de-bcdb-f6657d3484e4";
        db.add(&Connection::new(
            String::from(uuid),
            1,
            String::from("pass"),
            String::from("http://0.0.0.0/"),
        ))
        .unwrap();
        assert!(db
            .list()
            .unwrap()
            .iter()
            .map(|co| &co.uuid)
            .any(|row_uuid| row_uuid == uuid));
        db.rm(uuid).unwrap();
    }
}