use chrono::{DateTime, Utc}; use sled::Db; use std::fmt; use uuid::Uuid; mod comment; mod file; mod profile; mod react; mod report; mod server; mod submission; mod term_search; pub mod view; pub use comment::{Comment, CommentChanges, Store as CommentStore}; pub use file::Store as FileStore; pub use profile::{OwnerSource, Profile, ProfileChanges, Store as ProfileStore}; pub use react::Store as ReactStore; pub use report::Store as ReportStore; pub use server::{Server, ServerChanges, Store as ServerStore}; pub use submission::{Store as SubmissionStore, Submission, SubmissionChanges, Visibility}; pub use term_search::TermSearch; pub use view::Store as ViewStore; #[derive(Clone, Debug, thiserror::Error)] pub enum ValidationErrorKind { #[error("Provided string is too long, must be shorter than {maximum}")] TooLong { maximum: usize, proposed: usize }, #[error("Provided string must be present")] Empty, #[error("Provided changes {proposed} are older than current data {current}")] Outdated { current: DateTime, proposed: DateTime, }, #[error("Tried to save with a file that doesn't exist: {0}")] MissingFile(Uuid), } #[derive(Clone, Debug, thiserror::Error)] #[error("Failed to validate {field}: {kind}")] pub struct ValidationError { field: String, kind: ValidationErrorKind, } #[derive(Clone)] pub struct Store { pub profiles: profile::Store, pub files: file::Store, pub submissions: submission::Store, pub comments: comment::Store, pub reacts: react::Store, pub reports: report::Store, pub servers: server::Store, pub view: view::Store, } impl Store { pub fn build(max_handle_length: usize, db: &Db) -> Result { Ok(Store { profiles: profile::Store::build(max_handle_length, db)?, files: file::Store::build(db)?, submissions: submission::Store::build(db)?, comments: comment::Store::build(db)?, reacts: react::Store::build(db)?, reports: report::Store::build(db)?, servers: server::Store::build(db)?, view: view::Store::build(db)?, }) } pub(crate) fn is_domain_blocked(&self, domain: &str) -> Result { if let Some(id) = self.servers.by_domain(domain)? { return self.is_blocked(id); } Ok(false) } pub(crate) fn is_domain_federated(&self, domain: &str) -> Result { if let Some(id) = self.servers.by_domain(domain)? { return self.is_federated(id); } Ok(false) } pub(crate) fn is_blocked(&self, id: Uuid) -> Result { if let Some(self_id) = self.servers.get_self()? { let forward = self.view.server_blocks.by_forward(id, self_id)?.is_some(); let backward = self.view.server_blocks.by_forward(self_id, id)?.is_some(); return Ok(forward || backward); } Ok(true) } pub(crate) fn is_federated(&self, id: Uuid) -> Result { if let Some(self_id) = self.servers.get_self()? { let forward = self.view.server_follows.by_forward(id, self_id)?.is_some(); let backward = self.view.server_follows.by_forward(self_id, id)?.is_some(); return Ok(forward || backward); } Ok(false) } pub(crate) fn followers_for<'a>( &'a self, profile_id: Uuid, ) -> impl DoubleEndedIterator + 'a { self.view.follows.forward_iter(profile_id) } pub(crate) fn federated_servers<'a>(&'a self) -> impl DoubleEndedIterator + 'a { self.servers .get_self() .ok() .and_then(|opt| opt) .into_iter() .flat_map(move |server_id| { let iter1 = self.view.server_follows.forward_iter(server_id); let iter2 = self.view.server_follows.backward_iter(server_id); iter1.chain(iter2) }) } } #[derive(Clone, Debug)] pub struct Report { id: Uuid, reporter: Uuid, reporter_kind: ReporterKind, reported_item: Uuid, kind: ReportKind, note: Option, resolved: Option>, resolution: Option, forwarded: Option>, } impl Report { pub fn id(&self) -> Uuid { self.id } pub fn note(&self) -> Option<&str> { self.note.as_deref() } pub fn reporter_profile(&self) -> Option { if matches!(self.reporter_kind, ReporterKind::Profile) { Some(self.reporter) } else { None } } pub fn item(&self) -> Uuid { self.reported_item } pub fn reporter_server(&self) -> Option { if matches!(self.reporter_kind, ReporterKind::Server) { Some(self.reporter) } else { None } } pub fn kind(&self) -> ReportKind { self.kind } pub fn profile(&self) -> Option { if matches!(self.kind, ReportKind::Profile) { Some(self.reported_item) } else { None } } pub fn submission(&self) -> Option { if matches!(self.kind, ReportKind::Submission) { Some(self.reported_item) } else { None } } pub fn comment(&self) -> Option { if matches!(self.kind, ReportKind::Comment) { Some(self.reported_item) } else { None } } pub fn post(&self) -> Option { if matches!(self.kind, ReportKind::Post) { Some(self.reported_item) } else { None } } pub fn resolved(&self) -> Option> { self.resolved } pub fn resolution(&self) -> Option<&str> { self.resolution.as_deref() } pub fn forwarded(&self) -> Option> { self.forwarded } } #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub enum ReportKind { Post, Comment, Submission, Profile, } #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub(crate) enum ReporterKind { Profile, Server, } #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] enum ReportState { Open, Resolved, } #[derive(Clone, Debug)] pub struct PictRsFile { key: String, token: String, width: usize, height: usize, media_type: mime::Mime, } impl PictRsFile { pub(crate) fn new( key: &str, token: &str, width: usize, height: usize, media_type: mime::Mime, ) -> Self { PictRsFile { key: key.to_owned(), token: token.to_owned(), width, height, media_type, } } pub fn key(&self) -> &str { &self.key } pub(crate) fn token(&self) -> &str { &self.token } pub(crate) fn media_type(&self) -> &mime::Mime { &self.media_type } } #[derive(Clone, Debug)] pub enum FileSource { PictRs(PictRsFile), } #[derive(Clone, Debug)] pub struct File { id: Uuid, source: FileSource, } impl File { pub fn id(&self) -> Uuid { self.id } pub fn source(&self) -> &FileSource { &self.source } pub fn pictrs(&self) -> Option<&PictRsFile> { let FileSource::PictRs(ref file) = self.source; Some(file) } pub fn pictrs_key(&self) -> Option<&str> { self.pictrs().map(|p| p.key()) } } #[derive(Clone, Debug)] pub enum React { Submission(SubmissionReact), Reply(ReplyReact), } impl React { pub(crate) fn id(&self) -> Uuid { match self { React::Submission(SubmissionReact { id, .. }) => *id, React::Reply(ReplyReact { id, .. }) => *id, } } pub(crate) fn submission_id(&self) -> Uuid { match self { React::Submission(SubmissionReact { submission_id, .. }) => *submission_id, React::Reply(ReplyReact { submission_id, .. }) => *submission_id, } } pub(crate) fn profile_id(&self) -> Uuid { match self { React::Submission(SubmissionReact { profile_id, .. }) => *profile_id, React::Reply(ReplyReact { profile_id, .. }) => *profile_id, } } pub(crate) fn comment_id(&self) -> Option { match self { React::Reply(ReplyReact { comment_id, .. }) => Some(*comment_id), _ => None, } } pub(crate) fn react(&self) -> &str { match self { React::Submission(SubmissionReact { react, .. }) => &react, React::Reply(ReplyReact { react, .. }) => &react, } } pub(crate) fn published(&self) -> DateTime { match self { React::Submission(SubmissionReact { published, .. }) => *published, React::Reply(ReplyReact { published, .. }) => *published, } } } #[derive(Clone, Debug)] pub struct SubmissionReact { id: Uuid, submission_id: Uuid, profile_id: Uuid, react: String, published: DateTime, } #[derive(Clone, Debug)] pub struct ReplyReact { id: Uuid, submission_id: Uuid, profile_id: Uuid, comment_id: Uuid, react: String, published: DateTime, } #[derive(Clone, Debug)] pub struct Undo(pub T); #[derive(Debug, thiserror::Error)] pub enum StoreError { #[error("{0}")] Json(#[from] serde_json::Error), #[error("{0}")] Sled(#[from] sled::Error), #[error("{0}")] Transaction(#[from] sled::transaction::TransactionError), #[error("Profile changed during modification")] DoubleStore, #[error("Cannot update missing item")] Missing, #[error("Provided value is too long")] TooLong, #[error("The updated data is older than our stored data")] Outdated, #[error("Provided value must not be empty")] Empty, } fn modify( tree: &sled::transaction::TransactionalTree, key: &str, f: impl Fn(&mut T), ) -> Result<(), sled::transaction::ConflictableTransactionError> where T: serde::Serialize + Default, for<'de> T: serde::Deserialize<'de>, { let mut item = match tree.get(key.as_bytes())? { Some(ivec) => { let item: T = serde_json::from_slice(&ivec).expect("JSON is valid"); item } None => T::default(), }; (f)(&mut item); let item_vec = serde_json::to_vec(&item).expect("JSON is valid"); tree.insert(key.as_bytes(), item_vec.as_slice())?; Ok(()) } fn count( tree: &sled::transaction::TransactionalTree, key: &str, f: impl Fn(u64) -> u64, ) -> Result { let count = match tree.get(key.as_bytes())? { Some(ivec) => { let s = String::from_utf8_lossy(&ivec); let count: u64 = s.parse().unwrap_or(0); count } None => 0, }; let count = (f)(count); let count_string = count.to_string(); tree.insert(key.as_bytes(), count_string.as_bytes())?; Ok(count) } impl fmt::Display for OwnerSource { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { OwnerSource::Local(id) => write!(f, "local:{}", id), OwnerSource::Remote(s) => write!(f, "remote:{}", s), } } } impl fmt::Debug for Store { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Store") .field("profiles", &"ProfileStore") .field("files", &"FileStore") .field("submissions", &"SubmissionStore") .field("comments", &"CommentStore") .field("reacts", &"ReactStore") .field("view", &"ViewStore") .finish() } }