diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d7414ff --- /dev/null +++ b/.vscode/launch.json @@ -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}" + // } + ] +} diff --git a/Cargo.lock b/Cargo.lock index ce104ea..979643b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index c64ee8d..3c0c0ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/migrate/1662887862_schema.up.sql b/migrate/1662887862_schema.up.sql new file mode 100644 index 0000000..db94c90 --- /dev/null +++ b/migrate/1662887862_schema.up.sql @@ -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; + diff --git a/src/database/db.rs b/src/database/db.rs new file mode 100644 index 0000000..069ebb0 --- /dev/null +++ b/src/database/db.rs @@ -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>, +} + +impl DB { + pub async fn new(host: String, user: String, database: String) -> Result { + 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 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) + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..295525e --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,2 @@ +pub mod db; +pub mod users; diff --git a/src/database/users.rs b/src/database/users.rs new file mode 100644 index 0000000..e2503ba --- /dev/null +++ b/src/database/users.rs @@ -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>); + +impl Users { + pub fn new(client: Arc>) -> 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 { + 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, 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, + pub display_name: Option, +} + +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), +} diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..d964050 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,20 @@ +use warp::hyper::StatusCode; + +pub(crate) fn html_with_status(body: String, status: StatusCode) -> warp::http::Response { + 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 { + html_with_status(body, StatusCode::OK) +} + +// pub(crate) fn html(body: String) -> Result, warp::http::Error> { +// warp::http::Response::builder() +// .header("Content-Type", "text/html; charset=utf-8") +// .status(StatusCode::OK) +// .body(body) +// } diff --git a/src/main.rs b/src/main.rs index 691f4a9..12d2166 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(()) } diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..ccda0fc --- /dev/null +++ b/src/model.rs @@ -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 } + } +} diff --git a/src/servek.rs b/src/servek.rs deleted file mode 100644 index cba2c7a..0000000 --- a/src/servek.rs +++ /dev/null @@ -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 { - 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 + Clone { - Self::index().or(self.profile()) - } - - fn index() -> impl Filter + 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 + Clone { - warp::any().map(move || srv.clone()) - } - - fn profile(&self) -> impl Filter + 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 { - if let Some(internal) = err.find::() { - 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 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, -} diff --git a/src/servek/html.rs b/src/servek/html.rs new file mode 100644 index 0000000..f923dc2 --- /dev/null +++ b/src/servek/html.rs @@ -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 + Clone { + Self::index() + .or(self.profile().await) + .or(self.create_profile().await) + } + + fn index() -> impl Filter + 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 + Clone { + warp::any().map(move || srv.clone()) + } + + async fn profile(&self) -> impl Filter + 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 + 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()), + } + }), + ) + } +} diff --git a/src/servek/mod.rs b/src/servek/mod.rs new file mode 100644 index 0000000..5f68cc2 --- /dev/null +++ b/src/servek/mod.rs @@ -0,0 +1,2 @@ +mod html; +pub mod servek; diff --git a/src/servek/servek.rs b/src/servek/servek.rs new file mode 100644 index 0000000..0197635 --- /dev/null +++ b/src/servek/servek.rs @@ -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 { + let code; + let message; + + if err.is_not_found() { + code = StatusCode::NOT_FOUND; + message = "not found"; + } else if let Some(err) = err.find::() { + 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 for ServerError { + fn from(r: RenderError) -> Self { + Self::Internal(r.to_string()) + } +} + +impl From 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) + } +} diff --git a/src/svc/mod.rs b/src/svc/mod.rs new file mode 100644 index 0000000..0a9c08b --- /dev/null +++ b/src/svc/mod.rs @@ -0,0 +1 @@ +pub mod profiles; diff --git a/src/svc/profiles.rs b/src/svc/profiles.rs new file mode 100644 index 0000000..42ead63 --- /dev/null +++ b/src/svc/profiles.rs @@ -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 { + 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, + ) -> Result { + 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, + pub host: Option, +} + +impl From 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 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 for UserError { + fn from(err: anyhow::Error) -> Self { + Self::Other(format!("UserError: {}", err)) + } +} + +impl From 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 {} diff --git a/static/style/main.css b/static/style/main.css new file mode 100644 index 0000000..4fb35ab --- /dev/null +++ b/static/style/main.css @@ -0,0 +1,4 @@ +body { + background-color: black; + color: rebeccapurple; +} diff --git a/templates/html/404.html b/templates/html/404.html new file mode 100644 index 0000000..d6901de --- /dev/null +++ b/templates/html/404.html @@ -0,0 +1,12 @@ + + + + + flabk - not found + + + +

not found

+ + +