caracal 0.4.3

Nostr client for Gemini
use crate::storage::Storage;
use heed::types::{SerdeRmp, Str};
use heed::{Database, RoTxn};
use nostr::{Timestamp, Url};
use serde::{Deserialize, Serialize};
use std::error::Error;

use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, Deserialize)]
pub struct LongFormNoteDraft<'a> {
    #[serde(borrow)]
    pub body: Vec<&'a str>,
    pub title: Option<&'a str>,
    pub summary: Option<&'a str>,
    pub image: Option<Url>,
    pub ts_created: u64,
    pub ts_first_published: Option<u64>,
    pub datetime_created: DateTime<Utc>,
    pub datetime_expires: Option<DateTime<Utc>>,
    pub datetime_autopublish: Option<DateTime<Utc>>,
    pub attachments: Vec<&'a str>,
}

type LongFormDraftsDatabase<'a> =
    Database<Str, SerdeRmp<LongFormNoteDraft<'a>>>;

const DB_NAME: &str = "lf_note_drafts";

impl Storage {
    fn lf_drafts_db(
        &self,
    ) -> Result<LongFormDraftsDatabase<'_>, Box<dyn Error + Send + Sync>> {
        let rtxn = self.get_read_txn()?;

        self.env
            .open_database(&rtxn, Some(DB_NAME))?
            .ok_or(Box::from("Failed to open long-form drafts db"))
    }

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

    pub fn lf_draft_new(
        &self,
        id: &str,
    ) -> Result<(), Box<dyn Error + Send + Sync>> {
        let db = self.lf_drafts_db()?;

        let mut wtxn = self.env.write_txn()?;

        let draft = LongFormNoteDraft {
            body: Vec::new(),
            title: None,
            summary: None,
            image: None,
            ts_created: Timestamp::now().as_secs(),
            ts_first_published: None,
            datetime_created: Utc::now(),
            datetime_expires: None,
            datetime_autopublish: None,
            attachments: Vec::new(),
        };

        db.put(&mut wtxn, id, &draft)?;
        wtxn.commit()?;

        Ok(())
    }

    pub fn lf_draft_store(
        &self,
        id: &str,
        draft: LongFormNoteDraft,
    ) -> Result<(), Box<dyn Error + Send + Sync>> {
        let db = self.lf_drafts_db()?;
        let mut wtxn = self.env.write_txn()?;
        db.put(&mut wtxn, id, &draft)?;
        wtxn.commit()?;
        Ok(())
    }

    pub fn lf_draft_append_gemtext(
        &self,
        id: &str,
        gemtext: &str,
    ) -> Result<(), Box<dyn Error + Send + Sync>> {
        let db = self.lf_drafts_db()?;
        let mut rtxn = self.env.read_txn()?;
        let mut draft = self.lf_draft_get(id, &mut rtxn)?;

        draft.body.extend(gemtext.lines());

        let mut wtxn = self.env.write_txn()?;
        db.put(&mut wtxn, id, &draft)?;
        wtxn.commit()?;

        Ok(())
    }

    pub fn lf_draft_delete_last(
        &self,
        id: &str,
    ) -> Result<(), Box<dyn Error + Send + Sync>> {
        let db = self.lf_drafts_db()?;
        let mut rtxn = self.env.read_txn()?;
        let mut draft = self.lf_draft_get(id, &mut rtxn)?;

        if !draft.body.is_empty() {
            let len = draft.body.len() - 1;
            draft.body.truncate(len);

            let mut wtxn = self.env.write_txn()?;
            db.put(&mut wtxn, id, &draft)?;
            wtxn.commit()?;
        }

        Ok(())
    }

    pub fn lf_draft_get_content(
        &self,
        id: &str,
    ) -> Result<String, Box<dyn Error + Send + Sync>> {
        let rtxn = self.env.read_txn()?;

        match self.lf_drafts_db()?.get(&rtxn, id)? {
            Some(draft) => {
                let content = draft.body.join("\n");
                Ok(content)
            }
            None => Err(Box::from("Cannot get draft")),
        }
    }

    pub fn lf_draft_get<'c>(
        &'c self,
        id: &str,
        rtxn: &'c mut RoTxn,
    ) -> Result<LongFormNoteDraft<'c>, Box<dyn Error + Send + Sync>> {
        self.lf_drafts_db()?
            .get(rtxn, id)?
            .ok_or(Box::from("Cannot find draft"))
    }
}