Compare commits
18 Commits
1d9802530d
...
master
Author | SHA1 | Date |
---|---|---|
emilis | 933d4c5f74 | |
emilis | f4aae98656 | |
emilis | be7c415980 | |
emilis | d868170084 | |
emilis | 5473512787 | |
emilis | de12258368 | |
emilis | 62c4946dfb | |
emilis | 1e7c59c5a4 | |
emilis | 271eba8d7b | |
emilis | 94af6929ab | |
emilis | bd93207c25 | |
emilis | 7b538c0a0c | |
emilis | 5d21e028db | |
emilis | 9606eea9e8 | |
emilis | 2dbc6ad68a | |
emilis | 779e4aa2ea | |
emilis | a5eef831c7 | |
emilis | 1c5c9caf2a |
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
|
@ -1,20 +1,7 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "flabk"
|
|
||||||
version = "0.0.1"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
members = [
|
||||||
|
"flabk",
|
||||||
|
"flabk-derive",
|
||||||
|
]
|
||||||
|
|
||||||
[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"
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
[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
|
|
@ -0,0 +1,22 @@
|
||||||
|
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()
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
[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" }
|
|
@ -0,0 +1,148 @@
|
||||||
|
<?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>
|
After Width: | Height: | Size: 4.5 KiB |
|
@ -0,0 +1,281 @@
|
||||||
|
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>,
|
||||||
|
}
|
|
@ -0,0 +1,316 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
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?)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::users::Users;
|
use super::{keys::Keys, users::Users};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio_postgres::{tls::NoTlsStream, Client, Connection, NoTls, Socket};
|
use tokio_postgres::{Client, NoTls};
|
||||||
|
|
||||||
const DBERR_UNIQUE: &str = "23505";
|
const DBERR_UNIQUE: &str = "23505";
|
||||||
|
|
||||||
|
@ -30,10 +30,16 @@ impl DB {
|
||||||
pub fn users(&self) -> Users {
|
pub fn users(&self) -> Users {
|
||||||
Users::new(self.client.clone())
|
Users::new(self.client.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn keys(&self) -> Keys {
|
||||||
|
Keys::new(self.client.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum DBError {
|
pub enum DBError {
|
||||||
Duplicate,
|
Duplicate,
|
||||||
|
NotFound,
|
||||||
Other(tokio_postgres::Error),
|
Other(tokio_postgres::Error),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod keys;
|
||||||
pub mod users;
|
pub mod users;
|
|
@ -0,0 +1,147 @@
|
||||||
|
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(),
|
||||||
|
&[¶m],
|
||||||
|
)
|
||||||
|
.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(),
|
||||||
|
&[¶m],
|
||||||
|
)
|
||||||
|
.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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
#![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(())
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,332 @@
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
|
pub mod auth;
|
||||||
pub mod profiles;
|
pub mod profiles;
|
|
@ -0,0 +1,148 @@
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,242 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<!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>
|
|
@ -1,10 +1,38 @@
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id CHAR(22) NOT NULL PRIMARY KEY,
|
id CHAR(22) NOT NULL PRIMARY KEY,
|
||||||
username TEXT NOT NULL,
|
username TEXT NOT NULL,
|
||||||
host TEXT,
|
host TEXT,
|
||||||
display_name TEXT
|
display_name TEXT,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
avatar_uri TEXT,
|
||||||
|
bio TEXT,
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX u_username_host ON users (username, host);
|
CREATE UNIQUE INDEX u_username_host ON users (username, host);
|
||||||
CREATE UNIQUE INDEX u_username_local ON users (username) WHERE host IS NULL;
|
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)
|
||||||
|
);
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
|
@ -1,95 +0,0 @@
|
||||||
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
48
src/main.rs
|
@ -1,48 +0,0 @@
|
||||||
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
12
src/model.rs
|
@ -1,12 +0,0 @@
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
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)
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
mod html;
|
|
||||||
pub mod servek;
|
|
|
@ -1,116 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
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 {}
|
|
|
@ -1,8 +0,0 @@
|
||||||
body {
|
|
||||||
background-color: black;
|
|
||||||
color: rebeccapurple;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>flabk - not found</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>not found</h1>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>flabk</title>
|
|
||||||
<link rel="stylesheet" href="/static/style/main.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>hi</h1>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<!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>
|
|
Loading…
Reference in New Issue