caracal 0.2.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 thiserror::Error;
use tokio::sync::mpsc::{Receiver, Sender};

use crate::config::BaseRelaysConfig;
use crate::relays::default_relays;
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),
}

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_relay_list(&self) -> PathBuf {
        self.identity_path.join("nip65_relays.json")
    }

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

        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 relay_list_config(&self) -> BaseRelaysConfig {
        let mut config = BaseRelaysConfig::default();
        let _ = config.load(self.path_relay_list(), fast_config::Format::JSON);
        config
    }

    pub async fn advertise_relay_list(
        &self,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let relay_config = self.relay_list_config();

        let mut relay_metadata: HashMap<RelayUrl, Option<RelayMetadata>> =
            HashMap::new();

        for rurl in relay_config.read_relays {
            let _ = self.client.add_read_relay(&rurl).await;

            relay_metadata.insert(rurl, Some(RelayMetadata::Read));
        }

        for wurl in relay_config.write_relays {
            let _ = self.client.add_write_relay(&wurl).await;

            relay_metadata.insert(wurl, Some(RelayMetadata::Write));
        }

        let relay_list_builder = EventBuilder::relay_list(relay_metadata);

        self.client
            .send_event_builder_to(default_relays(), relay_list_builder)
            .await?;

        /* Send (fixed list for now to make the DMs UI work) inbox relays event */
        let inboxr: Vec<&str> =
            vec!["wss://relay.damus.io", "wss://nostr.bitcoiner.social"];

        let inbox_relays_builder = EventBuilder::new(Kind::InboxRelays, "")
            .tags(
                inboxr
                    .into_iter()
                    .map(|ir| Tag::custom(TagKind::Relay, vec![ir])),
            );

        self.client
            .send_event_builder_to(default_relays(), inbox_relays_builder)
            .await?;

        Ok(())
    }

    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 {
            if 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 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.clone())
    }
}

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.clone())
    }
}