Post-stream commit

This commit is contained in:
asonix 2023-04-01 18:51:51 -05:00
commit 4b44515fd9
8 changed files with 3738 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/target
/result
/.direnv
/.envrc
/data.bonsaidb

2730
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

18
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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(&timestamp) {
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(&current.key) {
if duration < Duration::from_secs(60 * 60) {
for (post_id, score) in &current.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())
}
}