163 lines
5.7 KiB
Rust
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));
|
|
});
|
|
}
|