lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit e049fd399ef6f6729da5c8d4ac6200b305e9fe11
parent 8b84f42e87ab4bf2f66932d634769a874526a8b3
Author: triesap <tyson@radroots.org>
Date:   Mon,  6 Oct 2025 13:58:47 +0100

nostr: add post and profile event adapters

Diffstat:
MCargo.lock | 1+
Mcrates/net-core/Cargo.toml | 4+++-
Mcrates/net-core/src/nostr_client/posts.rs | 60+++++++++++++++---------------------------------------------
Mcrates/net-core/src/nostr_client/profile.rs | 66+++++++++++++++---------------------------------------------------
Mcrates/nostr/Cargo.toml | 3++-
Mcrates/nostr/src/error.rs | 3+++
Acrates/nostr/src/event_adapters.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/nostr/src/events/mod.rs | 1+
Acrates/nostr/src/events/notes.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/nostr/src/lib.rs | 14++++++++++----
10 files changed, 163 insertions(+), 102 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1704,6 +1704,7 @@ dependencies = [ "nostr-sdk", "radroots-events", "radroots-log", + "radroots-nostr", "secrecy", "serde", "serde_json", diff --git a/crates/net-core/Cargo.toml b/crates/net-core/Cargo.toml @@ -19,7 +19,8 @@ nostr-client = [ "dep:secrecy", "dep:hex", "dep:tempfile", - "dep:serde_json" + "dep:serde_json", + "dep:radroots-nostr" ] directories = ["std", "dep:directories"] fs-persistence = ["std"] @@ -27,6 +28,7 @@ fs-persistence = ["std"] [dependencies] radroots-events = { workspace = true, optional = true, default-features = true, features = ["std", "serde", "typeshare"] } radroots-log = { workspace = true } +radroots-nostr = { workspace = true, optional = true, default-features = true, features = ["sdk", "events"] } directories = { workspace = true, optional = true } hex = { workspace = true, optional = true } nostr = { workspace = true, optional = true } diff --git a/crates/net-core/src/nostr_client/posts.rs b/crates/net-core/src/nostr_client/posts.rs @@ -1,17 +1,15 @@ -use std::time::Duration; - use crate::error::{NetError, Result}; -use radroots_events::post::models::{RadrootsPost, RadrootsPostEventMetadata}; +use radroots_events::post::models::RadrootsPostEventMetadata; use super::manager::NostrClientManager; -use nostr_sdk::prelude::*; impl NostrClientManager { pub async fn publish_text_note(&self, content: String) -> Result<String> { + let builder = radroots_nostr::events::notes::build_text_note(content); let out = self .inner .client - .send_event_builder(EventBuilder::text_note(content)) + .send_event_builder(builder) .await .map_err(|e| NetError::Msg(e.to_string()))?; Ok(out.val.to_string()) @@ -30,25 +28,14 @@ impl NostrClientManager { content: String, root_event_id_hex: Option<String>, ) -> Result<String> { - let parent_id = - EventId::from_hex(&parent_event_id_hex).map_err(|_| NetError::InvalidHex32)?; - let parent_pubkey = - PublicKey::from_hex(&parent_author_hex).map_err(|_| NetError::InvalidHex32)?; - - let mut tags: Vec<Tag> = Vec::new(); - - if let Some(root_hex) = root_event_id_hex { - if !root_hex.is_empty() { - if let Ok(root_id) = EventId::from_hex(&root_hex) { - tags.push(Tag::event(root_id)); - } - } - } - - tags.push(Tag::event(parent_id)); - tags.push(Tag::public_key(parent_pubkey)); + let builder = radroots_nostr::events::notes::build_reply( + &parent_event_id_hex, + &parent_author_hex, + content, + root_event_id_hex.as_deref(), + ) + .map_err(|e| NetError::Msg(e.to_string()))?; - let builder = EventBuilder::text_note(content).tags(tags); let out = self .inner .client @@ -84,28 +71,11 @@ impl NostrClientManager { limit: u16, since_unix: Option<u64>, ) -> Result<Vec<RadrootsPostEventMetadata>> { - let mut filter = Filter::new().kind(Kind::TextNote).limit(limit.into()); - if let Some(s) = since_unix { - filter = filter.since(Timestamp::from(s)); - } - let events = self - .inner - .client - .fetch_events(filter, Duration::from_secs(10)) - .await - .map_err(|e| NetError::Msg(e.to_string()))?; - let out = events - .into_iter() - .map(|ev| RadrootsPostEventMetadata { - id: ev.id.to_string(), - author: ev.pubkey.to_string(), - published_at: ev.created_at.as_u64() as u32, - post: RadrootsPost { - content: ev.content, - }, - }) - .collect(); - Ok(out) + let items = + radroots_nostr::events::notes::fetch_text_notes(&self.inner.client, limit, since_unix) + .await + .map_err(|e| NetError::Msg(e.to_string()))?; + Ok(items) } pub fn fetch_text_notes_blocking( diff --git a/crates/net-core/src/nostr_client/profile.rs b/crates/net-core/src/nostr_client/profile.rs @@ -1,7 +1,5 @@ -use std::time::Duration; - use crate::error::{NetError, Result}; -use radroots_events::profile::models::{RadrootsProfile, RadrootsProfileEventMetadata}; +use radroots_events::profile::models::RadrootsProfileEventMetadata; use super::manager::NostrClientManager; @@ -10,54 +8,21 @@ impl NostrClientManager { &self, author: nostr::PublicKey, ) -> Result<Option<RadrootsProfileEventMetadata>> { - let filter = nostr_sdk::prelude::Filter::new() - .authors(vec![author]) - .kind(nostr_sdk::prelude::Kind::Metadata) - .limit(1); - - let events = self - .inner - .client - .fetch_events(filter, Duration::from_secs(5)) - .await - .map_err(|e| NetError::Msg(e.to_string()))?; - - if let Some(ev) = events.into_iter().next() { - if let Ok(p) = serde_json::from_str::<RadrootsProfile>(&ev.content) { - let out = RadrootsProfileEventMetadata { - id: ev.id.to_string(), - author: ev.pubkey.to_string(), - published_at: ev.created_at.as_u64() as u32, - profile: p, - }; - return Ok(Some(out)); - } - if let Ok(md) = serde_json::from_str::<nostr::Metadata>(&ev.content) { - let p = RadrootsProfile { - name: md.name.unwrap_or_default(), - display_name: md.display_name, - nip05: md.nip05, - about: md.about, - website: md.website.map(|u| u.to_string()), - picture: md.picture.map(|u| u.to_string()), - banner: md.banner.map(|u| u.to_string()), - lud06: md.lud06, - lud16: md.lud16, - bot: None, - }; - let out = RadrootsProfileEventMetadata { - id: ev.id.to_string(), - author: ev.pubkey.to_string(), - published_at: ev.created_at.as_u64() as u32, - profile: p, - }; - return Ok(Some(out)); + let ev = radroots_nostr::events::metadata::fetch_metadata_for_author( + &self.inner.client, + author, + core::time::Duration::from_secs(5), + ) + .await + .map_err(|e| NetError::Msg(e.to_string()))?; + if let Some(e) = ev { + if let Some(meta) = radroots_nostr::event_adapters::to_profile_event_metadata(&e) { + return Ok(Some(meta)); } return Err(NetError::Msg( "failed to parse kind:0 metadata content".to_string(), )); } - Ok(None) } @@ -93,11 +58,10 @@ impl NostrClientManager { if let Some(v) = about { md = md.about(v); } - inner_for_task - .client - .set_metadata(&md) - .await - .map_err(|e| NetError::Msg(e.to_string()))?; + let _ = + radroots_nostr::events::metadata::post_metadata_event(&inner_for_task.client, &md) + .await + .map_err(|e| NetError::Msg(e.to_string()))?; Ok::<(), NetError>(()) })?; Ok("ok".to_string()) diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml @@ -11,6 +11,7 @@ default = ["std", "sdk"] std = [] sdk = ["dep:nostr-sdk"] codec = ["dep:radroots-events", "dep:radroots-events-codec"] +events = ["dep:radroots-events"] http = ["dep:reqwest"] [dependencies] @@ -20,5 +21,5 @@ nostr = { workspace = true, features = ["nip04"] } nostr-sdk = { workspace = true, optional = true } reqwest = { workspace = true, optional = true, default-features = false, features = ["json", "rustls-tls"] } serde = { workspace = true } -serde_json = { workspace = true } +serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/nostr/src/error.rs b/crates/nostr/src/error.rs @@ -18,6 +18,9 @@ pub enum NostrUtilsError { #[error("Event builder failure: {0}")] EventBuildError(#[from] nostr::event::builder::Error), + + #[error("Key error: {0}")] + KeyError(#[from] nostr::key::Error), } #[derive(Debug, Error)] diff --git a/crates/nostr/src/event_adapters.rs b/crates/nostr/src/event_adapters.rs @@ -0,0 +1,54 @@ +#[cfg(feature = "events")] +use radroots_events::post::models::{RadrootsPost, RadrootsPostEventMetadata}; +#[cfg(feature = "events")] +use radroots_events::profile::models::{RadrootsProfile, RadrootsProfileEventMetadata}; + +#[cfg(feature = "events")] +use nostr::event::Event; + +#[cfg(feature = "events")] +pub fn to_post_event_metadata(e: &Event) -> RadrootsPostEventMetadata { + RadrootsPostEventMetadata { + id: e.id.to_string(), + author: e.pubkey.to_string(), + published_at: e.created_at.as_u64() as u32, + post: RadrootsPost { + content: e.content.clone(), + }, + } +} + +#[cfg(feature = "events")] +pub fn to_profile_event_metadata(e: &Event) -> Option<RadrootsProfileEventMetadata> { + if let Ok(p) = serde_json::from_str::<RadrootsProfile>(&e.content) { + return Some(RadrootsProfileEventMetadata { + id: e.id.to_string(), + author: e.pubkey.to_string(), + published_at: e.created_at.as_u64() as u32, + profile: p, + }); + } + + if let Ok(md) = serde_json::from_str::<nostr::Metadata>(&e.content) { + let p = RadrootsProfile { + name: md.name.unwrap_or_default(), + display_name: md.display_name, + nip05: md.nip05, + about: md.about, + website: md.website.map(|u| u.to_string()), + picture: md.picture.map(|u| u.to_string()), + banner: md.banner.map(|u| u.to_string()), + lud06: md.lud06, + lud16: md.lud16, + bot: None, + }; + return Some(RadrootsProfileEventMetadata { + id: e.id.to_string(), + author: e.pubkey.to_string(), + published_at: e.created_at.as_u64() as u32, + profile: p, + }); + } + + None +} diff --git a/crates/nostr/src/events/mod.rs b/crates/nostr/src/events/mod.rs @@ -1,5 +1,6 @@ pub mod jobs; pub mod metadata; +pub mod notes; extern crate alloc; use alloc::{string::String, vec::Vec}; diff --git a/crates/nostr/src/events/notes.rs b/crates/nostr/src/events/notes.rs @@ -0,0 +1,59 @@ +use crate::error::NostrUtilsError; + +#[cfg(all(feature = "sdk", feature = "events"))] +use core::time::Duration; +use nostr::{ + event::{EventBuilder, EventId, Tag}, + key::PublicKey, +}; +#[cfg(all(feature = "sdk", feature = "events"))] +use nostr_sdk::prelude::{Client, Filter, Kind, Timestamp}; + +pub fn build_text_note(content: impl Into<String>) -> EventBuilder { + EventBuilder::text_note(content) +} + +pub fn build_reply( + parent_event_id_hex: &str, + parent_author_hex: &str, + content: impl Into<String>, + root_event_id_hex: Option<&str>, +) -> Result<EventBuilder, NostrUtilsError> { + let parent_id = EventId::from_hex(parent_event_id_hex)?; + let parent_pubkey = PublicKey::from_hex(parent_author_hex)?; + let mut tags: Vec<Tag> = Vec::new(); + + if let Some(root_hex) = root_event_id_hex { + if !root_hex.is_empty() { + if let Ok(root_id) = EventId::from_hex(root_hex) { + tags.push(Tag::event(root_id)); + } + } + } + + tags.push(Tag::event(parent_id)); + tags.push(Tag::public_key(parent_pubkey)); + + Ok(EventBuilder::text_note(content).tags(tags)) +} + +#[cfg(all(feature = "sdk", feature = "events"))] +pub async fn fetch_text_notes( + client: &Client, + limit: u16, + since_unix: Option<u64>, +) -> Result<Vec<radroots_events::post::models::RadrootsPostEventMetadata>, NostrUtilsError> { + let mut filter = Filter::new().kind(Kind::TextNote).limit(limit.into()); + + if let Some(s) = since_unix { + filter = filter.since(Timestamp::from(s)); + } + + let events = client.fetch_events(filter, Duration::from_secs(10)).await?; + let out = events + .into_iter() + .map(|ev| crate::event_adapters::to_post_event_metadata(&ev)) + .collect(); + + Ok(out) +} diff --git a/crates/nostr/src/lib.rs b/crates/nostr/src/lib.rs @@ -19,6 +19,9 @@ pub mod codec_adapters; #[cfg(feature = "http")] pub mod nip11; +#[cfg(feature = "events")] +pub mod event_adapters; + pub mod prelude { pub use crate::events::build_nostr_event; @@ -26,22 +29,25 @@ pub mod prelude { pub use crate::client::{nostr_fetch_event_by_id, nostr_send_event}; pub use crate::error::{NostrTagsResolveError, NostrUtilsError}; - pub use crate::filter::{nostr_filter_kind, nostr_filter_new_events, nostr_kind}; pub use crate::events::{ jobs::{nostr_build_event_job_feedback, nostr_build_event_job_result}, metadata::{build_metadata_event, fetch_metadata_for_author, post_metadata_event}, + notes::{build_reply as build_text_reply, build_text_note}, }; - pub use crate::relays::{add_relay, connect, remove_relay}; + #[cfg(all(feature = "sdk", feature = "events"))] + pub use crate::events::notes::fetch_text_notes; pub use crate::parse::{parse_pubkey, parse_pubkeys}; - + pub use crate::relays::{add_relay, connect, remove_relay}; pub use crate::tags::*; - pub use crate::util::npub_string; #[cfg(feature = "http")] pub use crate::nip11::fetch_nip11; + + #[cfg(feature = "events")] + pub use crate::event_adapters::{to_post_event_metadata, to_profile_event_metadata}; }