basic database

This commit is contained in:
emilis 2022-09-11 16:52:37 +01:00
parent 1a3c6f2209
commit 8be8d0ebe1
18 changed files with 867 additions and 119 deletions

45
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'flabk'",
"cargo": {
"args": [
"build",
"--bin=flabk",
"--package=flabk"
],
"filter": {
"name": "flabk",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
// {
// "type": "lldb",
// "request": "launch",
// "name": "Debug unit tests in executable 'flabk'",
// "cargo": {
// "args": [
// "test",
// "--no-run",
// "--bin=flabk",
// "--package=flabk"
// ],
// "filter": {
// "name": "flabk",
// "kind": "bin"
// }
// },
// "args": [],
// "cwd": "${workspaceFolder}"
// }
]
}

305
Cargo.lock generated
View File

@ -2,18 +2,71 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anyhow"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7"
[[package]]
name = "async-trait"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "base-62"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28ebd71b3e708e895b83ec2d35c6e2ef96e34945706bf4d73826354e84f89b2"
dependencies = [
"failure",
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "base64"
version = "0.13.0"
@ -66,6 +119,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -108,8 +167,37 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer 0.10.3",
"crypto-common",
"subtle",
]
[[package]]
name = "failure"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
dependencies = [
"backtrace",
"failure_derive",
]
[[package]]
name = "failure_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fastrand"
version = "1.8.0"
@ -124,10 +212,13 @@ name = "flabk"
version = "0.0.1"
dependencies = [
"anyhow",
"base-62",
"handlebars",
"rand",
"serde",
"serde_json",
"tokio",
"tokio-postgres",
"warp",
]
@ -162,6 +253,17 @@ version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
[[package]]
name = "futures-macro"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.24"
@ -181,6 +283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
dependencies = [
"futures-core",
"futures-macro",
"futures-sink",
"futures-task",
"pin-project-lite",
@ -209,6 +312,12 @@ dependencies = [
"wasi",
]
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "h2"
version = "0.3.14"
@ -282,6 +391,15 @@ dependencies = [
"libc",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.3",
]
[[package]]
name = "http"
version = "0.2.8"
@ -400,6 +518,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "md-5"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b48670c893079d3c2ed79114e3644b7004df1c361a4e0ad52e2e6940d07c3d"
dependencies = [
"digest 0.10.3",
]
[[package]]
name = "memchr"
version = "2.5.0"
@ -422,6 +549,15 @@ dependencies = [
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.4"
@ -452,6 +588,36 @@ dependencies = [
"twoway",
]
[[package]]
name = "num-bigint"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.13.1"
@ -462,6 +628,15 @@ dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.14.0"
@ -547,6 +722,24 @@ dependencies = [
"sha-1 0.10.0",
]
[[package]]
name = "phf"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_shared"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.0.12"
@ -579,6 +772,37 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "postgres-protocol"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c"
dependencies = [
"base64",
"byteorder",
"bytes",
"fallible-iterator",
"hmac",
"md-5",
"memchr",
"rand",
"sha2",
"stringprep",
]
[[package]]
name = "postgres-types"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73d946ec7d256b04dfadc4e6a3292324e6f417124750fc5c0950f981b703a0f1"
dependencies = [
"bytes",
"fallible-iterator",
"postgres-protocol",
"serde",
"serde_json",
]
[[package]]
name = "ppv-lite86"
version = "0.2.16"
@ -657,6 +881,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "ryu"
version = "1.0.11"
@ -759,6 +989,17 @@ dependencies = [
"digest 0.10.3",
]
[[package]]
name = "sha2"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.3",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
@ -768,6 +1009,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "slab"
version = "0.4.7"
@ -793,6 +1040,22 @@ dependencies = [
"winapi",
]
[[package]]
name = "stringprep"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.99"
@ -804,6 +1067,18 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "tempfile"
version = "3.3.0"
@ -885,6 +1160,30 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-postgres"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29a12c1b3e0704ae7dfc25562629798b29c72e6b1d0a681b6f29ab4ae5e7f7bf"
dependencies = [
"async-trait",
"byteorder",
"bytes",
"fallible-iterator",
"futures-channel",
"futures-util",
"log",
"parking_lot",
"percent-encoding",
"phf",
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"socket2",
"tokio",
"tokio-util 0.7.4",
]
[[package]]
name = "tokio-stream"
version = "0.1.9"
@ -1040,6 +1339,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-xid"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "url"
version = "2.3.1"

View File

@ -7,8 +7,11 @@ edition = "2021"
[dependencies]
anyhow = "1.0.64"
base-62 = "0.1.1"
handlebars = "4.3.3"
rand = "0.8.5"
serde = { version = "1.0.144", features = ["derive", "std", "serde_derive"]}
serde_json = "1.0.85"
tokio = { version = "1", features = ["full"] }
tokio-postgres = { version = "0.7.7", features = ["with-serde_json-1"] }
warp = { version = "0.3.2" }

View File

@ -0,0 +1,10 @@
CREATE TABLE users (
id CHAR(22) NOT NULL PRIMARY KEY,
username TEXT NOT NULL,
host TEXT,
display_name TEXT
);
CREATE UNIQUE INDEX u_username_host ON users (username, host);
CREATE UNIQUE INDEX u_username_local ON users (username) WHERE host IS NULL;

47
src/database/db.rs Normal file
View File

@ -0,0 +1,47 @@
use std::sync::Arc;
use super::users::Users;
use tokio::sync::Mutex;
use tokio_postgres::{tls::NoTlsStream, Client, Connection, NoTls, Socket};
const DBERR_UNIQUE: &str = "23505";
#[derive(Clone)]
pub struct DB {
client: Arc<Mutex<Client>>,
}
impl DB {
pub async fn new(host: String, user: String, database: String) -> Result<Self, anyhow::Error> {
let (cl, conn) = tokio_postgres::connect(
format!("host={host} user={user} dbname={database}").as_str(),
NoTls,
)
.await?;
tokio::spawn(async move {
if let Err(e) = conn.await {
eprintln!("connection error: {}", e);
}
});
let client = Arc::new(Mutex::new(cl));
Ok(Self { client })
}
pub fn users(&self) -> Users {
Users::new(self.client.clone())
}
}
pub enum DBError {
Duplicate,
Other(tokio_postgres::Error),
}
impl From<tokio_postgres::Error> for DBError {
fn from(err: tokio_postgres::Error) -> Self {
if let Some(code) = err.code() && code.code() == DBERR_UNIQUE {
return DBError::Duplicate;
}
DBError::Other(err)
}
}

2
src/database/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod db;
pub mod users;

95
src/database/users.rs Normal file
View File

@ -0,0 +1,95 @@
use std::sync::Arc;
use rand::Rng;
use tokio::sync::Mutex;
use tokio_postgres::{Client, Row};
use super::db;
#[derive(Clone)]
pub struct Users(Arc<Mutex<Client>>);
impl Users {
pub fn new(client: Arc<Mutex<Client>>) -> Self {
Self(client)
}
fn new_id() -> String {
let bytes = rand::thread_rng().gen::<[u8; 16]>();
base_62::encode(&bytes)
}
pub async fn create_user(&self, u: User) -> Result<User, db::DBError> {
let row = self.0.lock().await.query_one(
"insert into users (id, username, host, display_name) values ($1, $2, $3, $4) returning id",
&[&Self::new_id(), &u.username, &u.host, &u.display_name],
).await?;
Ok(User {
id: row.get("id"),
username: u.username,
host: u.host,
display_name: u.display_name,
})
}
pub async fn user(&self, by: UserSelect) -> Result<Option<User>, anyhow::Error> {
let where_param: String;
let where_clause = match by {
UserSelect::ID(id) => {
where_param = id;
"id = $1"
}
UserSelect::Username(username) => {
where_param = username;
"username = $1"
}
UserSelect::FullUsername(full) => {
where_param = full;
"(username || '@' || host) = $1"
}
};
let rows = self
.0
.lock()
.await
.query(
format!(
"select id, username, host, display_name from users where {}",
where_clause
)
.as_str(),
&[&where_param],
)
.await?;
if let Some(row) = rows.first() && rows.len() == 1 {
Ok(Some(User::from(row)))
} else {
Ok(None)
}
}
}
pub struct User {
pub id: String,
pub username: String,
pub host: Option<String>,
pub display_name: Option<String>,
}
impl From<&Row> for User {
fn from(row: &Row) -> Self {
Self {
id: row.get("id"),
username: row.get("username"),
host: row.get("host"),
display_name: row.get("display_name"),
}
}
}
pub enum UserSelect {
ID(String),
Username(String),
FullUsername(String),
}

20
src/helpers.rs Normal file
View File

@ -0,0 +1,20 @@
use warp::hyper::StatusCode;
pub(crate) fn html_with_status(body: String, status: StatusCode) -> warp::http::Response<String> {
warp::http::Response::builder()
.header("Content-Type", "text/html; charset=utf-8")
.status(status)
.body(body)
.expect("failed marshalling html response")
}
pub(crate) fn html(body: String) -> warp::http::Response<String> {
html_with_status(body, StatusCode::OK)
}
// pub(crate) fn html(body: String) -> Result<warp::http::Response<String>, warp::http::Error> {
// warp::http::Response::builder()
// .header("Content-Type", "text/html; charset=utf-8")
// .status(StatusCode::OK)
// .body(body)
// }

View File

@ -1,7 +1,13 @@
mod database;
mod helpers;
mod model;
mod servek;
mod svc;
use database::db::DB;
use serde::{Deserialize, Serialize};
use servek::Server;
use servek::servek::Server;
use svc::profiles::Profiler;
#[derive(Clone, Copy, Serialize, Deserialize)]
pub enum ActivityKind {
@ -31,7 +37,13 @@ pub struct ObjectLD {
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
Server::new().listen_and_serve(8008).await;
let db = DB::new(
"localhost".to_owned(),
"flabk".to_owned(),
"flabk".to_owned(),
)
.await?;
let profiler = Profiler::new(db.users());
Server::new(profiler).listen_and_serve(8008).await;
Ok(())
}

12
src/model.rs Normal file
View File

@ -0,0 +1,12 @@
use serde::Serialize;
#[derive(Clone, Serialize)]
pub struct Error<'a> {
pub error: &'a str,
}
impl<'a> Error<'a> {
pub fn error(error: &'a str) -> Self {
Self { error }
}
}

View File

@ -1,116 +0,0 @@
use core::panic;
use std::{convert::Infallible, fmt::Display};
use handlebars::{Handlebars, RenderError};
use serde::Serialize;
use warp::{hyper::StatusCode, reject::Reject, Filter, Rejection, Reply};
#[derive(Debug, Clone)]
pub struct Server {
hb: Handlebars<'static>,
profiler: Profiler,
}
#[derive(Debug, Clone)]
struct Profiler;
impl Profiler {
fn profile(&self, username: String) -> Result<Profile, Infallible> {
Ok(Profile { username })
}
}
impl Server {
pub fn new() -> Self {
let mut hb = Handlebars::new();
hb.register_template_string("profile", include_str!("../templates/html/profile.html"))
.expect("profile template");
let profiler = Profiler;
Self { hb, profiler }
}
pub async fn listen_and_serve(self, port: u16) -> ! {
println!("starting server on port {}", port);
warp::serve(self.html().recover(Self::handle_rejection))
.run(([127, 0, 0, 1], port))
.await;
panic!("server stopped prematurely")
}
fn html(&self) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
Self::index().or(self.profile())
}
fn index() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::get().and(warp::path::end().map(move || {
warp::reply::html(include_str!("../templates/html/index.html").to_owned())
}))
}
fn with_server(
srv: Server,
) -> impl Filter<Extract = (Server,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || srv.clone())
}
fn profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::get().and(
warp::path!("@" / String)
.and(Self::with_server(self.clone()))
.and_then(|username: String, srv: Server| async move {
match srv.hb.render(
"profile",
&serde_json::json!(srv.profiler.profile(username)?),
) {
Ok(html) => Ok(warp::reply::html(html)),
Err(err) => Err(InternalError::reject(err.to_string())),
}
}),
)
}
async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
if let Some(internal) = err.find::<InternalError>() {
println!("internal error: {}", internal);
return Ok(warp::reply::with_status(
"internal server error",
StatusCode::INTERNAL_SERVER_ERROR,
));
}
panic!()
}
}
#[derive(Clone, Debug)]
struct InternalError {
inner: String,
}
impl InternalError {
fn reject(err: String) -> Rejection {
warp::reject::custom(Self { inner: err })
}
}
impl Reject for InternalError {}
impl From<RenderError> for InternalError {
fn from(r: RenderError) -> Self {
Self {
inner: r.to_string(),
}
}
}
impl Display for InternalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}
#[derive(Serialize)]
struct Profile {
username: String,
}

68
src/servek/html.rs Normal file
View File

@ -0,0 +1,68 @@
use std::str::FromStr;
use warp::{hyper::Uri, Filter, Rejection, Reply};
use super::servek::{Server, ServerError};
impl Server {
pub(super) async fn html(
&self,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
Self::index()
.or(self.profile().await)
.or(self.create_profile().await)
}
fn index() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::get().and(warp::path::end().map(move || {
warp::reply::html(include_str!("../../templates/html/index.html").to_owned())
}))
}
fn with_server(
srv: Server,
) -> impl Filter<Extract = (Server,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || srv.clone())
}
async fn profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::get().and(
warp::path!("@" / String)
.and(Self::with_server(self.clone()))
.and_then(|username: String, srv: Server| async move {
match srv
.hb
.render(
"profile",
&serde_json::json!(srv.profiler.profile(username).await?),
)
.map(|html| warp::reply::html(html))
.map_err(|e| ServerError::from(e).reject_self())
{
Ok(resp) => Ok(resp),
Err(err) => Err(err),
}
}),
)
}
async fn create_profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::post().and(
warp::path!("@" / String)
.and(Self::with_server(self.clone()))
.and_then(|username: String, srv: Server| async move {
let user = srv
.profiler
.create_user(username, None)
.await
.map_err(|e| ServerError::from(e));
match user {
Ok(u) => Ok(warp::redirect(
Uri::from_str(format!("/@/{}", u.username).as_str()).unwrap(),
)),
Err(e) => Err(e.reject_self()),
}
}),
)
}
}

2
src/servek/mod.rs Normal file
View File

@ -0,0 +1,2 @@
mod html;
pub mod servek;

111
src/servek/servek.rs Normal file
View File

@ -0,0 +1,111 @@
use core::panic;
use std::{convert::Infallible, fmt::Display};
use handlebars::{Handlebars, RenderError};
use warp::{hyper::StatusCode, reject::Reject, Filter, Rejection, Reply};
use crate::{
model,
svc::profiles::{Profiler, UserError},
};
#[derive(Clone)]
pub struct Server {
pub(super) hb: Handlebars<'static>,
pub(super) profiler: Profiler,
}
impl Server {
pub fn new(profiler: Profiler) -> Self {
let mut hb = Handlebars::new();
hb.register_template_string("profile", include_str!("../../templates/html/profile.html"))
.expect("profile template");
Self { hb, profiler }
}
pub async fn listen_and_serve(self, port: u16) -> ! {
println!("starting server on port {}", port);
warp::serve(self.html().await.recover(Self::handle_rejection))
.run(([127, 0, 0, 1], port))
.await;
panic!("server stopped prematurely")
}
async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
let code;
let message;
if err.is_not_found() {
code = StatusCode::NOT_FOUND;
message = "not found";
} else if let Some(err) = err.find::<ServerError>() {
match err {
ServerError::Internal(err) => {
println!("internal server error: {}", err);
code = StatusCode::INTERNAL_SERVER_ERROR;
message = "internal server error";
}
ServerError::User(u) => match u {
UserError::Duplicate => {
code = StatusCode::BAD_REQUEST;
message = "duplicate entry";
}
UserError::Other(_) => {
panic!("FIXME: other case should already be handled in conversions")
}
UserError::NotFound => {
code = StatusCode::NOT_FOUND;
message = "not found";
}
},
}
} else {
// We should have expected this... Just log and say its a 500
println!("FIXME: unhandled rejection: {:?}", err);
code = StatusCode::INTERNAL_SERVER_ERROR;
message = "internal server error"
}
Ok(warp::reply::with_status(
warp::reply::json(&model::Error::error(message)),
code,
))
}
}
#[derive(Clone, Debug)]
pub(super) enum ServerError {
Internal(String),
User(UserError),
}
impl ServerError {
pub(super) fn reject_self(self) -> Rejection {
warp::reject::custom(self)
}
}
impl Reject for ServerError {}
impl From<RenderError> for ServerError {
fn from(r: RenderError) -> Self {
Self::Internal(r.to_string())
}
}
impl From<UserError> for ServerError {
fn from(u: UserError) -> Self {
match u {
UserError::Duplicate => Self::User(u),
UserError::NotFound => Self::User(u),
UserError::Other(o) => Self::Internal(format!("UserError: {}", o)),
}
}
}
impl Display for ServerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}

1
src/svc/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod profiles;

115
src/svc/profiles.rs Normal file
View File

@ -0,0 +1,115 @@
use serde::Serialize;
use warp::{reject::Reject, Rejection};
use crate::database::{
db,
users::{self, UserSelect, Users},
};
#[derive(Clone)]
pub struct Profiler {
db: Users,
}
impl Profiler {
pub fn new(db: Users) -> Self {
Self { db }
}
pub async fn profile(&self, username: String) -> Result<User, UserError> {
let select = if username.contains("@") {
UserSelect::FullUsername(username)
} else {
UserSelect::Username(username)
};
match self.db.user(select).await? {
Some(user) => Ok(User::from(user)),
None => Err(UserError::NotFound),
}
}
pub async fn create_user(
&self,
username: String,
display_name: Option<String>,
) -> Result<User, UserError> {
let result = self
.db
.create_user(
User {
id: String::new(),
username,
display_name,
host: None,
}
.into(),
)
.await?;
Ok(User::from(result))
}
}
#[derive(Clone, Debug, Serialize)]
pub struct User {
pub id: String,
pub username: String,
pub display_name: Option<String>,
pub host: Option<String>,
}
impl From<users::User> for User {
fn from(u: users::User) -> Self {
Self {
id: u.id,
username: u.username,
display_name: u.display_name,
host: u.host,
}
}
}
impl Into<users::User> for User {
fn into(self) -> users::User {
users::User {
id: self.id,
username: self.username,
display_name: self.display_name,
host: self.host,
}
}
}
#[derive(Debug, Clone)]
pub enum UserError {
Duplicate,
NotFound,
Other(String),
}
impl From<anyhow::Error> for UserError {
fn from(err: anyhow::Error) -> Self {
Self::Other(format!("UserError: {}", err))
}
}
impl From<db::DBError> for UserError {
fn from(err: db::DBError) -> Self {
match err {
db::DBError::Duplicate => Self::Duplicate,
db::DBError::Other(e) => Self::Other(e.to_string()),
}
}
}
impl ToString for UserError {
fn to_string(&self) -> String {
match self {
Self::Duplicate => String::from("duplicate insert"),
Self::NotFound => String::from("not found"),
Self::Other(err) => err.clone(),
}
}
}
impl Reject for UserError {}

4
static/style/main.css Normal file
View File

@ -0,0 +1,4 @@
body {
background-color: black;
color: rebeccapurple;
}

12
templates/html/404.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk - not found</title>
</head>
<body>
<h1>not found</h1>
</body>
</html>