commit e831539d23abb198139631cdb1a95b98a276f510
parent 98c28b5735327eaf50df382f12920050268d146d
Author: kernelkind <kernelkind@gmail.com>
Date: Thu, 26 Feb 2026 14:03:20 -0500
feat(outbox-int): migrate nostrverse to use outbox
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
3 files changed, 80 insertions(+), 76 deletions(-)
diff --git a/crates/notedeck_nostrverse/src/lib.rs b/crates/notedeck_nostrverse/src/lib.rs
@@ -21,10 +21,12 @@ pub use room_state::{
};
pub use room_view::{NostrverseResponse, render_editing_panel, show_room_view};
-use enostr::Pubkey;
+use enostr::{NormRelayUrl, Pubkey, RelayId};
use glam::Vec3;
use nostrdb::Filter;
-use notedeck::{AppContext, AppResponse};
+use notedeck::{
+ AppContext, AppResponse, RelaySelection, ScopedSubIdentity, SubConfig, SubKey, SubOwnerKey,
+};
use renderbud::Transform;
use egui_wgpu::wgpu;
@@ -39,6 +41,47 @@ fn demo_pubkey() -> Pubkey {
.unwrap_or_else(|_| Pubkey::from_hex(FALLBACK_PUBKEY_HEX).unwrap())
}
+/// Scoped-sub identity for nostrverse's dedicated relay room/presence feed.
+fn nostrverse_remote_sub_identity() -> ScopedSubIdentity {
+ ScopedSubIdentity::account(
+ SubOwnerKey::new("nostrverse-owner"),
+ SubKey::new("nostrverse-room-presence"),
+ )
+}
+
+/// Publish a locally ingested note to the dedicated nostrverse relay.
+fn publish_ingested_note(
+ publisher: &mut notedeck::ExplicitPublishApi<'_, '_>,
+ relay_url: &NormRelayUrl,
+ note: &nostrdb::Note<'_>,
+) {
+ publisher.publish_note(note, vec![RelayId::Websocket(relay_url.clone())]);
+}
+
+fn configured_relay_url() -> NormRelayUrl {
+ let raw = std::env::var("NOSTRVERSE_RELAY")
+ .unwrap_or_else(|_| NostrverseApp::DEFAULT_RELAY.to_string());
+ match NormRelayUrl::new(&raw) {
+ Ok(url) => url,
+ Err(err) => {
+ tracing::warn!(
+ "Invalid NOSTRVERSE_RELAY '{}': {err:?}; falling back to {}",
+ raw,
+ NostrverseApp::DEFAULT_RELAY
+ );
+ NormRelayUrl::new(NostrverseApp::DEFAULT_RELAY).expect("default nostrverse relay URL")
+ }
+ }
+}
+
+fn room_filter() -> Filter {
+ Filter::new().kinds([kinds::ROOM as u64]).build()
+}
+
+fn presence_filter() -> Filter {
+ Filter::new().kinds([kinds::PRESENCE as u64]).build()
+}
+
/// Avatar scale: water bottle model is ~0.26m, scaled to human height (~1.8m)
const AVATAR_SCALE: f32 = 7.0;
/// How fast the avatar yaw lerps toward the target (higher = faster)
@@ -104,9 +147,7 @@ pub struct NostrverseApp {
/// Model download/cache manager (initialized lazily in initialize())
model_cache: Option<model_cache::ModelCache>,
/// Dedicated relay URL for multiplayer sync (from NOSTRVERSE_RELAY env)
- relay_url: Option<String>,
- /// Pending relay subscription ID — Some means we still need to send REQ
- pending_relay_sub: Option<String>,
+ relay_url: NormRelayUrl,
}
impl NostrverseApp {
@@ -119,9 +160,7 @@ impl NostrverseApp {
let device = render_state.map(|rs| rs.device.clone());
let queue = render_state.map(|rs| rs.queue.clone());
- let relay_url = Some(
- std::env::var("NOSTRVERSE_RELAY").unwrap_or_else(|_| Self::DEFAULT_RELAY.to_string()),
- );
+ let relay_url = configured_relay_url();
let space_naddr = space_ref.to_naddr();
Self {
@@ -140,7 +179,6 @@ impl NostrverseApp {
start_time: std::time::Instant::now(),
model_cache: None,
relay_url,
- pending_relay_sub: None,
}
}
@@ -150,38 +188,6 @@ impl NostrverseApp {
Self::new(space_ref, render_state)
}
- /// Send a client message to the dedicated relay, if configured.
- fn send_to_relay(&self, pool: &mut enostr::RelayPool, msg: &enostr::ClientMessage) {
- if let Some(relay_url) = &self.relay_url {
- pool.send_to(msg, relay_url);
- }
- }
-
- /// Send the relay subscription once the relay is connected.
- fn maybe_send_relay_sub(&mut self, pool: &mut enostr::RelayPool) {
- let (Some(sub_id), Some(relay_url)) = (&self.pending_relay_sub, &self.relay_url) else {
- return;
- };
-
- let connected = pool
- .relays
- .iter()
- .any(|r| r.url() == relay_url && matches!(r.status(), enostr::RelayStatus::Connected));
-
- if !connected {
- return;
- }
-
- let room_filter = Filter::new().kinds([kinds::ROOM as u64]).build();
- let presence_filter = Filter::new().kinds([kinds::PRESENCE as u64]).build();
-
- let req = enostr::ClientMessage::req(sub_id.clone(), vec![room_filter, presence_filter]);
- pool.send_to(&req, relay_url);
-
- tracing::info!("Sent nostrverse subscription to {}", relay_url);
- self.pending_relay_sub = None;
- }
-
/// Load a glTF model and return its handle
fn load_model(&self, path: &str) -> Option<renderbud::Model> {
let renderer = self.renderer.as_ref()?;
@@ -198,7 +204,7 @@ impl NostrverseApp {
}
/// Initialize: ingest demo space into local nostrdb and subscribe.
- fn initialize(&mut self, ctx: &mut AppContext<'_>, egui_ctx: &egui::Context) {
+ fn initialize(&mut self, ctx: &mut AppContext<'_>) {
if self.initialized {
return;
}
@@ -211,19 +217,21 @@ impl NostrverseApp {
self.room_sub = Some(subscriptions::RoomSubscription::new(ctx.ndb));
self.presence_sub = Some(subscriptions::PresenceSubscription::new(ctx.ndb));
- // Add dedicated relay to pool (subscription sent on connect in maybe_send_relay_sub)
- if let Some(relay_url) = &self.relay_url {
- let egui_ctx = egui_ctx.clone();
- if let Err(e) = ctx
- .legacy_pool
- .add_url(relay_url.clone(), move || egui_ctx.request_repaint())
- {
- tracing::error!("Failed to add nostrverse relay {}: {}", relay_url, e);
- } else {
- tracing::info!("Added nostrverse relay: {}", relay_url);
- self.pending_relay_sub = Some(format!("nostrverse-{}", uuid::Uuid::new_v4()));
- }
- }
+ // Declare remote room/presence feed on the dedicated relay.
+ let relays = std::iter::once(self.relay_url.clone()).collect();
+ let config = SubConfig {
+ relays: RelaySelection::Explicit(relays),
+ filters: vec![room_filter(), presence_filter()],
+ use_transparent: false,
+ };
+ let _ = ctx
+ .remote
+ .scoped_subs(ctx.accounts)
+ .set_sub(nostrverse_remote_sub_identity(), config);
+ tracing::info!(
+ "Declared nostrverse scoped relay subscription on {}",
+ self.relay_url
+ );
// Try to load an existing space from nostrdb first
let txn = nostrdb::Transaction::new(ctx.ndb).expect("txn");
@@ -241,8 +249,9 @@ impl NostrverseApp {
if let Some(kp) = ctx.accounts.selected_filled() {
let builder = nostr_events::build_space_event(&space, &self.state.space_ref.id);
- if let Some((msg, _id)) = nostr_events::ingest_event(builder, ctx.ndb, kp) {
- self.send_to_relay(ctx.legacy_pool, &msg);
+ if let Some(note) = nostr_events::ingest_event(builder, ctx.ndb, kp) {
+ let mut publisher = ctx.remote.publisher_explicit();
+ publish_ingested_note(&mut publisher, &self.relay_url, ¬e);
}
}
// room_sub (set up above) will pick up the ingested event
@@ -373,9 +382,9 @@ impl NostrverseApp {
let space = convert::build_space(info, &self.state.objects);
let builder = nostr_events::build_space_event(&space, &self.state.space_ref.id);
- if let Some((msg, id)) = nostr_events::ingest_event(builder, ctx.ndb, kp) {
- self.last_save_id = Some(id);
- self.send_to_relay(ctx.legacy_pool, &msg);
+ if let Some(note) = nostr_events::ingest_event(builder, ctx.ndb, kp) {
+ self.last_save_id = Some(*note.id());
+ publish_ingested_note(&mut ctx.remote.publisher_explicit(), &self.relay_url, ¬e);
}
tracing::info!("Saved space '{}'", self.state.space_ref.id);
}
@@ -520,11 +529,11 @@ impl NostrverseApp {
.map(|u| u.position)
.unwrap_or(Vec3::ZERO);
- if let Some(msg) =
+ if let Some(note) =
self.presence_pub
.maybe_publish(ctx.ndb, kp, &self.space_naddr, self_pos, now)
{
- self.send_to_relay(ctx.legacy_pool, &msg);
+ publish_ingested_note(&mut ctx.remote.publisher_explicit(), &self.relay_url, ¬e);
}
}
@@ -638,11 +647,7 @@ impl NostrverseApp {
impl notedeck::App for NostrverseApp {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
// Initialize on first frame
- let egui_ctx = ui.ctx().clone();
- self.initialize(ctx, &egui_ctx);
-
- // Send relay subscription once connected
- self.maybe_send_relay_sub(ctx.legacy_pool);
+ self.initialize(ctx);
// Poll for space event updates
self.poll_space_updates(ctx.ndb);
diff --git a/crates/notedeck_nostrverse/src/nostr_events.rs b/crates/notedeck_nostrverse/src/nostr_events.rs
@@ -131,20 +131,18 @@ pub fn get_presence_space<'a>(note: &'a Note<'a>) -> Option<&'a str> {
}
/// Sign and ingest a nostr event into the local nostrdb.
-/// Returns the ClientMessage (for optional relay publishing) and
-/// the 32-byte event ID on success.
+///
+/// Returns the built note on success so callers can publish it directly.
pub fn ingest_event(
builder: NoteBuilder<'_>,
ndb: &Ndb,
kp: FilledKeypair,
-) -> Option<(enostr::ClientMessage, [u8; 32])> {
+) -> Option<Note<'static>> {
let note = builder
.sign(&kp.secret_key.secret_bytes())
.build()
.expect("build note");
- let id = *note.id();
-
let Ok(event) = enostr::ClientMessage::event(¬e) else {
tracing::error!("ingest_event: failed to build client message");
return None;
@@ -157,7 +155,7 @@ pub fn ingest_event(
let _ = ndb.process_event_with(&json, nostrdb::IngestMetadata::new().client(true));
- Some((event, id))
+ Some(note)
}
#[cfg(test)]
diff --git a/crates/notedeck_nostrverse/src/presence.rs b/crates/notedeck_nostrverse/src/presence.rs
@@ -125,7 +125,8 @@ impl PresencePublisher {
}
/// Maybe publish a presence heartbeat.
- /// Returns the ClientMessage if published (for optional relay forwarding).
+ ///
+ /// Returns the ingested note if published so the caller can forward it.
pub fn maybe_publish(
&mut self,
ndb: &Ndb,
@@ -133,7 +134,7 @@ impl PresencePublisher {
room_naddr: &str,
position: Vec3,
now: f64,
- ) -> Option<enostr::ClientMessage> {
+ ) -> Option<nostrdb::Note<'static>> {
let velocity = self.compute_velocity(position, now);
// Always update position sample for velocity computation
@@ -148,7 +149,7 @@ impl PresencePublisher {
let result = nostr_events::ingest_event(builder, ndb, kp);
self.record_publish(position, velocity, now);
- result.map(|(msg, _id)| msg)
+ result
}
}