From 597bbe94b9040c86a6d717b528fb06db28140348 Mon Sep 17 00:00:00 2001 From: asonix Date: Mon, 1 Feb 2021 21:56:41 -0600 Subject: [PATCH] Profiles: Add more audience options to submissions - In addition to Followers/Unlisted/Public, now there's two toggles to limit to logged-in users, or to limit to the current server Adding federated login-required for submissions is a TODO item. - Don't accept federated updates older than the submission's updated field --- profiles/src/apub/actions/apub/note.rs | 8 + profiles/src/apub/actions/comment.rs | 87 ++++---- profiles/src/apub/actions/mod.rs | 51 ++++- profiles/src/apub/actions/submission.rs | 47 ++++- profiles/src/apub/results/comment.rs | 48 ++++- profiles/src/apub/results/mod.rs | 16 ++ profiles/src/apub/results/submission.rs | 53 ++++- profiles/src/store/mod.rs | 182 +--------------- profiles/src/store/submission.rs | 268 +++++++++++++++++++++++- 9 files changed, 525 insertions(+), 235 deletions(-) diff --git a/profiles/src/apub/actions/apub/note.rs b/profiles/src/apub/actions/apub/note.rs index 1e56fd0..a5df527 100644 --- a/profiles/src/apub/actions/apub/note.rs +++ b/profiles/src/apub/actions/apub/note.rs @@ -167,6 +167,7 @@ pub(super) fn note( return Ok(Err(RecoverableError::MissingImages(missing_files))); } + log::warn!("logged_in_only not implmented"); return Ok(Ok(Box::new(CreateSubmission { note_apub_id: Some(note_id.to_owned()), profile_id, @@ -175,6 +176,8 @@ pub(super) fn note( published: Some(published.into()), files: existing_files, visibility: find_visibility(note), + local_only: false, + logged_in_only: false, }))); } @@ -368,6 +371,7 @@ pub(super) fn update_note( .and_then(|c| c.as_single_xsd_string()) .map(|s| s.to_owned()); let published = note.published().req("published")?; + let updated = note.updated().req("updated")?; let mut missing_files = vec![]; let mut existing_files = vec![]; @@ -408,6 +412,7 @@ pub(super) fn update_note( return Ok(Err(RecoverableError::MissingImages(missing_files))); } + log::warn!("logged_in_only not implmented"); Ok(Ok(Box::new(UpdateSubmission { submission_id, title, @@ -416,9 +421,12 @@ pub(super) fn update_note( description_source: None, visibility: Some(find_visibility(note)), published: Some(published.into()), + updated: Some(updated.into()), removed_files: None, new_files: None, only_files: Some(existing_files), + local_only: Some(false), + logged_in_only: Some(false), }))) } diff --git a/profiles/src/apub/actions/comment.rs b/profiles/src/apub/actions/comment.rs index 973f560..5192d0d 100644 --- a/profiles/src/apub/actions/comment.rs +++ b/profiles/src/apub/actions/comment.rs @@ -5,12 +5,13 @@ use crate::{ impl Action for CreateComment { fn perform(&self, ctx: &Context) -> Result>, Error> { - let submissioner_id = ctx + let submission = ctx .store .submissions .by_id(self.submission_id)? - .req("submission by id")? - .profile_id(); + .req("submission by id")?; + + let submissioner_id = submission.profile_id(); ctx.check_block(submissioner_id, self.profile_id)?; @@ -69,24 +70,30 @@ impl Action for CreateComment { } } - if ctx.is_local(self.profile_id)? && ctx.is_local(submissioner_id)? { - return Ok(Some(Box::new( - crate::apub::results::AnnounceCommentCreated { - submissioner_id, + if !submission.is_local_only() { + if ctx.is_local(self.profile_id)? && ctx.is_local(submissioner_id)? { + return Ok(Some(Box::new( + crate::apub::results::AnnounceCommentCreated { + submissioner_id, + comment_id: comment.id(), + }, + ))); + } else if ctx.is_local(self.profile_id)? { + return Ok(Some(Box::new(crate::apub::results::CommentCreated { comment_id: comment.id(), - }, - ))); - } else if ctx.is_local(self.profile_id)? { - return Ok(Some(Box::new(crate::apub::results::CommentCreated { + }))); + } else if ctx.is_local(submissioner_id)? { + if let Some(apub_id) = &self.note_apub_id { + return Ok(Some(Box::new(crate::apub::results::RemoteCommentCreated { + note_apub_id: apub_id.clone(), + submissioner_id, + }))); + } + } + } else { + return Ok(Some(Box::new(crate::apub::results::LocalCommentCreated { comment_id: comment.id(), }))); - } else if ctx.is_local(submissioner_id)? { - if let Some(apub_id) = &self.note_apub_id { - return Ok(Some(Box::new(crate::apub::results::RemoteCommentCreated { - note_apub_id: apub_id.clone(), - submissioner_id, - }))); - } } Ok(None) @@ -118,31 +125,39 @@ impl Action for UpdateComment { comment }; - let submissioner_id = ctx + let submission = ctx .store .submissions .by_id(comment.submission_id())? - .req("submission by id")? - .profile_id(); + .req("submission by id")?; - if ctx.is_local(comment.profile_id())? && ctx.is_local(submissioner_id)? { - return Ok(Some(Box::new( - crate::apub::results::AnnounceCommentUpdated { - submissioner_id, + let submissioner_id = submission.profile_id(); + let is_local_only = submission.is_local_only(); + + if !is_local_only { + if ctx.is_local(comment.profile_id())? && ctx.is_local(submissioner_id)? { + return Ok(Some(Box::new( + crate::apub::results::AnnounceCommentUpdated { + submissioner_id, + comment_id: comment.id(), + }, + ))); + } else if ctx.is_local(comment.profile_id())? { + return Ok(Some(Box::new(crate::apub::results::CommentUpdated { comment_id: comment.id(), - }, - ))); - } else if ctx.is_local(comment.profile_id())? { - return Ok(Some(Box::new(crate::apub::results::CommentUpdated { + }))); + } else if ctx.is_local(submissioner_id)? { + if let Some(apub_id) = &self.update_apub_id { + return Ok(Some(Box::new(crate::apub::results::RemoteCommentUpdated { + update_apub_id: apub_id.to_owned(), + submissioner_id, + }))); + } + } + } else { + return Ok(Some(Box::new(crate::apub::results::LocalCommentUpdated { comment_id: comment.id(), }))); - } else if ctx.is_local(submissioner_id)? { - if let Some(apub_id) = &self.update_apub_id { - return Ok(Some(Box::new(crate::apub::results::RemoteCommentUpdated { - update_apub_id: apub_id.to_owned(), - submissioner_id, - }))); - } } Ok(None) diff --git a/profiles/src/apub/actions/mod.rs b/profiles/src/apub/actions/mod.rs index f53eff7..5b3e891 100644 --- a/profiles/src/apub/actions/mod.rs +++ b/profiles/src/apub/actions/mod.rs @@ -269,6 +269,8 @@ pub struct CreateSubmission { files: Vec, published: Option>, visibility: Visibility, + local_only: bool, + logged_in_only: bool, } impl CreateSubmission { @@ -281,6 +283,8 @@ impl CreateSubmission { files: vec![file_id], published: None, visibility: Visibility::Public, + local_only: false, + logged_in_only: false, } } } @@ -292,19 +296,17 @@ pub struct UpdateSubmission { description: Option, description_source: Option, visibility: Option, + local_only: Option, + logged_in_only: Option, published: Option>, + updated: Option>, removed_files: Option>, new_files: Option>, only_files: Option>, } impl UpdateSubmission { - pub fn from_text( - submission_id: Uuid, - title: String, - description: Option, - visibility: Option, - ) -> Self { + pub fn from_text(submission_id: Uuid, title: String, description: Option) -> Self { let title_source = if title.trim().is_empty() { None } else { @@ -325,8 +327,34 @@ impl UpdateSubmission { title_source, description: description_source.as_deref().map(|s| bbcode(s, |v| v)), description_source, - visibility, + visibility: None, + local_only: None, + logged_in_only: None, published: None, + updated: Some(Utc::now()), + removed_files: None, + new_files: None, + only_files: None, + } + } + + pub fn from_visibility( + submission_id: Uuid, + visibility: Visibility, + local_only: bool, + logged_in_only: bool, + ) -> Self { + UpdateSubmission { + submission_id, + title: None, + title_source: None, + description: None, + description_source: None, + visibility: Some(visibility), + local_only: Some(local_only), + logged_in_only: Some(logged_in_only), + published: None, + updated: Some(Utc::now()), removed_files: None, new_files: None, only_files: None, @@ -341,7 +369,10 @@ impl UpdateSubmission { description: None, description_source: None, visibility: None, + local_only: None, + logged_in_only: None, published: None, + updated: Some(Utc::now()), removed_files: None, new_files: Some(vec![file_id]), only_files: None, @@ -357,6 +388,9 @@ impl UpdateSubmission { description_source: None, visibility: None, published: None, + updated: Some(Utc::now()), + local_only: None, + logged_in_only: None, removed_files: Some(vec![file_id]), new_files: None, only_files: None, @@ -371,7 +405,10 @@ impl UpdateSubmission { description: None, description_source: None, visibility: None, + local_only: None, + logged_in_only: None, published: Some(Utc::now()), + updated: None, removed_files: None, new_files: None, only_files: None, diff --git a/profiles/src/apub/actions/submission.rs b/profiles/src/apub/actions/submission.rs index 132a985..c8c2b60 100644 --- a/profiles/src/apub/actions/submission.rs +++ b/profiles/src/apub/actions/submission.rs @@ -16,12 +16,17 @@ impl Action for CreateSubmission { let submission_id = submission.id(); let mut changes = submission.update(); + changes + .local_only(self.local_only) + .logged_in_only(self.logged_in_only); + if self.published.is_some() { - changes.publish(self.published); + changes.published(self.published); } if let Some(description) = &self.description { changes.description(&hyaenidae_content::html(description)); } + let submission = if changes.any_changes() { ctx.store.submissions.update(&changes)? } else { @@ -61,9 +66,15 @@ impl Action for CreateSubmission { } if ctx.is_local(submission.profile_id())? && submission.published().is_some() { - return Ok(Some(Box::new(crate::apub::results::SubmissionCreated { - submission_id, - }))); + if submission.is_local_only() { + return Ok(Some(Box::new( + crate::apub::results::LocalSubmissionCreated { submission_id }, + ))); + } else { + return Ok(Some(Box::new(crate::apub::results::SubmissionCreated { + submission_id, + }))); + } } if submission.published().is_none() { @@ -90,7 +101,10 @@ impl Action for UpdateSubmission { let mut changes = submission.update(); if let Some(published) = self.published { - changes.publish(Some(published)); + changes.published(Some(published)); + } + if let Some(updated) = self.updated { + changes.updated(updated); } if let Some(title) = &self.title { changes.title(&hyaenidae_content::html(title)); @@ -107,6 +121,12 @@ impl Action for UpdateSubmission { if let Some(visibility) = self.visibility { changes.visibility(visibility); } + if let Some(local_only) = self.local_only { + changes.local_only(local_only); + } + if let Some(logged_in_only) = self.logged_in_only { + changes.logged_in_only(logged_in_only); + } let submission = if changes.any_changes() { ctx.store.submissions.update(&changes)? } else { @@ -178,7 +198,7 @@ impl Action for UpdateSubmission { } } - if ctx.is_local(profile_id)? { + if !submission.is_local_only() && ctx.is_local(profile_id)? { if newly_published { return Ok(Some(Box::new(crate::apub::results::SubmissionCreated { submission_id, @@ -188,6 +208,16 @@ impl Action for UpdateSubmission { submission_id, }))); } + } else { + if newly_published { + return Ok(Some(Box::new( + crate::apub::results::LocalSubmissionCreated { submission_id }, + ))); + } else { + return Ok(Some(Box::new( + crate::apub::results::LocalSubmissionUpdated { submission_id }, + ))); + } } Ok(None) @@ -238,7 +268,10 @@ impl Action for DeleteSubmission { }); let profile_id = undo_submission.0.profile_id(); - if ctx.is_local(profile_id)? && undo_submission.0.published().is_some() { + if !undo_submission.0.is_local_only() + && ctx.is_local(profile_id)? + && undo_submission.0.published().is_some() + { return Ok(Some(Box::new(crate::apub::results::SubmissionDeleted { profile_id, submission_id, diff --git a/profiles/src/apub/results/comment.rs b/profiles/src/apub/results/comment.rs index df9f16b..c863ab6 100644 --- a/profiles/src/apub/results/comment.rs +++ b/profiles/src/apub/results/comment.rs @@ -1,7 +1,7 @@ use super::{ AnnounceCommentCreated, AnnounceCommentDeleted, AnnounceCommentUpdated, CommentCreated, - CommentDeleted, CommentUpdated, RemoteCommentCreated, RemoteCommentDeleted, - RemoteCommentUpdated, + CommentDeleted, CommentUpdated, LocalCommentCreated, LocalCommentUpdated, RemoteCommentCreated, + RemoteCommentDeleted, RemoteCommentUpdated, }; use crate::{store::Comment, Context, Error, OnBehalfOf, Outbound, Required}; use activitystreams::{ @@ -173,6 +173,28 @@ impl Outbound for CommentCreated { } } +impl Outbound for LocalCommentCreated { + fn id(&self) -> Option { + Some(self.comment_id) + } + + fn ready(&self) -> bool { + false + } + + fn behalf(&self, _: &Context) -> Result { + Err(Error::Invalid) + } + + fn inboxes(&self, _: &Context) -> Result, Error> { + Err(Error::Invalid) + } + + fn to_apub(&self, _: &Context) -> Result { + Err(Error::Invalid) + } +} + impl Outbound for AnnounceCommentCreated { fn id(&self) -> Option { Some(self.comment_id) @@ -300,6 +322,28 @@ impl Outbound for CommentUpdated { } } +impl Outbound for LocalCommentUpdated { + fn id(&self) -> Option { + Some(self.comment_id) + } + + fn ready(&self) -> bool { + false + } + + fn behalf(&self, _: &Context) -> Result { + Err(Error::Invalid) + } + + fn inboxes(&self, _: &Context) -> Result, Error> { + Err(Error::Invalid) + } + + fn to_apub(&self, _: &Context) -> Result { + Err(Error::Invalid) + } +} + impl Outbound for AnnounceCommentUpdated { fn id(&self) -> Option { Some(self.comment_id) diff --git a/profiles/src/apub/results/mod.rs b/profiles/src/apub/results/mod.rs index f6f41a4..95ffedd 100644 --- a/profiles/src/apub/results/mod.rs +++ b/profiles/src/apub/results/mod.rs @@ -39,6 +39,10 @@ pub(super) struct SubmissionCreated { pub(super) submission_id: Uuid, } +pub(super) struct LocalSubmissionCreated { + pub(super) submission_id: Uuid, +} + pub(super) struct UnpublishedSubmissionCreated { pub(super) submission_id: Uuid, } @@ -47,6 +51,10 @@ pub(super) struct SubmissionUpdated { pub(super) submission_id: Uuid, } +pub(super) struct LocalSubmissionUpdated { + pub(super) submission_id: Uuid, +} + pub(super) struct SubmissionDeleted { pub(super) profile_id: Uuid, pub(super) submission_id: Uuid, @@ -56,6 +64,10 @@ pub(super) struct CommentCreated { pub(super) comment_id: Uuid, } +pub(super) struct LocalCommentCreated { + pub(super) comment_id: Uuid, +} + pub(super) struct AnnounceCommentCreated { pub(super) submissioner_id: Uuid, pub(super) comment_id: Uuid, @@ -70,6 +82,10 @@ pub(super) struct CommentUpdated { pub(super) comment_id: Uuid, } +pub(super) struct LocalCommentUpdated { + pub(super) comment_id: Uuid, +} + pub(super) struct AnnounceCommentUpdated { pub(super) submissioner_id: Uuid, pub(super) comment_id: Uuid, diff --git a/profiles/src/apub/results/submission.rs b/profiles/src/apub/results/submission.rs index a4d6ac6..90a5376 100644 --- a/profiles/src/apub/results/submission.rs +++ b/profiles/src/apub/results/submission.rs @@ -1,5 +1,6 @@ use super::{ - SubmissionCreated, SubmissionDeleted, SubmissionUpdated, UnpublishedSubmissionCreated, + LocalSubmissionCreated, LocalSubmissionUpdated, SubmissionCreated, SubmissionDeleted, + SubmissionUpdated, UnpublishedSubmissionCreated, }; use crate::{ store::{FileSource, Submission, Visibility}, @@ -82,9 +83,15 @@ fn build_submission( .set_summary(submission.title()) .set_published(published.into()) .set_attributed_to(person_id); + if let Some(updated) = submission.updated() { + note.set_updated(updated.into()); + } if let Some(description) = submission.description() { note.set_content(description); } + if submission.is_logged_in_only() { + log::warn!("logged_in_only is not implemented yet"); + } match submission.visibility() { Visibility::Public => { note.add_to(public()); @@ -136,6 +143,28 @@ impl Outbound for UnpublishedSubmissionCreated { } } +impl Outbound for LocalSubmissionCreated { + fn id(&self) -> Option { + Some(self.submission_id) + } + + fn ready(&self) -> bool { + false + } + + fn behalf(&self, _: &Context) -> Result { + Err(Error::Invalid) + } + + fn inboxes(&self, _: &Context) -> Result, Error> { + Err(Error::Invalid) + } + + fn to_apub(&self, _: &Context) -> Result { + Err(Error::Invalid) + } +} + impl Outbound for SubmissionCreated { fn id(&self) -> Option { Some(self.submission_id) @@ -244,6 +273,28 @@ impl Outbound for SubmissionUpdated { } } +impl Outbound for LocalSubmissionUpdated { + fn id(&self) -> Option { + Some(self.submission_id) + } + + fn ready(&self) -> bool { + false + } + + fn behalf(&self, _: &Context) -> Result { + Err(Error::Invalid) + } + + fn inboxes(&self, _: &Context) -> Result, Error> { + Err(Error::Invalid) + } + + fn to_apub(&self, _: &Context) -> Result { + Err(Error::Invalid) + } +} + impl Outbound for SubmissionDeleted { fn id(&self) -> Option { None diff --git a/profiles/src/store/mod.rs b/profiles/src/store/mod.rs index 5179e70..a83eea1 100644 --- a/profiles/src/store/mod.rs +++ b/profiles/src/store/mod.rs @@ -19,7 +19,9 @@ pub use profile::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; +pub use submission::{ + Store as SubmissionStore, Submission, SubmissionChanges, SubmissionFileChanges, Visibility, +}; pub use term_search::TermSearch; pub use view::Store as ViewStore; @@ -398,96 +400,6 @@ impl File { } } -#[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, - title_source: Option, - description: Option, - description_source: Option, - files: Vec, - published: Option>, - visibility: Visibility, -} - -impl Submission { - pub fn update(&self) -> SubmissionChanges { - SubmissionChanges { - id: self.id, - title: None, - title_source: None, - description: None, - description_source: None, - visibility: None, - published: self.published, - } - } - - pub fn update_files(&self) -> SubmissionFileChanges { - SubmissionFileChanges { - id: self.id, - original_files: self.files.clone(), - files: self.files.clone(), - } - } - - pub fn id(&self) -> Uuid { - self.id - } - - pub fn profile_id(&self) -> Uuid { - self.profile_id - } - - pub fn title(&self) -> &str { - &self.title - } - - pub fn title_source(&self) -> Option<&str> { - self.title_source.as_deref() - } - - pub fn description(&self) -> Option<&str> { - self.description.as_ref().map(|d| d.as_str()) - } - - pub fn description_source(&self) -> Option<&str> { - self.description_source.as_deref() - } - - pub fn files(&self) -> &[Uuid] { - &self.files - } - - pub fn published(&self) -> Option> { - self.published - } - - pub fn visibility(&self) -> Visibility { - self.visibility - } - - pub fn is_public(&self) -> bool { - matches!(self.visibility, Visibility::Public) - } - - pub fn is_unlisted(&self) -> bool { - matches!(self.visibility, Visibility::Unlisted) - } - - pub fn is_followers_only(&self) -> bool { - matches!(self.visibility, Visibility::Followers) - } -} - #[derive(Clone, Debug)] pub enum Comment { Submission(SubmissionComment), @@ -742,81 +654,6 @@ impl ProfileImageChanges { } } -#[derive(Debug)] -pub struct SubmissionChanges { - id: Uuid, - title: Option, - title_source: Option, - description: Option, - description_source: Option, - visibility: Option, - published: Option>, -} - -impl SubmissionChanges { - pub(crate) fn title(&mut self, title: &str) -> &mut Self { - self.title = Some(title.to_owned()); - self - } - - pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self { - self.title_source = Some(title_source.to_owned()); - self - } - - pub(crate) fn description(&mut self, description: &str) -> &mut Self { - self.description = Some(description.to_owned()); - self - } - - pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self { - self.description_source = Some(description_source.to_owned()); - self - } - - pub(crate) fn visibility(&mut self, visibility: Visibility) -> &mut Self { - self.visibility = Some(visibility); - self - } - - pub(crate) fn publish(&mut self, time: Option>) -> &mut Self { - if self.published.is_none() { - 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() - || self.visibility.is_some() - } -} - -#[derive(Debug)] -pub struct SubmissionFileChanges { - id: Uuid, - original_files: Vec, - files: Vec, -} - -impl SubmissionFileChanges { - pub fn add_file(&mut self, file: &File) -> &mut Self { - self.files.push(file.id); - self - } - - pub fn delete_file(&mut self, file: &File) -> &mut Self { - self.files.retain(|id| *id != file.id); - self - } - - pub(crate) fn any_changes(&self) -> bool { - self.original_files != self.files - } -} - #[derive(Debug)] pub struct CommentChanges { id: Uuid, @@ -860,6 +697,9 @@ pub enum StoreError { #[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, } @@ -911,16 +751,6 @@ fn count( Ok(()) } -impl fmt::Display for Visibility { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Visibility::Public => write!(f, "Public"), - Visibility::Unlisted => write!(f, "Unlisted"), - Visibility::Followers => write!(f, "Followers"), - } - } -} - impl fmt::Display for OwnerSource { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/profiles/src/store/submission.rs b/profiles/src/store/submission.rs index 012ade3..b2ffdc9 100644 --- a/profiles/src/store/submission.rs +++ b/profiles/src/store/submission.rs @@ -1,9 +1,53 @@ -use super::{StoreError, Submission, SubmissionChanges, SubmissionFileChanges, Undo, Visibility}; +use super::{File, StoreError, Undo}; use chrono::{DateTime, Utc}; use sled::{Db, Transactional, Tree}; -use std::io::Cursor; +use std::{fmt, io::Cursor}; use uuid::Uuid; +#[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, + title_source: Option, + description: Option, + description_source: Option, + files: Vec, + published: Option>, + updated: Option>, + visibility: Visibility, + local_only: bool, + logged_in_only: bool, +} + +#[derive(Debug)] +pub struct SubmissionChanges { + id: Uuid, + title: Option, + title_source: Option, + description: Option, + description_source: Option, + visibility: Option, + published: Option>, + updated: Option>, + local_only: Option, + logged_in_only: Option, +} + +#[derive(Debug)] +pub struct SubmissionFileChanges { + id: Uuid, + original_files: Vec, + files: Vec, +} + #[derive(Clone, Debug)] pub struct Store { submission_tree: Tree, @@ -19,14 +63,191 @@ struct StoredSubmission { id: Uuid, profile_id: Uuid, title: String, + #[serde(skip_serializing_if = "Option::is_none")] title_source: Option, + #[serde(skip_serializing_if = "Option::is_none")] description: Option, + #[serde(skip_serializing_if = "Option::is_none")] description_source: Option, files: Vec, + #[serde(skip_serializing_if = "Option::is_none")] published: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + updated: Option>, visibility: Visibility, + #[serde(default)] + local_only: bool, + #[serde(default)] + logged_in_only: bool, drafted_at: DateTime, - updated_at: DateTime, +} + +impl Submission { + pub fn update(&self) -> SubmissionChanges { + SubmissionChanges { + id: self.id, + title: None, + title_source: None, + description: None, + description_source: None, + visibility: None, + published: self.published, + updated: None, + local_only: None, + logged_in_only: None, + } + } + + pub fn update_files(&self) -> SubmissionFileChanges { + SubmissionFileChanges { + id: self.id, + original_files: self.files.clone(), + files: self.files.clone(), + } + } + + pub fn id(&self) -> Uuid { + self.id + } + + pub fn profile_id(&self) -> Uuid { + self.profile_id + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn title_source(&self) -> Option<&str> { + self.title_source.as_deref() + } + + pub fn description(&self) -> Option<&str> { + self.description.as_ref().map(|d| d.as_str()) + } + + pub fn description_source(&self) -> Option<&str> { + self.description_source.as_deref() + } + + pub fn files(&self) -> &[Uuid] { + &self.files + } + + pub fn published(&self) -> Option> { + self.published + } + + pub fn updated(&self) -> Option> { + self.updated + } + + pub fn visibility(&self) -> Visibility { + self.visibility + } + + pub fn is_public(&self) -> bool { + matches!(self.visibility, Visibility::Public) + } + + pub fn is_unlisted(&self) -> bool { + matches!(self.visibility, Visibility::Unlisted) + } + + pub fn is_followers_only(&self) -> bool { + matches!(self.visibility, Visibility::Followers) + } + + pub fn is_local_only(&self) -> bool { + self.local_only + } + + pub fn is_logged_in_only(&self) -> bool { + self.logged_in_only + } +} + +impl SubmissionChanges { + pub(crate) fn title(&mut self, title: &str) -> &mut Self { + self.title = Some(title.to_owned()); + self + } + + pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self { + self.title_source = Some(title_source.to_owned()); + self + } + + pub(crate) fn description(&mut self, description: &str) -> &mut Self { + self.description = Some(description.to_owned()); + self + } + + pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self { + self.description_source = Some(description_source.to_owned()); + self + } + + pub(crate) fn visibility(&mut self, visibility: Visibility) -> &mut Self { + if self.visibility.is_none() { + self.visibility = Some(visibility); + } + self + } + + pub(crate) fn published(&mut self, time: Option>) -> &mut Self { + if self.published.is_none() { + self.published = time.or_else(|| Some(Utc::now())); + } + self + } + + pub(crate) fn updated(&mut self, time: DateTime) -> &mut Self { + if self.published.is_some() { + self.updated = Some(time); + } + self + } + + pub(crate) fn local_only(&mut self, local_only: bool) -> &mut Self { + if self.published.is_none() { + self.local_only = Some(local_only); + } + self + } + + pub(crate) fn logged_in_only(&mut self, logged_in_only: bool) -> &mut Self { + if self.published.is_none() { + self.logged_in_only = Some(logged_in_only); + } + self + } + + pub(crate) fn any_changes(&self) -> bool { + self.title.is_some() + || self.description.is_some() + || self.published.is_some() + || self.updated.is_some() + || self.visibility.is_some() + || self.local_only.is_some() + || self.logged_in_only.is_some() + } +} + +impl SubmissionFileChanges { + pub(crate) fn add_file(&mut self, file: &File) -> &mut Self { + self.files.push(file.id); + self + } + + pub(crate) fn delete_file(&mut self, file: &File) -> &mut Self { + self.files.retain(|id| *id != file.id); + self + } + + pub(crate) fn any_changes(&self) -> bool { + self.original_files != self.files + } } impl Store { @@ -66,9 +287,11 @@ impl Store { description_source: None, files: vec![], published: None, + updated: None, visibility, + local_only: false, + logged_in_only: false, drafted_at: now, - updated_at: now, }; serde_json::to_writer(writer, &stored_submission)?; self.submission_tree @@ -139,6 +362,17 @@ impl Store { let mut stored_submission: StoredSubmission = serde_json::from_slice(&stored_submission_ivec)?; + if let Some(updated) = changes.updated { + if let Some(previously_updated) = stored_submission + .updated + .or_else(|| stored_submission.published) + { + if updated < previously_updated { + return Err(StoreError::Outdated); + } + } + } + let already_published = stored_submission.published.is_some(); if let Some(title) = &changes.title { @@ -156,8 +390,18 @@ impl Store { if let Some(visibility) = changes.visibility { stored_submission.visibility = visibility; } + if let Some(local_only) = changes.local_only { + stored_submission.local_only = local_only; + } + if let Some(logged_in_only) = changes.logged_in_only { + stored_submission.logged_in_only = logged_in_only; + } + if stored_submission.published.is_some() { + if let Some(updated) = changes.updated { + stored_submission.updated = Some(updated); + } + } stored_submission.published = changes.published; - stored_submission.updated_at = Utc::now(); let stored_submission_vec = serde_json::to_vec(&stored_submission)?; @@ -211,7 +455,6 @@ impl Store { serde_json::from_slice(&stored_submission_ivec)?; stored_submission.files = changes.files.clone(); - stored_submission.updated_at = Utc::now(); let stored_submission_vec = serde_json::to_vec(&stored_submission)?; @@ -589,7 +832,20 @@ impl From for Submission { description_source: ss.description_source, files: ss.files, published: ss.published, + updated: ss.updated, visibility: ss.visibility, + local_only: ss.local_only, + logged_in_only: ss.logged_in_only, + } + } +} + +impl fmt::Display for Visibility { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Visibility::Public => write!(f, "Public"), + Visibility::Unlisted => write!(f, "Unlisted"), + Visibility::Followers => write!(f, "Followers"), } } }