Compare commits

..

No commits in common. "master" and "wip/chunker" have entirely different histories.

53 changed files with 1580 additions and 4771 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
/target

1987
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
[workspace]
members = [
"flabk",
"flabk-derive",
]

View File

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

View File

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

46
flabk.go Normal file
View File

@ -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'"
// }

View File

@ -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}"
// }
]
}

View File

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

36
flabk/ap/object.go Normal file
View File

@ -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"`
}

52
flabk/ap/object_test.go Normal file
View File

@ -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)
}

View File

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

Before

Width:  |  Height:  |  Size: 4.5 KiB

46
flabk/flabweb/flabweb.go Normal file
View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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)
}
}

View File

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