caracal 0.3.9

Nostr client for Gemini
use std::collections::HashMap;

use fast_config::FastConfig;
use nostr::nips::nip46::NostrConnectURI;
use nostr::{Keys, NostrSigner};
use nostr_sdk::prelude::*;

use once_cell::sync::Lazy;
use std::ops::DerefMut;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use tokio::sync::mpsc::{Receiver, Sender};

use crate::config::{
    BaseRelaysConfig, NostrConfig, PrivateBookmarks, UIConfig,
};
use crate::storage::Storage;

#[derive(Error, Debug)]
pub enum UserError {
    #[error("Nostr client error: {0}")]
    NostrClientError(#[from] nostr_sdk::client::Error),
    #[error("Nostr database error: {0}")]
    NostrDatabaseError(#[from] DatabaseError),
    #[error("Nostr signer error: {0}")]
    NostrSignerError(#[from] SignerError),
    #[error("Blossom client error: {0}")]
    BlossomError(#[from] nostr_blossom::error::Error),
    #[error("IO error: {0}")]
    IOError(#[from] std::io::Error),
    #[error("Keys error: {0}")]
    KeysError(#[from] nostr::key::Error),
}

pub struct CaracalUser {
    pub identity_path: PathBuf,
    pub client: Arc<Client>,
    pub upload_keys: Keys,
    pub signer: Arc<dyn NostrSigner>,
    pub connect_uri: Option<NostrConnectURI>,
    pub storage: Storage,
    pub tx: Sender<u8>,
    pub rx: Receiver<u8>,
}

impl CaracalUser {
    pub fn path_nostr_config(&self) -> PathBuf {
        self.identity_path.join("nostr.json")
    }

    pub fn path_relay_list(&self) -> PathBuf {
        self.identity_path.join("nip65_relays.json")
    }

    pub fn path_ui(&self) -> PathBuf {
        self.identity_path.join("ui.json")
    }

    pub fn path_bookmarks(&self) -> PathBuf {
        self.identity_path.join("private_bookmarks.json")
    }

    pub fn get_client_keys(&self) -> Result<Keys, nostr::key::Error> {
        Keys::parse(&self.nostr_config().client_keys)
    }

    pub async fn configure(&self) {
        if !self.path_relay_list().is_file() {
            let config = BaseRelaysConfig::default();
            if config
                .save(self.path_relay_list(), fast_config::Format::JSON)
                .is_err()
            {
                eprintln!("Failed to save config");
            }
        }

        if !self.path_bookmarks().is_file() {
            let config = PrivateBookmarks::default();
            if config
                .save(self.path_bookmarks(), fast_config::Format::JSON)
                .is_err()
            {
                eprintln!("Failed to init bookmarks");
            }
        }

        if !self.path_ui().is_file() {
            let config = UIConfig::default();
            if config
                .save(self.path_ui(), fast_config::Format::JSON)
                .is_err()
            {
                eprintln!("Failed to save UI config");
            }
        }

        let ui_cfg = self.ui_config();
        rust_i18n::set_locale(&ui_cfg.locale);

        if let Err(err) = self.advertise_relay_list().await {
            eprintln!("Failed to advertise relay list: {err}");
        }
    }

    pub fn save_relay_list(
        &self,
        config: &BaseRelaysConfig,
    ) -> Result<(), fast_config::Error> {
        config.save(self.path_relay_list(), fast_config::Format::JSON)
    }

    pub fn save_ui_config(
        &self,
        config: &UIConfig,
    ) -> Result<(), fast_config::Error> {
        config.save(self.path_ui(), fast_config::Format::JSON)
    }

    pub fn save_nostr_config(
        &self,
        config: &NostrConfig,
    ) -> Result<(), fast_config::Error> {
        config.save(self.path_nostr_config(), fast_config::Format::JSON5)
    }

    pub fn relay_list_config(&self) -> BaseRelaysConfig {
        let mut config = BaseRelaysConfig::default();
        let _ = config.load(self.path_relay_list(), fast_config::Format::JSON);
        config
    }

    pub fn ui_config(&self) -> UIConfig {
        let mut config = UIConfig::default();
        let _ = config.load(self.path_ui(), fast_config::Format::JSON);
        config
    }

    pub fn nostr_config(&self) -> NostrConfig {
        let mut config = NostrConfig::default();
        let _ =
            config.load(self.path_nostr_config(), fast_config::Format::JSON5);
        config
    }

    pub fn priv_bookmarks(&self) -> PrivateBookmarks {
        let mut config = PrivateBookmarks::default();
        let _ = config.load(self.path_bookmarks(), fast_config::Format::JSON);
        config
    }

    pub async fn contact_list_pubkeys(
        &self,
    ) -> Result<Vec<PublicKey>, UserError> {
        let mut pubkeys = Vec::new();
        let signer_pubk = self.signer.get_public_key().await?;

        if let Ok(event) = self
            .db_query_first(
                Filter::new().kind(Kind::ContactList).author(signer_pubk),
            )
            .await
        {
            pubkeys.extend(event.tags.public_keys());
        }

        Ok(pubkeys)
    }

    /// Run a database query
    pub async fn db_query(
        &self,
        filter: Filter,
    ) -> Result<Events, DatabaseError> {
        self.client.database().query(filter).await
    }

    /// Run a database query and return the first event
    pub async fn db_query_first(
        &self,
        filter: Filter,
    ) -> Result<Event, DatabaseError> {
        self.db_query(filter).await.and_then(|events| {
            events.first_owned().ok_or(DatabaseError::NotSupported)
        })
    }

    /// Get our [`MuteList`] from the database
    pub async fn mute_list(&self) -> MuteList {
        let mut mute_list = MuteList::default();

        if let Ok(signer_pubk) = self.signer.get_public_key().await
            && let Ok(event) = self
                .db_query_first(
                    Filter::new().kind(Kind::MuteList).author(signer_pubk),
                )
                .await
        {
            mute_list.public_keys.extend(event.tags.public_keys());
            mute_list
                .hashtags
                .extend(event.tags.hashtags().map(str::to_string));
        }

        mute_list
    }

    pub async fn fetch_quick(
        &self,
        filter: Filter,
    ) -> Result<Events, nostr_sdk::client::Error> {
        self.client
            .fetch_events(filter, Duration::from_secs(5))
            .await
    }

    pub async fn send_builder(
        &self,
        event_builder: EventBuilder,
    ) -> Result<Output<EventId>, nostr_sdk::client::Error> {
        self.client.send_event_builder(event_builder).await
    }

    pub async fn public_key(&self) -> Result<PublicKey, SignerError> {
        self.signer.get_public_key().await
    }

    pub async fn sub(
        &self,
        filter: Filter,
    ) -> Result<Output<SubscriptionId>, nostr_sdk::client::Error> {
        self.client.subscribe(filter, None).await
    }
}

pub static mut USERS: Lazy<HashMap<String, CaracalUser>> =
    Lazy::new(HashMap::new);

pub fn lookup_user(fingerprint: String) -> Option<&'static CaracalUser> {
    #[allow(static_mut_refs)]
    unsafe {
        let us = USERS.deref_mut();
        us.get(&fingerprint)
    }
}

pub fn lookup_user_mut(
    fingerprint: String,
) -> Option<&'static mut CaracalUser> {
    #[allow(static_mut_refs)]
    unsafe {
        let us = USERS.deref_mut();
        us.get_mut(&fingerprint)
    }
}