nostr_rust

My fork of nostr_rust
git clone git://jb55.com/nostr_rust
Log | Files | Refs | README

commit 52810aabaaaeac736ea935173a11fa97e13b56a4
parent 20a570efcaaf3a868e450181846773fbd9af1c8c
Author: Thomas <31560900+0xtlt@users.noreply.github.com>
Date:   Sun,  6 Nov 2022 17:50:12 +0100

Merge pull request #3 from 0xtlt/nips/02

✨ NIP 02 Support
Diffstat:
MCHANGELOG.md | 10++++++++++
MCargo.toml | 2+-
Msrc/nips/mod.rs | 1+
Asrc/nips/nip2.rs | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/nostr_client.rs | 85++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
5 files changed, 219 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.3.0 - NIP-02 Support + +- Added: `Client.get_contact_list` method +- Added: `Client.set_contact_list` method +- Added: `Client.add_event` method +- Added: `Client.get_events` method +- Added: `Client.get_events_of` method +- Added: `ContactListTag` structure +- Added: `ContactListTag.to_tags` method + ## 0.2.0 - Architecture change - Removed: `Client.listen` function (Replaced by `Client.next_data`) diff --git a/Cargo.toml b/Cargo.toml @@ -8,7 +8,7 @@ keywords = ["nostr", "rust", "protocol", "encryption", "decryption"] categories = ["api-bindings"] license = "MIT" authors = ["Thomas Tastet"] -version = "0.2.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/nips/mod.rs b/src/nips/mod.rs @@ -1 +1,2 @@ pub mod nip1; +pub mod nip2; diff --git a/src/nips/nip2.rs b/src/nips/nip2.rs @@ -0,0 +1,123 @@ +use crate::{ + events::EventPrepare, nostr_client::Client, req::ReqFilter, utils::get_timestamp, Identity, +}; + +// Implementation of the NIP2 protocol +// https://github.com/nostr-protocol/nips/blob/master/02.md + +#[derive(Debug, Clone)] +pub struct ContactListTag { + /// 32-bytes hex key - the public key of the contact + pub key: String, + /// main relay URL + pub main_relay: Option<String>, + /// Petname - surname + pub surname: Option<String>, +} + +impl ContactListTag { + pub fn to_tags(&self) -> Vec<String> { + let mut tags: Vec<String> = vec![String::from("p"), self.key.clone()]; + + if let Some(main_relay) = &self.main_relay { + tags.push(main_relay.clone()); + + if let Some(surname) = &self.surname { + tags.push(surname.clone()); + } + } else if self.surname.is_some() { + tags.push(String::from("")); + + tags.push(self.surname.clone().unwrap()); + } + + tags + } +} + +impl Client { + /// Set the contact list of the identity + /// + /// # Example + /// ```rust + /// use nostr_rust::{nostr_client::Client, Identity, nips::nip2::ContactListTag}; + /// use std::str::FromStr; + /// let mut client = Client::new(vec!["wss://nostr-pub.wellorder.net"]).unwrap(); + /// let identity = Identity::from_str(env!("SECRET_KEY")).unwrap(); + /// + /// // Here we set the contact list of the identity + /// client.set_contact_list(&identity, vec![ContactListTag { + /// key: "884704bd421721e292edbff42eb77547fe115c6ff9825b08fc366be4cd69e9f6".to_string(), + /// main_relay: Some("wss://nostr-pub.wellorder.net".to_string()), + /// surname: Some("Rust Nostr Client".to_string()), + /// }]).unwrap(); + /// ``` + pub fn set_contact_list( + &mut self, + identity: &Identity, + contact_list: Vec<ContactListTag>, + ) -> Result<(), String> { + let event = EventPrepare { + pub_key: identity.public_key_str.clone(), + created_at: get_timestamp(), + kind: 3, + tags: contact_list + .iter() + .map(|contact| contact.to_tags()) + .collect(), + content: String::new(), + } + .to_event(identity); + + self.publish_event(&event)?; + Ok(()) + } + + /// Get the contact list of a pub key + /// + /// # Example + /// ```rust + /// use nostr_rust::{nostr_client::Client, Identity, nips::nip2::ContactListTag}; + /// use std::str::FromStr; + /// let mut client = Client::new(vec!["wss://nostr-pub.wellorder.net"]).unwrap(); + /// let contact_list = client.get_contact_list("884704bd421721e292edbff42eb77547fe115c6ff9825b08fc366be4cd69e9f6").unwrap(); + /// ``` + pub fn get_contact_list(&mut self, pubkey: &str) -> Result<Vec<ContactListTag>, String> { + let mut contact_list: Vec<ContactListTag> = vec![]; + + let events = self.get_events_of(vec![ReqFilter { + ids: None, + authors: Some(vec![pubkey.to_string()]), + kinds: Some(vec![3]), + e: None, + p: None, + since: None, + until: None, + limit: Some(1), + }])?; + + for event in events { + for tag in event.tags { + if tag[0] == "p" { + let mut contact = ContactListTag { + key: tag[1].clone(), + main_relay: None, + surname: None, + }; + + if tag.len() > 2 { + contact.main_relay = Some(tag[2].clone()); + + if tag.len() > 3 { + contact.surname = Some(tag[3].clone()); + } + } + + contact_list.push(contact); + } + } + } + + Ok(contact_list) + } +} diff --git a/src/nostr_client.rs b/src/nostr_client.rs @@ -4,12 +4,13 @@ use std::sync::{Arc, Mutex}; use crate::events::Event; use crate::req::{Req, ReqFilter}; use crate::websocket::SimplifiedWS; -use serde_json::json; +use serde_json::{json, Value}; use tungstenite::Message; /// Nostr Client pub struct Client { pub relays: HashMap<String, Arc<Mutex<SimplifiedWS>>>, + pub subscriptions: HashMap<String, Vec<Message>>, } impl Client { @@ -23,6 +24,7 @@ impl Client { pub fn new(default_relays: Vec<&str>) -> Result<Self, String> { let mut client = Self { relays: HashMap::new(), + subscriptions: HashMap::new(), }; for relay in default_relays { @@ -260,4 +262,85 @@ impl Client { Ok(()) } + + /// Add event to a subscription + pub fn add_event(&mut self, subscription_id: &str, message: Message) { + // Check if the subscription exists + if !self.subscriptions.contains_key(subscription_id) { + self.subscriptions + .insert(subscription_id.to_string(), Vec::new()); + } + + // Check if the message is already in the subscription + if !self.subscriptions[subscription_id].contains(&message) { + // Add the message to the subscription + self.subscriptions + .get_mut(subscription_id) + .unwrap() + .push(message); + } + } + + /// Get events and remove them from the subscription + pub fn get_events(&mut self, subscription_id: &str) -> Option<Vec<Message>> { + self.subscriptions.remove(subscription_id) + } + + /// Get events of a given filters + /// + /// # Example + /// ```rust + /// use nostr_rust::{nostr_client::Client, req::ReqFilter}; + /// let mut client = Client::new(vec!["wss://nostr-pub.wellorder.net"]).unwrap(); + /// let events = client.get_events_of(vec![ReqFilter { + /// ids: None, + /// authors: Some(vec!["884704bd421721e292edbff42eb77547fe115c6ff9825b08fc366be4cd69e9f6".to_string()]), + /// kinds: Some(vec![3]), + /// e: None, + /// p: None, + /// since: None, + /// until: None, + /// limit: Some(1), + /// }]).unwrap(); + /// ``` + pub fn get_events_of(&mut self, filters: Vec<ReqFilter>) -> Result<Vec<Event>, String> { + let mut events: Vec<Event> = Vec::new(); + + // Subscribe + let id = self.subscribe(filters)?; + + // Get the events + loop { + let data = self.next_data()?; + let mut break_loop = false; + + for (_, message) in data { + let event: Value = serde_json::from_str(&message.to_string()).unwrap(); + + if event[0] == "EOSE" && event[1].as_str() == Some(&id) { + break_loop = true; + break; + } + + self.add_event(&id, message); + } + + if break_loop { + break; + } + } + + // unsubscribe + self.unsubscribe(&id).unwrap(); + + // Get the events + if let Some(messages) = self.get_events(&id) { + for message in messages { + let event: Value = serde_json::from_str(&message.to_string()).unwrap(); + let event_object: Event = serde_json::from_value(event[2].clone()).unwrap(); + events.push(event_object); + } + } + Ok(events) + } }