use crate::identity::{conn_from_words, conn_words};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
use tokio::sync::RwLock;
#[derive(Debug, Error)]
pub enum ContactStorageError {
#[error("Contact not found: {0}")]
NotFound(String),
#[error("Invalid four-word identity: {0}")]
InvalidIdentity(String),
#[error("Storage error: {0}")]
StorageError(String),
#[error("Endpoint encoding error: {0}")]
EndpointEncodingError(String),
}
pub type ContactResult<T> = Result<T, ContactStorageError>;
const ENDPOINT_TTL_HOURS: u64 = 24;
const MAX_ENDPOINT_FAILURES: u32 = 3;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactRecord {
pub id: String,
pub four_words: Option<String>,
pub display_name: Option<String>,
pub is_favourite: bool,
#[serde(default)]
pub is_local_only: bool,
pub linked_at: Option<u64>,
pub last_sync_at: Option<u64>,
pub last_seen_endpoint: Option<String>,
pub endpoint_updated_at: Option<u64>,
pub endpoint_success_count: u32,
pub endpoint_failure_count: u32,
pub created_at: u64,
pub last_online_at: Option<u64>,
}
impl ContactRecord {
pub fn new(four_words: String) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
four_words: Some(four_words),
display_name: None,
is_favourite: false,
is_local_only: false,
linked_at: None,
last_sync_at: None,
last_seen_endpoint: None,
endpoint_updated_at: None,
endpoint_success_count: 0,
endpoint_failure_count: 0,
created_at: now_millis(),
last_online_at: None,
}
}
pub fn new_local(display_name: String) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
four_words: None,
display_name: Some(display_name),
is_favourite: false,
is_local_only: true,
linked_at: None,
last_sync_at: None,
last_seen_endpoint: None,
endpoint_updated_at: None,
endpoint_success_count: 0,
endpoint_failure_count: 0,
created_at: now_millis(),
last_online_at: None,
}
}
pub fn with_display_name(four_words: String, display_name: String) -> Self {
let mut contact = Self::new(four_words);
contact.display_name = Some(display_name);
contact
}
pub fn is_linked(&self) -> bool {
self.four_words.is_some() && !self.is_local_only
}
pub fn link_to_network(&mut self, four_words: String) {
self.four_words = Some(four_words);
self.is_local_only = false;
self.linked_at = Some(now_millis());
}
pub fn mark_synced(&mut self) {
self.last_sync_at = Some(now_millis());
}
pub fn effective_name(&self) -> String {
self.display_name
.clone()
.or_else(|| self.four_words.clone())
.unwrap_or_else(|| "Unknown".to_string())
}
pub fn get_valid_endpoint(&self) -> Option<SocketAddr> {
let endpoint_words = self.last_seen_endpoint.as_ref()?;
let updated_at = self.endpoint_updated_at?;
let now = now_millis();
let age_hours = (now.saturating_sub(updated_at)) / (1000 * 60 * 60);
if age_hours > ENDPOINT_TTL_HOURS {
return None;
}
if self.endpoint_failure_count >= MAX_ENDPOINT_FAILURES {
return None;
}
conn_from_words(endpoint_words).ok()
}
pub fn is_endpoint_stale(&self) -> bool {
match self.endpoint_updated_at {
Some(updated_at) => {
let now = now_millis();
let age_hours = (now.saturating_sub(updated_at)) / (1000 * 60 * 60);
age_hours > ENDPOINT_TTL_HOURS
}
None => true,
}
}
pub fn record_success(&mut self, addr: SocketAddr) {
if let Ok(words) = conn_words(&addr) {
self.last_seen_endpoint = Some(words);
self.endpoint_updated_at = Some(now_millis());
self.endpoint_success_count = self.endpoint_success_count.saturating_add(1);
self.endpoint_failure_count = 0;
self.last_online_at = Some(now_millis());
}
}
pub fn record_failure(&mut self) {
self.endpoint_failure_count = self.endpoint_failure_count.saturating_add(1);
}
pub fn reset_failures(&mut self) {
self.endpoint_failure_count = 0;
}
pub fn update_endpoint_from_words(&mut self, endpoint_words: &str) -> ContactResult<()> {
conn_from_words(endpoint_words).map_err(|e| {
ContactStorageError::EndpointEncodingError(format!(
"Invalid endpoint words '{}': {}",
endpoint_words, e
))
})?;
self.last_seen_endpoint = Some(endpoint_words.to_string());
self.endpoint_updated_at = Some(now_millis());
self.endpoint_failure_count = 0;
Ok(())
}
pub fn update_endpoint(&mut self, addr: &SocketAddr) -> ContactResult<()> {
let words = conn_words(addr).map_err(|e| {
ContactStorageError::EndpointEncodingError(format!(
"Failed to encode endpoint {}: {}",
addr, e
))
})?;
self.last_seen_endpoint = Some(words);
self.endpoint_updated_at = Some(now_millis());
self.endpoint_failure_count = 0;
Ok(())
}
pub fn mark_online(&mut self) {
self.last_online_at = Some(now_millis());
}
pub fn endpoint_age_hours(&self) -> Option<u64> {
self.endpoint_updated_at.map(|updated_at| {
let now = now_millis();
(now.saturating_sub(updated_at)) / (1000 * 60 * 60)
})
}
}
#[derive(Debug, Clone)]
pub struct ContactStore {
contacts: Arc<RwLock<HashMap<String, ContactRecord>>>,
four_words_index: Arc<RwLock<HashMap<String, String>>>,
}
impl Default for ContactStore {
fn default() -> Self {
Self::new()
}
}
impl ContactStore {
pub fn new() -> Self {
Self {
contacts: Arc::new(RwLock::new(HashMap::new())),
four_words_index: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn add(&self, contact: ContactRecord) -> ContactResult<()> {
let mut contacts = self.contacts.write().await;
let mut index = self.four_words_index.write().await;
if let Some(ref fw) = contact.four_words {
index.insert(fw.clone(), contact.id.clone());
}
contacts.insert(contact.id.clone(), contact);
Ok(())
}
pub async fn get_by_id(&self, id: &str) -> Option<ContactRecord> {
let contacts = self.contacts.read().await;
contacts.get(id).cloned()
}
pub async fn get(&self, four_words: &str) -> Option<ContactRecord> {
self.get_by_four_words(four_words).await
}
pub async fn get_by_four_words(&self, four_words: &str) -> Option<ContactRecord> {
let index = self.four_words_index.read().await;
if let Some(id) = index.get(four_words) {
let contacts = self.contacts.read().await;
return contacts.get(id).cloned();
}
None
}
pub async fn exists_by_id(&self, id: &str) -> bool {
let contacts = self.contacts.read().await;
contacts.contains_key(id)
}
pub async fn exists(&self, four_words: &str) -> bool {
let index = self.four_words_index.read().await;
index.contains_key(four_words)
}
pub async fn update(&self, contact: ContactRecord) -> ContactResult<()> {
let mut contacts = self.contacts.write().await;
let mut index = self.four_words_index.write().await;
if !contacts.contains_key(&contact.id) {
return Err(ContactStorageError::NotFound(contact.id.clone()));
}
if let Some(ref fw) = contact.four_words {
index.insert(fw.clone(), contact.id.clone());
}
contacts.insert(contact.id.clone(), contact);
Ok(())
}
pub async fn upsert(&self, contact: ContactRecord) {
let mut contacts = self.contacts.write().await;
let mut index = self.four_words_index.write().await;
if let Some(ref fw) = contact.four_words {
index.insert(fw.clone(), contact.id.clone());
}
contacts.insert(contact.id.clone(), contact);
}
pub async fn remove_by_id(&self, id: &str) -> ContactResult<ContactRecord> {
let mut contacts = self.contacts.write().await;
let mut index = self.four_words_index.write().await;
let contact = contacts
.remove(id)
.ok_or_else(|| ContactStorageError::NotFound(id.to_string()))?;
if let Some(ref fw) = contact.four_words {
index.remove(fw);
}
Ok(contact)
}
pub async fn remove(&self, four_words: &str) -> ContactResult<ContactRecord> {
let index = self.four_words_index.read().await;
let id = index
.get(four_words)
.cloned()
.ok_or_else(|| ContactStorageError::NotFound(four_words.to_string()))?;
drop(index);
self.remove_by_id(&id).await
}
pub async fn link_contact(&self, id: &str, four_words: &str) -> ContactResult<ContactRecord> {
let mut contacts = self.contacts.write().await;
let mut index = self.four_words_index.write().await;
let contact = contacts
.get_mut(id)
.ok_or_else(|| ContactStorageError::NotFound(id.to_string()))?;
contact.link_to_network(four_words.to_string());
index.insert(four_words.to_string(), id.to_string());
Ok(contact.clone())
}
pub async fn local_only(&self) -> Vec<ContactRecord> {
let contacts = self.contacts.read().await;
contacts
.values()
.filter(|c| c.is_local_only)
.cloned()
.collect()
}
pub async fn network_linked(&self) -> Vec<ContactRecord> {
let contacts = self.contacts.read().await;
contacts
.values()
.filter(|c| !c.is_local_only && c.four_words.is_some())
.cloned()
.collect()
}
pub async fn all(&self) -> Vec<ContactRecord> {
let contacts = self.contacts.read().await;
contacts.values().cloned().collect()
}
pub async fn favourites(&self) -> Vec<ContactRecord> {
let contacts = self.contacts.read().await;
contacts
.values()
.filter(|c| c.is_favourite)
.cloned()
.collect()
}
pub async fn with_valid_endpoints(&self) -> Vec<ContactRecord> {
let contacts = self.contacts.read().await;
contacts
.values()
.filter(|c| c.get_valid_endpoint().is_some())
.cloned()
.collect()
}
pub async fn update_endpoint(&self, four_words: &str, addr: &SocketAddr) -> ContactResult<()> {
let index = self.four_words_index.read().await;
let id = index
.get(four_words)
.cloned()
.ok_or_else(|| ContactStorageError::NotFound(four_words.to_string()))?;
drop(index);
let mut contacts = self.contacts.write().await;
let contact = contacts
.get_mut(&id)
.ok_or_else(|| ContactStorageError::NotFound(id.clone()))?;
contact.update_endpoint(addr)
}
pub async fn update_endpoint_by_id(&self, id: &str, addr: &SocketAddr) -> ContactResult<()> {
let mut contacts = self.contacts.write().await;
let contact = contacts
.get_mut(id)
.ok_or_else(|| ContactStorageError::NotFound(id.to_string()))?;
contact.update_endpoint(addr)
}
pub async fn record_success(&self, four_words: &str, addr: SocketAddr) -> ContactResult<()> {
let index = self.four_words_index.read().await;
let id = index
.get(four_words)
.cloned()
.ok_or_else(|| ContactStorageError::NotFound(four_words.to_string()))?;
drop(index);
let mut contacts = self.contacts.write().await;
let contact = contacts
.get_mut(&id)
.ok_or_else(|| ContactStorageError::NotFound(id.clone()))?;
contact.record_success(addr);
Ok(())
}
pub async fn record_failure(&self, four_words: &str) -> ContactResult<()> {
let index = self.four_words_index.read().await;
let id = index
.get(four_words)
.cloned()
.ok_or_else(|| ContactStorageError::NotFound(four_words.to_string()))?;
drop(index);
let mut contacts = self.contacts.write().await;
let contact = contacts
.get_mut(&id)
.ok_or_else(|| ContactStorageError::NotFound(id.clone()))?;
contact.record_failure();
Ok(())
}
pub async fn count(&self) -> usize {
let contacts = self.contacts.read().await;
contacts.len()
}
pub async fn clear(&self) {
let mut contacts = self.contacts.write().await;
let mut index = self.four_words_index.write().await;
contacts.clear();
index.clear();
}
pub async fn export(&self) -> Vec<ContactRecord> {
self.all().await
}
pub async fn import(&self, records: Vec<ContactRecord>) {
let mut contacts = self.contacts.write().await;
let mut index = self.four_words_index.write().await;
for record in records {
if let Some(ref fw) = record.four_words {
index.insert(fw.clone(), record.id.clone());
}
contacts.insert(record.id.clone(), record);
}
}
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contact_record_new() {
let contact = ContactRecord::new("ocean-forest-moon-star".to_string());
assert_eq!(
contact.four_words,
Some("ocean-forest-moon-star".to_string())
);
assert!(contact.display_name.is_none());
assert!(!contact.is_favourite);
assert!(!contact.is_local_only);
assert!(contact.is_linked());
assert!(contact.last_seen_endpoint.is_none());
assert_eq!(contact.endpoint_success_count, 0);
assert_eq!(contact.endpoint_failure_count, 0);
}
#[test]
fn test_contact_record_new_local() {
let contact = ContactRecord::new_local("Alice".to_string());
assert!(contact.four_words.is_none());
assert_eq!(contact.display_name, Some("Alice".to_string()));
assert!(contact.is_local_only);
assert!(!contact.is_linked());
assert_eq!(contact.effective_name(), "Alice");
}
#[test]
fn test_contact_record_link_to_network() {
let mut contact = ContactRecord::new_local("Alice".to_string());
assert!(contact.is_local_only);
assert!(!contact.is_linked());
contact.link_to_network("ocean-forest-moon-star".to_string());
assert!(!contact.is_local_only);
assert!(contact.is_linked());
assert_eq!(
contact.four_words,
Some("ocean-forest-moon-star".to_string())
);
assert!(contact.linked_at.is_some());
}
#[test]
fn test_contact_record_effective_name() {
let contact = ContactRecord::with_display_name(
"ocean-forest-moon-star".to_string(),
"Alice".to_string(),
);
assert_eq!(contact.effective_name(), "Alice");
let contact = ContactRecord::new("ocean-forest-moon-star".to_string());
assert_eq!(contact.effective_name(), "ocean-forest-moon-star");
let contact = ContactRecord::new_local("Bob".to_string());
assert_eq!(contact.effective_name(), "Bob");
}
#[test]
fn test_contact_record_with_display_name() {
let contact = ContactRecord::with_display_name(
"ocean-forest-moon-star".to_string(),
"Alice".to_string(),
);
assert_eq!(
contact.four_words,
Some("ocean-forest-moon-star".to_string())
);
assert_eq!(contact.display_name, Some("Alice".to_string()));
}
#[test]
fn test_record_success() {
let mut contact = ContactRecord::new("ocean-forest-moon-star".to_string());
let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
contact.record_success(addr);
assert!(contact.last_seen_endpoint.is_some());
assert!(contact.endpoint_updated_at.is_some());
assert_eq!(contact.endpoint_success_count, 1);
assert_eq!(contact.endpoint_failure_count, 0);
}
#[test]
fn test_record_failure() {
let mut contact = ContactRecord::new("ocean-forest-moon-star".to_string());
contact.record_failure();
assert_eq!(contact.endpoint_failure_count, 1);
contact.record_failure();
assert_eq!(contact.endpoint_failure_count, 2);
contact.record_failure();
assert_eq!(contact.endpoint_failure_count, 3);
}
#[test]
fn test_get_valid_endpoint_after_success() {
let mut contact = ContactRecord::new("ocean-forest-moon-star".to_string());
let addr: SocketAddr = "192.168.1.100:9000".parse().unwrap();
contact.record_success(addr);
let result = contact.get_valid_endpoint();
assert_eq!(result, Some(addr));
}
#[test]
fn test_get_valid_endpoint_too_many_failures() {
let mut contact = ContactRecord::new("ocean-forest-moon-star".to_string());
let addr: SocketAddr = "192.168.1.100:9000".parse().unwrap();
contact.record_success(addr);
for _ in 0..MAX_ENDPOINT_FAILURES {
contact.record_failure();
}
assert!(contact.get_valid_endpoint().is_none());
}
#[test]
fn test_reset_failures() {
let mut contact = ContactRecord::new("ocean-forest-moon-star".to_string());
let addr: SocketAddr = "192.168.1.100:9000".parse().unwrap();
contact.record_success(addr);
contact.record_failure();
contact.record_failure();
contact.record_failure();
contact.reset_failures();
assert_eq!(contact.endpoint_failure_count, 0);
assert!(contact.get_valid_endpoint().is_some());
}
#[tokio::test]
async fn test_contact_store_add_get() {
let store = ContactStore::new();
let contact = ContactRecord::new("ocean-forest-moon-star".to_string());
store.add(contact).await.unwrap();
let retrieved = store.get("ocean-forest-moon-star").await;
assert!(retrieved.is_some());
assert_eq!(
retrieved.unwrap().four_words,
Some("ocean-forest-moon-star".to_string())
);
}
#[tokio::test]
async fn test_contact_store_remove() {
let store = ContactStore::new();
let contact = ContactRecord::new("ocean-forest-moon-star".to_string());
store.add(contact).await.unwrap();
let removed = store.remove("ocean-forest-moon-star").await;
assert!(removed.is_ok());
assert!(store.get("ocean-forest-moon-star").await.is_none());
}
#[tokio::test]
async fn test_contact_store_favourites() {
let store = ContactStore::new();
let mut contact1 = ContactRecord::new("ocean-forest-moon-star".to_string());
contact1.is_favourite = true;
let contact2 = ContactRecord::new("river-mountain-cloud-wind".to_string());
store.add(contact1).await.unwrap();
store.add(contact2).await.unwrap();
let favourites = store.favourites().await;
assert_eq!(favourites.len(), 1);
assert_eq!(
favourites[0].four_words,
Some("ocean-forest-moon-star".to_string())
);
}
#[tokio::test]
async fn test_contact_store_with_valid_endpoints() {
let store = ContactStore::new();
let addr: SocketAddr = "192.168.1.100:9000".parse().unwrap();
let mut contact1 = ContactRecord::new("ocean-forest-moon-star".to_string());
contact1.record_success(addr);
let contact2 = ContactRecord::new("river-mountain-cloud-wind".to_string());
store.add(contact1).await.unwrap();
store.add(contact2).await.unwrap();
let with_endpoints = store.with_valid_endpoints().await;
assert_eq!(with_endpoints.len(), 1);
assert_eq!(
with_endpoints[0].four_words,
Some("ocean-forest-moon-star".to_string())
);
}
#[tokio::test]
async fn test_contact_store_update_endpoint() {
let store = ContactStore::new();
let contact = ContactRecord::new("ocean-forest-moon-star".to_string());
store.add(contact).await.unwrap();
let addr: SocketAddr = "192.168.1.100:9000".parse().unwrap();
store
.update_endpoint("ocean-forest-moon-star", &addr)
.await
.unwrap();
let updated = store.get("ocean-forest-moon-star").await.unwrap();
assert!(updated.last_seen_endpoint.is_some());
assert_eq!(updated.get_valid_endpoint(), Some(addr));
}
#[tokio::test]
async fn test_contact_store_record_success_failure() {
let store = ContactStore::new();
let contact = ContactRecord::new("ocean-forest-moon-star".to_string());
store.add(contact).await.unwrap();
let addr: SocketAddr = "192.168.1.100:9000".parse().unwrap();
store
.record_success("ocean-forest-moon-star", addr)
.await
.unwrap();
let updated = store.get("ocean-forest-moon-star").await.unwrap();
assert_eq!(updated.endpoint_success_count, 1);
assert_eq!(updated.endpoint_failure_count, 0);
store
.record_failure("ocean-forest-moon-star")
.await
.unwrap();
let updated = store.get("ocean-forest-moon-star").await.unwrap();
assert_eq!(updated.endpoint_failure_count, 1);
}
#[tokio::test]
async fn test_contact_store_export_import() {
let store = ContactStore::new();
let contact1 = ContactRecord::new("ocean-forest-moon-star".to_string());
let contact2 = ContactRecord::new("river-mountain-cloud-wind".to_string());
store.add(contact1).await.unwrap();
store.add(contact2).await.unwrap();
let exported = store.export().await;
assert_eq!(exported.len(), 2);
let new_store = ContactStore::new();
new_store.import(exported).await;
assert_eq!(new_store.count().await, 2);
assert!(new_store.get("ocean-forest-moon-star").await.is_some());
assert!(new_store.get("river-mountain-cloud-wind").await.is_some());
}
#[tokio::test]
async fn test_contact_store_get_by_id() {
let store = ContactStore::new();
let contact = ContactRecord::new("ocean-forest-moon-star".to_string());
let contact_id = contact.id.clone();
store.add(contact).await.unwrap();
let retrieved = store.get_by_id(&contact_id).await;
assert!(retrieved.is_some());
assert_eq!(
retrieved.unwrap().four_words,
Some("ocean-forest-moon-star".to_string())
);
}
#[tokio::test]
async fn test_contact_store_local_only_contacts() {
let store = ContactStore::new();
let contact1 = ContactRecord::new("ocean-forest-moon-star".to_string());
store.add(contact1).await.unwrap();
let contact2 = ContactRecord::new_local("Alice".to_string());
let local_id = contact2.id.clone();
store.add(contact2).await.unwrap();
let local_contacts = store.local_only().await;
assert_eq!(local_contacts.len(), 1);
assert_eq!(local_contacts[0].id, local_id);
assert!(local_contacts[0].is_local_only);
let linked_contacts = store.network_linked().await;
assert_eq!(linked_contacts.len(), 1);
assert!(!linked_contacts[0].is_local_only);
}
#[tokio::test]
async fn test_contact_store_link_contact() {
let store = ContactStore::new();
let contact = ContactRecord::new_local("Alice".to_string());
let contact_id = contact.id.clone();
store.add(contact).await.unwrap();
let local_contacts = store.local_only().await;
assert_eq!(local_contacts.len(), 1);
let linked = store
.link_contact(&contact_id, "ocean-forest-moon-star")
.await
.unwrap();
assert!(!linked.is_local_only);
assert_eq!(
linked.four_words,
Some("ocean-forest-moon-star".to_string())
);
assert!(linked.linked_at.is_some());
let local_contacts = store.local_only().await;
assert_eq!(local_contacts.len(), 0);
let linked_contacts = store.network_linked().await;
assert_eq!(linked_contacts.len(), 1);
let by_fw = store.get("ocean-forest-moon-star").await;
assert!(by_fw.is_some());
}
#[tokio::test]
async fn test_contact_store_remove_by_id() {
let store = ContactStore::new();
let contact = ContactRecord::new_local("Alice".to_string());
let contact_id = contact.id.clone();
store.add(contact).await.unwrap();
assert_eq!(store.count().await, 1);
let removed = store.remove_by_id(&contact_id).await;
assert!(removed.is_ok());
assert_eq!(store.count().await, 0);
assert!(store.get_by_id(&contact_id).await.is_none());
}
}