use crate::{db::Db, error::MyError}; use activitystreams::{uri, url::Url}; use async_rwlock::RwLock; use log::{debug, error}; use std::{ collections::{HashMap, HashSet}, sync::Arc, time::{Duration, SystemTime}, }; use tokio_postgres::types::Json; use uuid::Uuid; pub type ListenersCache = Arc>>; #[derive(Clone)] pub struct NodeCache { db: Db, listeners: ListenersCache, nodes: Arc>>, } impl NodeCache { pub fn new(db: Db, listeners: ListenersCache) -> Self { NodeCache { db, listeners, nodes: Arc::new(RwLock::new(HashMap::new())), } } pub async fn nodes(&self) -> Vec { let listeners: HashSet<_> = self.listeners.read().await.clone(); self.nodes .read() .await .iter() .filter_map(|(k, v)| { if listeners.contains(k) { Some(v.clone()) } else { None } }) .collect() } pub async fn is_nodeinfo_outdated(&self, listener: &Url) -> bool { let read_guard = self.nodes.read().await; let node = match read_guard.get(listener) { None => { debug!("No node for listener {}", listener); return true; } Some(node) => node, }; match node.info.as_ref() { Some(nodeinfo) => nodeinfo.outdated(), None => { debug!("No info for node {}", node.base); true } } } pub async fn is_contact_outdated(&self, listener: &Url) -> bool { let read_guard = self.nodes.read().await; let node = match read_guard.get(listener) { None => { debug!("No node for listener {}", listener); return true; } Some(node) => node, }; match node.contact.as_ref() { Some(contact) => contact.outdated(), None => { debug!("No contact for node {}", node.base); true } } } pub async fn is_instance_outdated(&self, listener: &Url) -> bool { let read_guard = self.nodes.read().await; let node = match read_guard.get(listener) { None => { debug!("No node for listener {}", listener); return true; } Some(node) => node, }; match node.instance.as_ref() { Some(instance) => instance.outdated(), None => { debug!("No instance for node {}", node.base); true } } } pub async fn cache_by_id(&self, id: Uuid) { if let Err(e) = self.do_cache_by_id(id).await { error!("Error loading node into cache, {}", e); } } pub async fn bust_by_id(&self, id: Uuid) { if let Err(e) = self.do_bust_by_id(id).await { error!("Error busting node cache, {}", e); } } async fn do_bust_by_id(&self, id: Uuid) -> Result<(), MyError> { let row_opt = self .db .pool() .get() .await? .query_opt( "SELECT ls.actor_id FROM listeners AS ls INNER JOIN nodes AS nd ON nd.listener_id = ls.id WHERE nd.id = $1::UUID LIMIT 1;", &[&id], ) .await?; let row = if let Some(row) = row_opt { row } else { return Ok(()); }; let listener: String = row.try_get(0)?; self.nodes.write().await.remove(&uri!(listener)); Ok(()) } async fn do_cache_by_id(&self, id: Uuid) -> Result<(), MyError> { let row_opt = self .db .pool() .get() .await? .query_opt( "SELECT ls.actor_id, nd.nodeinfo, nd.instance, nd.contact FROM nodes AS nd INNER JOIN listeners AS ls ON nd.listener_id = ls.id WHERE nd.id = $1::UUID LIMIT 1;", &[&id], ) .await?; let row = if let Some(row) = row_opt { row } else { return Ok(()); }; let listener: String = row.try_get(0)?; let listener = uri!(listener); let info: Option> = row.try_get(1)?; let instance: Option> = row.try_get(2)?; let contact: Option> = row.try_get(3)?; { let mut write_guard = self.nodes.write().await; let node = write_guard .entry(listener.clone()) .or_insert_with(|| Node::new(listener)); if let Some(info) = info { node.info = Some(info.0); } if let Some(instance) = instance { node.instance = Some(instance.0); } if let Some(contact) = contact { node.contact = Some(contact.0); } } Ok(()) } pub async fn set_info( &self, listener: &Url, software: String, version: String, reg: bool, ) -> Result<(), MyError> { if !self.listeners.read().await.contains(listener) { let mut nodes = self.nodes.write().await; nodes.remove(listener); return Ok(()); } let node = { let mut write_guard = self.nodes.write().await; let node = write_guard .entry(listener.clone()) .or_insert_with(|| Node::new(listener.clone())); node.set_info(software, version, reg); node.clone() }; self.save(listener, &node).await?; Ok(()) } pub async fn set_instance( &self, listener: &Url, title: String, description: String, version: String, reg: bool, requires_approval: bool, ) -> Result<(), MyError> { if !self.listeners.read().await.contains(listener) { let mut nodes = self.nodes.write().await; nodes.remove(listener); return Ok(()); } let node = { let mut write_guard = self.nodes.write().await; let node = write_guard .entry(listener.clone()) .or_insert_with(|| Node::new(listener.clone())); node.set_instance(title, description, version, reg, requires_approval); node.clone() }; self.save(listener, &node).await?; Ok(()) } pub async fn set_contact( &self, listener: &Url, username: String, display_name: String, url: Url, avatar: Url, ) -> Result<(), MyError> { if !self.listeners.read().await.contains(listener) { let mut nodes = self.nodes.write().await; nodes.remove(listener); return Ok(()); } let node = { let mut write_guard = self.nodes.write().await; let node = write_guard .entry(listener.clone()) .or_insert_with(|| Node::new(listener.clone())); node.set_contact(username, display_name, url, avatar); node.clone() }; self.save(listener, &node).await?; Ok(()) } pub async fn save(&self, listener: &Url, node: &Node) -> Result<(), MyError> { let row_opt = self .db .pool() .get() .await? .query_opt( "SELECT id FROM listeners WHERE actor_id = $1::TEXT LIMIT 1;", &[&listener.as_str()], ) .await?; let id: Uuid = if let Some(row) = row_opt { row.try_get(0)? } else { return Err(MyError::NotSubscribed(listener.as_str().to_owned())); }; self.db .pool() .get() .await? .execute( "INSERT INTO nodes ( listener_id, nodeinfo, instance, contact, created_at, updated_at ) VALUES ( $1::UUID, $2::JSONB, $3::JSONB, $4::JSONB, 'now', 'now' ) ON CONFLICT (listener_id) DO UPDATE SET nodeinfo = $2::JSONB, instance = $3::JSONB, contact = $4::JSONB;", &[ &id, &Json(&node.info), &Json(&node.instance), &Json(&node.contact), ], ) .await?; Ok(()) } } #[derive(Clone, Debug)] pub struct Node { pub base: Url, pub info: Option, pub instance: Option, pub contact: Option, } impl Node { pub fn new(mut url: Url) -> Self { url.set_fragment(None); url.set_query(None); url.set_path(""); Node { base: url, info: None, instance: None, contact: None, } } fn set_info(&mut self, software: String, version: String, reg: bool) -> &mut Self { self.info = Some(Info { software, version, reg, updated: SystemTime::now(), }); self } fn set_instance( &mut self, title: String, description: String, version: String, reg: bool, requires_approval: bool, ) -> &mut Self { self.instance = Some(Instance { title, description, version, reg, requires_approval, updated: SystemTime::now(), }); self } fn set_contact( &mut self, username: String, display_name: String, url: Url, avatar: Url, ) -> &mut Self { self.contact = Some(Contact { username, display_name, url: url.into(), avatar: avatar.into(), updated: SystemTime::now(), }); self } } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct Info { pub software: String, pub version: String, pub reg: bool, pub updated: SystemTime, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct Instance { pub title: String, pub description: String, pub version: String, pub reg: bool, pub requires_approval: bool, pub updated: SystemTime, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct Contact { pub username: String, pub display_name: String, pub url: Url, pub avatar: Url, pub updated: SystemTime, } static TEN_MINUTES: Duration = Duration::from_secs(60 * 10); impl Info { pub fn outdated(&self) -> bool { self.updated + TEN_MINUTES < SystemTime::now() } } impl Instance { pub fn outdated(&self) -> bool { self.updated + TEN_MINUTES < SystemTime::now() } } impl Contact { pub fn outdated(&self) -> bool { self.updated + TEN_MINUTES < SystemTime::now() } }