commit 4968ac9ef76b64ec646e3ff968efdd3d1d8f378f
parent 1fc64c9ff65b3b92b3452f3c85993b7aea81eac0
Author: triesap <tyson@radroots.org>
Date: Sun, 15 Feb 2026 18:11:43 +0000
nostr: add shared presence bootstrap and metadata helper exports
- export metadata field detection helper for shared service startup checks
- add shared presence bootstrap flow for connect profile metadata and handler publishing
- include profile-type tags when publishing fallback metadata events
- export new presence helper from prelude behind client codec and events features
Diffstat:
3 files changed, 80 insertions(+), 2 deletions(-)
diff --git a/nostr/src/events/application_handler.rs b/nostr/src/events/application_handler.rs
@@ -56,7 +56,7 @@ pub fn radroots_nostr_build_application_handler_event(
let mut content = String::new();
if let Some(md) = spec.metadata.as_ref() {
- if metadata_has_fields(md) {
+ if radroots_nostr_metadata_has_fields(md) {
content = serde_json::to_string(md).unwrap_or_default();
}
}
@@ -89,7 +89,7 @@ pub fn radroots_nostr_build_application_handler_event(
radroots_nostr_build_event(KIND_APPLICATION_HANDLER, content, tags)
}
-fn metadata_has_fields(md: &RadrootsNostrMetadata) -> bool {
+pub fn radroots_nostr_metadata_has_fields(md: &RadrootsNostrMetadata) -> bool {
md.name.is_some()
|| md.display_name.is_some()
|| md.about.is_some()
@@ -145,3 +145,23 @@ fn tag_value(event: &RadrootsNostrEvent, key: &str) -> Option<String> {
.iter()
.find_map(|tag| radroots_nostr_tag_first_value(tag, key))
}
+
+#[cfg(test)]
+mod tests {
+ use super::radroots_nostr_metadata_has_fields;
+ use crate::types::RadrootsNostrMetadata;
+
+ #[test]
+ fn metadata_has_fields_false_when_empty() {
+ assert!(!radroots_nostr_metadata_has_fields(
+ &RadrootsNostrMetadata::default()
+ ));
+ }
+
+ #[test]
+ fn metadata_has_fields_true_when_about_is_set() {
+ let mut metadata = RadrootsNostrMetadata::default();
+ metadata.about = Some("ready".to_string());
+ assert!(radroots_nostr_metadata_has_fields(&metadata));
+ }
+}
diff --git a/nostr/src/identity_profile.rs b/nostr/src/identity_profile.rs
@@ -1,9 +1,18 @@
use crate::client::RadrootsNostrClient;
use crate::error::RadrootsNostrError;
+#[cfg(feature = "events")]
+use crate::events::application_handler::{
+ RadrootsNostrApplicationHandlerSpec, radroots_nostr_metadata_has_fields,
+ radroots_nostr_publish_application_handler,
+};
use crate::events::metadata::radroots_nostr_build_metadata_event;
+#[cfg(feature = "events")]
+use crate::types::RadrootsNostrMetadata;
use crate::types::{
RadrootsNostrEventId, RadrootsNostrOutput, RadrootsNostrTag, RadrootsNostrTagKind,
};
+#[cfg(feature = "events")]
+use core::time::Duration;
use radroots_events::profile::RadrootsProfileType;
use radroots_events_codec::profile::encode::profile_build_tags;
use radroots_identity::RadrootsIdentity;
@@ -40,3 +49,48 @@ pub async fn radroots_nostr_publish_identity_profile_with_type(
let out = client.send_event_builder(builder).await?;
Ok(Some(out))
}
+
+#[cfg(feature = "events")]
+pub async fn radroots_nostr_bootstrap_service_presence(
+ client: &RadrootsNostrClient,
+ identity: &RadrootsIdentity,
+ profile_type: Option<RadrootsProfileType>,
+ metadata: &RadrootsNostrMetadata,
+ handler_spec: &RadrootsNostrApplicationHandlerSpec,
+ connect_timeout: Duration,
+) -> Result<(), RadrootsNostrError> {
+ client.connect().await;
+ client.wait_for_connection(connect_timeout).await;
+
+ let profile_published =
+ match radroots_nostr_publish_identity_profile_with_type(client, identity, profile_type)
+ .await?
+ {
+ Some(_) => true,
+ None => false,
+ };
+
+ if radroots_nostr_metadata_has_fields(metadata) && !profile_published {
+ let builder =
+ radroots_nostr_build_metadata_event(metadata).tags(profile_type_tags(profile_type));
+ client.send_event_builder(builder).await?;
+ }
+
+ radroots_nostr_publish_application_handler(client, handler_spec).await?;
+ Ok(())
+}
+
+fn profile_type_tags(profile_type: Option<RadrootsProfileType>) -> Vec<RadrootsNostrTag> {
+ let mut tag_list: Vec<RadrootsNostrTag> = Vec::new();
+ for mut tag in profile_build_tags(profile_type) {
+ if tag.is_empty() {
+ continue;
+ }
+ let key = tag.remove(0);
+ tag_list.push(RadrootsNostrTag::custom(
+ RadrootsNostrTagKind::Custom(key.into()),
+ tag,
+ ));
+ }
+ tag_list
+}
diff --git a/nostr/src/lib.rs b/nostr/src/lib.rs
@@ -63,6 +63,7 @@ pub mod prelude {
#[cfg(feature = "events")]
pub use crate::events::application_handler::{
RadrootsNostrApplicationHandlerSpec, radroots_nostr_build_application_handler_event,
+ radroots_nostr_metadata_has_fields,
};
#[cfg(all(feature = "client", feature = "events"))]
@@ -78,6 +79,9 @@ pub mod prelude {
radroots_nostr_publish_identity_profile, radroots_nostr_publish_identity_profile_with_type,
};
+ #[cfg(all(feature = "client", feature = "codec", feature = "events"))]
+ pub use crate::identity_profile::radroots_nostr_bootstrap_service_presence;
+
#[cfg(all(feature = "client", feature = "events"))]
pub use crate::events::post::radroots_nostr_fetch_post_events;