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