use chrono::{DateTime, Utc}; use sled::Db; use std::fmt; use uuid::Uuid; mod comment; mod file; mod profile; mod react; mod submission; pub mod view; #[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 view: view::Store, } impl Store { pub fn build(db: &Db) -> Result { Ok(Store { profiles: profile::Store::build(db)?, files: file::Store::build(db)?, submissions: submission::Store::build(db)?, comments: comment::Store::build(db)?, reacts: react::Store::build(db)?, view: view::Store::build(db)?, }) } } #[derive(Clone, Debug)] pub enum OwnerSource { Local(Uuid), Remote(String), } impl OwnerSource { pub fn is_local(&self) -> bool { matches!(self, OwnerSource::Local(_)) } } #[derive(Clone, Debug)] pub struct Profile { id: Uuid, owner_source: OwnerSource, handle: String, domain: String, display_name: Option, description: Option, icon: Option, banner: Option, published: DateTime, login_required: bool, } impl Profile { pub fn update(&self) -> ProfileChanges { ProfileChanges { id: self.id, display_name: None, description: None, login_required: None, } } pub fn update_images(&self) -> ProfileImageChanges { ProfileImageChanges { id: self.id, icon: None, banner: None, } } pub fn id(&self) -> Uuid { self.id } pub(crate) fn owner_source(&self) -> &OwnerSource { &self.owner_source } pub fn local_owner(&self) -> Option { match self.owner_source { OwnerSource::Local(id) => Some(id), _ => None, } } pub fn handle(&self) -> &str { &self.handle } pub fn domain(&self) -> &str { &self.domain } pub fn display_name(&self) -> Option<&str> { self.display_name.as_ref().map(|dn| dn.as_str()) } pub fn description(&self) -> Option<&str> { self.description.as_ref().map(|d| d.as_str()) } pub fn icon(&self) -> Option { self.icon } pub fn banner(&self) -> Option { self.banner } pub fn published(&self) -> DateTime { self.published } pub fn login_required(&self) -> bool { self.login_required } } #[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 } } #[derive(Clone, Debug)] pub enum FileSource { PictRs(PictRsFile), } #[derive(Clone, Debug)] pub struct File { id: Uuid, source: FileSource, } impl File { pub(crate) fn id(&self) -> Uuid { self.id } pub fn source(&self) -> &FileSource { &self.source } } #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] pub enum Visibility { Public, Unlisted, Followers, } #[derive(Clone, Debug)] pub struct Submission { id: Uuid, profile_id: Uuid, title: String, description: Option, files: Vec, published: Option>, visibility: Visibility, } impl Submission { pub fn update(&self) -> SubmissionChanges { SubmissionChanges { id: self.id, title: None, description: None, published: self.published, } } pub fn update_files(&self) -> SubmissionFileChanges { SubmissionFileChanges { id: self.id, files: self.files.clone(), changed: false, } } pub(crate) fn id(&self) -> Uuid { self.id } pub(crate) fn profile_id(&self) -> Uuid { self.profile_id } pub(crate) fn title(&self) -> &str { &self.title } pub(crate) fn description(&self) -> Option<&str> { self.description.as_ref().map(|d| d.as_str()) } pub(crate) fn files(&self) -> &[Uuid] { &self.files } pub(crate) fn published(&self) -> Option> { self.published } pub(crate) fn visibility(&self) -> Visibility { self.visibility } } #[derive(Clone, Debug)] pub enum Comment { Submission(SubmissionComment), Reply(ReplyComment), } impl Comment { pub(crate) fn id(&self) -> Uuid { match self { Comment::Reply(ReplyComment { id, .. }) => *id, Comment::Submission(SubmissionComment { id, .. }) => *id, } } pub(crate) fn submission_id(&self) -> Uuid { match self { Comment::Reply(ReplyComment { submission_id, .. }) => *submission_id, Comment::Submission(SubmissionComment { submission_id, .. }) => *submission_id, } } pub(crate) fn profile_id(&self) -> Uuid { match self { Comment::Reply(ReplyComment { profile_id, .. }) => *profile_id, Comment::Submission(SubmissionComment { profile_id, .. }) => *profile_id, } } pub(crate) fn comment_id(&self) -> Option { match self { Comment::Reply(ReplyComment { comment_id, .. }) => Some(*comment_id), _ => None, } } pub(crate) fn body(&self) -> &str { match self { Comment::Reply(ReplyComment { body, .. }) => &body, Comment::Submission(SubmissionComment { body, .. }) => &body, } } pub(crate) fn published(&self) -> DateTime { match self { Comment::Reply(ReplyComment { published, .. }) => *published, Comment::Submission(SubmissionComment { published, .. }) => *published, } } pub(crate) fn update(&self) -> CommentChanges { match self { Comment::Reply(rc) => rc.update(), Comment::Submission(sc) => sc.update(), } } } #[derive(Clone, Debug)] pub struct SubmissionComment { id: Uuid, submission_id: Uuid, profile_id: Uuid, body: String, published: DateTime, } impl SubmissionComment { fn update(&self) -> CommentChanges { CommentChanges { id: self.id, body: None, } } } #[derive(Clone, Debug)] pub struct ReplyComment { id: Uuid, submission_id: Uuid, profile_id: Uuid, comment_id: Uuid, body: String, published: DateTime, } impl ReplyComment { fn update(&self) -> CommentChanges { CommentChanges { id: self.id, body: None, } } } #[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)] pub struct ProfileChanges { id: Uuid, display_name: Option, description: Option, login_required: Option, } impl ProfileChanges { pub fn display_name(&mut self, display_name: &str) -> &mut Self { self.display_name = Some(display_name.to_owned()); self } pub fn description(&mut self, description: &str) -> &mut Self { self.description = Some(description.to_owned()); self } pub fn login_required(&mut self, required: bool) -> &mut Self { self.login_required = Some(required); self } pub(crate) fn any_changes(&self) -> bool { self.display_name.is_some() || self.description.is_some() || self.login_required.is_some() } } #[derive(Debug)] pub struct ProfileImageChanges { id: Uuid, icon: Option, banner: Option, } impl ProfileImageChanges { pub fn icon(&mut self, file: &File) -> &mut Self { self.icon = Some(file.id); self } pub fn banner(&mut self, file: &File) -> &mut Self { self.banner = Some(file.id); self } pub(crate) fn any_changes(&self) -> bool { self.icon.is_some() || self.banner.is_some() } } #[derive(Debug)] pub struct SubmissionChanges { id: Uuid, title: Option, description: Option, published: Option>, } impl SubmissionChanges { pub fn title(&mut self, title: &str) -> &mut Self { self.title = Some(title.to_owned()); self } pub fn description(&mut self, description: &str) -> &mut Self { self.description = Some(description.to_owned()); self } pub fn publish(&mut self, time: Option>) -> &mut Self { self.published = time.or_else(|| Some(Utc::now())); self } pub(crate) fn any_changes(&self) -> bool { self.title.is_some() || self.description.is_some() || self.published.is_some() } } #[derive(Debug)] pub struct SubmissionFileChanges { id: Uuid, files: Vec, changed: bool, } impl SubmissionFileChanges { pub fn add_file(&mut self, file: &File) -> &mut Self { self.files.push(file.id); self.changed = true; self } pub fn delete_file(&mut self, file: &File) -> &mut Self { self.files.retain(|id| *id != file.id); self.changed = true; self } pub(crate) fn any_changes(&self) -> bool { self.changed } } #[derive(Debug)] pub struct CommentChanges { id: Uuid, body: Option, } impl CommentChanges { pub fn body(&mut self, body: &str) -> &mut CommentChanges { self.body = Some(body.to_owned()); self } pub(crate) fn any_changes(&self) -> bool { self.body.is_some() } } #[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, } 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<(), sled::transaction::ConflictableTransactionError> { let count = match tree.get(key.as_bytes())? { Some(ivec) => { let s = String::from_utf8_lossy(&ivec); let count: u64 = s.parse().expect("Count is valid"); count } None => 0, }; let count = (f)(count).to_string(); tree.insert(key.as_bytes(), count.as_bytes())?; Ok(()) } 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() } }