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:
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};
}