use anyhow::Result;
use saorsa_gossip_presence::PresenceManager;
use saorsa_gossip_types::{FoafQuery, FoafResponse, PeerId};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
#[async_trait::async_trait]
pub trait FoafTransport: Send + Sync {
async fn send_query(&self, peer: PeerId, query: FoafQuery) -> Result<()>;
async fn wait_for_responses(&self, query_id: [u8; 16], timeout_ms: u64) -> Vec<FoafResponse>;
}
pub struct FoafDiscovery {
local_contacts: Arc<RwLock<HashMap<String, PeerId>>>,
max_hops: usize,
presence: Option<Arc<RwLock<PresenceManager>>>,
foaf_transport: Option<Arc<dyn FoafTransport>>,
our_peer_id: PeerId,
query_timeout_ms: u64,
}
impl Default for FoafDiscovery {
fn default() -> Self {
Self::new()
}
}
impl FoafDiscovery {
pub fn new() -> Self {
Self {
local_contacts: Arc::new(RwLock::new(HashMap::new())),
max_hops: 2,
presence: None,
foaf_transport: None,
our_peer_id: PeerId::new([0u8; 32]), query_timeout_ms: 5000,
}
}
pub fn with_presence(presence: Arc<RwLock<PresenceManager>>) -> Self {
Self {
local_contacts: Arc::new(RwLock::new(HashMap::new())),
max_hops: 2,
presence: Some(presence),
foaf_transport: None,
our_peer_id: PeerId::new([0u8; 32]),
query_timeout_ms: 5000,
}
}
pub fn with_config(
presence: Option<Arc<RwLock<PresenceManager>>>,
foaf_transport: Option<Arc<dyn FoafTransport>>,
our_peer_id: PeerId,
max_hops: usize,
) -> Self {
Self {
local_contacts: Arc::new(RwLock::new(HashMap::new())),
max_hops,
presence,
foaf_transport,
our_peer_id,
query_timeout_ms: 5000,
}
}
pub async fn find_contact(&self, four_words: &str) -> Result<PeerId> {
{
let contacts = self.local_contacts.read().await;
if let Some(peer_id) = contacts.get(four_words) {
debug!("Found {} in local cache", four_words);
return Ok(*peer_id);
}
}
if let Some(presence) = &self.presence {
let presence_guard = presence.read().await;
let groups = presence_guard.get_groups().await;
for topic_id in groups {
let presence_records = presence_guard.get_group_presence(topic_id).await;
for (peer_id, record) in presence_records {
if record.is_expired() {
continue;
}
if let Some(fw) = &record.four_words
&& fw == four_words
{
debug!("Found {} via presence in topic {:?}", four_words, topic_id);
let mut contacts = self.local_contacts.write().await;
contacts.insert(four_words.to_string(), peer_id);
return Ok(peer_id);
}
}
}
}
if let Some(transport) = &self.foaf_transport {
debug!("Starting FOAF query for {}", four_words);
let query_id = self.generate_query_id();
let contacts = self.get_contacts().await;
if contacts.is_empty() {
debug!("No contacts to query via FOAF");
return Err(anyhow::anyhow!(
"Contact {} not found. No contacts available for FOAF query.",
four_words
));
}
let query = FoafQuery {
query_id,
target_four_words: four_words.to_string(),
hop: 0,
max_hops: self.max_hops as u8,
visited: vec![self.our_peer_id],
originator: self.our_peer_id,
};
for (_, peer_id) in contacts.iter() {
if let Err(e) = transport.send_query(*peer_id, query.clone()).await {
warn!("Failed to send FOAF query to {:?}: {}", peer_id, e);
}
}
let responses = transport
.wait_for_responses(query_id, self.query_timeout_ms)
.await;
if let Some(response) = responses.first() {
info!(
"Found {} via FOAF query ({} hops)",
four_words, response.hops
);
let mut cache = self.local_contacts.write().await;
cache.insert(four_words.to_string(), response.peer_id);
return Ok(response.peer_id);
}
debug!("FOAF query returned no results for {}", four_words);
}
Err(anyhow::anyhow!(
"Contact {} not found via cache, presence, or FOAF queries.",
four_words
))
}
fn generate_query_id(&self) -> [u8; 16] {
use std::time::SystemTime;
let mut id = [0u8; 16];
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
id[..8].copy_from_slice(&now.to_le_bytes()[..8]);
id[8..].copy_from_slice(&self.our_peer_id.as_bytes()[..8]);
id
}
pub async fn add_contact(&self, four_words: String, peer_id: PeerId) {
let mut contacts = self.local_contacts.write().await;
contacts.insert(four_words, peer_id);
}
pub async fn remove_contact(&self, four_words: &str) {
let mut contacts = self.local_contacts.write().await;
contacts.remove(four_words);
}
pub async fn get_contacts(&self) -> Vec<(String, PeerId)> {
let contacts = self.local_contacts.read().await;
contacts.iter().map(|(k, v)| (k.clone(), *v)).collect()
}
}
#[derive(Debug, Clone)]
pub struct IntroducerConfig {
pub addresses: Vec<String>,
pub timeout_secs: u64,
}
impl Default for IntroducerConfig {
fn default() -> Self {
Self {
addresses: vec![
"142.93.199.50:11000".to_string(), "147.182.234.192:11000".to_string(), ],
timeout_secs: 10,
}
}
}
use saorsa_gossip_transport::GossipTransport;
fn parse_introducer_address(address: &str) -> Result<std::net::SocketAddr> {
if let Ok(addr) = address.parse::<std::net::SocketAddr>() {
return Ok(addr);
}
match crate::identity::conn_from_words(address) {
Ok(addr) => Ok(addr),
Err(e) => {
if let Some((words_part, port_str)) = address.rsplit_once(':')
&& let Ok(port) = port_str.parse::<u16>()
{
match crate::identity::conn_from_words(words_part) {
Ok(mut addr) => {
addr.set_port(port);
return Ok(addr);
}
Err(_) => {
let words_with_spaces = words_part.replace('-', " ");
if let Ok(mut addr) = crate::identity::conn_from_words(&words_with_spaces) {
addr.set_port(port);
return Ok(addr);
}
}
}
}
Err(anyhow::anyhow!(
"Failed to parse address '{}': not a valid socket address or four-word format: {}",
address,
e
))
}
}
}
pub async fn cold_start_discovery(
config: IntroducerConfig,
_transport: &dyn GossipTransport,
) -> Result<Vec<String>> {
if config.addresses.is_empty() {
warn!("No introducer nodes configured for cold start");
return Ok(vec![]);
}
let mut connected_introducers = vec![];
for introducer in &config.addresses {
info!("Connecting to introducer: {}", introducer);
match parse_introducer_address(introducer) {
Ok(addr) => {
info!("Parsed introducer address '{}' -> {}", introducer, addr);
connected_introducers.push(addr.to_string());
}
Err(e) => {
warn!("Failed to parse introducer address {}: {}", introducer, e);
}
}
}
if connected_introducers.is_empty() {
return Err(anyhow::anyhow!("Failed to connect to any introducers"));
}
info!("Connected to {} introducer(s)", connected_introducers.len());
Ok(connected_introducers)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_foaf_discovery_creation() {
let discovery = FoafDiscovery::new();
let contacts = discovery.get_contacts().await;
assert_eq!(contacts.len(), 0);
}
#[tokio::test]
async fn test_add_remove_contact() {
let discovery = FoafDiscovery::new();
let peer_id = PeerId::new([2u8; 32]);
discovery
.add_contact("ocean-forest-moon-star".to_string(), peer_id)
.await;
let contacts = discovery.get_contacts().await;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].0, "ocean-forest-moon-star");
discovery.remove_contact("ocean-forest-moon-star").await;
let contacts = discovery.get_contacts().await;
assert_eq!(contacts.len(), 0);
}
#[tokio::test]
async fn test_find_contact_in_cache() {
let discovery = FoafDiscovery::new();
let peer_id = PeerId::new([3u8; 32]);
discovery
.add_contact("river-mountain-cloud-light".to_string(), peer_id)
.await;
let found = discovery.find_contact("river-mountain-cloud-light").await;
assert!(found.is_ok());
assert_eq!(found.expect("should find cached contact"), peer_id);
}
}