hyaenidae/profiles/src/store/profile.rs

839 lines
25 KiB
Rust

use super::{StoreError, TermSearch, Undo, ValidationError, ValidationErrorKind};
use crate::State;
use chrono::{DateTime, Utc};
use hyaenidae_content::{bbcode, html, strip};
use sled::{Db, Transactional, Tree};
use std::io::Cursor;
use uuid::Uuid;
#[derive(Debug)]
enum ProfileChangesKind {
Create {
source: OwnerSource,
domain: String,
handle: String,
},
Update {
id: Uuid,
},
}
#[derive(Debug)]
pub struct ProfileChanges<'a> {
state: &'a State,
kind: ProfileChangesKind,
display_name: Option<String>,
display_name_source: Option<String>,
description: Option<String>,
description_source: Option<String>,
login_required: Option<bool>,
icon: Option<Uuid>,
banner: Option<Uuid>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
errors: Vec<ValidationError>,
}
#[derive(Clone, Copy, Debug)]
pub enum OwnerSource {
Local(Uuid),
Remote(Uuid),
}
#[derive(Clone, Debug)]
pub struct Profile {
id: Uuid,
owner_source: OwnerSource,
handle: String,
domain: String,
display_name: Option<String>,
display_name_source: Option<String>,
description: Option<String>,
description_source: Option<String>,
icon: Option<Uuid>,
banner: Option<Uuid>,
published: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
login_required: bool,
suspended: bool,
}
#[derive(Clone, Debug)]
pub struct Store {
profile_tree: Tree,
handle_tree: Tree,
owner_created_tree: Tree,
created_tree: Tree,
count_tree: Tree,
handle_index: TermSearch,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
enum StoredOwnerSource {
Local(Uuid),
Remote(Uuid),
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct StoredProfile {
id: Uuid,
owner_source: StoredOwnerSource,
handle: String,
domain: String,
#[serde(skip_serializing_if = "Option::is_none")]
display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
display_name_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
banner: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
published: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
updated: Option<DateTime<Utc>>,
login_required: bool,
created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
suspended_at: Option<DateTime<Utc>>,
}
impl<'a> ProfileChanges<'a> {
pub(crate) fn new(
state: &'a State,
source: OwnerSource,
domain: String,
handle: String,
) -> Result<Self, Vec<ValidationError>> {
let handle = strip(handle.trim());
let domain = strip(domain.trim());
let mut errors = vec![];
if handle.len() > state.content_config.max_handle_length {
errors.push(ValidationError {
field: String::from("handle"),
kind: ValidationErrorKind::TooLong {
maximum: state.content_config.max_handle_length,
proposed: handle.len(),
},
});
} else if handle.is_empty() {
errors.push(ValidationError {
field: String::from("handle"),
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 {
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
}
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(display_name_source)
}
pub(crate) fn description(&mut self, description: &str) -> &mut Self {
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
}
pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self {
self.description_source = Some(description_source.to_owned());
self.description(&bbcode(description_source, |v| self.state.map_nodeview(v)))
}
pub(crate) fn login_required(&mut self, required: bool) -> &mut Self {
self.login_required = Some(required);
self
}
pub(crate) fn icon(&mut self, file_id: Uuid) -> &mut Self {
self.icon = Some(file_id);
self
}
pub(crate) fn banner(&mut self, file_id: Uuid) -> &mut Self {
self.banner = Some(file_id);
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 {
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
}
pub(crate) fn any_changes(&self) -> bool {
self.display_name.is_some()
|| self.description.is_some()
|| self.login_required.is_some()
|| self.icon.is_some()
|| self.banner.is_some()
}
pub(crate) fn save(self) -> Result<Result<Profile, Vec<ValidationError>>, StoreError> {
if self.errors.is_empty() {
self.state.store.profiles.save(&self).map(Ok)
} else {
Ok(Err(self.errors))
}
}
}
impl OwnerSource {
pub fn is_local(&self) -> bool {
matches!(self, OwnerSource::Local(_))
}
}
impl Profile {
pub fn update<'a>(&self, state: &'a State) -> ProfileChanges<'a> {
ProfileChanges {
state,
kind: ProfileChangesKind::Update { id: self.id },
display_name: None,
display_name_source: None,
description: None,
description_source: None,
login_required: None,
icon: None,
banner: None,
published: self.published,
updated: self.updated,
errors: vec![],
}
}
pub fn id(&self) -> Uuid {
self.id
}
pub(crate) fn owner_source(&self) -> &OwnerSource {
&self.owner_source
}
pub fn local_owner(&self) -> Option<Uuid> {
match self.owner_source {
OwnerSource::Local(id) => Some(id),
_ => None,
}
}
pub fn handle(&self) -> &str {
&self.handle
}
pub fn domain(&self) -> &str {
&self.domain
}
pub fn display_name(&self) -> Option<&str> {
self.display_name.as_deref()
}
pub fn display_name_source(&self) -> Option<&str> {
self.display_name_source.as_deref()
}
pub fn description(&self) -> Option<&str> {
self.description.as_ref().map(|d| d.as_str())
}
pub fn description_source(&self) -> Option<&str> {
self.description_source.as_deref()
}
pub fn icon(&self) -> Option<Uuid> {
self.icon
}
pub fn banner(&self) -> Option<Uuid> {
self.banner
}
pub fn published(&self) -> Option<DateTime<Utc>> {
self.published
}
pub fn updated(&self) -> Option<DateTime<Utc>> {
self.updated
}
pub fn login_required(&self) -> bool {
self.login_required
}
pub fn is_suspended(&self) -> bool {
self.suspended
}
}
impl Store {
pub(super) fn build(max_handle_length: usize, db: &Db) -> Result<Self, sled::Error> {
Ok(Store {
profile_tree: db.open_tree("profiles/profiles")?,
handle_tree: db.open_tree("profiles/profiles/handles")?,
owner_created_tree: db.open_tree("/profiles/profiles/owner/created")?,
created_tree: db.open_tree("/profiles/profiles/created")?,
count_tree: db.open_tree("/profiles/profiles/count")?,
handle_index: TermSearch::build("handle", max_handle_length, db)?,
})
}
pub(crate) fn create<'a>(
&self,
state: &'a State,
source: OwnerSource,
handle: String,
domain: String,
) -> Result<ProfileChanges<'a>, Vec<ValidationError>> {
ProfileChanges::new(state, source, domain, handle)
}
fn save<'a>(&self, changes: &ProfileChanges<'a>) -> Result<Profile, StoreError> {
match &changes.kind {
ProfileChangesKind::Create {
source,
handle,
domain,
} => {
let id = self.do_create(*source, handle, domain)?;
self.do_update(id, changes)
}
ProfileChangesKind::Update { id } => self.do_update(*id, changes),
}
}
fn do_create(
&self,
source: OwnerSource,
handle: &str,
domain: &str,
) -> Result<Uuid, StoreError> {
let handle_lower = handle.to_lowercase();
let mut id;
let mut stored_profile;
let now = Utc::now().into();
let mut stored_profile_vec = vec![];
let stored_source = match &source {
OwnerSource::Local(id) => StoredOwnerSource::Local(*id),
OwnerSource::Remote(id) => StoredOwnerSource::Remote(*id),
};
while {
stored_profile_vec.clear();
let writer = Cursor::new(&mut stored_profile_vec);
id = Uuid::new_v4();
stored_profile = StoredProfile {
id,
owner_source: stored_source.clone(),
handle: handle.to_owned(),
domain: domain.to_owned(),
display_name: None,
display_name_source: None,
description: None,
description_source: None,
icon: None,
banner: None,
published: None,
updated: None,
login_required: true,
created_at: now,
suspended_at: None,
};
serde_json::to_writer(writer, &stored_profile)?;
self.profile_tree
.compare_and_swap(
id_profile_key(id),
None as Option<&[u8]>,
Some(stored_profile_vec.as_slice()),
)?
.is_err()
} {}
let source = source.clone();
// ensure handle uniqueness
match self.handle_tree.compare_and_swap(
handle_id_key(&handle_lower, domain),
None as Option<&[u8]>,
Some(id.as_bytes()),
) {
Ok(Ok(_)) => (),
Ok(Err(_)) => {
self.profile_tree.remove(id_profile_key(id))?;
return Err(StoreError::DoubleStore);
}
Err(e) => {
self.profile_tree.remove(id_profile_key(id))?;
return Err(e.into());
}
}
let res = [
&self.created_tree,
&self.owner_created_tree,
&self.count_tree,
]
.transaction(move |trees| {
let created_tree = &trees[0];
let owner_created_tree = &trees[1];
let count_tree = &trees[2];
created_tree.insert(created_profile_key(now, id).as_bytes(), id.as_bytes())?;
owner_created_tree.insert(
owner_created_profile_key(&source, now, id).as_bytes(),
id.as_bytes(),
)?;
super::count(count_tree, &owner_profile_count_key(&source), |c| {
c.saturating_add(1)
})?;
Ok(())
});
if let Err(e) = self.handle_index.insert(&handle_lower) {
log::error!("Failed to add {} to search index: {}", handle_lower, e);
}
if let Err(e) = res {
[&self.profile_tree, &self.handle_tree].transaction(move |trees| {
let profile_tree = &trees[0];
let handle_tree = &trees[1];
profile_tree.remove(id_profile_key(id).as_bytes())?;
handle_tree.remove(handle_id_key(&handle_lower, domain).as_bytes())?;
Ok(())
})?;
return Err(e.into());
}
Ok(id)
}
fn do_update<'a>(
&self,
id: Uuid,
profile_changes: &ProfileChanges<'a>,
) -> Result<Profile, StoreError> {
let stored_profile_ivec = match self.profile_tree.get(id_profile_key(id).as_bytes())? {
Some(ivec) => ivec,
None => return Err(StoreError::Missing),
};
let mut stored_profile: StoredProfile = serde_json::from_slice(&stored_profile_ivec)?;
if let Some(login_required) = profile_changes.login_required {
stored_profile.login_required = login_required;
}
if let Some(display_name) = &profile_changes.display_name {
stored_profile.display_name = Some(display_name.clone());
}
if let Some(display_name_source) = &profile_changes.display_name_source {
stored_profile.display_name_source = Some(display_name_source.clone());
}
if let Some(description) = &profile_changes.description {
stored_profile.description = Some(description.clone());
}
if let Some(description_source) = &profile_changes.description_source {
stored_profile.description_source = Some(description_source.clone());
}
if let Some(icon) = profile_changes.icon {
stored_profile.icon = Some(icon);
}
if let Some(banner) = profile_changes.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 {
stored_profile.updated = Some(updated);
}
let stored_profile_vec = serde_json::to_vec(&stored_profile)?;
if self
.profile_tree
.compare_and_swap(
id_profile_key(id).as_bytes(),
Some(&stored_profile_ivec),
Some(stored_profile_vec),
)?
.is_err()
{
return Err(StoreError::DoubleStore);
}
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> {
self.for_owner(OwnerSource::Local(id))
}
fn for_owner(&self, owner_source: OwnerSource) -> impl DoubleEndedIterator<Item = Uuid> {
self.owner_created_tree
.scan_prefix(owner_created_profile_prefix(&owner_source))
.values()
.filter_map(|res| res.ok())
.filter_map(|ivec| Uuid::from_slice(&ivec).ok())
}
fn date_range<K>(
&self,
range: impl std::ops::RangeBounds<K>,
) -> impl DoubleEndedIterator<Item = Uuid>
where
K: AsRef<[u8]>,
{
self.owner_created_tree
.range(range)
.values()
.filter_map(|res| res.ok())
.filter_map(move |ivec| Uuid::from_slice(&ivec).ok())
}
pub fn all(&self) -> impl DoubleEndedIterator<Item = Uuid> {
self.created_tree
.iter()
.values()
.filter_map(|res| res.ok())
.filter_map(|ivec| Uuid::from_slice(&ivec).ok())
.rev()
}
pub fn newer_than(&self, id: Uuid) -> impl DoubleEndedIterator<Item = Uuid> {
let this = self.clone();
self.profile_tree
.get(id_profile_key(id))
.ok()
.and_then(|opt| opt)
.and_then(|stored_profile_ivec| {
let stored_profile: StoredProfile =
serde_json::from_slice(&stored_profile_ivec).ok()?;
Some((
stored_profile.owner_source.into(),
stored_profile.created_at,
))
})
.into_iter()
.flat_map(move |(source, created_at)| {
let range_start = owner_created_profile_range_start(&source, created_at);
let range_start = range_start.as_bytes().to_vec();
this.date_range(range_start..)
})
}
pub fn older_than(&self, id: Uuid) -> impl DoubleEndedIterator<Item = Uuid> {
let this = self.clone();
self.profile_tree
.get(id_profile_key(id))
.ok()
.and_then(|opt| opt)
.and_then(|stored_profile_ivec| {
let stored_profile: StoredProfile =
serde_json::from_slice(&stored_profile_ivec).ok()?;
Some((
stored_profile.owner_source.into(),
stored_profile.created_at,
))
})
.into_iter()
.flat_map(move |(source, created_at)| {
let range_end = owner_created_profile_range_start(&source, created_at);
let range_end = range_end.as_bytes().to_vec();
this.date_range(..range_end)
})
.rev()
}
pub fn is_local(&self, id: Uuid) -> Result<Option<bool>, StoreError> {
if let Some(profile) = self.by_id(id)? {
return Ok(Some(profile.owner_source.is_local()));
}
Ok(None)
}
pub fn by_id(&self, id: Uuid) -> Result<Option<Profile>, StoreError> {
let stored_profile_ivec = match self.profile_tree.get(id_profile_key(id).as_bytes())? {
Some(ivec) => ivec,
None => return Ok(None),
};
let stored_profile: StoredProfile = serde_json::from_slice(&stored_profile_ivec)?;
Ok(Some(stored_profile.into()))
}
pub fn by_handle(&self, handle: &str, domain: &str) -> Result<Option<Uuid>, StoreError> {
let id_ivec = match self.handle_tree.get(handle_id_key(handle, domain))? {
Some(ivec) => ivec,
None => return Ok(None),
};
Ok(Uuid::from_slice(&id_ivec).ok())
}
pub(crate) fn suspend(&self, profile_id: Uuid) -> Result<Option<Undo<Profile>>, StoreError> {
let stored_profile_ivec = match self
.profile_tree
.get(id_profile_key(profile_id).as_bytes())?
{
Some(ivec) => ivec,
None => return Ok(None),
};
let now = Utc::now();
let mut stored_profile: StoredProfile = serde_json::from_slice(&stored_profile_ivec)?;
stored_profile.banner = None;
stored_profile.icon = None;
stored_profile.description = None;
stored_profile.display_name = None;
stored_profile.suspended_at = Some(now);
stored_profile.updated = Some(now);
let stored_profile_vec = serde_json::to_vec(&stored_profile)?;
if let Err(e) = self.handle_index.remove(&stored_profile.handle) {
log::error!(
"Failed to remove {} from handle index: {}",
stored_profile.handle,
e
);
}
if self
.profile_tree
.compare_and_swap(
id_profile_key(profile_id).as_bytes(),
Some(&stored_profile_ivec),
Some(stored_profile_vec),
)?
.is_err()
{
return Err(StoreError::DoubleStore);
}
Ok(Some(Undo(stored_profile.into())))
}
}
// Used to map id -> Profile
fn id_profile_key(id: Uuid) -> String {
format!("/profile/{}/data", id)
}
// Used to map handle -> id
fn handle_id_key(handle: &str, domain: &str) -> String {
format!(
"/handle/{}:{}/profile",
handle.to_lowercase(),
domain.to_lowercase()
)
}
fn handle_id_prefix(handle: &str) -> String {
format!("/handle/{}:", handle)
}
fn owner_created_profile_prefix(owner: &OwnerSource) -> String {
format!("/owner/{}/created", owner)
}
// Used to fetch profiles for a given owner in a user-recognizalbe order
fn owner_created_profile_key(owner: &OwnerSource, created_at: DateTime<Utc>, id: Uuid) -> String {
format!(
"/owner/{}/created/{}/profile/{}",
owner,
created_at.to_rfc3339(),
id
)
}
fn owner_created_profile_range_start(owner: &OwnerSource, created_at: DateTime<Utc>) -> String {
format!("/owner/{}/created/{}", owner, created_at.to_rfc3339(),)
}
// Used to map created_at -> id
fn created_profile_key(created_at: DateTime<Utc>, id: Uuid) -> String {
format!("/created/{}/profile/{}", created_at, id)
}
fn owner_profile_count_key(owner: &OwnerSource) -> String {
format!("/owner/{}/count", owner)
}
impl From<StoredOwnerSource> for OwnerSource {
fn from(os: StoredOwnerSource) -> Self {
match os {
StoredOwnerSource::Local(id) => OwnerSource::Local(id),
StoredOwnerSource::Remote(id) => OwnerSource::Remote(id),
}
}
}
impl From<StoredProfile> for Profile {
fn from(sp: StoredProfile) -> Self {
Profile {
id: sp.id,
owner_source: sp.owner_source.into(),
handle: sp.handle.to_owned(),
domain: sp.domain.to_owned(),
display_name: sp.display_name,
display_name_source: sp.display_name_source,
description: sp.description,
description_source: sp.description_source,
icon: sp.icon,
banner: sp.banner,
published: sp.published,
updated: sp.updated,
login_required: sp.login_required,
suspended: sp.suspended_at.is_some(),
}
}
}