radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit 69c92aa4d44638a35cc6e623756394d542f73b5e
parent 5b006af60a2b97c35436c3815968d2d8c114c027
Author: triesap <tyson@radroots.org>
Date:   Sun, 15 Feb 2026 18:13:41 +0000

app: use shared runtime and presence bootstrap primitives

- replace local cli flags with shared runtime service cli args
- flatten shared nostr service config into radrootsd configuration
- switch startup presence publishing to shared nostr bootstrap helper
- route nip89 identifier and extra tags through shared service config fields

Diffstat:
Msrc/app/cli.rs | 30++++--------------------------
Msrc/app/config.rs | 20++++----------------
Msrc/app/runtime.rs | 87+++++++++++++++++++++++--------------------------------------------------------
3 files changed, 33 insertions(+), 104 deletions(-)

diff --git a/src/app/cli.rs b/src/app/cli.rs @@ -1,6 +1,5 @@ -use std::path::PathBuf; - -use clap::{command, Parser, ValueHint}; +use clap::Parser; +use radroots_runtime::RadrootsServiceCliArgs; #[derive(Parser, Debug, Clone)] #[command( @@ -9,27 +8,6 @@ use clap::{command, Parser, ValueHint}; version = env!("CARGO_PKG_VERSION") )] pub struct Args { - #[arg( - long, - value_name = "PATH", - value_hint = ValueHint::FilePath, - default_value = "config.toml", - help = "Path to the daemon configuration file (defaults to config.toml)" - )] - pub config: PathBuf, - - #[arg( - long, - value_name = "PATH", - value_hint = ValueHint::FilePath, - help = "Path to the daemon identity file (json, txt, or raw 32-byte key; defaults to identity.json)", - )] - pub identity: Option<PathBuf>, - - #[arg( - long, - action = clap::ArgAction::SetTrue, - help = "Allow generating a new identity file if missing; if not set and identity file is absent, the daemon will fail" - )] - pub allow_generate_identity: bool, + #[command(flatten)] + pub service: RadrootsServiceCliArgs, } diff --git a/src/app/config.rs b/src/app/config.rs @@ -1,4 +1,5 @@ use radroots_nostr::prelude::RadrootsNostrMetadata; +use radroots_runtime::RadrootsNostrServiceConfig; use serde::{Deserialize, Serialize}; fn default_rpc_addr() -> String { @@ -33,10 +34,6 @@ fn default_nip46_perms() -> Vec<String> { Vec::new() } -fn default_nip46_nip89_extra_tags() -> Vec<Vec<String>> { - Vec::new() -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Nip46Config { #[serde(default = "default_nip46_session_ttl_secs")] @@ -45,10 +42,6 @@ pub struct Nip46Config { pub perms: Vec<String>, #[serde(default)] pub nostrconnect_url: Option<String>, - #[serde(default)] - pub nip89_identifier: Option<String>, - #[serde(default = "default_nip46_nip89_extra_tags")] - pub nip89_extra_tags: Vec<Vec<String>>, } impl Default for Nip46Config { @@ -57,8 +50,6 @@ impl Default for Nip46Config { session_ttl_secs: default_nip46_session_ttl_secs(), perms: default_nip46_perms(), nostrconnect_url: None, - nip89_identifier: None, - nip89_extra_tags: default_nip46_nip89_extra_tags(), } } } @@ -97,22 +88,19 @@ impl Default for RpcConfig { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Configuration { - pub logs_dir: String, + #[serde(flatten)] + pub service: RadrootsNostrServiceConfig, #[serde(default)] pub rpc: RpcConfig, #[serde(default)] pub rpc_addr: Option<String>, #[serde(default)] - pub relays: Vec<String>, - #[serde(default)] pub nip46: Nip46Config, } impl Configuration { pub fn rpc_addr(&self) -> &str { - self.rpc_addr - .as_deref() - .unwrap_or(self.rpc.addr.as_str()) + self.rpc_addr.as_deref().unwrap_or(self.rpc.addr.as_str()) } } diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use radroots_identity::RadrootsIdentity; +use std::time::Duration; use tracing::info; use crate::app::{cli, config}; @@ -7,22 +8,16 @@ use crate::core::Radrootsd; use crate::transport::jsonrpc; use crate::transport::nostr::listener::spawn_nip46_listener; use radroots_events::profile::RadrootsProfileType; -use radroots_events_codec::profile::encode::profile_type_tags; use radroots_nostr::prelude::{ - radroots_nostr_build_metadata_event, - radroots_nostr_publish_application_handler, - radroots_nostr_publish_identity_profile_with_type, - RadrootsNostrApplicationHandlerSpec, - RadrootsNostrKind, - RadrootsNostrTag, - RadrootsNostrTagKind, + RadrootsNostrApplicationHandlerSpec, RadrootsNostrKind, + radroots_nostr_bootstrap_service_presence, }; pub async fn run() -> Result<()> { let (args, settings): (cli::Args, config::Settings) = radroots_runtime::parse_and_load_path_with_init( - |a: &cli::Args| Some(a.config.as_path()), - |cfg: &config::Settings| cfg.config.logs_dir.as_str(), + |a: &cli::Args| Some(a.service.config.as_path()), + |cfg: &config::Settings| cfg.config.service.logs_dir.as_str(), None, ) .context("load configuration")?; @@ -30,8 +25,8 @@ pub async fn run() -> Result<()> { info!("Starting radrootsd"); let identity = RadrootsIdentity::load_or_generate( - args.identity.as_ref(), - args.allow_generate_identity, + args.service.identity.as_ref(), + args.service.allow_generate_identity, )?; let keys = identity.keys().clone(); let radrootsd = Radrootsd::new( @@ -40,72 +35,40 @@ pub async fn run() -> Result<()> { settings.config.nip46.clone(), ); - for relay in settings.config.relays.iter() { + for relay in settings.config.service.relays.iter() { radrootsd.client.add_relay(relay).await?; } - if !settings.config.relays.is_empty() { + if !settings.config.service.relays.is_empty() { let client = radrootsd.client.clone(); let md = settings.metadata.clone(); let identity = identity.clone(); let nip46_config = settings.config.nip46.clone(); - let relays = settings.config.relays.clone(); - let has_metadata = serde_json::to_value(&md) - .ok() - .and_then(|v| v.as_object().cloned()) - .map(|o| !o.is_empty()) - .unwrap_or(false); + let service_cfg = settings.config.service.clone(); tokio::spawn(async move { - client.connect().await; - let profile_published = - match radroots_nostr_publish_identity_profile_with_type( - &client, - &identity, - Some(RadrootsProfileType::Radrootsd), - ) - .await - { - Ok(Some(_)) => true, - Ok(None) => false, - Err(e) => { - tracing::warn!("Failed to publish identity profile: {e}"); - false - } - }; - if has_metadata && !profile_published { - let mut tags = Vec::new(); - for mut tag in profile_type_tags(RadrootsProfileType::Radrootsd) { - if tag.is_empty() { - continue; - } - let key = tag.remove(0); - tags.push(RadrootsNostrTag::custom( - RadrootsNostrTagKind::Custom(key.into()), - tag, - )); - } - let builder = radroots_nostr_build_metadata_event(&md).tags(tags); - if let Err(e) = client.send_event_builder(builder).await { - tracing::warn!("Failed to publish metadata on startup: {e}"); - } else { - tracing::info!("Published metadata on startup"); - } - } - let nip46_kind = RadrootsNostrKind::NostrConnect.as_u16() as u32; let handler_spec = RadrootsNostrApplicationHandlerSpec { kinds: vec![nip46_kind], - identifier: nip46_config.nip89_identifier.clone(), + identifier: service_cfg.nip89_identifier.clone(), metadata: Some(md.clone()), - extra_tags: nip46_config.nip89_extra_tags.clone(), - relays, + extra_tags: service_cfg.nip89_extra_tags.clone(), + relays: service_cfg.relays.clone(), nostrconnect_url: nip46_config.nostrconnect_url.clone(), }; - if let Err(e) = radroots_nostr_publish_application_handler(&client, &handler_spec).await { - tracing::warn!("Failed to publish NIP-89 announcement: {e}"); + if let Err(e) = radroots_nostr_bootstrap_service_presence( + &client, + &identity, + Some(RadrootsProfileType::Radrootsd), + &md, + &handler_spec, + Duration::from_secs(5), + ) + .await + { + tracing::warn!("Failed to publish service presence on startup: {e}"); } else { - tracing::info!("Published NIP-89 announcement"); + tracing::info!("Published service presence on startup"); } });