lib

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

commit f263315cb394ad3e113b46505129304937a1d618
parent bc7ba824865cdd4589d15989bad3f9f18cf12221
Author: triesap <tyson@radroots.org>
Date:   Wed, 31 Dec 2025 10:27:51 +0000

identity: persist RadrootsProfile and publish metadata via codec


- Add `radroots-events` dependency to identity and include profile field in identity file
- Update identity serialization/deserialization and emptiness checks to account for profile
- Gate profile kind import on serde_json and extend nostr codec feature wiring and errors
- Add client helper to publish identity profile as metadata and fix created_at seconds casting

Diffstat:
MCargo.lock | 1+
Mevents-codec/src/profile/encode.rs | 1+
Midentity/Cargo.toml | 1+
Midentity/src/identity.rs | 17++++++++++++++---
Midentity/tests/identity.rs | 36+++++++++++++++++++++++++++++++++++-
Mnostr/Cargo.toml | 2+-
Mnostr/src/error.rs | 4++++
Anostr/src/identity_profile.rs | 17+++++++++++++++++
Mnostr/src/job_adapter.rs | 2+-
Mnostr/src/lib.rs | 6++++++
10 files changed, 81 insertions(+), 6 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1762,6 +1762,7 @@ name = "radroots-identity" version = "0.1.0" dependencies = [ "nostr", + "radroots-events", "radroots-runtime", "serde", "serde_json", diff --git a/events-codec/src/profile/encode.rs b/events-codec/src/profile/encode.rs @@ -5,6 +5,7 @@ use radroots_events::profile::{ RADROOTS_PROFILE_TYPE_TAG_KEY, radroots_profile_type_tag_value, }; +#[cfg(feature = "serde_json")] use radroots_events::kinds::KIND_PROFILE; use nostr::Metadata; diff --git a/identity/Cargo.toml b/identity/Cargo.toml @@ -12,6 +12,7 @@ std = ["dep:radroots-runtime"] [dependencies] radroots-runtime = { workspace = true, optional = true } +radroots-events = { workspace = true, default-features = false, features = ["serde"] } nostr = { workspace = true, features = ["os-rng"] } serde = { workspace = true } serde_json = { workspace = true } diff --git a/identity/src/identity.rs b/identity/src/identity.rs @@ -1,6 +1,7 @@ use crate::error::IdentityError; use core::convert::Infallible; use nostr::{Keys, SecretKey}; +use radroots_events::profile::RadrootsProfile; use serde::{Deserialize, Serialize}; #[cfg(not(feature = "std"))] @@ -29,6 +30,8 @@ pub struct RadrootsIdentityProfile { pub metadata: Option<nostr::Event>, #[serde(skip_serializing_if = "Option::is_none")] pub application_handler: Option<nostr::Event>, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option<RadrootsProfile>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -42,6 +45,8 @@ pub struct RadrootsIdentityFile { pub metadata: Option<nostr::Event>, #[serde(skip_serializing_if = "Option::is_none")] pub application_handler: Option<nostr::Event>, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option<RadrootsProfile>, } #[derive(Debug, Clone, Copy)] @@ -52,7 +57,10 @@ pub enum RadrootsIdentitySecretKeyFormat { impl RadrootsIdentityProfile { pub fn is_empty(&self) -> bool { - self.identifier.is_none() && self.metadata.is_none() && self.application_handler.is_none() + self.identifier.is_none() + && self.metadata.is_none() + && self.application_handler.is_none() + && self.profile.is_none() } } @@ -149,13 +157,14 @@ impl RadrootsIdentity { RadrootsIdentitySecretKeyFormat::Hex => self.secret_key_hex(), RadrootsIdentitySecretKeyFormat::Nsec => self.secret_key_nsec(), }; - let (identifier, metadata, application_handler) = match &self.profile { + let (identifier, metadata, application_handler, profile) = match &self.profile { Some(profile) => ( profile.identifier.clone(), profile.metadata.clone(), profile.application_handler.clone(), + profile.profile.clone(), ), - None => (None, None, None), + None => (None, None, None, None), }; RadrootsIdentityFile { secret_key, @@ -163,6 +172,7 @@ impl RadrootsIdentity { identifier, metadata, application_handler, + profile, } } @@ -232,6 +242,7 @@ impl TryFrom<RadrootsIdentityFile> for RadrootsIdentity { identifier: file.identifier, metadata: file.metadata, application_handler: file.application_handler, + profile: file.profile, }; if profile.is_empty() { Ok(Self::new(keys)) diff --git a/identity/tests/identity.rs b/identity/tests/identity.rs @@ -1,4 +1,5 @@ -use radroots_identity::{IdentityError, RadrootsIdentity}; +use radroots_events::profile::RadrootsProfile; +use radroots_identity::{IdentityError, RadrootsIdentity, RadrootsIdentityProfile}; #[test] fn load_from_json_file_hex() { @@ -15,6 +16,39 @@ fn load_from_json_file_hex() { } #[test] +fn load_from_json_file_profile() { + let keys = nostr::Keys::generate(); + let mut identity = RadrootsIdentity::new(keys.clone()); + let profile = RadrootsProfile { + name: "relay-agent".to_string(), + display_name: Some("Relay Agent".to_string()), + nip05: None, + about: Some("hello".to_string()), + website: None, + picture: None, + banner: None, + lud06: None, + lud16: None, + bot: None, + }; + identity.set_profile(RadrootsIdentityProfile { + profile: Some(profile), + ..Default::default() + }); + let json = serde_json::to_string(&identity.to_file()).unwrap(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.json"); + std::fs::write(&path, json).unwrap(); + + let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); + let loaded_profile = loaded.profile().and_then(|p| p.profile.as_ref()).unwrap(); + assert_eq!(loaded_profile.name, "relay-agent"); + assert_eq!(loaded_profile.display_name.as_deref(), Some("Relay Agent")); + assert_eq!(loaded_profile.about.as_deref(), Some("hello")); +} + +#[test] fn load_from_text_file_hex() { let keys = nostr::Keys::generate(); let identity = RadrootsIdentity::new(keys.clone()); diff --git a/nostr/Cargo.toml b/nostr/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true default = ["std"] std = [] client = ["std", "dep:nostr-sdk", "dep:radroots-identity"] -codec = ["dep:radroots-events", "dep:radroots-events-codec"] +codec = ["dep:radroots-events", "dep:radroots-events-codec", "radroots-events-codec/nostr"] events = ["dep:radroots-events", "radroots-events/std", "radroots-events/serde"] http = ["dep:reqwest"] nip17 = ["std", "codec", "nostr/nip44", "nostr/nip59", "nostr/os-rng"] diff --git a/nostr/src/error.rs b/nostr/src/error.rs @@ -21,6 +21,10 @@ pub enum RadrootsNostrError { #[error("Key error: {0}")] KeyError(#[from] nostr::key::Error), + + #[cfg(feature = "codec")] + #[error("Profile encode error: {0}")] + ProfileEncodeError(#[from] radroots_events_codec::profile::error::ProfileEncodeError), } #[derive(Debug, Error)] diff --git a/nostr/src/identity_profile.rs b/nostr/src/identity_profile.rs @@ -0,0 +1,17 @@ +use crate::error::RadrootsNostrError; +use crate::events::metadata::radroots_nostr_post_metadata_event; +use crate::types::{RadrootsNostrEventId, RadrootsNostrOutput}; +use crate::client::RadrootsNostrClient; +use radroots_identity::RadrootsIdentity; + +pub async fn radroots_nostr_publish_identity_profile( + client: &RadrootsNostrClient, + identity: &RadrootsIdentity, +) -> Result<Option<RadrootsNostrOutput<RadrootsNostrEventId>>, RadrootsNostrError> { + let Some(profile) = identity.profile().and_then(|p| p.profile.as_ref()) else { + return Ok(None); + }; + let metadata = radroots_events_codec::profile::encode::to_metadata(profile)?; + let out = radroots_nostr_post_metadata_event(client, &metadata).await?; + Ok(Some(out)) +} diff --git a/nostr/src/job_adapter.rs b/nostr/src/job_adapter.rs @@ -61,7 +61,7 @@ impl JobEventLike for RadrootsNostrEventAdapter<'_> { self.author_hex.clone() } fn raw_published_at(&self) -> u32 { - self.evt.created_at.as_u64() as u32 + self.evt.created_at.as_secs() as u32 } fn raw_kind(&self) -> u32 { match self.evt.kind { diff --git a/nostr/src/lib.rs b/nostr/src/lib.rs @@ -33,6 +33,9 @@ pub mod event_adapters; #[cfg(feature = "events")] pub mod event_convert; +#[cfg(all(feature = "client", feature = "codec"))] +pub mod identity_profile; + pub mod prelude { pub use crate::events::radroots_nostr_build_event; @@ -69,6 +72,9 @@ pub mod prelude { radroots_nostr_post_metadata_event, }; + #[cfg(all(feature = "client", feature = "codec"))] + pub use crate::identity_profile::radroots_nostr_publish_identity_profile; + #[cfg(all(feature = "client", feature = "events"))] pub use crate::events::post::radroots_nostr_fetch_post_events;