Files
sovereign_browser_rust/src/relay_fetch.rs
T

163 lines
5.7 KiB
Rust

//! Post-login bootstrap relay fetch for sovereign_browser
//!
//! Port of `relay_fetch.c` / `relay_fetch.h` from the C project.
//! Uses `nostr_relay` from `rust_core_lib`.
use nostr_core::types::Filter;
use nostr_relay::pool::ReconnectConfig;
use tokio::runtime::Runtime;
use crate::db;
use crate::settings;
/// Maximum number of relays to parse from the bootstrap_relays setting.
const MAX_RELAYS: usize = 32;
/// Relay query timeout in seconds.
const RELAY_TIMEOUT_MS: u64 = 15000;
/// Fetch the user's kind 0, 3, and 10002 events from the bootstrap relays.
///
/// Filters out .onion relay URLs when Tor is not running, since
/// tokio_tungstenite cannot resolve .onion addresses through clearnet DNS.
pub fn relay_fetch_bootstrap(
pubkey_hex: &str,
relay_urls: &[String],
) -> Result<i32, Box<dyn std::error::Error>> {
if pubkey_hex.is_empty() {
eprintln!("[relay] No pubkey, skipping bootstrap fetch");
return Ok(0);
}
if relay_urls.is_empty() {
eprintln!("[relay] No bootstrap relays configured");
return Ok(0);
}
// Filter out .onion relays when Tor is not available.
// Tor may be running as a system daemon (not managed by the browser),
// so check the SOCKS port availability rather than just internal status.
let tor_available = crate::net_services::net_services_get_tor_socks_endpoint().is_some();
let filtered: Vec<String> = relay_urls.iter()
.filter(|url| {
if url.contains(".onion") && !tor_available {
println!("[relay] Skipping .onion relay (Tor not available): {}", url);
false
} else {
true
}
})
.cloned()
.collect();
if filtered.is_empty() {
eprintln!("[relay] All relays are .onion and Tor is not running");
return Ok(0);
}
println!(
"[relay] Fetching kind 0/3/10002/30003/30078/31123 for {} from {} relay(s)...",
pubkey_hex,
filtered.len()
);
// Build the filter
let filter = Filter::new()
.authors(vec![pubkey_hex.to_string()])
.kinds(vec![0, 3, 10002, 30003, 30078, 31123]);
// Use tokio runtime for async relay operations
let rt = Runtime::new()?;
let result = rt.block_on(async {
use nostr_relay::pool::RelayPool;
use std::sync::Arc;
let pool = Arc::new(RelayPool::new(Some(ReconnectConfig::default())));
// Add relays
for url in filtered.iter().take(MAX_RELAYS) {
let _ = pool.add_relay(url.as_str()).await;
}
// Connect to relays (each attempt bounded by a timeout)
pool.connect_all_with_timeout(10_000).await;
// Only query relays that actually connected — a FullSet subscription
// waits for EOSE from every relay it is given, so including a relay
// that failed to connect would stall the query until its timeout.
let connected = pool.connected_relay_urls().await;
if connected.is_empty() {
eprintln!("[relay] No relays connected — skipping query");
return Ok(0);
}
println!("[relay] {} of {} relay(s) connected", connected.len(), filtered.len());
// Spawn the pool event loop — it reads incoming WebSocket messages
// and dispatches EVENT/EOSE to subscriptions. Without it, query_sync
// never receives anything and always times out. The task is cancelled
// automatically when this runtime is dropped at the end of the fetch.
let pool_for_loop = pool.clone();
let event_loop = tokio::spawn(async move {
pool_for_loop.run(100).await;
});
// Query with timeout
let events_result = pool
.query_sync(&connected, filter, RELAY_TIMEOUT_MS)
.await;
event_loop.abort();
match events_result {
Ok(event_list) => {
let count = event_list.len();
println!("[relay] Fetched {} events", count);
// Store events in database
for event in &event_list {
let tags_json = serde_json::to_string(&event.tags).unwrap_or_default();
let _ = db::db_store_event(
&event.id.as_ref().map(|id| id.to_hex()).unwrap_or_default(),
&event.pubkey.to_hex(),
event.kind.as_u64(),
event.created_at,
&event.content,
&tags_json,
event.sig.as_ref().map(|s| s.to_hex()).as_deref(),
None,
);
}
// Disconnect
pool.disconnect_all().await;
Ok(count as i32)
}
Err(e) => {
eprintln!("[relay] Query failed: {:?}", e);
pool.disconnect_all().await;
Ok(0)
}
}
});
result
}
/// Background thread entry point for relay_fetch_bootstrap.
pub fn relay_fetch_thread(pubkey_hex: String) {
let relays = settings::settings_get_bootstrap_relays();
let relay_refs: Vec<String> = relays.iter().map(|s| s.clone()).collect();
match relay_fetch_bootstrap(&pubkey_hex, &relay_refs) {
Ok(count) => println!("[relay] Bootstrap fetch complete: {} events stored", count),
Err(e) => eprintln!("[relay] Bootstrap fetch error: {}", e),
}
// Refresh the user's avatar now that the kind 0 profile may be available
// in the database. This must run on the main thread because it touches
// GTK widgets.
let pubkey_for_avatar = pubkey_hex.clone();
glib::idle_add_once(move || {
crate::tab_manager::tab_manager_set_avatar(Some(&pubkey_for_avatar));
});
}