hyaenidae/profiles/src/store/mod.rs

485 lines
12 KiB
Rust

use chrono::{DateTime, Utc};
use sled::Db;
use std::fmt;
use uuid::Uuid;
mod comment;
mod file;
mod profile;
mod react;
mod report;
mod server;
mod submission;
mod term_search;
pub mod view;
pub use comment::{Comment, CommentChanges, Store as CommentStore};
pub use file::Store as FileStore;
pub use profile::{OwnerSource, Profile, ProfileChanges, Store as ProfileStore};
pub use react::Store as ReactStore;
pub use report::Store as ReportStore;
pub use server::{Server, ServerChanges, Store as ServerStore};
pub use submission::{Store as SubmissionStore, Submission, SubmissionChanges, Visibility};
pub use term_search::TermSearch;
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)]
pub struct Store {
pub profiles: profile::Store,
pub files: file::Store,
pub submissions: submission::Store,
pub comments: comment::Store,
pub reacts: react::Store,
pub reports: report::Store,
pub servers: server::Store,
pub view: view::Store,
}
impl Store {
pub fn build(max_handle_length: usize, db: &Db) -> Result<Store, sled::Error> {
Ok(Store {
profiles: profile::Store::build(max_handle_length, db)?,
files: file::Store::build(db)?,
submissions: submission::Store::build(db)?,
comments: comment::Store::build(db)?,
reacts: react::Store::build(db)?,
reports: report::Store::build(db)?,
servers: server::Store::build(db)?,
view: view::Store::build(db)?,
})
}
pub(crate) fn is_domain_blocked(&self, domain: &str) -> Result<bool, StoreError> {
if let Some(id) = self.servers.by_domain(domain)? {
return self.is_blocked(id);
}
Ok(false)
}
pub(crate) fn is_domain_federated(&self, domain: &str) -> Result<bool, StoreError> {
if let Some(id) = self.servers.by_domain(domain)? {
return self.is_federated(id);
}
Ok(false)
}
pub(crate) fn is_blocked(&self, id: Uuid) -> Result<bool, StoreError> {
if let Some(self_id) = self.servers.get_self()? {
let forward = self.view.server_blocks.by_forward(id, self_id)?.is_some();
let backward = self.view.server_blocks.by_forward(self_id, id)?.is_some();
return Ok(forward || backward);
}
Ok(true)
}
pub(crate) fn is_federated(&self, id: Uuid) -> Result<bool, StoreError> {
if let Some(self_id) = self.servers.get_self()? {
let forward = self.view.server_follows.by_forward(id, self_id)?.is_some();
let backward = self.view.server_follows.by_forward(self_id, id)?.is_some();
return Ok(forward || backward);
}
Ok(false)
}
pub(crate) fn followers_for<'a>(
&'a self,
profile_id: Uuid,
) -> impl DoubleEndedIterator<Item = Uuid> + 'a {
self.view.follows.forward_iter(profile_id)
}
pub(crate) fn federated_servers<'a>(&'a self) -> impl DoubleEndedIterator<Item = Uuid> + 'a {
self.servers
.get_self()
.ok()
.and_then(|opt| opt)
.into_iter()
.flat_map(move |server_id| {
let iter1 = self.view.server_follows.forward_iter(server_id);
let iter2 = self.view.server_follows.backward_iter(server_id);
iter1.chain(iter2)
})
}
}
#[derive(Clone, Debug)]
pub struct Report {
id: Uuid,
reporter: Uuid,
reporter_kind: ReporterKind,
reported_item: Uuid,
kind: ReportKind,
note: Option<String>,
resolved: Option<DateTime<Utc>>,
resolution: Option<String>,
forwarded: Option<DateTime<Utc>>,
}
impl Report {
pub fn id(&self) -> Uuid {
self.id
}
pub fn note(&self) -> Option<&str> {
self.note.as_deref()
}
pub fn reporter_profile(&self) -> Option<Uuid> {
if matches!(self.reporter_kind, ReporterKind::Profile) {
Some(self.reporter)
} else {
None
}
}
pub fn item(&self) -> Uuid {
self.reported_item
}
pub fn reporter_server(&self) -> Option<Uuid> {
if matches!(self.reporter_kind, ReporterKind::Server) {
Some(self.reporter)
} else {
None
}
}
pub fn kind(&self) -> ReportKind {
self.kind
}
pub fn profile(&self) -> Option<Uuid> {
if matches!(self.kind, ReportKind::Profile) {
Some(self.reported_item)
} else {
None
}
}
pub fn submission(&self) -> Option<Uuid> {
if matches!(self.kind, ReportKind::Submission) {
Some(self.reported_item)
} else {
None
}
}
pub fn comment(&self) -> Option<Uuid> {
if matches!(self.kind, ReportKind::Comment) {
Some(self.reported_item)
} else {
None
}
}
pub fn post(&self) -> Option<Uuid> {
if matches!(self.kind, ReportKind::Post) {
Some(self.reported_item)
} else {
None
}
}
pub fn resolved(&self) -> Option<DateTime<Utc>> {
self.resolved
}
pub fn resolution(&self) -> Option<&str> {
self.resolution.as_deref()
}
pub fn forwarded(&self) -> Option<DateTime<Utc>> {
self.forwarded
}
}
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
pub enum ReportKind {
Post,
Comment,
Submission,
Profile,
}
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
pub(crate) enum ReporterKind {
Profile,
Server,
}
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
enum ReportState {
Open,
Resolved,
}
#[derive(Clone, Debug)]
pub struct PictRsFile {
key: String,
token: String,
width: usize,
height: usize,
media_type: mime::Mime,
}
impl PictRsFile {
pub(crate) fn new(
key: &str,
token: &str,
width: usize,
height: usize,
media_type: mime::Mime,
) -> Self {
PictRsFile {
key: key.to_owned(),
token: token.to_owned(),
width,
height,
media_type,
}
}
pub fn key(&self) -> &str {
&self.key
}
pub(crate) fn token(&self) -> &str {
&self.token
}
pub(crate) fn media_type(&self) -> &mime::Mime {
&self.media_type
}
}
#[derive(Clone, Debug)]
pub enum FileSource {
PictRs(PictRsFile),
}
#[derive(Clone, Debug)]
pub struct File {
id: Uuid,
source: FileSource,
}
impl File {
pub fn id(&self) -> Uuid {
self.id
}
pub fn source(&self) -> &FileSource {
&self.source
}
pub fn pictrs(&self) -> Option<&PictRsFile> {
let FileSource::PictRs(ref file) = self.source;
Some(file)
}
pub fn pictrs_key(&self) -> Option<&str> {
self.pictrs().map(|p| p.key())
}
}
#[derive(Clone, Debug)]
pub enum React {
Submission(SubmissionReact),
Reply(ReplyReact),
}
impl React {
pub(crate) fn id(&self) -> Uuid {
match self {
React::Submission(SubmissionReact { id, .. }) => *id,
React::Reply(ReplyReact { id, .. }) => *id,
}
}
pub(crate) fn submission_id(&self) -> Uuid {
match self {
React::Submission(SubmissionReact { submission_id, .. }) => *submission_id,
React::Reply(ReplyReact { submission_id, .. }) => *submission_id,
}
}
pub(crate) fn profile_id(&self) -> Uuid {
match self {
React::Submission(SubmissionReact { profile_id, .. }) => *profile_id,
React::Reply(ReplyReact { profile_id, .. }) => *profile_id,
}
}
pub(crate) fn comment_id(&self) -> Option<Uuid> {
match self {
React::Reply(ReplyReact { comment_id, .. }) => Some(*comment_id),
_ => None,
}
}
pub(crate) fn react(&self) -> &str {
match self {
React::Submission(SubmissionReact { react, .. }) => &react,
React::Reply(ReplyReact { react, .. }) => &react,
}
}
pub(crate) fn published(&self) -> DateTime<Utc> {
match self {
React::Submission(SubmissionReact { published, .. }) => *published,
React::Reply(ReplyReact { published, .. }) => *published,
}
}
}
#[derive(Clone, Debug)]
pub struct SubmissionReact {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
react: String,
published: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct ReplyReact {
id: Uuid,
submission_id: Uuid,
profile_id: Uuid,
comment_id: Uuid,
react: String,
published: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct Undo<T>(pub T);
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("{0}")]
Json(#[from] serde_json::Error),
#[error("{0}")]
Sled(#[from] sled::Error),
#[error("{0}")]
Transaction(#[from] sled::transaction::TransactionError),
#[error("Profile changed during modification")]
DoubleStore,
#[error("Cannot update missing item")]
Missing,
#[error("Provided value is too long")]
TooLong,
#[error("The updated data is older than our stored data")]
Outdated,
#[error("Provided value must not be empty")]
Empty,
}
fn modify<T>(
tree: &sled::transaction::TransactionalTree,
key: &str,
f: impl Fn(&mut T),
) -> Result<(), sled::transaction::ConflictableTransactionError>
where
T: serde::Serialize + Default,
for<'de> T: serde::Deserialize<'de>,
{
let mut item = match tree.get(key.as_bytes())? {
Some(ivec) => {
let item: T = serde_json::from_slice(&ivec).expect("JSON is valid");
item
}
None => T::default(),
};
(f)(&mut item);
let item_vec = serde_json::to_vec(&item).expect("JSON is valid");
tree.insert(key.as_bytes(), item_vec.as_slice())?;
Ok(())
}
fn count(
tree: &sled::transaction::TransactionalTree,
key: &str,
f: impl Fn(u64) -> u64,
) -> Result<u64, sled::transaction::ConflictableTransactionError> {
let count = match tree.get(key.as_bytes())? {
Some(ivec) => {
let s = String::from_utf8_lossy(&ivec);
let count: u64 = s.parse().unwrap_or(0);
count
}
None => 0,
};
let count = (f)(count);
let count_string = count.to_string();
tree.insert(key.as_bytes(), count_string.as_bytes())?;
Ok(count)
}
impl fmt::Display for OwnerSource {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
OwnerSource::Local(id) => write!(f, "local:{}", id),
OwnerSource::Remote(s) => write!(f, "remote:{}", s),
}
}
}
impl fmt::Debug for Store {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Store")
.field("profiles", &"ProfileStore")
.field("files", &"FileStore")
.field("submissions", &"SubmissionStore")
.field("comments", &"CommentStore")
.field("reacts", &"ReactStore")
.field("view", &"ViewStore")
.finish()
}
}