Post-stream commit
This commit is contained in:
commit
4b44515fd9
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/target
|
||||
/result
|
||||
/.direnv
|
||||
/.envrc
|
||||
/data.bonsaidb
|
2730
Cargo.lock
generated
Normal file
2730
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "streaming-funtimes"
|
||||
description = "A simple activitypub relay"
|
||||
version = "0.1.0"
|
||||
authors = ["asonix <asonix@asonix.dog>"]
|
||||
license = "AGPL-3.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.3.1", default-features = false }
|
||||
bcrypt = "0.14.0"
|
||||
bonsaidb = { git = "https://github.com/KhonsuLabs/bonsaidb", version = "0.4.0", branch = "main", features = ["server", "client"] }
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
tokio = { version = "1.27.0", features = ["full"] }
|
||||
uuid = { version = "1.3.0", features = ["v4", "serde"] }
|
43
flake.lock
Normal file
43
flake.lock
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1678901627,
|
||||
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1680213900,
|
||||
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
25
flake.nix
Normal file
25
flake.nix
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
description = "stream funtimes";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.default = pkgs.hello;
|
||||
|
||||
devShell = with pkgs; mkShell {
|
||||
nativeBuildInputs = [ cargo cargo-outdated cargo-zigbuild clippy gcc protobuf rust-analyzer rustc rustfmt ];
|
||||
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
};
|
||||
});
|
||||
}
|
402
src/main.rs
Normal file
402
src/main.rs
Normal file
|
@ -0,0 +1,402 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
future::{ready, Future, Ready},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use actix_web::{
|
||||
error::{ErrorBadRequest, ErrorForbidden, ErrorInternalServerError, ErrorNotFound},
|
||||
web::{self, Data, Json, Path, Query},
|
||||
App, FromRequest, HttpRequest, HttpServer,
|
||||
};
|
||||
use bonsaidb::{
|
||||
core::{
|
||||
connection::{AsyncConnection, AsyncStorageConnection},
|
||||
schema::{InsertError, SerializedCollection},
|
||||
},
|
||||
local::config::Builder,
|
||||
server::{DefaultPermissions, NoBackend, Server, ServerConfiguration, ServerDatabase},
|
||||
};
|
||||
use schema::{
|
||||
Comment, CommentsByPost, MySchema, PostId, PostWithTags, PostsWithTagsByMultipleTags, Session,
|
||||
TagSet, TrendingPosts, TrendingPostsByRank, UserByUsername, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::schema::User;
|
||||
|
||||
mod permute;
|
||||
mod schema;
|
||||
|
||||
async fn index() -> String {
|
||||
"Hewwo Mr Obama".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
struct PostQuery {
|
||||
tags: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl PostQuery {
|
||||
// ?tags=hi&tags=hello&tags=howdy&something=anotherthing
|
||||
fn tags(&self) -> TagSet {
|
||||
let tags = self
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|(field_name, field_value)| {
|
||||
if field_name == "tags" {
|
||||
Some(field_value.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
TagSet::new(tags)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NewPost {
|
||||
title: String,
|
||||
body: String,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NewComment {
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NewUser {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Auth {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AuthResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
struct CurrentUser(pub UserId);
|
||||
|
||||
impl FromRequest for CurrentUser {
|
||||
type Error = actix_web::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||
|
||||
fn from_request(req: &actix_web::HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future {
|
||||
match Token::parse(req) {
|
||||
Err(e) => Box::pin(ready(Err(e))),
|
||||
Ok(Token(token)) => {
|
||||
let fut = Data::<Repo>::extract(req);
|
||||
|
||||
Box::pin(async move {
|
||||
let repo = fut.await?;
|
||||
|
||||
let session_option = repo
|
||||
.database
|
||||
.collection::<Session>()
|
||||
.get(&token)
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
let session =
|
||||
session_option.ok_or_else(|| ErrorBadRequest("No session for token"))?;
|
||||
|
||||
let session =
|
||||
Session::document_contents(&session).map_err(ErrorInternalServerError)?;
|
||||
|
||||
Ok(CurrentUser(session.user_id))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Token(String);
|
||||
|
||||
impl Token {
|
||||
fn parse(req: &HttpRequest) -> Result<Self, actix_web::Error> {
|
||||
match req.headers().get("Authorization") {
|
||||
Some(value) => match value.to_str().map_err(ErrorBadRequest) {
|
||||
Err(e) => Err(ErrorBadRequest(e).into()),
|
||||
Ok(s) => {
|
||||
let token = s.trim_start_matches("Bearer ").trim().to_string();
|
||||
if token.len() == 32 {
|
||||
Ok(Token(token))
|
||||
} else {
|
||||
println!("{token}, {}", token.len());
|
||||
Err(ErrorBadRequest("Invalid token").into())
|
||||
}
|
||||
}
|
||||
},
|
||||
None => Err(ErrorBadRequest("No token present").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for Token {
|
||||
type Error = actix_web::Error;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future {
|
||||
ready(Token::parse(req))
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
repo: Data<Repo>,
|
||||
Json(NewUser { username, password }): Json<NewUser>,
|
||||
) -> actix_web::Result<String> {
|
||||
let user_result = User::new(username.clone(), password)
|
||||
.map_err(ErrorInternalServerError)?
|
||||
.push_into_async(&repo.database)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Err(InsertError {
|
||||
error: bonsaidb::core::Error::UniqueKeyViolation { .. },
|
||||
..
|
||||
}) => Err(ErrorForbidden("Username already taken").into()),
|
||||
Err(e) => Err(ErrorInternalServerError(e).into()),
|
||||
Ok(_) => Ok(String::from("Created")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn login(repo: Data<Repo>, Json(auth): Json<Auth>) -> actix_web::Result<Json<AuthResponse>> {
|
||||
let (_, user) = repo
|
||||
.database
|
||||
.view::<UserByUsername>()
|
||||
.with_key(&auth.username)
|
||||
.query_with_collection_docs()
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?
|
||||
.documents
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| ErrorNotFound("No user with supplied username"))?;
|
||||
|
||||
if !user
|
||||
.contents
|
||||
.verify(auth.password)
|
||||
.map_err(ErrorInternalServerError)?
|
||||
{
|
||||
return Err(ErrorBadRequest("Invalid password").into());
|
||||
}
|
||||
|
||||
let session = Session::new(user.contents.id)
|
||||
.push_into_async(&repo.database)
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
||||
Ok(Json(AuthResponse {
|
||||
token: session.contents.token.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn create_post(
|
||||
CurrentUser(user_id): CurrentUser,
|
||||
repo: Data<Repo>,
|
||||
Json(new_post): Json<NewPost>,
|
||||
) -> actix_web::Result<String> {
|
||||
PostWithTags {
|
||||
user_id,
|
||||
id: PostId::default(),
|
||||
title: new_post.title,
|
||||
body: new_post.body,
|
||||
tags: new_post.tags,
|
||||
}
|
||||
.push_into_async(&repo.database)
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
||||
Ok("Created!".to_string())
|
||||
}
|
||||
|
||||
async fn get_post(repo: Data<Repo>, path: Path<PostId>) -> actix_web::Result<Json<PostWithTags>> {
|
||||
let post_id = path.into_inner();
|
||||
let option = repo
|
||||
.database
|
||||
.collection::<PostWithTags>()
|
||||
.get(&post_id)
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
||||
if let Some(document) = option {
|
||||
Ok(Json(
|
||||
PostWithTags::document_contents(&document).map_err(ErrorInternalServerError)?,
|
||||
))
|
||||
} else {
|
||||
Err(ErrorNotFound("Post does not exist").into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_all_posts(
|
||||
repo: Data<Repo>,
|
||||
query: Query<PostQuery>,
|
||||
) -> actix_web::Result<Json<Vec<PostWithTags>>> {
|
||||
let posts = if !query.tags.is_empty() {
|
||||
repo.database
|
||||
.view::<PostsWithTagsByMultipleTags>()
|
||||
.with_key(&query.tags())
|
||||
.query_with_collection_docs()
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?
|
||||
.documents
|
||||
.into_values()
|
||||
.map(|post| Ok(post.contents))
|
||||
.collect::<Result<Vec<_>, uuid::Error>>()
|
||||
.map_err(ErrorInternalServerError)?
|
||||
} else {
|
||||
repo.database
|
||||
.collection::<PostWithTags>()
|
||||
.all()
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?
|
||||
.into_iter()
|
||||
.map(|doc| PostWithTags::document_contents(&doc))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(ErrorInternalServerError)?
|
||||
};
|
||||
|
||||
Ok(Json(posts))
|
||||
}
|
||||
|
||||
async fn create_comment(
|
||||
CurrentUser(user_id): CurrentUser,
|
||||
Json(new_comment): Json<NewComment>,
|
||||
path: Path<PostId>,
|
||||
repo: Data<Repo>,
|
||||
) -> actix_web::Result<String> {
|
||||
let post_id = path.into_inner();
|
||||
|
||||
Comment {
|
||||
user_id,
|
||||
post_id,
|
||||
body: new_comment.body,
|
||||
}
|
||||
.push_into_async(&repo.database)
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
||||
let _ = TrendingPosts::now(post_id)
|
||||
.push_into_async(&repo.database)
|
||||
.await;
|
||||
|
||||
Ok(String::from("created"))
|
||||
}
|
||||
|
||||
async fn get_comments(
|
||||
path: Path<PostId>,
|
||||
repo: Data<Repo>,
|
||||
) -> actix_web::Result<Json<Vec<Comment>>> {
|
||||
let post_id = path.into_inner();
|
||||
let comments = repo
|
||||
.database
|
||||
.view::<CommentsByPost>()
|
||||
.with_key(&post_id)
|
||||
.query_with_collection_docs()
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
||||
Ok(Json(
|
||||
comments
|
||||
.documents
|
||||
.into_values()
|
||||
.map(|doc| doc.contents)
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn posts_by_comments(repo: Data<Repo>) -> actix_web::Result<Json<HashMap<PostId, usize>>> {
|
||||
let reduced = repo
|
||||
.database
|
||||
.view::<CommentsByPost>()
|
||||
.reduce_grouped()
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
||||
let hm = reduced
|
||||
.into_iter()
|
||||
.map(|mapped_value| (mapped_value.key, mapped_value.value))
|
||||
.collect();
|
||||
|
||||
Ok(Json(hm))
|
||||
}
|
||||
|
||||
async fn trending_posts(repo: Data<Repo>) -> actix_web::Result<Json<HashMap<PostId, f64>>> {
|
||||
let reduced = repo
|
||||
.database
|
||||
.view::<TrendingPostsByRank>()
|
||||
.reduce()
|
||||
.await
|
||||
.map_err(ErrorInternalServerError)?;
|
||||
|
||||
Ok(Json(reduced))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Repo {
|
||||
database: ServerDatabase<NoBackend>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let server = Server::open(
|
||||
ServerConfiguration::new("./data.bonsaidb")
|
||||
.default_permissions(DefaultPermissions::AllowAll)
|
||||
.with_schema::<MySchema>()?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server_database = server
|
||||
.create_database::<MySchema>("my-database", true)
|
||||
.await?;
|
||||
|
||||
let repo = Repo {
|
||||
database: server_database,
|
||||
};
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(Data::new(repo.clone()))
|
||||
.service(web::resource("/").route(web::get().to(index)))
|
||||
.service(
|
||||
web::scope("/users").service(web::resource("").route(web::post().to(create_user))),
|
||||
)
|
||||
.service(
|
||||
web::scope("/sessions").service(web::resource("").route(web::post().to(login))),
|
||||
)
|
||||
.service(
|
||||
web::scope("/posts")
|
||||
.service(
|
||||
web::resource("")
|
||||
.route(web::get().to(get_all_posts))
|
||||
.route(web::post().to(create_post)),
|
||||
)
|
||||
.service(web::resource("/by-comments").route(web::get().to(posts_by_comments)))
|
||||
.service(web::resource("/trending").route(web::get().to(trending_posts)))
|
||||
.service(
|
||||
web::scope("/{id}")
|
||||
.service(web::resource("").route(web::get().to(get_post)))
|
||||
.service(
|
||||
web::resource("/comments")
|
||||
.route(web::get().to(get_comments))
|
||||
.route(web::post().to(create_comment)),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.bind("0.0.0.0:8006")?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
65
src/permute.rs
Normal file
65
src/permute.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
pub struct Permuter<'a, T> {
|
||||
bound: Option<usize>,
|
||||
slice: &'a [T],
|
||||
inner: Option<(&'a T, Box<Permuter<'a, T>>)>,
|
||||
}
|
||||
|
||||
impl<'a, T> Permuter<'a, T> {
|
||||
fn new(slice: &'a [T], bound: Option<usize>) -> Self {
|
||||
Self {
|
||||
bound,
|
||||
slice,
|
||||
inner: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for Permuter<'a, T> {
|
||||
type Item = Vec<&'a T>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some((current, inner)) = &mut self.inner {
|
||||
if let Some(mut vec) = inner.next() {
|
||||
vec.push(current);
|
||||
Some(vec)
|
||||
} else if self.slice.is_empty() {
|
||||
None
|
||||
} else {
|
||||
self.inner = None;
|
||||
self.next()
|
||||
}
|
||||
} else if self.slice.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let (first, rest) = self.slice.split_at(1);
|
||||
self.slice = rest;
|
||||
self.inner = if let Some(bound) = self.bound {
|
||||
if bound > 0 {
|
||||
Some((&first[0], Box::new(Permuter::new(rest, Some(bound - 1)))))
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
Some((&first[0], Box::new(Permuter::new(rest, None))))
|
||||
};
|
||||
|
||||
Some(vec![&first[0]])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Permute<T> {
|
||||
fn permute(&self) -> Permuter<'_, T>;
|
||||
|
||||
fn permute_bounded(&self, bound: usize) -> Permuter<'_, T>;
|
||||
}
|
||||
|
||||
impl<T> Permute<T> for [T] {
|
||||
fn permute(&self) -> Permuter<'_, T> {
|
||||
Permuter::new(self, None)
|
||||
}
|
||||
|
||||
fn permute_bounded(&self, bound: usize) -> Permuter<'_, T> {
|
||||
Permuter::new(self, Some(bound))
|
||||
}
|
||||
}
|
450
src/schema.rs
Normal file
450
src/schema.rs
Normal file
|
@ -0,0 +1,450 @@
|
|||
use std::{collections::HashMap, str::Utf8Error, time::Duration};
|
||||
|
||||
use crate::permute::Permute;
|
||||
use bonsaidb::core::{
|
||||
document::Emit,
|
||||
key::{time::MinutesSinceUnixEpoch, Key, KeyEncoding},
|
||||
schema::{Collection, CollectionViewSchema, Schema, View},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Define what our database can store!!!
|
||||
#[derive(Debug, Schema)]
|
||||
#[schema(name = "MySchema", collections = [PostWithTags, Comment, TrendingPosts, User, Session])]
|
||||
pub struct MySchema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Collection)]
|
||||
#[collection(name = "posts-with-tags", primary_key = PostId, natural_id = |post: &PostWithTags| Some(post.id), views = [PostsWithTagsByTag, PostsWithTagsByMultipleTags])]
|
||||
pub struct PostWithTags {
|
||||
pub user_id: UserId,
|
||||
pub id: PostId,
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Collection)]
|
||||
#[collection(name = "comments", views = [CommentsByPost])]
|
||||
pub struct Comment {
|
||||
pub user_id: UserId,
|
||||
pub post_id: PostId,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Collection)]
|
||||
#[collection(name = "trending-posts", views = [TrendingPostsByRank])]
|
||||
pub struct TrendingPosts {
|
||||
pub id: PostId,
|
||||
pub timestamp: MinutesSinceUnixEpoch,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Collection)]
|
||||
#[collection(name = "users", primary_key = UserId, natural_id = |user: &User| Some(user.id), views = [UserByUsername])]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub username: String,
|
||||
hash: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Collection)]
|
||||
#[collection(name = "sessions", primary_key = String, natural_id = |session: &Session| Some(session.token.clone()), views = [SessionsByUserId])]
|
||||
pub struct Session {
|
||||
pub user_id: UserId,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(username: String, password: String) -> bcrypt::BcryptResult<Self> {
|
||||
Ok(Self {
|
||||
id: UserId::default(),
|
||||
username,
|
||||
hash: bcrypt::hash(password, bcrypt::DEFAULT_COST)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verify(&self, password: String) -> bcrypt::BcryptResult<bool> {
|
||||
bcrypt::verify(&password, &self.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(user_id: UserId) -> Self {
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, Distribution},
|
||||
thread_rng,
|
||||
};
|
||||
|
||||
let token = Alphanumeric
|
||||
.sample_iter(&mut thread_rng())
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
Self { user_id, token }
|
||||
}
|
||||
}
|
||||
|
||||
impl TrendingPosts {
|
||||
pub fn now(id: PostId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
timestamp: MinutesSinceUnixEpoch::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, View)]
|
||||
#[view(collection = PostWithTags, key = String, value = usize)]
|
||||
pub struct PostsWithTagsByTag;
|
||||
|
||||
#[derive(Debug, Clone, View)]
|
||||
#[view(collection = PostWithTags, key = TagSet, value = usize)]
|
||||
pub struct PostsWithTagsByMultipleTags;
|
||||
|
||||
#[derive(Debug, Clone, View)]
|
||||
#[view(collection = Comment, key = PostId, value = usize)]
|
||||
pub struct CommentsByPost;
|
||||
|
||||
#[derive(Debug, Clone, View)]
|
||||
#[view(collection = TrendingPosts, key = MinutesSinceUnixEpoch, value = HashMap<PostId, f64>)]
|
||||
pub struct TrendingPostsByRank;
|
||||
|
||||
#[derive(Debug, Clone, View)]
|
||||
#[view(collection = Session, key = UserId, value = usize)]
|
||||
pub struct SessionsByUserId;
|
||||
|
||||
#[derive(Debug, Clone, View)]
|
||||
#[view(collection = User, key = String, value = usize)]
|
||||
pub struct UserByUsername;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(transparent)]
|
||||
pub struct PostId {
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
impl Default for PostId {
|
||||
fn default() -> Self {
|
||||
PostId { id: Uuid::new_v4() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'k> KeyEncoding<'k> for PostId {
|
||||
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::Owned(self.id.as_bytes().to_vec()))
|
||||
}
|
||||
|
||||
fn describe<Visitor>(visitor: &mut Visitor)
|
||||
where
|
||||
Visitor: bonsaidb::core::key::KeyVisitor,
|
||||
{
|
||||
visitor.visit_type(bonsaidb::core::key::KeyKind::Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'k> Key<'k> for PostId {
|
||||
const CAN_OWN_BYTES: bool = false;
|
||||
|
||||
fn from_ord_bytes<'e>(
|
||||
bytes: bonsaidb::core::key::ByteSource<'k, 'e>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let id = Uuid::from_slice(bytes.as_ref())?;
|
||||
|
||||
Ok(PostId { id })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(transparent)]
|
||||
pub struct UserId {
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
impl Default for UserId {
|
||||
fn default() -> Self {
|
||||
UserId { id: Uuid::new_v4() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'k> KeyEncoding<'k> for UserId {
|
||||
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::Owned(self.id.as_bytes().to_vec()))
|
||||
}
|
||||
|
||||
fn describe<Visitor>(visitor: &mut Visitor)
|
||||
where
|
||||
Visitor: bonsaidb::core::key::KeyVisitor,
|
||||
{
|
||||
visitor.visit_type(bonsaidb::core::key::KeyKind::Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'k> Key<'k> for UserId {
|
||||
const CAN_OWN_BYTES: bool = false;
|
||||
|
||||
fn from_ord_bytes<'e>(
|
||||
bytes: bonsaidb::core::key::ByteSource<'k, 'e>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let id = Uuid::from_slice(bytes.as_ref())?;
|
||||
|
||||
Ok(UserId { id })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct TagSet {
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl TagSet {
|
||||
pub fn new(mut tags: Vec<String>) -> Self {
|
||||
tags.sort();
|
||||
|
||||
Self { tags }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'k> KeyEncoding<'k> for TagSet {
|
||||
type Error = Utf8Error;
|
||||
|
||||
const LENGTH: Option<usize> = None;
|
||||
|
||||
fn as_ord_bytes(&'k self) -> Result<std::borrow::Cow<'k, [u8]>, Self::Error> {
|
||||
let mut output = Vec::new();
|
||||
|
||||
for tag in &self.tags {
|
||||
output.extend_from_slice(tag.as_bytes());
|
||||
output.push(0);
|
||||
}
|
||||
|
||||
Ok(std::borrow::Cow::Owned(output))
|
||||
}
|
||||
|
||||
fn describe<Visitor>(visitor: &mut Visitor)
|
||||
where
|
||||
Visitor: bonsaidb::core::key::KeyVisitor,
|
||||
{
|
||||
visitor.visit_type(bonsaidb::core::key::KeyKind::Bytes)
|
||||
}
|
||||
}
|
||||
impl<'k> Key<'k> for TagSet {
|
||||
const CAN_OWN_BYTES: bool = false;
|
||||
|
||||
fn from_ord_bytes<'e>(
|
||||
bytes: bonsaidb::core::key::ByteSource<'k, 'e>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let indices = bytes
|
||||
.as_ref()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, byte)| if *byte == 0 { Some(index) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut tags = Vec::new();
|
||||
|
||||
let mut start = 0;
|
||||
|
||||
for index in indices {
|
||||
let string = std::str::from_utf8(&bytes.as_ref()[start..index])?;
|
||||
start = index + 1;
|
||||
|
||||
tags.push(String::from(string));
|
||||
}
|
||||
|
||||
Ok(TagSet { tags })
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionViewSchema for PostsWithTagsByTag {
|
||||
type View = Self;
|
||||
|
||||
fn map(
|
||||
&self,
|
||||
document: bonsaidb::core::document::CollectionDocument<<Self::View as View>::Collection>,
|
||||
) -> bonsaidb::core::schema::ViewMapResult<Self::View> {
|
||||
document
|
||||
.contents
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| document.header.emit_key_and_value(tag.clone(), 1))
|
||||
.collect::<Result<_, _>>()
|
||||
}
|
||||
|
||||
fn reduce(
|
||||
&self,
|
||||
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
|
||||
_rereduce: bool,
|
||||
) -> bonsaidb::core::schema::ReduceResult<Self::View> {
|
||||
Ok(mappings.iter().map(|mapping| mapping.value).sum())
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionViewSchema for PostsWithTagsByMultipleTags {
|
||||
type View = Self;
|
||||
|
||||
fn map(
|
||||
&self,
|
||||
document: bonsaidb::core::document::CollectionDocument<<Self::View as View>::Collection>,
|
||||
) -> bonsaidb::core::schema::ViewMapResult<Self::View> {
|
||||
let mut tags = document.contents.tags.clone();
|
||||
|
||||
tags.sort();
|
||||
tags.reverse();
|
||||
|
||||
tags.permute()
|
||||
.map(|tags| {
|
||||
document.header.emit_key_and_value(
|
||||
TagSet {
|
||||
tags: tags.into_iter().cloned().collect(),
|
||||
},
|
||||
1,
|
||||
)
|
||||
})
|
||||
.collect::<Result<_, _>>()
|
||||
}
|
||||
|
||||
fn reduce(
|
||||
&self,
|
||||
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
|
||||
_rereduce: bool,
|
||||
) -> bonsaidb::core::schema::ReduceResult<Self::View> {
|
||||
Ok(mappings.iter().map(|mapping| mapping.value).sum())
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionViewSchema for CommentsByPost {
|
||||
type View = Self;
|
||||
|
||||
fn map(
|
||||
&self,
|
||||
document: bonsaidb::core::document::CollectionDocument<<Self::View as View>::Collection>,
|
||||
) -> bonsaidb::core::schema::ViewMapResult<Self::View> {
|
||||
document
|
||||
.header
|
||||
.emit_key_and_value(document.contents.post_id, 1)
|
||||
}
|
||||
|
||||
fn reduce(
|
||||
&self,
|
||||
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
|
||||
_rereduce: bool,
|
||||
) -> bonsaidb::core::schema::ReduceResult<Self::View> {
|
||||
Ok(mappings.iter().map(|mapping| mapping.value).sum())
|
||||
}
|
||||
}
|
||||
|
||||
// if 1 comment was created now, that leads to a score of 1
|
||||
// if 1 comment was created 60 minutes ago, that leads to a score of 0
|
||||
// x = 0, y = 1
|
||||
// x = 60, y = 0
|
||||
fn the_algorithm(now: MinutesSinceUnixEpoch, timestamp: MinutesSinceUnixEpoch, score: f64) -> f64 {
|
||||
if let Ok(Some(duration)) = now.duration_since(×tamp) {
|
||||
let x = duration.as_secs() / 60;
|
||||
|
||||
let y = 1_f64 - ((x as f64).sqrt() * (1_f64 / 60_f64.sqrt()));
|
||||
|
||||
if y < 0_f64 {
|
||||
return 0_f64;
|
||||
}
|
||||
|
||||
return y * score;
|
||||
}
|
||||
|
||||
0_f64
|
||||
}
|
||||
|
||||
impl CollectionViewSchema for TrendingPostsByRank {
|
||||
type View = Self;
|
||||
|
||||
fn map(
|
||||
&self,
|
||||
document: bonsaidb::core::document::CollectionDocument<<Self::View as View>::Collection>,
|
||||
) -> bonsaidb::core::schema::ViewMapResult<Self::View> {
|
||||
let timestamp = document.contents.timestamp;
|
||||
let post_id = document.contents.id;
|
||||
|
||||
let mut hashmap = HashMap::new();
|
||||
hashmap.insert(post_id, 1_f64);
|
||||
|
||||
document.header.emit_key_and_value(timestamp, hashmap)
|
||||
}
|
||||
|
||||
fn reduce(
|
||||
&self,
|
||||
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
|
||||
_rereduce: bool,
|
||||
) -> bonsaidb::core::schema::ReduceResult<Self::View> {
|
||||
let now = mappings.iter().map(|mapping| mapping.key).max();
|
||||
|
||||
if let Some(now) = now {
|
||||
Ok(mappings.iter().fold(HashMap::new(), |mut acc, current| {
|
||||
if let Ok(Some(duration)) = now.duration_since(¤t.key) {
|
||||
if duration < Duration::from_secs(60 * 60) {
|
||||
for (post_id, score) in ¤t.value {
|
||||
let entry = acc.entry(*post_id).or_default();
|
||||
|
||||
*entry += the_algorithm(now, current.key, *score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acc
|
||||
}))
|
||||
} else {
|
||||
Ok(HashMap::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionViewSchema for SessionsByUserId {
|
||||
type View = Self;
|
||||
|
||||
fn map(
|
||||
&self,
|
||||
document: bonsaidb::core::document::CollectionDocument<<Self::View as View>::Collection>,
|
||||
) -> bonsaidb::core::schema::ViewMapResult<Self::View> {
|
||||
document
|
||||
.header
|
||||
.emit_key_and_value(document.contents.user_id, 1)
|
||||
}
|
||||
|
||||
fn reduce(
|
||||
&self,
|
||||
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
|
||||
_rereduce: bool,
|
||||
) -> bonsaidb::core::schema::ReduceResult<Self::View> {
|
||||
Ok(mappings.iter().map(|mapping| mapping.value).sum())
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionViewSchema for UserByUsername {
|
||||
type View = Self;
|
||||
|
||||
fn unique(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn map(
|
||||
&self,
|
||||
document: bonsaidb::core::document::CollectionDocument<<Self::View as View>::Collection>,
|
||||
) -> bonsaidb::core::schema::ViewMapResult<Self::View> {
|
||||
document
|
||||
.header
|
||||
.emit_key_and_value(document.contents.username.clone(), 1)
|
||||
}
|
||||
|
||||
fn reduce(
|
||||
&self,
|
||||
mappings: &[bonsaidb::core::schema::ViewMappedValue<Self::View>],
|
||||
_rereduce: bool,
|
||||
) -> bonsaidb::core::schema::ReduceResult<Self::View> {
|
||||
Ok(mappings.iter().map(|mapping| mapping.value).sum())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue