caracal 0.2.0

Nostr client for Gemini
mod drafts;
mod keys;
mod relations;
mod tags;

use std::error::Error;
use std::fs;
use std::path::Path;

use heed::types::{SerdeRmp, Str};
use heed::{Database, Env, EnvOpenOptions, RoTxn, RwTxn};
use serde::{Deserialize, Serialize};

use nostr_sdk::prelude::*;

#[derive(Debug)]
pub struct Storage {
    env: Env,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct FollowList<'a> {
    #[serde(borrow)]
    pub npubv: Vec<&'a str>,
}

type FollowingDatabase<'a> = Database<Str, SerdeRmp<FollowList<'a>>>;

const FOLLOWING_KEY: &str = "following";

impl Storage {
    pub fn new(path: &Path) -> Result<Storage, Box<dyn Error>> {
        fs::create_dir_all(path)?;

        let env = unsafe {
            EnvOpenOptions::new()
                .map_size(10 * 1024 * 1024)
                .max_dbs(1000)
                .open(path)?
        };

        Ok(Storage { env })
    }

    pub fn get_write_txn(
        &self,
    ) -> Result<RwTxn<'_>, Box<dyn Error + Send + Sync>> {
        Ok(self.env.write_txn()?)
    }

    pub fn get_read_txn(
        &self,
    ) -> Result<RoTxn<'_>, Box<dyn Error + Send + Sync>> {
        Ok(self.env.read_txn()?)
    }

    pub fn following_db(
        &self,
    ) -> Result<FollowingDatabase<'_>, Box<dyn Error + Send + Sync>> {
        let mut rtxn = self.get_read_txn()?;
        let dbo: Option<FollowingDatabase> =
            self.env.open_database(&mut rtxn, Some("following"))?;

        if dbo.is_some() {
            Ok(dbo.unwrap())
        } else {
            Err(Box::from("Error opening following db"))
        }
    }

    pub fn create_following_db(
        &self,
    ) -> Result<FollowingDatabase<'_>, Box<dyn Error + Send + Sync>> {
        let mut wtxn = self.env.write_txn()?;
        let db: FollowingDatabase =
            self.env.create_database(&mut wtxn, Some("following"))?;
        wtxn.commit()?;
        Ok(db)
    }

    pub fn get_followed_list<'c>(
        &'c self,
        rtxn: &'c mut RoTxn,
    ) -> Result<Box<Vec<&'c str>>, Box<dyn Error + Send + Sync>> {
        match self.following_db()?.get(rtxn, FOLLOWING_KEY)? {
            Some(flist) => {
                let mut v = Vec::new();
                for val in flist.npubv {
                    v.push(val);
                }
                Ok(Box::new(v))
            }
            None => Err(Box::from("Error reading flist")),
        }
    }

    pub fn get_followed_pubkeys<'c>(
        &'c self,
        rtxn: &'c mut RoTxn,
    ) -> Result<Vec<PublicKey>, Box<dyn Error + Send + Sync>> {
        match self.following_db()?.get(rtxn, FOLLOWING_KEY)? {
            Some(flist) => {
                let mut v = Vec::new();
                for val in flist.npubv {
                    if let Ok(pk) = PublicKey::parse(val) {
                        v.push(pk);
                    }
                }
                Ok(v)
            }
            None => Err(Box::from("Error reading flist")),
        }
    }

    pub fn follow(
        &self,
        npub: &String,
    ) -> Result<bool, Box<dyn Error + Send + Sync>> {
        let rtxn = self.env.read_txn()?;
        let db = self.following_db()?;
        let f: Option<FollowList> = db.get(&rtxn, FOLLOWING_KEY)?;

        match f {
            Some(mut flist) => {
                let mut wtxn = self.env.write_txn()?;
                let npubvs = npub.as_str();

                if !flist.npubv.contains(&npubvs) {
                    flist.npubv.push(npub.as_str());
                    db.put(&mut wtxn, FOLLOWING_KEY, &flist)?;
                    wtxn.commit()?;
                }
            }
            None => {
                let mut wtxn = self.env.write_txn()?;
                let list = FollowList {
                    npubv: vec![npub.as_str()],
                };
                db.put(&mut wtxn, FOLLOWING_KEY, &list)?;
                wtxn.commit()?;
            }
        }

        Ok(true)
    }

    pub fn unfollow(
        &self,
        npub: &String,
    ) -> Result<bool, Box<dyn Error + Send + Sync>> {
        let rtxn = self.env.read_txn()?;
        let db = self.following_db()?;
        let f: Option<FollowList> = db.get(&rtxn, FOLLOWING_KEY)?;

        match f {
            Some(mut flist) => {
                let mut wtxn = self.env.write_txn()?;
                let npubvs = npub.as_str();

                if flist.npubv.contains(&npubvs) {
                    flist.npubv.retain(|&x| x != npub);

                    db.put(&mut wtxn, FOLLOWING_KEY, &flist)?;
                    wtxn.commit()?;
                }
            }
            None => {
                let mut wtxn = self.env.write_txn()?;
                let list = FollowList {
                    npubv: vec![npub.as_str()],
                };
                db.put(&mut wtxn, FOLLOWING_KEY, &list)?;
                wtxn.commit()?;
            }
        }

        Ok(true)
    }
}