Compare commits
4 commits
dcf5ca3e0a
...
02c3088787
Author | SHA1 | Date | |
---|---|---|---|
asonix | 02c3088787 | ||
asonix | b35b7680f1 | ||
asonix | 6b08db0634 | ||
asonix | df9a45137d |
13
TODO.md
13
TODO.md
|
@ -32,6 +32,18 @@ A lot of modules in hyaenidae-server have circular dependencies, and a few are m
|
||||||
lines long, each. These should be separated where possible into smaller modules which create a
|
lines long, each. These should be separated where possible into smaller modules which create a
|
||||||
dependency DAG
|
dependency DAG
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
Right now, a lot of the validation happens in multiple places in the codebase, which is not great. I
|
||||||
|
think there should be a Single Way we do validation in order to keep ourselves sane in the future
|
||||||
|
when we're trying to debug stuff
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
- Enable validation on the [Thing]Changes structs
|
||||||
|
- ReactChanges
|
||||||
|
- Distinguish between local & federated when validation maxium lengths
|
||||||
|
- Make a global maximum length that things get truncated to if they're too long
|
||||||
|
- Expose validation failures in the user interface
|
||||||
|
|
||||||
## Pagination
|
## Pagination
|
||||||
Tasks:
|
Tasks:
|
||||||
- Enable pagination for notifications
|
- Enable pagination for notifications
|
||||||
|
@ -74,6 +86,7 @@ Tasks:
|
||||||
- Enable viewing of closed reports
|
- Enable viewing of closed reports
|
||||||
|
|
||||||
## Reacts
|
## Reacts
|
||||||
|
Rethink whether we actually want to do reacts. It may be that just having Likes is fine
|
||||||
Reacts are similar to "likes" on other platforms, but do not get added to a collection for a given
|
Reacts are similar to "likes" on other platforms, but do not get added to a collection for a given
|
||||||
profile. Reacts only notify the author of the item being reacted to.
|
profile. Reacts only notify the author of the item being reacted to.
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ impl Action for CreateComment {
|
||||||
changes.body_source(body_source);
|
changes.body_source(body_source);
|
||||||
}
|
}
|
||||||
|
|
||||||
let comment = changes.save()?;
|
let comment = changes.save()??;
|
||||||
|
|
||||||
if let Some(apub_id) = &self.note_apub_id {
|
if let Some(apub_id) = &self.note_apub_id {
|
||||||
ctx.apub.comment(apub_id, comment.id())?;
|
ctx.apub.comment(apub_id, comment.id())?;
|
||||||
|
@ -124,7 +124,7 @@ impl Action for UpdateComment {
|
||||||
changes.body_source(body_source);
|
changes.body_source(body_source);
|
||||||
}
|
}
|
||||||
let comment = if changes.any_changes() {
|
let comment = if changes.any_changes() {
|
||||||
changes.save()?
|
changes.save()??
|
||||||
} else {
|
} else {
|
||||||
comment
|
comment
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,10 +14,11 @@ impl Action for CreateProfile {
|
||||||
self.owner_source.clone(),
|
self.owner_source.clone(),
|
||||||
self.handle.clone(),
|
self.handle.clone(),
|
||||||
self.domain.clone(),
|
self.domain.clone(),
|
||||||
self.published,
|
)?;
|
||||||
);
|
|
||||||
|
|
||||||
changes.login_required(self.login_required);
|
changes
|
||||||
|
.login_required(self.login_required)
|
||||||
|
.published(self.published);
|
||||||
if let Some(display_name) = &self.display_name {
|
if let Some(display_name) = &self.display_name {
|
||||||
changes.display_name(display_name);
|
changes.display_name(display_name);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +35,7 @@ impl Action for CreateProfile {
|
||||||
changes.updated(updated);
|
changes.updated(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
let profile = changes.save()?;
|
let profile = changes.save()??;
|
||||||
|
|
||||||
if let Some(apub) = &self.apub {
|
if let Some(apub) = &self.apub {
|
||||||
ctx.apub.profile(&apub.person_apub_id, profile.id())?;
|
ctx.apub.profile(&apub.person_apub_id, profile.id())?;
|
||||||
|
@ -110,7 +111,7 @@ impl Action for UpdateProfile {
|
||||||
}
|
}
|
||||||
|
|
||||||
let profile = if changes.any_changes() {
|
let profile = if changes.any_changes() {
|
||||||
changes.save()?
|
changes.save()??
|
||||||
} else {
|
} else {
|
||||||
profile
|
profile
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ impl Action for CreateServer {
|
||||||
if let Some(description) = &self.description {
|
if let Some(description) = &self.description {
|
||||||
changes.description(description);
|
changes.description(description);
|
||||||
}
|
}
|
||||||
let server = changes.save()?;
|
let server = changes.save()??;
|
||||||
|
|
||||||
if let Some(apub) = &self.apub {
|
if let Some(apub) = &self.apub {
|
||||||
ctx.apub.server(&apub.application_apub_id, server.id())?;
|
ctx.apub.server(&apub.application_apub_id, server.id())?;
|
||||||
|
@ -66,7 +66,7 @@ impl Action for UpdateServer {
|
||||||
changes.description_source(description_source);
|
changes.description_source(description_source);
|
||||||
}
|
}
|
||||||
let server = if changes.any_changes() {
|
let server = if changes.any_changes() {
|
||||||
changes.save()?
|
changes.save()??
|
||||||
} else {
|
} else {
|
||||||
server
|
server
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,7 +32,7 @@ impl Action for CreateSubmission {
|
||||||
changes.add_file(*file);
|
changes.add_file(*file);
|
||||||
}
|
}
|
||||||
|
|
||||||
let submission = changes.save()?;
|
let submission = changes.save()??;
|
||||||
let submission_id = submission.id();
|
let submission_id = submission.id();
|
||||||
|
|
||||||
if let Some(apub_id) = &self.note_apub_id {
|
if let Some(apub_id) = &self.note_apub_id {
|
||||||
|
@ -150,7 +150,7 @@ impl Action for UpdateSubmission {
|
||||||
}
|
}
|
||||||
|
|
||||||
let submission = if changes.any_changes() {
|
let submission = if changes.any_changes() {
|
||||||
changes.save()?
|
changes.save()??
|
||||||
} else {
|
} else {
|
||||||
submission
|
submission
|
||||||
};
|
};
|
||||||
|
|
|
@ -132,8 +132,11 @@ impl Outbound for ProfileCreated {
|
||||||
shared_inbox: Some(shared_inbox),
|
shared_inbox: Some(shared_inbox),
|
||||||
..Endpoints::default()
|
..Endpoints::default()
|
||||||
})
|
})
|
||||||
.set_preferred_username(profile.handle())
|
.set_preferred_username(profile.handle());
|
||||||
.set_published(profile.published().into());
|
|
||||||
|
if let Some(published) = profile.published() {
|
||||||
|
person.set_published(published.into());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(display_name) = profile.display_name() {
|
if let Some(display_name) = profile.display_name() {
|
||||||
person.set_name(display_name);
|
person.set_name(display_name);
|
||||||
|
@ -235,9 +238,11 @@ impl Outbound for ProfileUpdated {
|
||||||
|
|
||||||
person
|
person
|
||||||
.set_id(person_id.clone())
|
.set_id(person_id.clone())
|
||||||
.set_preferred_username(profile.handle())
|
.set_preferred_username(profile.handle());
|
||||||
.set_published(profile.published().into());
|
|
||||||
|
|
||||||
|
if let Some(published) = profile.published() {
|
||||||
|
person.set_published(published.into());
|
||||||
|
}
|
||||||
if let Some(outbox) = endpoints.outbox {
|
if let Some(outbox) = endpoints.outbox {
|
||||||
person.set_outbox(outbox);
|
person.set_outbox(outbox);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ mod viewer;
|
||||||
|
|
||||||
use apub::ApubIds;
|
use apub::ApubIds;
|
||||||
use pictrs::ImageInfo;
|
use pictrs::ImageInfo;
|
||||||
use store::OwnerSource;
|
use store::{OwnerSource, ValidationError};
|
||||||
use viewer::Viewer;
|
use viewer::Viewer;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
@ -44,6 +44,9 @@ pub enum Error {
|
||||||
#[error("Blocked")]
|
#[error("Blocked")]
|
||||||
Blocked,
|
Blocked,
|
||||||
|
|
||||||
|
#[error("Validations failed")]
|
||||||
|
Validate(Vec<ValidationError>),
|
||||||
|
|
||||||
#[error("Error deleting file: {0}")]
|
#[error("Error deleting file: {0}")]
|
||||||
DeleteFile(#[from] pictrs::DeleteError),
|
DeleteFile(#[from] pictrs::DeleteError),
|
||||||
|
|
||||||
|
@ -60,6 +63,22 @@ pub enum Error {
|
||||||
Apub(#[from] apub::StoreError),
|
Apub(#[from] apub::StoreError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Vec<ValidationError>> for Error {
|
||||||
|
fn from(v: Vec<ValidationError>) -> Self {
|
||||||
|
Self::Validate(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub fn validation_errors(&self) -> Option<&[ValidationError]> {
|
||||||
|
if let Self::Validate(errors) = &self {
|
||||||
|
Some(errors.as_slice())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub enum OnBehalfOf {
|
pub enum OnBehalfOf {
|
||||||
Profile(Uuid),
|
Profile(Uuid),
|
||||||
|
@ -116,6 +135,21 @@ impl<T> Required<T> for Option<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ContentConfig {
|
||||||
|
pub max_bio_length: usize,
|
||||||
|
pub max_display_name_length: usize,
|
||||||
|
pub max_handle_length: usize,
|
||||||
|
pub max_domain_length: usize,
|
||||||
|
pub max_submission_title_length: usize,
|
||||||
|
pub max_submission_body_length: usize,
|
||||||
|
pub max_post_title_length: usize,
|
||||||
|
pub max_post_body_length: usize,
|
||||||
|
pub max_comment_length: usize,
|
||||||
|
pub max_server_title_length: usize,
|
||||||
|
pub max_server_body_length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
enum KeyOwner {
|
enum KeyOwner {
|
||||||
Server(Uuid),
|
Server(Uuid),
|
||||||
|
@ -129,6 +163,7 @@ pub struct State {
|
||||||
pub pictrs: pictrs::State,
|
pub pictrs: pictrs::State,
|
||||||
pub spawner: Arc<dyn Spawner + Send + Sync>,
|
pub spawner: Arc<dyn Spawner + Send + Sync>,
|
||||||
pub url_for: Arc<dyn UrlFor + Send + Sync>,
|
pub url_for: Arc<dyn UrlFor + Send + Sync>,
|
||||||
|
pub content_config: ContentConfig,
|
||||||
pub arbiter: Arbiter,
|
pub arbiter: Arbiter,
|
||||||
_db: Db,
|
_db: Db,
|
||||||
}
|
}
|
||||||
|
@ -140,15 +175,17 @@ impl State {
|
||||||
apub_info: impl ApubIds + Send + Sync + 'static,
|
apub_info: impl ApubIds + Send + Sync + 'static,
|
||||||
spawner: impl Spawner + Send + Sync + 'static,
|
spawner: impl Spawner + Send + Sync + 'static,
|
||||||
url_for: impl UrlFor + Send + Sync + 'static,
|
url_for: impl UrlFor + Send + Sync + 'static,
|
||||||
|
content_config: ContentConfig,
|
||||||
arbiter: Arbiter,
|
arbiter: Arbiter,
|
||||||
db: Db,
|
db: Db,
|
||||||
) -> Result<Arc<Self>, sled::Error> {
|
) -> Result<Arc<Self>, sled::Error> {
|
||||||
Ok(Arc::new(State {
|
Ok(Arc::new(State {
|
||||||
store: store::Store::build(&db)?,
|
store: store::Store::build(content_config.max_handle_length, &db)?,
|
||||||
apub: apub::Store::build(apub_info, &db)?,
|
apub: apub::Store::build(apub_info, &db)?,
|
||||||
pictrs: pictrs::State::new(pictrs_upstream, image_info),
|
pictrs: pictrs::State::new(pictrs_upstream, image_info),
|
||||||
spawner: Arc::new(spawner),
|
spawner: Arc::new(spawner),
|
||||||
url_for: Arc::new(url_for),
|
url_for: Arc::new(url_for),
|
||||||
|
content_config,
|
||||||
arbiter,
|
arbiter,
|
||||||
_db: db,
|
_db: db,
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{StoreError, Undo};
|
use super::{StoreError, Undo, ValidationError, ValidationErrorKind};
|
||||||
use crate::State;
|
use crate::State;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use hyaenidae_content::{bbcode, html};
|
use hyaenidae_content::{bbcode, html};
|
||||||
|
@ -39,6 +39,7 @@ pub struct CommentChanges<'a> {
|
||||||
body_source: Option<String>,
|
body_source: Option<String>,
|
||||||
published: Option<DateTime<Utc>>,
|
published: Option<DateTime<Utc>>,
|
||||||
updated: Option<DateTime<Utc>>,
|
updated: Option<DateTime<Utc>>,
|
||||||
|
errors: Vec<ValidationError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -115,7 +116,8 @@ impl Comment {
|
||||||
body: None,
|
body: None,
|
||||||
body_source: None,
|
body_source: None,
|
||||||
published: self.published,
|
published: self.published,
|
||||||
updated: None,
|
updated: self.updated,
|
||||||
|
errors: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,11 +144,29 @@ impl<'a> CommentChanges<'a> {
|
||||||
body_source: None,
|
body_source: None,
|
||||||
published: None,
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
errors: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn body(&mut self, body: &str) -> &mut Self {
|
pub(crate) fn body(&mut self, body: &str) -> &mut Self {
|
||||||
self.body = Some(html(body));
|
let body = html(body.trim());
|
||||||
|
|
||||||
|
if body.len() > self.state.content_config.max_comment_length {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("body"),
|
||||||
|
kind: ValidationErrorKind::TooLong {
|
||||||
|
maximum: self.state.content_config.max_comment_length,
|
||||||
|
proposed: body.len(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if body.is_empty() {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("body"),
|
||||||
|
kind: ValidationErrorKind::Empty,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.body = Some(body);
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,8 +183,18 @@ impl<'a> CommentChanges<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
|
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
|
||||||
if self.published.is_some() {
|
if let Some(published) = self.published {
|
||||||
self.updated = Some(updated);
|
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
|
self
|
||||||
}
|
}
|
||||||
|
@ -173,8 +203,12 @@ impl<'a> CommentChanges<'a> {
|
||||||
self.body.is_some() || self.published.is_some() || self.updated.is_some()
|
self.body.is_some() || self.published.is_some() || self.updated.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn save(self) -> Result<Comment, StoreError> {
|
pub(crate) fn save(self) -> Result<Result<Comment, Vec<ValidationError>>, StoreError> {
|
||||||
self.state.store.comments.save(&self)
|
if self.errors.is_empty() {
|
||||||
|
self.state.store.comments.save(&self).map(Ok)
|
||||||
|
} else {
|
||||||
|
Ok(Err(self.errors))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,16 +353,6 @@ impl Store {
|
||||||
|
|
||||||
let mut stored_comment: StoredComment = serde_json::from_slice(&stored_comment_ivec)?;
|
let mut stored_comment: StoredComment = serde_json::from_slice(&stored_comment_ivec)?;
|
||||||
|
|
||||||
if let Some(updated) = changes.updated {
|
|
||||||
if let Some(previously_updated) =
|
|
||||||
stored_comment.updated.or_else(|| stored_comment.published)
|
|
||||||
{
|
|
||||||
if updated < previously_updated {
|
|
||||||
return Err(StoreError::Outdated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(body) = &changes.body {
|
if let Some(body) = &changes.body {
|
||||||
stored_comment.body = body.clone();
|
stored_comment.body = body.clone();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,31 @@ pub use submission::{Store as SubmissionStore, Submission, SubmissionChanges, Vi
|
||||||
pub use term_search::TermSearch;
|
pub use term_search::TermSearch;
|
||||||
pub use view::Store as ViewStore;
|
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<Utc>,
|
||||||
|
proposed: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[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)]
|
#[derive(Clone)]
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
pub profiles: profile::Store,
|
pub profiles: profile::Store,
|
||||||
|
@ -36,9 +61,9 @@ pub struct Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
pub fn build(db: &Db) -> Result<Store, sled::Error> {
|
pub fn build(max_handle_length: usize, db: &Db) -> Result<Store, sled::Error> {
|
||||||
Ok(Store {
|
Ok(Store {
|
||||||
profiles: profile::Store::build(db)?,
|
profiles: profile::Store::build(max_handle_length, db)?,
|
||||||
files: file::Store::build(db)?,
|
files: file::Store::build(db)?,
|
||||||
submissions: submission::Store::build(db)?,
|
submissions: submission::Store::build(db)?,
|
||||||
comments: comment::Store::build(db)?,
|
comments: comment::Store::build(db)?,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{StoreError, TermSearch, Undo};
|
use super::{StoreError, TermSearch, Undo, ValidationError, ValidationErrorKind};
|
||||||
use crate::State;
|
use crate::State;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use hyaenidae_content::{bbcode, html, strip};
|
use hyaenidae_content::{bbcode, html, strip};
|
||||||
|
@ -12,7 +12,6 @@ enum ProfileChangesKind {
|
||||||
source: OwnerSource,
|
source: OwnerSource,
|
||||||
domain: String,
|
domain: String,
|
||||||
handle: String,
|
handle: String,
|
||||||
published: DateTime<Utc>,
|
|
||||||
},
|
},
|
||||||
Update {
|
Update {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
@ -30,7 +29,9 @@ pub struct ProfileChanges<'a> {
|
||||||
login_required: Option<bool>,
|
login_required: Option<bool>,
|
||||||
icon: Option<Uuid>,
|
icon: Option<Uuid>,
|
||||||
banner: Option<Uuid>,
|
banner: Option<Uuid>,
|
||||||
|
published: Option<DateTime<Utc>>,
|
||||||
updated: Option<DateTime<Utc>>,
|
updated: Option<DateTime<Utc>>,
|
||||||
|
errors: Vec<ValidationError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
@ -51,11 +52,12 @@ pub struct Profile {
|
||||||
description_source: Option<String>,
|
description_source: Option<String>,
|
||||||
icon: Option<Uuid>,
|
icon: Option<Uuid>,
|
||||||
banner: Option<Uuid>,
|
banner: Option<Uuid>,
|
||||||
published: DateTime<Utc>,
|
published: Option<DateTime<Utc>>,
|
||||||
updated: Option<DateTime<Utc>>,
|
updated: Option<DateTime<Utc>>,
|
||||||
login_required: bool,
|
login_required: bool,
|
||||||
suspended: bool,
|
suspended: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
profile_tree: Tree,
|
profile_tree: Tree,
|
||||||
|
@ -78,22 +80,34 @@ struct StoredProfile {
|
||||||
owner_source: StoredOwnerSource,
|
owner_source: StoredOwnerSource,
|
||||||
handle: String,
|
handle: String,
|
||||||
domain: String,
|
domain: String,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
display_name_source: Option<String>,
|
display_name_source: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
description_source: Option<String>,
|
description_source: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
icon: Option<Uuid>,
|
icon: Option<Uuid>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
banner: Option<Uuid>,
|
banner: Option<Uuid>,
|
||||||
published: DateTime<Utc>,
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
published: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
updated: Option<DateTime<Utc>>,
|
updated: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
login_required: bool,
|
login_required: bool,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
suspended_at: Option<DateTime<Utc>>,
|
suspended_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
@ -104,29 +118,86 @@ impl<'a> ProfileChanges<'a> {
|
||||||
source: OwnerSource,
|
source: OwnerSource,
|
||||||
domain: String,
|
domain: String,
|
||||||
handle: String,
|
handle: String,
|
||||||
published: DateTime<Utc>,
|
) -> Result<Self, Vec<ValidationError>> {
|
||||||
) -> Self {
|
let handle = strip(handle.trim());
|
||||||
ProfileChanges {
|
let domain = strip(domain.trim());
|
||||||
state,
|
|
||||||
kind: ProfileChangesKind::Create {
|
let mut errors = vec![];
|
||||||
source,
|
|
||||||
domain,
|
if handle.len() > state.content_config.max_handle_length {
|
||||||
handle,
|
errors.push(ValidationError {
|
||||||
published,
|
field: String::from("handle"),
|
||||||
},
|
kind: ValidationErrorKind::TooLong {
|
||||||
display_name: None,
|
maximum: state.content_config.max_handle_length,
|
||||||
display_name_source: None,
|
proposed: handle.len(),
|
||||||
description: None,
|
},
|
||||||
description_source: None,
|
});
|
||||||
login_required: None,
|
} else if handle.is_empty() {
|
||||||
icon: None,
|
errors.push(ValidationError {
|
||||||
banner: None,
|
field: String::from("handle"),
|
||||||
updated: None,
|
kind: ValidationErrorKind::Empty,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if domain.len() > state.content_config.max_domain_length {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
field: String::from("domain"),
|
||||||
|
kind: ValidationErrorKind::TooLong {
|
||||||
|
maximum: state.content_config.max_domain_length,
|
||||||
|
proposed: domain.len(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if domain.is_empty() {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
field: String::from("domain"),
|
||||||
|
kind: ValidationErrorKind::Empty,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(ProfileChanges {
|
||||||
|
state,
|
||||||
|
kind: ProfileChangesKind::Create {
|
||||||
|
source,
|
||||||
|
domain,
|
||||||
|
handle,
|
||||||
|
},
|
||||||
|
display_name: None,
|
||||||
|
display_name_source: None,
|
||||||
|
description: None,
|
||||||
|
description_source: None,
|
||||||
|
login_required: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
|
published: None,
|
||||||
|
updated: None,
|
||||||
|
errors: vec![],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(errors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn display_name(&mut self, display_name: &str) -> &mut Self {
|
pub(crate) fn display_name(&mut self, display_name: &str) -> &mut Self {
|
||||||
self.display_name = Some(strip(display_name));
|
let display_name = strip(display_name.trim());
|
||||||
|
|
||||||
|
if display_name.len() > self.state.content_config.max_display_name_length {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("display_name"),
|
||||||
|
kind: ValidationErrorKind::TooLong {
|
||||||
|
maximum: self.state.content_config.max_display_name_length,
|
||||||
|
proposed: display_name.len(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if display_name.is_empty() {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("display_name"),
|
||||||
|
kind: ValidationErrorKind::Empty,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.display_name = Some(display_name);
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +207,25 @@ impl<'a> ProfileChanges<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn description(&mut self, description: &str) -> &mut Self {
|
pub(crate) fn description(&mut self, description: &str) -> &mut Self {
|
||||||
self.description = Some(html(description));
|
let description = html(description.trim());
|
||||||
|
|
||||||
|
if description.len() > self.state.content_config.max_bio_length {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("description"),
|
||||||
|
kind: ValidationErrorKind::TooLong {
|
||||||
|
maximum: self.state.content_config.max_bio_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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,8 +249,27 @@ impl<'a> ProfileChanges<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn published(&mut self, published: DateTime<Utc>) -> &mut Self {
|
||||||
|
if self.published.is_none() {
|
||||||
|
self.published = Some(published);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
|
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
|
||||||
self.updated = Some(updated);
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,11 +279,14 @@ impl<'a> ProfileChanges<'a> {
|
||||||
|| self.login_required.is_some()
|
|| self.login_required.is_some()
|
||||||
|| self.icon.is_some()
|
|| self.icon.is_some()
|
||||||
|| self.banner.is_some()
|
|| self.banner.is_some()
|
||||||
|| self.updated.is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn save(self) -> Result<Profile, StoreError> {
|
pub(crate) fn save(self) -> Result<Result<Profile, Vec<ValidationError>>, StoreError> {
|
||||||
self.state.store.profiles.save(&self)
|
if self.errors.is_empty() {
|
||||||
|
self.state.store.profiles.save(&self).map(Ok)
|
||||||
|
} else {
|
||||||
|
Ok(Err(self.errors))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +308,9 @@ impl Profile {
|
||||||
login_required: None,
|
login_required: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
banner: None,
|
banner: None,
|
||||||
updated: None,
|
published: self.published,
|
||||||
|
updated: self.updated,
|
||||||
|
errors: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +361,7 @@ impl Profile {
|
||||||
self.banner
|
self.banner
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn published(&self) -> DateTime<Utc> {
|
pub fn published(&self) -> Option<DateTime<Utc>> {
|
||||||
self.published
|
self.published
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,14 +379,14 @@ impl Profile {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
pub(super) fn build(db: &Db) -> Result<Self, sled::Error> {
|
pub(super) fn build(max_handle_length: usize, db: &Db) -> Result<Self, sled::Error> {
|
||||||
Ok(Store {
|
Ok(Store {
|
||||||
profile_tree: db.open_tree("profiles/profiles")?,
|
profile_tree: db.open_tree("profiles/profiles")?,
|
||||||
handle_tree: db.open_tree("profiles/profiles/handles")?,
|
handle_tree: db.open_tree("profiles/profiles/handles")?,
|
||||||
owner_created_tree: db.open_tree("/profiles/profiles/owner/created")?,
|
owner_created_tree: db.open_tree("/profiles/profiles/owner/created")?,
|
||||||
created_tree: db.open_tree("/profiles/profiles/created")?,
|
created_tree: db.open_tree("/profiles/profiles/created")?,
|
||||||
count_tree: db.open_tree("/profiles/profiles/count")?,
|
count_tree: db.open_tree("/profiles/profiles/count")?,
|
||||||
handle_index: TermSearch::build("handle", 20, db)?,
|
handle_index: TermSearch::build("handle", max_handle_length, db)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,9 +396,8 @@ impl Store {
|
||||||
source: OwnerSource,
|
source: OwnerSource,
|
||||||
handle: String,
|
handle: String,
|
||||||
domain: String,
|
domain: String,
|
||||||
published: DateTime<Utc>,
|
) -> Result<ProfileChanges<'a>, Vec<ValidationError>> {
|
||||||
) -> ProfileChanges<'a> {
|
ProfileChanges::new(state, source, domain, handle)
|
||||||
ProfileChanges::new(state, source, domain, handle, published)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save<'a>(&self, changes: &ProfileChanges<'a>) -> Result<Profile, StoreError> {
|
fn save<'a>(&self, changes: &ProfileChanges<'a>) -> Result<Profile, StoreError> {
|
||||||
|
@ -294,9 +406,8 @@ impl Store {
|
||||||
source,
|
source,
|
||||||
handle,
|
handle,
|
||||||
domain,
|
domain,
|
||||||
published,
|
|
||||||
} => {
|
} => {
|
||||||
let id = self.do_create(*source, handle, domain, *published)?;
|
let id = self.do_create(*source, handle, domain)?;
|
||||||
self.do_update(id, changes)
|
self.do_update(id, changes)
|
||||||
}
|
}
|
||||||
ProfileChangesKind::Update { id } => self.do_update(*id, changes),
|
ProfileChangesKind::Update { id } => self.do_update(*id, changes),
|
||||||
|
@ -308,7 +419,6 @@ impl Store {
|
||||||
source: OwnerSource,
|
source: OwnerSource,
|
||||||
handle: &str,
|
handle: &str,
|
||||||
domain: &str,
|
domain: &str,
|
||||||
published: DateTime<Utc>,
|
|
||||||
) -> Result<Uuid, StoreError> {
|
) -> Result<Uuid, StoreError> {
|
||||||
let handle_lower = handle.to_lowercase();
|
let handle_lower = handle.to_lowercase();
|
||||||
|
|
||||||
|
@ -338,7 +448,7 @@ impl Store {
|
||||||
description_source: None,
|
description_source: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
banner: None,
|
banner: None,
|
||||||
published,
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
login_required: true,
|
login_required: true,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
|
@ -428,13 +538,6 @@ impl Store {
|
||||||
|
|
||||||
let mut stored_profile: StoredProfile = serde_json::from_slice(&stored_profile_ivec)?;
|
let mut stored_profile: StoredProfile = serde_json::from_slice(&stored_profile_ivec)?;
|
||||||
|
|
||||||
if let Some(updated) = profile_changes.updated {
|
|
||||||
let previously_updated = stored_profile.updated.unwrap_or(stored_profile.published);
|
|
||||||
if updated < previously_updated {
|
|
||||||
return Err(StoreError::Outdated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(login_required) = profile_changes.login_required {
|
if let Some(login_required) = profile_changes.login_required {
|
||||||
stored_profile.login_required = login_required;
|
stored_profile.login_required = login_required;
|
||||||
}
|
}
|
||||||
|
@ -456,6 +559,9 @@ impl Store {
|
||||||
if let Some(banner) = profile_changes.banner {
|
if let Some(banner) = profile_changes.banner {
|
||||||
stored_profile.banner = Some(banner);
|
stored_profile.banner = Some(banner);
|
||||||
}
|
}
|
||||||
|
if let Some(published) = profile_changes.published {
|
||||||
|
stored_profile.published = Some(published);
|
||||||
|
}
|
||||||
if let Some(updated) = profile_changes.updated {
|
if let Some(updated) = profile_changes.updated {
|
||||||
stored_profile.updated = Some(updated);
|
stored_profile.updated = Some(updated);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::{store::StoreError, State};
|
use crate::{
|
||||||
|
store::{StoreError, ValidationError, ValidationErrorKind},
|
||||||
|
State,
|
||||||
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use hyaenidae_content::{bbcode, html, strip};
|
use hyaenidae_content::{bbcode, html, strip};
|
||||||
use sled::{Db, Transactional, Tree};
|
use sled::{Db, Transactional, Tree};
|
||||||
|
@ -33,6 +36,7 @@ pub struct ServerChanges<'a> {
|
||||||
description_source: Option<String>,
|
description_source: Option<String>,
|
||||||
published: Option<DateTime<Utc>>,
|
published: Option<DateTime<Utc>>,
|
||||||
updated: Option<DateTime<Utc>>,
|
updated: Option<DateTime<Utc>>,
|
||||||
|
errors: Vec<ValidationError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -94,7 +98,8 @@ impl Server {
|
||||||
description: None,
|
description: None,
|
||||||
description_source: None,
|
description_source: None,
|
||||||
published: self.published,
|
published: self.published,
|
||||||
updated: None,
|
updated: self.updated,
|
||||||
|
errors: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,11 +115,30 @@ impl<'a> ServerChanges<'a> {
|
||||||
description_source: None,
|
description_source: None,
|
||||||
published: None,
|
published: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
errors: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn title(&mut self, title: &str) -> &mut Self {
|
pub(crate) fn title(&mut self, title: &str) -> &mut Self {
|
||||||
self.title = Some(strip(title));
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +148,25 @@ impl<'a> ServerChanges<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn description(&mut self, description: &str) -> &mut Self {
|
pub(crate) fn description(&mut self, description: &str) -> &mut Self {
|
||||||
self.description = Some(html(description));
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,18 +183,32 @@ impl<'a> ServerChanges<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
|
pub(crate) fn updated(&mut self, updated: DateTime<Utc>) -> &mut Self {
|
||||||
if self.published.is_some() {
|
if let Some(published) = self.published {
|
||||||
self.updated = Some(updated);
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn any_changes(&self) -> bool {
|
pub(crate) fn any_changes(&self) -> bool {
|
||||||
self.title.is_some() || self.description.is_some()
|
self.title.is_some() || self.description.is_some() || self.updated.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn save(self) -> Result<Server, StoreError> {
|
pub(crate) fn save(self) -> Result<Result<Server, Vec<ValidationError>>, StoreError> {
|
||||||
self.state.store.servers.save(&self)
|
if self.errors.is_empty() {
|
||||||
|
self.state.store.servers.save(&self).map(Ok)
|
||||||
|
} else {
|
||||||
|
Ok(Err(self.errors))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,20 +319,6 @@ impl Store {
|
||||||
None => return Err(StoreError::Missing),
|
None => return Err(StoreError::Missing),
|
||||||
};
|
};
|
||||||
|
|
||||||
let stored_updated = self.id_updated.get(id.as_bytes())?.and_then(date_from_ivec);
|
|
||||||
let stored_published = self
|
|
||||||
.id_published
|
|
||||||
.get(id.as_bytes())?
|
|
||||||
.and_then(date_from_ivec);
|
|
||||||
|
|
||||||
if let Some(updated) = changes.updated {
|
|
||||||
if let Some(previously_updated) = stored_updated.or_else(|| stored_published) {
|
|
||||||
if updated < previously_updated {
|
|
||||||
return Err(StoreError::Outdated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(title) = &changes.title {
|
if let Some(title) = &changes.title {
|
||||||
self.id_title.insert(id.as_bytes(), title.as_bytes())?;
|
self.id_title.insert(id.as_bytes(), title.as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{StoreError, Undo};
|
use super::{StoreError, Undo, ValidationError, ValidationErrorKind};
|
||||||
use crate::State;
|
use crate::State;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use hyaenidae_content::{bbcode, html, strip};
|
use hyaenidae_content::{bbcode, html, strip};
|
||||||
|
@ -52,6 +52,7 @@ pub struct SubmissionChanges<'a> {
|
||||||
sensitive: Option<bool>,
|
sensitive: Option<bool>,
|
||||||
original_files: Vec<Uuid>,
|
original_files: Vec<Uuid>,
|
||||||
files: Vec<Uuid>,
|
files: Vec<Uuid>,
|
||||||
|
errors: Vec<ValidationError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -101,12 +102,13 @@ impl Submission {
|
||||||
description_source: None,
|
description_source: None,
|
||||||
visibility: None,
|
visibility: None,
|
||||||
published: self.published,
|
published: self.published,
|
||||||
updated: None,
|
updated: self.updated,
|
||||||
local_only: None,
|
local_only: None,
|
||||||
logged_in_only: None,
|
logged_in_only: None,
|
||||||
sensitive: None,
|
sensitive: None,
|
||||||
original_files: self.files.clone(),
|
original_files: self.files.clone(),
|
||||||
files: self.files.clone(),
|
files: self.files.clone(),
|
||||||
|
errors: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,11 +194,29 @@ impl<'a> SubmissionChanges<'a> {
|
||||||
sensitive: None,
|
sensitive: None,
|
||||||
original_files: vec![],
|
original_files: vec![],
|
||||||
files: vec![],
|
files: vec![],
|
||||||
|
errors: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn title(&mut self, title: &str) -> &mut Self {
|
pub(crate) fn title(&mut self, title: &str) -> &mut Self {
|
||||||
self.title = Some(strip(title));
|
let title = strip(title.trim());
|
||||||
|
|
||||||
|
if title.len() > self.state.content_config.max_submission_title_length {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("title"),
|
||||||
|
kind: ValidationErrorKind::TooLong {
|
||||||
|
maximum: self.state.content_config.max_submission_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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +226,24 @@ impl<'a> SubmissionChanges<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn description(&mut self, description: &str) -> &mut Self {
|
pub(crate) fn description(&mut self, description: &str) -> &mut Self {
|
||||||
self.description = Some(html(description));
|
let description = html(description.trim());
|
||||||
|
|
||||||
|
if description.len() > self.state.content_config.max_submission_body_length {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("description"),
|
||||||
|
kind: ValidationErrorKind::TooLong {
|
||||||
|
maximum: self.state.content_config.max_submission_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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,8 +267,18 @@ impl<'a> SubmissionChanges<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn updated(&mut self, time: DateTime<Utc>) -> &mut Self {
|
pub(crate) fn updated(&mut self, time: DateTime<Utc>) -> &mut Self {
|
||||||
if self.published.is_some() {
|
if let Some(published) = self.published {
|
||||||
self.updated = Some(time);
|
if self.updated.unwrap_or(published) > time {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("updated"),
|
||||||
|
kind: ValidationErrorKind::Outdated {
|
||||||
|
current: self.updated.unwrap_or(published),
|
||||||
|
proposed: time,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.updated = Some(time);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -256,7 +303,14 @@ impl<'a> SubmissionChanges<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_file(&mut self, file_id: Uuid) -> &mut Self {
|
pub(crate) fn add_file(&mut self, file_id: Uuid) -> &mut Self {
|
||||||
self.files.push(file_id);
|
if let Ok(Some(_)) = self.state.store.files.by_id(file_id) {
|
||||||
|
self.files.push(file_id);
|
||||||
|
} else {
|
||||||
|
self.errors.push(ValidationError {
|
||||||
|
field: String::from("files"),
|
||||||
|
kind: ValidationErrorKind::MissingFile(file_id),
|
||||||
|
});
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,8 +331,12 @@ impl<'a> SubmissionChanges<'a> {
|
||||||
|| self.original_files != self.files
|
|| self.original_files != self.files
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn save(self) -> Result<Submission, StoreError> {
|
pub(crate) fn save(self) -> Result<Result<Submission, Vec<ValidationError>>, StoreError> {
|
||||||
self.state.store.submissions.save(&self)
|
if self.errors.is_empty() {
|
||||||
|
self.state.store.submissions.save(&self).map(Ok)
|
||||||
|
} else {
|
||||||
|
Ok(Err(self.errors))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,17 +461,6 @@ impl Store {
|
||||||
let mut stored_submission: StoredSubmission =
|
let mut stored_submission: StoredSubmission =
|
||||||
serde_json::from_slice(&stored_submission_ivec)?;
|
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();
|
let already_published = stored_submission.published.is_some();
|
||||||
|
|
||||||
if let Some(title) = &changes.title {
|
if let Some(title) = &changes.title {
|
||||||
|
|
|
@ -22,6 +22,7 @@ struct JobState {
|
||||||
pub(super) fn build(
|
pub(super) fn build(
|
||||||
base_url: Url,
|
base_url: Url,
|
||||||
pict_rs_upstream: Url,
|
pict_rs_upstream: Url,
|
||||||
|
content_config: hyaenidae_profiles::ContentConfig,
|
||||||
db: sled::Db,
|
db: sled::Db,
|
||||||
) -> Result<State, anyhow::Error> {
|
) -> Result<State, anyhow::Error> {
|
||||||
let storage = Storage::new();
|
let storage = Storage::new();
|
||||||
|
@ -32,6 +33,7 @@ pub(super) fn build(
|
||||||
Spawn(queue_handle.clone()),
|
Spawn(queue_handle.clone()),
|
||||||
base_url,
|
base_url,
|
||||||
pict_rs_upstream,
|
pict_rs_upstream,
|
||||||
|
content_config,
|
||||||
Arbiter::current(),
|
Arbiter::current(),
|
||||||
&db,
|
&db,
|
||||||
)?;
|
)?;
|
||||||
|
|
85
src/main.rs
85
src/main.rs
|
@ -82,6 +82,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let state = jobs::build(
|
let state = jobs::build(
|
||||||
config.base_url.clone(),
|
config.base_url.clone(),
|
||||||
config.pictrs_upstream.clone(),
|
config.pictrs_upstream.clone(),
|
||||||
|
config.content(),
|
||||||
db.clone(),
|
db.clone(),
|
||||||
)?;
|
)?;
|
||||||
let accounts_state = hyaenidae_accounts::state(&accounts_config, db.clone())?;
|
let accounts_state = hyaenidae_accounts::state(&accounts_config, db.clone())?;
|
||||||
|
@ -195,10 +196,92 @@ struct Config {
|
||||||
)]
|
)]
|
||||||
skip_signature_validation: bool,
|
skip_signature_validation: bool,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
env = "HYAENIDAE_MAX_BIO_LENGTH",
|
||||||
|
about = "Maximum allowed characters in a profile bio",
|
||||||
|
default_value = "500"
|
||||||
|
)]
|
||||||
|
max_bio: usize,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
env = "HYAENIDAE_MAX_DISPLAY_NAME_LENGTH",
|
||||||
|
about = "Maximum allowed characters in a profile display name",
|
||||||
|
default_value = "20"
|
||||||
|
)]
|
||||||
|
max_display_name: usize,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
env = "HYAENIDAE_MAX_HANDLE_LENGTH",
|
||||||
|
about = "Maximum allowed characters in a profile handle",
|
||||||
|
default_value = "20"
|
||||||
|
)]
|
||||||
|
max_handle: usize,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
env = "HYAENIDAE_MAX_SUBMISSION_TITLE_LENGTH",
|
||||||
|
about = "Maximum allowed characters in a submission title",
|
||||||
|
default_value = "50"
|
||||||
|
)]
|
||||||
|
max_submission_title: usize,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
env = "HYAENIDAE_MAX_SUBMISSION_BODY_LENGTH",
|
||||||
|
about = "Maximum allowed characters in a submission body",
|
||||||
|
default_value = "1000"
|
||||||
|
)]
|
||||||
|
max_submission_body: usize,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
env = "HYAENIDAE_MAX_POST_TITLE_LENGTH",
|
||||||
|
about = "Maximum allowed characters in a post title",
|
||||||
|
default_value = "50"
|
||||||
|
)]
|
||||||
|
max_post_title: usize,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
env = "HYAENIDAE_MAX_POST_BODY_LENGTH",
|
||||||
|
about = "Maximum allowed characters in a post body",
|
||||||
|
default_value = "1000"
|
||||||
|
)]
|
||||||
|
max_post_body: usize,
|
||||||
|
|
||||||
|
#[structopt(
|
||||||
|
long,
|
||||||
|
env = "HYAENIDAE_MAX_COMMENT_LENGTH",
|
||||||
|
about = "Maximum allowed characters in a comment",
|
||||||
|
default_value = "500"
|
||||||
|
)]
|
||||||
|
max_comment: usize,
|
||||||
|
|
||||||
#[structopt(short, long, about = "enable debug logging")]
|
#[structopt(short, long, about = "enable debug logging")]
|
||||||
debug: bool,
|
debug: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn content(&self) -> hyaenidae_profiles::ContentConfig {
|
||||||
|
hyaenidae_profiles::ContentConfig {
|
||||||
|
max_bio_length: self.max_bio,
|
||||||
|
max_display_name_length: self.max_display_name,
|
||||||
|
max_handle_length: self.max_handle,
|
||||||
|
max_domain_length: 256,
|
||||||
|
max_submission_title_length: self.max_submission_title,
|
||||||
|
max_submission_body_length: self.max_submission_body,
|
||||||
|
max_post_title_length: self.max_post_title,
|
||||||
|
max_post_body_length: self.max_post_body,
|
||||||
|
max_comment_length: self.max_comment,
|
||||||
|
max_server_title_length: 50,
|
||||||
|
max_server_body_length: 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct SecretKey {
|
struct SecretKey {
|
||||||
inner: Vec<u8>,
|
inner: Vec<u8>,
|
||||||
|
@ -245,6 +328,7 @@ impl State {
|
||||||
spawn: jobs::Spawn,
|
spawn: jobs::Spawn,
|
||||||
base_url: url::Url,
|
base_url: url::Url,
|
||||||
pict_rs_upstream: url::Url,
|
pict_rs_upstream: url::Url,
|
||||||
|
content_config: hyaenidae_profiles::ContentConfig,
|
||||||
arbiter: Arbiter,
|
arbiter: Arbiter,
|
||||||
db: &Db,
|
db: &Db,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
|
@ -263,6 +347,7 @@ impl State {
|
||||||
apub.clone(),
|
apub.clone(),
|
||||||
spawn.clone(),
|
spawn.clone(),
|
||||||
Urls,
|
Urls,
|
||||||
|
content_config,
|
||||||
arbiter,
|
arbiter,
|
||||||
db.clone(),
|
db.clone(),
|
||||||
)?,
|
)?,
|
||||||
|
|
Loading…
Reference in a new issue