This commit is contained in:
Aode (Lion) 2022-05-03 21:33:39 -05:00
commit 14b428cc1c
7 changed files with 4054 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
server-data.bonsaidb

3117
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "artsite"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bonsaidb = { version = "0.4.1", features = ["server-full", "client-full"] }
schema = { path = "./schema" }
tokio = { version = "1", features = ["full"] }
[workspace]
members = ["schema"]

14
schema/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "schema"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bonsaidb = "0.4.1"
num-derive = "0.3"
num-traits = "0.2"
serde = { version = "1", features = ["derive"] }
time = { version = "0.3.9", features = ["serde", "serde-well-known"] }
uuid = { version = "1", features = ["serde", "v4"] }

161
schema/src/keys.rs Normal file
View file

@ -0,0 +1,161 @@
use bonsaidb::core::key::{EnumKey, Key, KeyEncoding};
use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize};
use time::{format_description::well_known::Rfc3339, OffsetDateTime, UtcOffset};
use uuid::Uuid;
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Deserialize,
Serialize,
FromPrimitive,
ToPrimitive,
)]
pub enum ProfileKind {
Server,
Collaboration,
Personal,
Bot,
Unknown,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Deserialize,
Serialize,
FromPrimitive,
ToPrimitive,
)]
pub enum PostKind {
Media,
Text,
Comment,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Deserialize,
Serialize,
FromPrimitive,
ToPrimitive,
)]
pub enum ReportState {
Unresolved,
Closed,
MarkedSensitive,
Removed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct DatetimeKey {
#[serde(with = "time::serde::rfc3339")]
datetime: OffsetDateTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(transparent)]
pub struct UuidKey {
uuid: Uuid,
}
impl DatetimeKey {
pub fn now() -> Self {
Self {
datetime: OffsetDateTime::now_utc(),
}
}
pub fn from_datetime(datetime: OffsetDateTime) -> Self {
Self {
datetime: datetime.to_offset(UtcOffset::UTC),
}
}
pub fn into_inner(self) -> OffsetDateTime {
self.datetime
}
}
impl UuidKey {
pub fn generate() -> Self {
Self {
uuid: Uuid::new_v4(),
}
}
}
impl EnumKey for ProfileKind {}
impl EnumKey for PostKind {}
impl EnumKey for ReportState {}
impl<'k> KeyEncoding<'k, Self> for DatetimeKey {
type Error = time::Error;
const LENGTH: Option<usize> = None;
fn as_ord_bytes(&'k self) -> Result<std::borrow::Cow<'k, [u8]>, Self::Error> {
Ok(std::borrow::Cow::Owned(
self.datetime.format(&Rfc3339)?.into_bytes(),
))
}
}
impl<'k> Key<'k> for DatetimeKey {
fn from_ord_bytes(bytes: &'k [u8]) -> Result<Self, Self::Error> {
Ok(Self {
datetime: OffsetDateTime::parse(String::from_utf8_lossy(bytes).as_ref(), &Rfc3339)?,
})
}
}
impl<'k> KeyEncoding<'k, Self> for UuidKey {
type Error = uuid::Error;
const LENGTH: Option<usize> = Some(16);
fn as_ord_bytes(&'k self) -> Result<std::borrow::Cow<'k, [u8]>, Self::Error> {
Ok(std::borrow::Cow::Borrowed(self.uuid.as_bytes()))
}
}
impl<'k> Key<'k> for UuidKey {
fn from_ord_bytes(bytes: &'k [u8]) -> Result<Self, Self::Error> {
Ok(UuidKey {
uuid: Uuid::from_slice(bytes)?,
})
}
}
impl From<OffsetDateTime> for DatetimeKey {
fn from(datetime: OffsetDateTime) -> Self {
Self::from_datetime(datetime)
}
}
impl From<DatetimeKey> for OffsetDateTime {
fn from(key: DatetimeKey) -> Self {
key.into_inner()
}
}

660
schema/src/lib.rs Normal file
View file

@ -0,0 +1,660 @@
pub mod keys;
use bonsaidb::core::{
document::{CollectionDocument, Emit},
schema::{
view::{map::Mappings, CollectionViewSchema},
Collection, ReduceResult, Schema, View, ViewMapResult,
},
};
use keys::{DatetimeKey, PostKind, ProfileKind, ReportState, UuidKey};
use serde::{Deserialize, Serialize};
#[derive(Debug, Schema)]
#[schema(
name = "SiteSchema",
collections = [
Account,
Profile,
Media,
Post,
Tag,
Report,
]
)]
pub struct SiteSchema;
#[derive(Collection, Debug, Deserialize, Serialize)]
#[collection(
name = "accounts",
views = [AccountsByUsername, AccountsByEmail],
primary_key = UuidKey,
natural_id = |account: &Account| Some(account.id)
)]
pub struct Account {
id: UuidKey,
username: String,
emails: Vec<String>,
}
#[derive(Collection, Debug, Deserialize, Serialize)]
#[collection(
name = "profiles",
views = [ProfilesByHandle, RemoteProfilesByDomain, LocalProfilesById, ProfilesByCollaborator, ProfilesByKind],
primary_key = UuidKey,
natural_id = |profile: &Profile| Some(profile.id)
)]
pub struct Profile {
id: UuidKey,
kind: ProfileKind,
handle: String,
domain: Option<String>,
full_url: Option<String>,
name: Option<String>,
description: Option<String>,
// Account Key
local_account: Option<UuidKey>,
// Profile Keys
collaborators: Vec<UuidKey>,
created_at: DatetimeKey,
updated_at: DatetimeKey,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum MediaState {
Uploading {
upload_id: String,
},
Finished {
filename: String,
delete_token: String,
},
}
#[derive(Collection, Debug, Deserialize, Serialize)]
#[collection(
name = "media",
views = [],
primary_key = UuidKey,
natural_id = |media: &Media| Some(media.id)
)]
pub struct Media {
id: UuidKey,
state: MediaState,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum DeletedBy {
Author,
PageOwner {
// Profile key
profile_id: UuidKey,
},
Moderator {
// Account key
account_id: UuidKey,
},
}
#[derive(Debug, Deserialize, Serialize)]
pub struct History {
content: String,
date: DatetimeKey,
}
#[derive(Collection, Debug, Deserialize, Serialize)]
#[collection(
name = "post",
views = [PostsByKind, PostsByAuthor, PostsByTag, PostsByMention, PostsByParent],
primary_key = UuidKey,
natural_id = |post: &Post| Some(post.id)
)]
pub struct Post {
id: UuidKey,
deleted_by: Option<DeletedBy>,
title: String,
title_history: Vec<History>,
description: Option<String>,
description_history: Vec<History>,
// Media key
media: Vec<UuidKey>,
// Tag key
tags: Vec<UuidKey>,
// Profile key
mentions: Vec<UuidKey>,
// Post key
reply_to: Option<UuidKey>,
// Profile key
author: UuidKey,
created_at: DatetimeKey,
updated_at: DatetimeKey,
}
#[derive(Collection, Debug, Deserialize, Serialize)]
#[collection(
name = "tags",
views = [TagsByName],
primary_key = UuidKey,
natural_id = |tag: &Tag| Some(tag.id)
)]
pub struct Tag {
id: UuidKey,
name: String,
}
#[derive(Collection, Debug, Deserialize, Serialize)]
#[collection(
name = "reports",
views = [ReportsByPost, ReportsByState, ReportsByAccount],
primary_key = UuidKey,
natural_id = |report: &Report| Some(report.id)
)]
pub struct Report {
id: UuidKey,
message: Option<String>,
moderation_note: Option<String>,
// Post key
post: UuidKey,
state: ReportState,
// Account Key
handled_by: Option<UuidKey>,
created_at: DatetimeKey,
updated_at: DatetimeKey,
}
#[derive(Debug, Clone, View)]
#[view(collection = Account, key = String, value = usize, name = "by-username")]
pub struct AccountsByUsername;
#[derive(Debug, Clone, View)]
#[view(collection = Account, key = String, value = usize, name = "by-email")]
pub struct AccountsByEmail;
#[derive(Debug, Clone, View)]
#[view(collection = Profile, key = String, value = usize, name = "by-handle")]
pub struct ProfilesByHandle;
#[derive(Debug, Clone, View)]
#[view(collection = Profile, key = Option<String>, value = usize, name = "by-remote-domain")]
pub struct RemoteProfilesByDomain;
#[derive(Debug, Clone, View)]
#[view(collection = Profile, key = Option<UuidKey>, value = usize, name = "by-local-id")]
pub struct LocalProfilesById;
#[derive(Debug, Clone, View)]
#[view(collection = Profile, key = UuidKey, value = usize, name = "by-collaborator")]
pub struct ProfilesByCollaborator;
#[derive(Debug, Clone, View)]
#[view(collection = Profile, key = ProfileKind, value = usize, name = "by-kind")]
pub struct ProfilesByKind;
#[derive(Debug, Clone, View)]
#[view(collection = Post, key = PostKind, value = usize, name = "by-kind")]
pub struct PostsByKind;
#[derive(Debug, Clone, View)]
#[view(collection = Post, key = UuidKey, value = usize, name = "by-author")]
pub struct PostsByAuthor;
#[derive(Debug, Clone, View)]
#[view(collection = Post, key = UuidKey, value = usize, name = "by-tag")]
pub struct PostsByTag;
#[derive(Debug, Clone, View)]
#[view(collection = Post, key = UuidKey, value = usize, name = "by-mention")]
pub struct PostsByMention;
#[derive(Debug, Clone, View)]
#[view(collection = Post, key = Option<UuidKey>, value = usize, name = "by-parent")]
pub struct PostsByParent;
#[derive(Debug, Clone, View)]
#[view(collection = Tag, key = String, value = usize, name = "by-name")]
pub struct TagsByName;
#[derive(Debug, Clone, View)]
#[view(collection = Report, key = UuidKey, value = usize, name = "by-name")]
pub struct ReportsByPost;
#[derive(Debug, Clone, View)]
#[view(collection = Report, key = ReportState, value = usize, name = "by-state")]
pub struct ReportsByState;
#[derive(Debug, Clone, View)]
#[view(collection = Report, key = Option<UuidKey>, value = usize, name = "by-account")]
pub struct ReportsByAccount;
impl CollectionViewSchema for AccountsByUsername {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.username, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for AccountsByEmail {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
if document.contents.emails.is_empty() {
return Ok(Mappings::Simple(None));
}
let mut mappings = Vec::new();
for email in document.contents.emails {
mappings.extend(document.header.emit_key_and_value(email, 1)?);
}
Ok(Mappings::List(mappings))
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for ProfilesByHandle {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
let mut mappings = Vec::new();
if document.contents.handle.is_empty() {
return Ok(Mappings::Simple(None));
}
const MAX_SEARCH_LEN: usize = 16;
// enable searching for profiles by handle lol
// turns 'asonix' into 'a', 'as', 'aso', 'ason', 'asoni', 'asonix'
// cap search characters
for i in 1..=document.contents.handle.len().min(MAX_SEARCH_LEN) {
mappings.extend(
document
.header
.emit_key_and_value(document.contents.handle[..i].to_owned(), 1)?,
);
}
// Ensure we index the full handle if longer than max characters as well
if document.contents.handle.len() > MAX_SEARCH_LEN {
mappings.extend(
document
.header
.emit_key_and_value(document.contents.handle, 1)?,
);
}
Ok(Mappings::List(mappings))
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for RemoteProfilesByDomain {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.domain, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for LocalProfilesById {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.local_account, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for ProfilesByCollaborator {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
if document.contents.collaborators.is_empty() {
return Ok(Mappings::Simple(None));
}
let mut mappings = Vec::new();
for collaborator in document.contents.collaborators {
mappings.extend(document.header.emit_key_and_value(collaborator, 1)?);
}
Ok(Mappings::List(mappings))
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for ProfilesByKind {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.kind, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for PostsByKind {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self as View>::Collection>,
) -> ViewMapResult<Self::View> {
let kind = if !document.contents.media.is_empty() {
PostKind::Media
} else if document.contents.reply_to.is_some() {
PostKind::Comment
} else {
PostKind::Text
};
document.header.emit_key_and_value(kind, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for PostsByAuthor {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.author, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for PostsByTag {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
if document.contents.tags.is_empty() {
return Ok(Mappings::Simple(None));
}
let mut mappings = Vec::new();
for tag in document.contents.tags {
mappings.extend(document.header.emit_key_and_value(tag, 1)?);
}
Ok(Mappings::List(mappings))
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for PostsByMention {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
if document.contents.mentions.is_empty() {
return Ok(Mappings::Simple(None));
}
let mut mappings = Vec::new();
for mention in document.contents.mentions {
mappings.extend(document.header.emit_key_and_value(mention, 1)?);
}
Ok(Mappings::List(mappings))
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for PostsByParent {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.reply_to, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for TagsByName {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.name, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for ReportsByPost {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.post, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for ReportsByState {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.state, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl CollectionViewSchema for ReportsByAccount {
type View = Self;
fn map(
&self,
document: CollectionDocument<<Self::View as View>::Collection>,
) -> ViewMapResult<Self::View> {
document
.header
.emit_key_and_value(document.contents.handled_by, 1)
}
fn reduce(
&self,
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
_rereduce: bool,
) -> ReduceResult<Self::View> {
Ok(mappings.iter().map(|m| m.value).sum())
}
}
impl Tag {
pub fn new(name: String) -> Self {
Self {
id: UuidKey::generate(),
name,
}
}
pub fn name(&self) -> &str {
&self.name
}
}

86
src/main.rs Normal file
View file

@ -0,0 +1,86 @@
use bonsaidb::{
client::{url::Url, Client},
core::{
connection::{AsyncConnection, AsyncStorageConnection},
schema::SerializedCollection,
},
local::config::Builder,
server::{DefaultPermissions, Server, ServerConfiguration},
};
use schema::{Account, SiteSchema};
use std::time::Duration;
type Error = Box<dyn std::error::Error + Send + Sync>;
#[tokio::main]
async fn main() -> Result<(), Error> {
let server = Server::open(
ServerConfiguration::new("server-data.bonsaidb")
.default_permissions(DefaultPermissions::AllowAll)
.with_schema::<SiteSchema>()?,
)
.await?;
if server.certificate_chain().await.is_err() {
server.install_self_signed_certificate(true).await?;
}
let certificate = server
.certificate_chain()
.await?
.into_end_entity_certificate();
server
.create_database::<SiteSchema>("my-database", true)
.await?;
let task_server = server.clone();
tokio::spawn(async move { task_server.listen_on(5645).await });
tokio::time::sleep(Duration::from_millis(10)).await;
tokio::spawn(async move {
let client = Client::build(Url::parse("bonsaidb://localhost")?)
.with_certificate(certificate)
.finish()?
.database::<SiteSchema>("site-db")
.await?;
/*
println!("inserting");
for i in 0..50 {
let sides = i % 3 + 3;
Shape::new(sides).push_into_async(&client).await?;
}
println!("reducing");
for result in client
.view::<ShapesByNumberOfSides>()
.reduce_grouped()
.await?
{
println!("sides: {}, count: {}", result.key, result.value);
}
println!("fetching");
let six_sided = client
.view::<ShapesByNumberOfSides>()
.with_key(5)
.query_with_docs()
.await?;
for mapping in &six_sided {
let _ = Shape::document_contents(mapping.document)?;
}
println!("fetched");
*/
Ok(()) as Result<(), Error>
});
tokio::signal::ctrl_c().await?;
server.shutdown(Some(Duration::from_secs(5))).await?;
println!("Hello, world!");
Ok(())
}