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:
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)
+ }
}