Compare commits
No commits in common. "master" and "wip/chunker" have entirely different histories.
master
...
wip/chunke
|
@ -1 +0,0 @@
|
||||||
/target
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +0,0 @@
|
||||||
[workspace]
|
|
||||||
|
|
||||||
members = [
|
|
||||||
"flabk",
|
|
||||||
"flabk-derive",
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "flabk-derive"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
proc-macro2 = "1.0"
|
|
||||||
quote = "1.0"
|
|
||||||
syn = { version = "1.0", features = ["full"] }
|
|
||||||
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
|
@ -1,22 +0,0 @@
|
||||||
extern crate proc_macro;
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
use syn::{parse_macro_input, DeriveInput};
|
|
||||||
|
|
||||||
#[proc_macro_derive(LD)]
|
|
||||||
pub fn derive_ld(input: TokenStream) -> TokenStream {
|
|
||||||
let DeriveInput { ident, .. } = parse_macro_input!(input);
|
|
||||||
quote! {
|
|
||||||
impl crate::astreams::serde_ext::LDObject for #ident {
|
|
||||||
fn from_iri(s: &str) -> Self {
|
|
||||||
let mut ident = #ident::default();
|
|
||||||
ident.id = s.into();
|
|
||||||
ident
|
|
||||||
}
|
|
||||||
fn get_iri(&self) -> String {
|
|
||||||
self.id.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "sectorinf.com/emilis/flabk/flabk/flabweb"
|
||||||
|
|
||||||
|
// MUST (client and server) application/ld+json; profile="https://www.w3.org/ns/activitystreams
|
||||||
|
// SHOULD application/activity+json
|
||||||
|
|
||||||
|
// public actor https://www.w3.org/ns/activitystreams#Public
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
panic(flabweb.New("my.site").Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "@context": ["https://www.w3.org/ns/activitystreams",
|
||||||
|
// {"@language": "en"}],
|
||||||
|
// "type": "Like",
|
||||||
|
// "actor": "https://dustycloud.org/chris/",
|
||||||
|
// "name": "Chris liked 'Minimal ActivityPub update client'",
|
||||||
|
// "object": "https://rhiaro.co.uk/2016/05/minimal-activitypub",
|
||||||
|
// "to": ["https://rhiaro.co.uk/#amy",
|
||||||
|
// "https://dustycloud.org/followers",
|
||||||
|
// "https://rhiaro.co.uk/followers/"],
|
||||||
|
// "cc": "https://e14n.com/evan"
|
||||||
|
// }
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "@context": [
|
||||||
|
// "https://www.w3.org/ns/activitystreams",
|
||||||
|
// {
|
||||||
|
// "@language": "en"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "type": "Like",
|
||||||
|
// "to": [
|
||||||
|
// "https://rhiaro.co.uk/#amy",
|
||||||
|
// "https://dustycloud.org/followers",
|
||||||
|
// "https://rhiaro.co.uk/followers/"
|
||||||
|
// ],
|
||||||
|
// "cc": [
|
||||||
|
// "https://e14n.com/evan"
|
||||||
|
// ],
|
||||||
|
// "object": "https://rhiaro.co.uk/2016/05/minimal-activitypub",
|
||||||
|
// "actor": "https://dustycloud.org/chris/",
|
||||||
|
// "name": "Chris liked 'Minimal ActivityPub update client'"
|
||||||
|
// }
|
|
@ -1,45 +0,0 @@
|
||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug executable 'flabk'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--bin=flabk",
|
|
||||||
"--package=flabk"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "flabk",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// "type": "lldb",
|
|
||||||
// "request": "launch",
|
|
||||||
// "name": "Debug unit tests in executable 'flabk'",
|
|
||||||
// "cargo": {
|
|
||||||
// "args": [
|
|
||||||
// "test",
|
|
||||||
// "--no-run",
|
|
||||||
// "--bin=flabk",
|
|
||||||
// "--package=flabk"
|
|
||||||
// ],
|
|
||||||
// "filter": {
|
|
||||||
// "name": "flabk",
|
|
||||||
// "kind": "bin"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "args": [],
|
|
||||||
// "cwd": "${workspaceFolder}"
|
|
||||||
// }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "flabk"
|
|
||||||
version = "0.0.1"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.64"
|
|
||||||
argon2 = "0.4.1"
|
|
||||||
async-trait = "0.1.58"
|
|
||||||
axum = "0.5.17"
|
|
||||||
base-62 = "0.1.1"
|
|
||||||
handlebars = "4.3.3"
|
|
||||||
jsonwebtoken = "8.1.1"
|
|
||||||
mime_guess = "2.0.4"
|
|
||||||
rand = "0.8.5"
|
|
||||||
rand_core = { version = "0.6.3", features = ["std"] }
|
|
||||||
reqwest = { version = "0.11.12", features = [
|
|
||||||
"__tls",
|
|
||||||
"default-tls",
|
|
||||||
"hyper-tls",
|
|
||||||
"native-tls-crate",
|
|
||||||
"tokio-native-tls",
|
|
||||||
"serde_json",
|
|
||||||
"json",
|
|
||||||
] }
|
|
||||||
rust-embed = "6.4.0"
|
|
||||||
serde = { version = "1.0.144", features = ["derive", "std", "serde_derive"] }
|
|
||||||
serde_json = "1.0.85"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
tokio-postgres = { version = "0.7.7", features = ["with-serde_json-1"] }
|
|
||||||
tower-cookies = "0.7.0"
|
|
||||||
flabk-derive = { path = "../flabk-derive" }
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package ap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Object struct {
|
||||||
|
Context jsonld.Context `jsonld:"@context,omitempty,collapsible"`
|
||||||
|
Type string `jsonld:"type"`
|
||||||
|
Type2 string `jsonld:"@type"`
|
||||||
|
ID jsonld.IRI `jsonld:"id,omitempty"`
|
||||||
|
Name string `jsonld:"name,omitempty"`
|
||||||
|
Source *Source `jsonld:"source,omitempty"`
|
||||||
|
To []Object `jsonld:"to,omitempty,collapsible"`
|
||||||
|
CC []Object `jsonld:"cc,omitempty,collapsible"`
|
||||||
|
Object *Object `jsonld:"object,omitempty,collapsible"`
|
||||||
|
Actor *Actor `jsonld:"actor,omitempty,collapsible"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Object) Collapse() interface{} {
|
||||||
|
return o.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type Actor struct {
|
||||||
|
ID jsonld.IRI `jsonld:"id,omitempty"`
|
||||||
|
Name string `jsonld:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Actor) Collapse() interface{} {
|
||||||
|
return a.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type Source struct {
|
||||||
|
Content string `jsonld:"content,omitempty"`
|
||||||
|
MediaType string `jsonld:"mediaType,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package ap_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"sectorinf.com/emilis/flabk/flabk/ap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetExample() ap.Object {
|
||||||
|
return ap.Object{
|
||||||
|
Context: jsonld.Context{
|
||||||
|
{
|
||||||
|
IRI: jsonld.IRI("https://www.w3.org/ns/activitystreams"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Term: jsonld.LanguageKw,
|
||||||
|
IRI: "en",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Type: "Like",
|
||||||
|
Actor: &ap.Actor{
|
||||||
|
ID: "https://dustycloud.org/chris/",
|
||||||
|
},
|
||||||
|
Name: "Chris liked 'Minimal ActivityPub update client'",
|
||||||
|
Object: &ap.Object{
|
||||||
|
ID: "https://rhiaro.co.uk/2016/05/minimal-activitypub",
|
||||||
|
},
|
||||||
|
To: []ap.Object{
|
||||||
|
{ID: "https://rhiaro.co.uk/#amy"},
|
||||||
|
{ID: "https://dustycloud.org/followers"},
|
||||||
|
{ID: "https://rhiaro.co.uk/followers/"},
|
||||||
|
},
|
||||||
|
CC: []ap.Object{
|
||||||
|
{ID: "https://e14n.com/evan"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObject(t *testing.T) {
|
||||||
|
that := require.New(t)
|
||||||
|
result, err := jsonld.Marshal(GetExample())
|
||||||
|
that.NoError(err)
|
||||||
|
fmt.Println(string(result))
|
||||||
|
fmt.Println()
|
||||||
|
another := ap.Object{}
|
||||||
|
err = jsonld.Unmarshal(result, &another)
|
||||||
|
that.NoError(err)
|
||||||
|
fmt.Printf("%+v\n", another)
|
||||||
|
}
|
|
@ -1,148 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="130.52438mm"
|
|
||||||
height="130.52438mm"
|
|
||||||
viewBox="0 0 130.52438 130.52438"
|
|
||||||
version="1.1"
|
|
||||||
id="svg5"
|
|
||||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
|
|
||||||
sodipodi:docname="flabk_icon_placeholder.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview7"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="0.54866744"
|
|
||||||
inkscape:cx="-78.371699"
|
|
||||||
inkscape:cy="260.63147"
|
|
||||||
inkscape:window-width="1914"
|
|
||||||
inkscape:window-height="1041"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="16"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="layer2" />
|
|
||||||
<defs
|
|
||||||
id="defs2">
|
|
||||||
<rect
|
|
||||||
x="238.94496"
|
|
||||||
y="545.79144"
|
|
||||||
width="291.11197"
|
|
||||||
height="295.74161"
|
|
||||||
id="rect1130" />
|
|
||||||
<filter
|
|
||||||
style="color-interpolation-filters:sRGB"
|
|
||||||
inkscape:label="Roughen"
|
|
||||||
id="filter1273"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="1"
|
|
||||||
height="1">
|
|
||||||
<feTurbulence
|
|
||||||
type="fractalNoise"
|
|
||||||
numOctaves="5"
|
|
||||||
seed="145"
|
|
||||||
baseFrequency="0.001 10"
|
|
||||||
result="turbulence"
|
|
||||||
id="feTurbulence1269" />
|
|
||||||
<feDisplacementMap
|
|
||||||
in="SourceGraphic"
|
|
||||||
in2="turbulence"
|
|
||||||
scale="1.68439"
|
|
||||||
yChannelSelector="G"
|
|
||||||
xChannelSelector="R"
|
|
||||||
id="feDisplacementMap1271" />
|
|
||||||
</filter>
|
|
||||||
<rect
|
|
||||||
x="238.94496"
|
|
||||||
y="545.79144"
|
|
||||||
width="291.11197"
|
|
||||||
height="295.74161"
|
|
||||||
id="rect1130-3" />
|
|
||||||
<filter
|
|
||||||
style="color-interpolation-filters:sRGB"
|
|
||||||
inkscape:label="Roughen"
|
|
||||||
id="filter1273-6"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="1"
|
|
||||||
height="1">
|
|
||||||
<feTurbulence
|
|
||||||
type="fractalNoise"
|
|
||||||
numOctaves="5"
|
|
||||||
seed="145"
|
|
||||||
baseFrequency="0.001 10"
|
|
||||||
result="turbulence"
|
|
||||||
id="feTurbulence1269-7" />
|
|
||||||
<feDisplacementMap
|
|
||||||
in="SourceGraphic"
|
|
||||||
in2="turbulence"
|
|
||||||
scale="1.68439"
|
|
||||||
yChannelSelector="G"
|
|
||||||
xChannelSelector="R"
|
|
||||||
id="feDisplacementMap1271-5" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
style="display:inline"
|
|
||||||
transform="translate(-38.320595,-110.90324)">
|
|
||||||
<circle
|
|
||||||
style="display:inline;fill:#663399;fill-opacity:1;stroke-width:0.264583"
|
|
||||||
id="path1074"
|
|
||||||
cx="103.58279"
|
|
||||||
cy="176.16544"
|
|
||||||
r="65.262192" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer2"
|
|
||||||
inkscape:label="Layer 2"
|
|
||||||
transform="translate(-38.320595,-110.90324)">
|
|
||||||
<circle
|
|
||||||
style="display:inline;fill:#0f0617;fill-opacity:1;stroke-width:0.224394;stroke-dasharray:none"
|
|
||||||
id="path1074-5"
|
|
||||||
cx="103.58279"
|
|
||||||
cy="176.16544"
|
|
||||||
r="55.262001" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer3"
|
|
||||||
inkscape:label="Layer 3"
|
|
||||||
transform="translate(-38.320595,-110.90324)">
|
|
||||||
<g
|
|
||||||
id="g2829"
|
|
||||||
transform="matrix(0.88602282,0,0,0.87606946,13.533885,21.962211)">
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(2.0837961,0,0,2.645293,-442.06366,-1321.3309)"
|
|
||||||
id="text1128-3"
|
|
||||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1130-3);display:inline;fill:#482464;fill-opacity:1;stroke:none;stroke-width:1.00008;stroke-dasharray:none;filter:url(#filter1273-6)"><tspan
|
|
||||||
x="238.94531"
|
|
||||||
y="581.18164"
|
|
||||||
id="tspan2868">fK</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
transform="matrix(2.0837961,0,0,2.645293,-438.16349,-1321.0342)"
|
|
||||||
id="text1128"
|
|
||||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1130);fill:#ffffff;fill-opacity:1;stroke:none;filter:url(#filter1273)"><tspan
|
|
||||||
x="238.94531"
|
|
||||||
y="581.18164"
|
|
||||||
id="tspan2870">fK</tspan></text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.5 KiB |
|
@ -0,0 +1,46 @@
|
||||||
|
package flabweb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"sectorinf.com/emilis/flabk/util/uriutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EndpointOutbox = "outbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tempOutboxIDCauseImNotStoringThis = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
router *gin.Engine
|
||||||
|
v1 *gin.RouterGroup
|
||||||
|
hostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(hostname string) Server {
|
||||||
|
router := gin.Default()
|
||||||
|
v1 := router.Group("v1")
|
||||||
|
server := Server{
|
||||||
|
hostname: hostname,
|
||||||
|
router: router,
|
||||||
|
v1: v1,
|
||||||
|
}
|
||||||
|
v1.POST(EndpointOutbox, server.Outbox)
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) Run() error {
|
||||||
|
return s.router.Run(":8081")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) Outbox(ctx *gin.Context) {
|
||||||
|
id := tempOutboxIDCauseImNotStoringThis
|
||||||
|
tempOutboxIDCauseImNotStoringThis++
|
||||||
|
ctx.Header("Location", uriutil.JoinURIs(s.hostname, EndpointOutbox, strconv.Itoa(id)))
|
||||||
|
ctx.Status(http.StatusCreated)
|
||||||
|
}
|
|
@ -1,281 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use super::serde_ext::LDObject;
|
|
||||||
|
|
||||||
pub const CONTEXT_ID: &str = "https://www.w3.org/ns/activitystreams";
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize)]
|
|
||||||
pub struct APContext {
|
|
||||||
pub id: Option<String>,
|
|
||||||
#[serde(rename = "@context")]
|
|
||||||
pub ctx: ContextMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LDObject for APContext {
|
|
||||||
fn from_iri(iri: &str) -> Self {
|
|
||||||
let mut ctx = Self::default();
|
|
||||||
ctx.id = Some(iri.into());
|
|
||||||
ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_iri(&self) -> String {
|
|
||||||
self.id.clone().unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
|
||||||
pub struct ContextMap {
|
|
||||||
#[serde(rename = "@vocab")]
|
|
||||||
pub vocab: String,
|
|
||||||
#[serde(rename = "xsd")]
|
|
||||||
pub xsd: String,
|
|
||||||
#[serde(rename = "as")]
|
|
||||||
pub as_field: String,
|
|
||||||
#[serde(rename = "ldp")]
|
|
||||||
pub ldp: String,
|
|
||||||
#[serde(rename = "vcard")]
|
|
||||||
pub vcard: String,
|
|
||||||
#[serde(rename = "id")]
|
|
||||||
pub id: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub type_field: String,
|
|
||||||
pub accept: String,
|
|
||||||
pub activity: String,
|
|
||||||
pub intransitive_activity: String,
|
|
||||||
pub add: String,
|
|
||||||
pub announce: String,
|
|
||||||
pub application: String,
|
|
||||||
pub arrive: String,
|
|
||||||
pub article: String,
|
|
||||||
pub audio: String,
|
|
||||||
pub block: String,
|
|
||||||
pub collection: String,
|
|
||||||
pub collection_page: String,
|
|
||||||
pub relationship: String,
|
|
||||||
pub create: String,
|
|
||||||
pub delete: String,
|
|
||||||
pub dislike: String,
|
|
||||||
pub document: String,
|
|
||||||
pub event: String,
|
|
||||||
pub follow: String,
|
|
||||||
pub flag: String,
|
|
||||||
pub group: String,
|
|
||||||
pub ignore: String,
|
|
||||||
pub image: String,
|
|
||||||
pub invite: String,
|
|
||||||
pub join: String,
|
|
||||||
pub leave: String,
|
|
||||||
pub like: String,
|
|
||||||
pub link: String,
|
|
||||||
pub mention: String,
|
|
||||||
pub note: String,
|
|
||||||
pub object: String,
|
|
||||||
pub offer: String,
|
|
||||||
pub ordered_collection: String,
|
|
||||||
pub ordered_collection_page: String,
|
|
||||||
pub organization: String,
|
|
||||||
pub page: String,
|
|
||||||
pub person: String,
|
|
||||||
pub place: String,
|
|
||||||
pub profile: String,
|
|
||||||
pub question: String,
|
|
||||||
pub reject: String,
|
|
||||||
pub remove: String,
|
|
||||||
pub service: String,
|
|
||||||
pub tentative_accept: String,
|
|
||||||
pub tentative_reject: String,
|
|
||||||
pub tombstone: String,
|
|
||||||
pub undo: String,
|
|
||||||
pub update: String,
|
|
||||||
pub video: String,
|
|
||||||
pub view: String,
|
|
||||||
pub listen: String,
|
|
||||||
pub read: String,
|
|
||||||
#[serde(rename = "Move")]
|
|
||||||
pub move_field: String,
|
|
||||||
pub travel: String,
|
|
||||||
pub is_following: String,
|
|
||||||
pub is_followed_by: String,
|
|
||||||
pub is_contact: String,
|
|
||||||
pub is_member: String,
|
|
||||||
#[serde(rename = "subject")]
|
|
||||||
pub subject: Option<TypedField>,
|
|
||||||
#[serde(rename = "relationship")]
|
|
||||||
pub relationship2: Option<TypedField>,
|
|
||||||
#[serde(rename = "actor")]
|
|
||||||
pub actor: Option<TypedField>,
|
|
||||||
#[serde(rename = "attributedTo")]
|
|
||||||
pub attributed_to: Option<TypedField>,
|
|
||||||
#[serde(rename = "attachment")]
|
|
||||||
pub attachment: Option<TypedField>,
|
|
||||||
#[serde(rename = "bcc")]
|
|
||||||
pub bcc: Option<TypedField>,
|
|
||||||
#[serde(rename = "bto")]
|
|
||||||
pub bto: Option<TypedField>,
|
|
||||||
#[serde(rename = "cc")]
|
|
||||||
pub cc: Option<TypedField>,
|
|
||||||
#[serde(rename = "context")]
|
|
||||||
pub context: Option<TypedField>,
|
|
||||||
#[serde(rename = "current")]
|
|
||||||
pub current: Option<TypedField>,
|
|
||||||
#[serde(rename = "first")]
|
|
||||||
pub first: Option<TypedField>,
|
|
||||||
#[serde(rename = "generator")]
|
|
||||||
pub generator: Option<TypedField>,
|
|
||||||
#[serde(rename = "icon")]
|
|
||||||
pub icon: Option<TypedField>,
|
|
||||||
#[serde(rename = "image")]
|
|
||||||
pub image2: Option<TypedField>,
|
|
||||||
#[serde(rename = "inReplyTo")]
|
|
||||||
pub in_reply_to: Option<TypedField>,
|
|
||||||
#[serde(rename = "items")]
|
|
||||||
pub items: Option<TypedField>,
|
|
||||||
#[serde(rename = "instrument")]
|
|
||||||
pub instrument: Option<TypedField>,
|
|
||||||
#[serde(rename = "orderedItems")]
|
|
||||||
pub ordered_items: Option<TypedField>,
|
|
||||||
#[serde(rename = "last")]
|
|
||||||
pub last: Option<TypedField>,
|
|
||||||
#[serde(rename = "location")]
|
|
||||||
pub location: Option<TypedField>,
|
|
||||||
#[serde(rename = "next")]
|
|
||||||
pub next: Option<TypedField>,
|
|
||||||
#[serde(rename = "object")]
|
|
||||||
pub object2: Option<TypedField>,
|
|
||||||
#[serde(rename = "oneOf")]
|
|
||||||
pub one_of: Option<TypedField>,
|
|
||||||
#[serde(rename = "anyOf")]
|
|
||||||
pub any_of: Option<TypedField>,
|
|
||||||
#[serde(rename = "closed")]
|
|
||||||
pub closed: Option<TypedField>,
|
|
||||||
#[serde(rename = "origin")]
|
|
||||||
pub origin: Option<TypedField>,
|
|
||||||
#[serde(rename = "accuracy")]
|
|
||||||
pub accuracy: Option<TypedField>,
|
|
||||||
#[serde(rename = "prev")]
|
|
||||||
pub prev: Option<TypedField>,
|
|
||||||
#[serde(rename = "preview")]
|
|
||||||
pub preview: Option<TypedField>,
|
|
||||||
#[serde(rename = "replies")]
|
|
||||||
pub replies: Option<TypedField>,
|
|
||||||
#[serde(rename = "result")]
|
|
||||||
pub result: Option<TypedField>,
|
|
||||||
#[serde(rename = "audience")]
|
|
||||||
pub audience: Option<TypedField>,
|
|
||||||
#[serde(rename = "partOf")]
|
|
||||||
pub part_of: Option<TypedField>,
|
|
||||||
#[serde(rename = "tag")]
|
|
||||||
pub tag: Option<TypedField>,
|
|
||||||
#[serde(rename = "target")]
|
|
||||||
pub target: Option<TypedField>,
|
|
||||||
#[serde(rename = "to")]
|
|
||||||
pub to: Option<TypedField>,
|
|
||||||
#[serde(rename = "url")]
|
|
||||||
pub url: Option<TypedField>,
|
|
||||||
#[serde(rename = "altitude")]
|
|
||||||
pub altitude: Option<TypedField>,
|
|
||||||
#[serde(rename = "content")]
|
|
||||||
pub content: String,
|
|
||||||
#[serde(rename = "contentMap")]
|
|
||||||
pub content_map: Option<TypedField>,
|
|
||||||
#[serde(rename = "name")]
|
|
||||||
pub name: String,
|
|
||||||
#[serde(rename = "nameMap")]
|
|
||||||
pub name_map: Option<TypedField>,
|
|
||||||
#[serde(rename = "duration")]
|
|
||||||
pub duration: Option<TypedField>,
|
|
||||||
#[serde(rename = "endTime")]
|
|
||||||
pub end_time: Option<TypedField>,
|
|
||||||
#[serde(rename = "height")]
|
|
||||||
pub height: Option<TypedField>,
|
|
||||||
#[serde(rename = "href")]
|
|
||||||
pub href: Option<TypedField>,
|
|
||||||
#[serde(rename = "hreflang")]
|
|
||||||
pub hreflang: String,
|
|
||||||
#[serde(rename = "latitude")]
|
|
||||||
pub latitude: Option<TypedField>,
|
|
||||||
#[serde(rename = "longitude")]
|
|
||||||
pub longitude: Option<TypedField>,
|
|
||||||
#[serde(rename = "mediaType")]
|
|
||||||
pub media_type: String,
|
|
||||||
#[serde(rename = "published")]
|
|
||||||
pub published: Option<TypedField>,
|
|
||||||
#[serde(rename = "radius")]
|
|
||||||
pub radius: Option<TypedField>,
|
|
||||||
#[serde(rename = "rel")]
|
|
||||||
pub rel: String,
|
|
||||||
#[serde(rename = "startIndex")]
|
|
||||||
pub start_index: Option<TypedField>,
|
|
||||||
#[serde(rename = "startTime")]
|
|
||||||
pub start_time: Option<TypedField>,
|
|
||||||
#[serde(rename = "summary")]
|
|
||||||
pub summary: String,
|
|
||||||
#[serde(rename = "summaryMap")]
|
|
||||||
pub summary_map: Option<TypedField>,
|
|
||||||
#[serde(rename = "totalItems")]
|
|
||||||
pub total_items: Option<TypedField>,
|
|
||||||
#[serde(rename = "units")]
|
|
||||||
pub units: String,
|
|
||||||
#[serde(rename = "updated")]
|
|
||||||
pub updated: Option<TypedField>,
|
|
||||||
#[serde(rename = "width")]
|
|
||||||
pub width: Option<TypedField>,
|
|
||||||
#[serde(rename = "describes")]
|
|
||||||
pub describes: Option<TypedField>,
|
|
||||||
#[serde(rename = "formerType")]
|
|
||||||
pub former_type: Option<TypedField>,
|
|
||||||
#[serde(rename = "deleted")]
|
|
||||||
pub deleted: Option<TypedField>,
|
|
||||||
#[serde(rename = "inbox")]
|
|
||||||
pub inbox: Option<TypedField>,
|
|
||||||
#[serde(rename = "outbox")]
|
|
||||||
pub outbox: Option<TypedField>,
|
|
||||||
#[serde(rename = "following")]
|
|
||||||
pub following: Option<TypedField>,
|
|
||||||
#[serde(rename = "followers")]
|
|
||||||
pub followers: Option<TypedField>,
|
|
||||||
#[serde(rename = "streams")]
|
|
||||||
pub streams: Option<TypedField>,
|
|
||||||
#[serde(rename = "preferredUsername")]
|
|
||||||
pub preferred_username: String,
|
|
||||||
#[serde(rename = "endpoints")]
|
|
||||||
pub endpoints: Option<TypedField>,
|
|
||||||
#[serde(rename = "uploadMedia")]
|
|
||||||
pub upload_media: Option<TypedField>,
|
|
||||||
#[serde(rename = "proxyUrl")]
|
|
||||||
pub proxy_url: Option<TypedField>,
|
|
||||||
#[serde(rename = "liked")]
|
|
||||||
pub liked: Option<TypedField>,
|
|
||||||
#[serde(rename = "oauthAuthorizationEndpoint")]
|
|
||||||
pub oauth_authorization_endpoint: Option<TypedField>,
|
|
||||||
#[serde(rename = "oauthTokenEndpoint")]
|
|
||||||
pub oauth_token_endpoint: Option<TypedField>,
|
|
||||||
#[serde(rename = "provideClientKey")]
|
|
||||||
pub provide_client_key: Option<TypedField>,
|
|
||||||
#[serde(rename = "signClientKey")]
|
|
||||||
pub sign_client_key: Option<TypedField>,
|
|
||||||
#[serde(rename = "sharedInbox")]
|
|
||||||
pub shared_inbox: Option<TypedField>,
|
|
||||||
pub public: Option<TypedField>,
|
|
||||||
#[serde(rename = "source")]
|
|
||||||
pub source: String,
|
|
||||||
#[serde(rename = "likes")]
|
|
||||||
pub likes: Option<TypedField>,
|
|
||||||
#[serde(rename = "shares")]
|
|
||||||
pub shares: Option<TypedField>,
|
|
||||||
#[serde(rename = "alsoKnownAs")]
|
|
||||||
pub also_known_as: Option<TypedField>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize)]
|
|
||||||
pub struct TypedField {
|
|
||||||
#[serde(rename = "@id")]
|
|
||||||
pub id: String,
|
|
||||||
#[serde(rename = "@type")]
|
|
||||||
pub kind: Option<String>,
|
|
||||||
#[serde(rename = "@container")]
|
|
||||||
pub container: Option<String>,
|
|
||||||
}
|
|
|
@ -1,316 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use flabk_derive::LD;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::astreams::resolve::Resolvable;
|
|
||||||
|
|
||||||
use self::{
|
|
||||||
resolve::ResolveError,
|
|
||||||
serde_ext::{expand_partial, expand_partial_into_vec, into_vec},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod context;
|
|
||||||
pub mod resolve;
|
|
||||||
mod serde_ext;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
|
||||||
pub enum ObjectKind {
|
|
||||||
Article,
|
|
||||||
Audio,
|
|
||||||
Document,
|
|
||||||
Event,
|
|
||||||
Image,
|
|
||||||
Note,
|
|
||||||
Page,
|
|
||||||
Place,
|
|
||||||
Profile,
|
|
||||||
Relationship,
|
|
||||||
Tombstone,
|
|
||||||
Video,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
|
||||||
pub enum ActivityKind {
|
|
||||||
Accept,
|
|
||||||
Add,
|
|
||||||
Announce,
|
|
||||||
Arrive,
|
|
||||||
Block,
|
|
||||||
Create,
|
|
||||||
Delete,
|
|
||||||
Dislike,
|
|
||||||
Flag,
|
|
||||||
Follow,
|
|
||||||
Invite,
|
|
||||||
Join,
|
|
||||||
Leave,
|
|
||||||
Like,
|
|
||||||
Listen,
|
|
||||||
Move,
|
|
||||||
Offer,
|
|
||||||
Question,
|
|
||||||
Reject,
|
|
||||||
Read,
|
|
||||||
Remove,
|
|
||||||
TentativeReject,
|
|
||||||
TentativeAccept,
|
|
||||||
Travel,
|
|
||||||
Undo,
|
|
||||||
Update,
|
|
||||||
View,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ExpandError {
|
|
||||||
InvalidKind(Option<ActivityKind>),
|
|
||||||
NoAttribution,
|
|
||||||
ResolveIRI(String),
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ResolveError> for ExpandError {
|
|
||||||
fn from(err: ResolveError) -> Self {
|
|
||||||
Self::ResolveIRI(err.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Collection is a subtype of Object that
|
|
||||||
/// represents ordered or unordered sets of
|
|
||||||
/// Object or Link instances.
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Collection<T> {
|
|
||||||
pub summary: Option<String>,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: String,
|
|
||||||
#[serde(rename = "totalItems")]
|
|
||||||
pub total_items: Vec<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Attachment {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: String,
|
|
||||||
pub content: Option<String>,
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: maybe this can work as attachment w/ the ID being more like
|
|
||||||
// the LD IDs?
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Image {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub base: ObjectBase,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: Option<ObjectKind>,
|
|
||||||
pub id: String,
|
|
||||||
#[serde(deserialize_with = "into_vec")]
|
|
||||||
pub url: Vec<Link>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Audience {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
|
||||||
pub enum Unit {
|
|
||||||
#[serde(rename = "m")]
|
|
||||||
Meters,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Location {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: String,
|
|
||||||
pub name: String,
|
|
||||||
pub longitude: Option<f64>,
|
|
||||||
pub latitude: Option<f64>,
|
|
||||||
pub altitude: Option<f64>,
|
|
||||||
pub units: Unit,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Preview {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: String,
|
|
||||||
pub name: String,
|
|
||||||
pub duration: Option<String>,
|
|
||||||
pub url: Link,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct APResult {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ObjectBase contains base Activity Streams
|
|
||||||
/// members that all objects should have. Except kind.
|
|
||||||
///
|
|
||||||
/// This object is intended to be used by inlining it into
|
|
||||||
/// other serializable/deserializable objects.
|
|
||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ObjectBase {
|
|
||||||
// #[serde(rename = "@context")]
|
|
||||||
// #[serde(deserialize_with = "expand_partial")]
|
|
||||||
// pub ap_context: APContext,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub name_map: Option<HashMap<String, String>>,
|
|
||||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
|
||||||
pub attachment: Vec<Attachment>,
|
|
||||||
#[serde(deserialize_with = "expand_partial_into_vec")]
|
|
||||||
pub attributed_to: Vec<Actor>,
|
|
||||||
pub audience: Option<Audience>,
|
|
||||||
pub media_type: Option<String>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
pub content_map: Option<HashMap<String, String>>,
|
|
||||||
pub context: Option<String>,
|
|
||||||
pub start_time: Option<String>,
|
|
||||||
pub end_time: Option<String>,
|
|
||||||
pub generator: Option<Actor>,
|
|
||||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
|
||||||
pub icon: Vec<Link>,
|
|
||||||
// omit in_reply_to
|
|
||||||
pub location: Option<Location>,
|
|
||||||
pub preview: Option<Preview>,
|
|
||||||
pub published: Option<String>,
|
|
||||||
pub updated: Option<String>,
|
|
||||||
pub replies: Option<Collection<()>>, // TODO: type
|
|
||||||
pub summary: Option<String>,
|
|
||||||
pub summary_map: Option<HashMap<String, String>>,
|
|
||||||
#[serde(deserialize_with = "expand_partial_into_vec", default = "Vec::new")]
|
|
||||||
pub tag: Vec<Actor>,
|
|
||||||
// omit url for now: need to merge into_vec & expand_partial
|
|
||||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
|
||||||
pub to: Vec<String>,
|
|
||||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
|
||||||
pub bto: Vec<String>,
|
|
||||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
|
||||||
pub cc: Vec<String>,
|
|
||||||
#[serde(deserialize_with = "into_vec", default = "Vec::new")]
|
|
||||||
pub bcc: Vec<String>,
|
|
||||||
pub duration: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, LD, Default)]
|
|
||||||
pub struct Object {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub base: ObjectBase,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: Option<ObjectKind>,
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Link {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: Option<ActivityKind>,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub href: String,
|
|
||||||
#[serde(rename = "hreflang")]
|
|
||||||
pub href_lang: Option<String>,
|
|
||||||
#[serde(rename = "mediaType")]
|
|
||||||
pub media_type: Option<String>,
|
|
||||||
pub height: u32,
|
|
||||||
pub width: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An Activity is a subtype of Object that describes
|
|
||||||
/// some form of action that may happen,
|
|
||||||
/// is currently happening, or has already happened.
|
|
||||||
///
|
|
||||||
/// The Activity type itself serves as an abstract base type
|
|
||||||
/// for all types of activities.
|
|
||||||
/// It is important to note that the Activity type itself
|
|
||||||
/// does not carry any specific semantics about the kind of action being taken.
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct Activity {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub base: ObjectBase,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: Option<ActivityKind>,
|
|
||||||
pub id: String,
|
|
||||||
#[serde(deserialize_with = "expand_partial")]
|
|
||||||
pub actor: Option<Actor>,
|
|
||||||
#[serde(deserialize_with = "expand_partial")]
|
|
||||||
pub object: Option<Object>,
|
|
||||||
pub result: Option<APResult>,
|
|
||||||
pub target: Option<Basic>,
|
|
||||||
pub origin: Option<Basic>,
|
|
||||||
pub instrument: Option<Basic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Instances of IntransitiveActivity are a subtype of Activity
|
|
||||||
/// representing intransitive actions.
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct IntransitiveActivity {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub base: ObjectBase,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: Option<ActivityKind>,
|
|
||||||
pub id: String,
|
|
||||||
#[serde(deserialize_with = "expand_partial")]
|
|
||||||
pub actor: Option<Actor>,
|
|
||||||
pub result: Option<APResult>,
|
|
||||||
pub target: Option<Basic>,
|
|
||||||
pub origin: Option<Basic>,
|
|
||||||
pub instrument: Option<Basic>,
|
|
||||||
}
|
|
||||||
pub enum ActorType {
|
|
||||||
Application,
|
|
||||||
Group,
|
|
||||||
Organization,
|
|
||||||
Person,
|
|
||||||
Service,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Basic {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: Option<ActivityKind>,
|
|
||||||
pub name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, LD, Default)]
|
|
||||||
pub struct Actor {
|
|
||||||
pub id: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub kind: Option<ActivityKind>,
|
|
||||||
#[serde(rename = "attributedTo")]
|
|
||||||
pub attributed_to: Option<String>,
|
|
||||||
pub published: Option<String>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
pub context: Option<String>,
|
|
||||||
pub conversation: Option<String>,
|
|
||||||
pub url: Option<String>,
|
|
||||||
#[serde(rename = "to")]
|
|
||||||
#[serde(deserialize_with = "into_vec")]
|
|
||||||
pub to_uris: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn test() {
|
|
||||||
let obj = r#"{
|
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
|
||||||
"attributedTo": "http://localhost:3001/u/test",
|
|
||||||
"content": "honk donk",
|
|
||||||
"context": "data:,electrichonkytonk-2jqQ42HyJXctnBKTy1",
|
|
||||||
"conversation": "data:,electrichonkytonk-2jqQ42HyJXctnBKTy1",
|
|
||||||
"id": "htts://localhost:3001/u/test/h/6Q8BFF8W6PZT2ddngZ",
|
|
||||||
"published": "2022-09-30T19:04:45Z",
|
|
||||||
"summary": "",
|
|
||||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
|
||||||
"type": "Note",
|
|
||||||
"url": "https://localhost/u/test/h/6Q8BFF8W6PZT2ddngZ"
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
let obj = serde_json::from_str::<Object>(obj).unwrap();
|
|
||||||
println!();
|
|
||||||
println!("{:#?}", obj);
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
use std::{future::Future, string::FromUtf8Error};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use reqwest::{Method, StatusCode};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use super::serde_ext::LDObject;
|
|
||||||
|
|
||||||
const LD_CONTENT_TYPE: &str = "application/ld+json";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub(super) trait Resolvable: Sized {
|
|
||||||
async fn resolve(&self, resolver: &Resolver) -> Result<Self, ResolveError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T> Resolvable for T
|
|
||||||
where
|
|
||||||
T: LDObject + for<'de> Deserialize<'de> + Sync,
|
|
||||||
{
|
|
||||||
async fn resolve(&self, resolver: &Resolver) -> Result<Self, ResolveError> {
|
|
||||||
resolver.resolve_into(self.get_iri()).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct ResolveError(pub String);
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for ResolveError {
|
|
||||||
fn from(err: reqwest::Error) -> Self {
|
|
||||||
Self(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<FromUtf8Error> for ResolveError {
|
|
||||||
fn from(e: FromUtf8Error) -> Self {
|
|
||||||
Self(format!(
|
|
||||||
"invalid schema format (tried utf8): {}",
|
|
||||||
e.to_string()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) struct Resolver {
|
|
||||||
client: reqwest::Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resolver {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
client: reqwest::ClientBuilder::new().build().unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get<Out, F, Fut>(&self, iri: String, ok: F) -> Result<Out, ResolveError>
|
|
||||||
where
|
|
||||||
F: FnOnce(reqwest::Response) -> Fut,
|
|
||||||
Fut: Future<Output = Result<Out, ResolveError>>,
|
|
||||||
{
|
|
||||||
let resp = self
|
|
||||||
.client
|
|
||||||
.request(Method::GET, iri)
|
|
||||||
.header("Accept", LD_CONTENT_TYPE)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match resp.status() {
|
|
||||||
StatusCode::OK => Ok(ok(resp).await?),
|
|
||||||
status => Err(ResolveError(format!("non-ok status: {}", status))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn resolve_into<'a, T>(&self, iri: String) -> Result<T, ResolveError>
|
|
||||||
where
|
|
||||||
T: for<'de> serde::Deserialize<'de>,
|
|
||||||
{
|
|
||||||
Ok(self
|
|
||||||
.get(iri, |resp| async { Ok(resp.json().await?) })
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn resolve(&self, iri: String) -> Result<Vec<u8>, ResolveError> {
|
|
||||||
Ok(self
|
|
||||||
.get(iri, |resp| async { Ok(resp.bytes().await?.to_vec()) })
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,199 +0,0 @@
|
||||||
use std::{fmt, marker::PhantomData};
|
|
||||||
|
|
||||||
use serde::{
|
|
||||||
de::{self, MapAccess, Visitor},
|
|
||||||
Deserialize, Deserializer,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub trait LDObject {
|
|
||||||
fn from_iri(iri: &str) -> Self;
|
|
||||||
fn get_iri(&self) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> LDObject for Option<T>
|
|
||||||
where
|
|
||||||
T: LDObject,
|
|
||||||
{
|
|
||||||
fn from_iri(iri: &str) -> Self {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_iri(&self) -> String {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows a value that's a string to be expanded into an object (move string into id property via From<IRI>)
|
|
||||||
// AND the serialization of that object itself
|
|
||||||
pub fn expand_partial<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
|
||||||
where
|
|
||||||
T: Deserialize<'de> + LDObject,
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct BaseExpander<T>(PhantomData<fn() -> T>);
|
|
||||||
|
|
||||||
impl<'de, T> Visitor<'de> for BaseExpander<T>
|
|
||||||
where
|
|
||||||
T: Deserialize<'de> + LDObject,
|
|
||||||
{
|
|
||||||
type Value = T;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
formatter.write_str("string or map")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<T, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
Ok(LDObject::from_iri(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<M>(self, map: M) -> Result<T, M::Error>
|
|
||||||
where
|
|
||||||
M: MapAccess<'de>,
|
|
||||||
{
|
|
||||||
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(BaseExpander(PhantomData))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn expand_partial_into_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
|
|
||||||
where
|
|
||||||
T: Deserialize<'de> + LDObject,
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct BaseExpander<T>(PhantomData<fn() -> T>);
|
|
||||||
|
|
||||||
impl<'de, T> Visitor<'de> for BaseExpander<T>
|
|
||||||
where
|
|
||||||
T: Deserialize<'de> + LDObject,
|
|
||||||
{
|
|
||||||
type Value = Vec<T>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
formatter.write_str("string or map")
|
|
||||||
}
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
Ok(vec![LDObject::from_iri(value)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
|
|
||||||
where
|
|
||||||
M: MapAccess<'de>,
|
|
||||||
{
|
|
||||||
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(|v| vec![v])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::de::SeqAccess<'de>,
|
|
||||||
{
|
|
||||||
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(visitor))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(BaseExpander(PhantomData))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows deserialization of a single item into a vector of that item
|
|
||||||
// As long as they implement the From<String> trait
|
|
||||||
pub(super) fn into_vec<'de, D, Out>(deserializer: D) -> Result<Vec<Out>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
Out: serde::Deserialize<'de>,
|
|
||||||
{
|
|
||||||
struct VecVisitor<Out>(PhantomData<Vec<Out>>);
|
|
||||||
|
|
||||||
impl<'de, Out> Visitor<'de> for VecVisitor<Out>
|
|
||||||
where
|
|
||||||
Out: serde::Deserialize<'de>,
|
|
||||||
{
|
|
||||||
type Value = Vec<Out>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let name = std::any::type_name::<Out>();
|
|
||||||
formatter.write_str(format!("{} or Vec<{}>", name, name).as_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
Deserialize::deserialize(de::value::StrDeserializer::new(value)).map(|v| vec![v])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: MapAccess<'de>,
|
|
||||||
{
|
|
||||||
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(|v| vec![v])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::de::SeqAccess<'de>,
|
|
||||||
{
|
|
||||||
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(visitor))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(VecVisitor(PhantomData))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use flabk_derive::LD;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use super::expand_partial;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn expand_partial_populates_iri_from_string() {
|
|
||||||
#[derive(Deserialize, LD, Default)]
|
|
||||||
struct Context {
|
|
||||||
pub id: String,
|
|
||||||
pub context: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct WithContext {
|
|
||||||
#[serde(deserialize_with = "expand_partial")]
|
|
||||||
pub context: Context,
|
|
||||||
}
|
|
||||||
|
|
||||||
const JSONLD_INPUT: &str = r#"{"context": "https://www.w3.org/ns/activitystreams"}"#;
|
|
||||||
let result =
|
|
||||||
serde_json::from_str::<WithContext>(JSONLD_INPUT).expect("deserializing with expand");
|
|
||||||
|
|
||||||
assert!(result.context.id == "https://www.w3.org/ns/activitystreams");
|
|
||||||
assert!(result.context.context == false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn expand_partial_expands_into_object_fully() {
|
|
||||||
#[derive(Deserialize, LD, Default)]
|
|
||||||
struct Expanded {
|
|
||||||
pub id: String,
|
|
||||||
pub truth: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Expandable {
|
|
||||||
#[serde(deserialize_with = "expand_partial")]
|
|
||||||
pub expansive: Expanded,
|
|
||||||
}
|
|
||||||
|
|
||||||
const JSONLD_INPUT: &str = r#"{"expansive": { "id": "1", "truth": true }}"#;
|
|
||||||
let result =
|
|
||||||
serde_json::from_str::<Expandable>(JSONLD_INPUT).expect("deserializing with expand");
|
|
||||||
|
|
||||||
assert!(result.expansive.id == "1");
|
|
||||||
assert!(result.expansive.truth);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use super::{keys::Keys, users::Users};
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tokio_postgres::{Client, NoTls};
|
|
||||||
|
|
||||||
const DBERR_UNIQUE: &str = "23505";
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct DB {
|
|
||||||
client: Arc<Mutex<Client>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DB {
|
|
||||||
pub async fn new(host: String, user: String, database: String) -> Result<Self, anyhow::Error> {
|
|
||||||
let (cl, conn) = tokio_postgres::connect(
|
|
||||||
format!("host={host} user={user} dbname={database}").as_str(),
|
|
||||||
NoTls,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = conn.await {
|
|
||||||
eprintln!("connection error: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let client = Arc::new(Mutex::new(cl));
|
|
||||||
Ok(Self { client })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn users(&self) -> Users {
|
|
||||||
Users::new(self.client.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keys(&self) -> Keys {
|
|
||||||
Keys::new(self.client.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum DBError {
|
|
||||||
Duplicate,
|
|
||||||
NotFound,
|
|
||||||
Other(tokio_postgres::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<tokio_postgres::Error> for DBError {
|
|
||||||
fn from(err: tokio_postgres::Error) -> Self {
|
|
||||||
if let Some(code) = err.code() && code.code() == DBERR_UNIQUE {
|
|
||||||
return DBError::Duplicate;
|
|
||||||
}
|
|
||||||
DBError::Other(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tokio_postgres::Client;
|
|
||||||
|
|
||||||
use super::db::DBError;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Keys(Arc<Mutex<Client>>);
|
|
||||||
impl Keys {
|
|
||||||
pub fn new(client: Arc<Mutex<Client>>) -> Self {
|
|
||||||
Self(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_key(&self, key: &str) -> Result<String, DBError> {
|
|
||||||
Ok(self
|
|
||||||
.0
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.query("select value from keys where key = $1", &[&key])
|
|
||||||
.await?
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| DBError::NotFound)?
|
|
||||||
.get("value"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_key(&self, key: &str, value: &str) -> Result<(), DBError> {
|
|
||||||
self.0
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.execute(
|
|
||||||
"insert into keys (key, value) values ($1, $2)",
|
|
||||||
&[&key, &value],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod db;
|
|
||||||
pub mod keys;
|
|
||||||
pub mod users;
|
|
|
@ -1,147 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tokio_postgres::{Client, Row};
|
|
||||||
|
|
||||||
use crate::sec;
|
|
||||||
|
|
||||||
use super::db;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Users(Arc<Mutex<Client>>);
|
|
||||||
|
|
||||||
impl Users {
|
|
||||||
pub fn new(client: Arc<Mutex<Client>>) -> Self {
|
|
||||||
Self(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_user(&self, u: User) -> Result<User, db::DBError> {
|
|
||||||
let row = self.0.lock().await.query_one(
|
|
||||||
"insert into users (id, username, host, display_name, password_hash, email) values ($1, $2, $3, $4, $5, $6) returning (id, username, host, display_name, password_hash, email, avatar_uri, bio)",
|
|
||||||
&[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash, &u.email],
|
|
||||||
).await?;
|
|
||||||
Ok(User::from(&row))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn user_stats(&self, by: UserSelect) -> Result<UserStats, db::DBError> {
|
|
||||||
let (clause, param) = by.into();
|
|
||||||
let rows = self
|
|
||||||
.0
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.query(
|
|
||||||
format!(r#"select count(follows.*) as following, count(followed.*) as followers from users
|
|
||||||
left join follows on follows.user_id = users.id
|
|
||||||
left join follows followed on followed.follows_id = users.id
|
|
||||||
where {}"#, clause).as_str(),
|
|
||||||
&[¶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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
#![feature(const_type_name)]
|
|
||||||
#![feature(let_chains)]
|
|
||||||
mod astreams;
|
|
||||||
mod database;
|
|
||||||
mod sec;
|
|
||||||
mod servek;
|
|
||||||
mod svc;
|
|
||||||
|
|
||||||
use database::db::DB;
|
|
||||||
use servek::servek::Server;
|
|
||||||
use svc::{auth::Auth, profiles::Profiler};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), anyhow::Error> {
|
|
||||||
astreams::test().await;
|
|
||||||
let db = DB::new(
|
|
||||||
"localhost".to_owned(),
|
|
||||||
"flabk".to_owned(),
|
|
||||||
"flabk".to_owned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let profiler = Profiler::new(db.users());
|
|
||||||
let auth = Auth::new(db.keys(), db.users()).await;
|
|
||||||
Server::new(profiler, auth).listen_and_serve(8008).await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
use argon2::{
|
|
||||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
|
||||||
Argon2,
|
|
||||||
};
|
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
pub fn hash(password: String) -> String {
|
|
||||||
let password = password.as_bytes();
|
|
||||||
// Hash password to PHC string ($argon2id$v=19$...)
|
|
||||||
Argon2::default()
|
|
||||||
.hash_password(password, &SaltString::generate(&mut OsRng))
|
|
||||||
.unwrap()
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compare(password: &str, password_hash: &str) -> bool {
|
|
||||||
let hash = PasswordHash::new(&password_hash).unwrap();
|
|
||||||
Argon2::default()
|
|
||||||
.verify_password(password.as_bytes(), &hash)
|
|
||||||
.is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_id() -> String {
|
|
||||||
let bytes = rand::thread_rng().gen::<[u8; 16]>();
|
|
||||||
base_62::encode(&bytes)
|
|
||||||
}
|
|
|
@ -1,332 +0,0 @@
|
||||||
use std;
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
body::Full,
|
|
||||||
extract::Path,
|
|
||||||
http::{header, HeaderValue, StatusCode},
|
|
||||||
response::{self, IntoResponse, Response},
|
|
||||||
routing, Extension, Form, Router,
|
|
||||||
};
|
|
||||||
use mime_guess::mime;
|
|
||||||
use tower_cookies::{Cookie, Cookies};
|
|
||||||
|
|
||||||
use crate::svc::auth::{AuthError, Claims};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
servek::{Server, ServerError},
|
|
||||||
CreateProfileRequest, LoginRequest, NavType, Note, Notification, Profile, Redirect, Timeline,
|
|
||||||
WithNav,
|
|
||||||
};
|
|
||||||
use rust_embed::RustEmbed;
|
|
||||||
|
|
||||||
const AUTH_COOKIE_NAME: &str = "flabk_token";
|
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
|
||||||
#[folder = "static"]
|
|
||||||
struct StaticData;
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
pub(super) fn register_html(&self, router: &Router) -> Router {
|
|
||||||
router
|
|
||||||
.clone()
|
|
||||||
.route("/favicon.svg", routing::get(Self::favicon))
|
|
||||||
.route("/", routing::get(Self::index))
|
|
||||||
.route("/login", routing::get(Self::login_page).post(Self::login))
|
|
||||||
.route("/logout", routing::get(Self::logout))
|
|
||||||
.route(
|
|
||||||
"/signup",
|
|
||||||
routing::get(Self::signup_page).post(Self::create_user),
|
|
||||||
)
|
|
||||||
.route("/@/:username", routing::get(Self::profile))
|
|
||||||
.route("/static/*file", routing::get(Self::static_handler))
|
|
||||||
.fallback(routing::get(Self::handler_404))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_auth(&self, cookies: Cookies) -> Result<Claims, ServerError> {
|
|
||||||
match cookies.get(AUTH_COOKIE_NAME) {
|
|
||||||
Some(cookie) => self
|
|
||||||
.auth
|
|
||||||
.get_claims(cookie.value().to_owned())
|
|
||||||
.map_err(|e| {
|
|
||||||
if e.expired() {
|
|
||||||
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
|
||||||
}
|
|
||||||
e.into()
|
|
||||||
}),
|
|
||||||
None => Err(ServerError::NotLoggedIn),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_cookies(&self, cookies: Cookies) -> Result<WithNav<Option<Claims>>, ServerError> {
|
|
||||||
const LOGGED_OUT: Result<WithNav<Option<Claims>>, ServerError> = Ok(WithNav {
|
|
||||||
obj: None,
|
|
||||||
nav_type: NavType::LoggedOut,
|
|
||||||
});
|
|
||||||
if let Some(cookie) = cookies.get(AUTH_COOKIE_NAME) {
|
|
||||||
let claims = match self.auth.get_claims(cookie.value().to_owned()) {
|
|
||||||
Ok(claims) => claims,
|
|
||||||
Err(e) => {
|
|
||||||
if e.clone().expired() {
|
|
||||||
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
|
||||||
return LOGGED_OUT;
|
|
||||||
} else {
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(WithNav::new(Some(claims.clone()), claims.into()))
|
|
||||||
} else {
|
|
||||||
LOGGED_OUT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn index(
|
|
||||||
Extension(srv): Extension<Server>,
|
|
||||||
cookies: Cookies,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
// TODO: show actual posts, these are hardcoded currently to check
|
|
||||||
// out how they look
|
|
||||||
let profile1 = Box::new(Profile {
|
|
||||||
username: "@emilis@puff.place".to_string(),
|
|
||||||
display_name: "dusty rusty".to_string(),
|
|
||||||
avatar_url: "https://puff.place/files/thumbnail-501ec5ba-37d6-4163-8e8d-3478a689660d"
|
|
||||||
.to_string(),
|
|
||||||
});
|
|
||||||
let profile2 = Box::new(Profile::new("@emilk@another.place".to_string()));
|
|
||||||
let note1 = Note::new(profile1.clone(), Some("hey there goods".to_string()));
|
|
||||||
let note2 = Note::new(
|
|
||||||
profile2,
|
|
||||||
Some("@emilis@puff.place u dont got shit".to_string()),
|
|
||||||
);
|
|
||||||
let note3 = Note::new(profile1, Some("bithces :".to_string()));
|
|
||||||
|
|
||||||
match srv.get_auth(cookies) {
|
|
||||||
Ok(claims) => Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
response::Html(srv.hb.render(
|
|
||||||
"index",
|
|
||||||
&WithNav::new(
|
|
||||||
Timeline {
|
|
||||||
show_postbox: true,
|
|
||||||
notes: vec![note1, note2, note3],
|
|
||||||
},
|
|
||||||
NavType::LoggedIn,
|
|
||||||
),
|
|
||||||
)?),
|
|
||||||
)
|
|
||||||
.into_response()),
|
|
||||||
Err(e) => match e {
|
|
||||||
ServerError::NotLoggedIn => Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
response::Html(
|
|
||||||
srv.hb
|
|
||||||
.render("index", &WithNav::new((), NavType::LoggedOut))?,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into_response()),
|
|
||||||
_ => Err(e),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn favicon() -> impl IntoResponse {
|
|
||||||
(
|
|
||||||
StatusCode::OK,
|
|
||||||
(
|
|
||||||
[(
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
HeaderValue::from_static(mime::IMAGE_SVG.as_ref()),
|
|
||||||
)],
|
|
||||||
axum::body::boxed(Full::from(
|
|
||||||
include_bytes!("../../flabk_icon_placeholder.svg").as_ref(),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handler_404(
|
|
||||||
Extension(srv): Extension<Server>,
|
|
||||||
cookies: Cookies,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
Ok((
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
response::Html(srv.hb.render("err404", &srv.from_cookies(cookies)?)?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn logout(
|
|
||||||
Extension(srv): Extension<Server>,
|
|
||||||
cookies: Cookies,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
|
||||||
Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
response::Html(srv.hb.render(
|
|
||||||
"redirect",
|
|
||||||
&Redirect {
|
|
||||||
location: "/".to_owned(),
|
|
||||||
},
|
|
||||||
)?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login_page_with(
|
|
||||||
&self,
|
|
||||||
tag_name: String,
|
|
||||||
message: String,
|
|
||||||
) -> Result<response::Html<String>, ServerError> {
|
|
||||||
Ok(self
|
|
||||||
.hb
|
|
||||||
.render(
|
|
||||||
"login",
|
|
||||||
&serde_json::json!(Notification { message, tag_name }),
|
|
||||||
)
|
|
||||||
.map(|html| response::Html(html))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn login_page(
|
|
||||||
Extension(srv): Extension<Server>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
response::Html(srv.hb.render("login", &serde_json::json!(()))?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn login(
|
|
||||||
cookies: Cookies,
|
|
||||||
Extension(srv): Extension<Server>,
|
|
||||||
Form(login): Form<LoginRequest>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
if login.username == "" || login.password == "" {
|
|
||||||
return Ok((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
srv.login_page_with(
|
|
||||||
"error-partial".to_owned(),
|
|
||||||
"credentials required".to_owned(),
|
|
||||||
)?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let token = match srv.auth.login(login.username, login.password).await {
|
|
||||||
Ok(token) => token,
|
|
||||||
Err(e) => match e {
|
|
||||||
AuthError::InvalidCredentials => {
|
|
||||||
return Ok((
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
srv.login_page_with(
|
|
||||||
"error-partial".to_owned(),
|
|
||||||
"invalid credentials".to_owned(),
|
|
||||||
)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
e => return Err(e.into()),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if cookies.get(AUTH_COOKIE_NAME).is_some() {
|
|
||||||
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
|
|
||||||
}
|
|
||||||
cookies.add(Cookie::new(AUTH_COOKIE_NAME, token));
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
response::Html(srv.hb.render(
|
|
||||||
"redirect",
|
|
||||||
&Redirect {
|
|
||||||
location: "/".to_owned(),
|
|
||||||
},
|
|
||||||
)?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn profile(
|
|
||||||
cookies: Cookies,
|
|
||||||
Extension(srv): Extension<Server>,
|
|
||||||
Path(username): Path<String>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
response::Html(srv.hb.render(
|
|
||||||
"profile",
|
|
||||||
&WithNav::new(
|
|
||||||
srv.profiler.profile(username).await?,
|
|
||||||
srv.from_cookies(cookies)?.nav_type,
|
|
||||||
),
|
|
||||||
)?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_user(
|
|
||||||
Extension(srv): Extension<Server>,
|
|
||||||
Form(body): Form<CreateProfileRequest>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
if body.username == "" {
|
|
||||||
return srv.signup_page_with("error-partial".to_owned(), "empty username".to_owned());
|
|
||||||
}
|
|
||||||
if body.password == "" {
|
|
||||||
return srv.signup_page_with("error-partial".to_owned(), "empty password".to_owned());
|
|
||||||
}
|
|
||||||
if body.email == "" {
|
|
||||||
return srv.signup_page_with("error-partial".to_owned(), "empty email".to_owned());
|
|
||||||
}
|
|
||||||
srv.profiler
|
|
||||||
.create_user(body.username, body.password, body.email)
|
|
||||||
.await?;
|
|
||||||
srv.signup_page_with("success-partial".to_owned(), "signup successful".to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn signup_page(
|
|
||||||
Extension(srv): Extension<Server>,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
response::Html(srv.hb.render("signup", &serde_json::json!(()))?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signup_page_with(
|
|
||||||
&self,
|
|
||||||
tag_name: String,
|
|
||||||
message: String,
|
|
||||||
) -> Result<impl IntoResponse, ServerError> {
|
|
||||||
Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
response::Html(self.hb.render(
|
|
||||||
"signup",
|
|
||||||
&serde_json::json!(Notification { message, tag_name }),
|
|
||||||
)?),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn static_handler(Path(mut path): Path<String>) -> impl IntoResponse {
|
|
||||||
path.remove(0);
|
|
||||||
println!("getting path: {}", path);
|
|
||||||
StaticFile(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct StaticFile<T>(pub T);
|
|
||||||
|
|
||||||
impl<T> IntoResponse for StaticFile<T>
|
|
||||||
where
|
|
||||||
T: Into<String>,
|
|
||||||
{
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
let path = self.0.into();
|
|
||||||
|
|
||||||
match StaticData::get(path.as_str()) {
|
|
||||||
Some(content) => {
|
|
||||||
let body = axum::body::boxed(Full::from(content.data));
|
|
||||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
|
||||||
Response::builder()
|
|
||||||
.header(header::CONTENT_TYPE, mime.as_ref())
|
|
||||||
.body(body)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
None => Response::builder()
|
|
||||||
.status(StatusCode::NOT_FOUND)
|
|
||||||
.body(axum::body::boxed(Full::from("404")))
|
|
||||||
.unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
use axum::response::{Html, IntoResponse};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tower_cookies::{Cookie, Cookies};
|
|
||||||
|
|
||||||
use crate::svc::auth::{AuthError, Claims};
|
|
||||||
|
|
||||||
mod html;
|
|
||||||
pub mod servek;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
struct Timeline {
|
|
||||||
pub show_postbox: bool,
|
|
||||||
pub notes: Vec<Note>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
struct Note {
|
|
||||||
pub profile: Box<Profile>,
|
|
||||||
pub content: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Note {
|
|
||||||
fn new(profile: Box<Profile>, content: Option<String>) -> Self {
|
|
||||||
Self { profile, content }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
struct Profile {
|
|
||||||
pub display_name: String,
|
|
||||||
pub username: String,
|
|
||||||
pub avatar_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Profile {
|
|
||||||
fn new(username: String) -> Self {
|
|
||||||
let display_name = username
|
|
||||||
.strip_prefix('@')
|
|
||||||
.unwrap()
|
|
||||||
.split_once('@')
|
|
||||||
.unwrap()
|
|
||||||
.0
|
|
||||||
.to_string();
|
|
||||||
Profile {
|
|
||||||
display_name,
|
|
||||||
username,
|
|
||||||
avatar_url: "/favicon.svg".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
struct Notification {
|
|
||||||
pub message: String,
|
|
||||||
pub tag_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct LoginRequest {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct CreateProfileRequest {
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub enum HtmlOrRedirect {
|
|
||||||
// Html(Html<String>),
|
|
||||||
// Redirect(()),
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct Redirect {
|
|
||||||
pub location: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub enum NavType {
|
|
||||||
LoggedIn,
|
|
||||||
LoggedOut,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Result<Claims, AuthError>> for NavType {
|
|
||||||
fn from(claims: Result<Claims, AuthError>) -> Self {
|
|
||||||
if claims.map(|c| c.expired()).unwrap_or(true) {
|
|
||||||
NavType::LoggedOut
|
|
||||||
} else {
|
|
||||||
NavType::LoggedIn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Claims> for NavType {
|
|
||||||
fn from(c: Claims) -> Self {
|
|
||||||
if c.expired() {
|
|
||||||
NavType::LoggedOut
|
|
||||||
} else {
|
|
||||||
NavType::LoggedIn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct WithNav<T> {
|
|
||||||
pub nav_type: NavType,
|
|
||||||
pub obj: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> WithNav<T> {
|
|
||||||
pub fn new(obj: T, nav_type: NavType) -> WithNav<T> {
|
|
||||||
WithNav { nav_type, obj }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Into<serde_json::Value> for WithNav<T>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
fn into(self) -> serde_json::Value {
|
|
||||||
serde_json::json!(self)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,133 +0,0 @@
|
||||||
use core::panic;
|
|
||||||
use std::{fmt::Display, net::SocketAddr};
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
http::StatusCode,
|
|
||||||
response::{self, IntoResponse},
|
|
||||||
Extension, Router,
|
|
||||||
};
|
|
||||||
use handlebars::{Handlebars, RenderError};
|
|
||||||
use tower_cookies::CookieManagerLayer;
|
|
||||||
|
|
||||||
use crate::svc::{
|
|
||||||
auth::{Auth, AuthError},
|
|
||||||
profiles::{Profiler, UserError},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Server {
|
|
||||||
pub(super) hb: Handlebars<'static>,
|
|
||||||
pub(super) profiler: Profiler,
|
|
||||||
pub(super) auth: Auth,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
pub fn new(profiler: Profiler, auth: Auth) -> Self {
|
|
||||||
let mut hb = Handlebars::new();
|
|
||||||
hb.register_template_string("profile", include_str!("../../templates/html/profile.html"))
|
|
||||||
.expect("profile template");
|
|
||||||
hb.register_template_string("login", include_str!("../../templates/html/login.html"))
|
|
||||||
.expect("login template");
|
|
||||||
hb.register_template_string("signup", include_str!("../../templates/html/signup.html"))
|
|
||||||
.expect("login template");
|
|
||||||
hb.register_template_string(
|
|
||||||
"redirect",
|
|
||||||
include_str!("../../templates/html/html-redirect.html"),
|
|
||||||
)
|
|
||||||
.expect("redirect template");
|
|
||||||
hb.register_template_string("error-partial", r#"<h2 class="error">{{message}}</h2>"#)
|
|
||||||
.expect("error-partial");
|
|
||||||
hb.register_template_string("success-partial", r#"<h2 class="success">{{message}}</h2>"#)
|
|
||||||
.expect("success-partial");
|
|
||||||
hb.register_template_string("index", include_str!("../../templates/html/index.html"))
|
|
||||||
.expect("index");
|
|
||||||
hb.register_template_string("err404", include_str!("../../templates/html/404.html"))
|
|
||||||
.expect("err404");
|
|
||||||
hb.register_partial(
|
|
||||||
"LoggedOut",
|
|
||||||
include_str!("../../templates/html/nav-loggedout.html"),
|
|
||||||
)
|
|
||||||
.expect("LoggedOut");
|
|
||||||
hb.register_partial(
|
|
||||||
"LoggedIn",
|
|
||||||
include_str!("../../templates/html/nav-loggedin.html"),
|
|
||||||
)
|
|
||||||
.expect("LoggedIn");
|
|
||||||
Self { hb, profiler, auth }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn listen_and_serve(self, port: u16) -> ! {
|
|
||||||
let router = Router::new();
|
|
||||||
let router = self
|
|
||||||
.register_html(&router)
|
|
||||||
.layer(Extension::<Server>(self.clone()))
|
|
||||||
.layer(CookieManagerLayer::new());
|
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
|
||||||
println!("listening on {}", addr);
|
|
||||||
|
|
||||||
axum::Server::bind(&addr)
|
|
||||||
.serve(router.into_make_service())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
panic!("server stopped prematurely")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(super) enum ServerError {
|
|
||||||
Internal(String),
|
|
||||||
NotFound,
|
|
||||||
NotLoggedIn,
|
|
||||||
BadRequest(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RenderError> for ServerError {
|
|
||||||
fn from(r: RenderError) -> Self {
|
|
||||||
Self::Internal(r.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UserError> for ServerError {
|
|
||||||
fn from(u: UserError) -> Self {
|
|
||||||
match u {
|
|
||||||
UserError::Duplicate => Self::BadRequest("duplicate entry exists".to_owned()),
|
|
||||||
UserError::NotFound => Self::NotFound,
|
|
||||||
UserError::Other(o) => Self::Internal(format!("UserError: {}", o)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<AuthError> for ServerError {
|
|
||||||
fn from(a: AuthError) -> Self {
|
|
||||||
match a {
|
|
||||||
AuthError::InvalidCredentials => {
|
|
||||||
ServerError::BadRequest("invalid credentials".to_owned())
|
|
||||||
}
|
|
||||||
AuthError::ServerError(err) => ServerError::Internal(err),
|
|
||||||
AuthError::Expired => ServerError::BadRequest("expired token".to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ServerError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for ServerError {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
match self {
|
|
||||||
ServerError::Internal(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(),
|
|
||||||
ServerError::NotFound => (
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
response::Html(include_str!("../../templates/html/404.html")),
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
ServerError::BadRequest(err) => (StatusCode::BAD_REQUEST, err).into_response(),
|
|
||||||
ServerError::NotLoggedIn => (StatusCode::UNAUTHORIZED, "unauthorized").into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,124 +0,0 @@
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tower_cookies::Cookie;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::{
|
|
||||||
db::DBError,
|
|
||||||
keys::Keys,
|
|
||||||
users::{self, UserSelect, Users},
|
|
||||||
},
|
|
||||||
sec,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Auth {
|
|
||||||
secret: String,
|
|
||||||
users: Users,
|
|
||||||
}
|
|
||||||
|
|
||||||
const KEY_JWT_SECRET: &str = "JWT_SECRET";
|
|
||||||
const SECS_JWT_EXPIRE: u64 = 60 * 60; // 1hr
|
|
||||||
|
|
||||||
impl Auth {
|
|
||||||
pub async fn new(db: Keys, users: Users) -> Self {
|
|
||||||
Self {
|
|
||||||
secret: match db.get_key(KEY_JWT_SECRET).await {
|
|
||||||
Ok(secret) => secret,
|
|
||||||
Err(_) => {
|
|
||||||
// Create new secret and store to db
|
|
||||||
// If that fails, crash the application
|
|
||||||
let secret = sec::new_id();
|
|
||||||
db.set_key(KEY_JWT_SECRET, &secret).await.unwrap();
|
|
||||||
secret
|
|
||||||
}
|
|
||||||
},
|
|
||||||
users,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login(&self, username: String, password: String) -> Result<String, AuthError> {
|
|
||||||
let user = self.users.user(UserSelect::Username(username)).await?;
|
|
||||||
if !sec::compare(&password, &user.password_hash) {
|
|
||||||
return Err(AuthError::InvalidCredentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(jsonwebtoken::encode(
|
|
||||||
&Header::default(),
|
|
||||||
&Claims::from(user),
|
|
||||||
&EncodingKey::from_secret(self.secret.as_ref()),
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_claims(&self, token: String) -> Result<Claims, AuthError> {
|
|
||||||
Ok(jsonwebtoken::decode::<Claims>(
|
|
||||||
token.as_str(),
|
|
||||||
&DecodingKey::from_secret(self.secret.as_ref()),
|
|
||||||
&Validation::new(Algorithm::HS256),
|
|
||||||
)?
|
|
||||||
.claims)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Claims {
|
|
||||||
pub sub: String,
|
|
||||||
pub username: String,
|
|
||||||
pub exp: u64,
|
|
||||||
pub iat: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Claims {
|
|
||||||
pub fn expired(&self) -> bool {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs()
|
|
||||||
>= self.exp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<users::User> for Claims {
|
|
||||||
fn from(u: users::User) -> Self {
|
|
||||||
let now = SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
Claims {
|
|
||||||
sub: u.id,
|
|
||||||
username: u.username,
|
|
||||||
exp: now + SECS_JWT_EXPIRE,
|
|
||||||
iat: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum AuthError {
|
|
||||||
InvalidCredentials,
|
|
||||||
Expired,
|
|
||||||
ServerError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthError {
|
|
||||||
pub fn expired(&self) -> bool {
|
|
||||||
*self == Self::Expired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DBError> for AuthError {
|
|
||||||
fn from(_: DBError) -> Self {
|
|
||||||
Self::InvalidCredentials
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<jsonwebtoken::errors::Error> for AuthError {
|
|
||||||
fn from(e: jsonwebtoken::errors::Error) -> Self {
|
|
||||||
match e.kind() {
|
|
||||||
jsonwebtoken::errors::ErrorKind::ExpiredSignature => Self::Expired,
|
|
||||||
kind => Self::ServerError(e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod auth;
|
|
||||||
pub mod profiles;
|
|
|
@ -1,148 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::{
|
|
||||||
db,
|
|
||||||
users::{self, UserSelect, Users},
|
|
||||||
},
|
|
||||||
sec,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Profiler {
|
|
||||||
db: Users,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Profiler {
|
|
||||||
pub fn new(db: Users) -> Self {
|
|
||||||
Self { db }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn user(&self, username: String) -> Result<User, UserError> {
|
|
||||||
Ok(self.db.user(username.into()).await?.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn stats(&self, username: String) -> Result<UserStats, UserError> {
|
|
||||||
Ok(self.db.user_stats(username.into()).await?.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn profile(&self, username: String) -> Result<Profile, UserError> {
|
|
||||||
Ok((
|
|
||||||
self.user(username.clone()).await?,
|
|
||||||
self.stats(username).await?,
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_user(
|
|
||||||
&self,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
email: String,
|
|
||||||
) -> Result<User, UserError> {
|
|
||||||
let result = self
|
|
||||||
.db
|
|
||||||
.create_user(users::User {
|
|
||||||
id: String::new(),
|
|
||||||
username,
|
|
||||||
host: None,
|
|
||||||
display_name: None,
|
|
||||||
password_hash: sec::hash(password),
|
|
||||||
email,
|
|
||||||
avatar_uri: None,
|
|
||||||
bio: None,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(User::from(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub struct User {
|
|
||||||
pub id: String,
|
|
||||||
pub username: String,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
pub host: Option<String>,
|
|
||||||
pub avatar_uri: Option<String>,
|
|
||||||
pub bio: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct UserStats {
|
|
||||||
pub post_count: i64,
|
|
||||||
pub following: i64,
|
|
||||||
pub followers: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<users::UserStats> for UserStats {
|
|
||||||
fn from(u: users::UserStats) -> Self {
|
|
||||||
Self {
|
|
||||||
post_count: u.post_count,
|
|
||||||
following: u.following,
|
|
||||||
followers: u.followers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct Profile {
|
|
||||||
pub user: User,
|
|
||||||
pub stats: UserStats,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(User, UserStats)> for Profile {
|
|
||||||
fn from((user, stats): (User, UserStats)) -> Self {
|
|
||||||
Self {
|
|
||||||
user: user.into(),
|
|
||||||
stats: stats.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<users::User> for User {
|
|
||||||
fn from(u: users::User) -> Self {
|
|
||||||
Self {
|
|
||||||
id: u.id,
|
|
||||||
display_name: u.display_name.or(Some(format!(
|
|
||||||
"@{}{}",
|
|
||||||
u.username,
|
|
||||||
u.host
|
|
||||||
.clone()
|
|
||||||
.map(|h| format!("@{}", h))
|
|
||||||
.unwrap_or(String::new()),
|
|
||||||
))),
|
|
||||||
username: u.username,
|
|
||||||
host: u.host,
|
|
||||||
avatar_uri: u.avatar_uri,
|
|
||||||
bio: u.bio,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum UserError {
|
|
||||||
Duplicate,
|
|
||||||
NotFound,
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<db::DBError> for UserError {
|
|
||||||
fn from(err: db::DBError) -> Self {
|
|
||||||
match err {
|
|
||||||
db::DBError::Duplicate => Self::Duplicate,
|
|
||||||
db::DBError::NotFound => Self::NotFound,
|
|
||||||
db::DBError::Other(e) => Self::Other(e.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for UserError {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Duplicate => String::from("duplicate insert"),
|
|
||||||
Self::NotFound => String::from("not found"),
|
|
||||||
Self::Other(err) => err.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,242 +0,0 @@
|
||||||
body {
|
|
||||||
background-color: black;
|
|
||||||
color: rebeccapurple;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
label {
|
|
||||||
font-size: 160%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .big {
|
|
||||||
font-size: 200%;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.big>input {
|
|
||||||
font-size: 180%;
|
|
||||||
margin: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
input {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
border-color: rebeccapurple;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: rgba(255, 0, 0, 0.7);
|
|
||||||
font-weight: bold;
|
|
||||||
border: 5px solid rgba(99, 0, 0, 0.2);
|
|
||||||
background-color: rgba(95, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
color: rgba(0, 202, 0, 0.7);
|
|
||||||
font-weight: bold;
|
|
||||||
border: 5px solid rgba(0, 99, 0, 0.2);
|
|
||||||
background-color: rgba(0, 99, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#central {
|
|
||||||
width: 40%;
|
|
||||||
border: 5px solid rebeccapurple;
|
|
||||||
height: 30vw;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: rgba(13, 6, 19, 0.4);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: rebeccapurple;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 85vw;
|
|
||||||
border: 5px solid rebeccapurple;
|
|
||||||
height: 85vh;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
background-color: rgba(13, 6, 19, 0.4);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-avatar {
|
|
||||||
width: 128px;
|
|
||||||
height: 128px;
|
|
||||||
border: 3px solid rebeccapurple;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 110%;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile {
|
|
||||||
/* border: 3px solid rebeccapurple; */
|
|
||||||
padding: 5px;
|
|
||||||
display: flex;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-header {
|
|
||||||
text-align: left;
|
|
||||||
width: 128px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-bio {
|
|
||||||
border: 3px solid rgba(102, 51, 153, 0.5);
|
|
||||||
margin-left: 5px;
|
|
||||||
text-align: center;
|
|
||||||
height: fit-content;
|
|
||||||
height: -moz-fit-content;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats>div {
|
|
||||||
border: 3px solid rgba(102, 51, 153, 0.5);
|
|
||||||
height: fit-content;
|
|
||||||
height: -moz-fit-content;
|
|
||||||
text-align: center;
|
|
||||||
width: 33%;
|
|
||||||
float: left;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats {
|
|
||||||
height: fit-content;
|
|
||||||
margin: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
width: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
overflow: hidden;
|
|
||||||
border: 5px solid rgba(102, 51, 153, 0.5);
|
|
||||||
width: 85vw;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
background-color: rgba(13, 6, 19, 0.4);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li {
|
|
||||||
display: inline-block;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav>ul>li>a {
|
|
||||||
display: block;
|
|
||||||
line-height: 2em;
|
|
||||||
padding: 0.5em 0.5em;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: rgba(102, 51, 153, 0.75);
|
|
||||||
color: rgb(204, 179, 230);
|
|
||||||
border-color: rgba(102, 51, 153, 0.25);
|
|
||||||
border-style: initial;
|
|
||||||
font-size: large;
|
|
||||||
}
|
|
||||||
|
|
||||||
#postbox {
|
|
||||||
/* border: 3px solid rebeccapurple; */
|
|
||||||
/* padding: 5px; */
|
|
||||||
margin: 0 0 30px 0;
|
|
||||||
height: fit-content;
|
|
||||||
display: flex;
|
|
||||||
/* text-align: left; */
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline li {
|
|
||||||
display: grid;
|
|
||||||
list-style-type: none;
|
|
||||||
width: 85vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
#post-content {
|
|
||||||
color: rgb(204, 179, 230);
|
|
||||||
background-color: rebeccapurple;
|
|
||||||
border-width: 0px;
|
|
||||||
height: 60px;
|
|
||||||
width: 85vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
div>p {
|
|
||||||
font-size: small;
|
|
||||||
margin-left: 2px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
color: rgba(102, 51, 153, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-box {
|
|
||||||
color: rgb(204, 179, 230);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note:nth-child(even) {
|
|
||||||
background: rgba(102, 51, 153, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note:nth-child(odd) {
|
|
||||||
background: rgba(102, 51, 153, 0.125);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>flabk - not found</title>
|
|
||||||
<link rel="stylesheet" href="/static/style/main.css">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{> (lookup this "nav_type") }}
|
|
||||||
<h1>404 not found</h1>
|
|
||||||
<br />
|
|
||||||
<h1><a href="/">return</a></h1>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,18 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>redirecting</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<meta http-equiv="refresh" content="time; URL={{location}}" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,36 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>flabk</title>
|
|
||||||
<link rel="stylesheet" href="/static/style/main.css">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{> (lookup this "nav_type") }}
|
|
||||||
<div class="main" id="timeline-box">
|
|
||||||
{{#if obj.show_postbox}}
|
|
||||||
<div id="postbox">
|
|
||||||
<textarea id="post-content" placeholder="im tosti"></textarea>
|
|
||||||
<button>send</button>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
<div id="timeline">
|
|
||||||
{{#each obj.notes}}
|
|
||||||
<div class="note">
|
|
||||||
<img class="avatar" src="{{this.profile.avatar_url}}" alt="example" />
|
|
||||||
<div class="content">
|
|
||||||
<div class="user-info">
|
|
||||||
<p class="display-name">{{this.profile.display_name}}</p>
|
|
||||||
<p class="username">{{this.profile.username}}</p>
|
|
||||||
</div>
|
|
||||||
<p class="post-text">{{this.content}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,25 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>flabk - login</title>
|
|
||||||
<link rel="stylesheet" href="/static/style/main.css">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{> LoggedOut }}
|
|
||||||
<h1>login</h1>
|
|
||||||
<div id="central">
|
|
||||||
<p>login form</p>
|
|
||||||
{{> (lookup this "tag_name")}}
|
|
||||||
<form class="big" id="login" method="post" action="/login">
|
|
||||||
<input type="text" name="username">
|
|
||||||
<input type="password" name="password">
|
|
||||||
<input type="submit" value="Submit" hidden>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="/">home</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/@/{{obj.username}}">me</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/logout">log out</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="/">home</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/login">login</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/signup">sign up</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
|
@ -1,39 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>@{{obj.user.username}}</title>
|
|
||||||
<link rel="stylesheet" href="/static/style/main.css">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{> (lookup this "nav_type") }}
|
|
||||||
<div class="main">
|
|
||||||
<div class="profile">
|
|
||||||
<div class="profile-header">
|
|
||||||
<img class="profile-avatar" src="{{obj.user.avatar_uri}}" alt="{{obj.user.username}}'s avatar" />
|
|
||||||
<p class="name">{{obj.user.display_name}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="profile-bio">
|
|
||||||
<p>{{obj.user.bio}}</p>
|
|
||||||
<div class="stats">
|
|
||||||
<div id="posts">
|
|
||||||
<p class="stat-name">posts</p>
|
|
||||||
<p>{{obj.stats.post_count}}</p>
|
|
||||||
</div>
|
|
||||||
<div id="following">
|
|
||||||
<p class="stat-name">following</p>
|
|
||||||
<p>{{obj.stats.following}}</p>
|
|
||||||
</div>
|
|
||||||
<div id="followers">
|
|
||||||
<p class="stat-name">followers</p>
|
|
||||||
<p>{{obj.stats.followers}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,28 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>flabk - signup</title>
|
|
||||||
<link rel="stylesheet" href="/static/style/main.css">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{{> LoggedOut }}
|
|
||||||
<h1>signup</h1>
|
|
||||||
<div id="central">
|
|
||||||
<p>signup form</p>
|
|
||||||
{{> (lookup this "tag_name")}}
|
|
||||||
<form class="big" id="signup" method="post" action="/signup">
|
|
||||||
<input type="text" name="username" placeholder="username">
|
|
||||||
<br>
|
|
||||||
<input type="email" name="email" placeholder="email">
|
|
||||||
<br>
|
|
||||||
<input type="password" name="password" placeholder="password">
|
|
||||||
<input type="submit" value="Submit" hidden>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
module sectorinf.com/emilis/flabk
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.8.1
|
||||||
|
github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d
|
||||||
|
github.com/stretchr/testify v1.7.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.9.8 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||||
|
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,94 @@
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||||
|
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||||
|
github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d h1:Z/oRXMlZHjvjIqDma1FrIGL3iE5YL7MUI0bwYEZ6qbA=
|
||||||
|
github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||||
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||||
|
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||||
|
github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
|
||||||
|
github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||||
|
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||||
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
|
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||||
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
|
||||||
|
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo=
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -1,38 +0,0 @@
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
||||||
|
|
||||||
CREATE TABLE users (
|
|
||||||
id CHAR(22) NOT NULL PRIMARY KEY,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
host TEXT,
|
|
||||||
display_name TEXT,
|
|
||||||
email TEXT NOT NULL,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
avatar_uri TEXT,
|
|
||||||
bio TEXT,
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX u_username_host ON users (username, host);
|
|
||||||
CREATE UNIQUE INDEX u_username_local ON users (username) WHERE host IS NULL;
|
|
||||||
|
|
||||||
--id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
||||||
CREATE TABLE keys (
|
|
||||||
key TEXT NOT NULL PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE follows (
|
|
||||||
user_id CHAR(22) NOT NULL REFERENCES users(id),
|
|
||||||
follows_id CHAR(22) NOT NULL REFERENCES users(id),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
||||||
|
|
||||||
PRIMARY KEY (user_id, follows_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE follow_requests(
|
|
||||||
user_id CHAR(22) NOT NULL REFERENCES users(id),
|
|
||||||
follows_id CHAR(22) NOT NULL REFERENCES users(id),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
||||||
|
|
||||||
PRIMARY KEY (user_id, follows_id)
|
|
||||||
);
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Package coll provides generic collection types and functions
|
||||||
|
package coll
|
||||||
|
|
||||||
|
func Map[T, V any](c Vector[T], f func(T) V) Vector[V] {
|
||||||
|
out := make([]V, len(c))
|
||||||
|
for index := 0; index < len(c); index++ {
|
||||||
|
out[index] = f(c[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
return From(out...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Filter[T any](v Vector[T], f func(T) bool) Vector[T] {
|
||||||
|
out := WithCap[T](len(v))
|
||||||
|
for _, t := range v {
|
||||||
|
if f(t) {
|
||||||
|
out = out.Push(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func Take[T any](v Vector[T], howMany int) Vector[T] {
|
||||||
|
if len(v) == 0 {
|
||||||
|
return New[T]()
|
||||||
|
}
|
||||||
|
if len(v) < howMany {
|
||||||
|
howMany = len(v)
|
||||||
|
}
|
||||||
|
return From(v[:howMany-1]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Any[T any](v Vector[T], f func(T) bool) bool {
|
||||||
|
for _, t := range v {
|
||||||
|
if f(t) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnyOf[T comparable](v Vector[T], t T) bool {
|
||||||
|
for _, vT := range v {
|
||||||
|
if vT == t {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type numeric interface {
|
||||||
|
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func Min[T numeric](v Vector[T]) T {
|
||||||
|
var min T
|
||||||
|
if len(v) == 0 {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
min = v[0]
|
||||||
|
for _, t := range v {
|
||||||
|
if min > t {
|
||||||
|
min = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
func Max[T numeric](v Vector[T]) T {
|
||||||
|
var max T
|
||||||
|
for _, t := range v {
|
||||||
|
if max < t {
|
||||||
|
max = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return max
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package coll
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultSize = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type Vector[T any] []T
|
||||||
|
|
||||||
|
func New[T any]() Vector[T] {
|
||||||
|
return WithCap[T](DefaultSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCap[T any](capacity int) Vector[T] {
|
||||||
|
return make(Vector[T], 0, capacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func From[T any](v ...T) Vector[T] {
|
||||||
|
return Vector[T](v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Push(t T) Vector[T] {
|
||||||
|
if len(v) == cap(v) {
|
||||||
|
v = v.upsize()
|
||||||
|
}
|
||||||
|
|
||||||
|
v = v[:len(v)+1]
|
||||||
|
v[len(v)-1] = t
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Append(t ...T) Vector[T] {
|
||||||
|
if cap(v)-len(v) < len(t) {
|
||||||
|
v = v.upsizeSpecific(len(t))
|
||||||
|
}
|
||||||
|
startLen := len(v)
|
||||||
|
v = v[:len(v)+len(t)]
|
||||||
|
var tIndex int
|
||||||
|
for index := startLen; index < cap(v); index++ {
|
||||||
|
v[index] = t[tIndex]
|
||||||
|
tIndex++
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Remove(index int) Vector[T] {
|
||||||
|
return From(append(v[:index], v[index+1:]...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Faster remove function that can be used on vectors where ordering doesn't matter
|
||||||
|
func (v Vector[T]) RemoveUnordered(index int) Vector[T] {
|
||||||
|
v[index] = v[len(v)-1]
|
||||||
|
return v[:len(v)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) upsize() Vector[T] {
|
||||||
|
return v.upsizeSpecific(cap(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) upsizeSpecific(extraSize int) Vector[T] {
|
||||||
|
resized := make([]T, len(v), cap(v)+extraSize)
|
||||||
|
copy(resized, v)
|
||||||
|
return resized
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) panicIndex(index int) {
|
||||||
|
min := -1
|
||||||
|
if len(v) != 0 {
|
||||||
|
min = 0
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("index %d out of range [%d;%d)", index, min, len(v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSoft tries to get the requested index, or returns a default value if
|
||||||
|
// it's out of range
|
||||||
|
func (v Vector[T]) GetSoft(index int) T {
|
||||||
|
var result T
|
||||||
|
if index < 0 || index >= len(v) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return v[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Get(index int) T {
|
||||||
|
if index < 0 || index >= len(v) {
|
||||||
|
v.panicIndex(index)
|
||||||
|
}
|
||||||
|
return v[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Set(index int, value T) Vector[T] {
|
||||||
|
if index < 0 || index >= len(v) {
|
||||||
|
v.panicIndex(index)
|
||||||
|
}
|
||||||
|
v[index] = value
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Pop() (T, Vector[T]) {
|
||||||
|
t := v[len(v)-1]
|
||||||
|
return t, v[:len(v)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Sub(start, end int) Vector[T] {
|
||||||
|
if start >= len(v) {
|
||||||
|
v.panicIndex(start)
|
||||||
|
}
|
||||||
|
if end >= len(v) {
|
||||||
|
v.panicIndex(end)
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
return From(v[:end]...)
|
||||||
|
}
|
||||||
|
if end < 0 {
|
||||||
|
return From(v[start:]...)
|
||||||
|
}
|
||||||
|
return From(v[start:end]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Filter(f func(T) bool) Vector[T] {
|
||||||
|
return Filter(v, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Any(f func(T) bool) bool {
|
||||||
|
return Any(v, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Take(howMany int) Vector[T] {
|
||||||
|
return Take(v, howMany)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector[T]) Clone() Vector[T] {
|
||||||
|
clone := make([]T, len(v))
|
||||||
|
copy(clone, v)
|
||||||
|
v = clone
|
||||||
|
return v
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
package coll_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/coll"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVector(t *testing.T) {
|
||||||
|
tests := map[string]func(that *require.Assertions){
|
||||||
|
"push": func(that *require.Assertions) {
|
||||||
|
v := coll.New[int]()
|
||||||
|
v = v.Push(0).Push(1).Push(2).Push(3).Push(4).Push(5)
|
||||||
|
that.Len(v, 6)
|
||||||
|
that.EqualValues([]int{0, 1, 2, 3, 4, 5}, v)
|
||||||
|
},
|
||||||
|
"len": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3})
|
||||||
|
that.Len(v, len(v))
|
||||||
|
},
|
||||||
|
"from": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3})
|
||||||
|
that.Len(v, 3)
|
||||||
|
},
|
||||||
|
"pop": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3})
|
||||||
|
t, v := v.Pop()
|
||||||
|
that.Equal(3, t)
|
||||||
|
that.EqualValues([]int{1, 2}, v)
|
||||||
|
},
|
||||||
|
"withcap": func(that *require.Assertions) {
|
||||||
|
v := coll.WithCap[int](3)
|
||||||
|
that.Zero(len(v))
|
||||||
|
that.Len(v, 0)
|
||||||
|
},
|
||||||
|
"remove": func(that *require.Assertions) {
|
||||||
|
v := coll.New[int]().Push(0).Push(1).Push(2)
|
||||||
|
r1 := v.Remove(1)
|
||||||
|
that.Equal(2, len(r1))
|
||||||
|
that.EqualValues([]int{0, 2}, r1)
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Remove(-1)
|
||||||
|
})
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Remove(100000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"remove unordered": func(that *require.Assertions) {
|
||||||
|
v := coll.New[int]().Push(0).Push(1).Push(2)
|
||||||
|
v = v.RemoveUnordered(1)
|
||||||
|
that.Equal(2, len(v))
|
||||||
|
vSlice := v
|
||||||
|
if vSlice[0] == 0 {
|
||||||
|
that.Equal(2, vSlice[1])
|
||||||
|
} else {
|
||||||
|
that.Equal(2, vSlice[0])
|
||||||
|
that.Zero(vSlice[1])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"append": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3}...).Append(4, 5, 6)
|
||||||
|
that.Equal(6, len(v))
|
||||||
|
that.EqualValues([]int{1, 2, 3, 4, 5, 6}, v)
|
||||||
|
},
|
||||||
|
"getsoft": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3}...)
|
||||||
|
that.Equal(2, v.GetSoft(1))
|
||||||
|
that.Zero(v.GetSoft(-1000))
|
||||||
|
that.Zero(v.GetSoft(1000))
|
||||||
|
},
|
||||||
|
"get": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3}...)
|
||||||
|
that.Equal(2, v.Get(1))
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Get(-1000)
|
||||||
|
})
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Get(1000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"clone": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3}...)
|
||||||
|
|
||||||
|
v1 := v.Clone().Set(1, 1)
|
||||||
|
that.Equal(3, len(v1))
|
||||||
|
that.EqualValues([]int{1, 1, 3}, v1)
|
||||||
|
that.EqualValues([]int{1, 2, 3}, v)
|
||||||
|
},
|
||||||
|
"set": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3}...)
|
||||||
|
|
||||||
|
v1 := v.Clone().Set(1, 1)
|
||||||
|
that.Equal(3, len(v1))
|
||||||
|
that.EqualValues([]int{1, 1, 3}, v1)
|
||||||
|
|
||||||
|
v2 := v.Clone().Set(2, 1)
|
||||||
|
that.Equal(3, len(v2))
|
||||||
|
that.EqualValues([]int{1, 2, 1}, v2)
|
||||||
|
|
||||||
|
v3 := v.Clone().Set(0, 16)
|
||||||
|
that.Equal(3, len(v3))
|
||||||
|
that.EqualValues([]int{16, 2, 3}, v3)
|
||||||
|
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Set(-1000, 1)
|
||||||
|
})
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Set(3, 1)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"sub": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{0, 1, 2, 3, 4, 5, 6}...)
|
||||||
|
|
||||||
|
v1 := v.Clone().Sub(2, 4)
|
||||||
|
that.Equal(2, len(v1))
|
||||||
|
that.EqualValues([]int{2, 3}, v1)
|
||||||
|
|
||||||
|
v2 := v.Clone().Sub(2, 5)
|
||||||
|
that.Equal(3, len(v2))
|
||||||
|
that.EqualValues([]int{2, 3, 4}, v2)
|
||||||
|
|
||||||
|
v3 := v.Clone().Sub(5, -1)
|
||||||
|
that.Equal(2, len(v3))
|
||||||
|
that.EqualValues([]int{5, 6}, v3)
|
||||||
|
|
||||||
|
v4 := v.Clone().Sub(-1, 2)
|
||||||
|
that.Equal(2, len(v4))
|
||||||
|
that.EqualValues([]int{0, 1}, v4)
|
||||||
|
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Sub(0, 1000)
|
||||||
|
})
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Sub(4000, 4002)
|
||||||
|
})
|
||||||
|
that.Panics(func() {
|
||||||
|
v.Sub(2, 1)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"filter": func(that *require.Assertions) {
|
||||||
|
v := coll.From([]int{1, 2, 3, 4, 5, 6}...)
|
||||||
|
v = v.Filter(func(i int) bool { return i%2 == 0 })
|
||||||
|
that.Len(v, 3)
|
||||||
|
},
|
||||||
|
"any": func(that *require.Assertions) {
|
||||||
|
that.True(coll.From([]int{1, 2, 3, 4, 5, 6}...).Any(func(i int) bool { return i == 3 }))
|
||||||
|
that.False(coll.From([]int{1, 2, 3, 4, 5, 6}...).Any(func(i int) bool { return i == 666 }))
|
||||||
|
},
|
||||||
|
"take": func(that *require.Assertions) {
|
||||||
|
that.EqualValues([]int{1, 2, 3}, coll.From([]int{1, 2, 3, 4, 5, 6}).Take(3))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(tt *testing.T) {
|
||||||
|
test(require.New(tt))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package epk
|
||||||
|
|
||||||
|
import "github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
type ErrorTest func(*require.Assertions, error)
|
||||||
|
|
||||||
|
var (
|
||||||
|
NoError ErrorTest = func(a *require.Assertions, err error) {
|
||||||
|
a.NoError(err)
|
||||||
|
}
|
||||||
|
Error ErrorTest = func(a *require.Assertions, err error) {
|
||||||
|
a.Error(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ErrorIs(target error, contains ...string) ErrorTest {
|
||||||
|
return func(a *require.Assertions, err error) {
|
||||||
|
a.Error(err)
|
||||||
|
a.ErrorIs(err, target)
|
||||||
|
for _, elem := range contains {
|
||||||
|
a.Contains(err.Error(), elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotErrorIs(target error, contains ...string) ErrorTest {
|
||||||
|
return func(a *require.Assertions, err error) {
|
||||||
|
a.Error(err)
|
||||||
|
a.NotErrorIs(err, target)
|
||||||
|
for _, elem := range contains {
|
||||||
|
a.Contains(err.Error(), elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package consts
|
||||||
|
|
||||||
|
const (
|
||||||
|
PkgTag = "ld"
|
||||||
|
)
|
|
@ -0,0 +1,254 @@
|
||||||
|
package chunk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/coll"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
escapeChar = '\\'
|
||||||
|
unicodeEscape = 'u'
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoMatch = errors.New("no matching")
|
||||||
|
ErrMapMemberNotString = errors.New("map member not a string")
|
||||||
|
ErrUnexpectedSymbol = errors.New("unexpected symbol")
|
||||||
|
ErrMissingValue = errors.New("missing value")
|
||||||
|
ErrEscapeAtEnd = errors.New("escape character at the end of value")
|
||||||
|
ErrIncompleteEscape = errors.New("incomplete unicode escape sequence")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Chunk struct {
|
||||||
|
vector coll.Vector[byte]
|
||||||
|
posLeft int
|
||||||
|
posRight int
|
||||||
|
// todo global pos impl
|
||||||
|
globLeft int
|
||||||
|
globRight int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) LeftByte() byte {
|
||||||
|
return c.vector[c.posLeft]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) Null() bool {
|
||||||
|
return (c.posRight+1)-c.posLeft >= 4 && c.vector[c.posLeft] == 'n' && c.vector[c.posLeft+1] == 'u' && c.vector[c.posLeft+2] == 'l' && c.vector[c.posLeft+3] == 'l'
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) LeftPosf(format string, args ...any) error {
|
||||||
|
return fmt.Errorf(fmt.Sprintf("[%d] %s", c.globLeft, format), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match finds the matching closer to the symbol at the left position and sets the
|
||||||
|
// right position to this index
|
||||||
|
func (c Chunk) Match() (Chunk, error) {
|
||||||
|
start := c.vector[c.posLeft]
|
||||||
|
matcher, ok := matchers[start]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("Match called on %c with no matcher defined", c.vector[c.posLeft]))
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := c.posLeft + 1; index < len(c.vector); index++ {
|
||||||
|
if start != '"' && c.vector[index] == start {
|
||||||
|
sub, err := c.Child(index, len(c.vector)).Match()
|
||||||
|
if err != nil {
|
||||||
|
return c, fmt.Errorf("[%d] child %w", c.globLeft+(c.posLeft-index), err)
|
||||||
|
}
|
||||||
|
index += sub.posRight
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.vector[index] == escapeChar {
|
||||||
|
if index+1 == len(c.vector) {
|
||||||
|
return c, fmt.Errorf("[%d] %w", c.globLeft+(c.posLeft-index), ErrEscapeAtEnd)
|
||||||
|
}
|
||||||
|
if c.vector[index+1] == unicodeEscape {
|
||||||
|
if index+6 >= len(c.vector) {
|
||||||
|
return c, fmt.Errorf("[%d] %w", c.globLeft+(c.posLeft+index), ErrIncompleteEscape)
|
||||||
|
}
|
||||||
|
index += 5
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.vector[index] == matcher {
|
||||||
|
c.globRight -= c.posRight - index
|
||||||
|
c.posRight = index
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, c.LeftPosf("%w %c", ErrNoMatch, c.vector[c.posLeft])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) Copy() Chunk {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub returns an inclusive subchunk of [left;right]
|
||||||
|
func (c Chunk) Sub() Chunk {
|
||||||
|
c = c.Child(c.posLeft, c.posRight+1)
|
||||||
|
// To be inclusive we incremented posRight above, so
|
||||||
|
// to restore the original position, we must decrement
|
||||||
|
// it here before returning. Only if we have room to decrement it
|
||||||
|
if c.posRight > 0 {
|
||||||
|
c.posRight--
|
||||||
|
c.globRight--
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieCutter returns a subchunk of (left;right)
|
||||||
|
func (c Chunk) CookieCutter() Chunk {
|
||||||
|
return c.Child(c.posLeft+1, c.posRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func spaceAtIndex(v coll.Vector[byte], index int) bool {
|
||||||
|
return v[index] == 0x9 || v[index] == 0xa || v[index] == 0xd || v[index] == 0x20
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) AtSpace() bool {
|
||||||
|
return spaceAtIndex(c.vector, c.posLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) Seek() Chunk {
|
||||||
|
for c.posLeft < len(c.vector) && c.AtSpace() {
|
||||||
|
c.globLeft++
|
||||||
|
c.posLeft++
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step increments the left position by 1 if b
|
||||||
|
// is the byte at the current left position
|
||||||
|
func (c Chunk) StepIf(b byte) Chunk {
|
||||||
|
if c.vector[c.posLeft] != b {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
c.posLeft++
|
||||||
|
c.globLeft++
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) ValueEnd() (Chunk, error) {
|
||||||
|
switch c.vector[c.posLeft] {
|
||||||
|
case '"', '{', '[':
|
||||||
|
return c.Match()
|
||||||
|
default:
|
||||||
|
for index := c.posLeft; index <= c.posRight; index++ {
|
||||||
|
if c.vector[index] == ',' || spaceAtIndex(c.vector, index) {
|
||||||
|
if index == c.posLeft {
|
||||||
|
return c, c.LeftPosf("%w", ErrMissingValue)
|
||||||
|
}
|
||||||
|
return c.Child(c.posLeft, index), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip skips the current left position and then Seeks
|
||||||
|
func (c Chunk) Skip() Chunk {
|
||||||
|
if c.posLeft+1 < len(c.vector) {
|
||||||
|
c.posLeft++
|
||||||
|
c.globLeft++
|
||||||
|
}
|
||||||
|
return c.Seek()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchRule struct {
|
||||||
|
MatchByte byte
|
||||||
|
StartFromRight bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
matchers = map[byte]byte{
|
||||||
|
'{': '}',
|
||||||
|
'[': ']',
|
||||||
|
'"': '"',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(v []byte) Chunk {
|
||||||
|
posRight := len(v) - 1
|
||||||
|
if len(v) == 0 {
|
||||||
|
posRight = 0
|
||||||
|
}
|
||||||
|
return Chunk{
|
||||||
|
vector: coll.Vector[byte](v),
|
||||||
|
posLeft: 0,
|
||||||
|
posRight: posRight,
|
||||||
|
globRight: posRight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) Child(left, right int) Chunk {
|
||||||
|
sub := c.vector[left:right]
|
||||||
|
return Chunk{
|
||||||
|
vector: sub,
|
||||||
|
posLeft: 0,
|
||||||
|
posRight: len(sub) - 1,
|
||||||
|
globLeft: (c.globLeft - c.posLeft) + left,
|
||||||
|
globRight: (c.globRight - c.posRight) + (right - 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseFunc[T any] func(T) (T, error)
|
||||||
|
|
||||||
|
type Row struct {
|
||||||
|
Name string
|
||||||
|
Value Chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) String() string {
|
||||||
|
return string(c.vector[c.posLeft : c.posRight+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) Row() (Row, error) {
|
||||||
|
if c.vector[c.posLeft] != '"' {
|
||||||
|
return Row{}, c.LeftPosf("%w: %c", ErrMapMemberNotString, c.vector[c.posLeft])
|
||||||
|
}
|
||||||
|
name, err := c.Match()
|
||||||
|
if err != nil {
|
||||||
|
return Row{}, c.LeftPosf("match: %w", err)
|
||||||
|
}
|
||||||
|
postName := c.Copy()
|
||||||
|
postName.posLeft = name.posRight
|
||||||
|
postName.globLeft = name.globRight
|
||||||
|
postName = postName.Skip()
|
||||||
|
// Next we must get a :
|
||||||
|
if postName.vector[postName.posLeft] != ':' {
|
||||||
|
return Row{}, postName.LeftPosf("%w '%c', expected ':'", ErrUnexpectedSymbol, postName.vector[postName.posLeft])
|
||||||
|
}
|
||||||
|
value, err := postName.Skip().ValueEnd()
|
||||||
|
if err != nil {
|
||||||
|
return Row{}, postName.LeftPosf("value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row{
|
||||||
|
Name: name.String()[1 : name.posRight-name.posLeft],
|
||||||
|
Value: value,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// After returns the chunk with its left position, if possible,
|
||||||
|
// right after the global right position of v
|
||||||
|
func (c Chunk) After(v Chunk) Chunk {
|
||||||
|
// Add two, as one is for dealing with right side being exclusive
|
||||||
|
// in slice indexes, and another one to go on to the next
|
||||||
|
offset := (v.globRight - c.globLeft) + 1
|
||||||
|
// Then, make sure we don't go too far
|
||||||
|
if c.posLeft+offset >= len(c.vector) {
|
||||||
|
offset--
|
||||||
|
}
|
||||||
|
c.posLeft += offset
|
||||||
|
c.globLeft += offset
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chunk) EOF() bool {
|
||||||
|
return c.posLeft >= len(c.vector)-1
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package chunk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/coll"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSpace(t *testing.T) {
|
||||||
|
tests := []byte{0x9, 0xa, 0xd, 0x20}
|
||||||
|
|
||||||
|
that := require.New(t)
|
||||||
|
for _, test := range tests {
|
||||||
|
b := []byte("xhello world")
|
||||||
|
b[0] = test
|
||||||
|
that.True(New(b).AtSpace())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlign(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
c func() Chunk
|
||||||
|
v func(Chunk) Chunk
|
||||||
|
}{
|
||||||
|
"same-size": {
|
||||||
|
c: func() Chunk {
|
||||||
|
c := New(coll.Vector[byte](`{"heres some test data": "hi mom"}`))
|
||||||
|
c.posLeft = 15
|
||||||
|
c.globLeft = 15
|
||||||
|
return c
|
||||||
|
},
|
||||||
|
v: func(c Chunk) Chunk {
|
||||||
|
c.posLeft = 20
|
||||||
|
c.globLeft = 20
|
||||||
|
c.globRight = 22
|
||||||
|
c.posRight = 22
|
||||||
|
return c
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"cookie-cutter": {
|
||||||
|
c: func() Chunk {
|
||||||
|
c := New(coll.Vector[byte](`{"heres some test data": "hi mom"}`))
|
||||||
|
c.posLeft = 15
|
||||||
|
c.globLeft = 15
|
||||||
|
return c
|
||||||
|
},
|
||||||
|
v: func(c Chunk) Chunk {
|
||||||
|
return c.CookieCutter().CookieCutter()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"no-change": {
|
||||||
|
c: func() Chunk {
|
||||||
|
c := New(coll.Vector[byte](`{"heres some test data": "hi mom"}`))
|
||||||
|
c.posLeft = 15
|
||||||
|
c.globLeft = 15
|
||||||
|
return c
|
||||||
|
},
|
||||||
|
v: func(c Chunk) Chunk {
|
||||||
|
c.posRight = 15
|
||||||
|
c.globRight = 15
|
||||||
|
return c.Sub()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"contains": {
|
||||||
|
c: func() Chunk {
|
||||||
|
return New(coll.Vector[byte](`0123456789`))
|
||||||
|
},
|
||||||
|
v: func(c Chunk) Chunk {
|
||||||
|
return c.Child(5, 8)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(name, func(tt *testing.T) {
|
||||||
|
that := require.New(tt)
|
||||||
|
c := test.c()
|
||||||
|
v := test.v(c)
|
||||||
|
out := c.After(v)
|
||||||
|
that.Equal(v.globRight+1, out.globLeft)
|
||||||
|
that.Equal(string(c.vector[v.globRight+1]), string(out.LeftByte()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRow(t *testing.T) {
|
||||||
|
c := New(coll.Vector[byte](`{
|
||||||
|
"heres some test data": "hi mom",
|
||||||
|
"another field": "get it right",
|
||||||
|
"integer": -12345
|
||||||
|
}`))
|
||||||
|
that := require.New(t)
|
||||||
|
row, err := c.CookieCutter().Seek().Row()
|
||||||
|
that.NoError(err)
|
||||||
|
that.Equal("heres some test data", row.Name)
|
||||||
|
that.Equal(`"hi mom"`, row.Value.String())
|
||||||
|
row, err = c.After(row.Value).StepIf(',').Seek().Row()
|
||||||
|
that.NoError(err)
|
||||||
|
that.Equal("another field", row.Name)
|
||||||
|
that.Equal(`"get it right"`, row.Value.String())
|
||||||
|
row, err = c.After(row.Value).StepIf(',').Seek().Row()
|
||||||
|
that.NoError(err)
|
||||||
|
that.Equal("integer", row.Name)
|
||||||
|
that.Equal("-12345", row.Value.String())
|
||||||
|
that.True(c.After(row.Value).Seek().EOF())
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/coll"
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/ld/internal/consts"
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/ld/internal/parse/chunk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
memberSeparator = ','
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotArray = errors.New("value is not an array")
|
||||||
|
ErrNotMap = errors.New("value is not a map")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
boolVals = map[string]bool{
|
||||||
|
"true": true,
|
||||||
|
"false": false,
|
||||||
|
}
|
||||||
|
determineMap = func() map[byte]coll.Vector[reflect.Kind] {
|
||||||
|
m := map[byte]coll.Vector[reflect.Kind]{
|
||||||
|
'"': coll.From(reflect.String),
|
||||||
|
'[': coll.From(reflect.Array, reflect.Slice),
|
||||||
|
'{': coll.From(reflect.Map, reflect.Struct),
|
||||||
|
'-': coll.From(reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64),
|
||||||
|
't': coll.From(reflect.Bool),
|
||||||
|
'f': coll.From(reflect.Bool),
|
||||||
|
}
|
||||||
|
numbers := coll.From(
|
||||||
|
reflect.Int,
|
||||||
|
reflect.Int8,
|
||||||
|
reflect.Int16,
|
||||||
|
reflect.Int32,
|
||||||
|
reflect.Int64,
|
||||||
|
reflect.Uint,
|
||||||
|
reflect.Uint64,
|
||||||
|
reflect.Uint32,
|
||||||
|
reflect.Uint16,
|
||||||
|
reflect.Uint8,
|
||||||
|
reflect.Float32,
|
||||||
|
reflect.Float64,
|
||||||
|
)
|
||||||
|
for index := byte('0'); index <= '9'; index++ {
|
||||||
|
m[index] = numbers
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}()
|
||||||
|
sizes = map[reflect.Kind]int{
|
||||||
|
// Int and Uint values stolen from the [math] package, as the intSize
|
||||||
|
// constant is unexported
|
||||||
|
reflect.Int: 32 << (^uint(0) >> 63), // 32 or 64
|
||||||
|
reflect.Uint: 32 << (^uint(0) >> 63), // 32 or 64
|
||||||
|
reflect.Int64: 64,
|
||||||
|
reflect.Int32: 32,
|
||||||
|
reflect.Int16: 16,
|
||||||
|
reflect.Int8: 8,
|
||||||
|
reflect.Uint64: 64,
|
||||||
|
reflect.Uint32: 32,
|
||||||
|
reflect.Uint16: 16,
|
||||||
|
reflect.Uint8: 8,
|
||||||
|
reflect.Float32: 32,
|
||||||
|
reflect.Float64: 64,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetMap(val reflect.Value, typ reflect.Type, c chunk.Chunk) error {
|
||||||
|
if c.LeftByte() != '{' {
|
||||||
|
return c.LeftPosf("%w", ErrNotMap)
|
||||||
|
}
|
||||||
|
fields := GetStructFields(val, typ)
|
||||||
|
c = c.CookieCutter().Seek()
|
||||||
|
for !c.EOF() {
|
||||||
|
row, err := c.Row()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("row: %w", err)
|
||||||
|
}
|
||||||
|
field, ok := fields[row.Name]
|
||||||
|
if ok {
|
||||||
|
if err := SetValue(field, field.Type(), row.Value); err != nil {
|
||||||
|
return fmt.Errorf("set: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift c
|
||||||
|
c = c.After(row.Value).StepIf(memberSeparator).Seek()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetArray(val reflect.Value, typ reflect.Type, c chunk.Chunk) error {
|
||||||
|
if c.LeftByte() != '[' {
|
||||||
|
return c.LeftPosf("%w", ErrNotArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
c = c.CookieCutter().Seek()
|
||||||
|
elems := coll.New[chunk.Chunk]()
|
||||||
|
for !c.EOF() {
|
||||||
|
element, err := c.ValueEnd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting array elem: %w", err)
|
||||||
|
}
|
||||||
|
elems = elems.Push(element)
|
||||||
|
c = c.After(element).StepIf(memberSeparator).Seek()
|
||||||
|
}
|
||||||
|
elementType := typ.Elem()
|
||||||
|
arrayType := reflect.ArrayOf(len(elems), elementType)
|
||||||
|
array := reflect.New(arrayType).Elem()
|
||||||
|
for index, elem := range elems {
|
||||||
|
if err := SetValue(array.Index(index), elementType, elem); err != nil {
|
||||||
|
return fmt.Errorf("%w at array index [%d]", err, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val.Kind() == reflect.Slice {
|
||||||
|
array = array.Slice(0, len(elems))
|
||||||
|
}
|
||||||
|
val.Set(array)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines what [reflect.Kind] of object the given [chunk.Chunk] is
|
||||||
|
func Determine(c chunk.Chunk) coll.Vector[reflect.Kind] {
|
||||||
|
k, ok := determineMap[c.LeftByte()]
|
||||||
|
if !ok {
|
||||||
|
return coll.From(reflect.Invalid)
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetValue(val reflect.Value, typ reflect.Type, c chunk.Chunk) error {
|
||||||
|
if c.Null() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val.Kind() == reflect.Pointer {
|
||||||
|
if val.IsNil() {
|
||||||
|
val.Set(reflect.New(typ.Elem()))
|
||||||
|
}
|
||||||
|
return SetValue(val.Elem(), typ.Elem(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds := Determine(c)
|
||||||
|
if !coll.AnyOf(kinds, val.Kind()) {
|
||||||
|
return fmt.Errorf("kind %s not found in appropriate list %s for value [%s]", val.Kind(), kinds, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Map, reflect.Struct:
|
||||||
|
return SetMap(val, typ, c)
|
||||||
|
case reflect.String:
|
||||||
|
val.SetString(Escape(c.CookieCutter()))
|
||||||
|
case reflect.Bool:
|
||||||
|
b, ok := boolVals[c.String()]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("value [%s] is not a boolean", c.String())
|
||||||
|
}
|
||||||
|
val.SetBool(b)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
i, err := strconv.ParseInt(c.String(), 10, sizes[val.Kind()])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("value [%s] is not an %s", c.String(), val.Kind().String())
|
||||||
|
}
|
||||||
|
val.SetInt(i)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
u, err := strconv.ParseUint(c.String(), 10, sizes[val.Kind()])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("value [%s] is not an %s", c.String(), val.Kind().String())
|
||||||
|
}
|
||||||
|
val.SetUint(u)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
f, err := strconv.ParseFloat(c.String(), sizes[val.Kind()])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("value [%s] is not an %s", c.String(), val.Kind().String())
|
||||||
|
}
|
||||||
|
val.SetFloat(f)
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
return SetArray(val, typ, c)
|
||||||
|
default:
|
||||||
|
fmt.Println(val.Kind().String())
|
||||||
|
panic("unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Escape(v chunk.Chunk) string {
|
||||||
|
// TODO: string escaping
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStructFields(val reflect.Value, typ reflect.Type) map[string]reflect.Value {
|
||||||
|
total := val.NumField()
|
||||||
|
out := map[string]reflect.Value{}
|
||||||
|
for index := 0; index < total; index++ {
|
||||||
|
cName := StructName(typ.Field(index))
|
||||||
|
if cName != "-" {
|
||||||
|
out[cName] = val.Field(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func StructName(v reflect.StructField) string {
|
||||||
|
tag := v.Tag.Get(consts.PkgTag)
|
||||||
|
if tag != "" {
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
// Default to field name
|
||||||
|
return v.Name
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// // Package asld handles JSON-LD for asflab
|
||||||
|
// //
|
||||||
|
// // This will not go well
|
||||||
|
package ld
|
||||||
|
|
||||||
|
const (
|
||||||
|
pkgTag = "ld"
|
||||||
|
)
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/ld/internal/parse"
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/ld/internal/parse/chunk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Unmarshal[T any](v []byte) (T, error) {
|
||||||
|
ptr := new(T)
|
||||||
|
if err := parse.SetValue(reflect.ValueOf(ptr), reflect.TypeOf(ptr), chunk.New(v)); err != nil {
|
||||||
|
return *ptr, fmt.Errorf("unmarshal: %w", err)
|
||||||
|
}
|
||||||
|
return *ptr, nil
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
package ld_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/epk"
|
||||||
|
"sectorinf.com/emilis/flabk/pkg/ld"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testObj struct {
|
||||||
|
String string `ld:"string" json:"string"`
|
||||||
|
NoTag string
|
||||||
|
Int int
|
||||||
|
Int8 int8
|
||||||
|
Int16 int16
|
||||||
|
Int32 int32
|
||||||
|
Int64 int64
|
||||||
|
Uint uint
|
||||||
|
Uint8 uint8
|
||||||
|
Uint16 uint16
|
||||||
|
Uint32 uint32
|
||||||
|
Uint64 uint64
|
||||||
|
Float32 float32
|
||||||
|
Float64 float64
|
||||||
|
// Complex64 complex64
|
||||||
|
// Complex128 complex128
|
||||||
|
IntPtr *int
|
||||||
|
Int8Ptr *int8
|
||||||
|
Int16Ptr *int16
|
||||||
|
Int32Ptr *int32
|
||||||
|
Int64Ptr *int64
|
||||||
|
UintPtr *uint
|
||||||
|
Uint8Ptr *uint8
|
||||||
|
Uint16Ptr *uint16
|
||||||
|
Uint32Ptr *uint32
|
||||||
|
Uint64Ptr *uint64
|
||||||
|
Float32Ptr *float32
|
||||||
|
Float64Ptr *float64
|
||||||
|
// Complex64Ptr *complex64
|
||||||
|
// Complex128Ptr *complex128
|
||||||
|
TestPtr *testObj
|
||||||
|
TestArray []testObj
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshal(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
obj testObj
|
||||||
|
errCheck epk.ErrorTest
|
||||||
|
}{
|
||||||
|
"string children": {
|
||||||
|
obj: testObj{
|
||||||
|
String: "hello",
|
||||||
|
NoTag: "no_tag",
|
||||||
|
Int: math.MaxInt,
|
||||||
|
Int8: math.MaxInt8,
|
||||||
|
Int16: math.MaxInt16,
|
||||||
|
Int32: math.MaxInt32,
|
||||||
|
Int64: math.MaxInt64,
|
||||||
|
Uint: math.MaxUint,
|
||||||
|
Uint8: math.MaxUint8,
|
||||||
|
Uint16: math.MaxUint16,
|
||||||
|
Uint32: math.MaxUint32,
|
||||||
|
Uint64: math.MaxUint64,
|
||||||
|
Float32: math.MaxFloat32,
|
||||||
|
Float64: math.MaxFloat64,
|
||||||
|
TestPtr: &testObj{
|
||||||
|
String: "hello2",
|
||||||
|
},
|
||||||
|
TestArray: []testObj{
|
||||||
|
{
|
||||||
|
String: "hello3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
String: "hello4",
|
||||||
|
TestPtr: &testObj{
|
||||||
|
TestArray: []testObj{
|
||||||
|
{
|
||||||
|
String: "hello5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Complex64: complex(math.MaxFloat32, math.MaxFloat32),
|
||||||
|
// Complex128: complex(math.MaxFloat64, math.MaxFloat64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(name, func(tt *testing.T) {
|
||||||
|
that := require.New(tt)
|
||||||
|
objJSON, err := json.MarshalIndent(test.obj, "", " ")
|
||||||
|
that.NoError(err)
|
||||||
|
result, err := ld.Unmarshal[testObj](objJSON)
|
||||||
|
that.NoError(err)
|
||||||
|
that.Equal(test.obj, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBench(t *testing.T) {
|
||||||
|
obj := testObj{
|
||||||
|
String: "hello",
|
||||||
|
NoTag: "no_tag",
|
||||||
|
Int: math.MaxInt,
|
||||||
|
Int8: math.MaxInt8,
|
||||||
|
Int16: math.MaxInt16,
|
||||||
|
Int32: math.MaxInt32,
|
||||||
|
Int64: math.MaxInt64,
|
||||||
|
Uint: math.MaxUint,
|
||||||
|
Uint8: math.MaxUint8,
|
||||||
|
Uint16: math.MaxUint16,
|
||||||
|
Uint32: math.MaxUint32,
|
||||||
|
Uint64: math.MaxUint64,
|
||||||
|
Float32: math.MaxFloat32,
|
||||||
|
Float64: math.MaxFloat64,
|
||||||
|
// Complex64: complex(math.MaxFloat32, math.MaxFloat32),
|
||||||
|
// Complex128: complex(math.MaxFloat64, math.MaxFloat64),
|
||||||
|
}
|
||||||
|
that := require.New(t)
|
||||||
|
asldTotal := int64(0)
|
||||||
|
jsonTotal := int64(0)
|
||||||
|
|
||||||
|
jsonMax := int64(0)
|
||||||
|
jsonMin := math.MaxInt64
|
||||||
|
|
||||||
|
asldMax := int64(0)
|
||||||
|
asldMin := math.MaxInt64
|
||||||
|
|
||||||
|
count := int64(1 << 20)
|
||||||
|
for index := int64(0); index < count; index++ {
|
||||||
|
objJSON, err := json.Marshal(obj)
|
||||||
|
that.NoError(err)
|
||||||
|
|
||||||
|
asldStart := time.Now()
|
||||||
|
_, err = ld.Unmarshal[testObj](objJSON)
|
||||||
|
asldDur := time.Since(asldStart)
|
||||||
|
asldTotal += int64(asldDur)
|
||||||
|
if asldDur < time.Duration(asldMin) {
|
||||||
|
asldMin = int(asldDur)
|
||||||
|
}
|
||||||
|
if asldDur > time.Duration(asldMax) {
|
||||||
|
asldMax = int64(asldDur)
|
||||||
|
}
|
||||||
|
|
||||||
|
that.NoError(err)
|
||||||
|
a := testObj{}
|
||||||
|
|
||||||
|
jsonStart := time.Now()
|
||||||
|
err = json.Unmarshal(objJSON, &a)
|
||||||
|
jsonDur := time.Since(jsonStart)
|
||||||
|
jsonTotal += int64(jsonDur)
|
||||||
|
|
||||||
|
if jsonDur < time.Duration(jsonMin) {
|
||||||
|
jsonMin = int(jsonDur)
|
||||||
|
}
|
||||||
|
if jsonDur > time.Duration(jsonMax) {
|
||||||
|
jsonMax = int64(jsonDur)
|
||||||
|
}
|
||||||
|
|
||||||
|
that.NoError(err)
|
||||||
|
}
|
||||||
|
fmt.Println(count, "runs")
|
||||||
|
fmt.Printf("json avg (%s), min (%s), max (%s)\n", time.Duration(jsonTotal/count), time.Duration(jsonMin), time.Duration(jsonMax))
|
||||||
|
fmt.Printf("asld avg (%s), min (%s), max (%s)\n", time.Duration(asldTotal/count), time.Duration(asldMin), time.Duration(asldMax))
|
||||||
|
}
|
||||||
|
func jsonElemList[T any](that *require.Assertions, a any) [][]byte {
|
||||||
|
that.NotNil(a)
|
||||||
|
v, ok := a.([]T)
|
||||||
|
that.True(ok)
|
||||||
|
fields := make([][]byte, len(v))
|
||||||
|
for index, field := range v {
|
||||||
|
jsonField, err := json.Marshal(field)
|
||||||
|
that.NoError(err)
|
||||||
|
fields[index] = jsonField
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
type hello struct {
|
||||||
|
Hello string
|
||||||
|
}
|
||||||
|
|
||||||
|
// func TestArrayMembers(t *testing.T) {
|
||||||
|
// tests := map[string]struct {
|
||||||
|
// object any
|
||||||
|
// errCheck epk.ErrorTest
|
||||||
|
// toJsonElementList func(*require.Assertions, any) [][]byte
|
||||||
|
// }{
|
||||||
|
// "strings": {
|
||||||
|
// object: []string{"a", "b", "c", "d"},
|
||||||
|
// toJsonElementList: jsonElemList[string],
|
||||||
|
// },
|
||||||
|
// "ints": {
|
||||||
|
// object: []int{1, 2, 3, 4, 5, 6},
|
||||||
|
// toJsonElementList: jsonElemList[int],
|
||||||
|
// },
|
||||||
|
// "floats": {
|
||||||
|
// object: []float64{123.456, 789.0123, 456.789},
|
||||||
|
// toJsonElementList: jsonElemList[float64],
|
||||||
|
// },
|
||||||
|
// "strings with commas": {
|
||||||
|
// object: []string{"this, is what I do", "what, don't like it?"},
|
||||||
|
// toJsonElementList: jsonElemList[string],
|
||||||
|
// },
|
||||||
|
// "objects": {
|
||||||
|
// object: []hello{{"world"}, {"mom, dad"}},
|
||||||
|
// toJsonElementList: jsonElemList[hello],
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for name, test := range tests {
|
||||||
|
// test := test
|
||||||
|
// t.Run(name, func(tt *testing.T) {
|
||||||
|
// that := require.New(tt)
|
||||||
|
// objJson, err := json.Marshal(test.object)
|
||||||
|
// that.NoError(err)
|
||||||
|
// w := newWalker(objJson)
|
||||||
|
// members, err := arrayMembers(w)
|
||||||
|
// if test.errCheck == nil {
|
||||||
|
// test.errCheck = epk.NoError
|
||||||
|
// }
|
||||||
|
// test.errCheck(that, err)
|
||||||
|
// expected := test.toJsonElementList(that, test.object)
|
||||||
|
// that.Len(members, len(expected))
|
||||||
|
// for index, elem := range expected {
|
||||||
|
// equivMember := members[index]
|
||||||
|
// that.Equal(elem, equivMember.content)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -1,2 +0,0 @@
|
||||||
[toolchain]
|
|
||||||
channel = "nightly"
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package uriutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func JoinURIs(base string, parts ...string) string {
|
||||||
|
if len(base) < 8 || len(base) > 8 && base[:8] != "https://" {
|
||||||
|
base = "https://" + base
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.Join(parts, "/"))
|
||||||
|
}
|
Loading…
Reference in New Issue