use crate::{ store::{StoreError, ValidationError, ValidationErrorKind}, State, }; use chrono::{DateTime, Utc}; use hyaenidae_content::{bbcode, html, strip}; use sled::{Db, Transactional, Tree}; use uuid::Uuid; #[derive(Clone, Debug)] pub struct Server { id: Uuid, domain: String, created: DateTime, title: Option, title_source: Option, description: Option, description_source: Option, published: Option>, updated: Option>, } #[derive(Debug)] pub enum ServerChangesKind { Create { domain: String }, Update { id: Uuid }, } #[derive(Debug)] pub struct ServerChanges<'a> { state: &'a State, kind: ServerChangesKind, title: Option, title_source: Option, description: Option, description_source: Option, published: Option>, updated: Option>, errors: Vec, } #[derive(Clone)] pub struct Store { created_id: Tree, domain_id: Tree, id_domain: Tree, id_created: Tree, id_title: Tree, id_title_source: Tree, id_description: Tree, id_description_source: Tree, id_published: Tree, id_updated: Tree, self_server: Tree, } impl Server { pub fn id(&self) -> Uuid { self.id } pub fn domain(&self) -> &str { &self.domain } pub fn title(&self) -> Option<&str> { self.title.as_deref() } pub fn title_source(&self) -> Option<&str> { self.title_source.as_deref() } pub fn description(&self) -> Option<&str> { self.description.as_deref() } pub fn description_source(&self) -> Option<&str> { self.description_source.as_deref() } pub fn created(&self) -> DateTime { self.created } pub(crate) fn updated(&self) -> Option> { self.updated } pub(crate) fn changes<'a>(&self, state: &'a State) -> ServerChanges<'a> { ServerChanges { state, kind: ServerChangesKind::Update { id: self.id }, title: None, title_source: None, description: None, description_source: None, published: self.published, updated: self.updated, errors: vec![], } } } impl<'a> ServerChanges<'a> { fn new(state: &'a State, domain: String) -> Self { ServerChanges { state, kind: ServerChangesKind::Create { domain }, title: None, title_source: None, description: None, description_source: None, published: None, updated: None, errors: vec![], } } pub(crate) fn title(&mut self, title: &str) -> &mut Self { let title = strip(title.trim()); if title.len() > self.state.content_config.max_server_title_length { self.errors.push(ValidationError { field: String::from("title"), kind: ValidationErrorKind::TooLong { maximum: self.state.content_config.max_server_title_length, proposed: title.len(), }, }); } else if title.is_empty() { self.errors.push(ValidationError { field: String::from("title"), kind: ValidationErrorKind::Empty, }); } else { self.title = Some(title); } self } pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self { self.title_source = Some(title_source.to_owned()); self.title(title_source) } pub(crate) fn description(&mut self, description: &str) -> &mut Self { let description = html(description.trim()); if description.len() > self.state.content_config.max_server_body_length { self.errors.push(ValidationError { field: String::from("description"), kind: ValidationErrorKind::TooLong { maximum: self.state.content_config.max_server_body_length, proposed: description.len(), }, }); } else if description.is_empty() { self.errors.push(ValidationError { field: String::from("description"), kind: ValidationErrorKind::Empty, }); } else { self.description = Some(description); } self } pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self { self.description_source = Some(description_source.to_owned()); self.description(&bbcode(description_source, |v| self.state.map_nodeview(v))) } pub(crate) fn published(&mut self, published: DateTime) -> &mut Self { if self.published.is_none() { self.published = Some(published); } self } pub(crate) fn updated(&mut self, updated: DateTime) -> &mut Self { if let Some(published) = self.published { if self.updated.unwrap_or(published) > updated { self.errors.push(ValidationError { field: String::from("updated"), kind: ValidationErrorKind::Outdated { current: self.updated.unwrap_or(published), proposed: updated, }, }); } else { self.updated = Some(updated); } } self } pub(crate) fn any_changes(&self) -> bool { self.title.is_some() || self.description.is_some() || self.updated.is_some() } pub(crate) fn save(self) -> Result>, StoreError> { if self.errors.is_empty() { self.state.store.servers.save(&self).map(Ok) } else { Ok(Err(self.errors)) } } } impl Store { pub(super) fn build(db: &Db) -> Result { let created_id = db.open_tree("/profiles/servers/created_id")?; let domain_id = db.open_tree("/profiles/servers/domain_id")?; let id_domain = db.open_tree("/profiles/servers/id_domain")?; let id_created = db.open_tree("/profiles/servers/id_created")?; let id_title = db.open_tree("/profiles/servers/id_title")?; let id_title_source = db.open_tree("/profiles/servers/id_title_source")?; let id_description = db.open_tree("/profiles/servers/id_description")?; let id_description_source = db.open_tree("/profiles/servers/id_description_source")?; let id_published = db.open_tree("/profiles/servers/id_published")?; let id_updated = db.open_tree("/profiles/servers/id_updated")?; let self_server = db.open_tree("/profiles/servers/self_server")?; Ok(Store { created_id, domain_id, id_domain, id_created, id_title, id_title_source, id_description, id_description_source, id_published, id_updated, self_server, }) } pub(crate) fn self_exists(&self) -> bool { self.self_server.iter().next().is_some() } pub(crate) fn set_self(&self, id: Uuid) -> Result<(), StoreError> { self.self_server.insert("self", id.as_bytes())?; Ok(()) } pub(crate) fn is_self(&self, id: Uuid) -> Result { Ok(self .self_server .get("self")? .and_then(uuid_from_ivec) .map(|self_id| self_id == id) .unwrap_or(false)) } pub(crate) fn create<'a>(&self, state: &'a State, domain: String) -> ServerChanges<'a> { ServerChanges::new(state, domain) } pub(crate) fn save(&self, changes: &ServerChanges) -> Result { match &changes.kind { ServerChangesKind::Create { domain } => { let id = self.do_create(domain, Utc::now())?; self.do_update(id, changes) } ServerChangesKind::Update { id } => self.do_update(*id, changes), } } fn do_create(&self, domain: &str, created: DateTime) -> Result { let mut id; while { id = Uuid::new_v4(); self.id_domain .compare_and_swap( id.as_bytes(), None as Option<&[u8]>, Some(domain.as_bytes()), )? .is_err() } {} let domain_clone = domain.to_owned(); let res = [&self.created_id, &self.domain_id, &self.id_created].transaction(move |trees| { let created_id = &trees[0]; let domain_id = &trees[1]; let id_created = &trees[2]; created_id.insert(created.to_rfc3339().as_bytes(), id.as_bytes())?; domain_id.insert(domain_clone.as_bytes(), id.as_bytes())?; id_created.insert(id.as_bytes(), created.to_rfc3339().as_bytes())?; Ok(()) }); if let Err(e) = res { self.id_domain.remove(id.as_bytes())?; return Err(e.into()); } Ok(id) } fn do_update(&self, id: Uuid, changes: &ServerChanges) -> Result { let domain = match self.id_domain.get(id.as_bytes())?.map(string_from_ivec) { Some(domain) => domain, None => return Err(StoreError::Missing), }; let created = match self.id_created.get(id.as_bytes())?.and_then(date_from_ivec) { Some(date) => date, None => return Err(StoreError::Missing), }; if let Some(title) = &changes.title { self.id_title.insert(id.as_bytes(), title.as_bytes())?; } if let Some(title_source) = &changes.title_source { self.id_title_source .insert(id.as_bytes(), title_source.as_bytes())?; } if let Some(description) = &changes.description { self.id_description .insert(id.as_bytes(), description.as_bytes())?; } if let Some(description_source) = &changes.description_source { self.id_description_source .insert(id.as_bytes(), description_source.as_bytes())?; } if let Some(published) = changes.published { self.id_published .insert(id.as_bytes(), published.to_rfc3339().as_bytes())?; } if let Some(updated) = changes.updated { self.id_updated .insert(id.as_bytes(), updated.to_rfc3339().as_bytes())?; } Ok(Server { id, domain, created, title: changes.title.clone(), title_source: changes.title_source.clone(), description: changes.description.clone(), description_source: changes.description_source.clone(), published: changes.published, updated: changes.updated, }) } pub fn get_self(&self) -> Result, StoreError> { Ok(self.self_server.get("self")?.and_then(uuid_from_ivec)) } pub fn by_id(&self, id: Uuid) -> Result, StoreError> { let domain = self.id_domain.get(id.as_bytes())?.map(string_from_ivec); let created = self.id_created.get(id.as_bytes())?.and_then(date_from_ivec); let title = self.id_title.get(id.as_bytes())?.map(string_from_ivec); let title_source = self .id_title_source .get(id.as_bytes())? .map(string_from_ivec); let description = self .id_description .get(id.as_bytes())? .map(string_from_ivec); let description_source = self .id_description_source .get(id.as_bytes())? .map(string_from_ivec); let updated = self.id_updated.get(id.as_bytes())?.and_then(date_from_ivec); let published = self .id_published .get(id.as_bytes())? .and_then(date_from_ivec); Ok(domain.and_then(|domain| { created.map(|created| Server { id, domain, title, title_source, description, description_source, created, updated, published, }) })) } pub fn by_domain(&self, domain: &str) -> Result, StoreError> { Ok(self.domain_id.get(domain)?.and_then(uuid_from_ivec)) } pub fn known(&self) -> impl DoubleEndedIterator { self.created_id .iter() .values() .filter_map(|res| res.ok()) .filter_map(uuid_from_ivec) } } fn string_from_ivec(ivec: sled::IVec) -> String { String::from_utf8_lossy(&ivec).to_string() } fn uuid_from_ivec(ivec: sled::IVec) -> Option { Uuid::from_slice(&ivec).ok() } fn date_from_ivec(ivec: sled::IVec) -> Option> { String::from_utf8_lossy(&ivec).parse().ok() }