nostr-database 0.45.0-alpha.4

Events database trait for nostr apps.
Documentation
// Copyright (c) 2022-2023 Yuki Kishimoto
// Copyright (c) 2023-2025 Rust Nostr Developers
// Distributed under the MIT software license

//! Nostr database extension

use std::collections::{BTreeSet, HashMap, HashSet};

use nostr::prelude::*;

use crate::error::Error;
use crate::{Events, NostrDatabase, Profile, RelaysMap};

/// Nostr Event Store Extension
pub trait NostrDatabaseExt: NostrDatabase {
    /// Get public key metadata
    fn metadata(&self, public_key: PublicKey) -> BoxedFuture<'_, Result<Option<Metadata>, Error>> {
        Box::pin(async move {
            let filter = Filter::new()
                .author(public_key)
                .kind(Kind::Metadata)
                .limit(1);
            let events: Events = self.query(filter).await?;
            match events.first_owned() {
                Some(event) => Ok(Some(Metadata::from_json(event.content)?)),
                None => Ok(None),
            }
        })
    }

    /// Get contact list public keys
    fn contacts_public_keys(
        &self,
        public_key: PublicKey,
    ) -> BoxedFuture<'_, Result<HashSet<PublicKey>, Error>> {
        Box::pin(async move {
            let filter = Filter::new()
                .author(public_key)
                .kind(Kind::ContactList)
                .limit(1);
            let events: Events = self.query(filter).await?;
            match events.first_owned() {
                Some(event) => Ok(event.tags.public_keys().collect()),
                None => Ok(HashSet::new()),
            }
        })
    }

    /// Get contact list with metadata of [`PublicKey`]
    fn contacts(&self, public_key: PublicKey) -> BoxedFuture<'_, Result<BTreeSet<Profile>, Error>> {
        Box::pin(async move {
            let filter = Filter::new()
                .author(public_key)
                .kind(Kind::ContactList)
                .limit(1);
            let events: Events = self.query(filter).await?;
            match events.first_owned() {
                Some(event) => {
                    // Get contacts metadata
                    let filter = Filter::new()
                        .authors(event.tags.public_keys())
                        .kind(Kind::Metadata);
                    let mut contacts: HashSet<Profile> = self
                        .query(filter)
                        .await?
                        .into_iter()
                        .map(|e| {
                            let metadata: Metadata =
                                Metadata::from_json(&e.content).unwrap_or_default();
                            Profile::new(e.pubkey, metadata)
                        })
                        .collect();

                    // Extend with missing public keys
                    contacts.extend(event.tags.public_keys().map(Profile::from));

                    Ok(contacts.into_iter().collect())
                }
                None => Ok(BTreeSet::new()),
            }
        })
    }

    /// Get relays list for [PublicKey]
    ///
    /// <https://github.com/nostr-protocol/nips/blob/master/65.md>
    fn relay_list(&self, public_key: PublicKey) -> BoxedFuture<'_, Result<RelaysMap, Error>> {
        Box::pin(async move {
            // Query
            let filter: Filter = Filter::default()
                .author(public_key)
                .kind(Kind::RelayList)
                .limit(1);
            let events: Events = self.query(filter).await?;

            // Extract relay list (NIP65)
            match events.first() {
                Some(event) => Ok(nip65::extract_relay_list(event).collect()),
                None => Ok(HashMap::new()),
            }
        })
    }

    /// Get relays list for public keys
    ///
    /// <https://github.com/nostr-protocol/nips/blob/master/65.md>
    fn relay_lists<'a, I>(
        &'a self,
        public_keys: I,
    ) -> BoxedFuture<'a, Result<HashMap<PublicKey, RelaysMap>, Error>>
    where
        I: IntoIterator<Item = PublicKey> + Send + 'a,
    {
        Box::pin(async move {
            // Query
            let filter: Filter = Filter::default().authors(public_keys).kind(Kind::RelayList);
            let events: Events = self.query(filter).await?;

            let mut map = HashMap::with_capacity(events.len());

            for event in events.iter() {
                map.insert(event.pubkey, nip65::extract_relay_list(event).collect());
            }

            Ok(map)
        })
    }
}

impl<T: NostrDatabase + ?Sized> NostrDatabaseExt for T {}