commit 1f7704a69b4421c1fa543e5f77b6c94bfdd2dec6
parent 0c2fe0fc1da81aeee83621510d9e1d4c9e6da4a1
Author: triesap <tyson@radroots.org>
Date: Wed, 7 Jan 2026 17:14:12 +0000
events: add NIP-89 application handler
- add application handler spec and builder
- publish handler events via client helper
- include metadata content when present
- re-export helpers through nostr prelude
Diffstat:
3 files changed, 99 insertions(+), 0 deletions(-)
diff --git a/nostr/src/events/application_handler.rs b/nostr/src/events/application_handler.rs
@@ -0,0 +1,88 @@
+#![forbid(unsafe_code)]
+
+extern crate alloc;
+
+use alloc::{string::String, vec::Vec};
+
+use crate::error::RadrootsNostrError;
+use crate::events::radroots_nostr_build_event;
+use crate::types::{RadrootsNostrEventBuilder, RadrootsNostrMetadata};
+use radroots_events::kinds::KIND_APPLICATION_HANDLER;
+
+#[derive(Debug, Clone)]
+pub struct RadrootsNostrApplicationHandlerSpec {
+ pub kinds: Vec<u32>,
+ pub identifier: Option<String>,
+ pub metadata: Option<RadrootsNostrMetadata>,
+ pub extra_tags: Vec<Vec<String>>,
+}
+
+impl RadrootsNostrApplicationHandlerSpec {
+ pub fn new(kinds: Vec<u32>) -> Self {
+ Self {
+ kinds,
+ identifier: None,
+ metadata: None,
+ extra_tags: Vec::new(),
+ }
+ }
+}
+
+pub fn radroots_nostr_build_application_handler_event(
+ spec: &RadrootsNostrApplicationHandlerSpec,
+) -> Result<RadrootsNostrEventBuilder, RadrootsNostrError> {
+ if spec.kinds.is_empty() {
+ return Err(RadrootsNostrError::FilterTagError(
+ "application handler kinds are empty".to_string(),
+ ));
+ }
+
+ let identifier = spec
+ .identifier
+ .clone()
+ .unwrap_or_else(|| spec.kinds[0].to_string());
+
+ let mut content = String::new();
+ if let Some(md) = spec.metadata.as_ref() {
+ if metadata_has_fields(md) {
+ content = serde_json::to_string(md).unwrap_or_default();
+ }
+ }
+
+ let mut tags = Vec::new();
+ tags.push(vec!["d".to_string(), identifier]);
+ for kind in &spec.kinds {
+ tags.push(vec!["k".to_string(), kind.to_string()]);
+ }
+ for tag in &spec.extra_tags {
+ if tag.is_empty() {
+ continue;
+ }
+ tags.push(tag.clone());
+ }
+
+ radroots_nostr_build_event(KIND_APPLICATION_HANDLER, content, tags)
+}
+
+fn metadata_has_fields(md: &RadrootsNostrMetadata) -> bool {
+ md.name.is_some()
+ || md.display_name.is_some()
+ || md.about.is_some()
+ || md.website.is_some()
+ || md.picture.is_some()
+ || md.banner.is_some()
+ || md.nip05.is_some()
+ || md.lud06.is_some()
+ || md.lud16.is_some()
+ || !md.custom.is_empty()
+}
+
+#[cfg(feature = "client")]
+pub async fn radroots_nostr_publish_application_handler(
+ client: &crate::client::RadrootsNostrClient,
+ spec: &RadrootsNostrApplicationHandlerSpec,
+) -> Result<crate::types::RadrootsNostrOutput<crate::types::RadrootsNostrEventId>, RadrootsNostrError>
+{
+ let builder = radroots_nostr_build_application_handler_event(spec)?;
+ crate::client::radroots_nostr_send_event(client, builder).await
+}
diff --git a/nostr/src/events/mod.rs b/nostr/src/events/mod.rs
@@ -1,6 +1,8 @@
pub mod jobs;
pub mod metadata;
pub mod post;
+#[cfg(feature = "events")]
+pub mod application_handler;
extern crate alloc;
use alloc::{string::String, vec::Vec};
diff --git a/nostr/src/lib.rs b/nostr/src/lib.rs
@@ -68,6 +68,15 @@ pub mod prelude {
},
};
+ #[cfg(feature = "events")]
+ pub use crate::events::application_handler::{
+ radroots_nostr_build_application_handler_event,
+ RadrootsNostrApplicationHandlerSpec,
+ };
+
+ #[cfg(all(feature = "client", feature = "events"))]
+ pub use crate::events::application_handler::radroots_nostr_publish_application_handler;
+
#[cfg(feature = "client")]
pub use crate::events::metadata::{
radroots_nostr_fetch_metadata_for_author,