use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PresenceError {
#[error("Signing failed: {0}")]
SigningFailed(String),
#[error("Signature verification failed: {0}")]
VerificationFailed(String),
#[error("System time error")]
TimeError,
}
pub type PresenceResult<T> = Result<T, PresenceError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ConnectivityState {
#[default]
Unknown,
Connected,
FailedMaybeOffline,
FailedWhileOnline,
}
impl ConnectivityState {
pub fn is_viable(self) -> bool {
matches!(
self,
ConnectivityState::Unknown
| ConnectivityState::Connected
| ConnectivityState::FailedMaybeOffline
)
}
}
#[derive(Debug, Clone)]
pub struct CachedPresence {
pub record: PresenceRecord,
pub connectivity: ConnectivityState,
}
impl CachedPresence {
pub fn new(record: PresenceRecord) -> Self {
Self {
record,
connectivity: ConnectivityState::Unknown,
}
}
pub fn is_viable(&self) -> bool {
self.connectivity.is_viable()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PresenceRecord {
pub pubkey: Vec<u8>,
pub connection_words: String,
pub timestamp: u64,
pub signature: Vec<u8>,
}
impl PresenceRecord {
pub fn new_unsigned(pubkey: Vec<u8>, connection_words: String, timestamp: u64) -> Self {
Self {
pubkey,
connection_words,
timestamp,
signature: Vec::new(),
}
}
pub fn new_now(pubkey: Vec<u8>, connection_words: String) -> PresenceResult<Self> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| PresenceError::TimeError)?
.as_secs();
Ok(Self {
pubkey,
connection_words,
timestamp,
signature: Vec::new(),
})
}
pub fn is_fresher_than(&self, other: &Self) -> bool {
self.timestamp > other.timestamp
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PresenceQuery {
pub target_pubkey: Vec<u8>,
pub reply_to: std::net::SocketAddr,
}
impl PresenceQuery {
pub fn new(target_pubkey: Vec<u8>, reply_to: std::net::SocketAddr) -> Self {
Self {
target_pubkey,
reply_to,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PresenceResponse {
pub record: PresenceRecord,
}
impl PresenceResponse {
pub fn new(record: PresenceRecord) -> Self {
Self { record }
}
}
#[derive(Debug, Default)]
pub struct PresenceCache {
records: HashMap<Vec<u8>, CachedPresence>,
}
impl PresenceCache {
pub fn new() -> Self {
Self {
records: HashMap::new(),
}
}
pub fn insert(&mut self, record: PresenceRecord) -> bool {
match self.records.get(&record.pubkey) {
Some(existing) if !record.is_fresher_than(&existing.record) => false,
_ => {
self.records
.insert(record.pubkey.clone(), CachedPresence::new(record));
true
}
}
}
pub fn get(&self, pubkey: &[u8]) -> Option<&CachedPresence> {
self.records.get(pubkey)
}
pub fn get_viable(&self, pubkey: &[u8]) -> Option<&CachedPresence> {
self.records.get(pubkey).filter(|cp| cp.is_viable())
}
pub fn mark_connected(&mut self, pubkey: &[u8]) {
if let Some(cached) = self.records.get_mut(pubkey) {
cached.connectivity = ConnectivityState::Connected;
}
}
pub fn mark_failed(&mut self, pubkey: &[u8], we_are_online: bool) {
if let Some(cached) = self.records.get_mut(pubkey) {
cached.connectivity = if we_are_online {
ConnectivityState::FailedWhileOnline
} else {
ConnectivityState::FailedMaybeOffline
};
}
}
pub fn reset_failed_states(&mut self) {
for cached in self.records.values_mut() {
if matches!(
cached.connectivity,
ConnectivityState::FailedMaybeOffline | ConnectivityState::FailedWhileOnline
) {
cached.connectivity = ConnectivityState::Unknown;
}
}
}
pub fn contains(&self, pubkey: &[u8]) -> bool {
self.records.contains_key(pubkey)
}
pub fn remove(&mut self, pubkey: &[u8]) -> Option<CachedPresence> {
self.records.remove(pubkey)
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
pub fn get_all_sorted_by_freshness(&self) -> Vec<&CachedPresence> {
let mut records: Vec<_> = self.records.values().collect();
records.sort_by_key(|record| std::cmp::Reverse(record.record.timestamp));
records
}
pub fn get_viable_sorted_by_freshness(&self) -> Vec<&CachedPresence> {
let mut records: Vec<_> = self.records.values().filter(|cp| cp.is_viable()).collect();
records.sort_by_key(|record| std::cmp::Reverse(record.record.timestamp));
records
}
pub fn pubkeys(&self) -> Vec<&Vec<u8>> {
self.records.keys().collect()
}
pub fn clear(&mut self) {
self.records.clear();
}
pub fn count_by_state(&self) -> (usize, usize, usize, usize) {
let mut unknown = 0;
let mut connected = 0;
let mut failed_maybe_offline = 0;
let mut failed_while_online = 0;
for cached in self.records.values() {
match cached.connectivity {
ConnectivityState::Unknown => unknown += 1,
ConnectivityState::Connected => connected += 1,
ConnectivityState::FailedMaybeOffline => failed_maybe_offline += 1,
ConnectivityState::FailedWhileOnline => failed_while_online += 1,
}
}
(
unknown,
connected,
failed_maybe_offline,
failed_while_online,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_presence_record_freshness() {
let older = PresenceRecord {
pubkey: vec![1u8; 32],
connection_words: "old".to_string(),
timestamp: 1000,
signature: Vec::new(),
};
let newer = PresenceRecord {
pubkey: vec![1u8; 32],
connection_words: "new".to_string(),
timestamp: 2000,
signature: Vec::new(),
};
assert!(newer.is_fresher_than(&older));
assert!(!older.is_fresher_than(&newer));
assert!(!older.is_fresher_than(&older));
}
#[test]
fn test_connectivity_state_viability() {
assert!(ConnectivityState::Unknown.is_viable());
assert!(ConnectivityState::Connected.is_viable());
assert!(ConnectivityState::FailedMaybeOffline.is_viable());
assert!(!ConnectivityState::FailedWhileOnline.is_viable());
}
#[test]
fn test_cached_presence_viability() {
let record = PresenceRecord {
pubkey: vec![1u8; 32],
connection_words: "test".to_string(),
timestamp: 1000,
signature: Vec::new(),
};
let mut cached = CachedPresence::new(record);
assert!(cached.is_viable());
cached.connectivity = ConnectivityState::Connected;
assert!(cached.is_viable());
cached.connectivity = ConnectivityState::FailedMaybeOffline;
assert!(cached.is_viable());
cached.connectivity = ConnectivityState::FailedWhileOnline;
assert!(!cached.is_viable());
}
#[test]
fn test_presence_cache_insert_and_get() {
let mut cache = PresenceCache::new();
let pubkey = vec![1u8; 32];
let record = PresenceRecord {
pubkey: pubkey.clone(),
connection_words: "test".to_string(),
timestamp: 1000,
signature: Vec::new(),
};
assert!(cache.insert(record.clone()));
assert!(cache.contains(&pubkey));
assert_eq!(cache.get(&pubkey).map(|cp| cp.record.timestamp), Some(1000));
}
#[test]
fn test_presence_cache_fresher_replaces_older() {
let mut cache = PresenceCache::new();
let pubkey = vec![1u8; 32];
let older = PresenceRecord {
pubkey: pubkey.clone(),
connection_words: "old".to_string(),
timestamp: 1000,
signature: Vec::new(),
};
let newer = PresenceRecord {
pubkey: pubkey.clone(),
connection_words: "new".to_string(),
timestamp: 2000,
signature: Vec::new(),
};
assert!(cache.insert(older));
assert!(cache.insert(newer));
let cached = cache.get(&pubkey).expect("should exist");
assert_eq!(cached.record.connection_words, "new");
assert_eq!(cached.record.timestamp, 2000);
}
#[test]
fn test_presence_cache_older_rejected() {
let mut cache = PresenceCache::new();
let pubkey = vec![1u8; 32];
let newer = PresenceRecord {
pubkey: pubkey.clone(),
connection_words: "new".to_string(),
timestamp: 2000,
signature: Vec::new(),
};
let older = PresenceRecord {
pubkey: pubkey.clone(),
connection_words: "old".to_string(),
timestamp: 1000,
signature: Vec::new(),
};
assert!(cache.insert(newer));
assert!(!cache.insert(older));
let cached = cache.get(&pubkey).expect("should exist");
assert_eq!(cached.record.connection_words, "new");
}
#[test]
fn test_presence_cache_connectivity_state_transitions() {
let mut cache = PresenceCache::new();
let pubkey = vec![1u8; 32];
let record = PresenceRecord {
pubkey: pubkey.clone(),
connection_words: "test".to_string(),
timestamp: 1000,
signature: Vec::new(),
};
cache.insert(record);
assert_eq!(
cache.get(&pubkey).map(|cp| cp.connectivity),
Some(ConnectivityState::Unknown)
);
cache.mark_connected(&pubkey);
assert_eq!(
cache.get(&pubkey).map(|cp| cp.connectivity),
Some(ConnectivityState::Connected)
);
cache.mark_failed(&pubkey, true);
assert_eq!(
cache.get(&pubkey).map(|cp| cp.connectivity),
Some(ConnectivityState::FailedWhileOnline)
);
cache.mark_failed(&pubkey, false);
assert_eq!(
cache.get(&pubkey).map(|cp| cp.connectivity),
Some(ConnectivityState::FailedMaybeOffline)
);
}
#[test]
fn test_presence_cache_reset_failed_states() {
let mut cache = PresenceCache::new();
for i in 0..4 {
let record = PresenceRecord {
pubkey: vec![i; 32],
connection_words: format!("peer-{}", i),
timestamp: (i as u64) * 1000,
signature: Vec::new(),
};
cache.insert(record);
}
let pk0 = vec![0u8; 32];
let pk1 = vec![1u8; 32];
let pk2 = vec![2u8; 32];
cache.mark_connected(&pk0);
cache.mark_failed(&pk1, false);
cache.mark_failed(&pk2, true);
let (unknown, connected, maybe_offline, while_online) = cache.count_by_state();
assert_eq!(unknown, 1);
assert_eq!(connected, 1);
assert_eq!(maybe_offline, 1);
assert_eq!(while_online, 1);
cache.reset_failed_states();
let (unknown, connected, maybe_offline, while_online) = cache.count_by_state();
assert_eq!(unknown, 3);
assert_eq!(connected, 1);
assert_eq!(maybe_offline, 0);
assert_eq!(while_online, 0);
}
#[test]
fn test_presence_query_and_response() {
let pubkey = vec![1u8; 32];
let reply_to: std::net::SocketAddr = "127.0.0.1:9000".parse().unwrap();
let query = PresenceQuery::new(pubkey.clone(), reply_to);
assert_eq!(query.target_pubkey, pubkey);
assert_eq!(query.reply_to, reply_to);
let record = PresenceRecord {
pubkey,
connection_words: "test".to_string(),
timestamp: 1000,
signature: Vec::new(),
};
let response = PresenceResponse::new(record.clone());
assert_eq!(response.record, record);
}
}