Profiles: Move bbcode, html sanitizing to Changes types

Handle Updated timestamp for profiles, servers, comments
This commit is contained in:
asonix 2021-02-08 21:14:51 -06:00
parent 0916828a0a
commit 22d8f85fba
15 changed files with 672 additions and 466 deletions

View file

@ -57,6 +57,7 @@ pub(crate) fn application(
.published() .published()
.map(|published| published.into()) .map(|published| published.into())
.unwrap_or_else(|| Utc::now()); .unwrap_or_else(|| Utc::now());
let updated = application.updated().map(|updated| updated.into());
let endpoints = Endpoints { let endpoints = Endpoints {
inbox, inbox,
@ -78,6 +79,7 @@ pub(crate) fn application(
title_source: None, title_source: None,
description, description,
description_source: None, description_source: None,
updated: updated.req("updated")?,
}))) })))
} else { } else {
Ok(Ok(Box::new(CreateServer { Ok(Ok(Box::new(CreateServer {
@ -90,6 +92,7 @@ pub(crate) fn application(
title, title,
description, description,
published, published,
updated,
}))) })))
} }
} }
@ -158,6 +161,7 @@ pub(crate) fn update_application(
.map(|s| s.to_owned()); .map(|s| s.to_owned());
let public_key_id = application.ext_one.public_key.id.clone(); let public_key_id = application.ext_one.public_key.id.clone();
let public_key = application.ext_one.public_key.public_key_pem.clone(); let public_key = application.ext_one.public_key.public_key_pem.clone();
let updated = application.updated().req("updated")?.into();
let endpoints = Endpoints { let endpoints = Endpoints {
inbox, inbox,
@ -178,6 +182,7 @@ pub(crate) fn update_application(
title_source: None, title_source: None,
description, description,
description_source: None, description_source: None,
updated,
}))) })))
} }

View file

@ -91,9 +91,10 @@ pub(super) fn note(
submission_id, submission_id,
profile_id, profile_id,
comment_id: None, comment_id: None,
body: body.to_owned(), body: Some(body.to_owned()),
body_source: None, body_source: None,
published: published.into(), published: published.into(),
updated: updated.map(|u| u.into()),
}))); })));
} }
@ -110,9 +111,10 @@ pub(super) fn note(
submission_id, submission_id,
profile_id, profile_id,
comment_id: Some(comment_id), comment_id: Some(comment_id),
body: body.to_owned(), body: Some(body.to_owned()),
body_source: None, body_source: None,
published: published.into(), published: published.into(),
updated: updated.map(|u| u.into()),
}))); })));
} }
@ -123,9 +125,8 @@ pub(super) fn note(
let title = note let title = note
.summary() .summary()
.req("summary")? .and_then(|s| s.as_single_xsd_string())
.as_single_xsd_string() .map(|s| s.to_owned());
.req("summary string")?;
let description = note let description = note
.content() .content()
.and_then(|c| c.as_single_xsd_string()) .and_then(|c| c.as_single_xsd_string())
@ -174,7 +175,7 @@ pub(super) fn note(
return Ok(Ok(Box::new(CreateSubmission { return Ok(Ok(Box::new(CreateSubmission {
note_apub_id: Some(note_id.to_owned()), note_apub_id: Some(note_id.to_owned()),
profile_id, profile_id,
title: title.to_owned(), title,
description, description,
published: Some(published.into()), published: Some(published.into()),
updated: updated.map(|u| u.into()), updated: updated.map(|u| u.into()),
@ -324,6 +325,7 @@ pub(super) fn update_note(
let note_id = note.id_unchecked().req("note id")?; let note_id = note.id_unchecked().req("note id")?;
let note_id = recover!(note_id, ctx.apub.id_for_apub(note_id)?); let note_id = recover!(note_id, ctx.apub.id_for_apub(note_id)?);
let updated = note.updated().req("updated")?;
let profile_id = recover!(attributed_to, ctx.apub.id_for_apub(attributed_to)?); let profile_id = recover!(attributed_to, ctx.apub.id_for_apub(attributed_to)?);
let profile_id = profile_id let profile_id = profile_id
@ -353,6 +355,7 @@ pub(super) fn update_note(
comment_id, comment_id,
body, body,
body_source: None, body_source: None,
updated: updated.into(),
}))); })));
} }
@ -376,7 +379,6 @@ pub(super) fn update_note(
.and_then(|c| c.as_single_xsd_string()) .and_then(|c| c.as_single_xsd_string())
.map(|s| s.to_owned()); .map(|s| s.to_owned());
let published = note.published().req("published")?; let published = note.published().req("published")?;
let updated = note.updated().req("updated")?;
let mut missing_files = vec![]; let mut missing_files = vec![];
let mut existing_files = vec![]; let mut existing_files = vec![];
@ -425,7 +427,7 @@ pub(super) fn update_note(
description_source: None, description_source: None,
visibility: Some(find_visibility(note)), visibility: Some(find_visibility(note)),
published: Some(published.into()), published: Some(published.into()),
updated: Some(updated.into()), updated: updated.into(),
removed_files: None, removed_files: None,
new_files: None, new_files: None,
only_files: Some(existing_files), only_files: Some(existing_files),

View file

@ -137,7 +137,7 @@ pub(crate) fn person(
login_required: Some(login_required), login_required: Some(login_required),
icon, icon,
banner, banner,
updated, updated: updated.req("updated")?,
}))) })))
} else { } else {
Ok(Ok(Box::new(CreateProfile { Ok(Ok(Box::new(CreateProfile {
@ -223,7 +223,7 @@ pub(crate) fn update_person(
.map(|s| s.to_owned()); .map(|s| s.to_owned());
let public_key_id = person.ext_one.public_key.id.clone(); let public_key_id = person.ext_one.public_key.id.clone();
let public_key = person.ext_one.public_key.public_key_pem.clone(); let public_key = person.ext_one.public_key.public_key_pem.clone();
let updated = person.updated().map(|u| u.into()); let updated = person.updated().map(|u| u.into()).req("updated")?;
let login_required = person let login_required = person
.to() .to()

View file

@ -30,21 +30,24 @@ impl Action for CreateComment {
None None
}; };
let comment = ctx.store.comments.create( let mut changes =
self.submission_id,
self.profile_id,
self.comment_id,
&hyaenidae_content::html(&self.body),
self.published,
)?;
let comment = if let Some(body_source) = &self.body_source {
ctx.store ctx.store
.comments .comments
.update(comment.update().body_source(body_source))? .create(ctx, self.submission_id, self.profile_id, self.comment_id);
} else {
comment changes.published(self.published);
};
if let Some(updated) = self.updated {
changes.updated(updated);
}
if let Some(body) = &self.body {
changes.body(body);
}
if let Some(body_source) = &self.body_source {
changes.body_source(body_source);
}
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())?;
@ -112,15 +115,16 @@ impl Action for UpdateComment {
return Err(Error::Deleted); return Err(Error::Deleted);
} }
let mut changes = comment.update(); let mut changes = comment.update(ctx);
changes.updated(self.updated);
if let Some(body) = &self.body { if let Some(body) = &self.body {
changes.body(&hyaenidae_content::html(body)); changes.body(body);
} }
if let Some(body_source) = &self.body_source { if let Some(body_source) = &self.body_source {
changes.body_source(body_source); changes.body_source(body_source);
} }
let comment = if changes.any_changes() { let comment = if changes.any_changes() {
ctx.store.comments.update(&changes)? changes.save()?
} else { } else {
comment comment
}; };

View file

@ -3,7 +3,6 @@ use crate::{
store::{OwnerSource, Visibility}, store::{OwnerSource, Visibility},
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::bbcode;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
@ -36,6 +35,7 @@ pub struct CreateServer {
title: Option<String>, title: Option<String>,
description: Option<String>, description: Option<String>,
published: DateTime<Utc>, published: DateTime<Utc>,
updated: Option<DateTime<Utc>>,
} }
impl CreateServer { impl CreateServer {
@ -46,6 +46,7 @@ impl CreateServer {
title: None, title: None,
description: None, description: None,
published: Utc::now(), published: Utc::now(),
updated: None,
} }
} }
} }
@ -62,31 +63,23 @@ pub struct UpdateServer {
title_source: Option<String>, title_source: Option<String>,
description: Option<String>, description: Option<String>,
description_source: Option<String>, description_source: Option<String>,
updated: DateTime<Utc>,
} }
impl UpdateServer { impl UpdateServer {
pub fn from_text(server_id: Uuid, title: Option<String>, description: Option<String>) -> Self { pub fn from_text(
let title_source = title.and_then(|title| { server_id: Uuid,
if title.trim().is_empty() { title_source: Option<String>,
None description_source: Option<String>,
} else { ) -> Self {
Some(title.trim().to_owned())
}
});
let description_source = description.and_then(|description| {
if description.trim().is_empty() {
None
} else {
Some(description.trim().to_owned())
}
});
UpdateServer { UpdateServer {
apub: None, apub: None,
server_id, server_id,
title: title_source.clone(), title: None,
title_source, title_source,
description: description_source.as_deref().map(|s| bbcode(s, |v| v)), description: None,
description_source, description_source,
updated: Utc::now(),
} }
} }
} }
@ -194,9 +187,10 @@ pub struct CreateComment {
submission_id: Uuid, submission_id: Uuid,
profile_id: Uuid, profile_id: Uuid,
comment_id: Option<Uuid>, comment_id: Option<Uuid>,
body: String, body: Option<String>,
body_source: Option<String>, body_source: Option<String>,
published: DateTime<Utc>, published: DateTime<Utc>,
updated: Option<DateTime<Utc>>,
} }
impl CreateComment { impl CreateComment {
@ -204,19 +198,17 @@ impl CreateComment {
submission_id: Uuid, submission_id: Uuid,
profile_id: Uuid, profile_id: Uuid,
comment_id: Option<Uuid>, comment_id: Option<Uuid>,
body: String, body_source: String,
) -> Self { ) -> Self {
let body_source = body;
let body = bbcode(&body_source, |v| v);
CreateComment { CreateComment {
note_apub_id: None, note_apub_id: None,
submission_id, submission_id,
profile_id, profile_id,
comment_id, comment_id,
body, body: None,
body_source: Some(body_source), body_source: Some(body_source),
published: Utc::now(), published: Utc::now(),
updated: None,
} }
} }
} }
@ -228,21 +220,17 @@ pub struct UpdateComment {
comment_id: Uuid, comment_id: Uuid,
body: Option<String>, body: Option<String>,
body_source: Option<String>, body_source: Option<String>,
updated: DateTime<Utc>,
} }
impl UpdateComment { impl UpdateComment {
pub fn from_text(comment_id: Uuid, body: String) -> Self { pub fn from_text(comment_id: Uuid, body_source: String) -> Self {
let body_source = if body.trim().is_empty() {
None
} else {
Some(body.trim().to_owned())
};
UpdateComment { UpdateComment {
update_apub_id: None, update_apub_id: None,
comment_id, comment_id,
body: body_source.as_deref().map(|s| bbcode(s, |v| v)), body: None,
body_source, body_source: Some(body_source),
updated: Utc::now(),
} }
} }
} }
@ -264,7 +252,7 @@ impl DeleteComment {
pub struct CreateSubmission { pub struct CreateSubmission {
note_apub_id: Option<Url>, note_apub_id: Option<Url>,
profile_id: Uuid, profile_id: Uuid,
title: String, title: Option<String>,
description: Option<String>, description: Option<String>,
files: Vec<Uuid>, files: Vec<Uuid>,
published: Option<DateTime<Utc>>, published: Option<DateTime<Utc>>,
@ -280,7 +268,7 @@ impl CreateSubmission {
CreateSubmission { CreateSubmission {
note_apub_id: None, note_apub_id: None,
profile_id, profile_id,
title: "".to_owned(), title: None,
description: None, description: None,
files: vec![file_id], files: vec![file_id],
published: None, published: None,
@ -303,7 +291,7 @@ pub struct UpdateSubmission {
local_only: Option<bool>, local_only: Option<bool>,
logged_in_only: Option<bool>, logged_in_only: Option<bool>,
published: Option<DateTime<Utc>>, published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>, updated: DateTime<Utc>,
removed_files: Option<Vec<Uuid>>, removed_files: Option<Vec<Uuid>>,
new_files: Option<Vec<Uuid>>, new_files: Option<Vec<Uuid>>,
only_files: Option<Vec<Uuid>>, only_files: Option<Vec<Uuid>>,
@ -311,32 +299,22 @@ pub struct UpdateSubmission {
} }
impl UpdateSubmission { impl UpdateSubmission {
pub fn from_text(submission_id: Uuid, title: String, description: Option<String>) -> Self { pub fn from_text(
let title_source = if title.trim().is_empty() { submission_id: Uuid,
None title_source: String,
} else { description_source: Option<String>,
Some(title.trim().to_owned()) ) -> Self {
};
let description_source = description.and_then(|s| {
if s.trim().is_empty() {
None
} else {
Some(s.trim().to_owned())
}
});
UpdateSubmission { UpdateSubmission {
submission_id, submission_id,
title: title_source.clone(), title: None,
title_source, title_source: Some(title_source),
description: description_source.as_deref().map(|s| bbcode(s, |v| v)), description: None,
description_source, description_source,
visibility: None, visibility: None,
local_only: None, local_only: None,
logged_in_only: None, logged_in_only: None,
published: None, published: None,
updated: Some(Utc::now()), updated: Utc::now(),
removed_files: None, removed_files: None,
new_files: None, new_files: None,
only_files: None, only_files: None,
@ -360,7 +338,7 @@ impl UpdateSubmission {
local_only: Some(local_only), local_only: Some(local_only),
logged_in_only: Some(logged_in_only), logged_in_only: Some(logged_in_only),
published: None, published: None,
updated: Some(Utc::now()), updated: Utc::now(),
removed_files: None, removed_files: None,
new_files: None, new_files: None,
only_files: None, only_files: None,
@ -379,7 +357,7 @@ impl UpdateSubmission {
local_only: None, local_only: None,
logged_in_only: None, logged_in_only: None,
published: None, published: None,
updated: Some(Utc::now()), updated: Utc::now(),
removed_files: None, removed_files: None,
new_files: Some(vec![file_id]), new_files: Some(vec![file_id]),
only_files: None, only_files: None,
@ -396,7 +374,7 @@ impl UpdateSubmission {
description_source: None, description_source: None,
visibility: None, visibility: None,
published: None, published: None,
updated: Some(Utc::now()), updated: Utc::now(),
local_only: None, local_only: None,
logged_in_only: None, logged_in_only: None,
removed_files: Some(vec![file_id]), removed_files: Some(vec![file_id]),
@ -417,7 +395,7 @@ impl UpdateSubmission {
local_only: None, local_only: None,
logged_in_only: None, logged_in_only: None,
published: None, published: None,
updated: None, updated: Utc::now(),
removed_files: None, removed_files: None,
new_files: None, new_files: None,
only_files: None, only_files: None,
@ -436,7 +414,7 @@ impl UpdateSubmission {
local_only: None, local_only: None,
logged_in_only: None, logged_in_only: None,
published: Some(Utc::now()), published: Some(Utc::now()),
updated: None, updated: Utc::now(),
removed_files: None, removed_files: None,
new_files: None, new_files: None,
only_files: None, only_files: None,
@ -508,34 +486,26 @@ pub struct UpdateProfile {
login_required: Option<bool>, login_required: Option<bool>,
icon: Option<Uuid>, icon: Option<Uuid>,
banner: Option<Uuid>, banner: Option<Uuid>,
updated: Option<DateTime<Utc>>, updated: DateTime<Utc>,
} }
impl UpdateProfile { impl UpdateProfile {
pub fn from_text(profile_id: Uuid, display_name: String, description: String) -> Self { pub fn from_text(
let display_name_source = if display_name.trim().is_empty() { profile_id: Uuid,
None display_name_source: String,
} else { description_source: String,
Some(display_name.trim().to_owned()) ) -> Self {
};
let description_source = if description.trim().is_empty() {
None
} else {
Some(description.trim().to_owned())
};
UpdateProfile { UpdateProfile {
apub: None, apub: None,
profile_id, profile_id,
display_name: display_name_source.clone(), display_name: None,
display_name_source, display_name_source: Some(display_name_source),
description: description_source.as_deref().map(|s| bbcode(s, |v| v)), description: None,
description_source, description_source: Some(description_source),
login_required: None, login_required: None,
icon: None, icon: None,
banner: None, banner: None,
updated: Some(Utc::now()), updated: Utc::now(),
} }
} }
@ -550,7 +520,7 @@ impl UpdateProfile {
login_required: None, login_required: None,
icon: Some(icon), icon: Some(icon),
banner: None, banner: None,
updated: Some(Utc::now()), updated: Utc::now(),
} }
} }
@ -565,7 +535,7 @@ impl UpdateProfile {
login_required: None, login_required: None,
icon: None, icon: None,
banner: Some(banner), banner: Some(banner),
updated: Some(Utc::now()), updated: Utc::now(),
} }
} }
@ -580,7 +550,7 @@ impl UpdateProfile {
login_required: Some(login_required), login_required: Some(login_required),
icon: None, icon: None,
banner: None, banner: None,
updated: Some(Utc::now()), updated: Utc::now(),
} }
} }
} }

View file

@ -9,20 +9,20 @@ use crate::{
impl Action for CreateProfile { impl Action for CreateProfile {
fn perform(&self, ctx: &Context) -> Result<Option<Box<dyn Outbound + Send>>, Error> { fn perform(&self, ctx: &Context) -> Result<Option<Box<dyn Outbound + Send>>, Error> {
log::debug!("CreateProfile"); log::debug!("CreateProfile");
let profile = ctx.store.profiles.create( let mut changes = ctx.store.profiles.create(
ctx,
self.owner_source.clone(), self.owner_source.clone(),
&self.handle, self.handle.clone(),
&self.domain, self.domain.clone(),
self.published, self.published,
)?; );
let mut changes = profile.update();
changes.login_required(self.login_required); changes.login_required(self.login_required);
if let Some(display_name) = &self.display_name { if let Some(display_name) = &self.display_name {
changes.display_name(&hyaenidae_content::html(display_name)); changes.display_name(display_name);
} }
if let Some(description) = &self.description { if let Some(description) = &self.description {
changes.description(&hyaenidae_content::html(description)); changes.description(description);
} }
if let Some(banner) = self.banner { if let Some(banner) = self.banner {
changes.banner(banner); changes.banner(banner);
@ -34,11 +34,7 @@ impl Action for CreateProfile {
changes.updated(updated); changes.updated(updated);
} }
let profile = if changes.any_changes() { let profile = changes.save()?;
ctx.store.profiles.update(&changes)?
} else {
profile
};
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())?;
@ -75,25 +71,23 @@ impl Action for UpdateProfile {
.by_id(self.profile_id)? .by_id(self.profile_id)?
.req("profile by id")?; .req("profile by id")?;
let mut changes = profile.update(); let mut changes = profile.update(ctx);
changes.updated(self.updated);
if let Some(login_required) = self.login_required { if let Some(login_required) = self.login_required {
changes.login_required(login_required); changes.login_required(login_required);
} }
if let Some(display_name) = &self.display_name { if let Some(display_name) = &self.display_name {
changes.display_name(&hyaenidae_content::html(display_name)); changes.display_name(display_name);
} }
if let Some(display_name_source) = &self.display_name_source { if let Some(display_name_source) = &self.display_name_source {
changes.display_name_source(display_name_source); changes.display_name_source(display_name_source);
} }
if let Some(description) = &self.description { if let Some(description) = &self.description {
changes.description(&hyaenidae_content::html(description)); changes.description(description);
} }
if let Some(description_source) = &self.description_source { if let Some(description_source) = &self.description_source {
changes.description_source(description_source); changes.description_source(description_source);
} }
if let Some(updated) = self.updated {
changes.updated(updated);
}
let previous_banner = profile.banner(); let previous_banner = profile.banner();
let previous_icon = profile.icon(); let previous_icon = profile.icon();
@ -116,7 +110,7 @@ impl Action for UpdateProfile {
} }
let profile = if changes.any_changes() { let profile = if changes.any_changes() {
ctx.store.profiles.update(&changes)? changes.save()?
} else { } else {
profile profile
}; };

View file

@ -5,20 +5,19 @@ use crate::{
impl Action for CreateServer { impl Action for CreateServer {
fn perform(&self, ctx: &Context) -> Result<Option<Box<dyn Outbound + Send>>, Error> { fn perform(&self, ctx: &Context) -> Result<Option<Box<dyn Outbound + Send>>, Error> {
let server = ctx.store.servers.create(&self.domain, self.published)?; let mut changes = ctx.store.servers.create(ctx, self.domain.clone());
let mut changes = server.changes(); changes.published(self.published);
if let Some(updated) = self.updated {
changes.updated(updated);
}
if let Some(title) = &self.title { if let Some(title) = &self.title {
changes.title(&hyaenidae_content::html(title)); changes.title(title);
} }
if let Some(description) = &self.description { if let Some(description) = &self.description {
changes.description(&hyaenidae_content::html(description)); changes.description(description);
} }
let server = if changes.any_changes() { let server = changes.save()?;
ctx.store.servers.update(&changes)?
} else {
server
};
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())?;
@ -52,21 +51,22 @@ impl Action for UpdateServer {
.by_id(self.server_id)? .by_id(self.server_id)?
.req("server by id")?; .req("server by id")?;
let mut changes = server.changes(); let mut changes = server.changes(ctx);
changes.updated(self.updated);
if let Some(title) = &self.title { if let Some(title) = &self.title {
changes.title(&hyaenidae_content::html(title)); changes.title(title);
} }
if let Some(title_source) = &self.title_source { if let Some(title_source) = &self.title_source {
changes.title_source(title_source); changes.title_source(title_source);
} }
if let Some(description) = &self.description { if let Some(description) = &self.description {
changes.description(&hyaenidae_content::html(description)); changes.description(description);
} }
if let Some(description_source) = &self.description_source { if let Some(description_source) = &self.description_source {
changes.description_source(description_source); changes.description_source(description_source);
} }
let server = if changes.any_changes() { let server = if changes.any_changes() {
ctx.store.servers.update(&changes)? changes.save()?
} else { } else {
server server
}; };

View file

@ -7,25 +7,22 @@ use std::collections::HashSet;
impl Action for CreateSubmission { impl Action for CreateSubmission {
fn perform(&self, ctx: &Context) -> Result<Option<Box<dyn Outbound + Send>>, Error> { fn perform(&self, ctx: &Context) -> Result<Option<Box<dyn Outbound + Send>>, Error> {
log::debug!("CreateSubmission"); log::debug!("CreateSubmission");
let submission = ctx.store.submissions.create( let mut changes = ctx.store.submissions.create(ctx, self.profile_id);
self.profile_id,
&hyaenidae_content::html(&self.title),
self.visibility,
)?;
let submission_id = submission.id();
let mut changes = submission.update();
changes changes
.local_only(self.local_only) .local_only(self.local_only)
.logged_in_only(self.logged_in_only) .logged_in_only(self.logged_in_only)
.sensitive(self.sensitive); .sensitive(self.sensitive)
.visibility(self.visibility);
if self.published.is_some() { if self.published.is_some() {
changes.published(self.published); changes.published(self.published);
} }
if let Some(title) = &self.title {
changes.title(title);
}
if let Some(description) = &self.description { if let Some(description) = &self.description {
changes.description(&hyaenidae_content::html(description)); changes.description(description);
} }
if let Some(updated) = self.updated { if let Some(updated) = self.updated {
changes.updated(updated); changes.updated(updated);
@ -35,11 +32,8 @@ impl Action for CreateSubmission {
changes.add_file(*file); changes.add_file(*file);
} }
let submission = if changes.any_changes() { let submission = changes.save()?;
ctx.store.submissions.update(&changes)? let submission_id = submission.id();
} else {
submission
};
if let Some(apub_id) = &self.note_apub_id { if let Some(apub_id) = &self.note_apub_id {
ctx.apub.submission(apub_id, submission_id)?; ctx.apub.submission(apub_id, submission_id)?;
@ -95,21 +89,19 @@ impl Action for UpdateSubmission {
let initial_published = submission.published().is_some(); let initial_published = submission.published().is_some();
let mut changes = submission.update(); let mut changes = submission.update(ctx);
changes.updated(self.updated);
if let Some(published) = self.published { if let Some(published) = self.published {
changes.published(Some(published)); changes.published(Some(published));
} }
if let Some(updated) = self.updated {
changes.updated(updated);
}
if let Some(title) = &self.title { if let Some(title) = &self.title {
changes.title(&hyaenidae_content::html(title)); changes.title(title);
} }
if let Some(title_source) = &self.title_source { if let Some(title_source) = &self.title_source {
changes.title_source(title_source); changes.title_source(title_source);
} }
if let Some(description) = &self.description { if let Some(description) = &self.description {
changes.description(&hyaenidae_content::html(description)); changes.description(description);
} }
if let Some(description_source) = &self.description_source { if let Some(description_source) = &self.description_source {
changes.description_source(description_source); changes.description_source(description_source);
@ -158,7 +150,7 @@ impl Action for UpdateSubmission {
} }
let submission = if changes.any_changes() { let submission = if changes.any_changes() {
ctx.store.submissions.update(&changes)? changes.save()?
} else { } else {
submission submission
}; };

View file

@ -100,7 +100,7 @@ fn build_comment(
commenter_id: Url, commenter_id: Url,
ctx: &Context, ctx: &Context,
) -> Result<AnyBase, Error> { ) -> Result<AnyBase, Error> {
let published = comment.published(); let published = comment.published().req("comment must be published")?;
let endpoints = ctx let endpoints = ctx
.apub .apub
.endpoints_for_profile(comment.profile_id())? .endpoints_for_profile(comment.profile_id())?
@ -124,6 +124,9 @@ fn build_comment(
.set_published(published.into()) .set_published(published.into())
.set_attributed_to(commenter_id) .set_attributed_to(commenter_id)
.add_cc(public()); .add_cc(public());
if let Some(updated) = comment.updated() {
note.set_updated(updated.into());
}
if let Some(followers) = endpoints.followers { if let Some(followers) = endpoints.followers {
note.add_to(followers); note.add_to(followers);
} }

View file

@ -94,6 +94,9 @@ impl Outbound for ServerCreated {
}) })
.set_preferred_username(server.domain()) .set_preferred_username(server.domain())
.set_published(server.created().into()); .set_published(server.created().into());
if let Some(updated) = server.updated() {
application.set_updated(updated.into());
}
if let Some(title) = server.title() { if let Some(title) = server.title() {
application.set_name(title); application.set_name(title);
@ -190,6 +193,9 @@ impl Outbound for ServerUpdated {
.set_preferred_username(server.domain()) .set_preferred_username(server.domain())
.set_published(server.created().into()); .set_published(server.created().into());
if let Some(updated) = server.updated() {
application.set_updated(updated.into());
}
if let Some(outbox) = endpoints.outbox { if let Some(outbox) = endpoints.outbox {
application.set_outbox(outbox); application.set_outbox(outbox);
} }

View file

@ -1,9 +1,46 @@
use super::{Comment, CommentChanges, ReplyComment, StoreError, SubmissionComment, Undo}; use super::{StoreError, Undo};
use crate::State;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html};
use sled::{Db, Transactional, Tree}; use sled::{Db, Transactional, Tree};
use std::io::Cursor; use std::io::Cursor;
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct Comment {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
comment_id: Option<Uuid>,
body: String,
body_source: Option<String>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
deleted: bool,
}
#[derive(Debug)]
pub enum CommentChangesKind {
Create {
profile_id: Uuid,
submission_id: Uuid,
comment_id: Option<Uuid>,
},
Update {
id: Uuid,
},
}
#[derive(Debug)]
pub struct CommentChanges<'a> {
state: &'a State,
kind: CommentChangesKind,
body: Option<String>,
body_source: Option<String>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Store { pub struct Store {
comment_tree: Tree, comment_tree: Tree,
@ -24,13 +61,123 @@ struct StoredComment {
body: String, body: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
body_source: Option<String>, body_source: Option<String>,
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>>,
created_at: DateTime<Utc>, created_at: DateTime<Utc>,
updated_at: DateTime<Utc>, updated_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
deleted_at: Option<DateTime<Utc>>, deleted_at: Option<DateTime<Utc>>,
} }
impl Comment {
pub fn id(&self) -> Uuid {
self.id
}
pub fn submission_id(&self) -> Uuid {
self.submission_id
}
pub fn profile_id(&self) -> Uuid {
self.profile_id
}
pub fn comment_id(&self) -> Option<Uuid> {
self.comment_id
}
pub fn body(&self) -> &str {
if !self.deleted() {
self.body.as_str()
} else {
"Comment Deleted"
}
}
pub fn body_source(&self) -> Option<&str> {
self.body_source.as_deref()
}
pub fn published(&self) -> Option<DateTime<Utc>> {
self.published
}
pub fn updated(&self) -> Option<DateTime<Utc>> {
self.updated
}
pub(crate) fn update<'a>(&self, state: &'a State) -> CommentChanges<'a> {
CommentChanges {
state,
kind: CommentChangesKind::Update { id: self.id() },
body: None,
body_source: None,
published: self.published,
updated: None,
}
}
pub fn deleted(&self) -> bool {
self.deleted
}
}
impl<'a> CommentChanges<'a> {
fn new(
state: &'a State,
profile_id: Uuid,
submission_id: Uuid,
comment_id: Option<Uuid>,
) -> Self {
CommentChanges {
state,
kind: CommentChangesKind::Create {
profile_id,
submission_id,
comment_id,
},
body: None,
body_source: None,
published: None,
updated: None,
}
}
pub(crate) fn body(&mut self, body: &str) -> &mut Self {
self.body = Some(html(body));
self
}
pub(crate) fn body_source(&mut self, body_source: &str) -> &mut Self {
self.body_source = Some(body_source.to_owned());
self.body(&bbcode(body_source, |v| v))
}
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 {
if self.published.is_some() {
self.updated = Some(updated);
}
self
}
pub(crate) fn any_changes(&self) -> bool {
self.body.is_some() || self.published.is_some() || self.updated.is_some()
}
pub(crate) fn save(self) -> Result<Comment, StoreError> {
self.state.store.comments.save(&self)
}
}
impl Store { impl Store {
pub(super) fn build(db: &Db) -> Result<Self, sled::Error> { pub(super) fn build(db: &Db) -> Result<Self, sled::Error> {
Ok(Store { Ok(Store {
@ -43,14 +190,36 @@ impl Store {
}) })
} }
pub(crate) fn create( pub(crate) fn create<'a>(
&self,
state: &'a State,
submission_id: Uuid,
profile_id: Uuid,
comment_id: Option<Uuid>,
) -> CommentChanges<'a> {
CommentChanges::new(state, submission_id, profile_id, comment_id)
}
fn save(&self, changes: &CommentChanges) -> Result<Comment, StoreError> {
match &changes.kind {
CommentChangesKind::Create {
profile_id,
submission_id,
comment_id,
} => {
let id = self.do_create(*submission_id, *profile_id, *comment_id)?;
self.do_update(id, changes)
}
CommentChangesKind::Update { id } => self.do_update(*id, changes),
}
}
pub(crate) fn do_create(
&self, &self,
submission_id: Uuid, submission_id: Uuid,
profile_id: Uuid, profile_id: Uuid,
comment_id: Option<Uuid>, comment_id: Option<Uuid>,
body: &str, ) -> Result<Uuid, StoreError> {
published: DateTime<Utc>,
) -> Result<Comment, StoreError> {
let mut id; let mut id;
let mut stored_comment; let mut stored_comment;
@ -66,9 +235,10 @@ impl Store {
submission_id, submission_id,
profile_id, profile_id,
comment_id, comment_id,
body: body.to_owned(), body: String::new(),
body_source: None, body_source: None,
published, published: None,
updated: None,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
deleted_at: None, deleted_at: None,
@ -128,7 +298,7 @@ impl Store {
return Err(e.into()); return Err(e.into());
} }
Ok(stored_comment.into()) Ok(id)
} }
pub fn count(&self, submission_id: Uuid) -> Result<u64, StoreError> { pub fn count(&self, submission_id: Uuid) -> Result<u64, StoreError> {
@ -141,20 +311,36 @@ impl Store {
} }
} }
pub(crate) fn update(&self, changes: &CommentChanges) -> Result<Comment, StoreError> { fn do_update(&self, id: Uuid, changes: &CommentChanges) -> Result<Comment, StoreError> {
let stored_comment_ivec = match self.comment_tree.get(id_comment_key(changes.id))? { let stored_comment_ivec = match self.comment_tree.get(id_comment_key(id))? {
Some(ivec) => ivec, Some(ivec) => ivec,
None => return Err(StoreError::Missing), None => return Err(StoreError::Missing),
}; };
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();
} }
if let Some(body_source) = &changes.body_source { if let Some(body_source) = &changes.body_source {
stored_comment.body_source = Some(body_source.clone()); stored_comment.body_source = Some(body_source.clone());
} }
if let Some(published) = changes.published {
stored_comment.published = Some(published);
}
if let Some(updated) = changes.updated {
stored_comment.updated = Some(updated);
}
stored_comment.updated_at = Utc::now().into(); stored_comment.updated_at = Utc::now().into();
let stored_comment_vec = serde_json::to_vec(&stored_comment)?; let stored_comment_vec = serde_json::to_vec(&stored_comment)?;
@ -162,7 +348,7 @@ impl Store {
if self if self
.comment_tree .comment_tree
.compare_and_swap( .compare_and_swap(
id_comment_key(changes.id), id_comment_key(id),
Some(&stored_comment_ivec), Some(&stored_comment_ivec),
Some(stored_comment_vec.as_slice()), Some(stored_comment_vec.as_slice()),
)? )?
@ -356,27 +542,16 @@ fn uuid_from_ivec(ivec: sled::IVec) -> Option<Uuid> {
impl From<StoredComment> for Comment { impl From<StoredComment> for Comment {
fn from(sc: StoredComment) -> Self { fn from(sc: StoredComment) -> Self {
if let Some(comment_id) = sc.comment_id { Comment {
Comment::Reply(ReplyComment { id: sc.id,
id: sc.id, submission_id: sc.submission_id,
submission_id: sc.submission_id, profile_id: sc.profile_id,
profile_id: sc.profile_id, comment_id: sc.comment_id,
comment_id, body: sc.body,
body: sc.body, body_source: sc.body_source,
body_source: sc.body_source, published: sc.published,
published: sc.published, updated: sc.updated,
deleted: sc.deleted_at.is_some(), deleted: sc.deleted_at.is_some(),
})
} else {
Comment::Submission(SubmissionComment {
id: sc.id,
submission_id: sc.submission_id,
profile_id: sc.profile_id,
body: sc.body,
body_source: sc.body_source,
published: sc.published,
deleted: sc.deleted_at.is_some(),
})
} }
} }
} }

View file

@ -13,7 +13,7 @@ mod submission;
mod term_search; mod term_search;
pub mod view; pub mod view;
pub use comment::Store as CommentStore; pub use comment::{Comment, CommentChanges, Store as CommentStore};
pub use file::Store as FileStore; pub use file::Store as FileStore;
pub use profile::{OwnerSource, Profile, ProfileChanges, Store as ProfileStore}; pub use profile::{OwnerSource, Profile, ProfileChanges, Store as ProfileStore};
pub use react::Store as ReactStore; pub use react::Store as ReactStore;
@ -289,124 +289,6 @@ impl File {
} }
} }
#[derive(Clone, Debug)]
pub enum Comment {
Submission(SubmissionComment),
Reply(ReplyComment),
}
impl Comment {
pub fn id(&self) -> Uuid {
match self {
Comment::Reply(ReplyComment { id, .. }) => *id,
Comment::Submission(SubmissionComment { id, .. }) => *id,
}
}
pub fn submission_id(&self) -> Uuid {
match self {
Comment::Reply(ReplyComment { submission_id, .. }) => *submission_id,
Comment::Submission(SubmissionComment { submission_id, .. }) => *submission_id,
}
}
pub fn profile_id(&self) -> Uuid {
match self {
Comment::Reply(ReplyComment { profile_id, .. }) => *profile_id,
Comment::Submission(SubmissionComment { profile_id, .. }) => *profile_id,
}
}
pub fn comment_id(&self) -> Option<Uuid> {
match self {
Comment::Reply(ReplyComment { comment_id, .. }) => Some(*comment_id),
_ => None,
}
}
pub fn body(&self) -> &str {
if !self.deleted() {
match self {
Comment::Reply(ReplyComment { body, .. }) => &body,
Comment::Submission(SubmissionComment { body, .. }) => &body,
}
} else {
"Comment Deleted"
}
}
pub fn body_source(&self) -> Option<&str> {
match self {
Comment::Reply(ReplyComment { body_source, .. }) => body_source.as_deref(),
Comment::Submission(SubmissionComment { body_source, .. }) => body_source.as_deref(),
}
}
pub fn published(&self) -> DateTime<Utc> {
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(),
}
}
pub fn deleted(&self) -> bool {
match self {
Comment::Reply(rc) => rc.deleted,
Comment::Submission(sc) => sc.deleted,
}
}
}
#[derive(Clone, Debug)]
pub struct SubmissionComment {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
body: String,
body_source: Option<String>,
published: DateTime<Utc>,
deleted: bool,
}
impl SubmissionComment {
fn update(&self) -> CommentChanges {
CommentChanges {
id: self.id,
body: None,
body_source: None,
}
}
}
#[derive(Clone, Debug)]
pub struct ReplyComment {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
comment_id: Uuid,
body: String,
body_source: Option<String>,
published: DateTime<Utc>,
deleted: bool,
}
impl ReplyComment {
fn update(&self) -> CommentChanges {
CommentChanges {
id: self.id,
body: None,
body_source: None,
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum React { pub enum React {
Submission(SubmissionReact), Submission(SubmissionReact),
@ -479,29 +361,6 @@ pub struct ReplyReact {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Undo<T>(pub T); pub struct Undo<T>(pub T);
#[derive(Debug)]
pub struct CommentChanges {
id: Uuid,
body: Option<String>,
body_source: Option<String>,
}
impl CommentChanges {
pub(crate) fn body(&mut self, body: &str) -> &mut CommentChanges {
self.body = Some(body.to_owned());
self
}
pub(crate) fn body_source(&mut self, body_source: &str) -> &mut CommentChanges {
self.body_source = Some(body_source.to_owned());
self
}
pub(crate) fn any_changes(&self) -> bool {
self.body.is_some()
}
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum StoreError { pub enum StoreError {
#[error("{0}")] #[error("{0}")]

View file

@ -1,12 +1,28 @@
use super::{StoreError, TermSearch, Undo}; use super::{StoreError, TermSearch, Undo};
use crate::State;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html, strip};
use sled::{Db, Transactional, Tree}; use sled::{Db, Transactional, Tree};
use std::io::Cursor; use std::io::Cursor;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug)] #[derive(Debug)]
pub struct ProfileChanges { enum ProfileChangesKind {
id: Uuid, Create {
source: OwnerSource,
domain: String,
handle: String,
published: DateTime<Utc>,
},
Update {
id: Uuid,
},
}
#[derive(Debug)]
pub struct ProfileChanges<'a> {
state: &'a State,
kind: ProfileChangesKind,
display_name: Option<String>, display_name: Option<String>,
display_name_source: Option<String>, display_name_source: Option<String>,
description: Option<String>, description: Option<String>,
@ -17,7 +33,7 @@ pub struct ProfileChanges {
updated: Option<DateTime<Utc>>, updated: Option<DateTime<Utc>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Copy, Debug)]
pub enum OwnerSource { pub enum OwnerSource {
Local(Uuid), Local(Uuid),
Remote(Uuid), Remote(Uuid),
@ -82,25 +98,51 @@ struct StoredProfile {
suspended_at: Option<DateTime<Utc>>, suspended_at: Option<DateTime<Utc>>,
} }
impl ProfileChanges { impl<'a> ProfileChanges<'a> {
pub(crate) fn new(
state: &'a State,
source: OwnerSource,
domain: String,
handle: String,
published: DateTime<Utc>,
) -> Self {
ProfileChanges {
state,
kind: ProfileChangesKind::Create {
source,
domain,
handle,
published,
},
display_name: None,
display_name_source: None,
description: None,
description_source: None,
login_required: None,
icon: None,
banner: None,
updated: None,
}
}
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(display_name.to_owned()); self.display_name = Some(strip(display_name));
self self
} }
pub(crate) fn display_name_source(&mut self, display_name_source: &str) -> &mut Self { pub(crate) fn display_name_source(&mut self, display_name_source: &str) -> &mut Self {
self.display_name_source = Some(display_name_source.to_owned()); self.display_name_source = Some(display_name_source.to_owned());
self self.display_name(display_name_source)
} }
pub(crate) fn description(&mut self, description: &str) -> &mut Self { pub(crate) fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(description.to_owned()); self.description = Some(html(description));
self self
} }
pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self { pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self {
self.description_source = Some(description_source.to_owned()); self.description_source = Some(description_source.to_owned());
self self.description(&bbcode(description_source, |v| v))
} }
pub(crate) fn login_required(&mut self, required: bool) -> &mut Self { pub(crate) fn login_required(&mut self, required: bool) -> &mut Self {
@ -131,6 +173,10 @@ impl ProfileChanges {
|| self.banner.is_some() || self.banner.is_some()
|| self.updated.is_some() || self.updated.is_some()
} }
pub(crate) fn save(self) -> Result<Profile, StoreError> {
self.state.store.profiles.save(&self)
}
} }
impl OwnerSource { impl OwnerSource {
@ -140,9 +186,10 @@ impl OwnerSource {
} }
impl Profile { impl Profile {
pub fn update(&self) -> ProfileChanges { pub fn update<'a>(&self, state: &'a State) -> ProfileChanges<'a> {
ProfileChanges { ProfileChanges {
id: self.id, state,
kind: ProfileChangesKind::Update { id: self.id },
display_name: None, display_name: None,
display_name_source: None, display_name_source: None,
description: None, description: None,
@ -230,13 +277,39 @@ impl Store {
}) })
} }
pub(crate) fn create( pub(crate) fn create<'a>(
&self,
state: &'a State,
source: OwnerSource,
handle: String,
domain: String,
published: DateTime<Utc>,
) -> ProfileChanges<'a> {
ProfileChanges::new(state, source, domain, handle, published)
}
fn save<'a>(&self, changes: &ProfileChanges<'a>) -> Result<Profile, StoreError> {
match &changes.kind {
ProfileChangesKind::Create {
source,
handle,
domain,
published,
} => {
let id = self.do_create(*source, handle, domain, *published)?;
self.do_update(id, changes)
}
ProfileChangesKind::Update { id } => self.do_update(*id, changes),
}
}
fn do_create(
&self, &self,
source: OwnerSource, source: OwnerSource,
handle: &str, handle: &str,
domain: &str, domain: &str,
published: DateTime<Utc>, published: DateTime<Utc>,
) -> Result<Profile, StoreError> { ) -> Result<Uuid, StoreError> {
let handle_lower = handle.to_lowercase(); let handle_lower = handle.to_lowercase();
let mut id; let mut id;
@ -340,38 +413,15 @@ impl Store {
return Err(e.into()); return Err(e.into());
} }
Ok(stored_profile.into()) Ok(id)
} }
pub fn search<'a>( fn do_update<'a>(
&'a self, &self,
handle_term: &'a str, id: Uuid,
) -> impl DoubleEndedIterator<Item = Uuid> + 'a { profile_changes: &ProfileChanges<'a>,
self.handle_index ) -> Result<Profile, StoreError> {
.search(handle_term) let stored_profile_ivec = match self.profile_tree.get(id_profile_key(id).as_bytes())? {
.flat_map(move |handle| self.handle_iter(handle))
}
fn handle_iter<'a>(&'a self, handle: String) -> impl DoubleEndedIterator<Item = Uuid> + 'a {
self.handle_tree
.scan_prefix(handle_id_prefix(&handle))
.values()
.filter_map(|res| res.ok())
.filter_map(|ivec| Uuid::from_slice(&ivec).ok())
}
pub fn count(&self, source: OwnerSource) -> Result<u64, StoreError> {
match self.count_tree.get(owner_profile_count_key(&source))? {
Some(ivec) => Ok(String::from_utf8_lossy(&ivec).parse::<u64>().unwrap_or(0)),
None => Ok(0),
}
}
pub(crate) fn update(&self, profile_changes: &ProfileChanges) -> Result<Profile, StoreError> {
let stored_profile_ivec = match self
.profile_tree
.get(id_profile_key(profile_changes.id).as_bytes())?
{
Some(ivec) => ivec, Some(ivec) => ivec,
None => return Err(StoreError::Missing), None => return Err(StoreError::Missing),
}; };
@ -415,7 +465,7 @@ impl Store {
if self if self
.profile_tree .profile_tree
.compare_and_swap( .compare_and_swap(
id_profile_key(profile_changes.id).as_bytes(), id_profile_key(id).as_bytes(),
Some(&stored_profile_ivec), Some(&stored_profile_ivec),
Some(stored_profile_vec), Some(stored_profile_vec),
)? )?
@ -427,6 +477,30 @@ impl Store {
Ok(stored_profile.into()) Ok(stored_profile.into())
} }
pub fn search<'a>(
&'a self,
handle_term: &'a str,
) -> impl DoubleEndedIterator<Item = Uuid> + 'a {
self.handle_index
.search(handle_term)
.flat_map(move |handle| self.handle_iter(handle))
}
fn handle_iter<'a>(&'a self, handle: String) -> impl DoubleEndedIterator<Item = Uuid> + 'a {
self.handle_tree
.scan_prefix(handle_id_prefix(&handle))
.values()
.filter_map(|res| res.ok())
.filter_map(|ivec| Uuid::from_slice(&ivec).ok())
}
pub fn count(&self, source: OwnerSource) -> Result<u64, StoreError> {
match self.count_tree.get(owner_profile_count_key(&source))? {
Some(ivec) => Ok(String::from_utf8_lossy(&ivec).parse::<u64>().unwrap_or(0)),
None => Ok(0),
}
}
pub fn for_local(&self, id: Uuid) -> impl DoubleEndedIterator<Item = Uuid> { pub fn for_local(&self, id: Uuid) -> impl DoubleEndedIterator<Item = Uuid> {
self.for_owner(OwnerSource::Local(id)) self.for_owner(OwnerSource::Local(id))
} }

View file

@ -1,5 +1,6 @@
use crate::store::StoreError; use crate::{store::StoreError, State};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html, strip};
use sled::{Db, Transactional, Tree}; use sled::{Db, Transactional, Tree};
use uuid::Uuid; use uuid::Uuid;
@ -12,6 +13,43 @@ pub struct Server {
title_source: Option<String>, title_source: Option<String>,
description: Option<String>, description: Option<String>,
description_source: Option<String>, description_source: Option<String>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
}
#[derive(Debug)]
pub enum ServerChangesKind {
Create { domain: String },
Update { id: Uuid },
}
#[derive(Debug)]
pub struct ServerChanges<'a> {
state: &'a State,
kind: ServerChangesKind,
title: Option<String>,
title_source: Option<String>,
description: Option<String>,
description_source: Option<String>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
}
#[derive(Clone)]
pub struct Store {
created_id: Tree,
domain_id: Tree,
id_domain: Tree,
id_created: Tree,
id_title: Tree,
id_title_source: Tree,
id_description: Tree,
id_description_source: Tree,
id_published: Tree,
id_updated: Tree,
self_server: Tree,
} }
impl Server { impl Server {
@ -43,64 +81,79 @@ impl Server {
self.created self.created
} }
pub(crate) fn changes(&self) -> ServerChanges { pub(crate) fn updated(&self) -> Option<DateTime<Utc>> {
self.updated
}
pub(crate) fn changes<'a>(&self, state: &'a State) -> ServerChanges<'a> {
ServerChanges { ServerChanges {
id: self.id, state,
kind: ServerChangesKind::Update { id: self.id },
title: None, title: None,
title_source: None, title_source: None,
description: None, description: None,
description_source: None, description_source: None,
published: self.published,
updated: None,
} }
} }
} }
pub struct ServerChanges { impl<'a> ServerChanges<'a> {
id: Uuid, fn new(state: &'a State, domain: String) -> Self {
title: Option<String>, ServerChanges {
title_source: Option<String>, state,
description: Option<String>, kind: ServerChangesKind::Create { domain },
description_source: Option<String>, title: None,
} title_source: None,
description: None,
description_source: None,
published: None,
updated: None,
}
}
impl ServerChanges {
pub(crate) fn title(&mut self, title: &str) -> &mut Self { pub(crate) fn title(&mut self, title: &str) -> &mut Self {
self.title = Some(title.to_owned()); self.title = Some(strip(title));
self self
} }
pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self { pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self {
self.title_source = Some(title_source.to_owned()); self.title_source = Some(title_source.to_owned());
self self.title(title_source)
} }
pub(crate) fn description(&mut self, description: &str) -> &mut Self { pub(crate) fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(description.to_owned()); self.description = Some(html(description));
self self
} }
pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self { pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self {
self.description_source = Some(description_source.to_owned()); self.description_source = Some(description_source.to_owned());
self.description(&bbcode(description_source, |v| v))
}
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 {
if self.published.is_some() {
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()
} }
}
#[derive(Clone)] pub(crate) fn save(self) -> Result<Server, StoreError> {
pub struct Store { self.state.store.servers.save(&self)
created_id: Tree, }
domain_id: Tree,
id_domain: Tree,
id_created: Tree,
id_title: Tree,
id_title_source: Tree,
id_description: Tree,
id_description_source: Tree,
self_server: Tree,
} }
impl Store { impl Store {
@ -113,6 +166,8 @@ impl Store {
let id_title_source = db.open_tree("/profiles/servers/id_title_source")?; let id_title_source = db.open_tree("/profiles/servers/id_title_source")?;
let id_description = db.open_tree("/profiles/servers/id_description")?; let id_description = db.open_tree("/profiles/servers/id_description")?;
let id_description_source = db.open_tree("/profiles/servers/id_description_source")?; let id_description_source = db.open_tree("/profiles/servers/id_description_source")?;
let id_published = db.open_tree("/profiles/servers/id_published")?;
let id_updated = db.open_tree("/profiles/servers/id_updated")?;
let self_server = db.open_tree("/profiles/servers/self_server")?; let self_server = db.open_tree("/profiles/servers/self_server")?;
Ok(Store { Ok(Store {
@ -124,6 +179,8 @@ impl Store {
id_title_source, id_title_source,
id_description, id_description,
id_description_source, id_description_source,
id_published,
id_updated,
self_server, self_server,
}) })
} }
@ -146,11 +203,21 @@ impl Store {
.unwrap_or(false)) .unwrap_or(false))
} }
pub(crate) fn create( pub(crate) fn create<'a>(&self, state: &'a State, domain: String) -> ServerChanges<'a> {
&self, ServerChanges::new(state, domain)
domain: &str, }
created: DateTime<Utc>,
) -> Result<Server, StoreError> { pub(crate) fn save(&self, changes: &ServerChanges) -> Result<Server, StoreError> {
match &changes.kind {
ServerChangesKind::Create { domain } => {
let id = self.do_create(domain, Utc::now())?;
self.do_update(id, changes)
}
ServerChangesKind::Update { id } => self.do_update(*id, changes),
}
}
fn do_create(&self, domain: &str, created: DateTime<Utc>) -> Result<Uuid, StoreError> {
let mut id; let mut id;
while { while {
@ -183,60 +250,67 @@ impl Store {
return Err(e.into()); return Err(e.into());
} }
Ok(Server { Ok(id)
id,
domain: domain.to_owned(),
title: None,
title_source: None,
description: None,
description_source: None,
created,
})
} }
pub(crate) fn update(&self, changes: &ServerChanges) -> Result<Server, StoreError> { fn do_update(&self, id: Uuid, changes: &ServerChanges) -> Result<Server, StoreError> {
let domain = match self let domain = match self.id_domain.get(id.as_bytes())?.map(string_from_ivec) {
.id_domain
.get(changes.id.as_bytes())?
.map(string_from_ivec)
{
Some(domain) => domain, Some(domain) => domain,
None => return Err(StoreError::Missing), None => return Err(StoreError::Missing),
}; };
let created = match self let created = match self.id_created.get(id.as_bytes())?.and_then(date_from_ivec) {
.id_created
.get(changes.id.as_bytes())?
.and_then(date_from_ivec)
{
Some(date) => date, Some(date) => date,
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 self.id_title.insert(id.as_bytes(), title.as_bytes())?;
.insert(changes.id.as_bytes(), title.as_bytes())?;
} }
if let Some(title_source) = &changes.title_source { if let Some(title_source) = &changes.title_source {
self.id_title_source self.id_title_source
.insert(changes.id.as_bytes(), title_source.as_bytes())?; .insert(id.as_bytes(), title_source.as_bytes())?;
} }
if let Some(description) = &changes.description { if let Some(description) = &changes.description {
self.id_description self.id_description
.insert(changes.id.as_bytes(), description.as_bytes())?; .insert(id.as_bytes(), description.as_bytes())?;
} }
if let Some(description_source) = &changes.description_source { if let Some(description_source) = &changes.description_source {
self.id_description_source self.id_description_source
.insert(changes.id.as_bytes(), description_source.as_bytes())?; .insert(id.as_bytes(), description_source.as_bytes())?;
}
if let Some(published) = changes.published {
self.id_published
.insert(id.as_bytes(), published.to_rfc3339().as_bytes())?;
}
if let Some(updated) = changes.updated {
self.id_updated
.insert(id.as_bytes(), updated.to_rfc3339().as_bytes())?;
} }
Ok(Server { Ok(Server {
id: changes.id, id,
domain, domain,
created, created,
title: changes.title.clone(), title: changes.title.clone(),
title_source: changes.title_source.clone(), title_source: changes.title_source.clone(),
description: changes.description.clone(), description: changes.description.clone(),
description_source: changes.description_source.clone(), description_source: changes.description_source.clone(),
published: changes.published,
updated: changes.updated,
}) })
} }
@ -260,6 +334,11 @@ impl Store {
.id_description_source .id_description_source
.get(id.as_bytes())? .get(id.as_bytes())?
.map(string_from_ivec); .map(string_from_ivec);
let updated = self.id_updated.get(id.as_bytes())?.and_then(date_from_ivec);
let published = self
.id_published
.get(id.as_bytes())?
.and_then(date_from_ivec);
Ok(domain.and_then(|domain| { Ok(domain.and_then(|domain| {
created.map(|created| Server { created.map(|created| Server {
@ -270,6 +349,8 @@ impl Store {
description, description,
description_source, description_source,
created, created,
updated,
published,
}) })
})) }))
} }

View file

@ -1,5 +1,7 @@
use super::{StoreError, Undo}; use super::{StoreError, Undo};
use crate::State;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html, strip};
use sled::{Db, Transactional, Tree}; use sled::{Db, Transactional, Tree};
use std::{fmt, io::Cursor}; use std::{fmt, io::Cursor};
use uuid::Uuid; use uuid::Uuid;
@ -29,8 +31,15 @@ pub struct Submission {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct SubmissionChanges { pub enum SubmissionChangesKind {
id: Uuid, Create { profile_id: Uuid },
Update { id: Uuid },
}
#[derive(Debug)]
pub struct SubmissionChanges<'a> {
state: &'a State,
kind: SubmissionChangesKind,
title: Option<String>, title: Option<String>,
title_source: Option<String>, title_source: Option<String>,
description: Option<String>, description: Option<String>,
@ -82,9 +91,10 @@ struct StoredSubmission {
} }
impl Submission { impl Submission {
pub fn update(&self) -> SubmissionChanges { pub(crate) fn update<'a>(&self, state: &'a State) -> SubmissionChanges<'a> {
SubmissionChanges { SubmissionChanges {
id: self.id, state,
kind: SubmissionChangesKind::Update { id: self.id },
title: None, title: None,
title_source: None, title_source: None,
description: None, description: None,
@ -165,25 +175,44 @@ impl Submission {
} }
} }
impl SubmissionChanges { impl<'a> SubmissionChanges<'a> {
fn new(state: &'a State, profile_id: Uuid) -> Self {
SubmissionChanges {
state,
kind: SubmissionChangesKind::Create { profile_id },
title: None,
title_source: None,
description: None,
description_source: None,
visibility: None,
published: None,
updated: None,
local_only: None,
logged_in_only: None,
sensitive: None,
original_files: vec![],
files: vec![],
}
}
pub(crate) fn title(&mut self, title: &str) -> &mut Self { pub(crate) fn title(&mut self, title: &str) -> &mut Self {
self.title = Some(title.to_owned()); self.title = Some(strip(title));
self self
} }
pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self { pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self {
self.title_source = Some(title_source.to_owned()); self.title_source = Some(title_source.to_owned());
self self.title(title_source)
} }
pub(crate) fn description(&mut self, description: &str) -> &mut Self { pub(crate) fn description(&mut self, description: &str) -> &mut Self {
self.description = Some(description.to_owned()); self.description = Some(html(description));
self self
} }
pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self { pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self {
self.description_source = Some(description_source.to_owned()); self.description_source = Some(description_source.to_owned());
self self.description(&bbcode(description_source, |v| v))
} }
pub(crate) fn visibility(&mut self, visibility: Visibility) -> &mut Self { pub(crate) fn visibility(&mut self, visibility: Visibility) -> &mut Self {
@ -247,10 +276,14 @@ impl SubmissionChanges {
|| self.sensitive.is_some() || self.sensitive.is_some()
|| self.original_files != self.files || self.original_files != self.files
} }
pub(crate) fn save(self) -> Result<Submission, StoreError> {
self.state.store.submissions.save(&self)
}
} }
impl Store { impl Store {
pub fn build(db: &Db) -> Result<Self, sled::Error> { pub(super) fn build(db: &Db) -> Result<Self, sled::Error> {
Ok(Store { Ok(Store {
submission_tree: db.open_tree("profiles/submissions")?, submission_tree: db.open_tree("profiles/submissions")?,
profile_tree: db.open_tree("profiles/submissions/profile")?, profile_tree: db.open_tree("profiles/submissions/profile")?,
@ -261,12 +294,21 @@ impl Store {
}) })
} }
pub fn create( pub(crate) fn create<'a>(&self, state: &'a State, profile_id: Uuid) -> SubmissionChanges<'a> {
&self, SubmissionChanges::new(state, profile_id)
profile_id: Uuid, }
title: &str,
visibility: Visibility, fn save(&self, changes: &SubmissionChanges) -> Result<Submission, StoreError> {
) -> Result<Submission, StoreError> { match &changes.kind {
SubmissionChangesKind::Create { profile_id } => {
let id = self.do_create(*profile_id)?;
self.do_update(id, changes)
}
SubmissionChangesKind::Update { id } => self.do_update(*id, changes),
}
}
fn do_create(&self, profile_id: Uuid) -> Result<Uuid, StoreError> {
let mut id; let mut id;
let mut stored_submission; let mut stored_submission;
@ -280,14 +322,14 @@ impl Store {
stored_submission = StoredSubmission { stored_submission = StoredSubmission {
id, id,
profile_id, profile_id,
title: title.to_owned(), title: String::new(),
title_source: None, title_source: None,
description: None, description: None,
description_source: None, description_source: None,
files: vec![], files: vec![],
published: None, published: None,
updated: None, updated: None,
visibility, visibility: Visibility::Followers,
local_only: false, local_only: false,
logged_in_only: false, logged_in_only: false,
drafted_at: now, drafted_at: now,
@ -337,7 +379,7 @@ impl Store {
return Err(e.into()); return Err(e.into());
} }
Ok(stored_submission.into()) Ok(id)
} }
pub fn count(&self, profile_id: Uuid) -> Result<u64, StoreError> { pub fn count(&self, profile_id: Uuid) -> Result<u64, StoreError> {
@ -352,12 +394,11 @@ impl Store {
} }
} }
pub fn update(&self, changes: &SubmissionChanges) -> Result<Submission, StoreError> { fn do_update(&self, id: Uuid, changes: &SubmissionChanges) -> Result<Submission, StoreError> {
let stored_submission_ivec = let stored_submission_ivec = match self.submission_tree.get(id_submission_key(id))? {
match self.submission_tree.get(id_submission_key(changes.id))? { Some(ivec) => ivec,
Some(ivec) => ivec, None => return Err(StoreError::Missing),
None => return Err(StoreError::Missing), };
};
let mut stored_submission: StoredSubmission = let mut stored_submission: StoredSubmission =
serde_json::from_slice(&stored_submission_ivec)?; serde_json::from_slice(&stored_submission_ivec)?;
@ -412,7 +453,7 @@ impl Store {
if self if self
.submission_tree .submission_tree
.compare_and_swap( .compare_and_swap(
id_submission_key(changes.id), id_submission_key(id),
Some(&stored_submission_ivec), Some(&stored_submission_ivec),
Some(stored_submission_vec.as_slice()), Some(stored_submission_vec.as_slice()),
)? )?