Compare commits

..

No commits in common. "master" and "1d9802530d4b2255a05c7c348a6c2d212e53a672" have entirely different histories.

45 changed files with 902 additions and 3452 deletions

1129
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,20 @@
[workspace]
[package]
name = "flabk"
version = "0.0.1"
edition = "2021"
members = [
"flabk",
"flabk-derive",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.64"
base-62 = "0.1.1"
handlebars = "4.3.3"
mime_guess = "2.0.4"
rand = "0.8.5"
rust-embed = "6.4.0"
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" }
warp-embed = "0.4.0"

View File

@ -1,15 +0,0 @@
[package]
name = "flabk-derive"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = ["full"] }
[lib]
proc-macro = true

View File

@ -1,22 +0,0 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(LD)]
pub fn derive_ld(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input);
quote! {
impl crate::astreams::serde_ext::LDObject for #ident {
fn from_iri(s: &str) -> Self {
let mut ident = #ident::default();
ident.id = s.into();
ident
}
fn get_iri(&self) -> String {
self.id.clone()
}
}
}
.into()
}

View File

@ -1,34 +0,0 @@
[package]
name = "flabk"
version = "0.0.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.64"
argon2 = "0.4.1"
async-trait = "0.1.58"
axum = "0.5.17"
base-62 = "0.1.1"
handlebars = "4.3.3"
jsonwebtoken = "8.1.1"
mime_guess = "2.0.4"
rand = "0.8.5"
rand_core = { version = "0.6.3", features = ["std"] }
reqwest = { version = "0.11.12", features = [
"__tls",
"default-tls",
"hyper-tls",
"native-tls-crate",
"tokio-native-tls",
"serde_json",
"json",
] }
rust-embed = "6.4.0"
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"] }
tower-cookies = "0.7.0"
flabk-derive = { path = "../flabk-derive" }

View File

@ -1,148 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="130.52438mm"
height="130.52438mm"
viewBox="0 0 130.52438 130.52438"
version="1.1"
id="svg5"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
sodipodi:docname="flabk_icon_placeholder.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.54866744"
inkscape:cx="-78.371699"
inkscape:cy="260.63147"
inkscape:window-width="1914"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:current-layer="layer2" />
<defs
id="defs2">
<rect
x="238.94496"
y="545.79144"
width="291.11197"
height="295.74161"
id="rect1130" />
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Roughen"
id="filter1273"
x="0"
y="0"
width="1"
height="1">
<feTurbulence
type="fractalNoise"
numOctaves="5"
seed="145"
baseFrequency="0.001 10"
result="turbulence"
id="feTurbulence1269" />
<feDisplacementMap
in="SourceGraphic"
in2="turbulence"
scale="1.68439"
yChannelSelector="G"
xChannelSelector="R"
id="feDisplacementMap1271" />
</filter>
<rect
x="238.94496"
y="545.79144"
width="291.11197"
height="295.74161"
id="rect1130-3" />
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Roughen"
id="filter1273-6"
x="0"
y="0"
width="1"
height="1">
<feTurbulence
type="fractalNoise"
numOctaves="5"
seed="145"
baseFrequency="0.001 10"
result="turbulence"
id="feTurbulence1269-7" />
<feDisplacementMap
in="SourceGraphic"
in2="turbulence"
scale="1.68439"
yChannelSelector="G"
xChannelSelector="R"
id="feDisplacementMap1271-5" />
</filter>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(-38.320595,-110.90324)">
<circle
style="display:inline;fill:#663399;fill-opacity:1;stroke-width:0.264583"
id="path1074"
cx="103.58279"
cy="176.16544"
r="65.262192" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="translate(-38.320595,-110.90324)">
<circle
style="display:inline;fill:#0f0617;fill-opacity:1;stroke-width:0.224394;stroke-dasharray:none"
id="path1074-5"
cx="103.58279"
cy="176.16544"
r="55.262001" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
transform="translate(-38.320595,-110.90324)">
<g
id="g2829"
transform="matrix(0.88602282,0,0,0.87606946,13.533885,21.962211)">
<text
xml:space="preserve"
transform="matrix(2.0837961,0,0,2.645293,-442.06366,-1321.3309)"
id="text1128-3"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1130-3);display:inline;fill:#482464;fill-opacity:1;stroke:none;stroke-width:1.00008;stroke-dasharray:none;filter:url(#filter1273-6)"><tspan
x="238.94531"
y="581.18164"
id="tspan2868">fK</tspan></text>
<text
xml:space="preserve"
transform="matrix(2.0837961,0,0,2.645293,-438.16349,-1321.0342)"
id="text1128"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1130);fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter1273)"><tspan
x="238.94531"
y="581.18164"
id="tspan2870">fK</tspan></text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,281 +0,0 @@
use std::str::FromStr;
use serde::Deserialize;
use super::serde_ext::LDObject;
pub const CONTEXT_ID: &str = "https://www.w3.org/ns/activitystreams";
#[derive(Default, Debug, Clone, Deserialize)]
pub struct APContext {
pub id: Option<String>,
#[serde(rename = "@context")]
pub ctx: ContextMap,
}
impl LDObject for APContext {
fn from_iri(iri: &str) -> Self {
let mut ctx = Self::default();
ctx.id = Some(iri.into());
ctx
}
fn get_iri(&self) -> String {
self.id.clone().unwrap_or_default()
}
}
#[derive(Default, Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ContextMap {
#[serde(rename = "@vocab")]
pub vocab: String,
#[serde(rename = "xsd")]
pub xsd: String,
#[serde(rename = "as")]
pub as_field: String,
#[serde(rename = "ldp")]
pub ldp: String,
#[serde(rename = "vcard")]
pub vcard: String,
#[serde(rename = "id")]
pub id: String,
#[serde(rename = "type")]
pub type_field: String,
pub accept: String,
pub activity: String,
pub intransitive_activity: String,
pub add: String,
pub announce: String,
pub application: String,
pub arrive: String,
pub article: String,
pub audio: String,
pub block: String,
pub collection: String,
pub collection_page: String,
pub relationship: String,
pub create: String,
pub delete: String,
pub dislike: String,
pub document: String,
pub event: String,
pub follow: String,
pub flag: String,
pub group: String,
pub ignore: String,
pub image: String,
pub invite: String,
pub join: String,
pub leave: String,
pub like: String,
pub link: String,
pub mention: String,
pub note: String,
pub object: String,
pub offer: String,
pub ordered_collection: String,
pub ordered_collection_page: String,
pub organization: String,
pub page: String,
pub person: String,
pub place: String,
pub profile: String,
pub question: String,
pub reject: String,
pub remove: String,
pub service: String,
pub tentative_accept: String,
pub tentative_reject: String,
pub tombstone: String,
pub undo: String,
pub update: String,
pub video: String,
pub view: String,
pub listen: String,
pub read: String,
#[serde(rename = "Move")]
pub move_field: String,
pub travel: String,
pub is_following: String,
pub is_followed_by: String,
pub is_contact: String,
pub is_member: String,
#[serde(rename = "subject")]
pub subject: Option<TypedField>,
#[serde(rename = "relationship")]
pub relationship2: Option<TypedField>,
#[serde(rename = "actor")]
pub actor: Option<TypedField>,
#[serde(rename = "attributedTo")]
pub attributed_to: Option<TypedField>,
#[serde(rename = "attachment")]
pub attachment: Option<TypedField>,
#[serde(rename = "bcc")]
pub bcc: Option<TypedField>,
#[serde(rename = "bto")]
pub bto: Option<TypedField>,
#[serde(rename = "cc")]
pub cc: Option<TypedField>,
#[serde(rename = "context")]
pub context: Option<TypedField>,
#[serde(rename = "current")]
pub current: Option<TypedField>,
#[serde(rename = "first")]
pub first: Option<TypedField>,
#[serde(rename = "generator")]
pub generator: Option<TypedField>,
#[serde(rename = "icon")]
pub icon: Option<TypedField>,
#[serde(rename = "image")]
pub image2: Option<TypedField>,
#[serde(rename = "inReplyTo")]
pub in_reply_to: Option<TypedField>,
#[serde(rename = "items")]
pub items: Option<TypedField>,
#[serde(rename = "instrument")]
pub instrument: Option<TypedField>,
#[serde(rename = "orderedItems")]
pub ordered_items: Option<TypedField>,
#[serde(rename = "last")]
pub last: Option<TypedField>,
#[serde(rename = "location")]
pub location: Option<TypedField>,
#[serde(rename = "next")]
pub next: Option<TypedField>,
#[serde(rename = "object")]
pub object2: Option<TypedField>,
#[serde(rename = "oneOf")]
pub one_of: Option<TypedField>,
#[serde(rename = "anyOf")]
pub any_of: Option<TypedField>,
#[serde(rename = "closed")]
pub closed: Option<TypedField>,
#[serde(rename = "origin")]
pub origin: Option<TypedField>,
#[serde(rename = "accuracy")]
pub accuracy: Option<TypedField>,
#[serde(rename = "prev")]
pub prev: Option<TypedField>,
#[serde(rename = "preview")]
pub preview: Option<TypedField>,
#[serde(rename = "replies")]
pub replies: Option<TypedField>,
#[serde(rename = "result")]
pub result: Option<TypedField>,
#[serde(rename = "audience")]
pub audience: Option<TypedField>,
#[serde(rename = "partOf")]
pub part_of: Option<TypedField>,
#[serde(rename = "tag")]
pub tag: Option<TypedField>,
#[serde(rename = "target")]
pub target: Option<TypedField>,
#[serde(rename = "to")]
pub to: Option<TypedField>,
#[serde(rename = "url")]
pub url: Option<TypedField>,
#[serde(rename = "altitude")]
pub altitude: Option<TypedField>,
#[serde(rename = "content")]
pub content: String,
#[serde(rename = "contentMap")]
pub content_map: Option<TypedField>,
#[serde(rename = "name")]
pub name: String,
#[serde(rename = "nameMap")]
pub name_map: Option<TypedField>,
#[serde(rename = "duration")]
pub duration: Option<TypedField>,
#[serde(rename = "endTime")]
pub end_time: Option<TypedField>,
#[serde(rename = "height")]
pub height: Option<TypedField>,
#[serde(rename = "href")]
pub href: Option<TypedField>,
#[serde(rename = "hreflang")]
pub hreflang: String,
#[serde(rename = "latitude")]
pub latitude: Option<TypedField>,
#[serde(rename = "longitude")]
pub longitude: Option<TypedField>,
#[serde(rename = "mediaType")]
pub media_type: String,
#[serde(rename = "published")]
pub published: Option<TypedField>,
#[serde(rename = "radius")]
pub radius: Option<TypedField>,
#[serde(rename = "rel")]
pub rel: String,
#[serde(rename = "startIndex")]
pub start_index: Option<TypedField>,
#[serde(rename = "startTime")]
pub start_time: Option<TypedField>,
#[serde(rename = "summary")]
pub summary: String,
#[serde(rename = "summaryMap")]
pub summary_map: Option<TypedField>,
#[serde(rename = "totalItems")]
pub total_items: Option<TypedField>,
#[serde(rename = "units")]
pub units: String,
#[serde(rename = "updated")]
pub updated: Option<TypedField>,
#[serde(rename = "width")]
pub width: Option<TypedField>,
#[serde(rename = "describes")]
pub describes: Option<TypedField>,
#[serde(rename = "formerType")]
pub former_type: Option<TypedField>,
#[serde(rename = "deleted")]
pub deleted: Option<TypedField>,
#[serde(rename = "inbox")]
pub inbox: Option<TypedField>,
#[serde(rename = "outbox")]
pub outbox: Option<TypedField>,
#[serde(rename = "following")]
pub following: Option<TypedField>,
#[serde(rename = "followers")]
pub followers: Option<TypedField>,
#[serde(rename = "streams")]
pub streams: Option<TypedField>,
#[serde(rename = "preferredUsername")]
pub preferred_username: String,
#[serde(rename = "endpoints")]
pub endpoints: Option<TypedField>,
#[serde(rename = "uploadMedia")]
pub upload_media: Option<TypedField>,
#[serde(rename = "proxyUrl")]
pub proxy_url: Option<TypedField>,
#[serde(rename = "liked")]
pub liked: Option<TypedField>,
#[serde(rename = "oauthAuthorizationEndpoint")]
pub oauth_authorization_endpoint: Option<TypedField>,
#[serde(rename = "oauthTokenEndpoint")]
pub oauth_token_endpoint: Option<TypedField>,
#[serde(rename = "provideClientKey")]
pub provide_client_key: Option<TypedField>,
#[serde(rename = "signClientKey")]
pub sign_client_key: Option<TypedField>,
#[serde(rename = "sharedInbox")]
pub shared_inbox: Option<TypedField>,
pub public: Option<TypedField>,
#[serde(rename = "source")]
pub source: String,
#[serde(rename = "likes")]
pub likes: Option<TypedField>,
#[serde(rename = "shares")]
pub shares: Option<TypedField>,
#[serde(rename = "alsoKnownAs")]
pub also_known_as: Option<TypedField>,
}
#[derive(Default, Debug, Clone, Deserialize)]
pub struct TypedField {
#[serde(rename = "@id")]
pub id: String,
#[serde(rename = "@type")]
pub kind: Option<String>,
#[serde(rename = "@container")]
pub container: Option<String>,
}

View File

@ -1,316 +0,0 @@
use std::collections::HashMap;
use flabk_derive::LD;
use serde::{Deserialize, Serialize};
use crate::astreams::resolve::Resolvable;
use self::{
resolve::ResolveError,
serde_ext::{expand_partial, expand_partial_into_vec, into_vec},
};
pub mod context;
pub mod resolve;
mod serde_ext;
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub enum ObjectKind {
Article,
Audio,
Document,
Event,
Image,
Note,
Page,
Place,
Profile,
Relationship,
Tombstone,
Video,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub enum ActivityKind {
Accept,
Add,
Announce,
Arrive,
Block,
Create,
Delete,
Dislike,
Flag,
Follow,
Invite,
Join,
Leave,
Like,
Listen,
Move,
Offer,
Question,
Reject,
Read,
Remove,
TentativeReject,
TentativeAccept,
Travel,
Undo,
Update,
View,
}
#[derive(Debug, Clone)]
pub enum ExpandError {
InvalidKind(Option<ActivityKind>),
NoAttribution,
ResolveIRI(String),
Other(String),
}
impl From<ResolveError> for ExpandError {
fn from(err: ResolveError) -> Self {
Self::ResolveIRI(err.0)
}
}
/// A Collection is a subtype of Object that
/// represents ordered or unordered sets of
/// Object or Link instances.
#[derive(Debug, Clone, Deserialize)]
pub struct Collection<T> {
pub summary: Option<String>,
#[serde(rename = "type")]
pub kind: String,
#[serde(rename = "totalItems")]
pub total_items: Vec<T>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Attachment {
#[serde(rename = "type")]
pub kind: String,
pub content: Option<String>,
pub url: String,
}
// TODO: maybe this can work as attachment w/ the ID being more like
// the LD IDs?
#[derive(Debug, Clone, Deserialize)]
pub struct Image {
#[serde(flatten)]
pub base: ObjectBase,
#[serde(rename = "type")]
pub kind: Option<ObjectKind>,
pub id: String,
#[serde(deserialize_with = "into_vec")]
pub url: Vec<Link>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Audience {
#[serde(rename = "type")]
pub kind: String,
pub name: String,
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub enum Unit {
#[serde(rename = "m")]
Meters,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Location {
#[serde(rename = "type")]
pub kind: String,
pub name: String,
pub longitude: Option<f64>,
pub latitude: Option<f64>,
pub altitude: Option<f64>,
pub units: Unit,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Preview {
#[serde(rename = "type")]
pub kind: String,
pub name: String,
pub duration: Option<String>,
pub url: Link,
}
#[derive(Debug, Clone, Deserialize)]
pub struct APResult {
#[serde(rename = "type")]
pub kind: String,
pub name: String,
}
/// ObjectBase contains base Activity Streams
/// members that all objects should have. Except kind.
///
/// This object is intended to be used by inlining it into
/// other serializable/deserializable objects.
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ObjectBase {
// #[serde(rename = "@context")]
// #[serde(deserialize_with = "expand_partial")]
// pub ap_context: APContext,
pub name: Option<String>,
pub name_map: Option<HashMap<String, String>>,
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
pub attachment: Vec<Attachment>,
#[serde(deserialize_with = "expand_partial_into_vec")]
pub attributed_to: Vec<Actor>,
pub audience: Option<Audience>,
pub media_type: Option<String>,
pub content: Option<String>,
pub content_map: Option<HashMap<String, String>>,
pub context: Option<String>,
pub start_time: Option<String>,
pub end_time: Option<String>,
pub generator: Option<Actor>,
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
pub icon: Vec<Link>,
// omit in_reply_to
pub location: Option<Location>,
pub preview: Option<Preview>,
pub published: Option<String>,
pub updated: Option<String>,
pub replies: Option<Collection<()>>, // TODO: type
pub summary: Option<String>,
pub summary_map: Option<HashMap<String, String>>,
#[serde(deserialize_with = "expand_partial_into_vec", default = "Vec::new")]
pub tag: Vec<Actor>,
// omit url for now: need to merge into_vec & expand_partial
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
pub to: Vec<String>,
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
pub bto: Vec<String>,
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
pub cc: Vec<String>,
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
pub bcc: Vec<String>,
pub duration: Option<String>,
}
#[derive(Debug, Clone, Deserialize, LD, Default)]
pub struct Object {
#[serde(flatten)]
pub base: ObjectBase,
#[serde(rename = "type")]
pub kind: Option<ObjectKind>,
pub id: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Link {
#[serde(rename = "type")]
pub kind: Option<ActivityKind>,
pub name: Option<String>,
pub href: String,
#[serde(rename = "hreflang")]
pub href_lang: Option<String>,
#[serde(rename = "mediaType")]
pub media_type: Option<String>,
pub height: u32,
pub width: u32,
}
/// An Activity is a subtype of Object that describes
/// some form of action that may happen,
/// is currently happening, or has already happened.
///
/// The Activity type itself serves as an abstract base type
/// for all types of activities.
/// It is important to note that the Activity type itself
/// does not carry any specific semantics about the kind of action being taken.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Activity {
#[serde(flatten)]
pub base: ObjectBase,
#[serde(rename = "type")]
pub kind: Option<ActivityKind>,
pub id: String,
#[serde(deserialize_with = "expand_partial")]
pub actor: Option<Actor>,
#[serde(deserialize_with = "expand_partial")]
pub object: Option<Object>,
pub result: Option<APResult>,
pub target: Option<Basic>,
pub origin: Option<Basic>,
pub instrument: Option<Basic>,
}
/// Instances of IntransitiveActivity are a subtype of Activity
/// representing intransitive actions.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IntransitiveActivity {
#[serde(flatten)]
pub base: ObjectBase,
#[serde(rename = "type")]
pub kind: Option<ActivityKind>,
pub id: String,
#[serde(deserialize_with = "expand_partial")]
pub actor: Option<Actor>,
pub result: Option<APResult>,
pub target: Option<Basic>,
pub origin: Option<Basic>,
pub instrument: Option<Basic>,
}
pub enum ActorType {
Application,
Group,
Organization,
Person,
Service,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Basic {
#[serde(rename = "type")]
pub kind: Option<ActivityKind>,
pub name: Option<String>,
}
#[derive(Debug, Clone, Deserialize, LD, Default)]
pub struct Actor {
pub id: String,
#[serde(rename = "type")]
pub kind: Option<ActivityKind>,
#[serde(rename = "attributedTo")]
pub attributed_to: Option<String>,
pub published: Option<String>,
pub content: Option<String>,
pub context: Option<String>,
pub conversation: Option<String>,
pub url: Option<String>,
#[serde(rename = "to")]
#[serde(deserialize_with = "into_vec")]
pub to_uris: Vec<String>,
}
pub async fn test() {
let obj = r#"{
"@context": "https://www.w3.org/ns/activitystreams",
"attributedTo": "http://localhost:3001/u/test",
"content": "honk donk",
"context": "data:,electrichonkytonk-2jqQ42HyJXctnBKTy1",
"conversation": "data:,electrichonkytonk-2jqQ42HyJXctnBKTy1",
"id": "htts://localhost:3001/u/test/h/6Q8BFF8W6PZT2ddngZ",
"published": "2022-09-30T19:04:45Z",
"summary": "",
"to": "https://www.w3.org/ns/activitystreams#Public",
"type": "Note",
"url": "https://localhost/u/test/h/6Q8BFF8W6PZT2ddngZ"
}"#;
let obj = serde_json::from_str::<Object>(obj).unwrap();
println!();
println!("{:#?}", obj);
}

View File

@ -1,86 +0,0 @@
use std::{future::Future, string::FromUtf8Error};
use async_trait::async_trait;
use reqwest::{Method, StatusCode};
use serde::Deserialize;
use super::serde_ext::LDObject;
const LD_CONTENT_TYPE: &str = "application/ld+json";
#[async_trait]
pub(super) trait Resolvable: Sized {
async fn resolve(&self, resolver: &Resolver) -> Result<Self, ResolveError>;
}
#[async_trait]
impl<T> Resolvable for T
where
T: LDObject + for<'de> Deserialize<'de> + Sync,
{
async fn resolve(&self, resolver: &Resolver) -> Result<Self, ResolveError> {
resolver.resolve_into(self.get_iri()).await
}
}
#[derive(Debug)]
pub(crate) struct ResolveError(pub String);
impl From<reqwest::Error> for ResolveError {
fn from(err: reqwest::Error) -> Self {
Self(err.to_string())
}
}
impl From<FromUtf8Error> for ResolveError {
fn from(e: FromUtf8Error) -> Self {
Self(format!(
"invalid schema format (tried utf8): {}",
e.to_string()
))
}
}
pub(super) struct Resolver {
client: reqwest::Client,
}
impl Resolver {
pub fn new() -> Self {
Self {
client: reqwest::ClientBuilder::new().build().unwrap(),
}
}
async fn get<Out, F, Fut>(&self, iri: String, ok: F) -> Result<Out, ResolveError>
where
F: FnOnce(reqwest::Response) -> Fut,
Fut: Future<Output = Result<Out, ResolveError>>,
{
let resp = self
.client
.request(Method::GET, iri)
.header("Accept", LD_CONTENT_TYPE)
.send()
.await?;
match resp.status() {
StatusCode::OK => Ok(ok(resp).await?),
status => Err(ResolveError(format!("non-ok status: {}", status))),
}
}
pub async fn resolve_into<'a, T>(&self, iri: String) -> Result<T, ResolveError>
where
T: for<'de> serde::Deserialize<'de>,
{
Ok(self
.get(iri, |resp| async { Ok(resp.json().await?) })
.await?)
}
pub async fn resolve(&self, iri: String) -> Result<Vec<u8>, ResolveError> {
Ok(self
.get(iri, |resp| async { Ok(resp.bytes().await?.to_vec()) })
.await?)
}
}

View File

@ -1,199 +0,0 @@
use std::{fmt, marker::PhantomData};
use serde::{
de::{self, MapAccess, Visitor},
Deserialize, Deserializer,
};
pub trait LDObject {
fn from_iri(iri: &str) -> Self;
fn get_iri(&self) -> String;
}
impl<T> LDObject for Option<T>
where
T: LDObject,
{
fn from_iri(iri: &str) -> Self {
todo!()
}
fn get_iri(&self) -> String {
todo!()
}
}
// Allows a value that's a string to be expanded into an object (move string into id property via From<IRI>)
// AND the serialization of that object itself
pub fn expand_partial<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Deserialize<'de> + LDObject,
D: Deserializer<'de>,
{
struct BaseExpander<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for BaseExpander<T>
where
T: Deserialize<'de> + LDObject,
{
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<T, E>
where
E: de::Error,
{
Ok(LDObject::from_iri(value))
}
fn visit_map<M>(self, map: M) -> Result<T, M::Error>
where
M: MapAccess<'de>,
{
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
}
}
deserializer.deserialize_any(BaseExpander(PhantomData))
}
pub(super) fn expand_partial_into_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
where
T: Deserialize<'de> + LDObject,
D: Deserializer<'de>,
{
struct BaseExpander<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for BaseExpander<T>
where
T: Deserialize<'de> + LDObject,
{
type Value = Vec<T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![LDObject::from_iri(value)])
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(|v| vec![v])
}
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
where
S: serde::de::SeqAccess<'de>,
{
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(visitor))
}
}
deserializer.deserialize_any(BaseExpander(PhantomData))
}
// Allows deserialization of a single item into a vector of that item
// As long as they implement the From<String> trait
pub(super) fn into_vec<'de, D, Out>(deserializer: D) -> Result<Vec<Out>, D::Error>
where
D: Deserializer<'de>,
Out: serde::Deserialize<'de>,
{
struct VecVisitor<Out>(PhantomData<Vec<Out>>);
impl<'de, Out> Visitor<'de> for VecVisitor<Out>
where
Out: serde::Deserialize<'de>,
{
type Value = Vec<Out>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let name = std::any::type_name::<Out>();
formatter.write_str(format!("{} or Vec<{}>", name, name).as_str())
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Deserialize::deserialize(de::value::StrDeserializer::new(value)).map(|v| vec![v])
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(|v| vec![v])
}
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
where
S: serde::de::SeqAccess<'de>,
{
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(visitor))
}
}
deserializer.deserialize_any(VecVisitor(PhantomData))
}
#[cfg(test)]
mod tests {
use flabk_derive::LD;
use serde::Deserialize;
use super::expand_partial;
#[test]
fn expand_partial_populates_iri_from_string() {
#[derive(Deserialize, LD, Default)]
struct Context {
pub id: String,
pub context: bool,
}
#[derive(Deserialize)]
struct WithContext {
#[serde(deserialize_with = "expand_partial")]
pub context: Context,
}
const JSONLD_INPUT: &str = r#"{"context": "https://www.w3.org/ns/activitystreams"}"#;
let result =
serde_json::from_str::<WithContext>(JSONLD_INPUT).expect("deserializing with expand");
assert!(result.context.id == "https://www.w3.org/ns/activitystreams");
assert!(result.context.context == false);
}
#[test]
fn expand_partial_expands_into_object_fully() {
#[derive(Deserialize, LD, Default)]
struct Expanded {
pub id: String,
pub truth: bool,
}
#[derive(Deserialize)]
struct Expandable {
#[serde(deserialize_with = "expand_partial")]
pub expansive: Expanded,
}
const JSONLD_INPUT: &str = r#"{"expansive": { "id": "1", "truth": true }}"#;
let result =
serde_json::from_str::<Expandable>(JSONLD_INPUT).expect("deserializing with expand");
assert!(result.expansive.id == "1");
assert!(result.expansive.truth);
}
}

View File

@ -1,39 +0,0 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_postgres::Client;
use super::db::DBError;
#[derive(Clone)]
pub struct Keys(Arc<Mutex<Client>>);
impl Keys {
pub fn new(client: Arc<Mutex<Client>>) -> Self {
Self(client)
}
pub async fn get_key(&self, key: &str) -> Result<String, DBError> {
Ok(self
.0
.lock()
.await
.query("select value from keys where key = $1", &[&key])
.await?
.first()
.ok_or_else(|| DBError::NotFound)?
.get("value"))
}
pub async fn set_key(&self, key: &str, value: &str) -> Result<(), DBError> {
self.0
.lock()
.await
.execute(
"insert into keys (key, value) values ($1, $2)",
&[&key, &value],
)
.await?;
Ok(())
}
}

View File

@ -1,147 +0,0 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_postgres::{Client, Row};
use crate::sec;
use super::db;
#[derive(Clone)]
pub struct Users(Arc<Mutex<Client>>);
impl Users {
pub fn new(client: Arc<Mutex<Client>>) -> Self {
Self(client)
}
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, password_hash, email) values ($1, $2, $3, $4, $5, $6) returning (id, username, host, display_name, password_hash, email, avatar_uri, bio)",
&[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash, &u.email],
).await?;
Ok(User::from(&row))
}
pub async fn user_stats(&self, by: UserSelect) -> Result<UserStats, db::DBError> {
let (clause, param) = by.into();
let rows = self
.0
.lock()
.await
.query(
format!(r#"select count(follows.*) as following, count(followed.*) as followers from users
left join follows on follows.user_id = users.id
left join follows followed on followed.follows_id = users.id
where {}"#, clause).as_str(),
&[&param],
)
.await?;
Ok(rows.first().ok_or(db::DBError::NotFound)?.into())
}
pub async fn user(&self, by: UserSelect) -> Result<User, db::DBError> {
let (clause, param) = by.into();
let rows = self
.0
.lock()
.await
.query(
format!(
"select id, username, host, display_name, password_hash, email, avatar_uri, bio from users where {}",
clause,
)
.as_str(),
&[&param],
)
.await?;
if let Some(row) = rows.first() && rows.len() == 1 {
Ok(User::from(row))
} else {
Err(db::DBError::NotFound)
}
}
}
#[derive(Debug, Clone)]
pub struct User {
pub id: String,
pub username: String,
pub host: Option<String>,
pub display_name: Option<String>,
pub password_hash: String,
pub email: String,
pub avatar_uri: Option<String>,
pub bio: 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"),
password_hash: row.get("password_hash"),
email: row.get("email"),
avatar_uri: row.get("avatar_uri"),
bio: row.get("bio"),
}
}
}
pub enum UserSelect {
ID(String),
Username(String),
FullUsername(String),
}
impl From<String> for UserSelect {
fn from(username: String) -> Self {
if !username.contains("@") {
Self::Username(username)
} else {
Self::FullUsername(username)
}
}
}
impl Into<(String, String)> for UserSelect {
fn into(self) -> (String, String) {
let where_param: String;
let where_clause = match self {
UserSelect::ID(id) => {
where_param = id;
"users.id = $1"
}
UserSelect::Username(username) => {
where_param = username;
"users.username = $1"
}
UserSelect::FullUsername(full) => {
where_param = full;
"(users.username || '@' || users.host) = $1"
}
};
(where_clause.to_owned(), where_param)
}
}
#[derive(Debug, Clone)]
pub struct UserStats {
pub post_count: i64,
pub following: i64,
pub followers: i64,
}
impl From<&Row> for UserStats {
fn from(row: &Row) -> Self {
Self {
post_count: 100,
following: row.get("following"),
followers: row.get("followers"),
}
}
}

View File

@ -1,26 +0,0 @@
#![feature(const_type_name)]
#![feature(let_chains)]
mod astreams;
mod database;
mod sec;
mod servek;
mod svc;
use database::db::DB;
use servek::servek::Server;
use svc::{auth::Auth, profiles::Profiler};
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
astreams::test().await;
let db = DB::new(
"localhost".to_owned(),
"flabk".to_owned(),
"flabk".to_owned(),
)
.await?;
let profiler = Profiler::new(db.users());
let auth = Auth::new(db.keys(), db.users()).await;
Server::new(profiler, auth).listen_and_serve(8008).await;
Ok(())
}

View File

@ -1,26 +0,0 @@
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use rand::Rng;
pub fn hash(password: String) -> String {
let password = password.as_bytes();
// Hash password to PHC string ($argon2id$v=19$...)
Argon2::default()
.hash_password(password, &SaltString::generate(&mut OsRng))
.unwrap()
.to_string()
}
pub fn compare(password: &str, password_hash: &str) -> bool {
let hash = PasswordHash::new(&password_hash).unwrap();
Argon2::default()
.verify_password(password.as_bytes(), &hash)
.is_ok()
}
pub fn new_id() -> String {
let bytes = rand::thread_rng().gen::<[u8; 16]>();
base_62::encode(&bytes)
}

View File

@ -1,332 +0,0 @@
use std;
use axum::{
body::Full,
extract::Path,
http::{header, HeaderValue, StatusCode},
response::{self, IntoResponse, Response},
routing, Extension, Form, Router,
};
use mime_guess::mime;
use tower_cookies::{Cookie, Cookies};
use crate::svc::auth::{AuthError, Claims};
use super::{
servek::{Server, ServerError},
CreateProfileRequest, LoginRequest, NavType, Note, Notification, Profile, Redirect, Timeline,
WithNav,
};
use rust_embed::RustEmbed;
const AUTH_COOKIE_NAME: &str = "flabk_token";
#[derive(RustEmbed)]
#[folder = "static"]
struct StaticData;
impl Server {
pub(super) fn register_html(&self, router: &Router) -> Router {
router
.clone()
.route("/favicon.svg", routing::get(Self::favicon))
.route("/", routing::get(Self::index))
.route("/login", routing::get(Self::login_page).post(Self::login))
.route("/logout", routing::get(Self::logout))
.route(
"/signup",
routing::get(Self::signup_page).post(Self::create_user),
)
.route("/@/:username", routing::get(Self::profile))
.route("/static/*file", routing::get(Self::static_handler))
.fallback(routing::get(Self::handler_404))
}
fn get_auth(&self, cookies: Cookies) -> Result<Claims, ServerError> {
match cookies.get(AUTH_COOKIE_NAME) {
Some(cookie) => self
.auth
.get_claims(cookie.value().to_owned())
.map_err(|e| {
if e.expired() {
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
}
e.into()
}),
None => Err(ServerError::NotLoggedIn),
}
}
fn from_cookies(&self, cookies: Cookies) -> Result<WithNav<Option<Claims>>, ServerError> {
const LOGGED_OUT: Result<WithNav<Option<Claims>>, ServerError> = Ok(WithNav {
obj: None,
nav_type: NavType::LoggedOut,
});
if let Some(cookie) = cookies.get(AUTH_COOKIE_NAME) {
let claims = match self.auth.get_claims(cookie.value().to_owned()) {
Ok(claims) => claims,
Err(e) => {
if e.clone().expired() {
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
return LOGGED_OUT;
} else {
return Err(e.into());
}
}
};
Ok(WithNav::new(Some(claims.clone()), claims.into()))
} else {
LOGGED_OUT
}
}
async fn index(
Extension(srv): Extension<Server>,
cookies: Cookies,
) -> Result<impl IntoResponse, ServerError> {
// TODO: show actual posts, these are hardcoded currently to check
// out how they look
let profile1 = Box::new(Profile {
username: "@emilis@puff.place".to_string(),
display_name: "dusty rusty".to_string(),
avatar_url: "https://puff.place/files/thumbnail-501ec5ba-37d6-4163-8e8d-3478a689660d"
.to_string(),
});
let profile2 = Box::new(Profile::new("@emilk@another.place".to_string()));
let note1 = Note::new(profile1.clone(), Some("hey there goods".to_string()));
let note2 = Note::new(
profile2,
Some("@emilis@puff.place u dont got shit".to_string()),
);
let note3 = Note::new(profile1, Some("bithces :".to_string()));
match srv.get_auth(cookies) {
Ok(claims) => Ok((
StatusCode::OK,
response::Html(srv.hb.render(
"index",
&WithNav::new(
Timeline {
show_postbox: true,
notes: vec![note1, note2, note3],
},
NavType::LoggedIn,
),
)?),
)
.into_response()),
Err(e) => match e {
ServerError::NotLoggedIn => Ok((
StatusCode::OK,
response::Html(
srv.hb
.render("index", &WithNav::new((), NavType::LoggedOut))?,
),
)
.into_response()),
_ => Err(e),
},
}
}
async fn favicon() -> impl IntoResponse {
(
StatusCode::OK,
(
[(
header::CONTENT_TYPE,
HeaderValue::from_static(mime::IMAGE_SVG.as_ref()),
)],
axum::body::boxed(Full::from(
include_bytes!("../../flabk_icon_placeholder.svg").as_ref(),
)),
),
)
}
async fn handler_404(
Extension(srv): Extension<Server>,
cookies: Cookies,
) -> Result<impl IntoResponse, ServerError> {
Ok((
StatusCode::NOT_FOUND,
response::Html(srv.hb.render("err404", &srv.from_cookies(cookies)?)?),
))
}
async fn logout(
Extension(srv): Extension<Server>,
cookies: Cookies,
) -> Result<impl IntoResponse, ServerError> {
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
Ok((
StatusCode::OK,
response::Html(srv.hb.render(
"redirect",
&Redirect {
location: "/".to_owned(),
},
)?),
))
}
fn login_page_with(
&self,
tag_name: String,
message: String,
) -> Result<response::Html<String>, ServerError> {
Ok(self
.hb
.render(
"login",
&serde_json::json!(Notification { message, tag_name }),
)
.map(|html| response::Html(html))?)
}
async fn login_page(
Extension(srv): Extension<Server>,
) -> Result<impl IntoResponse, ServerError> {
Ok((
StatusCode::OK,
response::Html(srv.hb.render("login", &serde_json::json!(()))?),
))
}
async fn login(
cookies: Cookies,
Extension(srv): Extension<Server>,
Form(login): Form<LoginRequest>,
) -> Result<impl IntoResponse, ServerError> {
if login.username == "" || login.password == "" {
return Ok((
StatusCode::BAD_REQUEST,
srv.login_page_with(
"error-partial".to_owned(),
"credentials required".to_owned(),
)?,
));
}
let token = match srv.auth.login(login.username, login.password).await {
Ok(token) => token,
Err(e) => match e {
AuthError::InvalidCredentials => {
return Ok((
StatusCode::UNAUTHORIZED,
srv.login_page_with(
"error-partial".to_owned(),
"invalid credentials".to_owned(),
)?,
))
}
e => return Err(e.into()),
},
};
if cookies.get(AUTH_COOKIE_NAME).is_some() {
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
}
cookies.add(Cookie::new(AUTH_COOKIE_NAME, token));
Ok((
StatusCode::OK,
response::Html(srv.hb.render(
"redirect",
&Redirect {
location: "/".to_owned(),
},
)?),
))
}
async fn profile(
cookies: Cookies,
Extension(srv): Extension<Server>,
Path(username): Path<String>,
) -> Result<impl IntoResponse, ServerError> {
Ok((
StatusCode::OK,
response::Html(srv.hb.render(
"profile",
&WithNav::new(
srv.profiler.profile(username).await?,
srv.from_cookies(cookies)?.nav_type,
),
)?),
))
}
async fn create_user(
Extension(srv): Extension<Server>,
Form(body): Form<CreateProfileRequest>,
) -> Result<impl IntoResponse, ServerError> {
if body.username == "" {
return srv.signup_page_with("error-partial".to_owned(), "empty username".to_owned());
}
if body.password == "" {
return srv.signup_page_with("error-partial".to_owned(), "empty password".to_owned());
}
if body.email == "" {
return srv.signup_page_with("error-partial".to_owned(), "empty email".to_owned());
}
srv.profiler
.create_user(body.username, body.password, body.email)
.await?;
srv.signup_page_with("success-partial".to_owned(), "signup successful".to_owned())
}
async fn signup_page(
Extension(srv): Extension<Server>,
) -> Result<impl IntoResponse, ServerError> {
Ok((
StatusCode::OK,
response::Html(srv.hb.render("signup", &serde_json::json!(()))?),
))
}
fn signup_page_with(
&self,
tag_name: String,
message: String,
) -> Result<impl IntoResponse, ServerError> {
Ok((
StatusCode::OK,
response::Html(self.hb.render(
"signup",
&serde_json::json!(Notification { message, tag_name }),
)?),
))
}
async fn static_handler(Path(mut path): Path<String>) -> impl IntoResponse {
path.remove(0);
println!("getting path: {}", path);
StaticFile(path)
}
}
pub struct StaticFile<T>(pub T);
impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
fn into_response(self) -> axum::response::Response {
let path = self.0.into();
match StaticData::get(path.as_str()) {
Some(content) => {
let body = axum::body::boxed(Full::from(content.data));
let mime = mime_guess::from_path(path).first_or_octet_stream();
Response::builder()
.header(header::CONTENT_TYPE, mime.as_ref())
.body(body)
.unwrap()
}
None => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(axum::body::boxed(Full::from("404")))
.unwrap(),
}
}
}

View File

@ -1,126 +0,0 @@
use axum::response::{Html, IntoResponse};
use serde::{Deserialize, Serialize};
use tower_cookies::{Cookie, Cookies};
use crate::svc::auth::{AuthError, Claims};
mod html;
pub mod servek;
#[derive(Debug, Clone, Serialize)]
struct Timeline {
pub show_postbox: bool,
pub notes: Vec<Note>,
}
#[derive(Debug, Clone, Serialize)]
struct Note {
pub profile: Box<Profile>,
pub content: Option<String>,
}
impl Note {
fn new(profile: Box<Profile>, content: Option<String>) -> Self {
Self { profile, content }
}
}
#[derive(Debug, Clone, Serialize)]
struct Profile {
pub display_name: String,
pub username: String,
pub avatar_url: String,
}
impl Profile {
fn new(username: String) -> Self {
let display_name = username
.strip_prefix('@')
.unwrap()
.split_once('@')
.unwrap()
.0
.to_string();
Profile {
display_name,
username,
avatar_url: "/favicon.svg".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize)]
struct Notification {
pub message: String,
pub tag_name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CreateProfileRequest {
pub username: String,
pub password: String,
pub email: String,
}
// pub enum HtmlOrRedirect {
// Html(Html<String>),
// Redirect(()),
// }
#[derive(Debug, Clone, Serialize)]
pub struct Redirect {
pub location: String,
}
#[derive(Debug, Clone, Serialize)]
pub enum NavType {
LoggedIn,
LoggedOut,
}
impl From<Result<Claims, AuthError>> for NavType {
fn from(claims: Result<Claims, AuthError>) -> Self {
if claims.map(|c| c.expired()).unwrap_or(true) {
NavType::LoggedOut
} else {
NavType::LoggedIn
}
}
}
impl From<Claims> for NavType {
fn from(c: Claims) -> Self {
if c.expired() {
NavType::LoggedOut
} else {
NavType::LoggedIn
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct WithNav<T> {
pub nav_type: NavType,
pub obj: T,
}
impl<T> WithNav<T> {
pub fn new(obj: T, nav_type: NavType) -> WithNav<T> {
WithNav { nav_type, obj }
}
}
impl<T> Into<serde_json::Value> for WithNav<T>
where
T: Serialize,
{
fn into(self) -> serde_json::Value {
serde_json::json!(self)
}
}

View File

@ -1,133 +0,0 @@
use core::panic;
use std::{fmt::Display, net::SocketAddr};
use axum::{
http::StatusCode,
response::{self, IntoResponse},
Extension, Router,
};
use handlebars::{Handlebars, RenderError};
use tower_cookies::CookieManagerLayer;
use crate::svc::{
auth::{Auth, AuthError},
profiles::{Profiler, UserError},
};
#[derive(Clone)]
pub struct Server {
pub(super) hb: Handlebars<'static>,
pub(super) profiler: Profiler,
pub(super) auth: Auth,
}
impl Server {
pub fn new(profiler: Profiler, auth: Auth) -> Self {
let mut hb = Handlebars::new();
hb.register_template_string("profile", include_str!("../../templates/html/profile.html"))
.expect("profile template");
hb.register_template_string("login", include_str!("../../templates/html/login.html"))
.expect("login template");
hb.register_template_string("signup", include_str!("../../templates/html/signup.html"))
.expect("login template");
hb.register_template_string(
"redirect",
include_str!("../../templates/html/html-redirect.html"),
)
.expect("redirect template");
hb.register_template_string("error-partial", r#"<h2 class="error">{{message}}</h2>"#)
.expect("error-partial");
hb.register_template_string("success-partial", r#"<h2 class="success">{{message}}</h2>"#)
.expect("success-partial");
hb.register_template_string("index", include_str!("../../templates/html/index.html"))
.expect("index");
hb.register_template_string("err404", include_str!("../../templates/html/404.html"))
.expect("err404");
hb.register_partial(
"LoggedOut",
include_str!("../../templates/html/nav-loggedout.html"),
)
.expect("LoggedOut");
hb.register_partial(
"LoggedIn",
include_str!("../../templates/html/nav-loggedin.html"),
)
.expect("LoggedIn");
Self { hb, profiler, auth }
}
pub async fn listen_and_serve(self, port: u16) -> ! {
let router = Router::new();
let router = self
.register_html(&router)
.layer(Extension::<Server>(self.clone()))
.layer(CookieManagerLayer::new());
let addr = SocketAddr::from(([127, 0, 0, 1], port));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(router.into_make_service())
.await
.unwrap();
panic!("server stopped prematurely")
}
}
#[derive(Clone, Debug)]
pub(super) enum ServerError {
Internal(String),
NotFound,
NotLoggedIn,
BadRequest(String),
}
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::BadRequest("duplicate entry exists".to_owned()),
UserError::NotFound => Self::NotFound,
UserError::Other(o) => Self::Internal(format!("UserError: {}", o)),
}
}
}
impl From<AuthError> for ServerError {
fn from(a: AuthError) -> Self {
match a {
AuthError::InvalidCredentials => {
ServerError::BadRequest("invalid credentials".to_owned())
}
AuthError::ServerError(err) => ServerError::Internal(err),
AuthError::Expired => ServerError::BadRequest("expired token".to_owned()),
}
}
}
impl Display for ServerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
impl IntoResponse for ServerError {
fn into_response(self) -> axum::response::Response {
match self {
ServerError::Internal(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(),
ServerError::NotFound => (
StatusCode::NOT_FOUND,
response::Html(include_str!("../../templates/html/404.html")),
)
.into_response(),
ServerError::BadRequest(err) => (StatusCode::BAD_REQUEST, err).into_response(),
ServerError::NotLoggedIn => (StatusCode::UNAUTHORIZED, "unauthorized").into_response(),
}
}
}

View File

@ -1,124 +0,0 @@
use std::time::SystemTime;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize};
use tower_cookies::Cookie;
use crate::{
database::{
db::DBError,
keys::Keys,
users::{self, UserSelect, Users},
},
sec,
};
#[derive(Clone)]
pub struct Auth {
secret: String,
users: Users,
}
const KEY_JWT_SECRET: &str = "JWT_SECRET";
const SECS_JWT_EXPIRE: u64 = 60 * 60; // 1hr
impl Auth {
pub async fn new(db: Keys, users: Users) -> Self {
Self {
secret: match db.get_key(KEY_JWT_SECRET).await {
Ok(secret) => secret,
Err(_) => {
// Create new secret and store to db
// If that fails, crash the application
let secret = sec::new_id();
db.set_key(KEY_JWT_SECRET, &secret).await.unwrap();
secret
}
},
users,
}
}
pub async fn login(&self, username: String, password: String) -> Result<String, AuthError> {
let user = self.users.user(UserSelect::Username(username)).await?;
if !sec::compare(&password, &user.password_hash) {
return Err(AuthError::InvalidCredentials);
}
Ok(jsonwebtoken::encode(
&Header::default(),
&Claims::from(user),
&EncodingKey::from_secret(self.secret.as_ref()),
)?)
}
pub fn get_claims(&self, token: String) -> Result<Claims, AuthError> {
Ok(jsonwebtoken::decode::<Claims>(
token.as_str(),
&DecodingKey::from_secret(self.secret.as_ref()),
&Validation::new(Algorithm::HS256),
)?
.claims)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub username: String,
pub exp: u64,
pub iat: u64,
}
impl Claims {
pub fn expired(&self) -> bool {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
>= self.exp
}
}
impl From<users::User> for Claims {
fn from(u: users::User) -> Self {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
Claims {
sub: u.id,
username: u.username,
exp: now + SECS_JWT_EXPIRE,
iat: now,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AuthError {
InvalidCredentials,
Expired,
ServerError(String),
}
impl AuthError {
pub fn expired(&self) -> bool {
*self == Self::Expired
}
}
impl From<DBError> for AuthError {
fn from(_: DBError) -> Self {
Self::InvalidCredentials
}
}
impl From<jsonwebtoken::errors::Error> for AuthError {
fn from(e: jsonwebtoken::errors::Error) -> Self {
match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => Self::Expired,
kind => Self::ServerError(e.to_string()),
}
}
}

View File

@ -1,148 +0,0 @@
use serde::Serialize;
use crate::{
database::{
db,
users::{self, UserSelect, Users},
},
sec,
};
#[derive(Clone)]
pub struct Profiler {
db: Users,
}
impl Profiler {
pub fn new(db: Users) -> Self {
Self { db }
}
pub async fn user(&self, username: String) -> Result<User, UserError> {
Ok(self.db.user(username.into()).await?.into())
}
pub async fn stats(&self, username: String) -> Result<UserStats, UserError> {
Ok(self.db.user_stats(username.into()).await?.into())
}
pub async fn profile(&self, username: String) -> Result<Profile, UserError> {
Ok((
self.user(username.clone()).await?,
self.stats(username).await?,
)
.into())
}
pub async fn create_user(
&self,
username: String,
password: String,
email: String,
) -> Result<User, UserError> {
let result = self
.db
.create_user(users::User {
id: String::new(),
username,
host: None,
display_name: None,
password_hash: sec::hash(password),
email,
avatar_uri: None,
bio: None,
})
.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>,
pub avatar_uri: Option<String>,
pub bio: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UserStats {
pub post_count: i64,
pub following: i64,
pub followers: i64,
}
impl From<users::UserStats> for UserStats {
fn from(u: users::UserStats) -> Self {
Self {
post_count: u.post_count,
following: u.following,
followers: u.followers,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Profile {
pub user: User,
pub stats: UserStats,
}
impl From<(User, UserStats)> for Profile {
fn from((user, stats): (User, UserStats)) -> Self {
Self {
user: user.into(),
stats: stats.into(),
}
}
}
impl From<users::User> for User {
fn from(u: users::User) -> Self {
Self {
id: u.id,
display_name: u.display_name.or(Some(format!(
"@{}{}",
u.username,
u.host
.clone()
.map(|h| format!("@{}", h))
.unwrap_or(String::new()),
))),
username: u.username,
host: u.host,
avatar_uri: u.avatar_uri,
bio: u.bio,
}
}
}
#[derive(Debug, Clone)]
pub enum UserError {
Duplicate,
NotFound,
Other(String),
}
impl From<db::DBError> for UserError {
fn from(err: db::DBError) -> Self {
match err {
db::DBError::Duplicate => Self::Duplicate,
db::DBError::NotFound => Self::NotFound,
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(),
}
}
}

View File

@ -1,242 +0,0 @@
body {
background-color: black;
color: rebeccapurple;
}
h1,
h2,
label {
font-size: 160%;
}
h1 {
text-align: center;
}
/* .big {
font-size: 200%;
} */
.big>input {
font-size: 180%;
margin: 5%;
}
input {
background-color: black;
color: white;
border-color: rebeccapurple;
}
.error {
color: rgba(255, 0, 0, 0.7);
font-weight: bold;
border: 5px solid rgba(99, 0, 0, 0.2);
background-color: rgba(95, 0, 0, 0.5);
}
.success {
color: rgba(0, 202, 0, 0.7);
font-weight: bold;
border: 5px solid rgba(0, 99, 0, 0.2);
background-color: rgba(0, 99, 0, 0.5);
}
#central {
width: 40%;
border: 5px solid rebeccapurple;
height: 30vw;
padding: 10px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(13, 6, 19, 0.4);
font-weight: bold;
}
a {
color: rebeccapurple;
}
.main {
width: 85vw;
border: 5px solid rebeccapurple;
height: 85vh;
padding: 10px;
margin: 0 auto;
display: flex;
background-color: rgba(13, 6, 19, 0.4);
font-weight: bold;
}
.profile-avatar {
width: 128px;
height: 128px;
border: 3px solid rebeccapurple;
}
.name {
font-size: 110%;
text-align: center;
margin: 0;
}
.profile {
/* border: 3px solid rebeccapurple; */
padding: 5px;
display: flex;
text-align: left;
}
.profile-header {
text-align: left;
width: 128px;
}
.profile-bio {
border: 3px solid rgba(102, 51, 153, 0.5);
margin-left: 5px;
text-align: center;
height: fit-content;
height: -moz-fit-content;
margin-left: 10px;
}
.stats>div {
border: 3px solid rgba(102, 51, 153, 0.5);
height: fit-content;
height: -moz-fit-content;
text-align: center;
width: 33%;
float: left;
margin: 5px;
}
.stats {
height: fit-content;
margin: 5px;
overflow: hidden;
width: auto;
display: flex;
align-items: center;
}
.stat-name {
font-weight: bold;
}
nav {
overflow: hidden;
border: 5px solid rgba(102, 51, 153, 0.5);
width: 85vw;
padding-left: 10px;
padding-right: 10px;
margin-left: auto;
margin-right: auto;
margin-bottom: 10px;
background-color: rgba(13, 6, 19, 0.4);
font-weight: bold;
}
nav ul {
margin: 0;
padding: 0;
}
nav ul li {
display: inline-block;
list-style-type: none;
}
nav>ul>li>a {
display: block;
line-height: 2em;
padding: 0.5em 0.5em;
text-decoration: none;
}
.avatar {
width: 42px;
height: 42px;
}
button {
background-color: rgba(102, 51, 153, 0.75);
color: rgb(204, 179, 230);
border-color: rgba(102, 51, 153, 0.25);
border-style: initial;
font-size: large;
}
#postbox {
/* border: 3px solid rebeccapurple; */
/* padding: 5px; */
margin: 0 0 30px 0;
height: fit-content;
display: flex;
/* text-align: left; */
}
#timeline {
margin: 0;
padding: 0;
}
#timeline li {
display: grid;
list-style-type: none;
width: 85vw;
}
#post-content {
color: rgb(204, 179, 230);
background-color: rebeccapurple;
border-width: 0px;
height: 60px;
width: 85vw;
}
.note {
display: flex;
}
.content {
display: inline-flex;
flex-direction: column;
}
div>p {
font-size: small;
margin-left: 2px;
margin-right: 2px;
}
.display-name {
font-weight: bold;
}
.username {
color: rgba(102, 51, 153, 0.5);
}
.user-info {
display: inline-flex;
margin-top: 0px;
}
#timeline-box {
color: rgb(204, 179, 230);
display: flex;
flex-direction: column;
}
.note:nth-child(even) {
background: rgba(102, 51, 153, 0.25);
}
.note:nth-child(odd) {
background: rgba(102, 51, 153, 0.125);
}

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk - not found</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body>
{{> (lookup this "nav_type") }}
<h1>404 not found</h1>
<br />
<h1><a href="/">return</a></h1>
</body>
</html>

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>redirecting</title>
<style>
body {
background-color: black;
}
</style>
<meta http-equiv="refresh" content="time; URL={{location}}" />
</head>
<body>
</body>
</html>

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body>
{{> (lookup this "nav_type") }}
<div class="main" id="timeline-box">
{{#if obj.show_postbox}}
<div id="postbox">
<textarea id="post-content" placeholder="im tosti"></textarea>
<button>send</button>
</div>
{{/if}}
<div id="timeline">
{{#each obj.notes}}
<div class="note">
<img class="avatar" src="{{this.profile.avatar_url}}" alt="example" />
<div class="content">
<div class="user-info">
<p class="display-name">{{this.profile.display_name}}</p>
<p class="username">{{this.profile.username}}</p>
</div>
<p class="post-text">{{this.content}}</p>
</div>
</div>
{{/each}}
</div>
</div>
</body>
</html>

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk - login</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body>
{{> LoggedOut }}
<h1>login</h1>
<div id="central">
<p>login form</p>
{{> (lookup this "tag_name")}}
<form class="big" id="login" method="post" action="/login">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="Submit" hidden>
</form>
</div>
</body>
</html>

View File

@ -1,13 +0,0 @@
<nav>
<ul>
<li>
<a href="/">home</a>
</li>
<li>
<a href="/@/{{obj.username}}">me</a>
</li>
<li>
<a href="/logout">log out</a>
</li>
</ul>
</nav>

View File

@ -1,13 +0,0 @@
<nav>
<ul>
<li>
<a href="/">home</a>
</li>
<li>
<a href="/login">login</a>
</li>
<li>
<a href="/signup">sign up</a>
</li>
</ul>
</nav>

View File

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>@{{obj.user.username}}</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body>
{{> (lookup this "nav_type") }}
<div class="main">
<div class="profile">
<div class="profile-header">
<img class="profile-avatar" src="{{obj.user.avatar_uri}}" alt="{{obj.user.username}}'s avatar" />
<p class="name">{{obj.user.display_name}}</p>
</div>
<div class="profile-bio">
<p>{{obj.user.bio}}</p>
<div class="stats">
<div id="posts">
<p class="stat-name">posts</p>
<p>{{obj.stats.post_count}}</p>
</div>
<div id="following">
<p class="stat-name">following</p>
<p>{{obj.stats.following}}</p>
</div>
<div id="followers">
<p class="stat-name">followers</p>
<p>{{obj.stats.followers}}</p>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk - signup</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body>
{{> LoggedOut }}
<h1>signup</h1>
<div id="central">
<p>signup form</p>
{{> (lookup this "tag_name")}}
<form class="big" id="signup" method="post" action="/signup">
<input type="text" name="username" placeholder="username">
<br>
<input type="email" name="email" placeholder="email">
<br>
<input type="password" name="password" placeholder="password">
<input type="submit" value="Submit" hidden>
</form>
</div>
</body>
</html>

View File

@ -1,38 +1,10 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users (
id CHAR(22) NOT NULL PRIMARY KEY,
username TEXT NOT NULL,
host TEXT,
display_name TEXT,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
avatar_uri TEXT,
bio 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;
--id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
CREATE TABLE keys (
key TEXT NOT NULL PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE follows (
user_id CHAR(22) NOT NULL REFERENCES users(id),
follows_id CHAR(22) NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, follows_id)
);
CREATE TABLE follow_requests(
user_id CHAR(22) NOT NULL REFERENCES users(id),
follows_id CHAR(22) NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, follows_id)
);

View File

@ -1,2 +0,0 @@
[toolchain]
channel = "nightly"

View File

@ -1,8 +1,8 @@
use std::sync::Arc;
use super::{keys::Keys, users::Users};
use super::users::Users;
use tokio::sync::Mutex;
use tokio_postgres::{Client, NoTls};
use tokio_postgres::{tls::NoTlsStream, Client, Connection, NoTls, Socket};
const DBERR_UNIQUE: &str = "23505";
@ -30,16 +30,10 @@ impl DB {
pub fn users(&self) -> Users {
Users::new(self.client.clone())
}
pub fn keys(&self) -> Keys {
Keys::new(self.client.clone())
}
}
#[derive(Debug)]
pub enum DBError {
Duplicate,
NotFound,
Other(tokio_postgres::Error),
}

View File

@ -1,3 +1,2 @@
pub mod db;
pub mod keys;
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),
}

48
src/main.rs Normal file
View File

@ -0,0 +1,48 @@
mod database;
mod model;
mod servek;
mod svc;
use database::db::DB;
use serde::{Deserialize, Serialize};
use servek::servek::Server;
use svc::profiles::Profiler;
#[derive(Clone, Copy, Serialize, Deserialize)]
pub enum ActivityKind {
Create,
Like,
Note,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ActivityLD {
pub context_uri: String,
pub kind: ActivityKind,
pub actor_uri: String,
pub to_uris: Vec<String>,
pub object: Option<ObjectLD>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ObjectLD {
pub context_uri: String,
pub id: Option<String>,
pub kind: Option<ActivityKind>,
pub attributed_to: Option<String>,
pub published: Option<String>,
pub content: Option<String>,
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
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 }
}
}

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

@ -0,0 +1,91 @@
use std::str::FromStr;
use warp::{http::HeaderValue, hyper::Uri, path::Tail, reply::Response, Filter, Rejection, Reply};
use super::servek::{Server, ServerError};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "static"]
struct StaticData;
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.or(Server::static_files()))
}
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 {
srv.hb
.render(
"profile",
&serde_json::json!(srv
.profiler
.profile(username)
.await
.map_err(|e| ServerError::from(e))?),
)
.map(|html| warp::reply::html(html))
.map_err(|e| ServerError::from(e).reject_self())
}),
)
}
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()),
}
}),
)
}
fn static_files() -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::get().and(warp::path("static").and(warp::path::tail()).and_then(
|path: Tail| async move {
let asset = match StaticData::get(path.as_str()) {
Some(a) => a,
None => return Err(ServerError::NotFound.reject_self()),
};
let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream();
let mut res = Response::new(asset.data.into());
res.headers_mut().insert(
"Content-Type",
HeaderValue::from_str(mime.as_ref()).unwrap(),
);
Ok(res)
},
))
}
}

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

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

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

@ -0,0 +1,116 @@
use core::panic;
use std::{convert::Infallible, fmt::Display};
use handlebars::{Handlebars, RenderError};
use warp::{
hyper::StatusCode,
reject::{MethodNotAllowed, Reject},
Filter, Rejection, Reply,
};
use crate::{
database::users::User,
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::NotFound => {
code = StatusCode::NOT_FOUND;
message = "not found";
}
ServerError::Duplicate => {
code = StatusCode::BAD_REQUEST;
message = "duplicate entry exists";
}
}
} else if let Some(err) = err.find::<MethodNotAllowed>() {
println!("MethodNotAllowed: {:#?}", err);
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),
NotFound,
Duplicate,
}
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::Duplicate,
UserError::NotFound => Self::NotFound,
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)
}
}

View File

@ -1,2 +1 @@
pub mod auth;
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 {}

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

@ -0,0 +1,8 @@
body {
background-color: black;
color: rebeccapurple;
}
h1 {
text-align: center;
}

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>

13
templates/html/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk</title>
<link rel="stylesheet" href="/static/style/main.css">
</head>
<body>
<h1>hi</h1>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>@{{username}}</title>
<link rel="stylesheet" href="/static/style/main.css">
</head>
<body>
<h1>hi {{username}}, your id is {{id}}</h1>
</body>
</html>